gitweb: Highlight interesting parts of diff

Reading diff output is sometimes very hard, even if it's colored, especially if lines differ only in few characters. This is often true when a commit fixes a typo or renames some variables or functions. This commit teaches gitweb to highlight characters that are different between old and new line with a light green/red background. This should work in the similar manner as in Trac or GitHub. The algorithm that compares lines is based on contrib/diff-highlight. Basically, it works by determining common prefix/suffix of corresponding lines and highlightning only the middle part of lines. For more information, see contrib/diff-highlight/README. Combined diffs are not supported but a following commit will change it. Since we need to pass esc_html()'ed or esc_html_hl_regions()'ed lines to format_diff_lines(), so it was taught to accept preformatted lines passed as a reference. Signed-off-by: Michał Kiedrowicz <michal.kiedrowicz@gmail.com> Acked-by: Jakub Narębski <jnareb@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Michał Kiedrowicz committed Apr 11, 2012 at 23:18 UTC 5fb6ddf67afb3d0dc9b1fce64543f20b28a81b4b
2 files changed +103 -12
gitweb/gitweb.perl
+95 -12
@@ -2430,19 +2430,25 @@ sub format_cc_diff_chunk_header {
2430 }
2431
2432 # process patch (diff) line (not to be used for diff headers),
2433 -# returning HTML-formatted (but not wrapped) line
2433 +# returning HTML-formatted (but not wrapped) line.
2434 +# If the line is passed as a reference, it is treated as HTML and not
2435 +# esc_html()'ed.
2436 sub format_diff_line {
2437 my ($line, $diff_class, $from, $to) = @_;
2438
2437 - chomp $line;
2438 - $line = untabify($line);
2439 -
2440 - if ($from && $to && $line =~ m/^\@{2} /) {
2441 - $line = format_unidiff_chunk_header($line, $from, $to);
2442 - } elsif ($from && $to && $line =~ m/^\@{3}/) {
2443 - $line = format_cc_diff_chunk_header($line, $from, $to);
2439 + if (ref($line)) {
2440 + $line = $$line;
2441 } else {
2445 - $line = esc_html($line, -nbsp=>1);
2442 + chomp $line;
2443 + $line = untabify($line);
2444 +
2445 + if ($from && $to && $line =~ m/^\@{2} /) {
2446 + $line = format_unidiff_chunk_header($line, $from, $to);
2447 + } elsif ($from && $to && $line =~ m/^\@{3}/) {
2448 + $line = format_cc_diff_chunk_header($line, $from, $to);
2449 + } else {
2450 + $line = esc_html($line, -nbsp=>1);
2451 + }
2452 }
2453
2454 my $diff_classes = "diff";
@@ -5055,10 +5061,89 @@ sub print_inline_diff_lines {
5061 print @$ctx, @$rem, @$add;
5062 }
5063
5064 +# Format removed and added line, mark changed part and HTML-format them.
5065 +# Implementation is based on contrib/diff-highlight
5066 +sub format_rem_add_lines_pair {
5067 + my ($rem, $add) = @_;
5068 +
5069 + # We need to untabify lines before split()'ing them;
5070 + # otherwise offsets would be invalid.
5071 + chomp $rem;
5072 + chomp $add;
5073 + $rem = untabify($rem);
5074 + $add = untabify($add);
5075 +
5076 + my @rem = split(//, $rem);
5077 + my @add = split(//, $add);
5078 + my ($esc_rem, $esc_add);
5079 + # Ignore +/- character, thus $prefix_len is set to 1.
5080 + my ($prefix_len, $suffix_len) = (1, 0);
5081 + my ($prefix_has_nonspace, $suffix_has_nonspace);
5082 +
5083 + my $shorter = (@rem < @add) ? @rem : @add;
5084 + while ($prefix_len < $shorter) {
5085 + last if ($rem[$prefix_len] ne $add[$prefix_len]);
5086 +
5087 + $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
5088 + $prefix_len++;
5089 + }
5090 +
5091 + while ($prefix_len + $suffix_len < $shorter) {
5092 + last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
5093 +
5094 + $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
5095 + $suffix_len++;
5096 + }
5097 +
5098 + # Mark lines that are different from each other, but have some common
5099 + # part that isn't whitespace. If lines are completely different, don't
5100 + # mark them because that would make output unreadable, especially if
5101 + # diff consists of multiple lines.
5102 + if ($prefix_has_nonspace || $suffix_has_nonspace) {
5103 + $esc_rem = esc_html_hl_regions($rem, 'marked',
5104 + [$prefix_len, @rem - $suffix_len], -nbsp=>1);
5105 + $esc_add = esc_html_hl_regions($add, 'marked',
5106 + [$prefix_len, @add - $suffix_len], -nbsp=>1);
5107 + } else {
5108 + $esc_rem = esc_html($rem, -nbsp=>1);
5109 + $esc_add = esc_html($add, -nbsp=>1);
5110 + }
5111 +
5112 + return format_diff_line(\$esc_rem, 'rem'),
5113 + format_diff_line(\$esc_add, 'add');
5114 +}
5115 +
5116 +# HTML-format diff context, removed and added lines.
5117 +sub format_ctx_rem_add_lines {
5118 + my ($ctx, $rem, $add, $is_combined) = @_;
5119 + my (@new_ctx, @new_rem, @new_add);
5120 +
5121 + # Highlight if every removed line has a corresponding added line.
5122 + # Combined diffs are not supported at this moment.
5123 + if (!$is_combined && @$add > 0 && @$add == @$rem) {
5124 + for (my $i = 0; $i < @$add; $i++) {
5125 + my ($line_rem, $line_add) = format_rem_add_lines_pair(
5126 + $rem->[$i], $add->[$i]);
5127 + push @new_rem, $line_rem;
5128 + push @new_add, $line_add;
5129 + }
5130 + } else {
5131 + @new_rem = map { format_diff_line($_, 'rem') } @$rem;
5132 + @new_add = map { format_diff_line($_, 'add') } @$add;
5133 + }
5134 +
5135 + @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
5136 +
5137 + return (\@new_ctx, \@new_rem, \@new_add);
5138 +}
5139 +
5140 # Print context lines and then rem/add lines.
5141 sub print_diff_lines {
5142 my ($ctx, $rem, $add, $diff_style, $is_combined) = @_;
5143
5144 + ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
5145 + $is_combined);
5146 +
5147 if ($diff_style eq 'sidebyside' && !$is_combined) {
5148 print_sidebyside_diff_lines($ctx, $rem, $add);
5149 } else {
@@ -5090,11 +5175,9 @@ sub print_diff_chunk {
5175 foreach my $line_info (@chunk) {
5176 my ($class, $line) = @$line_info;
5177
5093 - $line = format_diff_line($line, $class, $from, $to);
5094 -
5178 # print chunk headers
5179 if ($class && $class eq 'chunk_header') {
5097 - print $line;
5180 + print format_diff_line($line, $class, $from, $to);
5181 next;
5182 }
5183
gitweb/static/gitweb.css
+8
@@ -438,6 +438,10 @@ div.diff.add {
438 color: #008800;
439 }
440
441 +div.diff.add span.marked {
442 + background-color: #aaffaa;
443 +}
444 +
445 div.diff.from_file a.path,
446 div.diff.from_file {
447 color: #aa0000;
@@ -447,6 +451,10 @@ div.diff.rem {
451 color: #cc0000;
452 }
453
454 +div.diff.rem span.marked {
455 + background-color: #ffaaaa;
456 +}
457 +
458 div.diff.chunk_header a,
459 div.diff.chunk_header {
460 color: #990099;