313 lines
10 KiB
Ruby
313 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module BitbucketImport
|
|
class Importer
|
|
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
|
|
metrics.track_finished_import
|
|
|
|
true
|
|
end
|
|
|
|
private
|
|
|
|
def handle_errors
|
|
return unless errors.any?
|
|
|
|
project.import_state.update_column(:last_error, {
|
|
message: 'The remote data could not be fully imported.',
|
|
errors: errors
|
|
}.to_json)
|
|
end
|
|
|
|
def store_pull_request_error(pull_request, ex)
|
|
backtrace = Gitlab::BacktraceCleaner.clean_backtrace(ex.backtrace)
|
|
error = { type: :pull_request, iid: pull_request.iid, errors: ex.message, trace: backtrace, raw_response: pull_request.raw&.to_json }
|
|
|
|
Gitlab::ErrorTracking.log_exception(ex, error)
|
|
|
|
# Omit the details from the database to avoid blowing up usage in the error column
|
|
error.delete(:trace)
|
|
error.delete(:raw_response)
|
|
|
|
errors << error
|
|
end
|
|
|
|
def gitlab_user_id(project, username)
|
|
find_user_id(username) || project.creator_id
|
|
end
|
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
def find_user_id(username)
|
|
return unless username
|
|
|
|
return users[username] if users.key?(username)
|
|
|
|
users[username] = User.by_provider_and_extern_uid(:bitbucket, username).select(:id).first&.id
|
|
end
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
|
|
def allocate_issues_internal_id!(project, client)
|
|
last_bitbucket_issue = client.last_issue(repo)
|
|
|
|
return unless last_bitbucket_issue
|
|
|
|
Issue.track_project_iid!(project, last_bitbucket_issue.iid)
|
|
end
|
|
|
|
def repo
|
|
@repo ||= client.repo(project.import_source)
|
|
end
|
|
|
|
def import_wiki
|
|
return if project.wiki.repository_exists?
|
|
|
|
wiki = WikiFormatter.new(project)
|
|
|
|
project.wiki.repository.import_repository(wiki.import_url)
|
|
rescue StandardError => e
|
|
errors << { type: :wiki, errors: e.message }
|
|
end
|
|
|
|
def import_issues
|
|
return unless repo.issues_enabled?
|
|
|
|
# If a user creates an issue while the import is in progress, this can lead to an import failure.
|
|
# The workaround is to allocate IIDs before starting the importer.
|
|
allocate_issues_internal_id!(project, client)
|
|
|
|
create_labels
|
|
|
|
issue_type_id = ::WorkItems::Type.default_issue_type.id
|
|
|
|
client.issues(repo).each do |issue|
|
|
import_issue(issue, issue_type_id)
|
|
end
|
|
end
|
|
|
|
# rubocop: disable CodeReuse/ActiveRecord
|
|
def import_issue(issue, issue_type_id)
|
|
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_id: Issue.available_states[issue.state],
|
|
author_id: gitlab_user_id(project, issue.author),
|
|
namespace_id: project.project_namespace_id,
|
|
milestone: milestone,
|
|
work_item_type_id: issue_type_id,
|
|
created_at: issue.created_at,
|
|
updated_at: issue.updated_at
|
|
)
|
|
|
|
metrics.issues_counter.increment
|
|
|
|
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
|
|
# rubocop: enable CodeReuse/ActiveRecord
|
|
|
|
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::FindOrCreateService.new(nil, project, label_params).execute(skip_authorization: true)
|
|
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|
|
|
import_pull_request(pull_request)
|
|
end
|
|
end
|
|
|
|
def import_pull_request(pull_request)
|
|
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),
|
|
created_at: pull_request.created_at,
|
|
updated_at: pull_request.updated_at
|
|
)
|
|
|
|
metrics.merge_requests_counter.increment
|
|
|
|
import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
|
|
rescue StandardError => e
|
|
store_pull_request_error(pull_request, e)
|
|
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|
|
|
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
|
|
|
|
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|
|
|
merge_request.notes.create!(pull_request_comment_attributes(comment))
|
|
rescue StandardError => e
|
|
errors << { type: :pull_request, iid: comment.iid, errors: e.message }
|
|
end
|
|
end
|
|
|
|
def pull_request_comment_attributes(comment)
|
|
{
|
|
project: project,
|
|
author_id: gitlab_user_id(project, comment.author),
|
|
note: comment_note(comment),
|
|
created_at: comment.created_at,
|
|
updated_at: comment.updated_at
|
|
}
|
|
end
|
|
|
|
def comment_note(comment)
|
|
author = @formatter.author_line(comment.author) unless find_user_id(comment.author)
|
|
|
|
author.to_s + comment.note.to_s
|
|
end
|
|
|
|
def log_base_data
|
|
{
|
|
class: self.class.name,
|
|
project_id: project.id,
|
|
project_path: project.full_path
|
|
}
|
|
end
|
|
|
|
def metrics
|
|
@metrics ||= Gitlab::Import::Metrics.new(:bitbucket_importer, @project)
|
|
end
|
|
end
|
|
end
|
|
end
|