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 @ 1297:0a574315af3e

History | View | Annotate | Download (7.88 KB)

1
# Redmine - project management software
2
# Copyright (C) 2006-2012  Jean-Philippe Lang
3
#
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
#
9
# 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
#
14
# 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
  class UnifiedDiff < Array
21
    attr_reader :diff_type, :diff_style
22

    
23
    def initialize(diff, options={})
24
      options.assert_valid_keys(:type, :style, :max_lines)
25
      diff = diff.split("\n") if diff.is_a?(String)
26
      @diff_type = options[:type] || 'inline'
27
      @diff_style = options[:style]
28
      lines = 0
29
      @truncated = false
30
      diff_table = DiffTable.new(diff_type, diff_style)
31
      diff.each do |line|
32
        line_encoding = nil
33
        if line.respond_to?(:force_encoding)
34
          line_encoding = line.encoding
35
          # TODO: UTF-16 and Japanese CP932 which is imcompatible with ASCII
36
          #       In Japan, diffrence between file path encoding
37
          #       and file contents encoding is popular.
38
          line.force_encoding('ASCII-8BIT')
39
        end
40
        unless diff_table.add_line line
41
          line.force_encoding(line_encoding) if line_encoding
42
          self << diff_table if diff_table.length > 0
43
          diff_table = DiffTable.new(diff_type, diff_style)
44
        end
45
        lines += 1
46
        if options[:max_lines] && lines > options[:max_lines]
47
          @truncated = true
48
          break
49
        end
50
      end
51
      self << diff_table unless diff_table.empty?
52
      self
53
    end
54

    
55
    def truncated?; @truncated; end
56
  end
57

    
58
  # Class that represents a file diff
59
  class DiffTable < Array
60
    attr_reader :file_name
61

    
62
    # Initialize with a Diff file and the type of Diff View
63
    # The type view must be inline or sbs (side_by_side)
64
    def initialize(type="inline", style=nil)
65
      @parsing = false
66
      @added = 0
67
      @removed = 0
68
      @type = type
69
      @style = style
70
      @file_name = nil
71
      @git_diff = false
72
    end
73

    
74
    # Function for add a line of this Diff
75
    # Returns false when the diff ends
76
    def add_line(line)
77
      unless @parsing
78
        if line =~ /^(---|\+\+\+) (.*)$/
79
          self.file_name = $2
80
        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
81
          @line_num_l = $2.to_i
82
          @line_num_r = $5.to_i
83
          @parsing = true
84
        end
85
      else
86
        if line =~ /^[^\+\-\s@\\]/
87
          @parsing = false
88
          return false
89
        elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
90
          @line_num_l = $2.to_i
91
          @line_num_r = $5.to_i
92
        else
93
          parse_line(line, @type)
94
        end
95
      end
96
      return true
97
    end
98

    
99
    def each_line
100
      prev_line_left, prev_line_right = nil, nil
101
      each do |line|
102
        spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1)
103
        yield spacing, line
104
        prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0
105
        prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0
106
      end
107
    end
108

    
109
    def inspect
110
      puts '### DIFF TABLE ###'
111
      puts "file : #{file_name}"
112
      self.each do |d|
113
        d.inspect
114
      end
115
    end
116

    
117
    private
118

    
119
    def file_name=(arg)
120
      both_git_diff = false
121
      if file_name.nil?
122
        @git_diff = true if arg =~ %r{^(a/|/dev/null)}
123
      else
124
        both_git_diff = (@git_diff && arg =~ %r{^(b/|/dev/null)})
125
      end
126
      if both_git_diff
127
        if file_name && arg == "/dev/null"
128
          # keep the original file name
129
          @file_name = file_name.sub(%r{^a/}, '')
130
        else
131
          # remove leading b/
132
          @file_name = arg.sub(%r{^b/}, '')
133
        end
134
      elsif @style == "Subversion"
135
        # removing trailing "(revision nn)"
136
        @file_name = arg.sub(%r{\t+\(.*\)$}, '')
137
      else
138
        @file_name = arg
139
      end
140
    end
141

    
142
    def diff_for_added_line
143
      if @type == 'sbs' && @removed > 0 && @added < @removed
144
        self[-(@removed - @added)]
145
      else
146
        diff = Diff.new
147
        self << diff
148
        diff
149
      end
150
    end
151

    
152
    def parse_line(line, type="inline")
153
      if line[0, 1] == "+"
154
        diff = diff_for_added_line
155
        diff.line_right = line[1..-1]
156
        diff.nb_line_right = @line_num_r
157
        diff.type_diff_right = 'diff_in'
158
        @line_num_r += 1
159
        @added += 1
160
        true
161
      elsif line[0, 1] == "-"
162
        diff = Diff.new
163
        diff.line_left = line[1..-1]
164
        diff.nb_line_left = @line_num_l
165
        diff.type_diff_left = 'diff_out'
166
        self << diff
167
        @line_num_l += 1
168
        @removed += 1
169
        true
170
      else
171
        write_offsets
172
        if line[0, 1] =~ /\s/
173
          diff = Diff.new
174
          diff.line_right = line[1..-1]
175
          diff.nb_line_right = @line_num_r
176
          diff.line_left = line[1..-1]
177
          diff.nb_line_left = @line_num_l
178
          self << diff
179
          @line_num_l += 1
180
          @line_num_r += 1
181
          true
182
        elsif line[0, 1] = "\\"
183
          true
184
        else
185
          false
186
        end
187
      end
188
    end
189

    
190
    def write_offsets
191
      if @added > 0 && @added == @removed
192
        @added.times do |i|
193
          line = self[-(1 + i)]
194
          removed = (@type == 'sbs') ? line : self[-(1 + @added + i)]
195
          offsets = offsets(removed.line_left, line.line_right)
196
          removed.offsets = line.offsets = offsets
197
        end
198
      end
199
      @added = 0
200
      @removed = 0
201
    end
202

    
203
    def offsets(line_left, line_right)
204
      if line_left.present? && line_right.present? && line_left != line_right
205
        max = [line_left.size, line_right.size].min
206
        starting = 0
207
        while starting < max && line_left[starting] == line_right[starting]
208
          starting += 1
209
        end
210
        ending = -1
211
        while ending >= -(max - starting) && line_left[ending] == line_right[ending]
212
          ending -= 1
213
        end
214
        unless starting == 0 && ending == -1
215
          [starting, ending]
216
        end
217
      end
218
    end
219
  end
220

    
221
  # A line of diff
222
  class Diff
223
    attr_accessor :nb_line_left
224
    attr_accessor :line_left
225
    attr_accessor :nb_line_right
226
    attr_accessor :line_right
227
    attr_accessor :type_diff_right
228
    attr_accessor :type_diff_left
229
    attr_accessor :offsets
230

    
231
    def initialize()
232
      self.nb_line_left = ''
233
      self.nb_line_right = ''
234
      self.line_left = ''
235
      self.line_right = ''
236
      self.type_diff_right = ''
237
      self.type_diff_left = ''
238
    end
239

    
240
    def type_diff
241
      type_diff_right == 'diff_in' ? type_diff_right : type_diff_left
242
    end
243

    
244
    def line
245
      type_diff_right == 'diff_in' ? line_right : line_left
246
    end
247

    
248
    def html_line_left
249
      line_to_html(line_left, offsets)
250
    end
251

    
252
    def html_line_right
253
      line_to_html(line_right, offsets)
254
    end
255

    
256
    def html_line
257
      line_to_html(line, offsets)
258
    end
259

    
260
    def inspect
261
      puts '### Start Line Diff ###'
262
      puts self.nb_line_left
263
      puts self.line_left
264
      puts self.nb_line_right
265
      puts self.line_right
266
    end
267

    
268
    private
269

    
270
    def line_to_html(line, offsets)
271
      if offsets
272
        s = ''
273
        unless offsets.first == 0
274
          s << CGI.escapeHTML(line[0..offsets.first-1])
275
        end
276
        s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
277
        unless offsets.last == -1
278
          s << CGI.escapeHTML(line[offsets.last+1..-1])
279
        end
280
        s
281
      else
282
        CGI.escapeHTML(line)
283
      end
284
    end
285
  end
286
end