File: lib/redmine/unified_diff.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: Redmine#18
  class: UnifiedDiff#20
inherits from
  Array ( Builtin-Module )
has properties
attribute: diff_type [R] #21
method: initialize / 2 #23
method: truncated? #54
  class: DiffTable#58
inherits from
  Array ( Builtin-Module )
has properties
attribute: file_name [R] #59
method: initialize / 1 #63
method: add_line / 1 #72
method: each_line #95
method: inspect #105
method: diff_for_added_line #115
method: parse_line / 2 #125
method: write_offsets #163
method: offsets / 2 #176
  class: Diff#195
inherits from
  Object ( Builtin-Module )
has properties
attribute: nb_line_left [RW] #196
attribute: line_left [RW] #197
attribute: nb_line_right [RW] #198
attribute: line_right [RW] #199
attribute: type_diff_right [RW] #200
attribute: type_diff_left [RW] #201
attribute: offsets [RW] #202
method: initialize #204
method: type_diff #213
method: line #217
method: html_line_left #221
method: html_line_right #225
method: html_line #229
method: inspect #233
method: line_to_html / 2 #243

Class Hierarchy

Object ( Builtin-Module )
Diff ( Redmine ) — #195
Array ( Builtin-Module )
  UnifiedDiff ( Redmine ) #20
  DiffTable ( Redmine ) #58

Code

   1  # Redmine - project management software
   2  # Copyright (C) 2006-2011  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
  22 
  23      def initialize(diff, options={})
  24        options.assert_valid_keys(:type, :max_lines)
  25        diff = diff.split("\n") if diff.is_a?(String)
  26        @diff_type = options[:type] || 'inline'
  27        lines = 0
  28        @truncated = false
  29        diff_table = DiffTable.new(@diff_type)
  30        diff.each do |line|
  31          line_encoding = nil
  32          if line.respond_to?(:force_encoding)
  33            line_encoding = line.encoding
  34            # TODO: UTF-16 and Japanese CP932 which is imcompatible with ASCII
  35            #       In Japan, diffrence between file path encoding
  36            #       and file contents encoding is popular.
  37            line.force_encoding('ASCII-8BIT')
  38          end
  39          unless diff_table.add_line line
  40            line.force_encoding(line_encoding) if line_encoding
  41            self << diff_table if diff_table.length > 0
  42            diff_table = DiffTable.new(diff_type)
  43          end
  44          lines += 1
  45          if options[:max_lines] && lines > options[:max_lines]
  46            @truncated = true
  47            break
  48          end
  49        end
  50        self << diff_table unless diff_table.empty?
  51        self
  52      end
  53 
  54      def truncated?; @truncated; end
  55    end
  56 
  57    # Class that represents a file diff
  58    class DiffTable < Array
  59      attr_reader :file_name
  60 
  61      # Initialize with a Diff file and the type of Diff View
  62      # The type view must be inline or sbs (side_by_side)
  63      def initialize(type="inline")
  64        @parsing = false
  65        @added = 0
  66        @removed = 0
  67        @type = type
  68      end
  69 
  70      # Function for add a line of this Diff
  71      # Returns false when the diff ends
  72      def add_line(line)
  73        unless @parsing
  74          if line =~ /^(---|\+\+\+) (.*)$/
  75            @file_name = $2
  76          elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
  77            @line_num_l = $2.to_i
  78            @line_num_r = $5.to_i
  79            @parsing = true
  80          end
  81        else
  82          if line =~ /^[^\+\-\s@\\]/
  83            @parsing = false
  84            return false
  85          elsif line =~ /^@@ (\+|\-)(\d+)(,\d+)? (\+|\-)(\d+)(,\d+)? @@/
  86            @line_num_l = $2.to_i
  87            @line_num_r = $5.to_i
  88          else
  89            parse_line(line, @type)
  90          end
  91        end
  92        return true
  93      end
  94 
  95      def each_line
  96        prev_line_left, prev_line_right = nil, nil
  97        each do |line|
  98          spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1)
  99          yield spacing, line
 100          prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0
 101          prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0
 102        end
 103      end
 104 
 105      def inspect
 106        puts '### DIFF TABLE ###'
 107        puts "file : #{file_name}"
 108        self.each do |d|
 109          d.inspect
 110        end
 111      end
 112 
 113      private
 114 
 115      def diff_for_added_line
 116        if @type == 'sbs' && @removed > 0 && @added < @removed
 117          self[-(@removed - @added)]
 118        else
 119          diff = Diff.new
 120          self << diff
 121          diff
 122        end
 123      end
 124 
 125      def parse_line(line, type="inline")
 126        if line[0, 1] == "+"
 127          diff = diff_for_added_line
 128          diff.line_right = line[1..-1]
 129          diff.nb_line_right = @line_num_r
 130          diff.type_diff_right = 'diff_in'
 131          @line_num_r += 1
 132          @added += 1
 133          true
 134        elsif line[0, 1] == "-"
 135          diff = Diff.new
 136          diff.line_left = line[1..-1]
 137          diff.nb_line_left = @line_num_l
 138          diff.type_diff_left = 'diff_out'
 139          self << diff
 140          @line_num_l += 1
 141          @removed += 1
 142          true
 143        else
 144          write_offsets
 145          if line[0, 1] =~ /\s/
 146            diff = Diff.new
 147            diff.line_right = line[1..-1]
 148            diff.nb_line_right = @line_num_r
 149            diff.line_left = line[1..-1]
 150            diff.nb_line_left = @line_num_l
 151            self << diff
 152            @line_num_l += 1
 153            @line_num_r += 1
 154            true
 155          elsif line[0, 1] = "\\"
 156            true
 157          else
 158            false
 159          end
 160        end
 161      end
 162 
 163      def write_offsets
 164        if @added > 0 && @added == @removed
 165          @added.times do |i|
 166            line = self[-(1 + i)]
 167            removed = (@type == 'sbs') ? line : self[-(1 + @added + i)]
 168            offsets = offsets(removed.line_left, line.line_right)
 169            removed.offsets = line.offsets = offsets
 170          end
 171        end
 172        @added = 0
 173        @removed = 0
 174      end
 175 
 176      def offsets(line_left, line_right)
 177        if line_left.present? && line_right.present? && line_left != line_right
 178          max = [line_left.size, line_right.size].min
 179          starting = 0
 180          while starting < max && line_left[starting] == line_right[starting]
 181            starting += 1
 182          end
 183          ending = -1
 184          while ending >= -(max - starting) && line_left[ending] == line_right[ending]
 185            ending -= 1
 186          end
 187          unless starting == 0 && ending == -1
 188            [starting, ending]
 189          end
 190        end
 191      end
 192    end
 193 
 194    # A line of diff
 195    class Diff
 196      attr_accessor :nb_line_left
 197      attr_accessor :line_left
 198      attr_accessor :nb_line_right
 199      attr_accessor :line_right
 200      attr_accessor :type_diff_right
 201      attr_accessor :type_diff_left
 202      attr_accessor :offsets
 203 
 204      def initialize()
 205        self.nb_line_left = ''
 206        self.nb_line_right = ''
 207        self.line_left = ''
 208        self.line_right = ''
 209        self.type_diff_right = ''
 210        self.type_diff_left = ''
 211      end
 212 
 213      def type_diff
 214        type_diff_right == 'diff_in' ? type_diff_right : type_diff_left
 215      end
 216 
 217      def line
 218        type_diff_right == 'diff_in' ? line_right : line_left
 219      end
 220 
 221      def html_line_left
 222        line_to_html(line_left, offsets)
 223      end
 224 
 225      def html_line_right
 226        line_to_html(line_right, offsets)
 227      end
 228 
 229      def html_line
 230        line_to_html(line, offsets)
 231      end
 232 
 233      def inspect
 234        puts '### Start Line Diff ###'
 235        puts self.nb_line_left
 236        puts self.line_left
 237        puts self.nb_line_right
 238        puts self.line_right
 239      end
 240 
 241      private
 242 
 243      def line_to_html(line, offsets)
 244        if offsets
 245          s = ''
 246          unless offsets.first == 0
 247            s << CGI.escapeHTML(line[0..offsets.first-1])
 248          end
 249          s << '<span>' + CGI.escapeHTML(line[offsets.first..offsets.last]) + '</span>'
 250          unless offsets.last == -1
 251            s << CGI.escapeHTML(line[offsets.last+1..-1])
 252          end
 253          s
 254        else
 255          CGI.escapeHTML(line)
 256        end
 257      end
 258    end
 259  end