373 lines
10 KiB
Ruby
373 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module GoogleCodeImport
|
|
class Importer
|
|
attr_reader :project, :repo, :closed_statuses
|
|
|
|
NICE_LABEL_COLOR_HASH =
|
|
{
|
|
'Status: New' => '#428bca',
|
|
'Status: Accepted' => '#5cb85c',
|
|
'Status: Started' => '#8e44ad',
|
|
'Priority: Critical' => '#ffcfcf',
|
|
'Priority: High' => '#deffcf',
|
|
'Priority: Medium' => '#fff5cc',
|
|
'Priority: Low' => '#cfe9ff',
|
|
'Type: Defect' => '#d9534f',
|
|
'Type: Enhancement' => '#44ad8e',
|
|
'Type: Task' => '#4b6dd0',
|
|
'Type: Review' => '#8e44ad',
|
|
'Type: Other' => '#7f8c8d'
|
|
}.freeze
|
|
|
|
def initialize(project)
|
|
@project = project
|
|
|
|
import_data = project.import_data.try(:data)
|
|
repo_data = import_data["repo"] if import_data
|
|
@repo = GoogleCodeImport::Repository.new(repo_data)
|
|
|
|
@closed_statuses = []
|
|
@known_labels = Set.new
|
|
end
|
|
|
|
def execute
|
|
return true unless repo.valid?
|
|
|
|
import_status_labels
|
|
|
|
import_labels
|
|
|
|
import_issues
|
|
|
|
true
|
|
end
|
|
|
|
private
|
|
|
|
def user_map
|
|
@user_map ||= begin
|
|
user_map = Hash.new do |hash, user|
|
|
# Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked.
|
|
Client.mask_email(user).sub("...", "\\.\\.\\.")
|
|
end
|
|
|
|
import_data = project.import_data.try(:data)
|
|
stored_user_map = import_data["user_map"] if import_data
|
|
user_map.update(stored_user_map) if stored_user_map
|
|
|
|
user_map
|
|
end
|
|
end
|
|
|
|
def import_status_labels
|
|
repo.raw_data["issuesConfig"]["statuses"].each do |status|
|
|
closed = !status["meansOpen"]
|
|
@closed_statuses << status["status"] if closed
|
|
|
|
name = nice_status_name(status["status"])
|
|
create_label(name)
|
|
@known_labels << name
|
|
end
|
|
end
|
|
|
|
def import_labels
|
|
repo.raw_data["issuesConfig"]["labels"].each do |label|
|
|
name = nice_label_name(label["label"])
|
|
create_label(name)
|
|
@known_labels << name
|
|
end
|
|
end
|
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
def import_issues
|
|
return unless repo.issues
|
|
|
|
while raw_issue = repo.issues.shift
|
|
author = user_map[raw_issue["author"]["name"]]
|
|
date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long)
|
|
|
|
comments = raw_issue["comments"]["items"]
|
|
issue_comment = comments.shift
|
|
|
|
content = format_content(issue_comment["content"])
|
|
attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"])
|
|
|
|
body = format_issue_body(author, date, content, attachments)
|
|
labels = import_issue_labels(raw_issue)
|
|
|
|
assignee_id = nil
|
|
if raw_issue.key?("owner")
|
|
username = user_map[raw_issue["owner"]["name"]]
|
|
|
|
if username.start_with?("@")
|
|
username = username[1..-1]
|
|
|
|
if user = UserFinder.new(username).find_by_username
|
|
assignee_id = user.id
|
|
end
|
|
end
|
|
end
|
|
|
|
issue = Issue.create!(
|
|
iid: raw_issue['id'],
|
|
project_id: project.id,
|
|
title: raw_issue['title'],
|
|
description: body,
|
|
author_id: project.creator_id,
|
|
assignee_ids: [assignee_id],
|
|
state: raw_issue['state'] == 'closed' ? 'closed' : 'opened'
|
|
)
|
|
|
|
issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
|
|
issue.update_attribute(:label_ids, issue_labels.pluck(:id))
|
|
|
|
import_issue_comments(issue, comments)
|
|
end
|
|
end
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
|
|
def import_issue_labels(raw_issue)
|
|
labels = []
|
|
|
|
raw_issue["labels"].each do |label|
|
|
name = nice_label_name(label)
|
|
labels << name
|
|
|
|
unless @known_labels.include?(name)
|
|
create_label(name)
|
|
@known_labels << name
|
|
end
|
|
end
|
|
|
|
labels << nice_status_name(raw_issue["status"])
|
|
labels
|
|
end
|
|
|
|
def import_issue_comments(issue, comments)
|
|
Note.transaction do
|
|
while raw_comment = comments.shift
|
|
next if raw_comment.key?("deletedBy")
|
|
|
|
content = format_content(raw_comment["content"])
|
|
updates = format_updates(raw_comment["updates"])
|
|
attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"])
|
|
|
|
next if content.blank? && updates.blank? && attachments.blank?
|
|
|
|
author = user_map[raw_comment["author"]["name"]]
|
|
date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long)
|
|
|
|
body = format_issue_comment_body(
|
|
raw_comment["id"],
|
|
author,
|
|
date,
|
|
content,
|
|
updates,
|
|
attachments
|
|
)
|
|
|
|
# Needs to match order of `comment_columns` below.
|
|
Note.create!(
|
|
project_id: project.id,
|
|
noteable_type: "Issue",
|
|
noteable_id: issue.id,
|
|
author_id: project.creator_id,
|
|
note: body
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def nice_label_color(name)
|
|
NICE_LABEL_COLOR_HASH[name] ||
|
|
case name
|
|
when /\AComponent:/
|
|
'#fff39e'
|
|
when /\AOpSys:/
|
|
'#e2e2e2'
|
|
when /\AMilestone:/
|
|
'#fee3ff'
|
|
when *closed_statuses.map { |s| nice_status_name(s) }
|
|
'#cfcfcf'
|
|
else
|
|
'#e2e2e2'
|
|
end
|
|
end
|
|
|
|
def nice_label_name(name)
|
|
name.sub("-", ": ")
|
|
end
|
|
|
|
def nice_status_name(name)
|
|
"Status: #{name}"
|
|
end
|
|
|
|
def linkify_issues(str)
|
|
str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
|
|
str = str.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
|
|
str
|
|
end
|
|
|
|
def escape_for_markdown(str)
|
|
# No headings and lists
|
|
str = str.gsub(/^#/, "\\#")
|
|
str = str.gsub(/^-/, "\\-")
|
|
|
|
# No inline code
|
|
str = str.gsub("`", "\\`")
|
|
|
|
# Carriage returns make me sad
|
|
str = str.delete("\r")
|
|
|
|
# Markdown ignores single newlines, but we need them as <br />.
|
|
str = str.gsub("\n", " \n")
|
|
|
|
str
|
|
end
|
|
|
|
def create_label(name)
|
|
params = { name: name, color: nice_label_color(name) }
|
|
::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
|
|
end
|
|
|
|
def format_content(raw_content)
|
|
linkify_issues(escape_for_markdown(raw_content))
|
|
end
|
|
|
|
def format_updates(raw_updates)
|
|
updates = []
|
|
|
|
if raw_updates.key?("status")
|
|
updates << "*Status: #{raw_updates["status"]}*"
|
|
end
|
|
|
|
if raw_updates.key?("owner")
|
|
updates << "*Owner: #{user_map[raw_updates["owner"]]}*"
|
|
end
|
|
|
|
if raw_updates.key?("cc")
|
|
cc = raw_updates["cc"].map do |l|
|
|
deleted = l.start_with?("-")
|
|
l = l[1..-1] if deleted
|
|
l = user_map[l]
|
|
l = "~~#{l}~~" if deleted
|
|
l
|
|
end
|
|
|
|
updates << "*Cc: #{cc.join(", ")}*"
|
|
end
|
|
|
|
if raw_updates.key?("labels")
|
|
labels = raw_updates["labels"].map do |l|
|
|
deleted = l.start_with?("-")
|
|
l = l[1..-1] if deleted
|
|
l = nice_label_name(l)
|
|
l = "~~#{l}~~" if deleted
|
|
l
|
|
end
|
|
|
|
updates << "*Labels: #{labels.join(", ")}*"
|
|
end
|
|
|
|
if raw_updates.key?("mergedInto")
|
|
updates << "*Merged into: ##{raw_updates["mergedInto"]}*"
|
|
end
|
|
|
|
if raw_updates.key?("blockedOn")
|
|
blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
|
|
format_blocking_updates(raw_blocked_on)
|
|
end
|
|
|
|
updates << "*Blocked on: #{blocked_ons.join(", ")}*"
|
|
end
|
|
|
|
if raw_updates.key?("blocking")
|
|
blockings = raw_updates["blocking"].map do |raw_blocked_on|
|
|
format_blocking_updates(raw_blocked_on)
|
|
end
|
|
|
|
updates << "*Blocking: #{blockings.join(", ")}*"
|
|
end
|
|
|
|
updates
|
|
end
|
|
|
|
def format_blocking_updates(raw_blocked_on)
|
|
name, id = raw_blocked_on.split(":", 2)
|
|
|
|
deleted = name.start_with?("-")
|
|
name = name[1..-1] if deleted
|
|
|
|
text =
|
|
if name == project.import_source
|
|
"##{id}"
|
|
else
|
|
"#{project.namespace.full_path}/#{name}##{id}"
|
|
end
|
|
|
|
text = "~~#{text}~~" if deleted
|
|
text
|
|
end
|
|
|
|
def format_attachments(issue_id, comment_id, raw_attachments)
|
|
return [] unless raw_attachments
|
|
|
|
raw_attachments.map do |attachment|
|
|
next if attachment["isDeleted"]
|
|
|
|
filename = attachment["fileName"]
|
|
link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}"
|
|
|
|
text = "[#{filename}](#{link})"
|
|
text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i
|
|
text
|
|
end.compact
|
|
end
|
|
|
|
def format_issue_comment_body(id, author, date, content, updates, attachments)
|
|
body = []
|
|
body << "*Comment #{id} by #{author} on #{date}*"
|
|
body << "---"
|
|
|
|
if content.blank?
|
|
content = "*(No comment has been entered for this change)*"
|
|
end
|
|
|
|
body << content
|
|
|
|
if updates.any?
|
|
body << "---"
|
|
body += updates
|
|
end
|
|
|
|
if attachments.any?
|
|
body << "---"
|
|
body += attachments
|
|
end
|
|
|
|
body.join("\n\n")
|
|
end
|
|
|
|
def format_issue_body(author, date, content, attachments)
|
|
body = []
|
|
body << "*By #{author} on #{date} (imported from Google Code)*"
|
|
body << "---"
|
|
|
|
if content.blank?
|
|
content = "*(No description has been entered for this issue)*"
|
|
end
|
|
|
|
body << content
|
|
|
|
if attachments.any?
|
|
body << "---"
|
|
body += attachments
|
|
end
|
|
|
|
body.join("\n\n")
|
|
end
|
|
end
|
|
end
|
|
end
|