]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'jn/mime-type-with-params'
authorJunio C Hamano <gitster@pobox.com>
Tue, 19 Jul 2011 16:45:41 +0000 (09:45 -0700)
committerJunio C Hamano <gitster@pobox.com>
Tue, 19 Jul 2011 16:45:41 +0000 (09:45 -0700)
* jn/mime-type-with-params:
  gitweb: Serve */*+xml 'blob_plain' as text/plain with $prevent_xss
  gitweb: Serve text/* 'blob_plain' as text/plain with $prevent_xss

1  2 
gitweb/gitweb.perl

diff --combined gitweb/gitweb.perl
index a62b510ba74bc4734c76fc823a73cc6003637908,2aec91307f0c0e9b30d27f7a3c57488b0ae38177..f3e567c8d7bc0bc556583326224e92e7c813f534
@@@ -7,62 -7,33 +7,62 @@@
  #
  # This program is licensed under the GPLv2
  
 +use 5.008;
  use strict;
  use warnings;
  use CGI qw(:standard :escapeHTML -nosticky);
  use CGI::Util qw(unescape);
 -use CGI::Carp qw(fatalsToBrowser);
 +use CGI::Carp qw(fatalsToBrowser set_message);
  use Encode;
  use Fcntl ':mode';
  use File::Find qw();
  use File::Basename qw(basename);
 +use Time::HiRes qw(gettimeofday tv_interval);
  binmode STDOUT, ':utf8';
  
 +our $t0 = [ gettimeofday() ];
 +our $number_of_git_cmds = 0;
 +
  BEGIN {
        CGI->compile() if $ENV{'MOD_PERL'};
  }
  
 -our $cgi = new CGI;
  our $version = "++GIT_VERSION++";
 -our $my_url = $cgi->url();
 -our $my_uri = $cgi->url(-absolute => 1);
  
 -# if we're called with PATH_INFO, we have to strip that
 -# from the URL to find our real URL
 -# we make $path_info global because it's also used later on
 -our $path_info = $ENV{"PATH_INFO"};
 -if ($path_info) {
 -      $my_url =~ s,\Q$path_info\E$,,;
 -      $my_uri =~ s,\Q$path_info\E$,,;
 +our ($my_url, $my_uri, $base_url, $path_info, $home_link);
 +sub evaluate_uri {
 +      our $cgi;
 +
 +      our $my_url = $cgi->url();
 +      our $my_uri = $cgi->url(-absolute => 1);
 +
 +      # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
 +      # needed and used only for URLs with nonempty PATH_INFO
 +      our $base_url = $my_url;
 +
 +      # When the script is used as DirectoryIndex, the URL does not contain the name
 +      # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
 +      # have to do it ourselves. We make $path_info global because it's also used
 +      # later on.
 +      #
 +      # Another issue with the script being the DirectoryIndex is that the resulting
 +      # $my_url data is not the full script URL: this is good, because we want
 +      # generated links to keep implying the script name if it wasn't explicitly
 +      # indicated in the URL we're handling, but it means that $my_url cannot be used
 +      # as base URL.
 +      # Therefore, if we needed to strip PATH_INFO, then we know that we have
 +      # to build the base URL ourselves:
 +      our $path_info = $ENV{"PATH_INFO"};
 +      if ($path_info) {
 +              if ($my_url =~ s,\Q$path_info\E$,, &&
 +                  $my_uri =~ s,\Q$path_info\E$,, &&
 +                  defined $ENV{'SCRIPT_NAME'}) {
 +                      $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
 +              }
 +      }
 +
 +      # target of the home link on top of all pages
 +      our $home_link = $my_uri || "/";
  }
  
  # core git executable to use
@@@ -77,6 -48,9 +77,6 @@@ our $projectroot = "++GITWEB_PROJECTROO
  # the number is relative to the projectroot
  our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
  
 -# target of the home link on top of all pages
 -our $home_link = $my_uri || "/";
 -
  # string of the home link on top of all pages
  our $home_link_str = "++GITWEB_HOME_LINK_STR++";
  
@@@ -100,13 -74,11 +100,13 @@@ our $stylesheet = undef
  our $logo = "++GITWEB_LOGO++";
  # URI of GIT favicon, assumed to be image/png type
  our $favicon = "++GITWEB_FAVICON++";
 +# URI of gitweb.js (JavaScript code for gitweb)
 +our $javascript = "++GITWEB_JS++";
  
  # URI and label (title) of GIT logo link
  #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
  #our $logo_label = "git documentation";
 -our $logo_url = "http://git.or.cz/";
 +our $logo_url = "http://git-scm.com/";
  our $logo_label = "git homepage";
  
  # source of projects list
@@@ -115,14 -87,6 +115,14 @@@ our $projects_list = "++GITWEB_LIST++"
  # the width (in characters) of the projects list "Description" column
  our $projects_list_description_width = 25;
  
 +# group projects by category on the projects list
 +# (enabled if this variable evaluates to true)
 +our $projects_list_group_categories = 0;
 +
 +# default category if none specified
 +# (leave the empty string for no category)
 +our $project_list_default_category = "";
 +
  # default order of projects list
  # valid values are none, project, descr, owner, and age
  our $default_projects_order = "project";
@@@ -172,12 -136,6 +172,12 @@@ our @diff_opts = ('-M'); # taken from g
  # the gitweb domain.
  our $prevent_xss = 0;
  
 +# Path to the highlight executable to use (must be the one from
 +# http://www.andre-simon.de due to assumptions about parameters and output).
 +# Useful if highlight is not installed on your webserver's PATH.
 +# [Default: highlight]
 +our $highlight_bin = "++HIGHLIGHT_BIN++";
 +
  # information about snapshot formats that gitweb is capable of serving
  our %known_snapshot_formats = (
        # name => {
        #       'suffix' => filename suffix,
        #       'format' => --format for git-archive,
        #       'compressor' => [compressor command and arguments]
 -      #                       (array reference, optional)}
 +      #                       (array reference, optional)
 +      #       'disabled' => boolean (optional)}
        #
        'tgz' => {
                'display' => 'tar.gz',
                'type' => 'application/x-gzip',
                'suffix' => '.tar.gz',
                'format' => 'tar',
 -              'compressor' => ['gzip']},
 +              'compressor' => ['gzip', '-n']},
  
        'tbz2' => {
                'display' => 'tar.bz2',
                'format' => 'tar',
                'compressor' => ['bzip2']},
  
 +      'txz' => {
 +              'display' => 'tar.xz',
 +              'type' => 'application/x-xz',
 +              'suffix' => '.tar.xz',
 +              'format' => 'tar',
 +              'compressor' => ['xz'],
 +              'disabled' => 1},
 +
        'zip' => {
                'display' => 'zip',
                'type' => 'application/x-zip',
  our %known_snapshot_format_aliases = (
        'gzip'  => 'tgz',
        'bzip2' => 'tbz2',
 +      'xz'    => 'txz',
  
        # backward compatibility: legacy gitweb config support
        'x-gzip' => undef, 'gz' => undef,
        'x-zip' => undef, '' => undef,
  );
  
 +# Pixel sizes for icons and avatars. If the default font sizes or lineheights
 +# are changed, it may be appropriate to change these values too via
 +# $GITWEB_CONFIG.
 +our %avatar_size = (
 +      'default' => 16,
 +      'double'  => 32
 +);
 +
 +# Used to set the maximum load that we will still respond to gitweb queries.
 +# If server load exceed this value then return "503 server busy" error.
 +# If gitweb cannot determined server load, it is taken to be 0.
 +# Leave it undefined (or set to 'undef') to turn off load checking.
 +our $maxload = 300;
 +
 +# configuration for 'highlight' (http://www.andre-simon.de/)
 +# match by basename
 +our %highlight_basename = (
 +      #'Program' => 'py',
 +      #'Library' => 'py',
 +      'SConstruct' => 'py', # SCons equivalent of Makefile
 +      'Makefile' => 'make',
 +);
 +# match by extension
 +our %highlight_ext = (
 +      # main extensions, defining name of syntax;
 +      # see files in /usr/share/highlight/langDefs/ directory
 +      map { $_ => $_ }
 +              qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
 +      # alternate extensions, see /etc/highlight/filetypes.conf
 +      'h' => 'c',
 +      map { $_ => 'sh'  } qw(bash zsh ksh),
 +      map { $_ => 'cpp' } qw(cxx c++ cc),
 +      map { $_ => 'php' } qw(php3 php4 php5 phps),
 +      map { $_ => 'pl'  } qw(perl pm), # perhaps also 'cgi'
 +      map { $_ => 'make'} qw(mak mk),
 +      map { $_ => 'xml' } qw(xhtml html htm),
 +);
 +
  # You define site-wide feature defaults here; override them with
  # $GITWEB_CONFIG as necessary.
  our %feature = (
        # return value of feature-sub indicates if to enable specified feature
        #
        # if there is no 'sub' key (no feature-sub), then feature cannot be
 -      # overriden
 +      # overridden
        #
        # use gitweb_get_feature(<feature>) to retrieve the <feature> value
        # (an array) or gitweb_check_feature(<feature>) to check if <feature>
        # $feature{'blame'}{'override'} = 1;
        # and in project config gitweb.blame = 0|1;
        'blame' => {
 -              'sub' => \&feature_blame,
 +              'sub' => sub { feature_bool('blame', @_) },
                'override' => 0,
                'default' => [0]},
  
        # Enable grep search, which will list the files in currently selected
        # tree containing the given string. Enabled by default. This can be
        # potentially CPU-intensive, of course.
 +      # Note that you need to have 'search' feature enabled too.
  
        # To enable system wide have in $GITWEB_CONFIG
        # $feature{'grep'}{'default'} = [1];
        # $feature{'grep'}{'override'} = 1;
        # and in project config gitweb.grep = 0|1;
        'grep' => {
 -              'sub' => \&feature_grep,
 +              'sub' => sub { feature_bool('grep', @_) },
                'override' => 0,
                'default' => [1]},
  
        # Enable the pickaxe search, which will list the commits that modified
        # a given string in a file. This can be practical and quite faster
        # alternative to 'blame', but still potentially CPU-intensive.
 +      # Note that you need to have 'search' feature enabled too.
  
        # To enable system wide have in $GITWEB_CONFIG
        # $feature{'pickaxe'}{'default'} = [1];
        # $feature{'pickaxe'}{'override'} = 1;
        # and in project config gitweb.pickaxe = 0|1;
        'pickaxe' => {
 -              'sub' => \&feature_pickaxe,
 +              'sub' => sub { feature_bool('pickaxe', @_) },
 +              'override' => 0,
 +              'default' => [1]},
 +
 +      # Enable showing size of blobs in a 'tree' view, in a separate
 +      # column, similar to what 'ls -l' does.  This cost a bit of IO.
 +
 +      # To disable system wide have in $GITWEB_CONFIG
 +      # $feature{'show-sizes'}{'default'} = [0];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'show-sizes'}{'override'} = 1;
 +      # and in project config gitweb.showsizes = 0|1;
 +      'show-sizes' => {
 +              'sub' => sub { feature_bool('showsizes', @_) },
                'override' => 0,
                'default' => [1]},
  
                'override' => 0,
                'default' => []},
  
 -      # Allow gitweb scan project content tags described in ctags/
 -      # of project repository, and display the popular Web 2.0-ish
 -      # "tag cloud" near the project list. Note that this is something
 -      # COMPLETELY different from the normal Git tags.
 +      # Allow gitweb scan project content tags of project repository,
 +      # and display the popular Web 2.0-ish "tag cloud" near the projects
 +      # list.  Note that this is something COMPLETELY different from the
 +      # normal Git tags.
  
        # gitweb by itself can show existing tags, but it does not handle
 -      # tagging itself; you need an external application for that.
 -      # For an example script, check Girocco's cgi/tagproj.cgi.
 +      # tagging itself; you need to do it externally, outside gitweb.
 +      # The format is described in git_get_project_ctags() subroutine.
        # You may want to install the HTML::TagCloud Perl module to get
        # a pretty tag cloud instead of just a list of tags.
  
        # To enable system wide have in $GITWEB_CONFIG
 -      # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
 +      # $feature{'ctags'}{'default'} = [1];
        # Project specific override is not supported.
 +
 +      # In the future whether ctags editing is enabled might depend
 +      # on the value, but using 1 should always mean no editing of ctags.
        'ctags' => {
                'override' => 0,
                'default' => [0]},
 +
 +      # The maximum number of patches in a patchset generated in patch
 +      # view. Set this to 0 or undef to disable patch view, or to a
 +      # negative number to remove any limit.
 +
 +      # To disable system wide have in $GITWEB_CONFIG
 +      # $feature{'patches'}{'default'} = [0];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'patches'}{'override'} = 1;
 +      # and in project config gitweb.patches = 0|n;
 +      # where n is the maximum number of patches allowed in a patchset.
 +      'patches' => {
 +              'sub' => \&feature_patches,
 +              'override' => 0,
 +              'default' => [16]},
 +
 +      # Avatar support. When this feature is enabled, views such as
 +      # shortlog or commit will display an avatar associated with
 +      # the email of the committer(s) and/or author(s).
 +
 +      # Currently available providers are gravatar and picon.
 +      # If an unknown provider is specified, the feature is disabled.
 +
 +      # Gravatar depends on Digest::MD5.
 +      # Picon currently relies on the indiana.edu database.
 +
 +      # To enable system wide have in $GITWEB_CONFIG
 +      # $feature{'avatar'}{'default'} = ['<provider>'];
 +      # where <provider> is either gravatar or picon.
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'avatar'}{'override'} = 1;
 +      # and in project config gitweb.avatar = <provider>;
 +      'avatar' => {
 +              'sub' => \&feature_avatar,
 +              'override' => 0,
 +              'default' => ['']},
 +
 +      # Enable displaying how much time and how many git commands
 +      # it took to generate and display page.  Disabled by default.
 +      # Project specific override is not supported.
 +      'timed' => {
 +              'override' => 0,
 +              'default' => [0]},
 +
 +      # Enable turning some links into links to actions which require
 +      # JavaScript to run (like 'blame_incremental').  Not enabled by
 +      # default.  Project specific override is currently not supported.
 +      'javascript-actions' => {
 +              'override' => 0,
 +              'default' => [0]},
 +
 +      # Enable and configure ability to change common timezone for dates
 +      # in gitweb output via JavaScript.  Enabled by default.
 +      # Project specific override is not supported.
 +      'javascript-timezone' => {
 +              'override' => 0,
 +              'default' => [
 +                      'local',     # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
 +                                   # or undef to turn off this feature
 +                      'gitweb_tz', # name of cookie where to store selected timezone
 +                      'datetime',  # CSS class used to mark up dates for manipulation
 +              ]},
 +
 +      # Syntax highlighting support. This is based on Daniel Svensson's
 +      # and Sham Chukoury's work in gitweb-xmms2.git.
 +      # It requires the 'highlight' program present in $PATH,
 +      # and therefore is disabled by default.
 +
 +      # To enable system wide have in $GITWEB_CONFIG
 +      # $feature{'highlight'}{'default'} = [1];
 +
 +      'highlight' => {
 +              'sub' => sub { feature_bool('highlight', @_) },
 +              'override' => 0,
 +              'default' => [0]},
 +
 +      # Enable displaying of remote heads in the heads list
 +
 +      # To enable system wide have in $GITWEB_CONFIG
 +      # $feature{'remote_heads'}{'default'} = [1];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'remote_heads'}{'override'} = 1;
 +      # and in project config gitweb.remote_heads = 0|1;
 +      'remote_heads' => {
 +              'sub' => sub { feature_bool('remote_heads', @_) },
 +              'override' => 0,
 +              'default' => [0]},
  );
  
  sub gitweb_get_feature {
                $feature{$name}{'sub'},
                $feature{$name}{'override'},
                @{$feature{$name}{'default'}});
 -      if (!$override) { return @defaults; }
 +      # project specific override is possible only if we have project
 +      our $git_dir; # global variable, declared later
 +      if (!$override || !defined $git_dir) {
 +              return @defaults;
 +      }
        if (!defined $sub) {
 -              warn "feature $name is not overrideable";
 +              warn "feature $name is not overridable";
                return @defaults;
        }
        return $sub->(@defaults);
@@@ -566,17 -367,16 +566,17 @@@ sub gitweb_check_feature 
  }
  
  
 -sub feature_blame {
 -      my ($val) = git_get_project_config('blame', '--bool');
 +sub feature_bool {
 +      my $key = shift;
 +      my ($val) = git_get_project_config($key, '--bool');
  
 -      if ($val eq 'true') {
 -              return 1;
 +      if (!defined $val) {
 +              return ($_[0]);
 +      } elsif ($val eq 'true') {
 +              return (1);
        } elsif ($val eq 'false') {
 -              return 0;
 +              return (0);
        }
 -
 -      return $_[0];
  }
  
  sub feature_snapshot {
        return @fmts;
  }
  
 -sub feature_grep {
 -      my ($val) = git_get_project_config('grep', '--bool');
 +sub feature_patches {
 +      my @val = (git_get_project_config('patches', '--int'));
  
 -      if ($val eq 'true') {
 -              return (1);
 -      } elsif ($val eq 'false') {
 -              return (0);
 +      if (@val) {
 +              return @val;
        }
  
        return ($_[0]);
  }
  
 -sub feature_pickaxe {
 -      my ($val) = git_get_project_config('pickaxe', '--bool');
 -
 -      if ($val eq 'true') {
 -              return (1);
 -      } elsif ($val eq 'false') {
 -              return (0);
 -      }
 +sub feature_avatar {
 +      my @val = (git_get_project_config('avatar'));
  
 -      return ($_[0]);
 +      return @val ? @val : @_;
  }
  
  # checking HEAD file with -e is fragile if the repository was
@@@ -632,79 -440,22 +632,79 @@@ sub filter_snapshot_fmts 
        @fmts = map {
                exists $known_snapshot_format_aliases{$_} ?
                       $known_snapshot_format_aliases{$_} : $_} @fmts;
 -      @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
 +      @fmts = grep {
 +              exists $known_snapshot_formats{$_} &&
 +              !$known_snapshot_formats{$_}{'disabled'}} @fmts;
 +}
  
 +# If it is set to code reference, it is code that it is to be run once per
 +# request, allowing updating configurations that change with each request,
 +# while running other code in config file only once.
 +#
 +# Otherwise, if it is false then gitweb would process config file only once;
 +# if it is true then gitweb config would be run for each request.
 +our $per_request_config = 1;
 +
 +# read and parse gitweb config file given by its parameter.
 +# returns true on success, false on recoverable error, allowing
 +# to chain this subroutine, using first file that exists.
 +# dies on errors during parsing config file, as it is unrecoverable.
 +sub read_config_file {
 +      my $filename = shift;
 +      return unless defined $filename;
 +      # die if there are errors parsing config file
 +      if (-e $filename) {
 +              do $filename;
 +              die $@ if $@;
 +              return 1;
 +      }
 +      return;
  }
  
 -our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
 -if (-e $GITWEB_CONFIG) {
 -      do $GITWEB_CONFIG;
 -} else {
 +our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
 +sub evaluate_gitweb_config {
 +      our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
        our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
 -      do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
 +
 +      # use first config file that exists
 +      read_config_file($GITWEB_CONFIG) or
 +      read_config_file($GITWEB_CONFIG_SYSTEM);
 +}
 +
 +# Get loadavg of system, to compare against $maxload.
 +# Currently it requires '/proc/loadavg' present to get loadavg;
 +# if it is not present it returns 0, which means no load checking.
 +sub get_loadavg {
 +      if( -e '/proc/loadavg' ){
 +              open my $fd, '<', '/proc/loadavg'
 +                      or return 0;
 +              my @load = split(/\s+/, scalar <$fd>);
 +              close $fd;
 +
 +              # The first three columns measure CPU and IO utilization of the last one,
 +              # five, and 10 minute periods.  The fourth column shows the number of
 +              # currently running processes and the total number of processes in the m/n
 +              # format.  The last column displays the last process ID used.
 +              return $load[0] || 0;
 +      }
 +      # additional checks for load average should go here for things that don't export
 +      # /proc/loadavg
 +
 +      return 0;
  }
  
  # version of the core git binary
 -our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
 +our $git_version;
 +sub evaluate_git_version {
 +      our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
 +      $number_of_git_cmds++;
 +}
  
 -$projects_list ||= $projectroot;
 +sub check_loadavg {
 +      if (defined $maxload && get_loadavg() > $maxload) {
 +              die_error(503, "The load average on the server is too high");
 +      }
 +}
  
  # ======================================================================
  # input validation and dispatch
@@@ -740,17 -491,12 +740,17 @@@ our @cgi_param_mapping = 
        snapshot_format => "sf",
        extra_options => "opt",
        search_use_regexp => "sr",
 +      ctag => "by_tag",
 +      # this must be last entry (for manipulation from JavaScript)
 +      javascript => "js"
  );
  our %cgi_param_mapping = @cgi_param_mapping;
  
  # we will also need to know the possible actions, for validation
  our %actions = (
        "blame" => \&git_blame,
 +      "blame_incremental" => \&git_blame_incremental,
 +      "blame_data" => \&git_blame_data,
        "blobdiff" => \&git_blobdiff,
        "blobdiff_plain" => \&git_blobdiff_plain,
        "blob" => \&git_blob,
        "heads" => \&git_heads,
        "history" => \&git_history,
        "log" => \&git_log,
 +      "patch" => \&git_patch,
 +      "patches" => \&git_patches,
 +      "remotes" => \&git_remotes,
        "rss" => \&git_rss,
        "atom" => \&git_atom,
        "search" => \&git_search,
@@@ -792,15 -535,11 +792,15 @@@ our %allowed_options = 
  # should be single values, but opt can be an array. We should probably
  # build an array of parameters that can be multi-valued, but since for the time
  # being it's only this one, we just single it out
 -while (my ($name, $symbol) = each %cgi_param_mapping) {
 -      if ($symbol eq 'opt') {
 -              $input_params{$name} = [ $cgi->param($symbol) ];
 -      } else {
 -              $input_params{$name} = $cgi->param($symbol);
 +sub evaluate_query_params {
 +      our $cgi;
 +
 +      while (my ($name, $symbol) = each %cgi_param_mapping) {
 +              if ($symbol eq 'opt') {
 +                      $input_params{$name} = [ $cgi->param($symbol) ];
 +              } else {
 +                      $input_params{$name} = $cgi->param($symbol);
 +              }
        }
  }
  
@@@ -839,10 -578,10 +839,10 @@@ sub evaluate_path_info 
                'history',
        );
  
 -      # we want to catch
 +      # we want to catch, among others
        # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
        my ($parentrefname, $parentpathname, $refname, $pathname) =
 -              ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
 +              ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
  
        # first, analyze the 'current' part
        if (defined $pathname) {
                # hash_base instead. It should also be noted that hand-crafted
                # links having 'history' as an action and no pathname or hash
                # set will fail, but that happens regardless of PATH_INFO.
 -              $input_params{'action'} ||= "shortlog";
 -              if (grep { $_ eq $input_params{'action'} } @wants_base) {
 +              if (defined $parentrefname) {
 +                      # if there is parent let the default be 'shortlog' action
 +                      # (for http://git.example.com/repo.git/A..B links); if there
 +                      # is no parent, dispatch will detect type of object and set
 +                      # action appropriately if required (if action is not set)
 +                      $input_params{'action'} ||= "shortlog";
 +              }
 +              if ($input_params{'action'} &&
 +                  grep { $_ eq $input_params{'action'} } @wants_base) {
                        $input_params{'hash_base'} ||= $refname;
                } else {
                        $input_params{'hash'} ||= $refname;
                # extensions. Allowed extensions are both the defined suffix
                # (which includes the initial dot already) and the snapshot
                # format key itself, with a prepended dot
 -              while (my ($fmt, %opt) = each %known_snapshot_formats) {
 +              while (my ($fmt, $opt) = each %known_snapshot_formats) {
                        my $hash = $refname;
 -                      my $sfx;
 -                      $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//;
 -                      next unless $sfx = $1;
 +                      unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
 +                              next;
 +                      }
 +                      my $sfx = $1;
                        # a valid suffix was found, so set the snapshot format
                        # and reset the hash parameter
                        $input_params{'snapshot_format'} = $fmt;
                }
        }
  }
 -evaluate_path_info();
  
 -our $action = $input_params{'action'};
 -if (defined $action) {
 -      if (!validate_action($action)) {
 -              die_error(400, "Invalid action parameter");
 +our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
 +     $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
 +     $searchtext, $search_regexp);
 +sub evaluate_and_validate_params {
 +      our $action = $input_params{'action'};
 +      if (defined $action) {
 +              if (!validate_action($action)) {
 +                      die_error(400, "Invalid action parameter");
 +              }
        }
 -}
  
 -# parameters which are pathnames
 -our $project = $input_params{'project'};
 -if (defined $project) {
 -      if (!validate_project($project)) {
 -              undef $project;
 -              die_error(404, "No such project");
 +      # parameters which are pathnames
 +      our $project = $input_params{'project'};
 +      if (defined $project) {
 +              if (!validate_project($project)) {
 +                      undef $project;
 +                      die_error(404, "No such project");
 +              }
        }
 -}
  
 -our $file_name = $input_params{'file_name'};
 -if (defined $file_name) {
 -      if (!validate_pathname($file_name)) {
 -              die_error(400, "Invalid file parameter");
 +      our $file_name = $input_params{'file_name'};
 +      if (defined $file_name) {
 +              if (!validate_pathname($file_name)) {
 +                      die_error(400, "Invalid file parameter");
 +              }
        }
 -}
  
 -our $file_parent = $input_params{'file_parent'};
 -if (defined $file_parent) {
 -      if (!validate_pathname($file_parent)) {
 -              die_error(400, "Invalid file parent parameter");
 +      our $file_parent = $input_params{'file_parent'};
 +      if (defined $file_parent) {
 +              if (!validate_pathname($file_parent)) {
 +                      die_error(400, "Invalid file parent parameter");
 +              }
        }
 -}
  
 -# parameters which are refnames
 -our $hash = $input_params{'hash'};
 -if (defined $hash) {
 -      if (!validate_refname($hash)) {
 -              die_error(400, "Invalid hash parameter");
 +      # parameters which are refnames
 +      our $hash = $input_params{'hash'};
 +      if (defined $hash) {
 +              if (!validate_refname($hash)) {
 +                      die_error(400, "Invalid hash parameter");
 +              }
        }
 -}
  
 -our $hash_parent = $input_params{'hash_parent'};
 -if (defined $hash_parent) {
 -      if (!validate_refname($hash_parent)) {
 -              die_error(400, "Invalid hash parent parameter");
 +      our $hash_parent = $input_params{'hash_parent'};
 +      if (defined $hash_parent) {
 +              if (!validate_refname($hash_parent)) {
 +                      die_error(400, "Invalid hash parent parameter");
 +              }
        }
 -}
  
 -our $hash_base = $input_params{'hash_base'};
 -if (defined $hash_base) {
 -      if (!validate_refname($hash_base)) {
 -              die_error(400, "Invalid hash base parameter");
 +      our $hash_base = $input_params{'hash_base'};
 +      if (defined $hash_base) {
 +              if (!validate_refname($hash_base)) {
 +                      die_error(400, "Invalid hash base parameter");
 +              }
        }
 -}
  
 -our @extra_options = @{$input_params{'extra_options'}};
 -# @extra_options is always defined, since it can only be (currently) set from
 -# CGI, and $cgi->param() returns the empty array in array context if the param
 -# is not set
 -foreach my $opt (@extra_options) {
 -      if (not exists $allowed_options{$opt}) {
 -              die_error(400, "Invalid option parameter");
 -      }
 -      if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
 -              die_error(400, "Invalid option parameter for this action");
 +      our @extra_options = @{$input_params{'extra_options'}};
 +      # @extra_options is always defined, since it can only be (currently) set from
 +      # CGI, and $cgi->param() returns the empty array in array context if the param
 +      # is not set
 +      foreach my $opt (@extra_options) {
 +              if (not exists $allowed_options{$opt}) {
 +                      die_error(400, "Invalid option parameter");
 +              }
 +              if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
 +                      die_error(400, "Invalid option parameter for this action");
 +              }
        }
 -}
  
 -our $hash_parent_base = $input_params{'hash_parent_base'};
 -if (defined $hash_parent_base) {
 -      if (!validate_refname($hash_parent_base)) {
 -              die_error(400, "Invalid hash parent base parameter");
 +      our $hash_parent_base = $input_params{'hash_parent_base'};
 +      if (defined $hash_parent_base) {
 +              if (!validate_refname($hash_parent_base)) {
 +                      die_error(400, "Invalid hash parent base parameter");
 +              }
        }
 -}
  
 -# other parameters
 -our $page = $input_params{'page'};
 -if (defined $page) {
 -      if ($page =~ m/[^0-9]/) {
 -              die_error(400, "Invalid page parameter");
 +      # other parameters
 +      our $page = $input_params{'page'};
 +      if (defined $page) {
 +              if ($page =~ m/[^0-9]/) {
 +                      die_error(400, "Invalid page parameter");
 +              }
        }
 -}
  
 -our $searchtype = $input_params{'searchtype'};
 -if (defined $searchtype) {
 -      if ($searchtype =~ m/[^a-z]/) {
 -              die_error(400, "Invalid searchtype parameter");
 +      our $searchtype = $input_params{'searchtype'};
 +      if (defined $searchtype) {
 +              if ($searchtype =~ m/[^a-z]/) {
 +                      die_error(400, "Invalid searchtype parameter");
 +              }
        }
 -}
  
 -our $search_use_regexp = $input_params{'search_use_regexp'};
 +      our $search_use_regexp = $input_params{'search_use_regexp'};
  
 -our $searchtext = $input_params{'searchtext'};
 -our $search_regexp;
 -if (defined $searchtext) {
 -      if (length($searchtext) < 2) {
 -              die_error(403, "At least two characters are required for search parameter");
 +      our $searchtext = $input_params{'searchtext'};
 +      our $search_regexp;
 +      if (defined $searchtext) {
 +              if (length($searchtext) < 2) {
 +                      die_error(403, "At least two characters are required for search parameter");
 +              }
 +              $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
        }
 -      $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
  }
  
  # path to the current git repository
  our $git_dir;
 -$git_dir = "$projectroot/$project" if $project;
 +sub evaluate_git_dir {
 +      our $git_dir = "$projectroot/$project" if $project;
 +}
 +
 +our (@snapshot_fmts, $git_avatar);
 +sub configure_gitweb_features {
 +      # list of supported snapshot formats
 +      our @snapshot_fmts = gitweb_get_feature('snapshot');
 +      @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
 +
 +      # check that the avatar feature is set to a known provider name,
 +      # and for each provider check if the dependencies are satisfied.
 +      # if the provider name is invalid or the dependencies are not met,
 +      # reset $git_avatar to the empty string.
 +      our ($git_avatar) = gitweb_get_feature('avatar');
 +      if ($git_avatar eq 'gravatar') {
 +              $git_avatar = '' unless (eval { require Digest::MD5; 1; });
 +      } elsif ($git_avatar eq 'picon') {
 +              # no dependencies
 +      } else {
 +              $git_avatar = '';
 +      }
 +}
  
 -# list of supported snapshot formats
 -our @snapshot_fmts = gitweb_get_feature('snapshot');
 -@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
 +# custom error handler: 'die <message>' is Internal Server Error
 +sub handle_errors_html {
 +      my $msg = shift; # it is already HTML escaped
 +
 +      # to avoid infinite loop where error occurs in die_error,
 +      # change handler to default handler, disabling handle_errors_html
 +      set_message("Error occured when inside die_error:\n$msg");
 +
 +      # you cannot jump out of die_error when called as error handler;
 +      # the subroutine set via CGI::Carp::set_message is called _after_
 +      # HTTP headers are already written, so it cannot write them itself
 +      die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
 +}
 +set_message(\&handle_errors_html);
  
  # dispatch
 -if (!defined $action) {
 -      if (defined $hash) {
 -              $action = git_get_type($hash);
 -      } elsif (defined $hash_base && defined $file_name) {
 -              $action = git_get_type("$hash_base:$file_name");
 -      } elsif (defined $project) {
 -              $action = 'summary';
 -      } else {
 -              $action = 'project_list';
 +sub dispatch {
 +      if (!defined $action) {
 +              if (defined $hash) {
 +                      $action = git_get_type($hash);
 +              } elsif (defined $hash_base && defined $file_name) {
 +                      $action = git_get_type("$hash_base:$file_name");
 +              } elsif (defined $project) {
 +                      $action = 'summary';
 +              } else {
 +                      $action = 'project_list';
 +              }
 +      }
 +      if (!defined($actions{$action})) {
 +              die_error(400, "Unknown action");
 +      }
 +      if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
 +          !$project) {
 +              die_error(400, "Project needed");
 +      }
 +      $actions{$action}->();
 +}
 +
 +sub reset_timer {
 +      our $t0 = [ gettimeofday() ]
 +              if defined $t0;
 +      our $number_of_git_cmds = 0;
 +}
 +
 +our $first_request = 1;
 +sub run_request {
 +      reset_timer();
 +
 +      evaluate_uri();
 +      if ($first_request) {
 +              evaluate_gitweb_config();
 +              evaluate_git_version();
 +      }
 +      if ($per_request_config) {
 +              if (ref($per_request_config) eq 'CODE') {
 +                      $per_request_config->();
 +              } elsif (!$first_request) {
 +                      evaluate_gitweb_config();
 +              }
        }
 +      check_loadavg();
 +
 +      # $projectroot and $projects_list might be set in gitweb config file
 +      $projects_list ||= $projectroot;
 +
 +      evaluate_query_params();
 +      evaluate_path_info();
 +      evaluate_and_validate_params();
 +      evaluate_git_dir();
 +
 +      configure_gitweb_features();
 +
 +      dispatch();
 +}
 +
 +our $is_last_request = sub { 1 };
 +our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
 +our $CGI = 'CGI';
 +our $cgi;
 +sub configure_as_fcgi {
 +      require CGI::Fast;
 +      our $CGI = 'CGI::Fast';
 +
 +      my $request_number = 0;
 +      # let each child service 100 requests
 +      our $is_last_request = sub { ++$request_number > 100 };
  }
 -if (!defined($actions{$action})) {
 -      die_error(400, "Unknown action");
 +sub evaluate_argv {
 +      my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
 +      configure_as_fcgi()
 +              if $script_name =~ /\.fcgi$/;
 +
 +      return unless (@ARGV);
 +
 +      require Getopt::Long;
 +      Getopt::Long::GetOptions(
 +              'fastcgi|fcgi|f' => \&configure_as_fcgi,
 +              'nproc|n=i' => sub {
 +                      my ($arg, $val) = @_;
 +                      return unless eval { require FCGI::ProcManager; 1; };
 +                      my $proc_manager = FCGI::ProcManager->new({
 +                              n_processes => $val,
 +                      });
 +                      our $pre_listen_hook    = sub { $proc_manager->pm_manage()        };
 +                      our $pre_dispatch_hook  = sub { $proc_manager->pm_pre_dispatch()  };
 +                      our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
 +              },
 +      );
 +}
 +
 +sub run {
 +      evaluate_argv();
 +
 +      $first_request = 1;
 +      $pre_listen_hook->()
 +              if $pre_listen_hook;
 +
 + REQUEST:
 +      while ($cgi = $CGI->new()) {
 +              $pre_dispatch_hook->()
 +                      if $pre_dispatch_hook;
 +
 +              run_request();
 +
 +              $post_dispatch_hook->()
 +                      if $post_dispatch_hook;
 +              $first_request = 0;
 +
 +              last REQUEST if ($is_last_request->());
 +      }
 +
 + DONE_GITWEB:
 +      1;
  }
 -if ($action !~ m/^(opml|project_list|project_index)$/ &&
 -    !$project) {
 -      die_error(400, "Project needed");
 +
 +run();
 +
 +if (defined caller) {
 +      # wrapped in a subroutine processing requests,
 +      # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
 +      return;
 +} else {
 +      # pure CGI script, serving single request
 +      exit;
  }
 -$actions{$action}->();
 -exit;
  
  ## ======================================================================
  ## action links
  
 -sub href (%) {
 +# possible values of extra options
 +# -full => 0|1      - use absolute/full URL ($my_uri/$my_url as base)
 +# -replay => 1      - start from a current view (replay with modifications)
 +# -path_info => 0|1 - don't use/use path_info URL (if possible)
 +# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
 +sub href {
        my %params = @_;
        # default is to use -absolute url() i.e. $my_uri
        my $href = $params{-full} ? $my_url : $my_uri;
  
 +      # implicit -replay, must be first of implicit params
 +      $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
 +
        $params{'project'} = $project unless exists $params{'project'};
  
        if ($params{-replay}) {
        }
  
        my $use_pathinfo = gitweb_check_feature('pathinfo');
 -      if ($use_pathinfo) {
 +      if (defined $params{'project'} &&
 +          (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
                # try to put as many parameters as possible in PATH_INFO:
                #   - project name
                #   - action
                $href =~ s,/$,,;
  
                # Then add the project name, if present
 -              $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
 +              $href .= "/".esc_path_info($params{'project'});
                delete $params{'project'};
  
                # since we destructively absorb parameters, we keep this
                # Summary just uses the project path URL, any other action is
                # added to the URL
                if (defined $params{'action'}) {
 -                      $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
 +                      $href .= "/".esc_path_info($params{'action'})
 +                              unless $params{'action'} eq 'summary';
                        delete $params{'action'};
                }
  
                        || $params{'hash_parent'} || $params{'hash'});
                if (defined $params{'hash_base'}) {
                        if (defined $params{'hash_parent_base'}) {
 -                              $href .= esc_url($params{'hash_parent_base'});
 +                              $href .= esc_path_info($params{'hash_parent_base'});
                                # skip the file_parent if it's the same as the file_name
 -                              delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
 -                              if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
 -                                      $href .= ":/".esc_url($params{'file_parent'});
 -                                      delete $params{'file_parent'};
 +                              if (defined $params{'file_parent'}) {
 +                                      if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
 +                                              delete $params{'file_parent'};
 +                                      } elsif ($params{'file_parent'} !~ /\.\./) {
 +                                              $href .= ":/".esc_path_info($params{'file_parent'});
 +                                              delete $params{'file_parent'};
 +                                      }
                                }
                                $href .= "..";
                                delete $params{'hash_parent'};
                                delete $params{'hash_parent_base'};
                        } elsif (defined $params{'hash_parent'}) {
 -                              $href .= esc_url($params{'hash_parent'}). "..";
 +                              $href .= esc_path_info($params{'hash_parent'}). "..";
                                delete $params{'hash_parent'};
                        }
  
 -                      $href .= esc_url($params{'hash_base'});
 +                      $href .= esc_path_info($params{'hash_base'});
                        if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
 -                              $href .= ":/".esc_url($params{'file_name'});
 +                              $href .= ":/".esc_path_info($params{'file_name'});
                                delete $params{'file_name'};
                        }
                        delete $params{'hash'};
                        delete $params{'hash_base'};
                } elsif (defined $params{'hash'}) {
 -                      $href .= esc_url($params{'hash'});
 +                      $href .= esc_path_info($params{'hash'});
                        delete $params{'hash'};
                }
  
        }
        $href .= "?" . join(';', @result) if scalar @result;
  
 +      # final transformation: trailing spaces must be escaped (URI-encoded)
 +      $href =~ s/(\s+)$/CGI::escape($1)/e;
 +
 +      if ($params{-anchor}) {
 +              $href .= "#".esc_param($params{-anchor});
 +      }
 +
        return $href;
  }
  
@@@ -1423,7 -988,6 +1423,7 @@@ sub validate_refname 
  # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
  sub to_utf8 {
        my $str = shift;
 +      return undef unless defined $str;
        if (utf8::valid($str)) {
                utf8::decode($str);
                return $str;
  # correct, but quoted slashes look too horrible in bookmarks
  sub esc_param {
        my $str = shift;
 -      $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
 -      $str =~ s/\+/%2B/g;
 +      return undef unless defined $str;
 +      $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
        $str =~ s/ /\+/g;
        return $str;
  }
  
 -# quote unsafe chars in whole URL, so some charactrs cannot be quoted
 +# the quoting rules for path_info fragment are slightly different
 +sub esc_path_info {
 +      my $str = shift;
 +      return undef unless defined $str;
 +
 +      # path_info doesn't treat '+' as space (specially), but '?' must be escaped
 +      $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
 +
 +      return $str;
 +}
 +
 +# quote unsafe chars in whole URL, so some characters cannot be quoted
  sub esc_url {
        my $str = shift;
 -      $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
 -      $str =~ s/\+/%2B/g;
 +      return undef unless defined $str;
 +      $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
        $str =~ s/ /\+/g;
        return $str;
  }
  
 +# quote unsafe characters in HTML attributes
 +sub esc_attr {
 +
 +      # for XHTML conformance escaping '"' to '&quot;' is not enough
 +      return esc_html(@_);
 +}
 +
  # replace invalid utf8 character with SUBSTITUTION sequence
 -sub esc_html ($;%) {
 +sub esc_html {
        my $str = shift;
        my %opts = @_;
  
 +      return undef unless defined $str;
 +
        $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
@@@ -1490,8 -1034,6 +1490,8 @@@ sub esc_path 
        my $str = shift;
        my %opts = @_;
  
 +      return undef unless defined $str;
 +
        $str = to_utf8($str);
        $str = $cgi->escapeHTML($str);
        if ($opts{'-nbsp'}) {
@@@ -1634,6 -1176,7 +1634,6 @@@ sub chop_str 
                $str =~ m/^(.*?)($begre)$/;
                my ($lead, $body) = ($1, $2);
                if (length($lead) > 4) {
 -                      $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
                        $lead = " ...";
                }
                return "$lead$body";
                $str =~ m/^(.*?)($begre)$/;
                my ($mid, $right) = ($1, $2);
                if (length($mid) > 5) {
 -                      $left  =~ s/&[^;]*$//;
 -                      $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
                        $mid = " ... ";
                }
                return "$left$mid$right";
                my $body = $1;
                my $tail = $2;
                if (length($tail) > 4) {
 -                      $body =~ s/&[^;]*$//;
                        $tail = "... ";
                }
                return "$body$tail";
@@@ -1669,7 -1215,7 +1669,7 @@@ sub chop_and_escape_str 
        if ($chopped eq $str) {
                return esc_html($chopped);
        } else {
 -              $str =~ s/([[:cntrl:]])/?/g;
 +              $str =~ s/[[:cntrl:]]/?/g;
                return $cgi->span({-title=>$str}, esc_html($chopped));
        }
  }
@@@ -1730,7 -1276,7 +1730,7 @@@ use constant 
  };
  
  # submodule/subproject, a commit object reference
 -sub S_ISGITLINK($) {
 +sub S_ISGITLINK {
        my $mode = shift;
  
        return (($mode & S_IFMT) == S_IFGITLINK)
@@@ -1818,11 -1364,13 +1818,11 @@@ sub format_log_line_html 
        my $line = shift;
  
        $line = esc_html($line, -nbsp=>1);
 -      if ($line =~ m/([0-9a-fA-F]{8,40})/) {
 -              my $hash_text = $1;
 -              my $link =
 -                      $cgi->a({-href => href(action=>"object", hash=>$hash_text),
 -                              -class => "text"}, $hash_text);
 -              $line =~ s/$hash_text/$link/;
 -      }
 +      $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
 +              $cgi->a({-href => href(action=>"object", hash=>$1),
 +                                      -class => "text"}, $1);
 +      }eg;
 +
        return $line;
  }
  
@@@ -1874,7 -1422,7 +1874,7 @@@ sub format_ref_marker 
                                        hash=>$dest
                                )}, $name);
  
 -                      $markers .= " <span class=\"$class\" title=\"$ref\">" .
 +                      $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
                                $link . "</span>";
                }
        }
@@@ -1892,117 -1440,15 +1892,117 @@@ sub format_subject_html 
        $extra = '' unless defined($extra);
  
        if (length($short) < length($long)) {
 +              $long =~ s/[[:cntrl:]]/?/g;
                return $cgi->a({-href => $href, -class => "list subject",
                                -title => to_utf8($long)},
 -                     esc_html($short) . $extra);
 +                     esc_html($short)) . $extra;
        } else {
                return $cgi->a({-href => $href, -class => "list subject"},
 -                     esc_html($long)  . $extra);
 +                     esc_html($long)) . $extra;
 +      }
 +}
 +
 +# Rather than recomputing the url for an email multiple times, we cache it
 +# after the first hit. This gives a visible benefit in views where the avatar
 +# for the same email is used repeatedly (e.g. shortlog).
 +# The cache is shared by all avatar engines (currently gravatar only), which
 +# are free to use it as preferred. Since only one avatar engine is used for any
 +# given page, there's no risk for cache conflicts.
 +our %avatar_cache = ();
 +
 +# Compute the picon url for a given email, by using the picon search service over at
 +# http://www.cs.indiana.edu/picons/search.html
 +sub picon_url {
 +      my $email = lc shift;
 +      if (!$avatar_cache{$email}) {
 +              my ($user, $domain) = split('@', $email);
 +              $avatar_cache{$email} =
 +                      "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
 +                      "$domain/$user/" .
 +                      "users+domains+unknown/up/single";
 +      }
 +      return $avatar_cache{$email};
 +}
 +
 +# Compute the gravatar url for a given email, if it's not in the cache already.
 +# Gravatar stores only the part of the URL before the size, since that's the
 +# one computationally more expensive. This also allows reuse of the cache for
 +# different sizes (for this particular engine).
 +sub gravatar_url {
 +      my $email = lc shift;
 +      my $size = shift;
 +      $avatar_cache{$email} ||=
 +              "http://www.gravatar.com/avatar/" .
 +                      Digest::MD5::md5_hex($email) . "?s=";
 +      return $avatar_cache{$email} . $size;
 +}
 +
 +# Insert an avatar for the given $email at the given $size if the feature
 +# is enabled.
 +sub git_get_avatar {
 +      my ($email, %opts) = @_;
 +      my $pre_white  = ($opts{-pad_before} ? "&nbsp;" : "");
 +      my $post_white = ($opts{-pad_after}  ? "&nbsp;" : "");
 +      $opts{-size} ||= 'default';
 +      my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
 +      my $url = "";
 +      if ($git_avatar eq 'gravatar') {
 +              $url = gravatar_url($email, $size);
 +      } elsif ($git_avatar eq 'picon') {
 +              $url = picon_url($email);
 +      }
 +      # Other providers can be added by extending the if chain, defining $url
 +      # as needed. If no variant puts something in $url, we assume avatars
 +      # are completely disabled/unavailable.
 +      if ($url) {
 +              return $pre_white .
 +                     "<img width=\"$size\" " .
 +                          "class=\"avatar\" " .
 +                          "src=\"".esc_url($url)."\" " .
 +                          "alt=\"\" " .
 +                     "/>" . $post_white;
 +      } else {
 +              return "";
 +      }
 +}
 +
 +sub format_search_author {
 +      my ($author, $searchtype, $displaytext) = @_;
 +      my $have_search = gitweb_check_feature('search');
 +
 +      if ($have_search) {
 +              my $performed = "";
 +              if ($searchtype eq 'author') {
 +                      $performed = "authored";
 +              } elsif ($searchtype eq 'committer') {
 +                      $performed = "committed";
 +              }
 +
 +              return $cgi->a({-href => href(action=>"search", hash=>$hash,
 +                              searchtext=>$author,
 +                              searchtype=>$searchtype), class=>"list",
 +                              title=>"Search for commits $performed by $author"},
 +                              $displaytext);
 +
 +      } else {
 +              return $displaytext;
        }
  }
  
 +# format the author name of the given commit with the given tag
 +# the author name is chopped and escaped according to the other
 +# optional parameters (see chop_str).
 +sub format_author_html {
 +      my $tag = shift;
 +      my $co = shift;
 +      my $author = chop_and_escape_str($co->{'author_name'}, @_);
 +      return "<$tag class=\"author\">" .
 +             format_search_author($co->{'author_name'}, "author",
 +                     git_get_avatar($co->{'author_email'}, -pad_after => 1) .
 +                     $author) .
 +             "</$tag>";
 +}
 +
  # format git diff header line, i.e. "diff --(git|combined|cc) ..."
  sub format_git_diff_header_line {
        my $line = shift;
@@@ -2365,7 -1811,6 +2365,7 @@@ sub get_feed_info 
  
  # returns path to the core git executable and the --git-dir parameter as list
  sub git_cmd {
 +      $number_of_git_cmds++;
        return $GIT, '--git-dir='.$git_dir;
  }
  
  # Try to avoid using this function wherever possible.
  sub quote_command {
        return join(' ',
 -                  map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
 +              map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
  }
  
  # get HEAD ref of given project as hash
  sub git_get_head_hash {
 -      my $project = shift;
 +      return git_get_full_hash(shift, 'HEAD');
 +}
 +
 +sub git_get_full_hash {
 +      return git_get_hash(@_);
 +}
 +
 +sub git_get_short_hash {
 +      return git_get_hash(@_, '--short=7');
 +}
 +
 +sub git_get_hash {
 +      my ($project, $hash, @options) = @_;
        my $o_git_dir = $git_dir;
        my $retval = undef;
        $git_dir = "$projectroot/$project";
 -      if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
 -              my $head = <$fd>;
 +      if (open my $fd, '-|', git_cmd(), 'rev-parse',
 +          '--verify', '-q', @options, $hash) {
 +              $retval = <$fd>;
 +              chomp $retval if defined $retval;
                close $fd;
 -              if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
 -                      $retval = $1;
 -              }
        }
        if (defined $o_git_dir) {
                $git_dir = $o_git_dir;
@@@ -2460,19 -1894,18 +2460,19 @@@ sub git_parse_project_config 
        return %config;
  }
  
 -# convert config value to boolean, 'true' or 'false'
 +# convert config value to boolean: 'true' or 'false'
  # no value, number > 0, 'true' and 'yes' values are true
  # rest of values are treated as false (never as error)
  sub config_to_bool {
        my $val = shift;
  
 +      return 1 if !defined $val;             # section.key
 +
        # strip leading and trailing whitespace
        $val =~ s/^\s+//;
        $val =~ s/\s+$//;
  
 -      return (!defined $val ||               # section.key
 -              ($val =~ /^\d+$/ && $val) ||   # section.key = 1
 +      return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
                ($val =~ /^(?:true|yes)$/i));  # section.key = true
  }
  
@@@ -2506,8 -1939,6 +2506,8 @@@ sub config_to_multi 
  sub git_get_project_config {
        my ($key, $type) = @_;
  
 +      return unless defined $git_dir;
 +
        # key sanity check
        return unless ($key);
        $key =~ s/^gitweb\.//;
                $config_file = "$git_dir/config";
        }
  
 +      # check if config variable (key) exists
 +      return unless exists $config{"gitweb.$key"};
 +
        # ensure given type
        if (!defined $type) {
                return $config{"gitweb.$key"};
@@@ -2596,94 -2024,38 +2596,94 @@@ sub git_get_path_by_hash 
  ## ......................................................................
  ## git utility functions, directly accessing git repository
  
 -sub git_get_project_description {
 -      my $path = shift;
 +# get the value of config variable either from file named as the variable
 +# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
 +# configuration variable in the repository config file.
 +sub git_get_file_or_project_config {
 +      my ($path, $name) = @_;
  
        $git_dir = "$projectroot/$path";
 -      open my $fd, "$git_dir/description"
 -              or return git_get_project_config('description');
 -      my $descr = <$fd>;
 +      open my $fd, '<', "$git_dir/$name"
 +              or return git_get_project_config($name);
 +      my $conf = <$fd>;
        close $fd;
 -      if (defined $descr) {
 -              chomp $descr;
 +      if (defined $conf) {
 +              chomp $conf;
        }
 -      return $descr;
 +      return $conf;
  }
  
 -sub git_get_project_ctags {
 +sub git_get_project_description {
 +      my $path = shift;
 +      return git_get_file_or_project_config($path, 'description');
 +}
 +
 +sub git_get_project_category {
        my $path = shift;
 +      return git_get_file_or_project_config($path, 'category');
 +}
 +
 +
 +# supported formats:
 +# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
 +#   - if its contents is a number, use it as tag weight,
 +#   - otherwise add a tag with weight 1
 +# * $GIT_DIR/ctags file, each line is a tag (with weight 1)
 +#   the same value multiple times increases tag weight
 +# * `gitweb.ctag' multi-valued repo config variable
 +sub git_get_project_ctags {
 +      my $project = shift;
        my $ctags = {};
  
 -      $git_dir = "$projectroot/$path";
 -      unless (opendir D, "$git_dir/ctags") {
 -              return $ctags;
 +      $git_dir = "$projectroot/$project";
 +      if (opendir my $dh, "$git_dir/ctags") {
 +              my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
 +              foreach my $tagfile (@files) {
 +                      open my $ct, '<', $tagfile
 +                              or next;
 +                      my $val = <$ct>;
 +                      chomp $val if $val;
 +                      close $ct;
 +
 +                      (my $ctag = $tagfile) =~ s#.*/##;
 +                      if ($val =~ /^\d+$/) {
 +                              $ctags->{$ctag} = $val;
 +                      } else {
 +                              $ctags->{$ctag} = 1;
 +                      }
 +              }
 +              closedir $dh;
 +
 +      } elsif (open my $fh, '<', "$git_dir/ctags") {
 +              while (my $line = <$fh>) {
 +                      chomp $line;
 +                      $ctags->{$line}++ if $line;
 +              }
 +              close $fh;
 +
 +      } else {
 +              my $taglist = config_to_multi(git_get_project_config('ctag'));
 +              foreach my $tag (@$taglist) {
 +                      $ctags->{$tag}++;
 +              }
        }
 -      foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
 -              open CT, $_ or next;
 -              my $val = <CT>;
 -              chomp $val;
 -              close CT;
 -              my $ctag = $_; $ctag =~ s#.*/##;
 -              $ctags->{$ctag} = $val;
 +
 +      return $ctags;
 +}
 +
 +# return hash, where keys are content tags ('ctags'),
 +# and values are sum of weights of given tag in every project
 +sub git_gather_all_ctags {
 +      my $projects = shift;
 +      my $ctags = {};
 +
 +      foreach my $p (@$projects) {
 +              foreach my $ct (keys %{$p->{'ctags'}}) {
 +                      $ctags->{$ct} += $p->{'ctags'}->{$ct};
 +              }
        }
 -      closedir D;
 -      $ctags;
 +
 +      return $ctags;
  }
  
  sub git_populate_project_tagcloud {
        }
  
        my $cloud;
 +      my $matched = $cgi->param('by_tag');
        if (eval { require HTML::TagCloud; 1; }) {
                $cloud = HTML::TagCloud->new;
 -              foreach (sort keys %ctags_lc) {
 +              foreach my $ctag (sort keys %ctags_lc) {
                        # Pad the title with spaces so that the cloud looks
                        # less crammed.
 -                      my $title = $ctags_lc{$_}->{topname};
 +                      my $title = esc_html($ctags_lc{$ctag}->{topname});
                        $title =~ s/ /&nbsp;/g;
                        $title =~ s/^/&nbsp;/g;
                        $title =~ s/$/&nbsp;/g;
 -                      $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
 +                      if (defined $matched && $matched eq $ctag) {
 +                              $title = qq(<span class="match">$title</span>);
 +                      }
 +                      $cloud->add($title, href(project=>undef, ctag=>$ctag),
 +                                  $ctags_lc{$ctag}->{count});
                }
        } else {
 -              $cloud = \%ctags_lc;
 +              $cloud = {};
 +              foreach my $ctag (keys %ctags_lc) {
 +                      my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
 +                      if (defined $matched && $matched eq $ctag) {
 +                              $title = qq(<span class="match">$title</span>);
 +                      }
 +                      $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
 +                      $cloud->{$ctag}{ctag} =
 +                              $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
 +              }
        }
 -      $cloud;
 +      return $cloud;
  }
  
  sub git_show_project_tagcloud {
        my ($cloud, $count) = @_;
 -      print STDERR ref($cloud)."..\n";
        if (ref $cloud eq 'HTML::TagCloud') {
                return $cloud->html_and_css($count);
        } else {
 -              my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
 -              return '<p align="center">' . join (', ', map {
 -                      "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
 -              } splice(@tags, 0, $count)) . '</p>';
 +              my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
 +              return
 +                      '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
 +                      join (', ', map {
 +                              $cloud->{$_}->{'ctag'}
 +                      } splice(@tags, 0, $count)) .
 +                      '</div>';
        }
  }
  
@@@ -2751,7 -2107,7 +2751,7 @@@ sub git_get_project_url_list 
        my $path = shift;
  
        $git_dir = "$projectroot/$path";
 -      open my $fd, "$git_dir/cloneurl"
 +      open my $fd, '<', "$git_dir/cloneurl"
                or return wantarray ?
                @{ config_to_multi(git_get_project_config('url')) } :
                   config_to_multi(git_get_project_config('url'));
  }
  
  sub git_get_projects_list {
 -      my ($filter) = @_;
 +      my $filter = shift || '';
        my @list;
  
 -      $filter ||= '';
        $filter =~ s/\.git$//;
  
 -      my $check_forks = gitweb_check_feature('forks');
 -
        if (-d $projects_list) {
                # search in directory
 -              my $dir = $projects_list . ($filter ? "/$filter" : '');
 +              my $dir = $projects_list;
                # remove the trailing "/"
                $dir =~ s!/+$!!;
 -              my $pfxlen = length("$dir");
 -              my $pfxdepth = ($dir =~ tr!/!!);
 +              my $pfxlen = length("$projects_list");
 +              my $pfxdepth = ($projects_list =~ tr!/!!);
 +              # when filtering, search only given subdirectory
 +              if ($filter) {
 +                      $dir .= "/$filter";
 +                      $dir =~ s!/+$!!;
 +              }
  
                File::Find::find({
                        follow_fast => 1, # follow symbolic links
                        follow_skip => 2, # ignore duplicates
                        dangling_symlinks => 0, # ignore dangling symlinks, silently
                        wanted => sub {
 +                              # global variables
 +                              our $project_maxdepth;
 +                              our $projectroot;
                                # skip project-list toplevel, if we get it.
                                return if (m!^[/.]$!);
                                # only directories can be git repositories
                                return unless (-d $_);
                                # don't traverse too deep (Find is super slow on os x)
 +                              # $project_maxdepth excludes depth of $projectroot
                                if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
                                        $File::Find::prune = 1;
                                        return;
                                }
  
 -                              my $subdir = substr($File::Find::name, $pfxlen + 1);
 +                              my $path = substr($File::Find::name, $pfxlen + 1);
                                # we check related file in $projectroot
 -                              my $path = ($filter ? "$filter/" : '') . $subdir;
                                if (check_export_ok("$projectroot/$path")) {
                                        push @list, { path => $path };
                                        $File::Find::prune = 1;
                # 'git%2Fgit.git Linus+Torvalds'
                # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
                # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
 -              my %paths;
 -              open my ($fd), $projects_list or return;
 +              open my $fd, '<', $projects_list or return;
        PROJECT:
                while (my $line = <$fd>) {
                        chomp $line;
                        if (!defined $path) {
                                next;
                        }
 -                      if ($filter ne '') {
 -                              # looking for forks;
 -                              my $pfx = substr($path, 0, length($filter));
 -                              if ($pfx ne $filter) {
 -                                      next PROJECT;
 -                              }
 -                              my $sfx = substr($path, length($filter));
 -                              if ($sfx !~ /^\/.*\.git$/) {
 -                                      next PROJECT;
 -                              }
 -                      } elsif ($check_forks) {
 -                      PATH:
 -                              foreach my $filter (keys %paths) {
 -                                      # looking for forks;
 -                                      my $pfx = substr($path, 0, length($filter));
 -                                      if ($pfx ne $filter) {
 -                                              next PATH;
 -                                      }
 -                                      my $sfx = substr($path, length($filter));
 -                                      if ($sfx !~ /^\/.*\.git$/) {
 -                                              next PATH;
 -                                      }
 -                                      # is a fork, don't include it in
 -                                      # the list
 -                                      next PROJECT;
 -                              }
 +                      # if $filter is rpovided, check if $path begins with $filter
 +                      if ($filter && $path !~ m!^\Q$filter\E/!) {
 +                              next;
                        }
                        if (check_export_ok("$projectroot/$path")) {
                                my $pr = {
                                        owner => to_utf8($owner),
                                };
                                push @list, $pr;
 -                              (my $forks_path = $path) =~ s/\.git$//;
 -                              $paths{$forks_path}++;
                        }
                }
                close $fd;
        return @list;
  }
  
 +# written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
 +# as side effects it sets 'forks' field to list of forks for forked projects
 +sub filter_forks_from_projects_list {
 +      my $projects = shift;
 +
 +      my %trie; # prefix tree of directories (path components)
 +      # generate trie out of those directories that might contain forks
 +      foreach my $pr (@$projects) {
 +              my $path = $pr->{'path'};
 +              $path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
 +              next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
 +              next unless ($path);      # skip '.git' repository: tests, git-instaweb
 +              next unless (-d $path);   # containing directory exists
 +              $pr->{'forks'} = [];      # there can be 0 or more forks of project
 +
 +              # add to trie
 +              my @dirs = split('/', $path);
 +              # walk the trie, until either runs out of components or out of trie
 +              my $ref = \%trie;
 +              while (scalar @dirs &&
 +                     exists($ref->{$dirs[0]})) {
 +                      $ref = $ref->{shift @dirs};
 +              }
 +              # create rest of trie structure from rest of components
 +              foreach my $dir (@dirs) {
 +                      $ref = $ref->{$dir} = {};
 +              }
 +              # create end marker, store $pr as a data
 +              $ref->{''} = $pr if (!exists $ref->{''});
 +      }
 +
 +      # filter out forks, by finding shortest prefix match for paths
 +      my @filtered;
 + PROJECT:
 +      foreach my $pr (@$projects) {
 +              # trie lookup
 +              my $ref = \%trie;
 +      DIR:
 +              foreach my $dir (split('/', $pr->{'path'})) {
 +                      if (exists $ref->{''}) {
 +                              # found [shortest] prefix, is a fork - skip it
 +                              push @{$ref->{''}{'forks'}}, $pr;
 +                              next PROJECT;
 +                      }
 +                      if (!exists $ref->{$dir}) {
 +                              # not in trie, cannot have prefix, not a fork
 +                              push @filtered, $pr;
 +                              next PROJECT;
 +                      }
 +                      # If the dir is there, we just walk one step down the trie.
 +                      $ref = $ref->{$dir};
 +              }
 +              # we ran out of trie
 +              # (shouldn't happen: it's either no match, or end marker)
 +              push @filtered, $pr;
 +      }
 +
 +      return @filtered;
 +}
 +
 +# note: fill_project_list_info must be run first,
 +# for 'descr_long' and 'ctags' to be filled
 +sub search_projects_list {
 +      my ($projlist, %opts) = @_;
 +      my $tagfilter  = $opts{'tagfilter'};
 +      my $searchtext = $opts{'searchtext'};
 +
 +      return @$projlist
 +              unless ($tagfilter || $searchtext);
 +
 +      my @projects;
 + PROJECT:
 +      foreach my $pr (@$projlist) {
 +
 +              if ($tagfilter) {
 +                      next unless ref($pr->{'ctags'}) eq 'HASH';
 +                      next unless
 +                              grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
 +              }
 +
 +              if ($searchtext) {
 +                      next unless
 +                              $pr->{'path'} =~ /$searchtext/ ||
 +                              $pr->{'descr_long'} =~ /$searchtext/;
 +              }
 +
 +              push @projects, $pr;
 +      }
 +
 +      return @projects;
 +}
 +
  our $gitweb_project_owner = undef;
  sub git_get_project_list_from_file {
  
        # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
        # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
        if (-f $projects_list) {
 -              open (my $fd , $projects_list);
 +              open(my $fd, '<', $projects_list);
                while (my $line = <$fd>) {
                        chomp $line;
                        my ($pr, $ow) = split ' ', $line;
@@@ -3000,44 -2285,6 +3000,44 @@@ sub git_get_last_activity 
        return (undef, undef);
  }
  
 +# Implementation note: when a single remote is wanted, we cannot use 'git
 +# remote show -n' because that command always work (assuming it's a remote URL
 +# if it's not defined), and we cannot use 'git remote show' because that would
 +# try to make a network roundtrip. So the only way to find if that particular
 +# remote is defined is to walk the list provided by 'git remote -v' and stop if
 +# and when we find what we want.
 +sub git_get_remotes_list {
 +      my $wanted = shift;
 +      my %remotes = ();
 +
 +      open my $fd, '-|' , git_cmd(), 'remote', '-v';
 +      return unless $fd;
 +      while (my $remote = <$fd>) {
 +              chomp $remote;
 +              $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
 +              next if $wanted and not $remote eq $wanted;
 +              my ($url, $key) = ($1, $2);
 +
 +              $remotes{$remote} ||= { 'heads' => () };
 +              $remotes{$remote}{$key} = $url;
 +      }
 +      close $fd or return;
 +      return wantarray ? %remotes : \%remotes;
 +}
 +
 +# Takes a hash of remotes as first parameter and fills it by adding the
 +# available remote heads for each of the indicated remotes.
 +sub fill_remote_heads {
 +      my $remotes = shift;
 +      my @heads = map { "remotes/$_" } keys %$remotes;
 +      my @remoteheads = git_get_heads_list(undef, @heads);
 +      foreach my $remote (keys %$remotes) {
 +              $remotes->{$remote}{'heads'} = [ grep {
 +                      $_->{'name'} =~ s!^$remote/!!
 +                      } @remoteheads ];
 +      }
 +}
 +
  sub git_get_references {
        my $type = shift || "";
        my %refs;
@@@ -3100,10 -2347,8 +3100,10 @@@ sub parse_date 
        $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
                             1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
  
 -      $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
 -      my $local = $epoch + ((int $1 + ($2/60)) * 3600);
 +      my ($tz_sign, $tz_hour, $tz_min) =
 +              ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
 +      $tz_sign = ($tz_sign eq '-' ? -1 : +1);
 +      my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
        ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
        $date{'hour_local'} = $hour;
        $date{'minute_local'} = $min;
@@@ -3131,14 -2376,8 +3131,14 @@@ sub parse_tag 
                        $tag{'name'} = $1;
                } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
                        $tag{'author'} = $1;
 -                      $tag{'epoch'} = $2;
 -                      $tag{'tz'} = $3;
 +                      $tag{'author_epoch'} = $2;
 +                      $tag{'author_tz'} = $3;
 +                      if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
 +                              $tag{'author_name'}  = $1;
 +                              $tag{'author_email'} = $2;
 +                      } else {
 +                              $tag{'author_name'} = $tag{'author'};
 +                      }
                } elsif ($line =~ m/--BEGIN/) {
                        push @comment, $line;
                        last;
@@@ -3178,7 -2417,7 +3178,7 @@@ sub parse_commit_text 
                } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
                        push @parents, $1;
                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
 -                      $co{'author'} = $1;
 +                      $co{'author'} = to_utf8($1);
                        $co{'author_epoch'} = $2;
                        $co{'author_tz'} = $3;
                        if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
                                $co{'author_name'} = $co{'author'};
                        }
                } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
 -                      $co{'committer'} = $1;
 +                      $co{'committer'} = to_utf8($1);
                        $co{'committer_epoch'} = $2;
                        $co{'committer_tz'} = $3;
 -                      $co{'committer_name'} = $co{'committer'};
                        if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
                                $co{'committer_name'}  = $1;
                                $co{'committer_email'} = $2;
@@@ -3353,36 -2593,21 +3353,36 @@@ sub parsed_difftree_line 
  }
  
  # parse line of git-ls-tree output
 -sub parse_ls_tree_line ($;%) {
 +sub parse_ls_tree_line {
        my $line = shift;
        my %opts = @_;
        my %res;
  
 -      #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 -      $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
 +      if ($opts{'-l'}) {
 +              #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717  panic.c'
 +              $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
  
 -      $res{'mode'} = $1;
 -      $res{'type'} = $2;
 -      $res{'hash'} = $3;
 -      if ($opts{'-z'}) {
 -              $res{'name'} = $4;
 +              $res{'mode'} = $1;
 +              $res{'type'} = $2;
 +              $res{'hash'} = $3;
 +              $res{'size'} = $4;
 +              if ($opts{'-z'}) {
 +                      $res{'name'} = $5;
 +              } else {
 +                      $res{'name'} = unquote($5);
 +              }
        } else {
 -              $res{'name'} = unquote($4);
 +              #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
 +              $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
 +
 +              $res{'mode'} = $1;
 +              $res{'type'} = $2;
 +              $res{'hash'} = $3;
 +              if ($opts{'-z'}) {
 +                      $res{'name'} = $4;
 +              } else {
 +                      $res{'name'} = unquote($4);
 +              }
        }
  
        return wantarray ? %res : \%res;
@@@ -3438,15 -2663,13 +3438,15 @@@ sub parse_from_to_diffinfo 
  ## parse to array of hashes functions
  
  sub git_get_heads_list {
 -      my $limit = shift;
 +      my ($limit, @classes) = @_;
 +      @classes = ('heads') unless @classes;
 +      my @patterns = map { "refs/$_" } @classes;
        my @headslist;
  
        open my $fd, '-|', git_cmd(), 'for-each-ref',
                ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
                '--format=%(objectname) %(refname) %(subject)%00%(committer)',
 -              'refs/heads'
 +              @patterns
                or return;
        while (my $line = <$fd>) {
                my %ref_item;
                my ($committer, $epoch, $tz) =
                        ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
                $ref_item{'fullname'}  = $name;
 -              $name =~ s!^refs/heads/!!;
 +              $name =~ s!^refs/(?:head|remote)s/!!;
  
                $ref_item{'name'}  = $name;
                $ref_item{'id'}    = $hash;
@@@ -3559,15 -2782,18 +3559,15 @@@ sub mimetype_guess_file 
        -r $mimemap or return undef;
  
        my %mimemap;
 -      open(MIME, $mimemap) or return undef;
 -      while (<MIME>) {
 +      open(my $mh, '<', $mimemap) or return undef;
 +      while (<$mh>) {
                next if m/^#/; # skip comments
 -              my ($mime, $exts) = split(/\t+/);
 -              if (defined $exts) {
 -                      my @exts = split(/\s+/, $exts);
 -                      foreach my $ext (@exts) {
 -                              $mimemap{$ext} = $mime;
 -                      }
 +              my ($mimetype, @exts) = split(/\s+/);
 +              foreach my $ext (@exts) {
 +                      $mimemap{$ext} = $mimetype;
                }
        }
 -      close(MIME);
 +      close($mh);
  
        $filename =~ /\.([^.]*)$/;
        return $mimemap{$1};
@@@ -3628,59 -2854,27 +3628,59 @@@ sub blob_contenttype 
        return $type;
  }
  
 +# guess file syntax for syntax highlighting; return undef if no highlighting
 +# the name of syntax can (in the future) depend on syntax highlighter used
 +sub guess_file_syntax {
 +      my ($highlight, $mimetype, $file_name) = @_;
 +      return undef unless ($highlight && defined $file_name);
 +      my $basename = basename($file_name, '.in');
 +      return $highlight_basename{$basename}
 +              if exists $highlight_basename{$basename};
 +
 +      $basename =~ /\.([^.]*)$/;
 +      my $ext = $1 or return undef;
 +      return $highlight_ext{$ext}
 +              if exists $highlight_ext{$ext};
 +
 +      return undef;
 +}
 +
 +# run highlighter and return FD of its output,
 +# or return original FD if no highlighting
 +sub run_highlighter {
 +      my ($fd, $highlight, $syntax) = @_;
 +      return $fd unless ($highlight && defined $syntax);
 +
 +      close $fd;
 +      open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
 +                quote_command($highlight_bin).
 +                " --replace-tabs=8 --fragment --syntax $syntax |"
 +              or die_error(500, "Couldn't open file or run syntax highlighter");
 +      return $fd;
 +}
 +
  ## ======================================================================
  ## functions printing HTML: header, footer, error page
  
 -sub git_header_html {
 -      my $status = shift || "200 OK";
 -      my $expires = shift;
 +sub get_page_title {
 +      my $title = to_utf8($site_name);
  
 -      my $title = "$site_name";
 -      if (defined $project) {
 -              $title .= " - " . to_utf8($project);
 -              if (defined $action) {
 -                      $title .= "/$action";
 -                      if (defined $file_name) {
 -                              $title .= " - " . esc_path($file_name);
 -                              if ($action eq "tree" && $file_name !~ m|/$|) {
 -                                      $title .= "/";
 -                              }
 -                      }
 -              }
 +      return $title unless (defined $project);
 +      $title .= " - " . to_utf8($project);
 +
 +      return $title unless (defined $action);
 +      $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
 +
 +      return $title unless (defined $file_name);
 +      $title .= " - " . esc_path($file_name);
 +      if ($action eq "tree" && $file_name !~ m|/$|) {
 +              $title .= "/";
        }
 -      my $content_type;
 +
 +      return $title;
 +}
 +
 +sub get_content_type_html {
        # require explicit support from the UA if we are to send the page as
        # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
        # we have to do this because MSIE sometimes globs '*/*', pretending to
        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);
 -      my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
 -      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">
 -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
 -<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
 -<!-- git core binaries version $git_version -->
 -<head>
 -<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
 -<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
 -<meta name="robots" content="index, nofollow"/>
 -<title>$title</title>
 -EOF
 -# print out each stylesheet that exist
 -      if (defined $stylesheet) {
 -#provides backwards capability for those people who define style sheet in a config file
 -              print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
 +              return 'application/xhtml+xml';
        } else {
 -              foreach my $stylesheet (@stylesheets) {
 -                      next unless $stylesheet;
 -                      print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
 -              }
 +              return 'text/html';
        }
 +}
 +
 +sub print_feed_meta {
        if (defined $project) {
                my %href_params = get_feed_info();
                if (!exists $href_params{'-title'}) {
                        $href_params{'-title'} = 'log';
                }
  
 -              foreach my $format qw(RSS Atom) {
 +              foreach my $format (qw(RSS Atom)) {
                        my $type = lc($format);
                        my %link_attr = (
                                '-rel' => 'alternate',
 -                              '-title' => "$project - $href_params{'-title'} - $format feed",
 +                              '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
                                '-type' => "application/$type+xml"
                        );
  
        } else {
                printf('<link rel="alternate" title="%s projects list" '.
                       'href="%s" type="text/plain; charset=utf-8" />'."\n",
 -                     $site_name, href(project=>undef, action=>"project_index"));
 +                     esc_attr($site_name), href(project=>undef, action=>"project_index"));
                printf('<link rel="alternate" title="%s projects feeds" '.
                       'href="%s" type="text/x-opml" />'."\n",
 -                     $site_name, href(project=>undef, action=>"opml"));
 -      }
 -      if (defined $favicon) {
 -              print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
 +                     esc_attr($site_name), href(project=>undef, action=>"opml"));
        }
 +}
  
 -      print "</head>\n" .
 -            "<body>\n";
 +sub print_header_links {
 +      my $status = shift;
  
 -      if (-f $site_header) {
 -              insert_file($site_header);
 +      # print out each stylesheet that exist, providing backwards capability
 +      # for those people who defined $stylesheet in a config file
 +      if (defined $stylesheet) {
 +              print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
 +      } else {
 +              foreach my $stylesheet (@stylesheets) {
 +                      next unless $stylesheet;
 +                      print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
 +              }
 +      }
 +      print_feed_meta()
 +              if ($status eq '200 OK');
 +      if (defined $favicon) {
 +              print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
        }
 +}
 +
 +sub print_nav_breadcrumbs {
 +      my %opts = @_;
  
 -      print "<div class=\"page_header\">\n" .
 -            $cgi->a({-href => esc_url($logo_url),
 -                     -title => $logo_label},
 -                    qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
        print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
        if (defined $project) {
                print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
                if (defined $action) {
 -                      print " / $action";
 +                      my $action_print = $action ;
 +                      if (defined $opts{-action_extra}) {
 +                              $action_print = $cgi->a({-href => href(action=>$action)},
 +                                      $action);
 +                      }
 +                      print " / $action_print";
 +              }
 +              if (defined $opts{-action_extra}) {
 +                      print " / $opts{-action_extra}";
                }
                print "\n";
        }
 +}
 +
 +sub print_search_form {
 +      if (!defined $searchtext) {
 +              $searchtext = "";
 +      }
 +      my $search_hash;
 +      if (defined $hash_base) {
 +              $search_hash = $hash_base;
 +      } elsif (defined $hash) {
 +              $search_hash = $hash;
 +      } else {
 +              $search_hash = "HEAD";
 +      }
 +      my $action = $my_uri;
 +      my $use_pathinfo = gitweb_check_feature('pathinfo');
 +      if ($use_pathinfo) {
 +              $action .= "/".esc_url($project);
 +      }
 +      print $cgi->startform(-method => "get", -action => $action) .
 +            "<div class=\"search\">\n" .
 +            (!$use_pathinfo &&
 +            $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
 +            $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
 +            $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
 +            $cgi->popup_menu(-name => 'st', -default => 'commit',
 +                             -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
 +            $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
 +            " search:\n",
 +            $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
 +            "<span title=\"Extended regular expression\">" .
 +            $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
 +                           -checked => $search_use_regexp) .
 +            "</span>" .
 +            "</div>" .
 +            $cgi->end_form() . "\n";
 +}
 +
 +sub git_header_html {
 +      my $status = shift || "200 OK";
 +      my $expires = shift;
 +      my %opts = @_;
 +
 +      my $title = get_page_title();
 +      my $content_type = get_content_type_html();
 +      print $cgi->header(-type=>$content_type, -charset => 'utf-8',
 +                         -status=> $status, -expires => $expires)
 +              unless ($opts{'-no_http_header'});
 +      my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
 +      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">
 +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
 +<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
 +<!-- git core binaries version $git_version -->
 +<head>
 +<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
 +<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
 +<meta name="robots" content="index, nofollow"/>
 +<title>$title</title>
 +EOF
 +      # the stylesheet, favicon etc urls won't work correctly with path_info
 +      # unless we set the appropriate base URL
 +      if ($ENV{'PATH_INFO'}) {
 +              print "<base href=\"".esc_url($base_url)."\" />\n";
 +      }
 +      print_header_links($status);
 +      print "</head>\n" .
 +            "<body>\n";
 +
 +      if (defined $site_header && -f $site_header) {
 +              insert_file($site_header);
 +      }
 +
 +      print "<div class=\"page_header\">\n";
 +      if (defined $logo) {
 +              print $cgi->a({-href => esc_url($logo_url),
 +                             -title => $logo_label},
 +                            $cgi->img({-src => esc_url($logo),
 +                                       -width => 72, -height => 27,
 +                                       -alt => "git",
 +                                       -class => "logo"}));
 +      }
 +      print_nav_breadcrumbs(%opts);
        print "</div>\n";
  
        my $have_search = gitweb_check_feature('search');
        if (defined $project && $have_search) {
 -              if (!defined $searchtext) {
 -                      $searchtext = "";
 -              }
 -              my $search_hash;
 -              if (defined $hash_base) {
 -                      $search_hash = $hash_base;
 -              } elsif (defined $hash) {
 -                      $search_hash = $hash;
 -              } else {
 -                      $search_hash = "HEAD";
 -              }
 -              my $action = $my_uri;
 -              my $use_pathinfo = gitweb_check_feature('pathinfo');
 -              if ($use_pathinfo) {
 -                      $action .= "/".esc_url($project);
 -              }
 -              print $cgi->startform(-method => "get", -action => $action) .
 -                    "<div class=\"search\">\n" .
 -                    (!$use_pathinfo &&
 -                    $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
 -                    $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
 -                    $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
 -                    $cgi->popup_menu(-name => 'st', -default => 'commit',
 -                                     -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
 -                    $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
 -                    " search:\n",
 -                    $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
 -                    "<span title=\"Extended regular expression\">" .
 -                    $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
 -                                   -checked => $search_use_regexp) .
 -                    "</span>" .
 -                    "</div>" .
 -                    $cgi->end_form() . "\n";
 +              print_search_form();
        }
  }
  
@@@ -3886,7 -3032,7 +3886,7 @@@ sub git_footer_html 
                }
                $href_params{'-title'} ||= 'log';
  
 -              foreach my $format qw(RSS Atom) {
 +              foreach my $format (qw(RSS Atom)) {
                        $href_params{'action'} = lc($format);
                        print $cgi->a({-href => href(%href_params),
                                      -title => "$href_params{'-title'} $format feed",
        }
        print "</div>\n"; # class="page_footer"
  
 -      if (-f $site_footer) {
 +      if (defined $t0 && gitweb_check_feature('timed')) {
 +              print "<div id=\"generating_info\">\n";
 +              print 'This page took '.
 +                    '<span id="generating_time" class="time_span">'.
 +                    tv_interval($t0, [ gettimeofday() ]).
 +                    ' seconds </span>'.
 +                    ' and '.
 +                    '<span id="generating_cmd">'.
 +                    $number_of_git_cmds.
 +                    '</span> git commands '.
 +                    " to generate.\n";
 +              print "</div>\n"; # class="page_footer"
 +      }
 +
 +      if (defined $site_footer && -f $site_footer) {
                insert_file($site_footer);
        }
  
 +      print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
 +      if (defined $action &&
 +          $action eq 'blame_incremental') {
 +              print qq!<script type="text/javascript">\n!.
 +                    qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
 +                    qq!           "!. href() .qq!");\n!.
 +                    qq!</script>\n!;
 +      } else {
 +              my ($jstimezone, $tz_cookie, $datetime_class) =
 +                      gitweb_get_feature('javascript-timezone');
 +
 +              print qq!<script type="text/javascript">\n!.
 +                    qq!window.onload = function () {\n!;
 +              if (gitweb_check_feature('javascript-actions')) {
 +                      print qq!       fixLinks();\n!;
 +              }
 +              if ($jstimezone && $tz_cookie && $datetime_class) {
 +                      print qq!       var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
 +                            qq!       onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
 +              }
 +              print qq!};\n!.
 +                    qq!</script>\n!;
 +      }
 +
        print "</body>\n" .
              "</html>";
  }
  
 -# die_error(<http_status_code>, <error_message>)
 +# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
  # Example: die_error(404, 'Hash not found')
  # By convention, use the following status codes (as defined in RFC 2616):
  # 400: Invalid or missing CGI parameters, or
  # 500: The server isn't configured properly, or
  #      an internal error occurred (e.g. failed assertions caused by bugs), or
  #      an unknown error occurred (e.g. the git binary died unexpectedly).
 +# 503: The server is currently unavailable (because it is overloaded,
 +#      or down for maintenance).  Generally, this is a temporary state.
  sub die_error {
        my $status = shift || 500;
 -      my $error = shift || "Internal server error";
 +      my $error = esc_html(shift) || "Internal Server Error";
 +      my $extra = shift;
 +      my %opts = @_;
  
 -      my %http_responses = (400 => '400 Bad Request',
 -                            403 => '403 Forbidden',
 -                            404 => '404 Not Found',
 -                            500 => '500 Internal Server Error');
 -      git_header_html($http_responses{$status});
 +      my %http_responses = (
 +              400 => '400 Bad Request',
 +              403 => '403 Forbidden',
 +              404 => '404 Not Found',
 +              500 => '500 Internal Server Error',
 +              503 => '503 Service Unavailable',
 +      );
 +      git_header_html($http_responses{$status}, undef, %opts);
        print <<EOF;
  <div class="page_body">
  <br /><br />
  $status - $error
  <br />
 -</div>
  EOF
 +      if (defined $extra) {
 +              print "<hr />\n" .
 +                    "$extra\n";
 +      }
 +      print "</div>\n";
 +
        git_footer_html();
 -      exit;
 +      goto DONE_GITWEB
 +              unless ($opts{'-error_handler'});
  }
  
  ## ----------------------------------------------------------------------
@@@ -4044,32 -3139,23 +4044,32 @@@ sub git_print_page_nav 
              "</div>\n";
  }
  
 +# returns a submenu for the nagivation of the refs views (tags, heads,
 +# remotes) with the current view disabled and the remotes view only
 +# available if the feature is enabled
 +sub format_ref_views {
 +      my ($current) = @_;
 +      my @ref_views = qw{tags heads};
 +      push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
 +      return join " | ", map {
 +              $_ eq $current ? $_ :
 +              $cgi->a({-href => href(action=>$_)}, $_)
 +      } @ref_views
 +}
 +
  sub format_paging_nav {
 -      my ($action, $hash, $head, $page, $has_next_link) = @_;
 +      my ($action, $page, $has_next_link) = @_;
        my $paging_nav;
  
  
 -      if ($hash ne $head || $page) {
 -              $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
 -      } else {
 -              $paging_nav .= "HEAD";
 -      }
 -
        if ($page > 0) {
 -              $paging_nav .= " &sdot; " .
 +              $paging_nav .=
 +                      $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
 +                      " &sdot; " .
                        $cgi->a({-href => href(-replay=>1, page=>$page-1),
                                 -accesskey => "p", -title => "Alt-p"}, "prev");
        } else {
 -              $paging_nav .= " &sdot; prev";
 +              $paging_nav .= "first &sdot; prev";
        }
  
        if ($has_next_link) {
@@@ -4100,111 -3186,22 +4100,111 @@@ sub git_print_header_div 
              "\n</div>\n";
  }
  
 -#sub git_print_authorship (\%) {
 +sub format_repo_url {
 +      my ($name, $url) = @_;
 +      return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
 +}
 +
 +# Group output by placing it in a DIV element and adding a header.
 +# Options for start_div() can be provided by passing a hash reference as the
 +# first parameter to the function.
 +# Options to git_print_header_div() can be provided by passing an array
 +# reference. This must follow the options to start_div if they are present.
 +# The content can be a scalar, which is output as-is, a scalar reference, which
 +# is output after html escaping, an IO handle passed either as *handle or
 +# *handle{IO}, or a function reference. In the latter case all following
 +# parameters will be taken as argument to the content function call.
 +sub git_print_section {
 +      my ($div_args, $header_args, $content);
 +      my $arg = shift;
 +      if (ref($arg) eq 'HASH') {
 +              $div_args = $arg;
 +              $arg = shift;
 +      }
 +      if (ref($arg) eq 'ARRAY') {
 +              $header_args = $arg;
 +              $arg = shift;
 +      }
 +      $content = $arg;
 +
 +      print $cgi->start_div($div_args);
 +      git_print_header_div(@$header_args);
 +
 +      if (ref($content) eq 'CODE') {
 +              $content->(@_);
 +      } elsif (ref($content) eq 'SCALAR') {
 +              print esc_html($$content);
 +      } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
 +              print <$content>;
 +      } elsif (!ref($content) && defined($content)) {
 +              print $content;
 +      }
 +
 +      print $cgi->end_div;
 +}
 +
 +sub format_timestamp_html {
 +      my $date = shift;
 +      my $strtime = $date->{'rfc2822'};
 +
 +      my (undef, undef, $datetime_class) =
 +              gitweb_get_feature('javascript-timezone');
 +      if ($datetime_class) {
 +              $strtime = qq!<span class="$datetime_class">$strtime</span>!;
 +      }
 +
 +      my $localtime_format = '(%02d:%02d %s)';
 +      if ($date->{'hour_local'} < 6) {
 +              $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
 +      }
 +      $strtime .= ' ' .
 +                  sprintf($localtime_format,
 +                          $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
 +
 +      return $strtime;
 +}
 +
 +# Outputs the author name and date in long form
  sub git_print_authorship {
        my $co = shift;
 +      my %opts = @_;
 +      my $tag = $opts{-tag} || 'div';
 +      my $author = $co->{'author_name'};
  
        my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
 -      print "<div class=\"author_date\">" .
 -            esc_html($co->{'author_name'}) .
 -            " [$ad{'rfc2822'}";
 -      if ($ad{'hour_local'} < 6) {
 -              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'});
 +      print "<$tag class=\"author_date\">" .
 +            format_search_author($author, "author", esc_html($author)) .
 +            " [".format_timestamp_html(\%ad)."]".
 +            git_get_avatar($co->{'author_email'}, -pad_before => 1) .
 +            "</$tag>\n";
 +}
 +
 +# Outputs table rows containing the full author or committer information,
 +# in the format expected for 'commit' view (& similar).
 +# Parameters are a commit hash reference, followed by the list of people
 +# to output information for. If the list is empty it defaults to both
 +# author and committer.
 +sub git_print_authorship_rows {
 +      my $co = shift;
 +      # too bad we can't use @people = @_ || ('author', 'committer')
 +      my @people = @_;
 +      @people = ('author', 'committer') unless @people;
 +      foreach my $who (@people) {
 +              my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
 +              print "<tr><td>$who</td><td>" .
 +                    format_search_author($co->{"${who}_name"}, $who,
 +                                         esc_html($co->{"${who}_name"})) . " " .
 +                    format_search_author($co->{"${who}_email"}, $who,
 +                                         esc_html("<" . $co->{"${who}_email"} . ">")) .
 +                    "</td><td rowspan=\"2\">" .
 +                    git_get_avatar($co->{"${who}_email"}, -size => 'double') .
 +                    "</td></tr>\n" .
 +                    "<tr>" .
 +                    "<td></td><td>" .
 +                    format_timestamp_html(\%wd) .
 +                    "</td>" .
 +                    "</tr>\n";
        }
 -      print "]</div>\n";
  }
  
  sub git_print_page_path {
        print "<br/></div>\n";
  }
  
 -# sub git_print_log (\@;%) {
 -sub git_print_log ($;%) {
 +sub git_print_log {
        my $log = shift;
        my %opts = @_;
  
@@@ -4303,7 -3301,7 +4303,7 @@@ sub git_get_link_target 
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or return;
        {
 -              local $/;
 +              local $/ = undef;
                $link_target = <$fd>;
        }
        close $fd
  # return target of link relative to top directory (top tree);
  # return undef if it is not possible (including absolute links).
  sub normalize_link_target {
 -      my ($link_target, $basedir, $hash_base) = @_;
 -
 -      # we can normalize symlink target only if $hash_base is provided
 -      return unless $hash_base;
 +      my ($link_target, $basedir) = @_;
  
        # absolute symlinks (beginning with '/') cannot be normalized
        return if (substr($link_target, 0, 1) eq '/');
@@@ -4364,9 -3365,6 +4364,9 @@@ sub git_print_tree_entry 
        # and link is the action links of the entry.
  
        print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
 +      if (exists $t->{'size'}) {
 +              print "<td class=\"size\">$t->{'size'}</td>\n";
 +      }
        if ($t->{'type'} eq "blob") {
                print "<td class=\"list\">" .
                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
                if (S_ISLNK(oct $t->{'mode'})) {
                        my $link_target = git_get_link_target($t->{'hash'});
                        if ($link_target) {
 -                              my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
 +                              my $norm_target = normalize_link_target($link_target, $basedir);
                                if (defined $norm_target) {
                                        print " -> " .
                                              $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
        } elsif ($t->{'type'} eq "tree") {
                print "<td class=\"list\">";
                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
 -                                           file_name=>"$basedir$t->{'name'}", %base_key)},
 +                                           file_name=>"$basedir$t->{'name'}",
 +                                           %base_key)},
                              esc_path($t->{'name'}));
                print "</td>\n";
                print "<td class=\"link\">";
                print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
 -                                           file_name=>"$basedir$t->{'name'}", %base_key)},
 +                                           file_name=>"$basedir$t->{'name'}",
 +                                           %base_key)},
                              "tree");
                if (defined $hash_base) {
                        print " | " .
@@@ -4552,8 -3548,7 +4552,8 @@@ sub git_difftree_body 
                                # link to patch
                                $patchno++;
                                print "<td class=\"link\">" .
 -                                    $cgi->a({-href => "#patch$patchno"}, "patch") .
 +                                    $cgi->a({-href => href(-anchor=>"patch$patchno")},
 +                                            "patch") .
                                      " | " .
                                      "</td>\n";
                        }
                }
                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
 +                      if (S_ISREG($from_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 ($action eq 'commitdiff') {
                                # link to patch
                                $patchno++;
 -                              print $cgi->a({-href => "#patch$patchno"}, "patch");
 -                              print " | ";
 +                              print $cgi->a({-href => href(-anchor=>"patch$patchno")},
 +                                            "patch") .
 +                                    " | ";
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff->{'file'})},
                        if ($action eq 'commitdiff') {
                                # link to patch
                                $patchno++;
 -                              print $cgi->a({-href => "#patch$patchno"}, "patch");
 -                              print " | ";
 +                              print $cgi->a({-href => href(-anchor=>"patch$patchno")},
 +                                            "patch") .
 +                                    " | ";
                        }
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
                                                     hash_base=>$parent, file_name=>$diff->{'file'})},
                        if ($action eq 'commitdiff') {
                                # link to patch
                                $patchno++;
 -                              print $cgi->a({-href => "#patch$patchno"}, "patch") .
 +                              print $cgi->a({-href => href(-anchor=>"patch$patchno")},
 +                                            "patch") .
                                      " | ";
                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
                                # "commit" view and modified file (not onlu mode changed)
                        if ($action eq 'commitdiff') {
                                # link to patch
                                $patchno++;
 -                              print $cgi->a({-href => "#patch$patchno"}, "patch") .
 +                              print $cgi->a({-href => href(-anchor=>"patch$patchno")},
 +                                            "patch") .
                                      " | ";
                        } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
                                # "commit" view and modified file (not only pure rename or copy)
@@@ -4916,8 -3907,8 +4916,8 @@@ sub git_patchset_body 
                print "</div>\n"; # class="patch"
        }
  
 -      # for compact combined (--cc) format, with chunk and patch simpliciaction
 -      # patchset might be empty, but there might be unprocessed raw lines
 +      # for compact combined (--cc) format, with chunk and patch simplification
 +      # the patchset might be empty, but there might be unprocessed raw lines
        for (++$patch_idx if $patch_number > 0;
             $patch_idx < @$difftree;
             ++$patch_idx) {
  
  # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
  
 -# fills project list info (age, description, owner, forks) for each
 -# project in the list, removing invalid projects from returned list
 +# fills project list info (age, description, owner, category, forks)
 +# for each project in the list, removing invalid projects from
 +# returned list
  # NOTE: modifies $projlist, but does not remove entries from it
  sub fill_project_list_info {
 -      my ($projlist, $check_forks) = @_;
 +      my $projlist = shift;
        my @projects;
  
        my $show_ctags = gitweb_check_feature('ctags');
                if (!defined $pr->{'owner'}) {
                        $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
                }
 -              if ($check_forks) {
 -                      my $pname = $pr->{'path'};
 -                      if (($pname =~ s/\.git$//) &&
 -                          ($pname !~ /\/$/) &&
 -                          (-d "$projectroot/$pname")) {
 -                              $pr->{'forks'} = "-d $projectroot/$pname";
 -                      }       else {
 -                              $pr->{'forks'} = 0;
 -                      }
 +              if ($show_ctags) {
 +                      $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
                }
 -              $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
 +              if ($projects_list_group_categories && !defined $pr->{'category'}) {
 +                      my $cat = git_get_project_category($pr->{'path'}) ||
 +                                                         $project_list_default_category;
 +                      $pr->{'category'} = to_utf8($cat);
 +              }
 +
                push @projects, $pr;
        }
  
        return @projects;
  }
  
 +sub sort_projects_list {
 +      my ($projlist, $order) = @_;
 +      my @projects;
 +
 +      my %order_info = (
 +              project => { key => 'path', type => 'str' },
 +              descr => { key => 'descr_long', type => 'str' },
 +              owner => { key => 'owner', type => 'str' },
 +              age => { key => 'age', type => 'num' }
 +      );
 +      my $oi = $order_info{$order};
 +      return @$projlist unless defined $oi;
 +      if ($oi->{'type'} eq 'str') {
 +              @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
 +      } else {
 +              @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
 +      }
 +
 +      return @projects;
 +}
 +
 +# returns a hash of categories, containing the list of project
 +# belonging to each category
 +sub build_projlist_by_category {
 +      my ($projlist, $from, $to) = @_;
 +      my %categories;
 +
 +      $from = 0 unless defined $from;
 +      $to = $#$projlist if (!defined $to || $#$projlist < $to);
 +
 +      for (my $i = $from; $i <= $to; $i++) {
 +              my $pr = $projlist->[$i];
 +              push @{$categories{ $pr->{'category'} }}, $pr;
 +      }
 +
 +      return wantarray ? %categories : \%categories;
 +}
 +
  # print 'sort by' <th> element, generating 'sort by $name' replay link
  # if that order is not selected
  sub print_sort_th {
 +      print format_sort_th(@_);
 +}
 +
 +sub format_sort_th {
        my ($name, $order, $header) = @_;
 +      my $sort_th = "";
        $header ||= ucfirst($name);
  
        if ($order eq $name) {
 -              print "<th>$header</th>\n";
 +              $sort_th .= "<th>$header</th>\n";
        } else {
 -              print "<th>" .
 -                    $cgi->a({-href => href(-replay=>1, order=>$name),
 -                             -class => "header"}, $header) .
 -                    "</th>\n";
 +              $sort_th .= "<th>" .
 +                          $cgi->a({-href => href(-replay=>1, order=>$name),
 +                                   -class => "header"}, $header) .
 +                          "</th>\n";
        }
 -}
  
 -sub git_project_list_body {
 -      # actually uses global variable $project
 -      my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
 +      return $sort_th;
 +}
  
 -      my $check_forks = gitweb_check_feature('forks');
 -      my @projects = fill_project_list_info($projlist, $check_forks);
 +sub git_project_list_rows {
 +      my ($projlist, $from, $to, $check_forks) = @_;
  
 -      $order ||= $default_projects_order;
        $from = 0 unless defined $from;
 -      $to = $#projects if (!defined $to || $#projects < $to);
 +      $to = $#$projlist if (!defined $to || $#$projlist < $to);
  
 -      my %order_info = (
 -              project => { key => 'path', type => 'str' },
 -              descr => { key => 'descr_long', type => 'str' },
 -              owner => { key => 'owner', type => 'str' },
 -              age => { key => 'age', type => 'num' }
 -      );
 -      my $oi = $order_info{$order};
 -      if ($oi->{'type'} eq 'str') {
 -              @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
 -      } else {
 -              @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
 -      }
 -
 -      my $show_ctags = gitweb_check_feature('ctags');
 -      if ($show_ctags) {
 -              my %ctags;
 -              foreach my $p (@projects) {
 -                      foreach my $ct (keys %{$p->{'ctags'}}) {
 -                              $ctags{$ct} += $p->{'ctags'}->{$ct};
 -                      }
 -              }
 -              my $cloud = git_populate_project_tagcloud(\%ctags);
 -              print git_show_project_tagcloud($cloud, 64);
 -      }
 -
 -      print "<table class=\"project_list\">\n";
 -      unless ($no_header) {
 -              print "<tr>\n";
 -              if ($check_forks) {
 -                      print "<th></th>\n";
 -              }
 -              print_sort_th('project', $order, 'Project');
 -              print_sort_th('descr', $order, 'Description');
 -              print_sort_th('owner', $order, 'Owner');
 -              print_sort_th('age', $order, 'Last Change');
 -              print "<th></th>\n" . # for links
 -                    "</tr>\n";
 -      }
 -      my $alternate = 1;
 -      my $tagfilter = $cgi->param('by_tag');
 -      for (my $i = $from; $i <= $to; $i++) {
 -              my $pr = $projects[$i];
 -
 -              next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
 -              next if $searchtext and not $pr->{'path'} =~ /$searchtext/
 -                      and not $pr->{'descr_long'} =~ /$searchtext/;
 -              # Weed out forks or non-matching entries of search
 -              if ($check_forks) {
 -                      my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
 -                      $forkbase="^$forkbase" if $forkbase;
 -                      next if not $searchtext and not $tagfilter and $show_ctags
 -                              and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
 -              }
 +      my $alternate = 1;
 +      for (my $i = $from; $i <= $to; $i++) {
 +              my $pr = $projlist->[$i];
  
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
 +
                if ($check_forks) {
                        print "<td>";
                        if ($pr->{'forks'}) {
 -                              print "<!-- $pr->{'forks'} -->\n";
 -                              print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
 +                              my $nforks = scalar @{$pr->{'forks'}};
 +                              if ($nforks > 0) {
 +                                      print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
 +                                                     -title => "$nforks forks"}, "+");
 +                              } else {
 +                                      print $cgi->span({-title => "$nforks forks"}, "+");
 +                              }
                        }
                        print "</td>\n";
                }
                      "</td>\n" .
                      "</tr>\n";
        }
 +}
 +
 +sub git_project_list_body {
 +      # actually uses global variable $project
 +      my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
 +      my @projects = @$projlist;
 +
 +      my $check_forks = gitweb_check_feature('forks');
 +      my $show_ctags  = gitweb_check_feature('ctags');
 +      my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
 +      $check_forks = undef
 +              if ($tagfilter || $searchtext);
 +
 +      # filtering out forks before filling info allows to do less work
 +      @projects = filter_forks_from_projects_list(\@projects)
 +              if ($check_forks);
 +      @projects = fill_project_list_info(\@projects);
 +      # searching projects require filling to be run before it
 +      @projects = search_projects_list(\@projects,
 +                                       'searchtext' => $searchtext,
 +                                       'tagfilter'  => $tagfilter)
 +              if ($tagfilter || $searchtext);
 +
 +      $order ||= $default_projects_order;
 +      $from = 0 unless defined $from;
 +      $to = $#projects if (!defined $to || $#projects < $to);
 +
 +      # short circuit
 +      if ($from > $to) {
 +              print "<center>\n".
 +                    "<b>No such projects found</b><br />\n".
 +                    "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
 +                    "</center>\n<br />\n";
 +              return;
 +      }
 +
 +      @projects = sort_projects_list(\@projects, $order);
 +
 +      if ($show_ctags) {
 +              my $ctags = git_gather_all_ctags(\@projects);
 +              my $cloud = git_populate_project_tagcloud($ctags);
 +              print git_show_project_tagcloud($cloud, 64);
 +      }
 +
 +      print "<table class=\"project_list\">\n";
 +      unless ($no_header) {
 +              print "<tr>\n";
 +              if ($check_forks) {
 +                      print "<th></th>\n";
 +              }
 +              print_sort_th('project', $order, 'Project');
 +              print_sort_th('descr', $order, 'Description');
 +              print_sort_th('owner', $order, 'Owner');
 +              print_sort_th('age', $order, 'Last Change');
 +              print "<th></th>\n" . # for links
 +                    "</tr>\n";
 +      }
 +
 +      if ($projects_list_group_categories) {
 +              # only display categories with projects in the $from-$to window
 +              @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
 +              my %categories = build_projlist_by_category(\@projects, $from, $to);
 +              foreach my $cat (sort keys %categories) {
 +                      unless ($cat eq "") {
 +                              print "<tr>\n";
 +                              if ($check_forks) {
 +                                      print "<td></td>\n";
 +                              }
 +                              print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
 +                              print "</tr>\n";
 +                      }
 +
 +                      git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
 +              }
 +      } else {
 +              git_project_list_rows(\@projects, $from, $to, $check_forks);
 +      }
 +
        if (defined $extra) {
                print "<tr>\n";
                if ($check_forks) {
        print "</table>\n";
  }
  
 +sub git_log_body {
 +      # uses global variable $project
 +      my ($commitlist, $from, $to, $refs, $extra) = @_;
 +
 +      $from = 0 unless defined $from;
 +      $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 +
 +      for (my $i = 0; $i <= $to; $i++) {
 +              my %co = %{$commitlist->[$i]};
 +              next if !%co;
 +              my $commit = $co{'id'};
 +              my $ref = format_ref_marker($refs, $commit);
 +              git_print_header_div('commit',
 +                             "<span class=\"age\">$co{'age_string'}</span>" .
 +                             esc_html($co{'title'}) . $ref,
 +                             $commit);
 +              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=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
 +                    "<br/>\n" .
 +                    "</div>\n";
 +                    git_print_authorship(\%co, -tag => 'span');
 +                    print "<br/>\n</div>\n";
 +
 +              print "<div class=\"log_body\">\n";
 +              git_print_log($co{'comment'}, -final_empty_line=> 1);
 +              print "</div>\n";
 +      }
 +      if ($extra) {
 +              print "<div class=\"page_nav\">\n";
 +              print "$extra\n";
 +              print "</div>\n";
 +      }
 +}
 +
  sub git_shortlog_body {
        # uses global variable $project
        my ($commitlist, $from, $to, $refs, $extra) = @_;
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
 -              my $author = chop_and_escape_str($co{'author_name'}, 10);
                # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 -                    "<td><i>" . $author . "</i></td>\n" .
 -                    "<td>";
 +                    format_author_html('td', \%co, 10) . "<td>";
                print format_subject_html($co{'title'}, $co{'title_short'},
                                          href(action=>"commit", hash=>$commit), $ref);
                print "</td>\n" .
  
  sub git_history_body {
        # Warning: assumes constant type (blob or tree) during history
 -      my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
 +      my ($commitlist, $from, $to, $refs, $extra,
 +          $file_name, $file_hash, $ftype) = @_;
  
        $from = 0 unless defined $from;
        $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
 -      # shortlog uses      chop_str($co{'author_name'}, 10)
 -              my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 -                    "<td><i>" . $author . "</i></td>\n" .
 -                    "<td>";
 +      # shortlog:   format_author_html('td', \%co, 10)
 +                    format_author_html('td', \%co, 15, 3) . "<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);
                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
  
                if ($ftype eq 'blob') {
 -                      my $blob_current = git_get_hash_by_path($hash_base, $file_name);
 +                      my $blob_current = $file_hash;
                        my $blob_parent  = git_get_hash_by_path($commit, $file_name);
                        if (defined $blob_current && defined $blob_parent &&
                                        $blob_current ne $blob_parent) {
@@@ -5414,7 -4296,7 +5414,7 @@@ sub git_heads_body 
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
                      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
 -                    $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
 +                    $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
                      "</td>\n" .
                      "</tr>";
        }
        print "</table>\n";
  }
  
 +# Display a single remote block
 +sub git_remote_block {
 +      my ($remote, $rdata, $limit, $head) = @_;
 +
 +      my $heads = $rdata->{'heads'};
 +      my $fetch = $rdata->{'fetch'};
 +      my $push = $rdata->{'push'};
 +
 +      my $urls_table = "<table class=\"projects_list\">\n" ;
 +
 +      if (defined $fetch) {
 +              if ($fetch eq $push) {
 +                      $urls_table .= format_repo_url("URL", $fetch);
 +              } else {
 +                      $urls_table .= format_repo_url("Fetch URL", $fetch);
 +                      $urls_table .= format_repo_url("Push URL", $push) if defined $push;
 +              }
 +      } elsif (defined $push) {
 +              $urls_table .= format_repo_url("Push URL", $push);
 +      } else {
 +              $urls_table .= format_repo_url("", "No remote URL");
 +      }
 +
 +      $urls_table .= "</table>\n";
 +
 +      my $dots;
 +      if (defined $limit && $limit < @$heads) {
 +              $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
 +      }
 +
 +      print $urls_table;
 +      git_heads_body($heads, $head, 0, $limit, $dots);
 +}
 +
 +# Display a list of remote names with the respective fetch and push URLs
 +sub git_remotes_list {
 +      my ($remotedata, $limit) = @_;
 +      print "<table class=\"heads\">\n";
 +      my $alternate = 1;
 +      my @remotes = sort keys %$remotedata;
 +
 +      my $limited = $limit && $limit < @remotes;
 +
 +      $#remotes = $limit - 1 if $limited;
 +
 +      while (my $remote = shift @remotes) {
 +              my $rdata = $remotedata->{$remote};
 +              my $fetch = $rdata->{'fetch'};
 +              my $push = $rdata->{'push'};
 +              if ($alternate) {
 +                      print "<tr class=\"dark\">\n";
 +              } else {
 +                      print "<tr class=\"light\">\n";
 +              }
 +              $alternate ^= 1;
 +              print "<td>" .
 +                    $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
 +                             -class=> "list name"},esc_html($remote)) .
 +                    "</td>";
 +              print "<td class=\"link\">" .
 +                    (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
 +                    " | " .
 +                    (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
 +                    "</td>";
 +
 +              print "</tr>\n";
 +      }
 +
 +      if ($limited) {
 +              print "<tr>\n" .
 +                    "<td colspan=\"3\">" .
 +                    $cgi->a({-href => href(action=>"remotes")}, "...") .
 +                    "</td>\n" . "</tr>\n";
 +      }
 +
 +      print "</table>";
 +}
 +
 +# Display remote heads grouped by remote, unless there are too many
 +# remotes, in which case we only display the remote names
 +sub git_remotes_body {
 +      my ($remotedata, $limit, $head) = @_;
 +      if ($limit and $limit < keys %$remotedata) {
 +              git_remotes_list($remotedata, $limit);
 +      } else {
 +              fill_remote_heads($remotedata);
 +              while (my ($remote, $rdata) = each %$remotedata) {
 +                      git_print_section({-class=>"remote", -id=>$remote},
 +                              ["remotes", $remote, $remote], sub {
 +                                      git_remote_block($remote, $rdata, $limit, $head);
 +                              });
 +              }
 +      }
 +}
 +
  sub git_search_grep_body {
        my ($commitlist, $from, $to, $extra) = @_;
        $from = 0 unless defined $from;
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
 -              my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
                print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 -                    "<td><i>" . $author . "</i></td>\n" .
 +                    format_author_html('td', \%co, 15, 5) .
                      "<td>" .
                      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
                               -class => "list subject"},
@@@ -5597,7 -4385,7 +5597,7 @@@ sub git_project_list 
        }
  
        git_header_html();
 -      if (-f $home_text) {
 +      if (defined $home_text && -f $home_text) {
                print "<div class=\"index_include\">\n";
                insert_file($home_text);
                print "</div>\n";
@@@ -5630,10 -4418,7 +5630,10 @@@ sub git_forks 
  }
  
  sub git_project_index {
 -      my @projects = git_get_projects_list($project);
 +      my @projects = git_get_projects_list();
 +      if (!@projects) {
 +              die_error(404, "No projects found");
 +      }
  
        print $cgi->header(
                -type => 'text/plain',
@@@ -5661,7 -4446,6 +5661,7 @@@ sub git_summary 
        my %co = parse_commit("HEAD");
        my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
        my $head = $co{'id'};
 +      my $remote_heads = gitweb_check_feature('remote_heads');
  
        my $owner = git_get_project_owner($project);
  
        # there are more ...
        my @taglist  = git_get_tags_list(16);
        my @headlist = git_get_heads_list(16);
 +      my %remotedata = $remote_heads ? git_get_remotes_list() : ();
        my @forklist;
        my $check_forks = gitweb_check_feature('forks');
  
        if ($check_forks) {
 +              # find forks of a project
                @forklist = git_get_projects_list($project);
 +              # filter out forks of forks
 +              @forklist = filter_forks_from_projects_list(\@forklist)
 +                      if (@forklist);
        }
  
        git_header_html();
              "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
              "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
        if (defined $cd{'rfc2822'}) {
 -              print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
 +              print "<tr id=\"metadata_lchange\"><td>last change</td>" .
 +                    "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
        }
  
        # use per project git URL list in $projectroot/$project/cloneurl
        @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
        foreach my $git_url (@url_list) {
                next unless $git_url;
 -              print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
 +              print format_repo_url($url_tag, $git_url);
                $url_tag = "";
        }
  
        my $show_ctags = gitweb_check_feature('ctags');
        if ($show_ctags) {
                my $ctags = git_get_project_ctags($project);
 -              my $cloud = git_populate_project_tagcloud($ctags);
 -              print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
 -              print "</td>\n<td>" unless %$ctags;
 -              print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
 -              print "</td>\n<td>" if %$ctags;
 -              print git_show_project_tagcloud($cloud, 48);
 -              print "</td></tr>";
 +              if (%$ctags) {
 +                      # without ability to add tags, don't show if there are none
 +                      my $cloud = git_populate_project_tagcloud($ctags);
 +                      print "<tr id=\"metadata_ctags\">" .
 +                            "<td>content tags</td>" .
 +                            "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
 +                            "</tr>\n";
 +              }
        }
  
        print "</table>\n";
                               $cgi->a({-href => href(action=>"heads")}, "..."));
        }
  
 +      if (%remotedata) {
 +              git_print_header_div('remotes');
 +              git_remotes_body(\%remotedata, 15, $head);
 +      }
 +
        if (@forklist) {
                git_print_header_div('forks');
                git_project_list_body(\@forklist, 'age', 0, 15,
  }
  
  sub git_tag {
 -      my $head = git_get_head_hash($project);
 -      git_header_html();
 -      git_print_page_nav('','', $head,undef,$head);
        my %tag = parse_tag($hash);
  
        if (! %tag) {
                die_error(404, "Unknown tag object");
        }
  
 +      my $head = git_get_head_hash($project);
 +      git_header_html();
 +      git_print_page_nav('','', $head,undef,$head);
        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
        print "<div class=\"title_text\">\n" .
              "<table class=\"object_header\">\n" .
                                              $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";
 +              git_print_authorship_rows(\%tag, 'author');
        }
        print "</table>\n\n" .
              "</div>\n";
        git_footer_html();
  }
  
 -sub git_blame {
 -      my $fd;
 -      my $ftype;
 +sub git_blame_common {
 +      my $format = shift || 'porcelain';
 +      if ($format eq 'porcelain' && $cgi->param('js')) {
 +              $format = 'incremental';
 +              $action = 'blame_incremental'; # for page title etc
 +      }
  
 +      # permissions
        gitweb_check_feature('blame')
 -          or die_error(403, "Blame view not allowed");
 +              or die_error(403, "Blame view not allowed");
  
 +      # error checking
        die_error(400, "No file name given") unless $file_name;
        $hash_base ||= git_get_head_hash($project);
 -      die_error(404, "Couldn't find base commit") unless ($hash_base);
 +      die_error(404, "Couldn't find base commit") unless $hash_base;
        my %co = parse_commit($hash_base)
                or die_error(404, "Commit not found");
 +      my $ftype = "blob";
        if (!defined $hash) {
                $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
                        or die_error(404, "Error looking up file");
 +      } else {
 +              $ftype = git_get_type($hash);
 +              if ($ftype !~ "blob") {
 +                      die_error(400, "Object is not a blob");
 +              }
        }
 -      $ftype = git_get_type($hash);
 -      if ($ftype !~ "blob") {
 -              die_error(400, "Object is not a blob");
 +
 +      my $fd;
 +      if ($format eq 'incremental') {
 +              # get file contents (as base)
 +              open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
 +                      or die_error(500, "Open git-cat-file failed");
 +      } elsif ($format eq 'data') {
 +              # run git-blame --incremental
 +              open $fd, "-|", git_cmd(), "blame", "--incremental",
 +                      $hash_base, "--", $file_name
 +                      or die_error(500, "Open git-blame --incremental failed");
 +      } else {
 +              # run git-blame --porcelain
 +              open $fd, "-|", git_cmd(), "blame", '-p',
 +                      $hash_base, '--', $file_name
 +                      or die_error(500, "Open git-blame --porcelain failed");
        }
 -      open ($fd, "-|", git_cmd(), "blame", '-p', '--',
 -            $file_name, $hash_base)
 -              or die_error(500, "Open git-blame failed");
 +
 +      # incremental blame data returns early
 +      if ($format eq 'data') {
 +              print $cgi->header(
 +                      -type=>"text/plain", -charset => "utf-8",
 +                      -status=> "200 OK");
 +              local $| = 1; # output autoflush
 +              print while <$fd>;
 +              close $fd
 +                      or print "ERROR $!\n";
 +
 +              print 'END';
 +              if (defined $t0 && gitweb_check_feature('timed')) {
 +                      print ' '.
 +                            tv_interval($t0, [ gettimeofday() ]).
 +                            ' '.$number_of_git_cmds;
 +              }
 +              print "\n";
 +
 +              return;
 +      }
 +
 +      # page header
        git_header_html();
        my $formats_nav =
                $cgi->a({-href => href(action=>"blob", -replay=>1)},
                        "blob") .
 +              " | ";
 +      if ($format eq 'incremental') {
 +              $formats_nav .=
 +                      $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
 +                              "blame") . " (non-incremental)";
 +      } else {
 +              $formats_nav .=
 +                      $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
 +                              "blame") . " (incremental)";
 +      }
 +      $formats_nav .=
                " | " .
                $cgi->a({-href => href(action=>"history", -replay=>1)},
                        "history") .
                " | " .
 -              $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
 +              $cgi->a({-href => href(action=>$action, 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, $hash_base);
 -      my @rev_color = (qw(light2 dark2));
 +
 +      # page body
 +      if ($format eq 'incremental') {
 +              print "<noscript>\n<div class=\"error\"><center><b>\n".
 +                    "This page requires JavaScript to run.\n Use ".
 +                    $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
 +                            'this page').
 +                    " instead.\n".
 +                    "</b></center></div>\n</noscript>\n";
 +
 +              print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
 +      }
 +
 +      print qq!<div class="page_body">\n!;
 +      print qq!<div id="progress_info">... / ...</div>\n!
 +              if ($format eq 'incremental');
 +      print qq!<table id="blame_table" class="blame" width="100%">\n!.
 +            #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
 +            qq!<thead>\n!.
 +            qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
 +            qq!</thead>\n!.
 +            qq!<tbody>\n!;
 +
 +      my @rev_color = qw(light dark);
        my $num_colors = scalar(@rev_color);
        my $current_color = 0;
 -      my $last_rev;
 -      print <<HTML;
 -<div class="page_body">
 -<table class="blame">
 -<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
 -HTML
 -      my %metainfo = ();
 -      while (1) {
 -              $_ = <$fd>;
 -              last unless defined $_;
 -              my ($full_rev, $orig_lineno, $lineno, $group_size) =
 -                  /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
 -              if (!exists $metainfo{$full_rev}) {
 -                      $metainfo{$full_rev} = {};
 -              }
 -              my $meta = $metainfo{$full_rev};
 -              while (<$fd>) {
 -                      last if (s/^\t//);
 -                      if (/^(\S+) (.*)$/) {
 -                              $meta->{$1} = $2;
 -                      }
 -              }
 -              my $data = $_;
 -              chomp $data;
 -              my $rev = substr($full_rev, 0, 8);
 -              my $author = $meta->{'author'};
 -              my %date = parse_date($meta->{'author-time'},
 -                                    $meta->{'author-tz'});
 -              my $date = $date{'iso-tz'};
 -              if ($group_size) {
 -                      $current_color = ++$current_color % $num_colors;
 -              }
 -              print "<tr class=\"$rev_color[$current_color]\">\n";
 -              if ($group_size) {
 -                      print "<td class=\"sha1\"";
 -                      print " title=\"". esc_html($author) . ", $date\"";
 -                      print " rowspan=\"$group_size\"" if ($group_size > 1);
 -                      print ">";
 -                      print $cgi->a({-href => href(action=>"commit",
 -                                                   hash=>$full_rev,
 -                                                   file_name=>$file_name)},
 -                                    esc_html($rev));
 -                      print "</td>\n";
 +
 +      if ($format eq 'incremental') {
 +              my $color_class = $rev_color[$current_color];
 +
 +              #contents of a file
 +              my $linenr = 0;
 +      LINE:
 +              while (my $line = <$fd>) {
 +                      chomp $line;
 +                      $linenr++;
 +
 +                      print qq!<tr id="l$linenr" class="$color_class">!.
 +                            qq!<td class="sha1"><a href=""> </a></td>!.
 +                            qq!<td class="linenr">!.
 +                            qq!<a class="linenr" href="">$linenr</a></td>!;
 +                      print qq!<td class="pre">! . esc_html($line) . "</td>\n";
 +                      print qq!</tr>\n!;
                }
 -              open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
 -                      or die_error(500, "Open git-rev-parse failed");
 -              my $parent_commit = <$dd>;
 -              close $dd;
 -              chomp($parent_commit);
 -              my $blamed = href(action => 'blame',
 -                                file_name => $meta->{'filename'},
 -                                hash_base => $parent_commit);
 -              print "<td class=\"linenr\">";
 -              print $cgi->a({ -href => "$blamed#l$orig_lineno",
 -                              -id => "l$lineno",
 -                              -class => "linenr" },
 -                            esc_html($lineno));
 -              print "</td>";
 -              print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
 -              print "</tr>\n";
 +
 +      } else { # porcelain, i.e. ordinary blame
 +              my %metainfo = (); # saves information about commits
 +
 +              # blame data
 +      LINE:
 +              while (my $line = <$fd>) {
 +                      chomp $line;
 +                      # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
 +                      # no <lines in group> for subsequent lines in group of lines
 +                      my ($full_rev, $orig_lineno, $lineno, $group_size) =
 +                         ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
 +                      if (!exists $metainfo{$full_rev}) {
 +                              $metainfo{$full_rev} = { 'nprevious' => 0 };
 +                      }
 +                      my $meta = $metainfo{$full_rev};
 +                      my $data;
 +                      while ($data = <$fd>) {
 +                              chomp $data;
 +                              last if ($data =~ s/^\t//); # contents of line
 +                              if ($data =~ /^(\S+)(?: (.*))?$/) {
 +                                      $meta->{$1} = $2 unless exists $meta->{$1};
 +                              }
 +                              if ($data =~ /^previous /) {
 +                                      $meta->{'nprevious'}++;
 +                              }
 +                      }
 +                      my $short_rev = substr($full_rev, 0, 8);
 +                      my $author = $meta->{'author'};
 +                      my %date =
 +                              parse_date($meta->{'author-time'}, $meta->{'author-tz'});
 +                      my $date = $date{'iso-tz'};
 +                      if ($group_size) {
 +                              $current_color = ($current_color + 1) % $num_colors;
 +                      }
 +                      my $tr_class = $rev_color[$current_color];
 +                      $tr_class .= ' boundary' if (exists $meta->{'boundary'});
 +                      $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
 +                      $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
 +                      print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
 +                      if ($group_size) {
 +                              print "<td class=\"sha1\"";
 +                              print " title=\"". esc_html($author) . ", $date\"";
 +                              print " rowspan=\"$group_size\"" if ($group_size > 1);
 +                              print ">";
 +                              print $cgi->a({-href => href(action=>"commit",
 +                                                           hash=>$full_rev,
 +                                                           file_name=>$file_name)},
 +                                            esc_html($short_rev));
 +                              if ($group_size >= 2) {
 +                                      my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
 +                                      if (@author_initials) {
 +                                              print "<br />" .
 +                                                    esc_html(join('', @author_initials));
 +                                              #           or join('.', ...)
 +                                      }
 +                              }
 +                              print "</td>\n";
 +                      }
 +                      # 'previous' <sha1 of parent commit> <filename at commit>
 +                      if (exists $meta->{'previous'} &&
 +                          $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
 +                              $meta->{'parent'} = $1;
 +                              $meta->{'file_parent'} = unquote($2);
 +                      }
 +                      my $linenr_commit =
 +                              exists($meta->{'parent'}) ?
 +                              $meta->{'parent'} : $full_rev;
 +                      my $linenr_filename =
 +                              exists($meta->{'file_parent'}) ?
 +                              $meta->{'file_parent'} : unquote($meta->{'filename'});
 +                      my $blamed = href(action => 'blame',
 +                                        file_name => $linenr_filename,
 +                                        hash_base => $linenr_commit);
 +                      print "<td class=\"linenr\">";
 +                      print $cgi->a({ -href => "$blamed#l$orig_lineno",
 +                                      -class => "linenr" },
 +                                    esc_html($lineno));
 +                      print "</td>";
 +                      print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
 +                      print "</tr>\n";
 +              } # end while
 +
        }
 -      print "</table>\n";
 -      print "</div>";
 +
 +      # footer
 +      print "</tbody>\n".
 +            "</table>\n"; # class="blame"
 +      print "</div>\n";   # class="blame_body"
        close $fd
                or print "Reading blob failed\n";
 +
        git_footer_html();
  }
  
 +sub git_blame {
 +      git_blame_common();
 +}
 +
 +sub git_blame_incremental {
 +      git_blame_common('incremental');
 +}
 +
 +sub git_blame_data {
 +      git_blame_common('data');
 +}
 +
  sub git_tags {
        my $head = git_get_head_hash($project);
        git_header_html();
 -      git_print_page_nav('','', $head,undef,$head);
 +      git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
        git_print_header_div('summary', $project);
  
        my @tagslist = git_get_tags_list();
  sub git_heads {
        my $head = git_get_head_hash($project);
        git_header_html();
 -      git_print_page_nav('','', $head,undef,$head);
 +      git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
        git_print_header_div('summary', $project);
  
        my @headslist = git_get_heads_list();
        git_footer_html();
  }
  
 +# used both for single remote view and for list of all the remotes
 +sub git_remotes {
 +      gitweb_check_feature('remote_heads')
 +              or die_error(403, "Remote heads view is disabled");
 +
 +      my $head = git_get_head_hash($project);
 +      my $remote = $input_params{'hash'};
 +
 +      my $remotedata = git_get_remotes_list($remote);
 +      die_error(500, "Unable to get remote information") unless defined $remotedata;
 +
 +      unless (%$remotedata) {
 +              die_error(404, defined $remote ?
 +                      "Remote $remote not found" :
 +                      "No remotes found");
 +      }
 +
 +      git_header_html(undef, undef, -action_extra => $remote);
 +      git_print_page_nav('', '',  $head, undef, $head,
 +              format_ref_views($remote ? '' : 'remotes'));
 +
 +      fill_remote_heads($remotedata);
 +      if (defined $remote) {
 +              git_print_header_div('remotes', "$remote remote for $project");
 +              git_remote_block($remote, $remotedata->{$remote}, undef, $head);
 +      } else {
 +              git_print_header_div('summary', "$project remotes");
 +              git_remotes_body($remotedata, undef, $head);
 +      }
 +
 +      git_footer_html();
 +}
 +
  sub git_blob_plain {
        my $type = shift;
        my $expires;
        # want to be sure not to break that by serving the image as an
        # attachment (though Firefox 3 doesn't seem to care).
        my $sandbox = $prevent_xss &&
-               $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
+               $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
+       # serve text/* as text/plain
+       if ($prevent_xss &&
+           ($type =~ m!^text/[a-z]+\b(.*)$! ||
+            ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
+               my $rest = $1;
+               $rest = defined $rest ? $rest : '';
+               $type = "text/plain$rest";
+       }
  
        print $cgi->header(
                -type => $type,
                -content_disposition =>
                        ($sandbox ? 'attachment' : 'inline')
                        . '; filename="' . $save_as . '"');
 -      undef $/;
 +      local $/ = undef;
        binmode STDOUT, ':raw';
        print <$fd>;
        binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
 -      $/ = "\n";
        close $fd;
  }
  
@@@ -6179,7 -4797,6 +6188,7 @@@ sub git_blob 
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(500, "Couldn't cat $file_name, $hash");
        my $mimetype = blob_mimetype($fd, $file_name);
 +      # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
        if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
                close $fd;
                return git_blob_plain($mimetype);
        # we can have blame only for text/* mimetype
        $have_blame &&= ($mimetype =~ m!^text/!);
  
 +      my $highlight = gitweb_check_feature('highlight');
 +      my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
 +      $fd = run_highlighter($fd, $highlight, $syntax)
 +              if $syntax;
 +
        git_header_html(undef, $expires);
        my $formats_nav = '';
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
        } else {
                print "<div class=\"page_nav\">\n" .
                      "<br/><br/></div>\n" .
 -                    "<div class=\"title\">$hash</div>\n";
 +                    "<div class=\"title\">".esc_html($hash)."</div>\n";
        }
        git_print_page_path($file_name, "blob", $hash_base);
        print "<div class=\"page_body\">\n";
        if ($mimetype =~ m!^image/!) {
 -              print qq!<img type="$mimetype"!;
 +              print qq!<img type="!.esc_attr($mimetype).qq!"!;
                if ($file_name) {
 -                      print qq! alt="$file_name" title="$file_name"!;
 +                      print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
                }
                print qq! src="! .
                      href(action=>"blob_plain", hash=>$hash,
                        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, -nbsp=>1);
 +                      printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
 +                             $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
                }
        }
        close $fd
@@@ -6263,25 -4875,18 +6272,25 @@@ sub git_tree 
                }
        }
        die_error(404, "No such tree") unless defined($hash);
 -      $/ = "\0";
 -      open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
 -              or die_error(500, "Open git-ls-tree failed");
 -      my @entries = map { chomp; $_ } <$fd>;
 -      close $fd or die_error(404, "Reading tree failed");
 -      $/ = "\n";
 +
 +      my $show_sizes = gitweb_check_feature('show-sizes');
 +      my $have_blame = gitweb_check_feature('blame');
 +
 +      my @entries = ();
 +      {
 +              local $/ = "\0";
 +              open my $fd, "-|", git_cmd(), "ls-tree", '-z',
 +                      ($show_sizes ? '-l' : ()), @extra_options, $hash
 +                      or die_error(500, "Open git-ls-tree failed");
 +              @entries = map { chomp; $_ } <$fd>;
 +              close $fd
 +                      or die_error(404, "Reading tree failed");
 +      }
  
        my $refs = git_get_references();
        my $ref = format_ref_marker($refs, $hash_base);
        git_header_html();
        my $basedir = '';
 -      my $have_blame = gitweb_check_feature('blame');
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
                my @views_nav = ();
                if (defined $file_name) {
                        # FIXME: Should be available when we have no hash base as well.
                        push @views_nav, $snapshot_links;
                }
 -              git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
 +              git_print_page_nav('tree','', $hash_base, undef, undef,
 +                                 join(' | ', @views_nav));
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
        } else {
                undef $hash_base;
                print "<div class=\"page_nav\">\n";
                print "<br/><br/></div>\n";
 -              print "<div class=\"title\">$hash</div>\n";
 +              print "<div class=\"title\">".esc_html($hash)."</div>\n";
        }
        if (defined $file_name) {
                $basedir = $file_name;
                undef $up unless $up;
                # based on git_print_tree_entry
                print '<td class="mode">' . mode_str('040000') . "</td>\n";
 +              print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
                print '<td class="list">';
 -              print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
 +              print $cgi->a({-href => href(action=>"tree",
 +                                           hash_base=>$hash_base,
                                             file_name=>$up)},
                              "..");
                print "</td>\n";
                print "</tr>\n";
        }
        foreach my $line (@entries) {
 -              my %t = parse_ls_tree_line($line, -z => 1);
 +              my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
  
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
        git_footer_html();
  }
  
 +sub snapshot_name {
 +      my ($project, $hash) = @_;
 +
 +      # path/to/project.git  -> project
 +      # path/to/project/.git -> project
 +      my $name = to_utf8($project);
 +      $name =~ s,([^/])/*\.git$,$1,;
 +      $name = basename($name);
 +      # sanitize name
 +      $name =~ s/[[:cntrl:]]/?/g;
 +
 +      my $ver = $hash;
 +      if ($hash =~ /^[0-9a-fA-F]+$/) {
 +              # shorten SHA-1 hash
 +              my $full_hash = git_get_full_hash($project, $hash);
 +              if ($full_hash =~ /^$hash/ && length($hash) > 7) {
 +                      $ver = git_get_short_hash($project, $hash);
 +              }
 +      } elsif ($hash =~ m!^refs/tags/(.*)$!) {
 +              # tags don't need shortened SHA-1 hash
 +              $ver = $1;
 +      } else {
 +              # branches and other need shortened SHA-1 hash
 +              if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
 +                      $ver = $1;
 +              }
 +              $ver .= '-' . git_get_short_hash($project, $hash);
 +      }
 +      # in case of hierarchical branch names
 +      $ver =~ s!/!.!g;
 +
 +      # name = project-version_string
 +      $name = "$name-$ver";
 +
 +      return wantarray ? ($name, $name) : $name;
 +}
 +
  sub git_snapshot {
        my $format = $input_params{'snapshot_format'};
        if (!@snapshot_fmts) {
                die_error(400, "Invalid snapshot format parameter");
        } elsif (!exists($known_snapshot_formats{$format})) {
                die_error(400, "Unknown snapshot format");
 +      } elsif ($known_snapshot_formats{$format}{'disabled'}) {
 +              die_error(403, "Snapshot format not allowed");
        } elsif (!grep($_ eq $format, @snapshot_fmts)) {
                die_error(403, "Unsupported snapshot format");
        }
  
 -      if (!defined $hash) {
 -              $hash = git_get_head_hash($project);
 +      my $type = git_get_type("$hash^{}");
 +      if (!$type) {
 +              die_error(404, 'Object does not exist');
 +      }  elsif ($type eq 'blob') {
 +              die_error(400, 'Object is not a tree-ish');
        }
  
 -      my $name = $project;
 -      $name =~ s,([^/])/*\.git$,$1,;
 -      $name = basename($name);
 -      my $filename = to_utf8($name);
 -      $name =~ s/\047/\047\\\047\047/g;
 -      my $cmd;
 -      $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
 -      $cmd = quote_command(
 +      my ($name, $prefix) = snapshot_name($project, $hash);
 +      my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
 +      my $cmd = quote_command(
                git_cmd(), 'archive',
                "--format=$known_snapshot_formats{$format}{'format'}",
 -              "--prefix=$name/", $hash);
 +              "--prefix=$prefix/", $hash);
        if (exists $known_snapshot_formats{$format}{'compressor'}) {
                $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
        }
  
 +      $filename =~ s/(["\\])/\\$1/g;
        print $cgi->header(
                -type => $known_snapshot_formats{$format}{'type'},
 -              -content_disposition => 'inline; filename="' . "$filename" . '"',
 +              -content_disposition => 'inline; filename="' . $filename . '"',
                -status => '200 OK');
  
        open my $fd, "-|", $cmd
        close $fd;
  }
  
 -sub git_log {
 +sub git_log_generic {
 +      my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
 +
        my $head = git_get_head_hash($project);
 -      if (!defined $hash) {
 -              $hash = $head;
 +      if (!defined $base) {
 +              $base = $head;
        }
        if (!defined $page) {
                $page = 0;
        }
        my $refs = git_get_references();
  
 -      my @commitlist = parse_commits($hash, 101, (100 * $page));
 -
 -      my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
 +      my $commit_hash = $base;
 +      if (defined $parent) {
 +              $commit_hash = "$parent..$base";
 +      }
 +      my @commitlist =
 +              parse_commits($commit_hash, 101, (100 * $page),
 +                            defined $file_name ? ($file_name, "--full-history") : ());
  
 -      git_header_html();
 -      git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 +      my $ftype;
 +      if (!defined $file_hash && defined $file_name) {
 +              # some commits could have deleted file in question,
 +              # and not have it in tree, but one of them has to have it
 +              for (my $i = 0; $i < @commitlist; $i++) {
 +                      $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
 +                      last if defined $file_hash;
 +              }
 +      }
 +      if (defined $file_hash) {
 +              $ftype = git_get_type($file_hash);
 +      }
 +      if (defined $file_name && !defined $ftype) {
 +              die_error(500, "Unknown type of object");
 +      }
 +      my %co;
 +      if (defined $file_name) {
 +              %co = parse_commit($base)
 +                      or die_error(404, "Unknown commit object");
 +      }
  
 -      if (!@commitlist) {
 -              my %co = parse_commit($hash);
  
 -              git_print_header_div('summary', $project);
 -              print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
 +      my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
 +      my $next_link = '';
 +      if ($#commitlist >= 100) {
 +              $next_link =
 +                      $cgi->a({-href => href(-replay=>1, page=>$page+1),
 +                               -accesskey => "n", -title => "Alt-n"}, "next");
        }
 -      my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
 -      for (my $i = 0; $i <= $to; $i++) {
 -              my %co = %{$commitlist[$i]};
 -              next if !%co;
 -              my $commit = $co{'id'};
 -              my $ref = format_ref_marker($refs, $commit);
 -              my %ad = parse_date($co{'author_epoch'});
 -              git_print_header_div('commit',
 -                             "<span class=\"age\">$co{'age_string'}</span>" .
 -                             esc_html($co{'title'}) . $ref,
 -                             $commit);
 -              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=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
 -                    "<br/>\n" .
 -                    "</div>\n" .
 -                    "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
 -                    "</div>\n";
 -
 -              print "<div class=\"log_body\">\n";
 -              git_print_log($co{'comment'}, -final_empty_line=> 1);
 -              print "</div>\n";
 +      my $patch_max = gitweb_get_feature('patches');
 +      if ($patch_max && !defined $file_name) {
 +              if ($patch_max < 0 || @commitlist <= $patch_max) {
 +                      $paging_nav .= " &sdot; " .
 +                              $cgi->a({-href => href(action=>"patches", -replay=>1)},
 +                                      "patches");
 +              }
        }
 -      if ($#commitlist >= 100) {
 -              print "<div class=\"page_nav\">\n";
 -              print $cgi->a({-href => href(-replay=>1, page=>$page+1),
 -                             -accesskey => "n", -title => "Alt-n"}, "next");
 -              print "</div>\n";
 +
 +      git_header_html();
 +      git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
 +      if (defined $file_name) {
 +              git_print_header_div('commit', esc_html($co{'title'}), $base);
 +      } else {
 +              git_print_header_div('summary', $project)
        }
 +      git_print_page_path($file_name, $ftype, $hash_base)
 +              if (defined $file_name);
 +
 +      $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
 +                   $file_name, $file_hash, $ftype);
 +
        git_footer_html();
  }
  
 +sub git_log {
 +      git_log_generic('log', \&git_log_body,
 +                      $hash, $hash_parent);
 +}
 +
  sub git_commit {
        $hash ||= $hash_base || "HEAD";
        my %co = parse_commit($hash)
            or die_error(404, "Unknown commit object");
 -      my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
 -      my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
  
        my $parent  = $co{'parent'};
        my $parents = $co{'parents'}; # listref
                        } @$parents ) .
                        ')';
        }
 +      if (gitweb_check_feature('patches') && @$parents <= 1) {
 +              $formats_nav .= " | " .
 +                      $cgi->a({-href => href(action=>"patch", -replay=>1)},
 +                              "patch");
 +      }
  
        if (!defined $parent) {
                $parent = "--root";
        }
        print "<div class=\"title_text\">\n" .
              "<table class=\"object_header\">\n";
 -      print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
 -            "<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'});
 -      } else {
 -              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";
 +      git_print_authorship_rows(\%co);
        print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
        print "<tr>" .
              "<td>tree</td>" .
@@@ -6772,7 -5328,7 +6781,7 @@@ sub git_blobdiff 
                        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";
 +                      print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
                }
                if (defined $file_name) {
                        git_print_page_path($file_name, "blob", $hash_base);
@@@ -6823,14 -5379,7 +6832,14 @@@ sub git_blobdiff_plain 
  }
  
  sub git_commitdiff {
 -      my $format = shift || 'html';
 +      my %params = @_;
 +      my $format = $params{-format} || 'html';
 +
 +      my ($patch_max) = gitweb_get_feature('patches');
 +      if ($format eq 'patch') {
 +              die_error(403, "Patch view not allowed") unless $patch_max;
 +      }
 +
        $hash ||= $hash_base || "HEAD";
        my %co = parse_commit($hash)
            or die_error(404, "Unknown commit object");
                $formats_nav =
                        $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
                                "raw");
 +              if ($patch_max && @{$co{'parents'}} <= 1) {
 +                      $formats_nav .= " | " .
 +                              $cgi->a({-href => href(action=>"patch", -replay=>1)},
 +                                      "patch");
 +              }
  
                if (defined $hash_parent &&
                    $hash_parent ne '-c' && $hash_parent ne '--cc') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
                        '-p', $hash_parent_param, $hash, "--"
                        or die_error(500, "Open git-diff-tree failed");
 -
 +      } elsif ($format eq 'patch') {
 +              # For commit ranges, we limit the output to the number of
 +              # patches specified in the 'patches' feature.
 +              # For single commits, we limit the output to a single patch,
 +              # diverging from the git-format-patch default.
 +              my @commit_spec = ();
 +              if ($hash_parent) {
 +                      if ($patch_max > 0) {
 +                              push @commit_spec, "-$patch_max";
 +                      }
 +                      push @commit_spec, '-n', "$hash_parent..$hash";
 +              } else {
 +                      if ($params{-single}) {
 +                              push @commit_spec, '-1';
 +                      } else {
 +                              if ($patch_max > 0) {
 +                                      push @commit_spec, "-$patch_max";
 +                              }
 +                              push @commit_spec, "-n";
 +                      }
 +                      push @commit_spec, '--root', $hash;
 +              }
 +              open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
 +                      '--encoding=utf8', '--stdout', @commit_spec
 +                      or die_error(500, "Open git-format-patch failed");
        } else {
                die_error(400, "Unknown commitdiff format");
        }
                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);
 -              git_print_authorship(\%co);
 +              print "<div class=\"title_text\">\n" .
 +                    "<table class=\"object_header\">\n";
 +              git_print_authorship_rows(\%co);
 +              print "</table>".
 +                    "</div>\n";
                print "<div class=\"page_body\">\n";
                if (@{$co{'comment'}} > 1) {
                        print "<div class=\"log\">\n";
                        print to_utf8($line) . "\n";
                }
                print "---\n\n";
 +      } elsif ($format eq 'patch') {
 +              my $filename = basename($project) . "-$hash.patch";
 +
 +              print $cgi->header(
 +                      -type => 'text/plain',
 +                      -charset => 'utf-8',
 +                      -expires => $expires,
 +                      -content_disposition => 'inline; filename="' . "$filename" . '"');
        }
  
        # write patch
                print <$fd>;
                close $fd
                        or print "Reading git-diff-tree failed\n";
 +      } elsif ($format eq 'patch') {
 +              local $/ = undef;
 +              print <$fd>;
 +              close $fd
 +                      or print "Reading git-format-patch failed\n";
        }
  }
  
  sub git_commitdiff_plain {
 -      git_commitdiff('plain');
 +      git_commitdiff(-format => 'plain');
  }
  
 -sub git_history {
 -      if (!defined $hash_base) {
 -              $hash_base = git_get_head_hash($project);
 -      }
 -      if (!defined $page) {
 -              $page = 0;
 -      }
 -      my $ftype;
 -      my %co = parse_commit($hash_base)
 -          or die_error(404, "Unknown commit object");
 -
 -      my $refs = git_get_references();
 -      my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 -
 -      my @commitlist = parse_commits($hash_base, 101, (100 * $page),
 -                                     $file_name, "--full-history")
 -          or die_error(404, "No such file or directory on given branch");
 -
 -      if (!defined $hash && defined $file_name) {
 -              # some commits could have deleted file in question,
 -              # and not have it in tree, but one of them has to have it
 -              for (my $i = 0; $i <= @commitlist; $i++) {
 -                      $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
 -                      last if defined $hash;
 -              }
 -      }
 -      if (defined $hash) {
 -              $ftype = git_get_type($hash);
 -      }
 -      if (!defined $ftype) {
 -              die_error(500, "Unknown type of object");
 -      }
 -
 -      my $paging_nav = '';
 -      if ($page > 0) {
 -              $paging_nav .=
 -                      $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 -                                             file_name=>$file_name)},
 -                              "first");
 -              $paging_nav .= " &sdot; " .
 -                      $cgi->a({-href => href(-replay=>1, page=>$page-1),
 -                               -accesskey => "p", -title => "Alt-p"}, "prev");
 -      } else {
 -              $paging_nav .= "first";
 -              $paging_nav .= " &sdot; prev";
 -      }
 -      my $next_link = '';
 -      if ($#commitlist >= 100) {
 -              $next_link =
 -                      $cgi->a({-href => href(-replay=>1, page=>$page+1),
 -                               -accesskey => "n", -title => "Alt-n"}, "next");
 -              $paging_nav .= " &sdot; $next_link";
 -      } else {
 -              $paging_nav .= " &sdot; next";
 -      }
 -
 -      git_header_html();
 -      git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
 -      git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 -      git_print_page_path($file_name, $ftype, $hash_base);
 +# format-patch-style patches
 +sub git_patch {
 +      git_commitdiff(-format => 'patch', -single => 1);
 +}
  
 -      git_history_body(\@commitlist, 0, 99,
 -                       $refs, $hash_base, $ftype, $next_link);
 +sub git_patches {
 +      git_commitdiff(-format => 'patch');
 +}
  
 -      git_footer_html();
 +sub git_history {
 +      git_log_generic('history', \&git_history_body,
 +                      $hash_base, $hash_parent_base,
 +                      $file_name, $hash);
  }
  
  sub git_search {
                        $paging_nav .= " &sdot; next";
                }
  
 -              if ($#commitlist >= 100) {
 -              }
 -
                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
                git_print_header_div('commit', esc_html($co{'title'}), $hash);
 -              git_search_grep_body(\@commitlist, 0, 99, $next_link);
 +              if ($page == 0 && !@commitlist) {
 +                      print "<p>No match.</p>\n";
 +              } else {
 +                      git_search_grep_body(\@commitlist, 0, 99, $next_link);
 +              }
        }
  
        if ($searchtype eq 'pickaxe') {
  
                print "<table class=\"pickaxe search\">\n";
                my $alternate = 1;
 -              $/ = "\n";
 +              local $/ = "\n";
                open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
                        '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
                        ($search_use_regexp ? '--pickaxe-regex' : ());
                print "<table class=\"grep_search\">\n";
                my $alternate = 1;
                my $matches = 0;
 -              $/ = "\n";
 +              local $/ = "\n";
                open my $fd, "-|", git_cmd(), 'grep', '-n',
                        $search_use_regexp ? ('-E', '-i') : '-F',
                        $searchtext, $co{'tree'};
  }
  
  sub git_shortlog {
 -      my $head = git_get_head_hash($project);
 -      if (!defined $hash) {
 -              $hash = $head;
 -      }
 -      if (!defined $page) {
 -              $page = 0;
 -      }
 -      my $refs = git_get_references();
 -
 -      my $commit_hash = $hash;
 -      if (defined $hash_parent) {
 -              $commit_hash = "$hash_parent..$hash";
 -      }
 -      my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
 -
 -      my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
 -      my $next_link = '';
 -      if ($#commitlist >= 100) {
 -              $next_link =
 -                      $cgi->a({-href => href(-replay=>1, page=>$page+1),
 -                               -accesskey => "n", -title => "Alt-n"}, "next");
 -      }
 -
 -      git_header_html();
 -      git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
 -      git_print_header_div('summary', $project);
 -
 -      git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
 -
 -      git_footer_html();
 +      git_log_generic('shortlog', \&git_shortlog_body,
 +                      $hash, $hash_parent);
  }
  
  ## ......................................................................
@@@ -7359,25 -5941,7 +7368,25 @@@ sub git_feed 
        }
        if (defined($commitlist[0])) {
                %latest_commit = %{$commitlist[0]};
 -              %latest_date   = parse_date($latest_commit{'author_epoch'});
 +              my $latest_epoch = $latest_commit{'committer_epoch'};
 +              %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
 +              my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
 +              if (defined $if_modified) {
 +                      my $since;
 +                      if (eval { require HTTP::Date; 1; }) {
 +                              $since = HTTP::Date::str2time($if_modified);
 +                      } elsif (eval { require Time::ParseDate; 1; }) {
 +                              $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
 +                      }
 +                      if (defined $since && $latest_epoch <= $since) {
 +                              print $cgi->header(
 +                                      -type => $content_type,
 +                                      -charset => 'utf-8',
 +                                      -last_modified => $latest_date{'rfc2822'},
 +                                      -status => '304 Not Modified');
 +                              return;
 +                      }
 +              }
                print $cgi->header(
                        -type => $content_type,
                        -charset => 'utf-8',
                print "<title>$title</title>\n" .
                      "<link>$alt_url</link>\n" .
                      "<description>$descr</description>\n" .
 -                    "<language>en</language>\n";
 +                    "<language>en</language>\n" .
 +                    # project owner is responsible for 'editorial' content
 +                    "<managingEditor>$owner</managingEditor>\n";
 +              if (defined $logo || defined $favicon) {
 +                      # prefer the logo to the favicon, since RSS
 +                      # doesn't allow both
 +                      my $img = esc_url($logo || $favicon);
 +                      print "<image>\n" .
 +                            "<url>$img</url>\n" .
 +                            "<title>$title</title>\n" .
 +                            "<link>$alt_url</link>\n" .
 +                            "</image>\n";
 +              }
 +              if (%latest_date) {
 +                      print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
 +                      print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
 +              }
 +              print "<generator>gitweb v.$version/$git_version</generator>\n";
        } elsif ($format eq 'atom') {
                print <<XML;
  <feed xmlns="http://www.w3.org/2005/Atom">
@@@ -7470,7 -6017,7 +7479,7 @@@ XM
                if (defined $favicon) {
                        print "<icon>" . esc_url($favicon) . "</icon>\n";
                }
 -              if (defined $logo_url) {
 +              if (defined $logo) {
                        # not twice as wide as tall: 72 x 27 pixels
                        print "<logo>" . esc_url($logo) . "</logo>\n";
                }
                } else {
                        print "<updated>$latest_date{'iso-8601'}</updated>\n";
                }
 +              print "<generator version='$version/$git_version'>gitweb</generator>\n";
        }
  
        # contents
                if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
                        last;
                }
 -              my %cd = parse_date($co{'author_epoch'});
 +              my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
  
                # get list of changed files
                open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
        # end of feed
        if ($format eq 'rss') {
                print "</channel>\n</rss>\n";
 -      }       elsif ($format eq 'atom') {
 +      } elsif ($format eq 'atom') {
                print "</feed>\n";
        }
  }
@@@ -7601,15 -6147,8 +7610,15 @@@ sub git_atom 
  
  sub git_opml {
        my @list = git_get_projects_list();
 +      if (!@list) {
 +              die_error(404, "No projects found");
 +      }
 +
 +      print $cgi->header(
 +              -type => 'text/xml',
 +              -charset => 'utf-8',
 +              -content_disposition => 'inline; filename="opml.xml"');
  
 -      print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
        print <<XML;
  <?xml version="1.0" encoding="utf-8"?>
  <opml version="1.0">
@@@ -7633,8 -6172,8 +7642,8 @@@ XM
                }
  
                my $path = esc_html(chop_str($proj{'path'}, 25, 5));
 -              my $rss  = "$my_url?p=$proj{'path'};a=rss";
 -              my $html = "$my_url?p=$proj{'path'};a=summary";
 +              my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
 +              my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
                print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
        }
        print <<XML;