Merge branch 'master' into gl/web
authorJunio C Hamano <junkio@cox.net>
Sat, 26 Aug 2006 08:08:39 +0000 (01:08 -0700)
committerJunio C Hamano <junkio@cox.net>
Sat, 26 Aug 2006 08:08:39 +0000 (01:08 -0700)
* master: (34 commits)
gitweb: git_annotate didn't expect negative numeric timezone
git-svn: add the 'dcommit' command
git-svn: recommend rebase for syncing against an SVN repo
git-svn: establish new connections on commit after fork
describe: fix off-by-one error in --abbrev=40 handling
git-svn(1): improve asciidoc markup
gitview.txt: improve asciidoc markup
git(7): put the synopsis in a verse style paragraph
gitk(1): expand the manpage to look less like a template
git-blame(1): mention options in the synopsis and advertise pickaxe
git-ls-remote(1): document --upload-pack
git-apply(1): document missing options and improve existing ones
update-index -g
n is in fact unused, and is later shadowed.
use name[len] in switch directly, instead of creating a shadowed variable.
builtin-grep.c: remove unused debugging piece.
remove ugly shadowing of loop indexes in subloops.
missing 'static' keywords
git_dir holds pointers to local strings, hence MUST be const.
avoid to use error that shadows the function name, use err instead.
...

1  2 
gitweb/gitweb.perl
diff --combined gitweb/gitweb.perl
index 0ae18103c1cba74a7278de1b24b8052467be882b,966c54a63cacf20d2a71241f738df0378e46f569..e00a6ed6e0689efe0405041040e9a852bbeb7020
@@@ -15,7 -15,6 +15,7 @@@ use CGI::Carp qw(fatalsToBrowser)
  use Encode;
  use Fcntl ':mode';
  use File::Find qw();
 +use File::Basename qw(basename);
  binmode STDOUT, ':utf8';
  
  our $cgi = new CGI;
@@@ -31,8 -30,11 +31,8 @@@ our $GIT = "++GIT_BINDIR++/git"
  #our $projectroot = "/pub/scm";
  our $projectroot = "++GITWEB_PROJECTROOT++";
  
 -# location for temporary files needed for diffs
 -our $git_temp = "/tmp/gitweb";
 -
  # target of the home link on top of all pages
 -our $home_link = $my_uri;
 +our $home_link = $my_uri || "/";
  
  # string of the home link on top of all pages
  our $home_link_str = "++GITWEB_HOME_LINK_STR++";
@@@ -64,76 -66,6 +64,76 @@@ our $default_text_plain_charset  = unde
  # (relative to the current git repository)
  our $mimetypes_file = undef;
  
 +# You define site-wide feature defaults here; override them with
 +# $GITWEB_CONFIG as necessary.
 +our %feature = (
 +      # feature => {'sub' => feature-sub, 'override' => allow-override, 'default' => [ default options...]
 +      # if feature is overridable, feature-sub will be called with default options;
 +      # return value indicates if to enable specified feature
 +
 +      'blame' => {
 +              'sub' => \&feature_blame,
 +              'override' => 0,
 +              'default' => [0]},
 +
 +      'snapshot' => {
 +              'sub' => \&feature_snapshot,
 +              'override' => 0,
 +              #         => [content-encoding, suffix, program]
 +              'default' => ['x-gzip', 'gz', 'gzip']},
 +);
 +
 +sub gitweb_check_feature {
 +      my ($name) = @_;
 +      return undef unless exists $feature{$name};
 +      my ($sub, $override, @defaults) = (
 +              $feature{$name}{'sub'},
 +              $feature{$name}{'override'},
 +              @{$feature{$name}{'default'}});
 +      if (!$override) { return @defaults; }
 +      return $sub->(@defaults);
 +}
 +
 +# To enable system wide have in $GITWEB_CONFIG
 +# $feature{'blame'}{'default'} =  [1];
 +# To have project specific config enable override in  $GITWEB_CONFIG
 +# $feature{'blame'}{'override'} =  1;
 +# and in project config gitweb.blame = 0|1;
 +
 +sub feature_blame {
 +      my ($val) = git_get_project_config('blame', '--bool');
 +
 +      if ($val eq 'true') {
 +              return 1;
 +      } elsif ($val eq 'false') {
 +              return 0;
 +      }
 +
 +      return $_[0];
 +}
 +
 +# To disable system wide have in $GITWEB_CONFIG
 +# $feature{'snapshot'}{'default'} =  [undef];
 +# To have project specific config enable override in  $GITWEB_CONFIG
 +# $feature{'blame'}{'override'} =  1;
 +# and in project config  gitweb.snapshot = none|gzip|bzip2
 +
 +sub feature_snapshot {
 +      my ($ctype, $suffix, $command) = @_;
 +
 +      my ($val) = git_get_project_config('snapshot');
 +
 +      if ($val eq 'gzip') {
 +              return ('x-gzip', 'gz', 'gzip');
 +      } elsif ($val eq 'bzip2') {
 +              return ('x-bzip2', 'bz2', 'bzip2');
 +      } elsif ($val eq 'none') {
 +              return ();
 +      }
 +
 +      return ($ctype, $suffix, $command);
 +}
 +
  our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
  require $GITWEB_CONFIG if -e $GITWEB_CONFIG;
  
  our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
  
  $projects_list ||= $projectroot;
 -if (! -d $git_temp) {
 -      mkdir($git_temp, 0700) || die_error(undef, "Couldn't mkdir $git_temp");
 -}
  
  # ======================================================================
  # input validation and dispatch
@@@ -149,15 -84,19 +149,15 @@@ if (defined $action) 
        if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
                die_error(undef, "Invalid action parameter");
        }
 -      # action which does not check rest of parameters
 -      if ($action eq "opml") {
 -              git_opml();
 -              exit;
 -      }
  }
  
  our $project = ($cgi->param('p') || $ENV{'PATH_INFO'});
  if (defined $project) {
        $project =~ s|^/||;
        $project =~ s|/$||;
 +      $project = undef unless $project;
  }
 -if (defined $project && $project) {
 +if (defined $project) {
        if (!validate_input($project)) {
                die_error(undef, "Invalid project parameter");
        }
                die_error(undef, "No such project");
        }
        $ENV{'GIT_DIR'} = "$projectroot/$project";
 -} else {
 -      git_project_list();
 -      exit;
  }
  
  our $file_name = $cgi->param('f');
@@@ -177,13 -119,6 +177,13 @@@ if (defined $file_name) 
        }
  }
  
 +our $file_parent = $cgi->param('fp');
 +if (defined $file_parent) {
 +      if (!validate_input($file_parent)) {
 +              die_error(undef, "Invalid file parent parameter");
 +      }
 +}
 +
  our $hash = $cgi->param('h');
  if (defined $hash) {
        if (!validate_input($hash)) {
@@@ -205,13 -140,6 +205,13 @@@ if (defined $hash_base) 
        }
  }
  
 +our $hash_parent_base = $cgi->param('hpb');
 +if (defined $hash_parent_base) {
 +      if (!validate_input($hash_parent_base)) {
 +              die_error(undef, "Invalid hash parent base parameter");
 +      }
 +}
 +
  our $page = $cgi->param('pg');
  if (defined $page) {
        if ($page =~ m/[^0-9]$/) {
@@@ -247,17 -175,9 +247,17 @@@ my %actions = 
        "tag" => \&git_tag,
        "tags" => \&git_tags,
        "tree" => \&git_tree,
 +      "snapshot" => \&git_snapshot,
 +      # those below don't need $project
 +      "opml" => \&git_opml,
 +      "project_list" => \&git_project_list,
  );
  
 -$action = 'summary' if (!defined($action));
 +if (defined $project) {
 +      $action ||= 'summary';
 +} else {
 +      $action ||= 'project_list';
 +}
  if (!defined($actions{$action})) {
        die_error(undef, "Unknown action");
  }
@@@ -268,32 -188,26 +268,32 @@@ exit
  ## action links
  
  sub href(%) {
 -      my %mapping = (
 -              action => "a",
 +      my %params = @_;
 +
 +      my @mapping = (
                project => "p",
 +              action => "a",
                file_name => "f",
 +              file_parent => "fp",
                hash => "h",
                hash_parent => "hp",
                hash_base => "hb",
 +              hash_parent_base => "hpb",
                page => "pg",
                searchtext => "s",
        );
 +      my %mapping = @mapping;
  
 -      my %params = @_;
        $params{"project"} ||= $project;
  
 -      my $href = "$my_uri?";
 -      $href .= esc_param( join(";",
 -              map { "$mapping{$_}=$params{$_}" } keys %params
 -      ) );
 -
 -      return $href;
 +      my @result = ();
 +      for (my $i = 0; $i < @mapping; $i += 2) {
 +              my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
 +              if (defined $params{$name}) {
 +                      push @result, $symbol . "=" . esc_param($params{$name});
 +              }
 +      }
 +      return "$my_uri?" . join(';', @result);
  }
  
  
@@@ -448,13 -362,7 +448,13 @@@ sub mode_str 
  
  # convert file mode in octal to file type string
  sub file_type {
 -      my $mode = oct shift;
 +      my $mode = shift;
 +
 +      if ($mode !~ m/^[0-7]+$/) {
 +              return $mode;
 +      } else {
 +              $mode = oct $mode;
 +      }
  
        if (S_ISDIR($mode & S_IFMT)) {
                return "directory";
@@@ -480,9 -388,7 +480,9 @@@ sub format_log_line_html 
        if ($line =~ m/([0-9a-fA-F]{40})/) {
                my $hash_text = $1;
                if (git_get_type($hash_text) eq "commit") {
 -                      my $link = $cgi->a({-class => "text", -href => href(action=>"commit", hash=>$hash_text)}, $hash_text);
 +                      my $link =
 +                              $cgi->a({-href => href(action=>"commit", hash=>$hash_text),
 +                                      -class => "text"}, $hash_text);
                        $line =~ s/$hash_text/$link/;
                }
        }
@@@ -523,35 -429,15 +523,35 @@@ sub format_subject_html 
        $extra = '' unless defined($extra);
  
        if (length($short) < length($long)) {
 -              return $cgi->a({-href => $href, -class => "list",
 +              return $cgi->a({-href => $href, -class => "list subject",
                                -title => $long},
                       esc_html($short) . $extra);
        } else {
 -              return $cgi->a({-href => $href, -class => "list"},
 +              return $cgi->a({-href => $href, -class => "list subject"},
                       esc_html($long)  . $extra);
        }
  }
  
 +sub format_diff_line {
 +      my $line = shift;
 +      my $char = substr($line, 0, 1);
 +      my $diff_class = "";
 +
 +      chomp $line;
 +
 +      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";
 +      }
 +      $line = untabify($line);
 +      return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
 +}
 +
  ## ----------------------------------------------------------------------
  ## git utility subroutines, invoking git commands
  
@@@ -586,21 -472,24 +586,21 @@@ sub git_get_type 
  }
  
  sub git_get_project_config {
 -      my $key = shift;
 +      my ($key, $type) = @_;
  
        return unless ($key);
        $key =~ s/^gitweb\.//;
        return if ($key =~ m/\W/);
  
 -      my $val = qx($GIT repo-config --get gitweb.$key);
 +      my @x = ($GIT, 'repo-config');
 +      if (defined $type) { push @x, $type; }
 +      push @x, "--get";
 +      push @x, "gitweb.$key";
 +      my $val = qx(@x);
 +      chomp $val;
        return ($val);
  }
  
 -sub git_get_project_config_bool {
 -      my $val = git_get_project_config (@_);
 -      if ($val and $val =~ m/true|yes|on/) {
 -              return (1);
 -      }
 -      return; # implicit false
 -}
 -
  # get hash of given path at given ref
  sub git_get_hash_by_path {
        my $base = shift;
        return $3;
  }
  
 +# converts symbolic name to hash
 +sub git_to_hash {
 +      my @params = @_;
 +      return undef unless @params;
 +
 +      open my $fd, "-|", $GIT, "rev-parse", @params
 +              or return undef;
 +      my @hashes = map { chomp; $_ } <$fd>;
 +      close $fd;
 +
 +      if (wantarray) {
 +              return @hashes;
 +      } elsif (scalar(@hashes) == 1) {
 +              # single hash
 +              return $hashes[0];
 +      } else {
 +              return \@hashes;
 +      }
 +}
 +
  ## ......................................................................
  ## git utility functions, directly accessing git repository
  
@@@ -777,73 -646,6 +777,73 @@@ sub git_get_references 
        return \%refs;
  }
  
 +sub git_get_following_references {
 +      my $hash = shift || return undef;
 +      my $type = shift;
 +      my $base = shift || $hash_base || "HEAD";
 +
 +      my $refs = git_get_references($type);
 +      open my $fd, "-|", $GIT, "rev-list", $base
 +              or return undef;
 +      my @commits = map { chomp; $_ } <$fd>;
 +      close $fd
 +              or return undef;
 +
 +      my @reflist;
 +      my $lastref;
 +
 +      foreach my $commit (@commits) {
 +              foreach my $ref (@{$refs->{$commit}}) {
 +                      $lastref = $ref;
 +                      push @reflist, $lastref;
 +              }
 +              if ($commit eq $hash) {
 +                      last;
 +              }
 +      }
 +
 +      return wantarray ? @reflist : $lastref;
 +}
 +
 +sub git_get_preceding_references {
 +      my $hash = shift || return undef;
 +      my $type = shift;
 +
 +      my $refs = git_get_references($type);
 +      open my $fd, "-|", $GIT, "rev-list", $hash
 +              or return undef;
 +      my @commits = map { chomp; $_ } <$fd>;
 +      close $fd
 +              or return undef;
 +
 +      my @reflist;
 +
 +      foreach my $commit (@commits) {
 +              foreach my $ref (@{$refs->{$commit}}) {
 +                      return $ref unless wantarray;
 +                      push @reflist, $ref;
 +              }
 +      }
 +
 +      return @reflist;
 +}
 +
 +sub git_get_rev_name_tags {
 +      my $hash = shift || return undef;
 +
 +      open my $fd, "-|", $GIT, "name-rev", "--tags", $hash
 +              or return;
 +      my $name_rev = <$fd>;
 +      close $fd;
 +
 +      if ($name_rev =~ m|^$hash tags/(.*)$|) {
 +              return $1;
 +      } else {
 +              # catches also '$hash undefined' output
 +              return undef;
 +      }
 +}
 +
  ## ----------------------------------------------------------------------
  ## parse to hash functions
  
@@@ -860,10 -662,8 +860,10 @@@ sub parse_date 
        $date{'mday'} = $mday;
        $date{'day'} = $days[$wday];
        $date{'month'} = $months[$mon];
 -      $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
 -      $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
 +      $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
 +                         $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
 +      $date{'mday-time'} = sprintf "%d %s %02d:%02d",
 +                           $mday, $months[$mon], $hour ,$min;
  
        $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
        my $local = $epoch + ((int $1 + ($2/60)) * 3600);
@@@ -920,8 -720,7 +920,8 @@@ sub parse_commit 
                @commit_lines = @$commit_text;
        } else {
                $/ = "\0";
 -              open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id or return;
 +              open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id
 +                      or return;
                @commit_lines = split '\n', <$fd>;
                close $fd or return;
                $/ = "\n";
@@@ -1047,34 -846,6 +1047,34 @@@ sub parse_ref 
        return %ref_item;
  }
  
 +# parse line of git-diff-tree "raw" output
 +sub parse_difftree_raw_line {
 +      my $line = shift;
 +      my %res;
 +
 +      # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
 +      # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
 +      if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
 +              $res{'from_mode'} = $1;
 +              $res{'to_mode'} = $2;
 +              $res{'from_id'} = $3;
 +              $res{'to_id'} = $4;
 +              $res{'status'} = $5;
 +              $res{'similarity'} = $6;
 +              if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
 +                      ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
 +              } else {
 +                      $res{'file'} = unquote($7);
 +              }
 +      }
 +      # 'c512b523472485aef4fff9e57b229d9d243c967f'
 +      #elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
 +      #       $res{'commit'} = $1;
 +      #}
 +
 +      return wantarray ? %res : \%res;
 +}
 +
  ## ......................................................................
  ## parse to array of hashes functions
  
@@@ -1215,15 -986,12 +1215,15 @@@ sub git_header_html 
        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
        # we have to do this because MSIE sometimes globs '*/*', pretending to
        # support xhtml+xml but choking when it gets what it asked for.
 -      if (defined $cgi->http('HTTP_ACCEPT') && $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ && $cgi->Accept('application/xhtml+xml') != 0) {
 +      if (defined $cgi->http('HTTP_ACCEPT') &&
 +          $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
 +          $cgi->Accept('application/xhtml+xml') != 0) {
                $content_type = 'application/xhtml+xml';
        } else {
                $content_type = 'text/html';
        }
 -      print $cgi->header(-type=>$content_type, -charset => 'utf-8', -status=> $status, -expires => $expires);
 +      print $cgi->header(-type=>$content_type, -charset => 'utf-8',
 +                         -status=> $status, -expires => $expires);
        print <<EOF;
  <?xml version="1.0" encoding="utf-8"?>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@@ -1290,7 -1058,7 +1290,7 @@@ sub git_footer_html 
                }
                print $cgi->a({-href => href(action=>"rss"), -class => "rss_logo"}, "RSS") . "\n";
        } else {
 -              print $cgi->a({-href => "$my_uri?" . esc_param("a=opml"), -class => "rss_logo"}, "OPML") . "\n";
 +              print $cgi->a({-href => href(action=>"opml"), -class => "rss_logo"}, "OPML") . "\n";
        }
        print "</div>\n" .
              "</body>\n" .
@@@ -1302,13 -1070,11 +1302,13 @@@ sub die_error 
        my $error = shift || "Malformed query, file missing or permission denied";
  
        git_header_html($status);
 -      print "<div class=\"page_body\">\n" .
 -            "<br/><br/>\n" .
 -            "$status - $error\n" .
 -            "<br/>\n" .
 -            "</div>\n";
 +      print <<EOF;
 +<div class="page_body">
 +<br /><br />
 +$status - $error
 +<br />
 +</div>
 +EOF
        git_footer_html();
        exit;
  }
@@@ -1398,91 -1164,22 +1398,91 @@@ sub git_print_header_div 
  sub git_print_page_path {
        my $name = shift;
        my $type = shift;
 +      my $hb = shift;
  
        if (!defined $name) {
 -              print "<div class=\"page_path\"><b>/</b></div>\n";
 +              print "<div class=\"page_path\">/</div>\n";
        } elsif (defined $type && $type eq 'blob') {
 -              print "<div class=\"page_path\"><b>" .
 -                      $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)}, esc_html($name)) . "</b><br/></div>\n";
 +              print "<div class=\"page_path\">";
 +              if (defined $hb) {
 +                      print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
 +                                                   hash_base=>$hb)},
 +                                    esc_html($name));
 +              } else {
 +                      print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)},
 +                                    esc_html($name));
 +              }
 +              print "<br/></div>\n";
        } else {
 -              print "<div class=\"page_path\"><b>" . esc_html($name) . "</b><br/></div>\n";
 +              print "<div class=\"page_path\">" . esc_html($name) . "<br/></div>\n";
 +      }
 +}
 +
 +sub git_print_log {
 +      my $log = shift;
 +
 +      # remove leading empty lines
 +      while (defined $log->[0] && $log->[0] eq "") {
 +              shift @$log;
 +      }
 +
 +      # print log
 +      my $signoff = 0;
 +      my $empty = 0;
 +      foreach my $line (@$log) {
 +              # print only one empty line
 +              # do not print empty line after signoff
 +              if ($line eq "") {
 +                      next if ($empty || $signoff);
 +                      $empty = 1;
 +              } else {
 +                      $empty = 0;
 +              }
 +              if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 +                      $signoff = 1;
 +                      print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
 +              } else {
 +                      $signoff = 0;
 +                      print format_log_line_html($line) . "<br/>\n";
 +              }
        }
  }
  
 +sub git_print_simplified_log {
 +      my $log = shift;
 +      my $remove_title = shift;
 +
 +      shift @$log if $remove_title;
 +      # remove leading empty lines
 +      while (defined $log->[0] && $log->[0] eq "") {
 +              shift @$log;
 +      }
 +
 +      # simplify and print log
 +      my $empty = 0;
 +      foreach my $line (@$log) {
 +              # remove signoff lines
 +              if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 +                      next;
 +              }
 +              # print only one empty line
 +              if ($line eq "") {
 +                      next if $empty;
 +                      $empty = 1;
 +              } else {
 +                      $empty = 0;
 +              }
 +              print format_log_line_html($line) . "<br/>\n";
 +      }
 +      # end with single empty line
 +      print "<br/>\n" unless $empty;
 +}
 +
  ## ......................................................................
  ## functions printing large fragments of HTML
  
  sub git_difftree_body {
 -      my ($difftree, $parent) = @_;
 +      my ($difftree, $hash, $parent) = @_;
  
        print "<div class=\"list_head\">\n";
        if ($#{$difftree} > 10) {
        print "<table class=\"diff_tree\">\n";
        my $alternate = 0;
        foreach my $line (@{$difftree}) {
 -              # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
 -              # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
 -              if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
 -                      next;
 -              }
 -              my $from_mode = $1;
 -              my $to_mode = $2;
 -              my $from_id = $3;
 -              my $to_id = $4;
 -              my $status = $5;
 -              my $similarity = $6; # score
 -              my $file = validate_input(unquote($7));
 +              my %diff = parse_difftree_raw_line($line);
  
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                }
                $alternate ^= 1;
  
 -              if ($status eq "A") { # created
 -                      my $mode_chng = "";
 -                      if (S_ISREG(oct $to_mode)) {
 -                              $mode_chng = sprintf(" with mode: %04o", (oct $to_mode) & 0777);
 +              my ($to_mode_oct, $to_mode_str, $to_file_type);
 +              my ($from_mode_oct, $from_mode_str, $from_file_type);
 +              if ($diff{'to_mode'} ne ('0' x 6)) {
 +                      $to_mode_oct = oct $diff{'to_mode'};
 +                      if (S_ISREG($to_mode_oct)) { # only for regular file
 +                              $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
 +                      }
 +                      $to_file_type = file_type($diff{'to_mode'});
 +              }
 +              if ($diff{'from_mode'} ne ('0' x 6)) {
 +                      $from_mode_oct = oct $diff{'from_mode'};
 +                      if (S_ISREG($to_mode_oct)) { # only for regular file
 +                              $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
                        }
 +                      $from_file_type = file_type($diff{'from_mode'});
 +              }
 +
 +              if ($diff{'status'} eq "A") { # created
 +                      my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
 +                      $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
 +                      $mode_chng   .= "]</span>";
                        print "<td>" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file),
 -                                    -class => "list"}, esc_html($file)) .
 +                            $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 +                                                   hash_base=>$hash, file_name=>$diff{'file'}),
 +                                    -class => "list"}, esc_html($diff{'file'})) .
                              "</td>\n" .
 -                            "<td><span class=\"file_status new\">[new " . file_type($to_mode) . "$mode_chng]</span></td>\n" .
 +                            "<td>$mode_chng</td>\n" .
                              "<td class=\"link\">" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file)}, "blob") .
 +                            $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 +                                                   hash_base=>$hash, file_name=>$diff{'file'})},
 +                                    "blob") .
                              "</td>\n";
  
 -              } elsif ($status eq "D") { # deleted
 +              } elsif ($diff{'status'} eq "D") { # deleted
 +                      my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
                        print "<td>" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$file),
 -                                     -class => "list"}, esc_html($file)) . "</td>\n" .
 -                            "<td><span class=\"file_status deleted\">[deleted " . file_type($from_mode). "]</span></td>\n" .
 +                            $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
 +                                                   hash_base=>$parent, file_name=>$diff{'file'}),
 +                                     -class => "list"}, esc_html($diff{'file'})) .
 +                            "</td>\n" .
 +                            "<td>$mode_chng</td>\n" .
                              "<td class=\"link\">" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$file)}, "blob") . " | " .
 -                            $cgi->a({-href => href(action=>"history", hash_base=>$parent, file_name=>$file)}, "history") .
 -                            "</td>\n"
 +                            $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
 +                                                   hash_base=>$parent, file_name=>$diff{'file'})},
 +                                    "blob") .
 +                            " | " .
 +                            $cgi->a({-href => href(action=>"history", hash_base=>$parent,
 +                                                   file_name=>$diff{'file'})},\
 +                                    "history") .
 +                            "</td>\n";
  
 -              } elsif ($status eq "M" || $status eq "T") { # modified, or type changed
 +              } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
                        my $mode_chnge = "";
 -                      if ($from_mode != $to_mode) {
 -                              $mode_chnge = " <span class=\"file_status mode_chnge\">[changed";
 -                              if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) {
 -                                      $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode);
 +                      if ($diff{'from_mode'} != $diff{'to_mode'}) {
 +                              $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
 +                              if ($from_file_type != $to_file_type) {
 +                                      $mode_chnge .= " from $from_file_type to $to_file_type";
                                }
 -                              if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) {
 -                                      if (S_ISREG($from_mode) && S_ISREG($to_mode)) {
 -                                              $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777);
 -                                      } elsif (S_ISREG($to_mode)) {
 -                                              $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777);
 +                              if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
 +                                      if ($from_mode_str && $to_mode_str) {
 +                                              $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
 +                                      } elsif ($to_mode_str) {
 +                                              $mode_chnge .= " mode: $to_mode_str";
                                        }
                                }
                                $mode_chnge .= "]</span>\n";
                        }
                        print "<td>";
 -                      if ($to_id ne $from_id) { # modified
 -                              print $cgi->a({-href => href(action=>"blobdiff", hash=>$to_id, hash_parent=>$from_id, hash_base=>$hash, file_name=>$file),
 -                                            -class => "list"}, esc_html($file));
 -                      } else { # mode changed
 -                              print $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file),
 -                                            -class => "list"}, esc_html($file));
 +                      if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
 +                              print $cgi->a({-href => href(action=>"blobdiff",
 +                                                           hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                           hash_base=>$hash, hash_parent_base=>$parent,
 +                                                           file_name=>$diff{'file'}),
 +                                            -class => "list"}, esc_html($diff{'file'}));
 +                      } else { # only mode changed
 +                              print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 +                                                           hash_base=>$hash, file_name=>$diff{'file'}),
 +                                            -class => "list"}, esc_html($diff{'file'}));
                        }
                        print "</td>\n" .
                              "<td>$mode_chnge</td>\n" .
                              "<td class=\"link\">" .
 -                              $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$file)}, "blob");
 -                      if ($to_id ne $from_id) { # modified
 -                              print $cgi->a({-href => href(action=>"blobdiff", hash=>$to_id, hash_parent=>$from_id, hash_base=>$hash, file_name=>$file)}, "diff");
 -                      }
 -                      print " | " . $cgi->a({-href => href(action=>"history", hash_base=>$hash, file_name=>$file)}, "history") . "\n";
 -                      print "</td>\n";
 -
 -              } elsif ($status eq "R") { # renamed
 -                      my ($from_file, $to_file) = split "\t", $file;
 -                      my $mode_chng = "";
 -                      if ($from_mode != $to_mode) {
 -                              $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777);
 -                      }
 -                      print "<td>" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file),
 -                                    -class => "list"}, esc_html($to_file)) . "</td>\n" .
 -                            "<td><span class=\"file_status moved\">[moved from " .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$from_file),
 -                                    -class => "list"}, esc_html($from_file)) .
 -                            " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" .
 -                            "<td class=\"link\">" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file)}, "blob");
 -                      if ($to_id ne $from_id) {
 +                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 +                                                     hash_base=>$hash, file_name=>$diff{'file'})},
 +                                      "blob");
 +                      if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
                                print " | " .
 -                                    $cgi->a({-href => "$my_uri?" .
 -                                            esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file;fp=$from_file")}, "diff");
 +                                      $cgi->a({-href => href(action=>"blobdiff",
 +                                                             hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                             hash_base=>$hash, hash_parent_base=>$parent,
 +                                                             file_name=>$diff{'file'})},
 +                                              "diff");
                        }
 +                      print " | " .
 +                              $cgi->a({-href => href(action=>"history",
 +                                                     hash_base=>$hash, file_name=>$diff{'file'})},
 +                                      "history");
                        print "</td>\n";
  
 -              } elsif ($status eq "C") { # copied
 -                      my ($from_file, $to_file) = split "\t", $file;
 +              } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
 +                      my %status_name = ('R' => 'moved', 'C' => 'copied');
 +                      my $nstatus = $status_name{$diff{'status'}};
                        my $mode_chng = "";
 -                      if ($from_mode != $to_mode) {
 -                              $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777);
 +                      if ($diff{'from_mode'} != $diff{'to_mode'}) {
 +                              # mode also for directories, so we cannot use $to_mode_str
 +                              $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
                        }
                        print "<td>" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file),
 -                                    -class => "list"}, esc_html($to_file)) . "</td>\n" .
 -                            "<td><span class=\"file_status copied\">[copied from " .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$from_id, hash_base=>$parent, file_name=>$from_file),
 -                                    -class => "list"}, esc_html($from_file)) .
 -                            " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" .
 +                            $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 +                                                   hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
 +                                    -class => "list"}, esc_html($diff{'to_file'})) . "</td>\n" .
 +                            "<td><span class=\"file_status $nstatus\">[$nstatus from " .
 +                            $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
 +                                                   hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}),
 +                                    -class => "list"}, esc_html($diff{'from_file'})) .
 +                            " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
                              "<td class=\"link\">" .
 -                            $cgi->a({-href => href(action=>"blob", hash=>$to_id, hash_base=>$hash, file_name=>$to_file)}, "blob");
 -                      if ($to_id ne $from_id) {
 +                            $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 +                                                   hash=>$diff{'to_id'}, file_name=>$diff{'to_file'})},
 +                                    "blob");
 +                      if ($diff{'to_id'} ne $diff{'from_id'}) {
                                print " | " .
 -                                    $cgi->a({-href => "$my_uri?" .
 -                                            esc_param("p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file;fp=$from_file")}, "diff");
 +                                      $cgi->a({-href => href(action=>"blobdiff",
 +                                                             hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                             hash_base=>$hash, hash_parent_base=>$parent,
 +                                                             file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
 +                                              "diff");
                        }
                        print "</td>\n";
 +
                } # we should not encounter Unmerged (U) or Unknown (X) status
                print "</tr>\n";
        }
        print "</table>\n";
  }
  
 +sub git_patchset_body {
 +      my ($fd, $difftree, $hash, $hash_parent) = @_;
 +
 +      my $patch_idx = 0;
 +      my $in_header = 0;
 +      my $patch_found = 0;
 +      my $diffinfo;
 +
 +      print "<div class=\"patchset\">\n";
 +
 +      LINE:
 +      while (my $patch_line @$fd>) {
 +              chomp $patch_line;
 +
 +              if ($patch_line =~ m/^diff /) { # "git diff" header
 +                      # beginning of patch (in patchset)
 +                      if ($patch_found) {
 +                              # close previous patch
 +                              print "</div>\n"; # class="patch"
 +                      } else {
 +                              # first patch in patchset
 +                              $patch_found = 1;
 +                      }
 +                      print "<div class=\"patch\">\n";
 +
 +                      if (ref($difftree->[$patch_idx]) eq "HASH") {
 +                              $diffinfo = $difftree->[$patch_idx];
 +                      } else {
 +                              $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
 +                      }
 +                      $patch_idx++;
 +
 +                      # for now, no extended header, hence we skip empty patches
 +                      # companion to  next LINE if $in_header;
 +                      if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change
 +                              $in_header = 1;
 +                              next LINE;
 +                      }
 +
 +                      if ($diffinfo->{'status'} eq "A") { # added
 +                              print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
 +                                    $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 +                                                           hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
 +                                            $diffinfo->{'to_id'}) . "(new)" .
 +                                    "</div>\n"; # class="diff_info"
 +
 +                      } elsif ($diffinfo->{'status'} eq "D") { # deleted
 +                              print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
 +                                    $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 +                                                           hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
 +                                            $diffinfo->{'from_id'}) . "(deleted)" .
 +                                    "</div>\n"; # class="diff_info"
 +
 +                      } elsif ($diffinfo->{'status'} eq "R" || # renamed
 +                               $diffinfo->{'status'} eq "C" || # copied
 +                               $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff)
 +                              print "<div class=\"diff_info\">" .
 +                                    file_type($diffinfo->{'from_mode'}) . ":" .
 +                                    $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 +                                                           hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'from_file'})},
 +                                            $diffinfo->{'from_id'}) .
 +                                    " -> " .
 +                                    file_type($diffinfo->{'to_mode'}) . ":" .
 +                                    $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 +                                                           hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'to_file'})},
 +                                            $diffinfo->{'to_id'});
 +                              print "</div>\n"; # class="diff_info"
 +
 +                      } else { # modified, mode changed, ...
 +                              print "<div class=\"diff_info\">" .
 +                                    file_type($diffinfo->{'from_mode'}) . ":" .
 +                                    $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 +                                                           hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
 +                                            $diffinfo->{'from_id'}) .
 +                                    " -> " .
 +                                    file_type($diffinfo->{'to_mode'}) . ":" .
 +                                    $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 +                                                           hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
 +                                            $diffinfo->{'to_id'});
 +                              print "</div>\n"; # class="diff_info"
 +                      }
 +
 +                      #print "<div class=\"diff extended_header\">\n";
 +                      $in_header = 1;
 +                      next LINE;
 +              } # start of patch in patchset
 +
 +
 +              if ($in_header && $patch_line =~ m/^---/) {
 +                      #print "</div>\n"; # class="diff extended_header"
 +                      $in_header = 0;
 +
 +                      my $file = $diffinfo->{'from_file'};
 +                      $file  ||= $diffinfo->{'file'};
 +                      $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 +                                                     hash=>$diffinfo->{'from_id'}, file_name=>$file),
 +                                      -class => "list"}, esc_html($file));
 +                      $patch_line =~ s|a/.*$|a/$file|g;
 +                      print "<div class=\"diff from_file\">$patch_line</div>\n";
 +
 +                      $patch_line = <$fd>;
 +                      chomp $patch_line;
 +
 +                      #$patch_line =~ m/^+++/;
 +                      $file    = $diffinfo->{'to_file'};
 +                      $file  ||= $diffinfo->{'file'};
 +                      $file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 +                                                     hash=>$diffinfo->{'to_id'}, file_name=>$file),
 +                                      -class => "list"}, esc_html($file));
 +                      $patch_line =~ s|b/.*|b/$file|g;
 +                      print "<div class=\"diff to_file\">$patch_line</div>\n";
 +
 +                      next LINE;
 +              }
 +              next LINE if $in_header;
 +
 +              print format_diff_line($patch_line);
 +      }
 +      print "</div>\n" if $patch_found; # class="patch"
 +
 +      print "</div>\n"; # class="patchset"
 +}
 +
 +# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 +
  sub git_shortlog_body {
        # uses global variable $project
        my ($revlist, $from, $to, $refs, $extra) = @_;
 +
 +      my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
 +      my $have_snapshot = (defined $ctype && defined $suffix);
 +
        $from = 0 unless defined $from;
        $to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
  
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
                      "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
                      "<td>";
 -              print format_subject_html($co{'title'}, $co{'title_short'}, href(action=>"commit", hash=>$commit), $ref);
 +              print format_subject_html($co{'title'}, $co{'title_short'},
 +                                        href(action=>"commit", hash=>$commit), $ref);
                print "</td>\n" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
 -                    $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
 -                    "</td>\n" .
 +                    $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
 +              if ($have_snapshot) {
 +                      print " | " .  $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
 +              }
 +              print "</td>\n" .
                      "</tr>\n";
        }
        if (defined $extra) {
@@@ -1840,8 -1386,7 +1840,8 @@@ sub git_history_body 
                      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
                      "<td>";
                # originally git_history used chop_str($co{'title'}, 50)
 -              print format_subject_html($co{'title'}, $co{'title_short'}, href(action=>"commit", hash=>$commit), $ref);
 +              print format_subject_html($co{'title'}, $co{'title_short'},
 +                                        href(action=>"commit", hash=>$commit), $ref);
                print "</td>\n" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
                        if (defined $blob_current && defined $blob_parent &&
                                        $blob_current ne $blob_parent) {
                                print " | " .
 -                                      $cgi->a({-href => href(action=>"blobdiff", hash=>$blob_current, hash_parent=>$blob_parent, hash_base=>$commit, file_name=>$file_name)},
 +                                      $cgi->a({-href => href(action=>"blobdiff",
 +                                                             hash=>$blob_current, hash_parent=>$blob_parent,
 +                                                             hash_base=>$hash_base, hash_parent_base=>$commit,
 +                                                             file_name=>$file_name)},
                                                "diff to current");
                        }
                }
@@@ -1898,12 -1440,11 +1898,12 @@@ sub git_tags_body 
                print "<td><i>$tag{'age'}</i></td>\n" .
                      "<td>" .
                      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
 -                             -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") .
 +                             -class => "list name"}, esc_html($tag{'name'})) .
                      "</td>\n" .
                      "<td>";
                if (defined $comment) {
 -                      print format_subject_html($comment, $comment_short, href(action=>"tag", hash=>$tag{'id'}));
 +                      print format_subject_html($comment, $comment_short,
 +                                                href(action=>"tag", hash=>$tag{'id'}));
                }
                print "</td>\n" .
                      "<td class=\"selflink\">";
@@@ -1953,7 -1494,7 +1953,7 @@@ sub git_heads_body 
                print "<td><i>$tag{'age'}</i></td>\n" .
                      ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") .
                      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
 -                             -class => "list"}, "<b>" . esc_html($tag{'name'}) . "</b>") .
 +                             -class => "list name"},esc_html($tag{'name'})) .
                      "</td>\n" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
        print "</table>\n";
  }
  
 -## ----------------------------------------------------------------------
 -## functions printing large fragments, format as one of arguments
 -
 -sub git_diff_print {
 -      my $from = shift;
 -      my $from_name = shift;
 -      my $to = shift;
 -      my $to_name = shift;
 -      my $format = shift || "html";
 -
 -      my $from_tmp = "/dev/null";
 -      my $to_tmp = "/dev/null";
 -      my $pid = $$;
 -
 -      # create tmp from-file
 -      if (defined $from) {
 -              $from_tmp = "$git_temp/gitweb_" . $$ . "_from";
 -              open my $fd2, "> $from_tmp";
 -              open my $fd, "-|", $GIT, "cat-file", "blob", $from;
 -              my @file = <$fd>;
 -              print $fd2 @file;
 -              close $fd2;
 -              close $fd;
 -      }
 -
 -      # create tmp to-file
 -      if (defined $to) {
 -              $to_tmp = "$git_temp/gitweb_" . $$ . "_to";
 -              open my $fd2, "> $to_tmp";
 -              open my $fd, "-|", $GIT, "cat-file", "blob", $to;
 -              my @file = <$fd>;
 -              print $fd2 @file;
 -              close $fd2;
 -              close $fd;
 -      }
 -
 -      open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp";
 -      if ($format eq "plain") {
 -              undef $/;
 -              print <$fd>;
 -              $/ = "\n";
 -      } else {
 -              while (my $line = <$fd>) {
 -                      chomp $line;
 -                      my $char = substr($line, 0, 1);
 -                      my $diff_class = "";
 -                      if ($char eq '+') {
 -                              $diff_class = " add";
 -                      } elsif ($char eq "-") {
 -                              $diff_class = " rem";
 -                      } elsif ($char eq "@") {
 -                              $diff_class = " chunk_header";
 -                      } elsif ($char eq "\\") {
 -                              # skip errors
 -                              next;
 -                      }
 -                      $line = untabify($line);
 -                      print "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
 -              }
 -      }
 -      close $fd;
 -
 -      if (defined $from) {
 -              unlink($from_tmp);
 -      }
 -      if (defined $to) {
 -              unlink($to_tmp);
 -      }
 -}
 -
 -
  ## ======================================================================
  ## ======================================================================
  ## actions
@@@ -2062,16 -1674,16 +2062,16 @@@ sub git_project_list 
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
 -              print "<td>" . $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary"),
 +              print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
                                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
                      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
                      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
                print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" .
                      $pr->{'commit'}{'age_string'} . "</td>\n" .
                      "<td class=\"link\">" .
 -                    $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=summary")}, "summary")   . " | " .
 -                    $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=shortlog")}, "shortlog") . " | " .
 -                    $cgi->a({-href => "$my_uri?" . esc_param("p=$pr->{'path'};a=log")}, "log") .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
 +                    $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") .
                      "</td>\n" .
                      "</tr>\n";
        }
@@@ -2143,17 -1755,13 +2143,17 @@@ sub git_tag 
              "<table cellspacing=\"0\">\n" .
              "<tr>\n" .
              "<td>object</td>\n" .
 -            "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, $tag{'object'}) . "</td>\n" .
 -            "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})}, $tag{'type'}) . "</td>\n" .
 +            "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
 +                             $tag{'object'}) . "</td>\n" .
 +            "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
 +                                            $tag{'type'}) . "</td>\n" .
              "</tr>\n";
        if (defined($tag{'author'})) {
                my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
                print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
 -              print "<tr><td></td><td>" . $ad{'rfc2822'} . sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) . "</td></tr>\n";
 +              print "<tr><td></td><td>" . $ad{'rfc2822'} .
 +                      sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
 +                      "</td></tr>\n";
        }
        print "</table>\n\n" .
              "</div>\n";
  sub git_blame2 {
        my $fd;
        my $ftype;
 -      die_error(undef, "Permission denied") if (!git_get_project_config_bool ('blame'));
 +
 +      if (!gitweb_check_feature('blame')) {
 +              die_error('403 Permission denied', "Permission denied");
 +      }
        die_error('404 Not Found', "File name not defined") if (!$file_name);
        $hash_base ||= git_get_head_hash($project);
        die_error(undef, "Couldn't find base commit") unless ($hash_base);
                or die_error(undef, "Open git-blame failed");
        git_header_html();
        my $formats_nav =
 -              $cgi->a({-href => href(action=>"blobl", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blob") .
 -              " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, "head");
 +              $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
 +                      "blob") .
 +              " | " .
 +              $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
 +                      "head");
        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 -      git_print_page_path($file_name, $ftype);
 +      git_print_page_path($file_name, $ftype, $hash_base);
        my @rev_color = (qw(light2 dark2));
        my $num_colors = scalar(@rev_color);
        my $current_color = 0;
        my $last_rev;
 -      print "<div class=\"page_body\">\n";
 -      print "<table class=\"blame\">\n";
 -      print "<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n";
 +      print <<HTML;
 +<div class="page_body">
 +<table class="blame">
 +<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
 +HTML
        while (<$fd>) {
                /^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
                my $full_rev = $1;
                }
                print "<tr class=\"$rev_color[$current_color]\">\n";
                print "<td class=\"sha1\">" .
 -                      $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)}, esc_html($rev)) . "</td>\n";
 -              print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" . esc_html($lineno) . "</a></td>\n";
 +                      $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)},
 +                              esc_html($rev)) . "</td>\n";
 +              print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" .
 +                    esc_html($lineno) . "</a></td>\n";
                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
                print "</tr>\n";
        }
        print "</table>\n";
        print "</div>";
 -      close $fd or print "Reading blob failed\n";
 +      close $fd
 +              or print "Reading blob failed\n";
        git_footer_html();
  }
  
  sub git_blame {
        my $fd;
 -      die_error('403 Permission denied', "Permission denied") if (!git_get_project_config_bool ('blame'));
 +
 +      if (!gitweb_check_feature('blame')) {
 +              die_error('403 Permission denied', "Permission denied");
 +      }
        die_error('404 Not Found', "File name not defined") if (!$file_name);
        $hash_base ||= git_get_head_hash($project);
        die_error(undef, "Couldn't find base commit") unless ($hash_base);
                or die_error(undef, "Open git-annotate failed");
        git_header_html();
        my $formats_nav =
 -              $cgi->a({-href => href(action=>"blobl", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blob") .
 -              " | " . $cgi->a({-href => href(action=>"blame", file_name=>$file_name)}, "head");
 +              $cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
 +                      "blob") .
 +              " | " .
 +              $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
 +                      "head");
        git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
        git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 -      git_print_page_path($file_name, 'blob');
 +      git_print_page_path($file_name, 'blob', $hash_base);
        print "<div class=\"page_body\">\n";
        print <<HTML;
  <table class="blame">
@@@ -2291,7 -1882,7 +2291,7 @@@ HTM
                chomp $line;
                $line_class_num = ($line_class_num + 1) % $line_class_len;
  
-               if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) {
+               if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
                        $long_rev = $1;
                        $author   = $2;
                        $time     = $3;
  HTML
        } # while (my $line = <$fd>)
        print "</table>\n\n";
 -      close $fd or print "Reading blob failed.\n";
 +      close $fd
 +              or print "Reading blob failed.\n";
        print "</div>";
        git_footer_html();
  }
@@@ -2379,8 -1969,7 +2379,8 @@@ sub git_blob_plain 
                $save_as .= '.txt';
        }
  
 -      print $cgi->header(-type => "$type", '-content-disposition' => "inline; filename=\"$save_as\"");
 +      print $cgi->header(-type => "$type",
 +                         -content_disposition => "inline; filename=\"$save_as\"");
        undef $/;
        binmode STDOUT, ':raw';
        print <$fd>;
@@@ -2399,7 -1988,7 +2399,7 @@@ sub git_blob 
                        die_error(undef, "No file name defined");
                }
        }
 -      my $have_blame = git_get_project_config_bool ('blame');
 +      my $have_blame = gitweb_check_feature('blame');
        open my $fd, "-|", $GIT, "cat-file", "blob", $hash
                or die_error(undef, "Couldn't cat $file_name, $hash");
        my $mimetype = blob_mimetype($fd, $file_name);
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
                if (defined $file_name) {
                        if ($have_blame) {
 -                              $formats_nav .= $cgi->a({-href => href(action=>"blame", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)}, "blame") . " | ";
 +                              $formats_nav .=
 +                                      $cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
 +                                                             hash=>$hash, file_name=>$file_name)},
 +                                              "blame") .
 +                                      " | ";
                        }
                        $formats_nav .=
 -                              $cgi->a({-href => href(action=>"blob_plain", hash=>$hash, file_name=>$file_name)}, "plain") .
 -                              " | " . $cgi->a({-href => href(action=>"blob", hash_base=>"HEAD", file_name=>$file_name)}, "head");
 +                              $cgi->a({-href => href(action=>"blob_plain",
 +                                                     hash=>$hash, file_name=>$file_name)},
 +                                      "plain") .
 +                              " | " .
 +                              $cgi->a({-href => href(action=>"blob",
 +                                                     hash_base=>"HEAD", file_name=>$file_name)},
 +                                      "head");
                } else {
 -                      $formats_nav .= $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain");
 +                      $formats_nav .=
 +                              $cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain");
                }
                git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
                git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
                      "<br/><br/></div>\n" .
                      "<div class=\"title\">$hash</div>\n";
        }
 -      git_print_page_path($file_name, "blob");
 +      git_print_page_path($file_name, "blob", $hash_base);
        print "<div class=\"page_body\">\n";
        my $nr;
        while (my $line = <$fd>) {
                chomp $line;
                $nr++;
                $line = untabify($line);
 -              printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n", $nr, $nr, $nr, esc_html($line);
 +              printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
 +                     $nr, $nr, $nr, esc_html($line);
        }
 -      close $fd or print "Reading blob failed.\n";
 +      close $fd
 +              or print "Reading blob failed.\n";
        print "</div>";
        git_footer_html();
  }
@@@ -2474,11 -2051,11 +2474,11 @@@ sub git_tree 
        my $refs = git_get_references();
        my $ref = format_ref_marker($refs, $hash_base);
        git_header_html();
 -      my $base_key = "";
 +      my %base_key = ();
        my $base = "";
 -      my $have_blame = git_get_project_config_bool ('blame');
 +      my $have_blame = gitweb_check_feature('blame');
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 -              $base_key = ";hb=$hash_base";
 +              $base_key{hash_base} = $hash_base;
                git_print_page_nav('tree','', $hash_base);
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
        } else {
        if (defined $file_name) {
                $base = esc_html("$file_name/");
        }
 -      git_print_page_path($file_name, 'tree');
 +      git_print_page_path($file_name, 'tree', $hash_base);
        print "<div class=\"page_body\">\n";
        print "<table cellspacing=\"0\">\n";
        my $alternate = 0;
                print "<td class=\"mode\">" . mode_str($t_mode) . "</td>\n";
                if ($t_type eq "blob") {
                        print "<td class=\"list\">" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name"), -class => "list"}, esc_html($t_name)) .
 +                            $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key),
 +                                    -class => "list"}, esc_html($t_name)) .
                              "</td>\n" .
                              "<td class=\"link\">" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$t_hash$base_key;f=$base$t_name")}, "blob");
 +                            $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
 +                                    "blob");
                        if ($have_blame) {
 -                              print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blame;h=$t_hash$base_key;f=$base$t_name")}, "blame");
 +                              print " | " .
 +                                      $cgi->a({-href => href(action=>"blame", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
 +                                              "blame");
                        }
 -                      print " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;h=$t_hash;hb=$hash_base;f=$base$t_name")}, "history") .
 -                            " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob_plain;h=$t_hash;f=$base$t_name")}, "raw") .
 +                      print " | " .
 +                            $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
 +                                                   hash=>$t_hash, file_name=>"$base$t_name")},
 +                                    "history") .
 +                            " | " .
 +                            $cgi->a({-href => href(action=>"blob_plain",
 +                                                   hash=>$t_hash, file_name=>"$base$t_name")},
 +                                    "raw") .
                              "</td>\n";
                } elsif ($t_type eq "tree") {
                        print "<td class=\"list\">" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, esc_html($t_name)) .
 +                            $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
 +                                    esc_html($t_name)) .
                              "</td>\n" .
                              "<td class=\"link\">" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$t_hash$base_key;f=$base$t_name")}, "tree") .
 -                            " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=history;hb=$hash_base;f=$base$t_name")}, "history") .
 +                            $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
 +                                    "tree") .
 +                            " | " .
 +                            $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, file_name=>"$base$t_name")},
 +                                    "history") .
                              "</td>\n";
                }
                print "</tr>\n";
        git_footer_html();
  }
  
 +sub git_snapshot {
 +
 +      my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
 +      my $have_snapshot = (defined $ctype && defined $suffix);
 +      if (!$have_snapshot) {
 +              die_error('403 Permission denied', "Permission denied");
 +      }
 +
 +      if (!defined $hash) {
 +              $hash = git_get_head_hash($project);
 +      }
 +
 +      my $filename = basename($project) . "-$hash.tar.$suffix";
 +
 +      print $cgi->header(-type => 'application/x-tar',
 +                         -content_encoding => $ctype,
 +                         -content_disposition => "inline; filename=\"$filename\"",
 +                         -status => '200 OK');
 +
 +      open my $fd, "-|", "$GIT tar-tree $hash \'$project\' | $command" or
 +              die_error(undef, "Execute git-tar-tree failed.");
 +      binmode STDOUT, ':raw';
 +      print <$fd>;
 +      binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
 +      close $fd;
 +
 +}
 +
  sub git_log {
        my $head = git_get_head_hash($project);
        if (!defined $hash) {
                print "<div class=\"title_text\">\n" .
                      "<div class=\"log_link\">\n" .
                      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
 -                    " | " . $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
 +                    " | " .
 +                    $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
                      "<br/>\n" .
                      "</div>\n" .
                      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
 -                    "</div>\n" .
 -                    "<div class=\"log_body\">\n";
 -              my $comment = $co{'comment'};
 -              my $empty = 0;
 -              foreach my $line (@$comment) {
 -                      if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 -                              next;
 -                      }
 -                      if ($line eq "") {
 -                              if ($empty) {
 -                                      next;
 -                              }
 -                              $empty = 1;
 -                      } else {
 -                              $empty = 0;
 -                      }
 -                      print format_log_line_html($line) . "<br/>\n";
 -              }
 -              if (!$empty) {
 -                      print "<br/>\n";
 -              }
 +                    "</div>\n";
 +
 +              print "<div class=\"log_body\">\n";
 +              git_print_simplified_log($co{'comment'});
                print "</div>\n";
        }
        git_footer_html();
@@@ -2655,21 -2206,15 +2655,21 @@@ sub git_commit 
        }
        my $refs = git_get_references();
        my $ref = format_ref_marker($refs, $co{'id'});
 +
 +      my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
 +      my $have_snapshot = (defined $ctype && defined $suffix);
 +
        my $formats_nav = '';
        if (defined $file_name && defined $co{'parent'}) {
                my $parent = $co{'parent'};
 -              $formats_nav .= $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)}, "blame");
 +              $formats_nav .=
 +                      $cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
 +                              "blame");
        }
        git_header_html(undef, $expires);
        git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
 -                   $hash, $co{'tree'}, $hash,
 -                   $formats_nav);
 +                         $hash, $co{'tree'}, $hash,
 +                         $formats_nav);
  
        if (defined $co{'parent'}) {
                git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
              "<tr>" .
              "<td></td><td> $ad{'rfc2822'}";
        if ($ad{'hour_local'} < 6) {
 -              printf(" (<span class=\"atnight\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 +              printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
 +                     $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
        } else {
 -              printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 +              printf(" (%02d:%02d %s)",
 +                     $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
        }
        print "</td>" .
              "</tr>\n";
        print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
 -      print "<tr><td></td><td> $cd{'rfc2822'}" . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
 +      print "<tr><td></td><td> $cd{'rfc2822'}" .
 +            sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
 +            "</td></tr>\n";
        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
        print "<tr>" .
              "<td>tree</td>" .
              "<td class=\"sha1\">" .
 -            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash), class => "list"}, $co{'tree'}) .
 -            "</td>" .
 -            "<td class=\"link\">" . $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)}, "tree") .
 +            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
 +                     class => "list"}, $co{'tree'}) .
              "</td>" .
 +            "<td class=\"link\">" .
 +            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
 +                    "tree");
 +      if ($have_snapshot) {
 +              print " | " .
 +                    $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
 +      }
 +      print "</td>" .
              "</tr>\n";
        my $parents = $co{'parents'};
        foreach my $par (@$parents) {
                print "<tr>" .
                      "<td>parent</td>" .
 -                    "<td class=\"sha1\">" . $cgi->a({-href => href(action=>"commit", hash=>$par), class => "list"}, $par) . "</td>" .
 +                    "<td class=\"sha1\">" .
 +                    $cgi->a({-href => href(action=>"commit", hash=>$par),
 +                             class => "list"}, $par) .
 +                    "</td>" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
 -                    " | " . $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") .
 +                    " | " .
 +                    $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") .
                      "</td>" .
                      "</tr>\n";
        }
        print "</table>".
              "</div>\n";
 +
        print "<div class=\"page_body\">\n";
 -      my $comment = $co{'comment'};
 -      my $empty = 0;
 -      my $signed = 0;
 -      foreach my $line (@$comment) {
 -              # print only one empty line
 -              if ($line eq "") {
 -                      if ($empty || $signed) {
 -                              next;
 -                      }
 -                      $empty = 1;
 -              } else {
 -                      $empty = 0;
 -              }
 -              if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 -                      $signed = 1;
 -                      print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
 -              } else {
 -                      $signed = 0;
 -                      print format_log_line_html($line) . "<br/>\n";
 -              }
 -      }
 +      git_print_log($co{'comment'});
        print "</div>\n";
  
 -      git_difftree_body(\@difftree, $parent);
 +      git_difftree_body(\@difftree, $hash, $parent);
  
        git_footer_html();
  }
  
  sub git_blobdiff {
 -      mkdir($git_temp, 0700);
 -      git_header_html();
 -      if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 -              my $formats_nav =
 -                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blobdiff_plain;h=$hash;hp=$hash_parent")}, "plain");
 -              git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 -              git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 -      } else {
 -              print "<div class=\"page_nav\">\n" .
 -                    "<br/><br/></div>\n" .
 -                    "<div class=\"title\">$hash vs $hash_parent</div>\n";
 -      }
 -      git_print_page_path($file_name, "blob");
 -      print "<div class=\"page_body\">\n" .
 -            "<div class=\"diff_info\">blob:" .
 -            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash_parent;hb=$hash_base;f=$file_name")}, $hash_parent) .
 -            " -> blob:" .
 -            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, $hash) .
 -            "</div>\n";
 -      git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
 -      print "</div>";
 -      git_footer_html();
 -}
 +      my $format = shift || 'html';
  
 -sub git_blobdiff_plain {
 -      mkdir($git_temp, 0700);
 -      print $cgi->header(-type => "text/plain", -charset => 'utf-8');
 -      git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain");
 -}
 +      my $fd;
 +      my @difftree;
 +      my %diffinfo;
 +      my $expires;
  
 -sub git_commitdiff {
 -      mkdir($git_temp, 0700);
 -      my %co = parse_commit($hash);
 -      if (!%co) {
 -              die_error(undef, "Unknown commit object");
 -      }
 -      if (!defined $hash_parent) {
 -              $hash_parent = $co{'parent'} || '--root';
 +      # preparing $fd and %diffinfo for git_patchset_body
 +      # new style URI
 +      if (defined $hash_base && defined $hash_parent_base) {
 +              if (defined $file_name) {
 +                      # read raw output
 +                      open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C', $hash_parent_base, $hash_base,
 +                              "--", $file_name
 +                              or die_error(undef, "Open git-diff-tree failed");
 +                      @difftree = map { chomp; $_ } <$fd>;
 +                      close $fd
 +                              or die_error(undef, "Reading git-diff-tree failed");
 +                      @difftree
 +                              or die_error('404 Not Found', "Blob diff not found");
 +
 +              } elsif (defined $hash) { # try to find filename from $hash
 +                      if ($hash !~ /[0-9a-fA-F]{40}/) {
 +                              $hash = git_to_hash($hash);
 +                      }
 +
 +                      # read filtered raw output
 +                      open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C', $hash_parent_base, $hash_base
 +                              or die_error(undef, "Open git-diff-tree failed");
 +                      @difftree =
 +                              # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
 +                              # $hash == to_id
 +                              grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
 +                              map { chomp; $_ } <$fd>;
 +                      close $fd
 +                              or die_error(undef, "Reading git-diff-tree failed");
 +                      @difftree
 +                              or die_error('404 Not Found', "Blob diff not found");
 +
 +              } else {
 +                      die_error('404 Not Found', "Missing one of the blob diff parameters");
 +              }
 +
 +              if (@difftree > 1) {
 +                      die_error('404 Not Found', "Ambiguous blob diff specification");
 +              }
 +
 +              %diffinfo = parse_difftree_raw_line($difftree[0]);
 +              $file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'};
 +              $file_name   ||= $diffinfo{'to_file'}   || $diffinfo{'file'};
 +
 +              $hash_parent ||= $diffinfo{'from_id'};
 +              $hash        ||= $diffinfo{'to_id'};
 +
 +              # non-textual hash id's can be cached
 +              if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
 +                  $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
 +                      $expires = '+1d';
 +              }
 +
 +              # open patch output
 +              open $fd, "-|", $GIT, "diff-tree", '-r', '-p', '-M', '-C', $hash_parent_base, $hash_base,
 +                      "--", $file_name
 +                      or die_error(undef, "Open git-diff-tree failed");
        }
 -      open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash
 -              or die_error(undef, "Open git-diff-tree failed");
 -      my @difftree = map { chomp; $_ } <$fd>;
 -      close $fd or die_error(undef, "Reading git-diff-tree failed");
  
 -      # non-textual hash id's can be cached
 -      my $expires;
 -      if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
 -              $expires = "+1d";
 +      # old/legacy style URI
 +      if (!%diffinfo && # if new style URI failed
 +          defined $hash && defined $hash_parent) {
 +              # fake git-diff-tree raw output
 +              $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
 +              $diffinfo{'from_id'} = $hash_parent;
 +              $diffinfo{'to_id'}   = $hash;
 +              if (defined $file_name) {
 +                      if (defined $file_parent) {
 +                              $diffinfo{'status'} = '2';
 +                              $diffinfo{'from_file'} = $file_parent;
 +                              $diffinfo{'to_file'}   = $file_name;
 +                      } else { # assume not renamed
 +                              $diffinfo{'status'} = '1';
 +                              $diffinfo{'from_file'} = $file_name;
 +                              $diffinfo{'to_file'}   = $file_name;
 +                      }
 +              } else { # no filename given
 +                      $diffinfo{'status'} = '2';
 +                      $diffinfo{'from_file'} = $hash_parent;
 +                      $diffinfo{'to_file'}   = $hash;
 +              }
 +
 +              # non-textual hash id's can be cached
 +              if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
 +                  $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
 +                      $expires = '+1d';
 +              }
 +
 +              # open patch output
 +              #open $fd, "-|", $GIT, "diff", '-p', $hash_parent, $hash
 +              open $fd, "-|", $GIT, "diff", '-p', $hash, $hash_parent
 +                      or die_error(undef, "Open git-diff failed");
 +      } else  {
 +              die_error('404 Not Found', "Missing one of the blob diff parameters")
 +                      unless %diffinfo;
        }
 -      my $refs = git_get_references();
 -      my $ref = format_ref_marker($refs, $co{'id'});
 -      my $formats_nav =
 -              $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commitdiff_plain;h=$hash;hp=$hash_parent")}, "plain");
 -      git_header_html(undef, $expires);
 -      git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
 -      git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
 -      print "<div class=\"page_body\">\n";
 -      my $comment = $co{'comment'};
 -      my $empty = 0;
 -      my $signed = 0;
 -      my @log = @$comment;
 -      # remove first and empty lines after that
 -      shift @log;
 -      while (defined $log[0] && $log[0] eq "") {
 -              shift @log;
 -      }
 -      foreach my $line (@log) {
 -              if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 -                      next;
 +
 +      # header
 +      if ($format eq 'html') {
 +              my $formats_nav =
 +                      $cgi->a({-href => href(action=>"blobdiff_plain",
 +                                             hash=>$hash, hash_parent=>$hash_parent,
 +                                             hash_base=>$hash_base, hash_parent_base=>$hash_parent_base,
 +                                             file_name=>$file_name, file_parent=>$file_parent)},
 +                              "plain");
 +              git_header_html(undef, $expires);
 +              if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 +                      git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 +                      git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 +              } else {
 +                      print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
 +                      print "<div class=\"title\">$hash vs $hash_parent</div>\n";
                }
 -              if ($line eq "") {
 -                      if ($empty) {
 -                              next;
 -                      }
 -                      $empty = 1;
 +              if (defined $file_name) {
 +                      git_print_page_path($file_name, "blob", $hash_base);
                } else {
 -                      $empty = 0;
 +                      print "<div class=\"page_path\"></div>\n";
                }
 -              print format_log_line_html($line) . "<br/>\n";
 +
 +      } elsif ($format eq 'plain') {
 +              print $cgi->header(
 +                      -type => 'text/plain',
 +                      -charset => 'utf-8',
 +                      -expires => $expires,
 +                      -content_disposition => qq(inline; filename="${file_name}.patch"));
 +
 +              print "X-Git-Url: " . $cgi->self_url() . "\n\n";
 +
 +      } else {
 +              die_error(undef, "Unknown blobdiff format");
        }
 -      print "<br/>\n";
 -      foreach my $line (@difftree) {
 -              # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M      ls-files.c'
 -              # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M      rev-tree.c'
 -              if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
 -                      next;
 -              }
 -              my $from_mode = $1;
 -              my $to_mode = $2;
 -              my $from_id = $3;
 -              my $to_id = $4;
 -              my $status = $5;
 -              my $file = validate_input(unquote($6));
 -              if ($status eq "A") {
 -                      print "<div class=\"diff_info\">" . file_type($to_mode) . ":" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id) . "(new)" .
 -                            "</div>\n";
 -                      git_diff_print(undef, "/dev/null", $to_id, "b/$file");
 -              } elsif ($status eq "D") {
 -                      print "<div class=\"diff_info\">" . file_type($from_mode) . ":" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash_parent;f=$file")}, $from_id) . "(deleted)" .
 -                            "</div>\n";
 -                      git_diff_print($from_id, "a/$file", undef, "/dev/null");
 -              } elsif ($status eq "M") {
 -                      if ($from_id ne $to_id) {
 -                              print "<div class=\"diff_info\">" .
 -                                    file_type($from_mode) . ":" .
 -                                    $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$from_id;hb=$hash_parent;f=$file")}, $from_id) .
 -                                    " -> " .
 -                                    file_type($to_mode) . ":" .
 -                                    $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id);
 -                              print "</div>\n";
 -                              git_diff_print($from_id, "a/$file",  $to_id, "b/$file");
 -                      }
 +
 +      # patch
 +      if ($format eq 'html') {
 +              print "<div class=\"page_body\">\n";
 +
 +              git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
 +              close $fd;
 +
 +              print "</div>\n"; # class="page_body"
 +              git_footer_html();
 +
 +      } else {
 +              while (my $line = <$fd>) {
 +                      $line =~ s!a/($hash|$hash_parent)!a/$diffinfo{'from_file'}!g;
 +                      $line =~ s!b/($hash|$hash_parent)!b/$diffinfo{'to_file'}!g;
 +
 +                      print $line;
 +
 +                      last if $line =~ m!^\+\+\+!;
                }
 +              local $/ = undef;
 +              print <$fd>;
 +              close $fd;
        }
 -      print "<br/>\n" .
 -            "</div>";
 -      git_footer_html();
  }
  
 -sub git_commitdiff_plain {
 -      mkdir($git_temp, 0700);
 +sub git_blobdiff_plain {
 +      git_blobdiff('plain');
 +}
 +
 +sub git_commitdiff {
 +      my $format = shift || 'html';
        my %co = parse_commit($hash);
        if (!%co) {
                die_error(undef, "Unknown commit object");
        if (!defined $hash_parent) {
                $hash_parent = $co{'parent'} || '--root';
        }
 -      open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash
 -              or die_error(undef, "Open git-diff-tree failed");
 -      my @difftree = map { chomp; $_ } <$fd>;
 -      close $fd or die_error(undef, "Reading diff-tree failed");
  
 -      # try to figure out the next tag after this commit
 -      my $tagname;
 -      my $refs = git_get_references("tags");
 -      open $fd, "-|", $GIT, "rev-list", "HEAD";
 -      my @commits = map { chomp; $_ } <$fd>;
 -      close $fd;
 -      foreach my $commit (@commits) {
 -              if (defined $refs->{$commit}) {
 -                      $tagname = $refs->{$commit}
 -              }
 -              if ($commit eq $hash) {
 -                      last;
 +      # read commitdiff
 +      my $fd;
 +      my @difftree;
 +      if ($format eq 'html') {
 +              open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C',
 +                      "--patch-with-raw", "--full-index", $hash_parent, $hash
 +                      or die_error(undef, "Open git-diff-tree failed");
 +
 +              while (chomp(my $line = <$fd>)) {
 +                      # empty line ends raw part of diff-tree output
 +                      last unless $line;
 +                      push @difftree, $line;
                }
 -      }
  
 -      print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"git-$hash.patch\"");
 -      my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
 -      my $comment = $co{'comment'};
 -      print "From: $co{'author'}\n" .
 -            "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n".
 -            "Subject: $co{'title'}\n";
 -      if (defined $tagname) {
 -              print "X-Git-Tag: $tagname\n";
 +      } elsif ($format eq 'plain') {
 +              open $fd, "-|", $GIT, "diff-tree", '-r', '-p', '-B', $hash_parent, $hash
 +                      or die_error(undef, "Open git-diff-tree failed");
 +
 +      } else {
 +              die_error(undef, "Unknown commitdiff format");
        }
 -      print "X-Git-Url: $my_url?p=$project;a=commitdiff;h=$hash\n" .
 -            "\n";
  
 -      foreach my $line (@$comment) {;
 -              print "$line\n";
 +      # non-textual hash id's can be cached
 +      my $expires;
 +      if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
 +              $expires = "+1d";
        }
 -      print "---\n\n";
  
 -      foreach my $line (@difftree) {
 -              if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
 -                      next;
 -              }
 -              my $from_id = $3;
 -              my $to_id = $4;
 -              my $status = $5;
 -              my $file = $6;
 -              if ($status eq "A") {
 -                      git_diff_print(undef, "/dev/null", $to_id, "b/$file", "plain");
 -              } elsif ($status eq "D") {
 -                      git_diff_print($from_id, "a/$file", undef, "/dev/null", "plain");
 -              } elsif ($status eq "M") {
 -                      git_diff_print($from_id, "a/$file",  $to_id, "b/$file", "plain");
 -              }
 +      # write commit message
 +      if ($format eq 'html') {
 +              my $refs = git_get_references();
 +              my $ref = format_ref_marker($refs, $co{'id'});
 +              my $formats_nav =
 +                      $cgi->a({-href => href(action=>"commitdiff_plain",
 +                                             hash=>$hash, hash_parent=>$hash_parent)},
 +                              "plain");
 +
 +              git_header_html(undef, $expires);
 +              git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
 +              git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
 +              print "<div class=\"page_body\">\n";
 +              print "<div class=\"log\">\n";
 +              git_print_simplified_log($co{'comment'}, 1); # skip title
 +              print "</div>\n"; # class="log"
 +
 +      } elsif ($format eq 'plain') {
 +              my $refs = git_get_references("tags");
 +              my $tagname = git_get_rev_name_tags($hash);
 +              my $filename = basename($project) . "-$hash.patch";
 +
 +              print $cgi->header(
 +                      -type => 'text/plain',
 +                      -charset => 'utf-8',
 +                      -expires => $expires,
 +                      -content_disposition => qq(inline; filename="$filename"));
 +              my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
 +              print <<TEXT;
 +From: $co{'author'}
 +Date: $ad{'rfc2822'} ($ad{'tz_local'})
 +Subject: $co{'title'}
 +TEXT
 +              print "X-Git-Tag: $tagname\n" if $tagname;
 +              print "X-Git-Url: " . $cgi->self_url() . "\n\n";
 +
 +              foreach my $line (@{$co{'comment'}}) {
 +                      print "$line\n";
 +              }
 +              print "---\n\n";
 +      }
 +
 +      # write patch
 +      if ($format eq 'html') {
 +              #git_difftree_body(\@difftree, $hash, $hash_parent);
 +              #print "<br/>\n";
 +
 +              git_patchset_body($fd, \@difftree, $hash, $hash_parent);
 +              close $fd;
 +              print "</div>\n"; # class="page_body"
 +              git_footer_html();
 +
 +      } elsif ($format eq 'plain') {
 +              local $/ = undef;
 +              print <$fd>;
 +              close $fd
 +                      or print "Reading git-diff-tree failed\n";
        }
  }
  
 +sub git_commitdiff_plain {
 +      git_commitdiff('plain');
 +}
 +
  sub git_history {
        if (!defined $hash_base) {
                $hash_base = git_get_head_hash($project);
        if (defined $hash) {
                $ftype = git_get_type($hash);
        }
 -      git_print_page_path($file_name, $ftype);
 +      git_print_page_path($file_name, $ftype, $hash_base);
  
        open my $fd, "-|",
                $GIT, "rev-list", "--full-history", $hash_base, "--", $file_name;
@@@ -3095,8 -2559,7 +3095,8 @@@ sub git_search 
                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
                              "<td>" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" . esc_html(chop_str($co{'title'}, 50)) . "</b><br/>");
 +                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
 +                                     esc_html(chop_str($co{'title'}, 50)) . "<br/>");
                        my $comment = $co{'comment'};
                        foreach my $line (@$comment) {
                                if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
                        }
                        print "</td>\n" .
                              "<td class=\"link\">" .
 -                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") .
 -                            " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
 +                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
 +                            " | " .
 +                            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
                        print "</td>\n" .
                              "</tr>\n";
                }
                                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
                                              "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
                                              "<td>" .
 -                                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" .
 -                                            esc_html(chop_str($co{'title'}, 50)) . "</b><br/>");
 +                                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
 +                                                    -class => "list subject"},
 +                                                    esc_html(chop_str($co{'title'}, 50)) . "<br/>");
                                        while (my $setref = shift @files) {
                                                my %set = %$setref;
 -                                              print $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=blob;h=$set{'id'};hb=$co{'id'};f=$set{'file'}"), class => "list"},
 -                                                    "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
 +                                              print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
 +                                                                           hash=>$set{'id'}, file_name=>$set{'file'}),
 +                                                            -class => "list"},
 +                                                            "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
                                                      "<br/>\n";
                                        }
                                        print "</td>\n" .
                                              "<td class=\"link\">" .
 -                                            $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=commit;h=$co{'id'}")}, "commit") .
 -                                            " | " . $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
 +                                            $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
 +                                            " | " .
 +                                            $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
                                        print "</td>\n" .
                                              "</tr>\n";
                                }
@@@ -3198,7 -2656,7 +3198,7 @@@ sub git_shortlog 
        my $next_link = '';
        if ($#revlist >= (100 * ($page+1)-1)) {
                $next_link =
 -                      $cgi->a({-href => "$my_uri?" . esc_param("p=$project;a=shortlog;h=$hash;pg=" . ($page+1)),
 +                      $cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
                                 -title => "Alt-n"}, "next");
        }
  
@@@ -3222,15 -2680,13 +3222,15 @@@ sub git_rss 
        my @revlist = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading git-rev-list failed");
        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
 -      print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
 -            "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n";
 -      print "<channel>\n";
 -      print "<title>$project</title>\n".
 -            "<link>" . esc_html("$my_url?p=$project;a=summary") . "</link>\n".
 -            "<description>$project log</description>\n".
 -            "<language>en</language>\n";
 +      print <<XML;
 +<?xml version="1.0" encoding="utf-8"?>
 +<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
 +<channel>
 +<title>$project $my_uri $my_url</title>
 +<link>${\esc_html("$my_url?p=$project;a=summary")}</link>
 +<description>$project log</description>
 +<language>en</language>
 +XML
  
        for (my $i = 0; $i <= $#revlist; $i++) {
                my $commit = $revlist[$i];
@@@ -3279,15 -2735,13 +3279,15 @@@ sub git_opml 
        my @list = git_get_projects_list();
  
        print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
 -      print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
 -            "<opml version=\"1.0\">\n".
 -            "<head>".
 -            "  <title>$site_name Git OPML Export</title>\n".
 -            "</head>\n".
 -            "<body>\n".
 -            "<outline text=\"git RSS feeds\">\n";
 +      print <<XML;
 +<?xml version="1.0" encoding="utf-8"?>
 +<opml version="1.0">
 +<head>
 +  <title>$site_name Git OPML Export</title>
 +</head>
 +<body>
 +<outline text="git RSS feeds">
 +XML
  
        foreach my $pr (@list) {
                my %proj = %$pr;
                my $html = "$my_url?p=$proj{'path'};a=summary";
                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
        }
 -      print "</outline>\n".
 -            "</body>\n".
 -            "</opml>\n";
 +      print <<XML;
 +</outline>
 +</body>
 +</opml>
 +XML
  }