importer.rb 11.3 KB
Newer Older
1 2 3 4
module Gitlab
  module BitbucketServerImport
    class Importer
      include Gitlab::ShellAdapter
5
      attr_reader :recover_missing_commits
6
      attr_reader :project, :project_key, :repository_slug, :client, :errors, :users
7

8
      REMOTE_NAME = 'bitbucket_server'.freeze
9
      BATCH_SIZE = 100
10

11 12
      TempBranch = Struct.new(:name, :sha)

13 14 15 16 17 18 19 20
      def self.imports_repository?
        true
      end

      def self.refmap
        [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head']
      end

21 22 23 24 25 26
      # 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)
27
        @project = project
28
        @recover_missing_commits = recover_missing_commits
29 30
        @project_key = project.import_data.data['project_key']
        @repository_slug = project.import_data.data['repo_slug']
31 32 33 34
        @client = BitbucketServer::Client.new(project.import_data.credentials)
        @formatter = Gitlab::ImportFormatter.new
        @errors = []
        @users = {}
35
        @temp_branches = []
36 37 38
      end

      def execute
39
        import_repository
40
        import_pull_requests
41
        delete_temp_branches
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
        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

S
Stan Hu 已提交
58
      def gitlab_user_id(email)
59
        find_user_id(email) || project.creator_id
60 61
      end

62 63
      def find_user_id(email)
        return nil unless email
64

65
        return users[email] if users.key?(email)
66

S
Stan Hu 已提交
67 68 69 70
        user = User.find_by_any_email(email)
        users[email] = user&.id if user

        user&.id
71 72 73
      end

      def repo
74
        @repo ||= client.repo(project_key, repository_slug)
75 76
      end

77 78 79 80
      def sha_exists?(sha)
        project.repository.commit(sha)
      end

81 82 83 84
      def temp_branch_name(pull_request, suffix)
        "gitlab/import/pull-request/#{pull_request.iid}/#{suffix}"
      end

85 86 87 88
      # This method restores required SHAs that GitLab needs to create diffs
      # into branch names as the following:
      #
      # gitlab/import/pull-request/N/{to,from}
89 90
      def restore_branches(pull_requests)
        shas_to_restore = []
91

92
        pull_requests.each do |pull_request|
93 94 95 96
          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)
97
        end
98

99
        # Create the branches on the Bitbucket Server first
100
        created_branches = restore_branch_shas(shas_to_restore)
101

102
        @temp_branches += created_branches
103
        # Now sync the repository so we get the new branches
104
        import_repository unless created_branches.empty?
105 106
      end

107
      def restore_branch_shas(shas_to_restore)
108 109 110
        shas_to_restore.each_with_object([]) do |temp_branch, branches_created|
          branch_name = temp_branch.name
          sha = temp_branch.sha
111

112
          next if sha_exists?(sha)
113

114 115 116 117 118
          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}")
119 120 121 122 123 124 125 126 127 128 129 130 131
          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?

132
        raise e.message
133 134
      end

135 136 137 138 139 140 141 142 143
      # 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.
144
      def import_pull_requests
145 146 147 148 149 150
        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|
151
          restore_branches(batch) if recover_missing_commits
152 153 154 155 156 157 158

          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
159 160 161 162
          end
        end
      end

163
      def delete_temp_branches
164
        @temp_branches.each do |branch|
165
          begin
166 167
            client.delete_branch(project_key, repository_slug, branch.name, branch.sha)
            project.repository.delete_branch(branch.name)
168
          rescue BitbucketServer::Connection::ConnectionError => e
169
            @errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message }
170 171 172 173
          end
        end
      end

174 175 176 177 178 179 180 181 182
      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
S
Stan Hu 已提交
183
        author_id = gitlab_user_id(pull_request.author_email)
184

185 186 187 188 189 190 191 192 193 194 195 196 197
        project.merge_requests.find_by(iid: pull_request.iid)&.destroy

        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,
S
Stan Hu 已提交
198
          author_id: author_id,
199 200 201 202 203 204 205 206 207 208
          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

209
      def import_pull_request_comments(pull_request, merge_request)
210
        comments, other_activities = client.activities(project_key, repository_slug, pull_request.iid).partition(&:comment?)
211

212
        merge_event = other_activities.find(&:merge_event?)
213 214
        import_merge_event(merge_request, merge_event) if merge_event

215
        inline_comments, pr_comments = comments.partition(&:inline_comment?)
216

217
        import_inline_comments(inline_comments.map(&:comment), merge_request)
218
        import_standalone_pr_comments(pr_comments.map(&:comment), merge_request)
219 220
      end

221
      def import_merge_event(merge_request, merge_event)
222
        committer = merge_event.committer_email
223

S
Stan Hu 已提交
224
        user_id = gitlab_user_id(committer)
225
        timestamp = merge_event.merge_timestamp
226
        metric = MergeRequest::Metrics.find_or_initialize_by(merge_request: merge_request)
S
Stan Hu 已提交
227
        metric.update(merged_by_id: user_id, merged_at: timestamp)
228 229
      end

230
      def import_inline_comments(inline_comments, merge_request)
231
        inline_comments.each do |comment|
232
          position = build_position(merge_request, comment)
233
          parent = create_diff_note(merge_request, comment, position)
S
Stan Hu 已提交
234 235 236

          next unless parent&.persisted?

237 238
          discussion_id = parent.discussion_id

S
Stan Hu 已提交
239
          comment.comments.each do |reply|
240
            create_diff_note(merge_request, reply, position, discussion_id)
241 242 243 244
          end
        end
      end

245
      def create_diff_note(merge_request, comment, position, discussion_id = nil)
S
Stan Hu 已提交
246
        attributes = pull_request_comment_attributes(comment)
S
Stan Hu 已提交
247
        attributes.merge!(position: position, type: 'DiffNote')
248
        attributes[:discussion_id] = discussion_id if discussion_id
S
Stan Hu 已提交
249

250 251 252 253 254 255 256
        note = merge_request.notes.build(attributes)

        if note.valid?
          note.save
          return note
        end

S
Stan Hu 已提交
257 258 259
        # 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.
260
        create_fallback_diff_note(merge_request, comment)
S
Stan Hu 已提交
261 262 263 264 265
      rescue StandardError => e
        errors << { type: :pull_request, id: comment.id, errors: e.message }
        nil
      end

266 267
      def create_fallback_diff_note(merge_request, comment)
        attributes = pull_request_comment_attributes(comment)
268
        attributes[:note] = "*Comment on file: #{comment.file_path}, old line: #{comment.old_pos}, new line: #{comment.new_pos}*\n\n" + attributes[:note]
269 270 271 272

        merge_request.notes.create!(attributes)
      end

273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288
      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))
289 290 291 292

            comment.comments.each do |replies|
              merge_request.notes.create!(pull_request_comment_attributes(replies))
            end
293
          rescue StandardError => e
294
            errors << { type: :pull_request, iid: comment.id, errors: e.message }
295 296 297 298 299 300 301 302
          end
        end
      end

      def pull_request_comment_attributes(comment)
        {
          project: project,
          note: comment.note,
S
Stan Hu 已提交
303
          author_id: gitlab_user_id(comment.author_email),
304 305 306 307 308 309 310
          created_at: comment.created_at,
          updated_at: comment.updated_at
        }
      end
    end
  end
end