Welcome to mirror list, hosted at ThFree Co, Russian Federation.

inline_diff.rb « diff « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: cf769262958e5d5b802024349788afc047fe489d (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
119
120
121
122
123
124
125
126
127
128
129
130
# frozen_string_literal: true

module Gitlab
  module Diff
    class InlineDiff
      # Regex to find a run of deleted lines followed by the same number of added lines
      LINE_PAIRS_PATTERN = %r{
        # Runs start at the beginning of the string (the first line) or after a space (for an unchanged line)
        (?:\A|\s)

        # This matches a number of `-`s followed by the same number of `+`s through recursion
        (?<del_ins>
          -
          \g<del_ins>?
          \+
        )

        # Runs end at the end of the string (the last line) or before a space (for an unchanged line)
        (?=\s|\z)
      }x.freeze

      attr_accessor :old_line, :new_line, :offset

      def initialize(old_line, new_line, offset: 0)
        @old_line = old_line[offset..-1]
        @new_line = new_line[offset..-1]
        @offset = offset
      end

      def inline_diffs(project: nil)
        # Skip inline diff if empty line was replaced with content
        return if old_line == ""

        if Feature.enabled?(:improved_merge_diff_highlighting, project, default_enabled: :yaml)
          CharDiff.new(old_line, new_line).changed_ranges(offset: offset)
        else
          deprecated_diff
        end
      end

      class << self
        def for_lines(lines, project: nil)
          changed_line_pairs = find_changed_line_pairs(lines)

          inline_diffs = []

          changed_line_pairs.each do |old_index, new_index|
            old_line = lines[old_index]
            new_line = lines[new_index]

            old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs(project: project)

            inline_diffs[old_index] = old_diffs
            inline_diffs[new_index] = new_diffs
          end

          inline_diffs
        end

        private

        # Finds pairs of old/new line pairs that represent the same line that changed
        # rubocop: disable CodeReuse/ActiveRecord
        def find_changed_line_pairs(lines)
          # Prefixes of all diff lines, indicating their types
          # For example: `" - +  -+  ---+++ --+  -++"`
          line_prefixes = lines.each_with_object(+"") { |line, s| s << (line[0] || ' ') }.gsub(/[^ +-]/, ' ')

          changed_line_pairs = []
          line_prefixes.scan(LINE_PAIRS_PATTERN) do
            # For `"---+++"`, `begin_index == 0`, `end_index == 6`
            begin_index, end_index = Regexp.last_match.offset(:del_ins)

            # For `"---+++"`, `changed_line_count == 3`
            changed_line_count = (end_index - begin_index) / 2

            halfway_index = begin_index + changed_line_count
            (begin_index...halfway_index).each do |i|
              # For `"---+++"`, index 1 maps to 1 + 3 = 4
              changed_line_pairs << [i, i + changed_line_count]
            end
          end

          changed_line_pairs
        end
        # rubocop: enable CodeReuse/ActiveRecord
      end

      private

      # See: https://gitlab.com/gitlab-org/gitlab/-/issues/299884
      def deprecated_diff
        lcp = longest_common_prefix(old_line, new_line)
        lcs = longest_common_suffix(old_line[lcp..-1], new_line[lcp..-1])

        lcp += offset
        old_length = old_line.length + offset
        new_length = new_line.length + offset

        old_diff_range = lcp..(old_length - lcs - 1)
        new_diff_range = lcp..(new_length - lcs - 1)

        old_diffs = [old_diff_range] if old_diff_range.begin <= old_diff_range.end
        new_diffs = [new_diff_range] if new_diff_range.begin <= new_diff_range.end

        [old_diffs, new_diffs]
      end

      def longest_common_prefix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
        max_length = [a.length, b.length].max

        length = 0
        (0..max_length - 1).each do |pos|
          old_char = a[pos]
          new_char = b[pos]

          break if old_char != new_char

          length += 1
        end

        length
      end

      def longest_common_suffix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
        longest_common_prefix(a.reverse, b.reverse)
      end
    end
  end
end