Skip to content

Commit c6cfa5b

Browse files
committed
Merge branch 'mk/gitweb-diff-hl'
"gitweb" learns to highlight the patch it outputs even more. By Michał Kiedrowicz (7) and Jakub Narębski (1) * mk/gitweb-diff-hl: gitweb: Refinement highlightning in combined diffs gitweb: Highlight interesting parts of diff gitweb: Push formatting diff lines to print_diff_chunk() gitweb: Use print_diff_chunk() for both side-by-side and inline diffs gitweb: Extract print_sidebyside_diff_lines() gitweb: Pass esc_html_hl_regions() options to esc_html() gitweb: esc_html_hl_regions(): Don't create empty <span> elements gitweb: Use descriptive names in esc_html_hl_regions()
2 parents 77cab8a + 51ef7a6 commit c6cfa5b

File tree

2 files changed

+244
-87
lines changed

2 files changed

+244
-87
lines changed

gitweb/gitweb.perl

Lines changed: 236 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,20 +1732,29 @@ sub chop_and_escape_str {
17321732
# '<span class="mark">foo</span>bar'
17331733
sub esc_html_hl_regions {
17341734
my ($str, $css_class, @sel) = @_;
1735-
return esc_html($str) unless @sel;
1735+
my %opts = grep { ref($_) ne 'ARRAY' } @sel;
1736+
@sel = grep { ref($_) eq 'ARRAY' } @sel;
1737+
return esc_html($str, %opts) unless @sel;
17361738

17371739
my $out = '';
17381740
my $pos = 0;
17391741

17401742
for my $s (@sel) {
1741-
$out .= esc_html(substr($str, $pos, $s->[0] - $pos))
1742-
if ($s->[0] - $pos > 0);
1743-
$out .= $cgi->span({-class => $css_class},
1744-
esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
1743+
my ($begin, $end) = @$s;
17451744

1746-
$pos = $s->[1];
1745+
# Don't create empty <span> elements.
1746+
next if $end <= $begin;
1747+
1748+
my $escaped = esc_html(substr($str, $begin, $end - $begin),
1749+
%opts);
1750+
1751+
$out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
1752+
if ($begin - $pos > 0);
1753+
$out .= $cgi->span({-class => $css_class}, $escaped);
1754+
1755+
$pos = $end;
17471756
}
1748-
$out .= esc_html(substr($str, $pos))
1757+
$out .= esc_html(substr($str, $pos), %opts)
17491758
if ($pos < length($str));
17501759

17511760
return $out;
@@ -2421,26 +2430,32 @@ sub format_cc_diff_chunk_header {
24212430
}
24222431

24232432
# process patch (diff) line (not to be used for diff headers),
2424-
# returning class and HTML-formatted (but not wrapped) line
2425-
sub process_diff_line {
2426-
my $line = shift;
2427-
my ($from, $to) = @_;
2428-
2429-
my $diff_class = diff_line_class($line, $from, $to);
2430-
2431-
chomp $line;
2432-
$line = untabify($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+
2439+
if (ref($line)) {
2440+
$line = $$line;
2441+
} else {
2442+
chomp $line;
2443+
$line = untabify($line);
24332444

2434-
if ($from && $to && $line =~ m/^\@{2} /) {
2435-
$line = format_unidiff_chunk_header($line, $from, $to);
2436-
return $diff_class, $line;
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+
}
24372453

2438-
} elsif ($from && $to && $line =~ m/^\@{3}/) {
2439-
$line = format_cc_diff_chunk_header($line, $from, $to);
2440-
return $diff_class, $line;
2454+
my $diff_classes = "diff";
2455+
$diff_classes .= " $diff_class" if ($diff_class);
2456+
$line = "<div class=\"$diff_classes\">$line</div>\n";
24412457

2442-
}
2443-
return $diff_class, esc_html($line, -nbsp=>1);
2458+
return $line;
24442459
}
24452460

24462461
# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
@@ -4994,10 +5009,186 @@ sub git_difftree_body {
49945009
print "</table>\n";
49955010
}
49965011

4997-
sub print_sidebyside_diff_chunk {
4998-
my @chunk = @_;
5012+
# Print context lines and then rem/add lines in a side-by-side manner.
5013+
sub print_sidebyside_diff_lines {
5014+
my ($ctx, $rem, $add) = @_;
5015+
5016+
# print context block before add/rem block
5017+
if (@$ctx) {
5018+
print join '',
5019+
'<div class="chunk_block ctx">',
5020+
'<div class="old">',
5021+
@$ctx,
5022+
'</div>',
5023+
'<div class="new">',
5024+
@$ctx,
5025+
'</div>',
5026+
'</div>';
5027+
}
5028+
5029+
if (!@$add) {
5030+
# pure removal
5031+
print join '',
5032+
'<div class="chunk_block rem">',
5033+
'<div class="old">',
5034+
@$rem,
5035+
'</div>',
5036+
'</div>';
5037+
} elsif (!@$rem) {
5038+
# pure addition
5039+
print join '',
5040+
'<div class="chunk_block add">',
5041+
'<div class="new">',
5042+
@$add,
5043+
'</div>',
5044+
'</div>';
5045+
} else {
5046+
print join '',
5047+
'<div class="chunk_block chg">',
5048+
'<div class="old">',
5049+
@$rem,
5050+
'</div>',
5051+
'<div class="new">',
5052+
@$add,
5053+
'</div>',
5054+
'</div>';
5055+
}
5056+
}
5057+
5058+
# Print context lines and then rem/add lines in inline manner.
5059+
sub print_inline_diff_lines {
5060+
my ($ctx, $rem, $add) = @_;
5061+
5062+
print @$ctx, @$rem, @$add;
5063+
}
5064+
5065+
# Format removed and added line, mark changed part and HTML-format them.
5066+
# Implementation is based on contrib/diff-highlight
5067+
sub format_rem_add_lines_pair {
5068+
my ($rem, $add, $num_parents) = @_;
5069+
5070+
# We need to untabify lines before split()'ing them;
5071+
# otherwise offsets would be invalid.
5072+
chomp $rem;
5073+
chomp $add;
5074+
$rem = untabify($rem);
5075+
$add = untabify($add);
5076+
5077+
my @rem = split(//, $rem);
5078+
my @add = split(//, $add);
5079+
my ($esc_rem, $esc_add);
5080+
# Ignore leading +/- characters for each parent.
5081+
my ($prefix_len, $suffix_len) = ($num_parents, 0);
5082+
my ($prefix_has_nonspace, $suffix_has_nonspace);
5083+
5084+
my $shorter = (@rem < @add) ? @rem : @add;
5085+
while ($prefix_len < $shorter) {
5086+
last if ($rem[$prefix_len] ne $add[$prefix_len]);
5087+
5088+
$prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
5089+
$prefix_len++;
5090+
}
5091+
5092+
while ($prefix_len + $suffix_len < $shorter) {
5093+
last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
5094+
5095+
$suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
5096+
$suffix_len++;
5097+
}
5098+
5099+
# Mark lines that are different from each other, but have some common
5100+
# part that isn't whitespace. If lines are completely different, don't
5101+
# mark them because that would make output unreadable, especially if
5102+
# diff consists of multiple lines.
5103+
if ($prefix_has_nonspace || $suffix_has_nonspace) {
5104+
$esc_rem = esc_html_hl_regions($rem, 'marked',
5105+
[$prefix_len, @rem - $suffix_len], -nbsp=>1);
5106+
$esc_add = esc_html_hl_regions($add, 'marked',
5107+
[$prefix_len, @add - $suffix_len], -nbsp=>1);
5108+
} else {
5109+
$esc_rem = esc_html($rem, -nbsp=>1);
5110+
$esc_add = esc_html($add, -nbsp=>1);
5111+
}
5112+
5113+
return format_diff_line(\$esc_rem, 'rem'),
5114+
format_diff_line(\$esc_add, 'add');
5115+
}
5116+
5117+
# HTML-format diff context, removed and added lines.
5118+
sub format_ctx_rem_add_lines {
5119+
my ($ctx, $rem, $add, $num_parents) = @_;
5120+
my (@new_ctx, @new_rem, @new_add);
5121+
my $can_highlight = 0;
5122+
my $is_combined = ($num_parents > 1);
5123+
5124+
# Highlight if every removed line has a corresponding added line.
5125+
if (@$add > 0 && @$add == @$rem) {
5126+
$can_highlight = 1;
5127+
5128+
# Highlight lines in combined diff only if the chunk contains
5129+
# diff between the same version, e.g.
5130+
#
5131+
# - a
5132+
# - b
5133+
# + c
5134+
# + d
5135+
#
5136+
# Otherwise the highlightling would be confusing.
5137+
if ($is_combined) {
5138+
for (my $i = 0; $i < @$add; $i++) {
5139+
my $prefix_rem = substr($rem->[$i], 0, $num_parents);
5140+
my $prefix_add = substr($add->[$i], 0, $num_parents);
5141+
5142+
$prefix_rem =~ s/-/+/g;
5143+
5144+
if ($prefix_rem ne $prefix_add) {
5145+
$can_highlight = 0;
5146+
last;
5147+
}
5148+
}
5149+
}
5150+
}
5151+
5152+
if ($can_highlight) {
5153+
for (my $i = 0; $i < @$add; $i++) {
5154+
my ($line_rem, $line_add) = format_rem_add_lines_pair(
5155+
$rem->[$i], $add->[$i], $num_parents);
5156+
push @new_rem, $line_rem;
5157+
push @new_add, $line_add;
5158+
}
5159+
} else {
5160+
@new_rem = map { format_diff_line($_, 'rem') } @$rem;
5161+
@new_add = map { format_diff_line($_, 'add') } @$add;
5162+
}
5163+
5164+
@new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
5165+
5166+
return (\@new_ctx, \@new_rem, \@new_add);
5167+
}
5168+
5169+
# Print context lines and then rem/add lines.
5170+
sub print_diff_lines {
5171+
my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
5172+
my $is_combined = $num_parents > 1;
5173+
5174+
($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
5175+
$num_parents);
5176+
5177+
if ($diff_style eq 'sidebyside' && !$is_combined) {
5178+
print_sidebyside_diff_lines($ctx, $rem, $add);
5179+
} else {
5180+
# default 'inline' style and unknown styles
5181+
print_inline_diff_lines($ctx, $rem, $add);
5182+
}
5183+
}
5184+
5185+
sub print_diff_chunk {
5186+
my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
49995187
my (@ctx, @rem, @add);
50005188

5189+
# The class of the previous line.
5190+
my $prev_class = '';
5191+
50015192
return unless @chunk;
50025193

50035194
# incomplete last line might be among removed or added lines,
@@ -5016,55 +5207,19 @@ sub print_sidebyside_diff_chunk {
50165207

50175208
# print chunk headers
50185209
if ($class && $class eq 'chunk_header') {
5019-
print $line;
5210+
print format_diff_line($line, $class, $from, $to);
50205211
next;
50215212
}
50225213

5023-
## print from accumulator when type of class of lines change
5024-
# empty contents block on start rem/add block, or end of chunk
5025-
if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
5026-
print join '',
5027-
'<div class="chunk_block ctx">',
5028-
'<div class="old">',
5029-
@ctx,
5030-
'</div>',
5031-
'<div class="new">',
5032-
@ctx,
5033-
'</div>',
5034-
'</div>';
5035-
@ctx = ();
5036-
}
5037-
# empty add/rem block on start context block, or end of chunk
5038-
if ((@rem || @add) && (!$class || $class eq 'ctx')) {
5039-
if (!@add) {
5040-
# pure removal
5041-
print join '',
5042-
'<div class="chunk_block rem">',
5043-
'<div class="old">',
5044-
@rem,
5045-
'</div>',
5046-
'</div>';
5047-
} elsif (!@rem) {
5048-
# pure addition
5049-
print join '',
5050-
'<div class="chunk_block add">',
5051-
'<div class="new">',
5052-
@add,
5053-
'</div>',
5054-
'</div>';
5055-
} else {
5056-
# assume that it is change
5057-
print join '',
5058-
'<div class="chunk_block chg">',
5059-
'<div class="old">',
5060-
@rem,
5061-
'</div>',
5062-
'<div class="new">',
5063-
@add,
5064-
'</div>',
5065-
'</div>';
5066-
}
5067-
@rem = @add = ();
5214+
## print from accumulator when have some add/rem lines or end
5215+
# of chunk (flush context lines), or when have add and rem
5216+
# lines and new block is reached (otherwise add/rem lines could
5217+
# be reordered)
5218+
if (!$class || ((@rem || @add) && $class eq 'ctx') ||
5219+
(@rem && @add && $class ne $prev_class)) {
5220+
print_diff_lines(\@ctx, \@rem, \@add,
5221+
$diff_style, $num_parents);
5222+
@ctx = @rem = @add = ();
50685223
}
50695224

50705225
## adding lines to accumulator
@@ -5080,6 +5235,8 @@ sub print_sidebyside_diff_chunk {
50805235
if ($class eq 'ctx') {
50815236
push @ctx, $line;
50825237
}
5238+
5239+
$prev_class = $class;
50835240
}
50845241
}
50855242

@@ -5201,27 +5358,19 @@ sub git_patchset_body {
52015358

52025359
next PATCH if ($patch_line =~ m/^diff /);
52035360

5204-
my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
5205-
my $diff_classes = "diff";
5206-
$diff_classes .= " $class" if ($class);
5207-
$line = "<div class=\"$diff_classes\">$line</div>\n";
5361+
my $class = diff_line_class($patch_line, \%from, \%to);
52085362

5209-
if ($diff_style eq 'sidebyside' && !$is_combined) {
5210-
if ($class eq 'chunk_header') {
5211-
print_sidebyside_diff_chunk(@chunk);
5212-
@chunk = ( [ $class, $line ] );
5213-
} else {
5214-
push @chunk, [ $class, $line ];
5215-
}
5216-
} else {
5217-
# default 'inline' style and unknown styles
5218-
print $line;
5363+
if ($class eq 'chunk_header') {
5364+
print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5365+
@chunk = ();
52195366
}
5367+
5368+
push @chunk, [ $class, $patch_line ];
52205369
}
52215370

52225371
} continue {
52235372
if (@chunk) {
5224-
print_sidebyside_diff_chunk(@chunk);
5373+
print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
52255374
@chunk = ();
52265375
}
52275376
print "</div>\n"; # class="patch"

0 commit comments

Comments
 (0)