summaryrefslogtreecommitdiff
path: root/lib/gitlab/git/conflict/resolver.rb
blob: c3cb0264112d7875c7431b0619136a2f6f305a3f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
module Gitlab
  module Git
    module Conflict
      class Resolver
        ConflictSideMissing = Class.new(StandardError)
        ResolutionError = Class.new(StandardError)

        def initialize(target_repository, our_commit_oid, their_commit_oid)
          @target_repository = target_repository
          @our_commit_oid = our_commit_oid
          @their_commit_oid = their_commit_oid
        end

        def conflicts
          @conflicts ||= begin
            @target_repository.gitaly_migrate(:conflicts_list_conflict_files) do |is_enabled|
              if is_enabled
                gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
              else
                rugged_list_conflict_files
              end
            end
          end
        rescue GRPC::FailedPrecondition => e
          raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message)
        rescue Rugged::ReferenceError, Rugged::OdbError, GRPC::BadStatus => e
          raise Gitlab::Git::CommandError.new(e)
        end

        def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:)
          source_repository.gitaly_migrate(:conflicts_resolve_conflicts) do |is_enabled|
            if is_enabled
              gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
            else
              rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
            end
          end
        end

        def conflict_for_path(conflicts, old_path, new_path)
          conflicts.find do |conflict|
            conflict.their_path == old_path && conflict.our_path == new_path
          end
        end

        private

        def conflict_files(repository, index)
          index.conflicts.map do |conflict|
            raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]

            Gitlab::Git::Conflict::File.new(
              repository,
              @our_commit_oid,
              conflict,
              index.merge_file(conflict[:ours][:path])[:data]
            )
          end
        end

        def gitaly_conflicts_client(repository)
          repository.gitaly_conflicts_client(@our_commit_oid, @their_commit_oid)
        end

        def write_resolved_file_to_index(repository, index, file, params)
          if params[:sections]
            resolved_lines = file.resolve_lines(params[:sections])
            new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")

            new_file << "\n" if file.our_blob.data.end_with?("\n")
          elsif params[:content]
            new_file = file.resolve_content(params[:content])
          end

          our_path = file.our_path

          oid = repository.rugged.write(new_file, :blob)
          index.add(path: our_path, oid: oid, mode: file.our_mode)
          index.conflict_remove(our_path)
        end

        def rugged_list_conflict_files
          target_index = @target_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)

          # We don't need to do `with_repo_branch_commit` here, because the target
          # project always fetches source refs when creating merge request diffs.
          conflict_files(@target_repository, target_index)
        end

        def rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
          source_repository.with_repo_branch_commit(@target_repository, target_branch) do
            index = source_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
            conflicts = conflict_files(source_repository, index)

            resolution.files.each do |file_params|
              conflict_file = conflict_for_path(conflicts, file_params[:old_path], file_params[:new_path])

              write_resolved_file_to_index(source_repository, index, conflict_file, file_params)
            end

            unless index.conflicts.empty?
              missing_files = index.conflicts.map { |file| file[:ours][:path] }

              raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
            end

            commit_params = {
              message: resolution.commit_message,
              parents: [@our_commit_oid, @their_commit_oid]
            }

            source_repository.commit_index(resolution.user, source_branch, index, commit_params)
          end
        end
      end
    end
  end
end