summaryrefslogtreecommitdiff
path: root/lib/gitlab/inline_diff.rb
blob: 44507bde25deaef0e8f16adab6ca7af2670c99d7 (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
module Gitlab
  class InlineDiff
    class << self

      START  = "#!idiff-start!#"
      FINISH = "#!idiff-finish!#"

      def processing(diff_arr)
        indexes = _indexes_of_changed_lines diff_arr

        indexes.each do |index|
          first_line = diff_arr[index+1]
          second_line = diff_arr[index+2]

          # Skip inline diff if empty line was replaced with content
          next if first_line == "-\n"

          first_token = find_first_token(first_line, second_line)
          apply_first_token(diff_arr, index, first_token)

          last_token = find_last_token(first_line, second_line, first_token)
          apply_last_token(diff_arr, index, last_token)
        end

        diff_arr
      end

      def apply_first_token(diff_arr, index, first_token)
        start = first_token + START

        if first_token.empty?
          # In case if we remove string of spaces in commit
          diff_arr[index+1].sub!("-", "-" => "-#{START}")
          diff_arr[index+2].sub!("+", "+" => "+#{START}")
        else
          diff_arr[index+1].sub!(first_token, first_token => start)
          diff_arr[index+2].sub!(first_token, first_token => start)
        end
      end

      def apply_last_token(diff_arr, index, last_token)
        # This is tricky: escape backslashes so that `sub` doesn't interpret them
        # as backreferences. Regexp.escape does NOT do the right thing.
        replace_token = FINISH + last_token.gsub(/\\/, '\&\&')
        diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
        diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
      end

      def find_first_token(first_line, second_line)
        max_length = [first_line.size, second_line.size].max
        first_the_same_symbols = 0

        (0..max_length + 1).each do |i|
          first_the_same_symbols = i - 1

          if first_line[i] != second_line[i] && i > 0
            break
          end
        end

        first_line[0..first_the_same_symbols][1..-1]
      end

      def find_last_token(first_line, second_line, first_token)
        max_length = [first_line.size, second_line.size].max
        last_the_same_symbols = 0

        (1..max_length + 1).each do |i|
          last_the_same_symbols = -i
          shortest_line = second_line.size > first_line.size ? first_line : second_line

          if (first_line[-i] != second_line[-i]) || "#{first_token}#{START}".size == shortest_line[1..-i].size
            break
          end
        end

        last_the_same_symbols += 1
        first_line[last_the_same_symbols..-1]
      end

      def _indexes_of_changed_lines(diff_arr)
        chain_of_first_symbols = ""
        diff_arr.each_with_index do |line, i|
          chain_of_first_symbols += line[0]
        end
        chain_of_first_symbols.gsub!(/[^\-\+]/, "#")

        offset = 0
        indexes = []
        while index = chain_of_first_symbols.index("#-+#", offset)
          indexes << index
          offset = index + 1
        end
        indexes
      end

      def replace_markers(line)
        line.gsub!(START, "<span class='idiff'>")
        line.gsub!(FINISH, "</span>")
        line
      end
    end
  end
end