summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/bitbucket_server/client.rb55
-rw-r--r--lib/bitbucket_server/collection.rb21
-rw-r--r--lib/bitbucket_server/connection.rb97
-rw-r--r--lib/bitbucket_server/page.rb34
-rw-r--r--lib/bitbucket_server/paginator.rb36
-rw-r--r--lib/bitbucket_server/representation/activity.rb81
-rw-r--r--lib/bitbucket_server/representation/base.rb15
-rw-r--r--lib/bitbucket_server/representation/comment.rb84
-rw-r--r--lib/bitbucket_server/representation/pull_request.rb73
-rw-r--r--lib/bitbucket_server/representation/pull_request_comment.rb96
-rw-r--r--lib/bitbucket_server/representation/repo.rb61
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb308
-rw-r--r--lib/gitlab/bitbucket_server_import/project_creator.rb36
-rw-r--r--lib/gitlab/import_sources.rb3
14 files changed, 999 insertions, 1 deletions
diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb
new file mode 100644
index 00000000000..bfa8301f643
--- /dev/null
+++ b/lib/bitbucket_server/client.rb
@@ -0,0 +1,55 @@
+module BitbucketServer
+ class Client
+ attr_reader :connection
+
+ def initialize(options = {})
+ @connection = Connection.new(options)
+ end
+
+ def pull_requests(project_key, repo)
+ path = "/projects/#{project_key}/repos/#{repo}/pull-requests?state=ALL"
+ get_collection(path, :pull_request)
+ end
+
+ def activities(project_key, repo, pull_request)
+ path = "/projects/#{project_key}/repos/#{repo}/pull-requests/#{pull_request}/activities"
+ get_collection(path, :activity)
+ end
+
+ def repo(project, repo_name)
+ parsed_response = connection.get("/projects/#{project}/repos/#{repo_name}")
+ BitbucketServer::Representation::Repo.new(parsed_response)
+ end
+
+ def repos
+ path = "/repos"
+ get_collection(path, :repo)
+ end
+
+ def create_branch(project_key, repo, branch_name, sha)
+ payload = {
+ name: branch_name,
+ startPoint: sha,
+ message: 'GitLab temporary branch for import'
+ }
+
+ connection.post("/projects/#{project_key}/repos/#{repo}/branches", payload.to_json)
+ end
+
+ def delete_branch(project_key, repo, branch_name, sha)
+ payload = {
+ name: Gitlab::Git::BRANCH_REF_PREFIX + branch_name,
+ dryRun: false
+ }
+
+ connection.delete(:branches, "/projects/#{project_key}/repos/#{repo}/branches", payload.to_json)
+ end
+
+ private
+
+ def get_collection(path, type)
+ paginator = BitbucketServer::Paginator.new(connection, path, type)
+ BitbucketServer::Collection.new(paginator)
+ end
+ end
+end
diff --git a/lib/bitbucket_server/collection.rb b/lib/bitbucket_server/collection.rb
new file mode 100644
index 00000000000..1f199c99854
--- /dev/null
+++ b/lib/bitbucket_server/collection.rb
@@ -0,0 +1,21 @@
+module BitbucketServer
+ class Collection < Enumerator
+ def initialize(paginator)
+ super() do |yielder|
+ loop do
+ paginator.items.each { |item| yielder << item }
+ end
+ end
+
+ lazy
+ end
+
+ def method_missing(method, *args)
+ return super unless self.respond_to?(method)
+
+ self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend
+ block_given? ? yield(item) : item
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb
new file mode 100644
index 00000000000..3918944aa81
--- /dev/null
+++ b/lib/bitbucket_server/connection.rb
@@ -0,0 +1,97 @@
+module BitbucketServer
+ class Connection
+ include ActionView::Helpers::SanitizeHelper
+
+ DEFAULT_API_VERSION = '1.0'.freeze
+
+ attr_reader :api_version, :base_uri, :username, :token
+
+ ConnectionError = Class.new(StandardError)
+
+ def initialize(options = {})
+ @api_version = options.fetch(:api_version, DEFAULT_API_VERSION)
+ @base_uri = options[:base_uri]
+ @username = options[:user]
+ @token = options[:password]
+ end
+
+ def get(path, extra_query = {})
+ response = Gitlab::HTTP.get(build_url(path),
+ basic_auth: auth,
+ query: extra_query)
+
+ check_errors!(response)
+
+ response.parsed_response
+ end
+
+ def post(path, body)
+ response = Gitlab::HTTP.post(build_url(path),
+ basic_auth: auth,
+ headers: post_headers,
+ body: body)
+
+ check_errors!(response)
+
+ response.parsed_response
+ end
+
+ # We need to support two different APIs for deletion:
+ #
+ # /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/branches/default
+ # /rest/branch-utils/1.0/projects/{projectKey}/repos/{repositorySlug}/branches
+ def delete(resource, path, body)
+ url = delete_url(resource, path)
+
+ response = Gitlab::HTTP.delete(url,
+ basic_auth: auth,
+ headers: post_headers,
+ body: body)
+
+ check_errors!(response)
+
+ response.parsed_response
+ end
+
+ private
+
+ def check_errors!(response)
+ return if response.code >= 200 && response.code < 300
+
+ details =
+ if response.parsed_response && response.parsed_response.is_a?(Hash)
+ sanitize(response.parsed_response.dig('errors', 0, 'message'))
+ end
+
+ message = "Error #{response.code}"
+ message += ": #{details}" if details
+ raise ConnectionError, message
+ end
+
+ def auth
+ @auth ||= { username: username, password: token }
+ end
+
+ def post_headers
+ @post_headers ||= { 'Content-Type' => 'application/json' }
+ end
+
+ def build_url(path)
+ return path if path.starts_with?(root_url)
+
+ "#{root_url}#{path}"
+ end
+
+ def root_url
+ "#{base_uri}/rest/api/#{api_version}"
+ end
+
+ def delete_url(resource, path)
+ if resource == :branches
+ "#{base_uri}/rest/branch-utils/#{api_version}#{path}"
+ else
+ build_url(path)
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/page.rb b/lib/bitbucket_server/page.rb
new file mode 100644
index 00000000000..17be8cbb860
--- /dev/null
+++ b/lib/bitbucket_server/page.rb
@@ -0,0 +1,34 @@
+module BitbucketServer
+ class Page
+ attr_reader :attrs, :items
+
+ def initialize(raw, type)
+ @attrs = parse_attrs(raw)
+ @items = parse_values(raw, representation_class(type))
+ end
+
+ def next?
+ !attrs.fetch(:isLastPage, true)
+ end
+
+ def next
+ attrs.fetch(:nextPageStart)
+ end
+
+ private
+
+ def parse_attrs(raw)
+ raw.slice(*%w(size nextPageStart isLastPage)).symbolize_keys
+ end
+
+ def parse_values(raw, bitbucket_rep_class)
+ return [] unless raw['values'] && raw['values'].is_a?(Array)
+
+ bitbucket_rep_class.decorate(raw['values'])
+ end
+
+ def representation_class(type)
+ BitbucketServer::Representation.const_get(type.to_s.camelize)
+ end
+ end
+end
diff --git a/lib/bitbucket_server/paginator.rb b/lib/bitbucket_server/paginator.rb
new file mode 100644
index 00000000000..a17045be97e
--- /dev/null
+++ b/lib/bitbucket_server/paginator.rb
@@ -0,0 +1,36 @@
+module BitbucketServer
+ class Paginator
+ PAGE_LENGTH = 25
+
+ def initialize(connection, url, type)
+ @connection = connection
+ @type = type
+ @url = url
+ @page = nil
+ end
+
+ def items
+ raise StopIteration unless has_next_page?
+
+ @page = fetch_next_page
+ @page.items
+ end
+
+ private
+
+ attr_reader :connection, :page, :url, :type
+
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
+ def next_offset
+ page.nil? ? 0 : page.next
+ end
+
+ def fetch_next_page
+ parsed_response = connection.get(@url, start: next_offset, limit: PAGE_LENGTH)
+ Page.new(parsed_response, type)
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/activity.rb b/lib/bitbucket_server/representation/activity.rb
new file mode 100644
index 00000000000..7c552c7f428
--- /dev/null
+++ b/lib/bitbucket_server/representation/activity.rb
@@ -0,0 +1,81 @@
+module BitbucketServer
+ module Representation
+ class Activity < Representation::Base
+ def action
+ raw['action']
+ end
+
+ def comment?
+ action == 'COMMENTED'.freeze
+ end
+
+ def inline_comment?
+ comment? && comment_anchor
+ end
+
+ def comment
+ return unless comment?
+
+ @comment ||=
+ if inline_comment?
+ PullRequestComment.new(raw)
+ else
+ Comment.new(raw)
+ end
+ end
+
+ # XXX Move this into MergeEvent
+ def merge_event?
+ action == 'MERGED'
+ end
+
+ def committer_user
+ commit.fetch('committer', {})['displayName']
+ end
+
+ def committer_email
+ commit.fetch('committer', {})['emailAddress']
+ end
+
+ def merge_timestamp
+ timestamp = commit.fetch('committer', {})['commiterTimestamp']
+
+ Time.at(timestamp / 1000.0) if timestamp.is_a?(Integer)
+ end
+
+ def commit
+ raw.fetch('commit', {})
+ end
+
+ def created_at
+ Time.at(created_date / 1000) if created_date.is_a?(Integer)
+ end
+
+ def updated_at
+ Time.at(updated_date / 1000) if created_date.is_a?(Integer)
+ end
+
+ private
+
+ def raw_comment
+ raw.fetch('comment', {})
+ end
+
+ def comment_anchor
+ raw['commentAnchor']
+ end
+
+ def author
+ raw_comment.fetch('author', {})
+ end
+
+ def created_date
+ comment['createdDate']
+ end
+
+ def updated_date
+ comment['updatedDate']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/base.rb b/lib/bitbucket_server/representation/base.rb
new file mode 100644
index 00000000000..11b32b70c4c
--- /dev/null
+++ b/lib/bitbucket_server/representation/base.rb
@@ -0,0 +1,15 @@
+module BitbucketServer
+ module Representation
+ class Base
+ attr_reader :raw
+
+ def initialize(raw)
+ @raw = raw
+ end
+
+ def self.decorate(entries)
+ entries.map { |entry| new(entry)}
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/comment.rb b/lib/bitbucket_server/representation/comment.rb
new file mode 100644
index 00000000000..44f76139fc4
--- /dev/null
+++ b/lib/bitbucket_server/representation/comment.rb
@@ -0,0 +1,84 @@
+module BitbucketServer
+ module Representation
+ # A general comment with the structure:
+ # "comment": {
+ # "author": {
+ # "active": true,
+ # "displayName": "root",
+ # "emailAddress": "stanhu+bitbucket@gitlab.com",
+ # "id": 1,
+ # "links": {
+ # "self": [
+ # {
+ # "href": "http://localhost:7990/users/root"
+ # }
+ # ]
+ # },
+ # "name": "root",
+ # "slug": "root",
+ # "type": "NORMAL"
+ # }
+ # }
+ # }
+ class Comment < Representation::Base
+ def id
+ raw_comment['id']
+ end
+
+ def author_username
+ author['username']
+ end
+
+ def author_email
+ author['displayName']
+ end
+
+ def note
+ raw_comment['text']
+ end
+
+ def created_at
+ Time.at(created_date / 1000) if created_date.is_a?(Integer)
+ end
+
+ def updated_at
+ Time.at(updated_date / 1000) if created_date.is_a?(Integer)
+ end
+
+ def comments
+ workset = [raw_comment['comments']].compact
+ all_comments = []
+
+ until workset.empty?
+ comments = workset.pop
+
+ comments.each do |comment|
+ new_comments = comment.delete('comments')
+ workset << new_comments if new_comments
+ all_comments << Comment.new({ 'comment' => comment })
+ end
+ end
+
+ all_comments
+ end
+
+ private
+
+ def raw_comment
+ raw.fetch('comment', {})
+ end
+
+ def author
+ raw_comment.fetch('author', {})
+ end
+
+ def created_date
+ raw_comment['createdDate']
+ end
+
+ def updated_date
+ raw_comment['updatedDate']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/pull_request.rb b/lib/bitbucket_server/representation/pull_request.rb
new file mode 100644
index 00000000000..344b6806a91
--- /dev/null
+++ b/lib/bitbucket_server/representation/pull_request.rb
@@ -0,0 +1,73 @@
+module BitbucketServer
+ module Representation
+ class PullRequest < Representation::Base
+ def author
+ raw.dig('author', 'user', 'name')
+ end
+
+ def author_email
+ raw.dig('author', 'user', 'emailAddress')
+ end
+
+ def description
+ raw['description']
+ end
+
+ def iid
+ raw['id']
+ end
+
+ def state
+ if raw['state'] == 'MERGED'
+ 'merged'
+ elsif raw['state'] == 'DECLINED'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def merged?
+ state == 'merged'
+ end
+
+ def created_at
+ Time.at(created_date / 1000) if created_date.is_a?(Integer)
+ end
+
+ def updated_at
+ Time.at(updated_date / 1000) if created_date.is_a?(Integer)
+ end
+
+ def title
+ raw['title']
+ end
+
+ def source_branch_name
+ dig('fromRef', 'id')
+ end
+
+ def source_branch_sha
+ dig('fromRef', 'latestCommit')
+ end
+
+ def target_branch_name
+ dig('toRef', 'id')
+ end
+
+ def target_branch_sha
+ dig('toRef', 'latestCommit')
+ end
+
+ private
+
+ def created_date
+ raw['createdDate']
+ end
+
+ def updated_date
+ raw['updatedDate']
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/pull_request_comment.rb b/lib/bitbucket_server/representation/pull_request_comment.rb
new file mode 100644
index 00000000000..c7d08e604fd
--- /dev/null
+++ b/lib/bitbucket_server/representation/pull_request_comment.rb
@@ -0,0 +1,96 @@
+module BitbucketServer
+ module Representation
+ # An inline comment with the following structure that identifies
+ # the part of the diff:
+ #
+ # "commentAnchor": {
+ # "diffType": "EFFECTIVE",
+ # "fileType": "TO",
+ # "fromHash": "c5f4288162e2e6218180779c7f6ac1735bb56eab",
+ # "line": 1,
+ # "lineType": "ADDED",
+ # "orphaned": false,
+ # "path": "CHANGELOG.md",
+ # "toHash": "a4c2164330f2549f67c13f36a93884cf66e976be"
+ # }
+ class PullRequestComment < Comment
+ def file_type
+ comment_anchor['fileType']
+ end
+
+ def from_sha
+ comment_anchor['fromHash']
+ end
+
+ def to_sha
+ comment_anchor['toHash']
+ end
+
+ def to?
+ file_type == 'TO'
+ end
+
+ def from?
+ file_type == 'FROM'
+ end
+
+ def added?
+ line_type == 'ADDED'
+ end
+
+ def removed?
+ line_type == 'REMOVED'
+ end
+
+ def new_pos
+ return if removed?
+ return unless line_position
+
+ line_position[1]
+ end
+
+ def old_pos
+ return if added?
+ return unless line_position
+
+ line_position[0]
+ end
+
+ def file_path
+ comment_anchor.fetch('path')
+ end
+
+ private
+
+ def line_type
+ comment_anchor['lineType']
+ end
+
+ def line_position
+ @line_position ||= diff_hunks.each do |hunk|
+ segments = hunk.fetch('segments', [])
+ segments.each do |segment|
+ lines = segment.fetch('lines', [])
+ lines.each do |line|
+ if line['commentIds']&.include?(id)
+ return [line['source'], line['destination']]
+ end
+ end
+ end
+ end
+ end
+
+ def comment_anchor
+ raw.fetch('commentAnchor', {})
+ end
+
+ def diff
+ raw.fetch('diff', {})
+ end
+
+ def diff_hunks
+ diff.fetch('hunks', [])
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/representation/repo.rb b/lib/bitbucket_server/representation/repo.rb
new file mode 100644
index 00000000000..1338f877fc1
--- /dev/null
+++ b/lib/bitbucket_server/representation/repo.rb
@@ -0,0 +1,61 @@
+module BitbucketServer
+ module Representation
+ class Repo < Representation::Base
+ def initialize(raw)
+ super(raw)
+ end
+
+ def project_name
+ raw.dig('project', 'name')
+ end
+
+ def slug
+ raw['slug']
+ end
+
+ def browse_url
+ raw.dig('links', 'self').first.fetch('href')
+ end
+
+ def clone_url
+ raw['links']['clone'].find { |link| link['name'].starts_with?('http') }.fetch('href')
+ end
+
+ def description
+ project['description']
+ end
+
+ def full_name
+ "#{project_name}/#{name}"
+ end
+
+ def issues_enabled?
+ true
+ end
+
+ def name
+ raw['name']
+ end
+
+ def valid?
+ raw['scmId'] == 'git'
+ end
+
+ def visibility_level
+ if project['public']
+ Gitlab::VisibilityLevel::PUBLIC
+ else
+ Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
+
+ def project
+ raw['project']
+ end
+
+ def to_s
+ full_name
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb
new file mode 100644
index 00000000000..f5febd266cb
--- /dev/null
+++ b/lib/gitlab/bitbucket_server_import/importer.rb
@@ -0,0 +1,308 @@
+module Gitlab
+ module BitbucketServerImport
+ class Importer
+ include Gitlab::ShellAdapter
+ attr_reader :recover_missing_commits
+ attr_reader :project, :project_key, :repository_slug, :client, :errors, :users
+
+ REMOTE_NAME = 'bitbucket_server'.freeze
+ BATCH_SIZE = 100
+
+ TempBranch = Struct.new(:name, :sha)
+
+ def self.imports_repository?
+ true
+ end
+
+ def self.refmap
+ [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head']
+ end
+
+ # Unlike GitHub, you can't grab the commit SHAs for pull requests that
+ # have been closed but not merged even though Bitbucket has these
+ # commits internally. We can recover these pull requests by creating a
+ # branch with the Bitbucket REST API, but by default we turn this
+ # behavior off.
+ def initialize(project, recover_missing_commits: false)
+ @project = project
+ @recover_missing_commits = recover_missing_commits
+ @project_key = project.import_data.data['project_key']
+ @repository_slug = project.import_data.data['repo_slug']
+ @client = BitbucketServer::Client.new(project.import_data.credentials)
+ @formatter = Gitlab::ImportFormatter.new
+ @errors = []
+ @users = {}
+ @temp_branches = []
+ end
+
+ def execute
+ import_repository
+ import_pull_requests
+ delete_temp_branches
+ 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(email)
+ find_user_id(email) || project.creator_id
+ end
+
+ def find_user_id(email)
+ return nil unless email
+
+ return users[email] if users.key?(email)
+
+ user = User.find_by_any_email(email)
+ users[email] = user&.id if user
+
+ user&.id
+ end
+
+ def repo
+ @repo ||= client.repo(project_key, repository_slug)
+ end
+
+ def sha_exists?(sha)
+ project.repository.commit(sha)
+ end
+
+ def temp_branch_name(pull_request, suffix)
+ "gitlab/import/pull-request/#{pull_request.iid}/#{suffix}"
+ end
+
+ # This method restores required SHAs that GitLab needs to create diffs
+ # into branch names as the following:
+ #
+ # gitlab/import/pull-request/N/{to,from}
+ def restore_branches(pull_requests)
+ shas_to_restore = []
+
+ pull_requests.each do |pull_request|
+ shas_to_restore << TempBranch.new(temp_branch_name(pull_request, :from),
+ pull_request.source_branch_sha)
+ shas_to_restore << TempBranch.new(temp_branch_name(pull_request, :to),
+ pull_request.target_branch_sha)
+ end
+
+ # Create the branches on the Bitbucket Server first
+ created_branches = restore_branch_shas(shas_to_restore)
+
+ @temp_branches += created_branches
+ # Now sync the repository so we get the new branches
+ import_repository unless created_branches.empty?
+ end
+
+ def restore_branch_shas(shas_to_restore)
+ shas_to_restore.each_with_object([]) do |temp_branch, branches_created|
+ branch_name = temp_branch.name
+ sha = temp_branch.sha
+
+ next if sha_exists?(sha)
+
+ begin
+ client.create_branch(project_key, repository_slug, branch_name, sha)
+ branches_created << temp_branch
+ rescue BitbucketServer::Connection::ConnectionError => e
+ Rails.logger.warn("BitbucketServerImporter: Unable to recreate branch for SHA #{sha}: #{e}")
+ end
+ end
+ end
+
+ def import_repository
+ project.ensure_repository
+ project.repository.fetch_as_mirror(project.import_url, refmap: self.class.refmap, remote_name: REMOTE_NAME)
+ rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
+ # Expire cache to prevent scenarios such as:
+ # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
+ # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
+ project.repository.expire_content_cache if project.repository_exists?
+
+ raise e.message
+ end
+
+ # Bitbucket Server keeps tracks of references for open pull requests in
+ # refs/heads/pull-requests, but closed and merged requests get moved
+ # into hidden internal refs under stash-refs/pull-requests. Unless the
+ # SHAs involved are at the tip of a branch or tag, there is no way to
+ # retrieve the server for those commits.
+ #
+ # To avoid losing history, we use the Bitbucket API to re-create the branch
+ # on the remote server. Then we have to issue a `git fetch` to download these
+ # branches.
+ def import_pull_requests
+ pull_requests = client.pull_requests(project_key, repository_slug).to_a
+
+ # Creating branches on the server and fetching the newly-created branches
+ # may take a number of network round-trips. Do this in batches so that we can
+ # avoid doing a git fetch for every new branch.
+ pull_requests.each_slice(BATCH_SIZE) do |batch|
+ restore_branches(batch) if recover_missing_commits
+
+ batch.each do |pull_request|
+ begin
+ import_bitbucket_pull_request(pull_request)
+ 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
+ end
+
+ def delete_temp_branches
+ @temp_branches.each do |branch|
+ begin
+ client.delete_branch(project_key, repository_slug, branch.name, branch.sha)
+ project.repository.delete_branch(branch.name)
+ rescue BitbucketServer::Connection::ConnectionError => e
+ @errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message }
+ end
+ end
+ end
+
+ def import_bitbucket_pull_request(pull_request)
+ description = ''
+ description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author_email)
+ 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
+ author_id = gitlab_user_id(pull_request.author_email)
+
+ attributes = {
+ iid: pull_request.iid,
+ title: pull_request.title,
+ description: description,
+ source_project: project,
+ source_branch: Gitlab::Git.ref_name(pull_request.source_branch_name),
+ source_branch_sha: source_branch_sha,
+ target_project: project,
+ target_branch: Gitlab::Git.ref_name(pull_request.target_branch_name),
+ target_branch_sha: target_branch_sha,
+ state: pull_request.state,
+ author_id: author_id,
+ assignee_id: nil,
+ created_at: pull_request.created_at,
+ updated_at: pull_request.updated_at
+ }
+
+ attributes[:merge_commit_sha] = target_branch_sha if pull_request.merged?
+ merge_request = project.merge_requests.create!(attributes)
+ import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
+ end
+
+ def import_pull_request_comments(pull_request, merge_request)
+ comments, other_activities = client.activities(project_key, repository_slug, pull_request.iid).partition(&:comment?)
+
+ merge_event = other_activities.find(&:merge_event?)
+ import_merge_event(merge_request, merge_event) if merge_event
+
+ inline_comments, pr_comments = comments.partition(&:inline_comment?)
+
+ import_inline_comments(inline_comments.map(&:comment), merge_request)
+ import_standalone_pr_comments(pr_comments.map(&:comment), merge_request)
+ end
+
+ def import_merge_event(merge_request, merge_event)
+ committer = merge_event.committer_email
+
+ user_id = gitlab_user_id(committer)
+ timestamp = merge_event.merge_timestamp
+ metric = MergeRequest::Metrics.find_or_initialize_by(merge_request: merge_request)
+ metric.update(merged_by_id: user_id, merged_at: timestamp)
+ end
+
+ def import_inline_comments(inline_comments, merge_request)
+ inline_comments.each do |comment|
+ position = build_position(merge_request, comment)
+ parent = create_diff_note(merge_request, comment, position)
+
+ next unless parent&.persisted?
+
+ discussion_id = parent.discussion_id
+
+ comment.comments.each do |reply|
+ create_diff_note(merge_request, reply, position, discussion_id)
+ end
+ end
+ end
+
+ def create_diff_note(merge_request, comment, position, discussion_id = nil)
+ attributes = pull_request_comment_attributes(comment)
+ attributes.merge!(position: position, type: 'DiffNote')
+ attributes[:discussion_id] = discussion_id if discussion_id
+
+ note = merge_request.notes.build(attributes)
+
+ if note.valid?
+ note.save
+ return note
+ end
+
+ # Bitbucket Server supports the ability to comment on any line, not just the
+ # line in the diff. If we can't add the note as a DiffNote, fallback to creating
+ # a regular note.
+ create_fallback_diff_note(merge_request, comment)
+ rescue StandardError => e
+ errors << { type: :pull_request, id: comment.id, errors: e.message }
+ nil
+ end
+
+ def create_fallback_diff_note(merge_request, comment)
+ attributes = pull_request_comment_attributes(comment)
+ attributes[:note] = "*Comment on file: #{comment.file_path}, old line: #{comment.old_pos}, new line: #{comment.new_pos}*\n\n" + attributes[:note]
+
+ merge_request.notes.create!(attributes)
+ 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))
+
+ comment.comments.each do |replies|
+ merge_request.notes.create!(pull_request_comment_attributes(replies))
+ end
+ rescue StandardError => e
+ errors << { type: :pull_request, iid: comment.id, errors: e.message }
+ end
+ end
+ end
+
+ def pull_request_comment_attributes(comment)
+ {
+ project: project,
+ note: comment.note,
+ author_id: gitlab_user_id(comment.author_email),
+ created_at: comment.created_at,
+ updated_at: comment.updated_at
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bitbucket_server_import/project_creator.rb b/lib/gitlab/bitbucket_server_import/project_creator.rb
new file mode 100644
index 00000000000..35e8cd7e0ab
--- /dev/null
+++ b/lib/gitlab/bitbucket_server_import/project_creator.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module BitbucketServerImport
+ class ProjectCreator
+ attr_reader :project_key, :repo_slug, :repo, :name, :namespace, :current_user, :session_data
+
+ def initialize(project_key, repo_slug, repo, name, namespace, current_user, session_data)
+ @project_key = project_key
+ @repo_slug = repo_slug
+ @repo = repo
+ @name = name
+ @namespace = namespace
+ @current_user = current_user
+ @session_data = session_data
+ end
+
+ def execute
+ ::Projects::CreateService.new(
+ current_user,
+ name: name,
+ path: name,
+ description: repo.description,
+ namespace_id: namespace.id,
+ visibility_level: repo.visibility_level,
+ import_type: 'bitbucket_server',
+ import_source: repo.browse_url,
+ import_url: repo.clone_url,
+ import_data: {
+ credentials: session_data,
+ data: { project_key: project_key, repo_slug: repo_slug }
+ },
+ skip_wiki: true
+ ).execute
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index af9b880ef9e..f901d1fdb5a 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -10,7 +10,8 @@ module Gitlab
# We exclude `bare_repository` here as it has no import class associated
ImportTable = [
ImportSource.new('github', 'GitHub', Gitlab::GithubImport::ParallelImporter),
- ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('bitbucket', 'Bitbucket Cloud', Gitlab::BitbucketImport::Importer),
+ ImportSource.new('bitbucket_server', 'Bitbucket Server', Gitlab::BitbucketServerImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
ImportSource.new('fogbugz', 'FogBugz', Gitlab::FogbugzImport::Importer),