8c467b9175
Inline diff comments did not have the proper position, so even though they had line codes the merge request validation would fail. Now we cache the line position for each parent comment and use that. Closes #50052
261 lines
8.9 KiB
Ruby
261 lines
8.9 KiB
Ruby
module Gitlab
|
|
module BitbucketImport
|
|
class Importer
|
|
include Gitlab::ShellAdapter
|
|
|
|
LABELS = [{ title: 'bug', color: '#FF0000' },
|
|
{ title: 'enhancement', color: '#428BCA' },
|
|
{ title: 'proposal', color: '#69D100' },
|
|
{ title: 'task', color: '#7F8C8D' }].freeze
|
|
|
|
attr_reader :project, :client, :errors, :users
|
|
|
|
def initialize(project)
|
|
@project = project
|
|
@client = Bitbucket::Client.new(project.import_data.credentials)
|
|
@formatter = Gitlab::ImportFormatter.new
|
|
@labels = {}
|
|
@errors = []
|
|
@users = {}
|
|
end
|
|
|
|
def execute
|
|
import_wiki
|
|
import_issues
|
|
import_pull_requests
|
|
handle_errors
|
|
|
|
true
|
|
end
|
|
|
|
private
|
|
|
|
def handle_errors
|
|
return unless errors.any?
|
|
|
|
project.update_column(:import_error, {
|
|
message: 'The remote data could not be fully imported.',
|
|
errors: errors
|
|
}.to_json)
|
|
end
|
|
|
|
def gitlab_user_id(project, username)
|
|
find_user_id(username) || project.creator_id
|
|
end
|
|
|
|
def find_user_id(username)
|
|
return nil unless username
|
|
|
|
return users[username] if users.key?(username)
|
|
|
|
users[username] = User.select(:id)
|
|
.joins(:identities)
|
|
.find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username)
|
|
.try(:id)
|
|
end
|
|
|
|
def repo
|
|
@repo ||= client.repo(project.import_source)
|
|
end
|
|
|
|
def import_wiki
|
|
return if project.wiki.repository_exists?
|
|
|
|
disk_path = project.wiki.disk_path
|
|
import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
|
|
gitlab_shell.import_repository(project.repository_storage, disk_path, import_url)
|
|
rescue StandardError => e
|
|
errors << { type: :wiki, errors: e.message }
|
|
end
|
|
|
|
def import_issues
|
|
return unless repo.issues_enabled?
|
|
|
|
create_labels
|
|
|
|
client.issues(repo).each do |issue|
|
|
begin
|
|
description = ''
|
|
description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
|
|
description += issue.description
|
|
|
|
label_name = issue.kind
|
|
milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
|
|
|
|
gitlab_issue = project.issues.create!(
|
|
iid: issue.iid,
|
|
title: issue.title,
|
|
description: description,
|
|
state: issue.state,
|
|
author_id: gitlab_user_id(project, issue.author),
|
|
milestone: milestone,
|
|
created_at: issue.created_at,
|
|
updated_at: issue.updated_at
|
|
)
|
|
|
|
gitlab_issue.labels << @labels[label_name]
|
|
|
|
import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
|
|
rescue StandardError => e
|
|
errors << { type: :issue, iid: issue.iid, errors: e.message }
|
|
end
|
|
end
|
|
end
|
|
|
|
def import_issue_comments(issue, gitlab_issue)
|
|
client.issue_comments(repo, issue.iid).each do |comment|
|
|
# The note can be blank for issue service messages like "Changed title: ..."
|
|
# We would like to import those comments as well but there is no any
|
|
# specific parameter that would allow to process them, it's just an empty comment.
|
|
# To prevent our importer from just crashing or from creating useless empty comments
|
|
# we do this check.
|
|
next unless comment.note.present?
|
|
|
|
note = ''
|
|
note += @formatter.author_line(comment.author) unless find_user_id(comment.author)
|
|
note += comment.note
|
|
|
|
begin
|
|
gitlab_issue.notes.create!(
|
|
project: project,
|
|
note: note,
|
|
author_id: gitlab_user_id(project, comment.author),
|
|
created_at: comment.created_at,
|
|
updated_at: comment.updated_at
|
|
)
|
|
rescue StandardError => e
|
|
errors << { type: :issue_comment, iid: issue.iid, errors: e.message }
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_labels
|
|
LABELS.each do |label_params|
|
|
label = ::Labels::CreateService.new(label_params).execute(project: project)
|
|
if label.valid?
|
|
@labels[label_params[:title]] = label
|
|
else
|
|
raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.full_name}\""
|
|
end
|
|
end
|
|
end
|
|
|
|
def import_pull_requests
|
|
pull_requests = client.pull_requests(repo)
|
|
|
|
pull_requests.each do |pull_request|
|
|
begin
|
|
description = ''
|
|
description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
|
|
description += pull_request.description
|
|
|
|
source_branch_sha = pull_request.source_branch_sha
|
|
target_branch_sha = pull_request.target_branch_sha
|
|
source_branch_sha = project.repository.commit(source_branch_sha)&.sha || source_branch_sha
|
|
target_branch_sha = project.repository.commit(target_branch_sha)&.sha || target_branch_sha
|
|
|
|
merge_request = project.merge_requests.create!(
|
|
iid: pull_request.iid,
|
|
title: pull_request.title,
|
|
description: description,
|
|
source_project: project,
|
|
source_branch: pull_request.source_branch_name,
|
|
source_branch_sha: source_branch_sha,
|
|
target_project: project,
|
|
target_branch: pull_request.target_branch_name,
|
|
target_branch_sha: target_branch_sha,
|
|
state: pull_request.state,
|
|
author_id: gitlab_user_id(project, pull_request.author),
|
|
assignee_id: nil,
|
|
created_at: pull_request.created_at,
|
|
updated_at: pull_request.updated_at
|
|
)
|
|
|
|
import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
|
|
rescue StandardError => e
|
|
errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, trace: e.backtrace.join("\n"), raw_response: pull_request.raw }
|
|
end
|
|
end
|
|
end
|
|
|
|
def import_pull_request_comments(pull_request, merge_request)
|
|
comments = client.pull_request_comments(repo, pull_request.iid)
|
|
|
|
inline_comments, pr_comments = comments.partition(&:inline?)
|
|
|
|
import_inline_comments(inline_comments, pull_request, merge_request)
|
|
import_standalone_pr_comments(pr_comments, merge_request)
|
|
end
|
|
|
|
def import_inline_comments(inline_comments, pull_request, merge_request)
|
|
position_map = {}
|
|
discussion_map = {}
|
|
|
|
children, parents = inline_comments.partition(&:has_parent?)
|
|
|
|
# The Bitbucket API returns threaded replies as parent-child
|
|
# relationships. We assume that the child can appear in any order in
|
|
# the JSON.
|
|
parents.each do |comment|
|
|
position_map[comment.iid] = build_position(merge_request, comment)
|
|
end
|
|
|
|
children.each do |comment|
|
|
position_map[comment.iid] = position_map.fetch(comment.parent_id, nil)
|
|
end
|
|
|
|
inline_comments.each do |comment|
|
|
begin
|
|
attributes = pull_request_comment_attributes(comment)
|
|
attributes[:discussion_id] = discussion_map[comment.parent_id] if comment.has_parent?
|
|
|
|
attributes.merge!(
|
|
position: position_map[comment.iid],
|
|
type: 'DiffNote')
|
|
|
|
note = merge_request.notes.create!(attributes)
|
|
|
|
# We can't store a discussion ID until a note is created, so if
|
|
# replies are created before the parent the discussion ID won't be
|
|
# linked properly.
|
|
discussion_map[comment.iid] = note.discussion_id
|
|
rescue StandardError => e
|
|
errors << { type: :pull_request, iid: comment.iid, errors: e.message }
|
|
end
|
|
end
|
|
end
|
|
|
|
def build_position(merge_request, pr_comment)
|
|
params = {
|
|
diff_refs: merge_request.diff_refs,
|
|
old_path: pr_comment.file_path,
|
|
new_path: pr_comment.file_path,
|
|
old_line: pr_comment.old_pos,
|
|
new_line: pr_comment.new_pos
|
|
}
|
|
|
|
Gitlab::Diff::Position.new(params)
|
|
end
|
|
|
|
def import_standalone_pr_comments(pr_comments, merge_request)
|
|
pr_comments.each do |comment|
|
|
begin
|
|
merge_request.notes.create!(pull_request_comment_attributes(comment))
|
|
rescue StandardError => e
|
|
errors << { type: :pull_request, iid: comment.iid, errors: e.message }
|
|
end
|
|
end
|
|
end
|
|
|
|
def pull_request_comment_attributes(comment)
|
|
{
|
|
project: project,
|
|
note: comment.note,
|
|
author_id: gitlab_user_id(project, comment.author),
|
|
created_at: comment.created_at,
|
|
updated_at: comment.updated_at
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|