290 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			290 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| module Gitlab
 | |
|   module Conflict
 | |
|     class File
 | |
|       include Gitlab::Routing
 | |
|       include IconsHelper
 | |
|       include Gitlab::Utils::StrongMemoize
 | |
| 
 | |
|       CONTEXT_LINES = 3
 | |
| 
 | |
|       CONFLICT_MARKER_OUR = 'conflict_marker_our'
 | |
|       CONFLICT_MARKER_THEIR = 'conflict_marker_their'
 | |
|       CONFLICT_MARKER_SEPARATOR = 'conflict_marker'
 | |
| 
 | |
|       CONFLICT_TYPES = {
 | |
|         "old" => "conflict_their",
 | |
|         "new" => "conflict_our"
 | |
|       }.freeze
 | |
| 
 | |
|       attr_reader :merge_request
 | |
| 
 | |
|       # 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
 | |
|       attr_reader :raw
 | |
| 
 | |
|       delegate :type, :content, :path, :ancestor_path, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw
 | |
| 
 | |
|       def initialize(raw, merge_request:)
 | |
|         @raw = raw
 | |
|         @merge_request = merge_request
 | |
|         @match_line_headers = {}
 | |
|       end
 | |
| 
 | |
|       def lines
 | |
|         return @lines if defined?(@lines)
 | |
| 
 | |
|         @lines = raw.lines.nil? ? nil : map_raw_lines(raw.lines)
 | |
|       end
 | |
| 
 | |
|       def resolve_lines(resolution)
 | |
|         map_raw_lines(raw.resolve_lines(resolution))
 | |
|       end
 | |
| 
 | |
|       def highlight_lines!
 | |
|         their_highlight = Gitlab::Highlight.highlight(their_path, their_lines, language: their_language).lines
 | |
|         our_highlight = Gitlab::Highlight.highlight(our_path, our_lines, language: our_language).lines
 | |
| 
 | |
|         lines.each do |line|
 | |
|           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
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def diff_lines_for_serializer
 | |
|         # calculate sections and highlight lines before changing types
 | |
|         sections && highlight_lines!
 | |
| 
 | |
|         sections.flat_map do |section|
 | |
|           if section[:conflict]
 | |
|             lines = []
 | |
| 
 | |
|             lines << create_separator_line(section[:lines].first, CONFLICT_MARKER_OUR)
 | |
| 
 | |
|             current_type = section[:lines].first.type
 | |
|             section[:lines].each do |line|
 | |
|               if line.type != current_type # insert a separator between our changes and theirs
 | |
|                 lines << create_separator_line(line, CONFLICT_MARKER_SEPARATOR)
 | |
|                 current_type = line.type
 | |
|               end
 | |
| 
 | |
|               line.type = CONFLICT_TYPES[line.type]
 | |
| 
 | |
|               # Swap the positions around due to conflicts/diffs display inconsistency
 | |
|               # https://gitlab.com/gitlab-org/gitlab/-/issues/291989
 | |
|               line.old_pos, line.new_pos = line.new_pos, line.old_pos
 | |
| 
 | |
|               lines << line
 | |
|             end
 | |
| 
 | |
|             lines << create_separator_line(lines.last, CONFLICT_MARKER_THEIR)
 | |
| 
 | |
|             lines
 | |
|           else
 | |
|             section[:lines]
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def sections
 | |
|         return @sections if @sections
 | |
| 
 | |
|         chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
 | |
|         match_line = nil
 | |
| 
 | |
|         sections_count = chunked_lines.size
 | |
| 
 | |
|         @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
 | |
|           section = nil
 | |
| 
 | |
|           # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
 | |
|           # always shown in full.
 | |
|           if no_conflict
 | |
|             conflict_before = i > 0
 | |
|             conflict_after = (sections_count - i) > 1
 | |
| 
 | |
|             if conflict_before && conflict_after
 | |
|               # Create a gap in a long context section.
 | |
|               if lines.length > CONTEXT_LINES * 2
 | |
|                 head_lines = lines.first(CONTEXT_LINES)
 | |
|                 tail_lines = lines.last(CONTEXT_LINES)
 | |
| 
 | |
|                 # Ensure any existing match line has text for all lines up to the last
 | |
|                 # line of its context.
 | |
|                 update_match_line_text(match_line, head_lines.last)
 | |
| 
 | |
|                 # Insert a new match line after the created gap.
 | |
|                 match_line = create_match_line(tail_lines.first)
 | |
| 
 | |
|                 section = [
 | |
|                   { conflict: false, lines: head_lines },
 | |
|                   { conflict: false, lines: tail_lines.unshift(match_line) }
 | |
|                 ]
 | |
|               end
 | |
|             elsif conflict_after
 | |
|               tail_lines = lines.last(CONTEXT_LINES)
 | |
| 
 | |
|               # Create a gap and insert a match line at the start.
 | |
|               if lines.length > tail_lines.length
 | |
|                 match_line = create_match_line(tail_lines.first)
 | |
| 
 | |
|                 tail_lines.unshift(match_line)
 | |
|               end
 | |
| 
 | |
|               lines = tail_lines
 | |
|             elsif conflict_before
 | |
|               # We're at the end of the file (no conflicts after)
 | |
|               number_of_trailing_lines = lines.size
 | |
| 
 | |
|               # Remove extra trailing lines
 | |
|               lines = lines.first(CONTEXT_LINES)
 | |
| 
 | |
|               if number_of_trailing_lines > CONTEXT_LINES
 | |
|                 lines << create_match_line(lines.last)
 | |
|               end
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           # We want to update the match line's text every time unless we've already
 | |
|           # created a gap and its corresponding match line.
 | |
|           update_match_line_text(match_line, lines.last) unless section
 | |
| 
 | |
|           section ||= { conflict: !no_conflict, lines: lines }
 | |
|           section[:id] = line_code(lines.first) unless no_conflict
 | |
|           section
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def line_code(line)
 | |
|         Gitlab::Git.diff_line_code(our_path, line.new_pos, line.old_pos)
 | |
|       end
 | |
| 
 | |
|       def create_match_line(line)
 | |
|         Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
 | |
|       end
 | |
| 
 | |
|       def create_separator_line(line, type)
 | |
|         Gitlab::Diff::Line.new('', type, line.index, nil, nil)
 | |
|       end
 | |
| 
 | |
|       # 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
 | |
| 
 | |
|       # 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.
 | |
|       def update_match_line_text(match_line, line)
 | |
|         return unless match_line
 | |
| 
 | |
|         header = find_match_line_header(match_line.index - 1)
 | |
| 
 | |
|         match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
 | |
|       end
 | |
| 
 | |
|       def as_json(opts = {})
 | |
|         json_hash = {
 | |
|           old_path: their_path,
 | |
|           new_path: our_path,
 | |
|           blob_icon: file_type_icon_class('file', our_mode, our_path),
 | |
|           blob_path: project_blob_path(merge_request.project, ::File.join(merge_request.diff_refs.head_sha, our_path))
 | |
|         }
 | |
| 
 | |
|         json_hash.tap do |json_hash|
 | |
|           if opts[:full_content]
 | |
|             json_hash[:content] = content
 | |
|           else
 | |
|             json_hash[:sections] = sections if type.text?
 | |
|             json_hash[:type] = type
 | |
|             json_hash[:content_path] = content_path
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def content_path
 | |
|         conflict_for_path_project_merge_request_path(merge_request.project,
 | |
|                                                      merge_request,
 | |
|                                                      old_path: their_path,
 | |
|                                                      new_path: our_path)
 | |
|       end
 | |
| 
 | |
|       def conflict_type(diff_file)
 | |
|         if ancestor_path.present?
 | |
|           if our_path.present? && their_path.present?
 | |
|             :both_modified
 | |
|           elsif their_path.blank?
 | |
|             :modified_source_removed_target
 | |
|           else
 | |
|             :modified_target_removed_source
 | |
|           end
 | |
|         else
 | |
|           if our_path.present? && their_path.present?
 | |
|             :both_added
 | |
|           elsif their_path.blank?
 | |
|             diff_file.renamed_file? ? :renamed_same_file : :removed_target_renamed_source
 | |
|           else
 | |
|             :removed_source_renamed_target
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       private
 | |
| 
 | |
|       def map_raw_lines(raw_lines)
 | |
|         raw_lines.map do |raw_line|
 | |
|           Gitlab::Diff::Line.new(raw_line[:full_line], raw_line[:type],
 | |
|             raw_line[:line_obj_index], raw_line[:line_old],
 | |
|             raw_line[:line_new], parent_file: self)
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def their_language
 | |
|         strong_memoize(:their_language) do
 | |
|           repository.gitattribute(their_path, 'gitlab-language')
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def our_language
 | |
|         strong_memoize(:our_language) do
 | |
|           if our_path == their_path
 | |
|             their_language
 | |
|           else
 | |
|             repository.gitattribute(our_path, 'gitlab-language')
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def their_lines
 | |
|         strong_memoize(:their_lines) do
 | |
|           lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def our_lines
 | |
|         strong_memoize(:our_lines) do
 | |
|           lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
 | |
|         end
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |