1# frozen_string_literal: true
2
3module Gitlab
4  module Git
5    module Conflict
6      class File
7        UnsupportedEncoding = Class.new(StandardError)
8
9        attr_reader :ancestor_path, :their_path, :our_path, :our_mode, :repository, :commit_oid
10
11        attr_accessor :raw_content
12
13        def initialize(repository, commit_oid, conflict, raw_content)
14          @repository = repository
15          @commit_oid = commit_oid
16          @ancestor_path = conflict[:ancestor][:path]
17          @their_path = conflict[:theirs][:path]
18          @our_path = conflict[:ours][:path]
19          @our_mode = conflict[:ours][:mode]
20          @raw_content = raw_content
21        end
22
23        def lines
24          return @lines if defined?(@lines)
25
26          begin
27            @type = 'text'
28            @lines = Gitlab::Git::Conflict::Parser.parse(content,
29                                                         our_path: our_path,
30                                                         their_path: their_path)
31          rescue Gitlab::Git::Conflict::Parser::ParserError
32            @type = 'text-editor'
33            @lines = nil
34          end
35        end
36
37        def content
38          @content ||= @raw_content.dup.force_encoding('UTF-8')
39
40          raise UnsupportedEncoding unless @content.valid_encoding?
41
42          @content
43        end
44
45        def type
46          lines unless @type
47
48          @type.inquiry
49        end
50
51        def our_blob
52          # REFACTOR NOTE: the source of `commit_oid` used to be
53          # `merge_request.diff_refs.head_sha`. Instead of passing this value
54          # around the new lib structure, I decided to use `@commit_oid` which is
55          # equivalent to `merge_request.source_branch_head.raw.rugged_commit.oid`.
56          # That is what `merge_request.diff_refs.head_sha` is equivalent to when
57          # `merge_request` is not persisted (see `MergeRequest#diff_head_commit`).
58          # I think using the same oid is more consistent anyways, but if Conflicts
59          # start breaking, the change described above is a good place to look at.
60          @our_blob ||= repository.blob_at(@commit_oid, our_path)
61        end
62
63        def line_code(line)
64          Gitlab::Git.diff_line_code(our_path, line[:line_new], line[:line_old])
65        end
66
67        def resolve_lines(resolution)
68          section_id = nil
69
70          lines.map do |line|
71            unless line[:type]
72              section_id = nil
73              next line
74            end
75
76            section_id ||= line_code(line)
77
78            case resolution[section_id]
79            when 'head'
80              next unless line[:type] == 'new'
81            when 'origin'
82              next unless line[:type] == 'old'
83            else
84              raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Missing resolution for section ID: #{section_id}"
85            end
86
87            line
88          end.compact
89        end
90
91        def resolve_content(resolution)
92          if resolution == content
93            raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Resolved content has no changes for file #{our_path}"
94          end
95
96          resolution
97        end
98
99        def path
100          # There are conflict scenarios (e.g. file is removed on source) wherein
101          # our_path will be blank/nil. Since we are indexing them by path in
102          # `#conflicts` helper and we want to match the diff file to a conflict
103          # in `DiffFileEntity#highlighted_diff_lines`, we need to fallback to
104          # their_path (this is the path on target).
105          our_path.presence || their_path
106        end
107      end
108    end
109  end
110end
111