BigW Consortium Gitlab

file.rb 7.89 KB
Newer Older
1 2 3
module Gitlab
  module Conflict
    class File
4
      include Gitlab::Routing
5
      include IconsHelper
6

7
      MissingResolution = Class.new(ResolutionError)
8

9 10
      CONTEXT_LINES = 3

11
      attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
12

13
      def initialize(merge_file_result, conflict, merge_request:)
14
        @merge_file_result = merge_file_result
15 16
        @their_path = conflict[:theirs][:path]
        @our_path = conflict[:ours][:path]
17
        @our_mode = conflict[:ours][:mode]
18 19
        @merge_request = merge_request
        @repository = merge_request.project.repository
20
        @match_line_headers = {}
21 22
      end

23 24 25 26
      def content
        merge_file_result[:data]
      end

27 28 29 30
      def our_blob
        @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path)
      end

31 32 33 34 35 36
      def type
        lines unless @type

        @type.inquiry
      end

37 38
      # Array of Gitlab::Diff::Line objects
      def lines
39 40 41 42 43
        return @lines if defined?(@lines)

        begin
          @type = 'text'
          @lines = Gitlab::Conflict::Parser.new.parse(content,
44
                                                      our_path: our_path,
45
                                                      their_path: their_path,
Sean McGivern committed
46
                                                      parent_file: self)
47 48 49 50
        rescue Gitlab::Conflict::Parser::ParserError
          @type = 'text-editor'
          @lines = nil
        end
51 52
      end

53
      def resolve_lines(resolution)
54
        section_id = nil
55 56 57

        lines.map do |line|
          unless line.type
58
            section_id = nil
59 60 61
            next line
          end

62
          section_id ||= line_code(line)
63

64
          case resolution[section_id]
65
          when 'head'
66
            next unless line.type == 'new'
67
          when 'origin'
68 69
            next unless line.type == 'old'
          else
70
            raise MissingResolution, "Missing resolution for section ID: #{section_id}"
71 72 73 74 75 76
          end

          line
        end.compact
      end

77 78 79 80 81 82 83 84
      def resolve_content(resolution)
        if resolution == content
          raise MissingResolution, "Resolved content has no changes for file #{our_path}"
        end

        resolution
      end

85
      def highlight_lines!
86 87 88
        their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
        our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")

Sean McGivern committed
89 90
        their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
        our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
91

92
        lines.each do |line|
Douwe Maan committed
93 94 95 96 97 98
          line.rich_text =
            if line.type == 'old'
              their_highlight[line.old_line - 1].try(:html_safe)
            else
              our_highlight[line.new_line - 1].try(:html_safe)
            end
99 100 101 102 103 104
        end
      end

      def sections
        return @sections if @sections

105
        chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
106 107
        match_line = nil

108 109
        sections_count = chunked_lines.size

110 111 112
        @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
          section = nil

113 114
          # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
          # always shown in full.
115 116
          if no_conflict
            conflict_before = i > 0
117
            conflict_after = (sections_count - i) > 1
118 119

            if conflict_before && conflict_after
120
              # Create a gap in a long context section.
121
              if lines.length > CONTEXT_LINES * 2
122
                head_lines = lines.first(CONTEXT_LINES)
123
                tail_lines = lines.last(CONTEXT_LINES)
124

125 126
                # Ensure any existing match line has text for all lines up to the last
                # line of its context.
127
                update_match_line_text(match_line, head_lines.last)
128

129
                # Insert a new match line after the created gap.
130
                match_line = create_match_line(tail_lines.first)
131 132

                section = [
133
                  { conflict: false, lines: head_lines },
134 135 136 137
                  { conflict: false, lines: tail_lines.unshift(match_line) }
                ]
              end
            elsif conflict_after
138 139
              tail_lines = lines.last(CONTEXT_LINES)

140 141
              # Create a gap and insert a match line at the start.
              if lines.length > tail_lines.length
142 143 144 145 146 147
                match_line = create_match_line(tail_lines.first)

                tail_lines.unshift(match_line)
              end

              lines = tail_lines
148
            elsif conflict_before
149 150
              # We're at the end of the file (no conflicts after), so just remove extra
              # trailing lines.
151 152 153 154
              lines = lines.first(CONTEXT_LINES)
            end
          end

155 156
          # We want to update the match line's text every time unless we've already
          # created a gap and its corresponding match line.
157
          update_match_line_text(match_line, lines.last) unless section
158

159 160 161
          section ||= { conflict: !no_conflict, lines: lines }
          section[:id] = line_code(lines.first) unless no_conflict
          section
162 163 164
        end
      end

165 166 167 168
      def line_code(line)
        Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
      end

169 170 171 172
      def create_match_line(line)
        Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
      end

173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
      # Any line beginning with a letter, an underscore, or a dollar can be used in a
      # match line header. Only context sections can contain match lines, as match lines
      # have to exist in both versions of the file.
      def find_match_line_header(index)
        return @match_line_headers[index] if @match_line_headers.key?(index)

        @match_line_headers[index] = begin
          if index >= 0
            line = lines[index]

            if line.type.nil? && line.text.match(/\A[A-Za-z$_]/)
              " #{line.text}"
            else
              find_match_line_header(index - 1)
            end
          end
        end
      end

192 193 194
      # Set the match line's text for the current line. A match line takes its start
      # position and context header (where present) from itself, and its end position from
      # the line passed in.
195
      def update_match_line_text(match_line, line)
196 197
        return unless match_line

198
        header = find_match_line_header(match_line.index - 1)
199 200 201 202

        match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
      end

203 204
      def as_json(opts = {})
        json_hash = {
205 206
          old_path: their_path,
          new_path: our_path,
207
          blob_icon: file_type_icon_class('file', our_mode, our_path),
208
          blob_path: project_blob_path(merge_request.project, ::File.join(merge_request.diff_refs.head_sha, our_path))
209
        }
210

211 212 213
        json_hash.tap do |json_hash|
          if opts[:full_content]
            json_hash[:content] = content
214
            json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode)
215 216 217 218 219
          else
            json_hash[:sections] = sections if type.text?
            json_hash[:type] = type
            json_hash[:content_path] = content_path
          end
220 221 222 223
        end
      end

      def content_path
224 225 226 227
        conflict_for_path_project_merge_request_path(merge_request.project,
                                                     merge_request,
                                                     old_path: their_path,
                                                     new_path: our_path)
228
      end
229 230 231

      # Don't try to print merge_request or repository.
      def inspect
232
        instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable|
233 234 235 236 237 238 239
          value = instance_variable_get("@#{instance_variable}")

          "#{instance_variable}=\"#{value}\""
        end

        "#<#{self.class} #{instance_variables.join(' ')}>"
      end
240 241 242
    end
  end
end