To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / lib / redmine / unified_diff.rb @ 1568:bc47b68a9487

History | View | Annotate | Download (8.33 KB)

1 441:cbce1fd3b1b7 Chris
# Redmine - project management software
2 1494:e248c7af89ec Chris
# Copyright (C) 2006-2014  Jean-Philippe Lang
3 0:513646585e45 Chris
#
4
# This program is free software; you can redistribute it and/or
5
# modify it under the terms of the GNU General Public License
6
# as published by the Free Software Foundation; either version 2
7
# of the License, or (at your option) any later version.
8 909:cbb26bc654de Chris
#
9 0:513646585e45 Chris
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13 909:cbb26bc654de Chris
#
14 0:513646585e45 Chris
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17
18
module Redmine
19
  # Class used to parse unified diffs
20 441:cbce1fd3b1b7 Chris
  class UnifiedDiff < Array
21 1115:433d4f72a19b Chris
    attr_reader :diff_type, :diff_style
22 909:cbb26bc654de Chris
23 0:513646585e45 Chris
    def initialize(diff, options={})
24 1115:433d4f72a19b Chris
      options.assert_valid_keys(:type, :style, :max_lines)
25 0:513646585e45 Chris
      diff = diff.split("\n") if diff.is_a?(String)
26 441:cbce1fd3b1b7 Chris
      @diff_type = options[:type] || 'inline'
27 1115:433d4f72a19b Chris
      @diff_style = options[:style]
28 0:513646585e45 Chris
      lines = 0
29
      @truncated = false
30 1115:433d4f72a19b Chris
      diff_table = DiffTable.new(diff_type, diff_style)
31 1464:261b3d9a4903 Chris
      diff.each do |line_raw|
32
        line = Redmine::CodesetUtil.to_utf8_by_setting(line_raw)
33
        unless diff_table.add_line(line)
34 245:051f544170fe Chris
          self << diff_table if diff_table.length > 0
35 1115:433d4f72a19b Chris
          diff_table = DiffTable.new(diff_type, diff_style)
36 0:513646585e45 Chris
        end
37
        lines += 1
38
        if options[:max_lines] && lines > options[:max_lines]
39
          @truncated = true
40
          break
41
        end
42
      end
43
      self << diff_table unless diff_table.empty?
44
      self
45
    end
46 245:051f544170fe Chris
47 0:513646585e45 Chris
    def truncated?; @truncated; end
48
  end
49
50
  # Class that represents a file diff
51 909:cbb26bc654de Chris
  class DiffTable < Array
52 441:cbce1fd3b1b7 Chris
    attr_reader :file_name
53 0:513646585e45 Chris
54
    # Initialize with a Diff file and the type of Diff View
55
    # The type view must be inline or sbs (side_by_side)
56 1115:433d4f72a19b Chris
    def initialize(type="inline", style=nil)
57 0:513646585e45 Chris
      @parsing = false
58 441:cbce1fd3b1b7 Chris
      @added = 0
59
      @removed = 0
60 0:513646585e45 Chris
      @type = type
61 1115:433d4f72a19b Chris
      @style = style
62
      @file_name = nil
63
      @git_diff = false
64 0:513646585e45 Chris
    end
65
66
    # Function for add a line of this Diff
67
    # Returns false when the diff ends
68
    def add_line(line)
69
      unless @parsing
70
        if line =~ /^(---|\+\+\+) (.*)$/
71 1115:433d4f72a19b Chris
          self.file_name = $2
72 0:513646585e45 Chris
        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
73
          @line_num_l = $2.to_i
74
          @line_num_r = $5.to_i
75
          @parsing = true
76
        end
77
      else
78 1464:261b3d9a4903 Chris
        if line =~ %r{^[^\+\-\s@\\]}
79 0:513646585e45 Chris
          @parsing = false
80
          return false
81
        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
82
          @line_num_l = $2.to_i
83
          @line_num_r = $5.to_i
84
        else
85 909:cbb26bc654de Chris
          parse_line(line, @type)
86 0:513646585e45 Chris
        end
87
      end
88
      return true
89
    end
90 909:cbb26bc654de Chris
91 441:cbce1fd3b1b7 Chris
    def each_line
92
      prev_line_left, prev_line_right = nil, nil
93
      each do |line|
94
        spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1)
95
        yield spacing, line
96
        prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0
97
        prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0
98
      end
99
    end
100 0:513646585e45 Chris
101
    def inspect
102
      puts '### DIFF TABLE ###'
103
      puts "file : #{file_name}"
104
      self.each do |d|
105
        d.inspect
106
      end
107
    end
108
109 441:cbce1fd3b1b7 Chris
    private
110 0:513646585e45 Chris
111 1115:433d4f72a19b Chris
    def file_name=(arg)
112
      both_git_diff = false
113
      if file_name.nil?
114
        @git_diff = true if arg =~ %r{^(a/|/dev/null)}
115
      else
116
        both_git_diff = (@git_diff && arg =~ %r{^(b/|/dev/null)})
117
      end
118
      if both_git_diff
119
        if file_name && arg == "/dev/null"
120
          # keep the original file name
121
          @file_name = file_name.sub(%r{^a/}, '')
122
        else
123
          # remove leading b/
124
          @file_name = arg.sub(%r{^b/}, '')
125
        end
126
      elsif @style == "Subversion"
127
        # removing trailing "(revision nn)"
128
        @file_name = arg.sub(%r{\t+\(.*\)$}, '')
129
      else
130
        @file_name = arg
131
      end
132
    end
133
134 441:cbce1fd3b1b7 Chris
    def diff_for_added_line
135
      if @type == 'sbs' && @removed > 0 && @added < @removed
136
        self[-(@removed - @added)]
137
      else
138
        diff = Diff.new
139
        self << diff
140
        diff
141
      end
142
    end
143 0:513646585e45 Chris
144
    def parse_line(line, type="inline")
145
      if line[0, 1] == "+"
146 441:cbce1fd3b1b7 Chris
        diff = diff_for_added_line
147 929:5f33065ddc4b Chris
        diff.line_right = line[1..-1]
148 0:513646585e45 Chris
        diff.nb_line_right = @line_num_r
149
        diff.type_diff_right = 'diff_in'
150
        @line_num_r += 1
151 441:cbce1fd3b1b7 Chris
        @added += 1
152 0:513646585e45 Chris
        true
153
      elsif line[0, 1] == "-"
154 441:cbce1fd3b1b7 Chris
        diff = Diff.new
155 929:5f33065ddc4b Chris
        diff.line_left = line[1..-1]
156 0:513646585e45 Chris
        diff.nb_line_left = @line_num_l
157
        diff.type_diff_left = 'diff_out'
158 441:cbce1fd3b1b7 Chris
        self << diff
159 0:513646585e45 Chris
        @line_num_l += 1
160 441:cbce1fd3b1b7 Chris
        @removed += 1
161 0:513646585e45 Chris
        true
162 441:cbce1fd3b1b7 Chris
      else
163
        write_offsets
164
        if line[0, 1] =~ /\s/
165
          diff = Diff.new
166 929:5f33065ddc4b Chris
          diff.line_right = line[1..-1]
167 441:cbce1fd3b1b7 Chris
          diff.nb_line_right = @line_num_r
168 929:5f33065ddc4b Chris
          diff.line_left = line[1..-1]
169 441:cbce1fd3b1b7 Chris
          diff.nb_line_left = @line_num_l
170
          self << diff
171
          @line_num_l += 1
172
          @line_num_r += 1
173
          true
174
        elsif line[0, 1] = "\\"
175 0:513646585e45 Chris
          true
176
        else
177
          false
178
        end
179
      end
180
    end
181 909:cbb26bc654de Chris
182 441:cbce1fd3b1b7 Chris
    def write_offsets
183
      if @added > 0 && @added == @removed
184
        @added.times do |i|
185
          line = self[-(1 + i)]
186
          removed = (@type == 'sbs') ? line : self[-(1 + @added + i)]
187
          offsets = offsets(removed.line_left, line.line_right)
188
          removed.offsets = line.offsets = offsets
189
        end
190
      end
191
      @added = 0
192
      @removed = 0
193
    end
194 909:cbb26bc654de Chris
195 441:cbce1fd3b1b7 Chris
    def offsets(line_left, line_right)
196
      if line_left.present? && line_right.present? && line_left != line_right
197
        max = [line_left.size, line_right.size].min
198
        starting = 0
199
        while starting < max && line_left[starting] == line_right[starting]
200
          starting += 1
201
        end
202 1464:261b3d9a4903 Chris
        if (! "".respond_to?(:force_encoding)) && starting < line_left.size
203
          while line_left[starting].ord.between?(128, 191) && starting > 0
204
            starting -= 1
205
          end
206
        end
207 441:cbce1fd3b1b7 Chris
        ending = -1
208 1464:261b3d9a4903 Chris
        while ending >= -(max - starting) && (line_left[ending] == line_right[ending])
209 441:cbce1fd3b1b7 Chris
          ending -= 1
210
        end
211 1464:261b3d9a4903 Chris
        if (! "".respond_to?(:force_encoding)) && ending > (-1 * line_left.size)
212
          while line_left[ending].ord.between?(128, 255) && ending < -1
213
            if line_left[ending].ord.between?(128, 191)
214
              if line_left[ending + 1].ord.between?(128, 191)
215
                ending += 1
216
              else
217
                break
218
              end
219
            else
220
              ending += 1
221
            end
222
          end
223
        end
224 441:cbce1fd3b1b7 Chris
        unless starting == 0 && ending == -1
225
          [starting, ending]
226
        end
227
      end
228
    end
229
  end
230 0:513646585e45 Chris
231
  # A line of diff
232 909:cbb26bc654de Chris
  class Diff
233 0:513646585e45 Chris
    attr_accessor :nb_line_left
234
    attr_accessor :line_left
235
    attr_accessor :nb_line_right
236
    attr_accessor :line_right
237
    attr_accessor :type_diff_right
238
    attr_accessor :type_diff_left
239 441:cbce1fd3b1b7 Chris
    attr_accessor :offsets
240 909:cbb26bc654de Chris
241 0:513646585e45 Chris
    def initialize()
242
      self.nb_line_left = ''
243
      self.nb_line_right = ''
244
      self.line_left = ''
245
      self.line_right = ''
246
      self.type_diff_right = ''
247
      self.type_diff_left = ''
248
    end
249 909:cbb26bc654de Chris
250 441:cbce1fd3b1b7 Chris
    def type_diff
251
      type_diff_right == 'diff_in' ? type_diff_right : type_diff_left
252
    end
253 909:cbb26bc654de Chris
254 441:cbce1fd3b1b7 Chris
    def line
255
      type_diff_right == 'diff_in' ? line_right : line_left
256
    end
257 909:cbb26bc654de Chris
258 441:cbce1fd3b1b7 Chris
    def html_line_left
259 929:5f33065ddc4b Chris
      line_to_html(line_left, offsets)
260 441:cbce1fd3b1b7 Chris
    end
261 909:cbb26bc654de Chris
262 441:cbce1fd3b1b7 Chris
    def html_line_right
263 929:5f33065ddc4b Chris
      line_to_html(line_right, offsets)
264 441:cbce1fd3b1b7 Chris
    end
265 909:cbb26bc654de Chris
266 441:cbce1fd3b1b7 Chris
    def html_line
267 929:5f33065ddc4b Chris
      line_to_html(line, offsets)
268 441:cbce1fd3b1b7 Chris
    end
269 0:513646585e45 Chris
270
    def inspect
271
      puts '### Start Line Diff ###'
272
      puts self.nb_line_left
273
      puts self.line_left
274
      puts self.nb_line_right
275
      puts self.line_right
276
    end
277 929:5f33065ddc4b Chris
278
    private
279
280
    def line_to_html(line, offsets)
281 1464:261b3d9a4903 Chris
      html = line_to_html_raw(line, offsets)
282
      html.force_encoding('UTF-8') if html.respond_to?(:force_encoding)
283
      html
284
    end
285
286
    def line_to_html_raw(line, offsets)
287 929:5f33065ddc4b Chris
      if offsets
288
        s = ''
289
        unless offsets.first == 0
290
          s << CGI.escapeHTML(line[0..offsets.first-1])
291
        end
292
        s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
293
        unless offsets.last == -1
294
          s << CGI.escapeHTML(line[offsets.last+1..-1])
295
        end
296
        s
297
      else
298
        CGI.escapeHTML(line)
299
      end
300
    end
301 0:513646585e45 Chris
  end
302
end