gitweb: Add a feature to show side-by-side diff

This commits adds to support for showing "side-by-side" style diff. Currently you have to hand-craft the URL; navigation for selecting diff style is to be added in the next commit. The diff output in unified format from "git diff-tree" is reorganized to side-by-side style chunk by chunk with format_sidebyside_diff_chunk(). This reorganization requires knowledge about diff line classification, so format_diff_line() was renamed to process_diff_line(), and changed to return tuple (list) consisting of class of diff line and of HTML-formatted (but not wrapped in <div class="diff ...">...</div>) diff line. Wrapping is now done by caller, i.e. git_patchset_body(). Gitweb uses float+margin CSS-based layout for "side by side" diff. You can specify style of diff with "ds" ('diff_style') query parameter. Currently supported values are 'inline' and 'sidebyside'; the default is 'inline'. Another solution would be to use "opt" ('extra_options') for that... though current use of it in gitweb seems to suggest that "opt" is more about passing extra options to underlying git commands, and "git diff" doesn't support '--side-by-side' like GNU diff does, (yet?). Signed-off-by: Kato Kazuyoshi <kato.kazuyoshi@gmail.com> Signed-off-by: Jakub Narebski <jnareb@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kato Kazuyoshi committed Oct 31, 2011 at 00:36 UTC 6ba1eb51b94c4777d84e4c9c868096735b1e16dd
2 files changed +122 -11
gitweb/gitweb.perl
+105 -11
@@ -759,6 +759,7 @@ our @cgi_param_mapping = (
759 extra_options => "opt",
760 search_use_regexp => "sr",
761 ctag => "by_tag",
762 + diff_style => "ds",
763 # this must be last entry (for manipulation from JavaScript)
764 javascript => "js"
765 );
@@ -2317,28 +2318,27 @@ sub format_cc_diff_chunk_header {
2318 return $line;
2319 }
2320
2320 -# format patch (diff) line (not to be used for diff headers)
2321 -sub format_diff_line {
2321 +# process patch (diff) line (not to be used for diff headers),
2322 +# returning class and HTML-formatted (but not wrapped) line
2323 +sub process_diff_line {
2324 my $line = shift;
2325 my ($from, $to) = @_;
2326
2327 my $diff_class = diff_line_class($line, $from, $to);
2326 - my $diff_classes = "diff";
2327 - $diff_classes .= " $diff_class" if ($diff_class);
2328
2329 chomp $line;
2330 $line = untabify($line);
2331
2332 if ($from && $to && $line =~ m/^\@{2} /) {
2333 $line = format_unidiff_chunk_header($line, $from, $to);
2334 - return "<div class=\"$diff_classes\">$line</div>\n";
2334 + return $diff_class, $line;
2335
2336 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2337 $line = format_cc_diff_chunk_header($line, $from, $to);
2338 - return "<div class=\"$diff_classes\">$line</div>\n";
2338 + return $diff_class, $line;
2339
2340 }
2341 - return "<div class=\"$diff_classes\">" . esc_html($line, -nbsp=>1) . "</div>\n";
2341 + return $diff_class, esc_html($line, -nbsp=>1);
2342 }
2343
2344 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
@@ -4860,8 +4860,78 @@ sub git_difftree_body {
4860 print "</table>\n";
4861 }
4862
4863 +sub print_sidebyside_diff_chunk {
4864 + my @chunk = @_;
4865 + my (@ctx, @rem, @add);
4866 +
4867 + return unless @chunk;
4868 +
4869 + # incomplete last line might be among removed or added lines,
4870 + # or both, or among context lines: find which
4871 + for (my $i = 1; $i < @chunk; $i++) {
4872 + if ($chunk[$i][0] eq 'incomplete') {
4873 + $chunk[$i][0] = $chunk[$i-1][0];
4874 + }
4875 + }
4876 +
4877 + # guardian
4878 + push @chunk, ["", ""];
4879 +
4880 + foreach my $line_info (@chunk) {
4881 + my ($class, $line) = @$line_info;
4882 +
4883 + # print chunk headers
4884 + if ($class && $class eq 'chunk_header') {
4885 + print $line;
4886 + next;
4887 + }
4888 +
4889 + ## print from accumulator when type of class of lines change
4890 + # empty contents block on start rem/add block, or end of chunk
4891 + if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
4892 + print join '',
4893 + '<div class="chunk_block">',
4894 + '<div class="old">',
4895 + @ctx,
4896 + '</div>',
4897 + '<div class="new">',
4898 + @ctx,
4899 + '</div>',
4900 + '</div>';
4901 + @ctx = ();
4902 + }
4903 + # empty add/rem block on start context block, or end of chunk
4904 + if ((@rem || @add) && (!$class || $class eq 'ctx')) {
4905 + print join '',
4906 + '<div class="chunk_block">',
4907 + '<div class="old">',
4908 + @rem,
4909 + '</div>',
4910 + '<div class="new">',
4911 + @add,
4912 + '</div>',
4913 + '</div>';
4914 + @rem = @add = ();
4915 + }
4916 +
4917 + ## adding lines to accumulator
4918 + # guardian value
4919 + last unless $line;
4920 + # rem, add or change
4921 + if ($class eq 'rem') {
4922 + push @rem, $line;
4923 + } elsif ($class eq 'add') {
4924 + push @add, $line;
4925 + }
4926 + # context line
4927 + if ($class eq 'ctx') {
4928 + push @ctx, $line;
4929 + }
4930 + }
4931 +}
4932 +
4933 sub git_patchset_body {
4864 - my ($fd, $difftree, $hash, @hash_parents) = @_;
4934 + my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
4935 my ($hash_parent) = $hash_parents[0];
4936
4937 my $is_combined = (@hash_parents > 1);
@@ -4871,6 +4941,7 @@ sub git_patchset_body {
4941 my $diffinfo;
4942 my $to_name;
4943 my (%from, %to);
4944 + my @chunk; # for side-by-side diff
4945
4946 print "<div class=\"patchset\">\n";
4947
@@ -4977,10 +5048,29 @@ sub git_patchset_body {
5048
5049 next PATCH if ($patch_line =~ m/^diff /);
5050
4980 - print format_diff_line($patch_line, \%from, \%to);
5051 + my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
5052 + my $diff_classes = "diff";
5053 + $diff_classes .= " $class" if ($class);
5054 + $line = "<div class=\"$diff_classes\">$line</div>\n";
5055 +
5056 + if ($diff_style eq 'sidebyside' && !$is_combined) {
5057 + if ($class eq 'chunk_header') {
5058 + print_sidebyside_diff_chunk(@chunk);
5059 + @chunk = ( [ $class, $line ] );
5060 + } else {
5061 + push @chunk, [ $class, $line ];
5062 + }
5063 + } else {
5064 + # default 'inline' style and unknown styles
5065 + print $line;
5066 + }
5067 }
5068
5069 } continue {
5070 + if (@chunk) {
5071 + print_sidebyside_diff_chunk(@chunk);
5072 + @chunk = ();
5073 + }
5074 print "</div>\n"; # class="patch"
5075 }
5076
@@ -6976,6 +7066,7 @@ sub git_object {
7066
7067 sub git_blobdiff {
7068 my $format = shift || 'html';
7069 + my $diff_style = $input_params{'diff_style'} || 'inline';
7070
7071 my $fd;
7072 my @difftree;
@@ -7085,7 +7176,8 @@ sub git_blobdiff {
7176 if ($format eq 'html') {
7177 print "<div class=\"page_body\">\n";
7178
7088 - git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
7179 + git_patchset_body($fd, $diff_style,
7180 + [ \%diffinfo ], $hash_base, $hash_parent_base);
7181 close $fd;
7182
7183 print "</div>\n"; # class="page_body"
@@ -7113,6 +7205,7 @@ sub git_blobdiff_plain {
7205 sub git_commitdiff {
7206 my %params = @_;
7207 my $format = $params{-format} || 'html';
7208 + my $diff_style = $input_params{'diff_style'} || 'inline';
7209
7210 my ($patch_max) = gitweb_get_feature('patches');
7211 if ($format eq 'patch') {
@@ -7316,7 +7409,8 @@ sub git_commitdiff {
7409 $use_parents ? @{$co{'parents'}} : $hash_parent);
7410 print "<br/>\n";
7411
7319 - git_patchset_body($fd, \@difftree, $hash,
7412 + git_patchset_body($fd, $diff_style,
7413 + \@difftree, $hash,
7414 $use_parents ? @{$co{'parents'}} : $hash_parent);
7415 close $fd;
7416 print "</div>\n"; # class="page_body"
gitweb/static/gitweb.css
+17
@@ -475,6 +475,23 @@ div.diff.nodifferences {
475 color: #600000;
476 }
477
478 +/* side-by-side diff */
479 +div.chunk_block {
480 + overflow: hidden;
481 +}
482 +
483 +div.chunk_block div.old {
484 + float: left;
485 + width: 50%;
486 + overflow: hidden;
487 +}
488 +
489 +div.chunk_block div.new {
490 + margin-left: 50%;
491 + width: 50%;
492 +}
493 +
494 +
495 div.index_include {
496 border: solid #d9d8d1;
497 border-width: 0px 0px 1px;