Code

gitweb: Give side-by-side diff extra CSS styling
[git.git] / gitweb / gitweb.perl
index 85d64b244dead86132de8a2a980cfbfc27c86494..f69ed08310df851c44bf2dc9cdb10b2ee2d73577 100755 (executable)
@@ -85,6 +85,8 @@ our $home_link_str = "++GITWEB_HOME_LINK_STR++";
 our $site_name = "++GITWEB_SITENAME++"
                  || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
 
+# html snippet to include in the <head> section of each page
+our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
 # filename of html text to include at top of each page
 our $site_header = "++GITWEB_SITE_HEADER++";
 # html text to include at home page
@@ -757,6 +759,7 @@ our @cgi_param_mapping = (
        extra_options => "opt",
        search_use_regexp => "sr",
        ctag => "by_tag",
+       diff_style => "ds",
        # this must be last entry (for manipulation from JavaScript)
        javascript => "js"
 );
@@ -2223,93 +2226,119 @@ sub format_diff_cc_simplified {
        return $result;
 }
 
-# format patch (diff) line (not to be used for diff headers)
-sub format_diff_line {
-       my $line = shift;
-       my ($from, $to) = @_;
-       my $diff_class = "";
-
-       chomp $line;
+sub diff_line_class {
+       my ($line, $from, $to) = @_;
 
+       # ordinary diff
+       my $num_sign = 1;
+       # combined diff
        if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
-               # combined diff
-               my $prefix = substr($line, 0, scalar @{$from->{'href'}});
-               if ($line =~ m/^\@{3}/) {
-                       $diff_class = " chunk_header";
-               } elsif ($line =~ m/^\\/) {
-                       $diff_class = " incomplete";
-               } elsif ($prefix =~ tr/+/+/) {
-                       $diff_class = " add";
-               } elsif ($prefix =~ tr/-/-/) {
-                       $diff_class = " rem";
-               }
-       } else {
-               # assume ordinary diff
-               my $char = substr($line, 0, 1);
-               if ($char eq '+') {
-                       $diff_class = " add";
-               } elsif ($char eq '-') {
-                       $diff_class = " rem";
-               } elsif ($char eq '@') {
-                       $diff_class = " chunk_header";
-               } elsif ($char eq "\\") {
-                       $diff_class = " incomplete";
-               }
+               $num_sign = scalar @{$from->{'href'}};
+       }
+
+       my @diff_line_classifier = (
+               { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
+               { regexp => qr/^\\/,               class => "incomplete"  },
+               { regexp => qr/^ {$num_sign}/,     class => "ctx" },
+               # classifier for context must come before classifier add/rem,
+               # or we would have to use more complicated regexp, for example
+               # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
+               { regexp => qr/^[+ ]{$num_sign}/,   class => "add" },
+               { regexp => qr/^[- ]{$num_sign}/,   class => "rem" },
+       );
+       for my $clsfy (@diff_line_classifier) {
+               return $clsfy->{'class'}
+                       if ($line =~ $clsfy->{'regexp'});
        }
-       $line = untabify($line);
-       if ($from && $to && $line =~ m/^\@{2} /) {
-               my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
-                       $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
 
-               $from_lines = 0 unless defined $from_lines;
-               $to_lines   = 0 unless defined $to_lines;
+       # fallback
+       return "";
+}
 
-               if ($from->{'href'}) {
-                       $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
-                                            -class=>"list"}, $from_text);
-               }
-               if ($to->{'href'}) {
-                       $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
-                                            -class=>"list"}, $to_text);
-               }
-               $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
-                       "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
-               return "<div class=\"diff$diff_class\">$line</div>\n";
-       } elsif ($from && $to && $line =~ m/^\@{3}/) {
-               my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
-               my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
+# assumes that $from and $to are defined and correctly filled,
+# and that $line holds a line of chunk header for unified diff
+sub format_unidiff_chunk_header {
+       my ($line, $from, $to) = @_;
 
-               @from_text = split(' ', $ranges);
-               for (my $i = 0; $i < @from_text; ++$i) {
-                       ($from_start[$i], $from_nlines[$i]) =
-                               (split(',', substr($from_text[$i], 1)), 0);
-               }
+       my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
+               $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
 
-               $to_text   = pop @from_text;
-               $to_start  = pop @from_start;
-               $to_nlines = pop @from_nlines;
+       $from_lines = 0 unless defined $from_lines;
+       $to_lines   = 0 unless defined $to_lines;
 
-               $line = "<span class=\"chunk_info\">$prefix ";
-               for (my $i = 0; $i < @from_text; ++$i) {
-                       if ($from->{'href'}[$i]) {
-                               $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
-                                                 -class=>"list"}, $from_text[$i]);
-                       } else {
-                               $line .= $from_text[$i];
-                       }
-                       $line .= " ";
-               }
-               if ($to->{'href'}) {
-                       $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
-                                         -class=>"list"}, $to_text);
+       if ($from->{'href'}) {
+               $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
+                                    -class=>"list"}, $from_text);
+       }
+       if ($to->{'href'}) {
+               $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
+                                    -class=>"list"}, $to_text);
+       }
+       $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
+               "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
+       return $line;
+}
+
+# assumes that $from and $to are defined and correctly filled,
+# and that $line holds a line of chunk header for combined diff
+sub format_cc_diff_chunk_header {
+       my ($line, $from, $to) = @_;
+
+       my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
+       my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
+
+       @from_text = split(' ', $ranges);
+       for (my $i = 0; $i < @from_text; ++$i) {
+               ($from_start[$i], $from_nlines[$i]) =
+                       (split(',', substr($from_text[$i], 1)), 0);
+       }
+
+       $to_text   = pop @from_text;
+       $to_start  = pop @from_start;
+       $to_nlines = pop @from_nlines;
+
+       $line = "<span class=\"chunk_info\">$prefix ";
+       for (my $i = 0; $i < @from_text; ++$i) {
+               if ($from->{'href'}[$i]) {
+                       $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
+                                         -class=>"list"}, $from_text[$i]);
                } else {
-                       $line .= $to_text;
+                       $line .= $from_text[$i];
                }
-               $line .= " $prefix</span>" .
-                        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
-               return "<div class=\"diff$diff_class\">$line</div>\n";
+               $line .= " ";
+       }
+       if ($to->{'href'}) {
+               $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
+                                 -class=>"list"}, $to_text);
+       } else {
+               $line .= $to_text;
        }
-       return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
+       $line .= " $prefix</span>" .
+                "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
+       return $line;
+}
+
+# process patch (diff) line (not to be used for diff headers),
+# returning class and HTML-formatted (but not wrapped) line
+sub process_diff_line {
+       my $line = shift;
+       my ($from, $to) = @_;
+
+       my $diff_class = diff_line_class($line, $from, $to);
+
+       chomp $line;
+       $line = untabify($line);
+
+       if ($from && $to && $line =~ m/^\@{2} /) {
+               $line = format_unidiff_chunk_header($line, $from, $to);
+               return $diff_class, $line;
+
+       } elsif ($from && $to && $line =~ m/^\@{3}/) {
+               $line = format_cc_diff_chunk_header($line, $from, $to);
+               return $diff_class, $line;
+
+       }
+       return $diff_class, esc_html($line, -nbsp=>1);
 }
 
 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
@@ -2886,7 +2915,7 @@ sub filter_forks_from_projects_list {
                $path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
                next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
                next unless ($path);      # skip '.git' repository: tests, git-instaweb
-               next unless (-d $path);   # containing directory exists
+               next unless (-d "$projectroot/$path"); # containing directory exists
                $pr->{'forks'} = [];      # there can be 0 or more forks of project
 
                # add to trie
@@ -3879,6 +3908,11 @@ EOF
                print "<base href=\"".esc_url($base_url)."\" />\n";
        }
        print_header_links($status);
+
+       if (defined $site_html_head_string) {
+               print to_utf8($site_html_head_string);
+       }
+
        print "</head>\n" .
              "<body>\n";
 
@@ -4826,8 +4860,97 @@ sub git_difftree_body {
        print "</table>\n";
 }
 
+sub print_sidebyside_diff_chunk {
+       my @chunk = @_;
+       my (@ctx, @rem, @add);
+
+       return unless @chunk;
+
+       # incomplete last line might be among removed or added lines,
+       # or both, or among context lines: find which
+       for (my $i = 1; $i < @chunk; $i++) {
+               if ($chunk[$i][0] eq 'incomplete') {
+                       $chunk[$i][0] = $chunk[$i-1][0];
+               }
+       }
+
+       # guardian
+       push @chunk, ["", ""];
+
+       foreach my $line_info (@chunk) {
+               my ($class, $line) = @$line_info;
+
+               # print chunk headers
+               if ($class && $class eq 'chunk_header') {
+                       print $line;
+                       next;
+               }
+
+               ## print from accumulator when type of class of lines change
+               # empty contents block on start rem/add block, or end of chunk
+               if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
+                       print join '',
+                               '<div class="chunk_block ctx">',
+                                       '<div class="old">',
+                                       @ctx,
+                                       '</div>',
+                                       '<div class="new">',
+                                       @ctx,
+                                       '</div>',
+                               '</div>';
+                       @ctx = ();
+               }
+               # empty add/rem block on start context block, or end of chunk
+               if ((@rem || @add) && (!$class || $class eq 'ctx')) {
+                       if (!@add) {
+                               # pure removal
+                               print join '',
+                                       '<div class="chunk_block rem">',
+                                               '<div class="old">',
+                                               @rem,
+                                               '</div>',
+                                       '</div>';
+                       } elsif (!@rem) {
+                               # pure addition
+                               print join '',
+                                       '<div class="chunk_block add">',
+                                               '<div class="new">',
+                                               @add,
+                                               '</div>',
+                                       '</div>';
+                       } else {
+                               # assume that it is change
+                               print join '',
+                                       '<div class="chunk_block chg">',
+                                               '<div class="old">',
+                                               @rem,
+                                               '</div>',
+                                               '<div class="new">',
+                                               @add,
+                                               '</div>',
+                                       '</div>';
+                       }
+                       @rem = @add = ();
+               }
+
+               ## adding lines to accumulator
+               # guardian value
+               last unless $line;
+               # rem, add or change
+               if ($class eq 'rem') {
+                       push @rem, $line;
+               } elsif ($class eq 'add') {
+                       push @add, $line;
+               }
+               # context line
+               if ($class eq 'ctx') {
+                       push @ctx, $line;
+               }
+       }
+}
+
 sub git_patchset_body {
-       my ($fd, $difftree, $hash, @hash_parents) = @_;
+       my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
        my ($hash_parent) = $hash_parents[0];
 
        my $is_combined = (@hash_parents > 1);
@@ -4837,6 +4960,7 @@ sub git_patchset_body {
        my $diffinfo;
        my $to_name;
        my (%from, %to);
+       my @chunk; # for side-by-side diff
 
        print "<div class=\"patchset\">\n";
 
@@ -4943,10 +5067,29 @@ sub git_patchset_body {
 
                        next PATCH if ($patch_line =~ m/^diff /);
 
-                       print format_diff_line($patch_line, \%from, \%to);
+                       my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
+                       my $diff_classes = "diff";
+                       $diff_classes .= " $class" if ($class);
+                       $line = "<div class=\"$diff_classes\">$line</div>\n";
+
+                       if ($diff_style eq 'sidebyside' && !$is_combined) {
+                               if ($class eq 'chunk_header') {
+                                       print_sidebyside_diff_chunk(@chunk);
+                                       @chunk = ( [ $class, $line ] );
+                               } else {
+                                       push @chunk, [ $class, $line ];
+                               }
+                       } else {
+                               # default 'inline' style and unknown styles
+                               print $line;
+                       }
                }
 
        } continue {
+               if (@chunk) {
+                       print_sidebyside_diff_chunk(@chunk);
+                       @chunk = ();
+               }
                print "</div>\n"; # class="patch"
        }
 
@@ -6942,6 +7085,7 @@ sub git_object {
 
 sub git_blobdiff {
        my $format = shift || 'html';
+       my $diff_style = $input_params{'diff_style'} || 'inline';
 
        my $fd;
        my @difftree;
@@ -7051,7 +7195,8 @@ sub git_blobdiff {
        if ($format eq 'html') {
                print "<div class=\"page_body\">\n";
 
-               git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
+               git_patchset_body($fd, $diff_style,
+                                 [ \%diffinfo ], $hash_base, $hash_parent_base);
                close $fd;
 
                print "</div>\n"; # class="page_body"
@@ -7079,6 +7224,7 @@ sub git_blobdiff_plain {
 sub git_commitdiff {
        my %params = @_;
        my $format = $params{-format} || 'html';
+       my $diff_style = $input_params{'diff_style'} || 'inline';
 
        my ($patch_max) = gitweb_get_feature('patches');
        if ($format eq 'patch') {
@@ -7282,7 +7428,8 @@ sub git_commitdiff {
                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
                print "<br/>\n";
 
-               git_patchset_body($fd, \@difftree, $hash,
+               git_patchset_body($fd, $diff_style,
+                                 \@difftree, $hash,
                                  $use_parents ? @{$co{'parents'}} : $hash_parent);
                close $fd;
                print "</div>\n"; # class="page_body"