From 21c79827ff73916aa7fa8a6cce0645fc34a7c7fd Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Fri, 11 Mar 2011 20:23:29 +0000 Subject: [PATCH] Highlight changes inside diff lines (#7139). git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@5094 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/views/common/_diff.rhtml | 50 ++++----- lib/redmine/unified_diff.rb | 170 +++++++++++++++++++---------- public/stylesheets/application.css | 2 + test/fixtures/diffs/partials.diff | 46 ++++++++ test/unit/lib/redmine/unified_diff_test.rb | 59 +++++++++- 5 files changed, 241 insertions(+), 86 deletions(-) create mode 100644 test/fixtures/diffs/partials.diff diff --git a/app/views/common/_diff.rhtml b/app/views/common/_diff.rhtml index 619790c1..03b06a0c 100644 --- a/app/views/common/_diff.rhtml +++ b/app/views/common/_diff.rhtml @@ -1,66 +1,56 @@ <% diff = Redmine::UnifiedDiff.new(diff, :type => diff_type, :max_lines => Setting.diff_max_lines_displayed.to_i) -%> + <% diff.each do |table_file| -%>
-<% if diff_type == 'sbs' -%> +<% if diff.diff_type == 'sbs' -%> -<% prev_line_left, prev_line_right = nil, nil -%> -<% table_file.keys.sort.each do |key| -%> -<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> +<% table_file.each_line do |spacing, line| -%> +<% if spacing -%> - + + <% end -%> - - + - - + -<% prev_line_left, prev_line_right = table_file[key].nb_line_left.to_i, table_file[key].nb_line_right.to_i -%> <% end -%>
<%=to_utf8 table_file.file_name %>
............
<%= table_file[key].nb_line_left %> -
<%=to_utf8 table_file[key].line_left %>
+
<%= line.nb_line_left %> +
<%=to_utf8 line.html_line_left %>
<%= table_file[key].nb_line_right %> -
<%=to_utf8 table_file[key].line_right %>
+
<%= line.nb_line_right %> +
<%=to_utf8 line.html_line_right %>
<% else -%> - +
-<% prev_line_left, prev_line_right = nil, nil -%> -<% table_file.keys.sort.each do |key, line| %> -<% if prev_line_left && prev_line_right && (table_file[key].nb_line_left != prev_line_left+1) && (table_file[key].nb_line_right != prev_line_right+1) -%> +<% table_file.each_line do |spacing, line| %> +<% if spacing -%> - + <% end -%> - - - <% if table_file[key].line_left.empty? -%> - + + - <% else -%> - - <% end -%> -<% prev_line_left = table_file[key].nb_line_left.to_i if table_file[key].nb_line_left.to_i > 0 -%> -<% prev_line_right = table_file[key].nb_line_right.to_i if table_file[key].nb_line_right.to_i > 0 -%> <% end -%>
<%=to_utf8 table_file.file_name %>
............
<%= table_file[key].nb_line_left %><%= table_file[key].nb_line_right %> -
<%=to_utf8 table_file[key].line_right %>
+
<%= line.nb_line_left %><%= line.nb_line_right %> +
<%=to_utf8 line.html_line %>
-
<%=to_utf8 table_file[key].line_left %>
-
<% end -%> -
<% end -%> diff --git a/lib/redmine/unified_diff.rb b/lib/redmine/unified_diff.rb index 430b1254..f77721d6 100644 --- a/lib/redmine/unified_diff.rb +++ b/lib/redmine/unified_diff.rb @@ -1,5 +1,5 @@ -# redMine - project management software -# Copyright (C) 2006-2008 Jean-Philippe Lang +# Redmine - project management software +# Copyright (C) 2006-2011 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -17,14 +17,16 @@ module Redmine # Class used to parse unified diffs - class UnifiedDiff < Array + class UnifiedDiff < Array + attr_reader :diff_type + def initialize(diff, options={}) options.assert_valid_keys(:type, :max_lines) diff = diff.split("\n") if diff.is_a?(String) - diff_type = options[:type] || 'inline' + @diff_type = options[:type] || 'inline' lines = 0 @truncated = false - diff_table = DiffTable.new(diff_type) + diff_table = DiffTable.new(@diff_type) diff.each do |line| line_encoding = nil if line.respond_to?(:force_encoding) @@ -53,17 +55,15 @@ module Redmine end # Class that represents a file diff - class DiffTable < Hash - attr_reader :file_name, :line_num_l, :line_num_r + class DiffTable < Array + attr_reader :file_name # Initialize with a Diff file and the type of Diff View # The type view must be inline or sbs (side_by_side) def initialize(type="inline") @parsing = false - @nb_line = 1 - @start = false - @before = 'same' - @second = true + @added = 0 + @removed = 0 @type = type end @@ -86,11 +86,21 @@ module Redmine @line_num_l = $2.to_i @line_num_r = $5.to_i else - @nb_line += 1 if parse_line(line, @type) + parse_line(line, @type) end end return true end + + def each_line + prev_line_left, prev_line_right = nil, nil + each do |line| + spacing = prev_line_left && prev_line_right && (line.nb_line_left != prev_line_left+1) && (line.nb_line_right != prev_line_right+1) + yield spacing, line + prev_line_left = line.nb_line_left.to_i if line.nb_line_left.to_i > 0 + prev_line_right = line.nb_line_right.to_i if line.nb_line_right.to_i > 0 + end + end def inspect puts '### DIFF TABLE ###' @@ -100,74 +110,91 @@ module Redmine end end - private - # Test if is a Side By Side type - def sbs?(type, func) - if @start and type == "sbs" - if @before == func and @second - tmp_nb_line = @nb_line - self[tmp_nb_line] = Diff.new - else - @second = false - tmp_nb_line = @start - @start += 1 - @nb_line -= 1 - end - else - tmp_nb_line = @nb_line - @start = @nb_line - self[tmp_nb_line] = Diff.new - @second = true - end - unless self[tmp_nb_line] - @nb_line += 1 - self[tmp_nb_line] = Diff.new - else - self[tmp_nb_line] - end - end + private # Escape the HTML for the diff def escapeHTML(line) CGI.escapeHTML(line) end + + def diff_for_added_line + if @type == 'sbs' && @removed > 0 && @added < @removed + self[-(@removed - @added)] + else + diff = Diff.new + self << diff + diff + end + end def parse_line(line, type="inline") if line[0, 1] == "+" - diff = sbs? type, 'add' - @before = 'add' + diff = diff_for_added_line diff.line_right = escapeHTML line[1..-1] diff.nb_line_right = @line_num_r diff.type_diff_right = 'diff_in' @line_num_r += 1 + @added += 1 true elsif line[0, 1] == "-" - diff = sbs? type, 'remove' - @before = 'remove' - diff.line_left = escapeHTML line[1..-1] - diff.nb_line_left = @line_num_l - diff.type_diff_left = 'diff_out' - @line_num_l += 1 - true - elsif line[0, 1] =~ /\s/ - @before = 'same' - @start = false diff = Diff.new - diff.line_right = escapeHTML line[1..-1] - diff.nb_line_right = @line_num_r diff.line_left = escapeHTML line[1..-1] diff.nb_line_left = @line_num_l - self[@nb_line] = diff + diff.type_diff_left = 'diff_out' + self << diff @line_num_l += 1 - @line_num_r += 1 + @removed += 1 true - elsif line[0, 1] = "\\" + else + write_offsets + if line[0, 1] =~ /\s/ + diff = Diff.new + diff.line_right = escapeHTML line[1..-1] + diff.nb_line_right = @line_num_r + diff.line_left = escapeHTML line[1..-1] + diff.nb_line_left = @line_num_l + self << diff + @line_num_l += 1 + @line_num_r += 1 + true + elsif line[0, 1] = "\\" true else false end end end + + def write_offsets + if @added > 0 && @added == @removed + @added.times do |i| + line = self[-(1 + i)] + removed = (@type == 'sbs') ? line : self[-(1 + @added + i)] + offsets = offsets(removed.line_left, line.line_right) + removed.offsets = line.offsets = offsets + end + end + @added = 0 + @removed = 0 + end + + def offsets(line_left, line_right) + if line_left.present? && line_right.present? && line_left != line_right + max = [line_left.size, line_right.size].min + starting = 0 + while starting < max && line_left[starting] == line_right[starting] + starting += 1 + end + ending = -1 + while ending >= -(max - starting) && line_left[ending] == line_right[ending] + ending -= 1 + end + unless starting == 0 && ending == -1 + [starting, ending] + end + end + end + end # A line of diff class Diff @@ -177,6 +204,7 @@ module Redmine attr_accessor :line_right attr_accessor :type_diff_right attr_accessor :type_diff_left + attr_accessor :offsets def initialize() self.nb_line_left = '' @@ -186,6 +214,38 @@ module Redmine self.type_diff_right = '' self.type_diff_left = '' end + + def type_diff + type_diff_right == 'diff_in' ? type_diff_right : type_diff_left + end + + def line + type_diff_right == 'diff_in' ? line_right : line_left + end + + def html_line_left + if offsets + line_left.dup.insert(offsets.first, '').insert(offsets.last, '') + else + line_left + end + end + + def html_line_right + if offsets + line_right.dup.insert(offsets.first, '').insert(offsets.last, '') + else + line_right + end + end + + def html_line + if offsets + line.dup.insert(offsets.first, '').insert(offsets.last, '') + else + line + end + end def inspect puts '### Start Line Diff ###' diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index fb762d15..d3420205 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -672,7 +672,9 @@ div.autocomplete ul li span.informal { /***** Diff *****/ .diff_out { background: #fcc; } +.diff_out span { background: #faa; } .diff_in { background: #cfc; } +.diff_in span { background: #afa; } .text-diff { padding: 1em; diff --git a/test/fixtures/diffs/partials.diff b/test/fixtures/diffs/partials.diff new file mode 100644 index 00000000..f745776c --- /dev/null +++ b/test/fixtures/diffs/partials.diff @@ -0,0 +1,46 @@ +--- partials.txt Wed Jan 19 12:06:17 2011 ++++ partials.1.txt Wed Jan 19 12:06:10 2011 +@@ -1,31 +1,31 @@ +-Lorem ipsum dolor sit amet, consectetur adipiscing elit ++Lorem ipsum dolor sit amet, consectetur adipiscing xx + Praesent et sagittis dui. Vivamus ac diam diam +-Ut sed auctor justo ++xxx auctor justo + Suspendisse venenatis sollicitudin magna quis suscipit +-Sed blandit gravida odio ac ultrices ++Sed blandit gxxxxa odio ac ultrices + Morbi rhoncus est ut est aliquam tempus +-Morbi id nisi vel felis tincidunt tempus ++Morbi id nisi vel felis xx tempus + Mauris auctor sagittis ante eu luctus +-Fusce commodo felis sed ligula congue molestie ++Fusce commodo felis sed ligula congue + Lorem ipsum dolor sit amet, consectetur adipiscing elit +-Praesent et sagittis dui. Vivamus ac diam diam ++et sagittis dui. Vivamus ac diam diam + Ut sed auctor justo + Suspendisse venenatis sollicitudin magna quis suscipit + Sed blandit gravida odio ac ultrices + +-Lorem ipsum dolor sit amet, consectetur adipiscing elit +-Praesent et sagittis dui. Vivamus ac diam diam ++Lorem ipsum dolor sit amet, xxxx adipiscing elit + Ut sed auctor justo + Suspendisse venenatis sollicitudin magna quis suscipit + Sed blandit gravida odio ac ultrices +-Morbi rhoncus est ut est aliquam tempus ++Morbi rhoncus est ut est xxxx tempus ++New line + Morbi id nisi vel felis tincidunt tempus + Mauris auctor sagittis ante eu luctus + Fusce commodo felis sed ligula congue molestie + +-Lorem ipsum dolor sit amet, consectetur adipiscing elit +-Praesent et sagittis dui. Vivamus ac diam diam +-Ut sed auctor justo ++Lorem ipsum dolor sit amet, xxxxtetur adipiscing elit ++Praesent et xxxxx. Vivamus ac diam diam ++Ut sed auctor + Suspendisse venenatis sollicitudin magna quis suscipit + Sed blandit gravida odio ac ultrices + Morbi rhoncus est ut est aliquam tempus diff --git a/test/unit/lib/redmine/unified_diff_test.rb b/test/unit/lib/redmine/unified_diff_test.rb index e6da01c8..13653e3a 100644 --- a/test/unit/lib/redmine/unified_diff_test.rb +++ b/test/unit/lib/redmine/unified_diff_test.rb @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2008 Jean-Philippe Lang +# Copyright (C) 2006-2011 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -34,6 +34,63 @@ class Redmine::UnifiedDiffTest < ActiveSupport::TestCase assert_equal 2, diff.size end + def test_inline_partials + diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff')) + assert_equal 1, diff.size + diff = diff.first + assert_equal 43, diff.size + + assert_equal [51, -1], diff[0].offsets + assert_equal [51, -1], diff[1].offsets + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[1].html_line + + assert_nil diff[2].offsets + assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[2].html_line + + assert_equal [0, -14], diff[3].offsets + assert_equal [0, -14], diff[4].offsets + assert_equal 'Ut sed auctor justo', diff[3].html_line + assert_equal 'xxx auctor justo', diff[4].html_line + + assert_equal [13, -19], diff[6].offsets + assert_equal [13, -19], diff[7].offsets + + assert_equal [24, -8], diff[9].offsets + assert_equal [24, -8], diff[10].offsets + + assert_equal [37, -1], diff[12].offsets + assert_equal [37, -1], diff[13].offsets + + assert_equal [0, -38], diff[15].offsets + assert_equal [0, -38], diff[16].offsets + end + + def test_side_by_side_partials + diff = Redmine::UnifiedDiff.new(read_diff_fixture('partials.diff'), :type => 'sbs') + assert_equal 1, diff.size + diff = diff.first + assert_equal 32, diff.size + + assert_equal [51, -1], diff[0].offsets + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing elit', diff[0].html_line_left + assert_equal 'Lorem ipsum dolor sit amet, consectetur adipiscing xx', diff[0].html_line_right + + assert_nil diff[1].offsets + assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_left + assert_equal 'Praesent et sagittis dui. Vivamus ac diam diam', diff[1].html_line_right + + assert_equal [0, -14], diff[2].offsets + assert_equal 'Ut sed auctor justo', diff[2].html_line_left + assert_equal 'xxx auctor justo', diff[2].html_line_right + + assert_equal [13, -19], diff[4].offsets + assert_equal [24, -8], diff[6].offsets + assert_equal [37, -1], diff[8].offsets + assert_equal [0, -38], diff[10].offsets + + end + def test_line_starting_with_dashes diff = Redmine::UnifiedDiff.new(<<-DIFF --- old.txt Wed Nov 11 14:24:58 2009 -- 2.11.0