+ # match <path>
+ if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
+ $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+ esc_path($from->{'file'}));
+ }
+ if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
+ $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+ esc_path($to->{'file'}));
+ }
+ # match single <mode>
+ if ($line =~ m/\s(\d{6})$/) {
+ $line .= '<span class="info"> (' .
+ file_type_long($1) .
+ ')</span>';
+ }
+ # match <hash>
+ if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
+ # can match only for combined diff
+ $line = 'index ';
+ for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+ if ($from->{'href'}[$i]) {
+ $line .= $cgi->a({-href=>$from->{'href'}[$i],
+ -class=>"hash"},
+ substr($diffinfo->{'from_id'}[$i],0,7));
+ } else {
+ $line .= '0' x 7;
+ }
+ # separator
+ $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
+ }
+ $line .= '..';
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'to_id'},0,7));
+ } else {
+ $line .= '0' x 7;
+ }
+
+ } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
+ # can match only for ordinary diff
+ my ($from_link, $to_link);
+ if ($from->{'href'}) {
+ $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'from_id'},0,7));
+ } else {
+ $from_link = '0' x 7;
+ }
+ if ($to->{'href'}) {
+ $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
+ substr($diffinfo->{'to_id'},0,7));
+ } else {
+ $to_link = '0' x 7;
+ }
+ my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
+ $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
+ }
+
+ return $line . "<br/>\n";
+}
+
+# format from-file/to-file diff header
+sub format_diff_from_to_header {
+ my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
+ my $line;
+ my $result = '';
+
+ $line = $from_line;
+ #assert($line =~ m/^---/) if DEBUG;
+ # no extra formatting for "^--- /dev/null"
+ if (! $diffinfo->{'nparents'}) {
+ # ordinary (single parent) diff
+ if ($line =~ m!^--- "?a/!) {
+ if ($from->{'href'}) {
+ $line = '--- a/' .
+ $cgi->a({-href=>$from->{'href'}, -class=>"path"},
+ esc_path($from->{'file'}));
+ } else {
+ $line = '--- a/' .
+ esc_path($from->{'file'});
+ }
+ }
+ $result .= qq!<div class="diff from_file">$line</div>\n!;
+
+ } else {
+ # combined diff (merge commit)
+ for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
+ if ($from->{'href'}[$i]) {
+ $line = '--- ' .
+ $cgi->a({-href=>href(action=>"blobdiff",
+ hash_parent=>$diffinfo->{'from_id'}[$i],
+ hash_parent_base=>$parents[$i],
+ file_parent=>$from->{'file'}[$i],
+ hash=>$diffinfo->{'to_id'},
+ hash_base=>$hash,
+ file_name=>$to->{'file'}),
+ -class=>"path",
+ -title=>"diff" . ($i+1)},
+ $i+1) .
+ '/' .
+ $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
+ esc_path($from->{'file'}[$i]));
+ } else {
+ $line = '--- /dev/null';
+ }
+ $result .= qq!<div class="diff from_file">$line</div>\n!;
+ }
+ }
+
+ $line = $to_line;
+ #assert($line =~ m/^\+\+\+/) if DEBUG;
+ # no extra formatting for "^+++ /dev/null"
+ if ($line =~ m!^\+\+\+ "?b/!) {
+ if ($to->{'href'}) {
+ $line = '+++ b/' .
+ $cgi->a({-href=>$to->{'href'}, -class=>"path"},
+ esc_path($to->{'file'}));
+ } else {
+ $line = '+++ b/' .
+ esc_path($to->{'file'});
+ }
+ }
+ $result .= qq!<div class="diff to_file">$line</div>\n!;
+
+ return $result;
+}
+
+# create note for patch simplified by combined diff
+sub format_diff_cc_simplified {
+ my ($diffinfo, @parents) = @_;
+ my $result = '';
+
+ $result .= "<div class=\"diff header\">" .
+ "diff --cc ";
+ if (!is_deleted($diffinfo)) {
+ $result .= $cgi->a({-href => href(action=>"blob",
+ hash_base=>$hash,
+ hash=>$diffinfo->{'to_id'},
+ file_name=>$diffinfo->{'to_file'}),
+ -class => "path"},
+ esc_path($diffinfo->{'to_file'}));
+ } else {
+ $result .= esc_path($diffinfo->{'to_file'});
+ }
+ $result .= "</div>\n" . # class="diff header"
+ "<div class=\"diff nodifferences\">" .
+ "Simple merge" .
+ "</div>\n"; # class="diff nodifferences"
+
+ return $result;
+}
+
+sub diff_line_class {
+ my ($line, $from, $to) = @_;
+
+ # ordinary diff
+ my $num_sign = 1;
+ # combined diff
+ if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
+ $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'});
+ }
+
+ # fallback
+ return "";
+}
+
+# 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) = @_;
+
+ 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;
+
+ 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 .= $from_text[$i];
+ }
+ $line .= " ";
+ }
+ if ($to->{'href'}) {
+ $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
+ -class=>"list"}, $to_text);
+ } else {
+ $line .= $to_text;
+ }
+ $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_)",
+# linked. Pass the hash of the tree/commit to snapshot.
+sub format_snapshot_links {
+ my ($hash) = @_;
+ my $num_fmts = @snapshot_fmts;
+ if ($num_fmts > 1) {
+ # A parenthesized list of links bearing format names.
+ # e.g. "snapshot (_tar.gz_ _zip_)"
+ return "snapshot (" . join(' ', map
+ $cgi->a({
+ -href => href(
+ action=>"snapshot",
+ hash=>$hash,
+ snapshot_format=>$_
+ )
+ }, $known_snapshot_formats{$_}{'display'})
+ , @snapshot_fmts) . ")";
+ } elsif ($num_fmts == 1) {
+ # A single "snapshot" link whose tooltip bears the format name.
+ # i.e. "_snapshot_"
+ my ($fmt) = @snapshot_fmts;
+ return
+ $cgi->a({
+ -href => href(
+ action=>"snapshot",
+ hash=>$hash,
+ snapshot_format=>$fmt
+ ),
+ -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
+ }, "snapshot");
+ } else { # $num_fmts == 0
+ return undef;
+ }
+}
+
+## ......................................................................
+## functions returning values to be passed, perhaps after some
+## transformation, to other functions; e.g. returning arguments to href()
+
+# returns hash to be passed to href to generate gitweb URL
+# in -title key it returns description of link
+sub get_feed_info {
+ my $format = shift || 'Atom';
+ my %res = (action => lc($format));
+
+ # feed links are possible only for project views
+ return unless (defined $project);
+ # some views should link to OPML, or to generic project feed,
+ # or don't have specific feed yet (so they should use generic)
+ return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
+
+ my $branch;
+ # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
+ # from tag links; this also makes possible to detect branch links
+ if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
+ (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
+ $branch = $1;
+ }
+ # find log type for feed description (title)
+ my $type = 'log';
+ if (defined $file_name) {
+ $type = "history of $file_name";
+ $type .= "/" if ($action eq 'tree');
+ $type .= " on '$branch'" if (defined $branch);
+ } else {
+ $type = "log of $branch" if (defined $branch);
+ }
+
+ $res{-title} = $type;
+ $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
+ $res{'file_name'} = $file_name;
+
+ return %res;
+}
+
+## ----------------------------------------------------------------------
+## git utility subroutines, invoking git commands
+
+# returns path to the core git executable and the --git-dir parameter as list
+sub git_cmd {
+ $number_of_git_cmds++;
+ return $GIT, '--git-dir='.$git_dir;
+}
+
+# quote the given arguments for passing them to the shell
+# quote_command("command", "arg 1", "arg with ' and ! characters")
+# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
+# Try to avoid using this function wherever possible.
+sub quote_command {
+ return join(' ',
+ map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
+}
+
+# get HEAD ref of given project as hash
+sub git_get_head_hash {
+ return git_get_full_hash(shift, 'HEAD');
+}
+
+sub git_get_full_hash {
+ return git_get_hash(@_);
+}
+
+sub git_get_short_hash {
+ return git_get_hash(@_, '--short=7');
+}
+
+sub git_get_hash {
+ my ($project, $hash, @options) = @_;