Merge branch 'jc/web-blame'
authorJunio C Hamano <junkio@cox.net>
Wed, 25 Oct 2006 20:18:06 +0000 (13:18 -0700)
committerJunio C Hamano <junkio@cox.net>
Wed, 25 Oct 2006 20:18:06 +0000 (13:18 -0700)
* jc/web-blame:
gitweb: spell "blame --porcelain" with -p
blame: Document and add help text for -f, -n, and -p
gitweb: blame porcelain: lineno and orig lineno swapped
Remove git-annotate.perl and create a builtin-alias for git-blame
gitweb: use blame --porcelain
git-blame --porcelain
blame.c: move code to output metainfo into a separate function.
git-blame: --show-number (and -n)
git-blame: --show-name (and -f)
blame.c: whitespace and formatting clean-up.
Gitweb - provide site headers and footers
gitweb: blame: Mouse-over commit-8 shows author and date
gitweb: blame: print commit-8 on the leading row of a commit-block
Revert 954a6183756a073723a7c9fd8d2feb13132876b0
gitweb: prepare for repositories with packed refs.
gitweb: make leftmost column of blame less cluttered.

1  2 
Makefile
builtin.h
git.c
gitweb/gitweb.perl
diff --combined Makefile
index 66c8b4b127cc5c0380ab7d8cb5f4d1dcb89b18a7,473ff271fe2fc3c4d7a5157460f6b837d9e97405..95f37fe7779deb3901be20cb7a1869b7e2de77b0
+++ b/Makefile
@@@ -132,6 -132,8 +132,8 @@@ GITWEB_HOMETEXT = indextext.htm
  GITWEB_CSS = gitweb.css
  GITWEB_LOGO = git-logo.png
  GITWEB_FAVICON = git-favicon.png
+ GITWEB_SITE_HEADER =
+ GITWEB_SITE_FOOTER =
  
  export prefix bindir gitexecdir template_dir GIT_PYTHON_DIR
  
@@@ -173,7 -175,7 +175,7 @@@ SCRIPT_SH = 
  SCRIPT_PERL = \
        git-archimport.perl git-cvsimport.perl git-relink.perl \
        git-shortlog.perl git-rerere.perl \
-       git-annotate.perl git-cvsserver.perl \
+       git-cvsserver.perl \
        git-svnimport.perl git-cvsexportcommit.perl \
        git-send-email.perl git-svn.perl
  
@@@ -265,6 -267,7 +267,7 @@@ LIB_OBJS = 
  
  BUILTIN_OBJS = \
        builtin-add.o \
+       builtin-annotate.o \
        builtin-apply.o \
        builtin-archive.o \
        builtin-cat-file.o \
@@@ -675,6 -678,8 +678,8 @@@ gitweb/gitweb.cgi: gitweb/gitweb.per
            -e 's|++GITWEB_CSS++|$(GITWEB_CSS)|g' \
            -e 's|++GITWEB_LOGO++|$(GITWEB_LOGO)|g' \
            -e 's|++GITWEB_FAVICON++|$(GITWEB_FAVICON)|g' \
+           -e 's|++GITWEB_SITE_HEADER++|$(GITWEB_SITE_HEADER)|g' \
+           -e 's|++GITWEB_SITE_FOOTER++|$(GITWEB_SITE_FOOTER)|g' \
            $< >$@+
        chmod +x $@+
        mv $@+ $@
@@@ -760,8 -765,6 +765,8 @@@ $(LIB_FILE): $(LIB_OBJS
        rm -f $@ && $(AR) rcs $@ $(LIB_OBJS)
  
  XDIFF_OBJS=xdiff/xdiffi.o xdiff/xprepare.o xdiff/xutils.o xdiff/xemit.o
 +$(XDIFF_OBJS): xdiff/xinclude.h xdiff/xmacros.h xdiff/xdiff.h xdiff/xtypes.h \
 +      xdiff/xutils.h xdiff/xprepare.h xdiff/xdiffi.h xdiff/xemit.h
  
  $(XDIFF_LIB): $(XDIFF_OBJS)
        rm -f $@ && $(AR) rcs $@ $(XDIFF_OBJS)
diff --combined builtin.h
index f71b9629b9c8671dd17296fabac7e1952b3fb9e9,2c5d900acc9e2c6497501c3088efa176b955cb9d..4ac32d7780f3ebaf0bd91b639ed4781a8ab9d0e4
+++ b/builtin.h
@@@ -11,9 -11,9 +11,10 @@@ extern int mailinfo(FILE *in, FILE *out
  extern int split_mbox(const char **mbox, const char *dir, int allow_bare, int nr_prec, int skip);
  extern void stripspace(FILE *in, FILE *out);
  extern int write_tree(unsigned char *sha1, int missing_ok, const char *prefix);
 +extern void prune_packed_objects(int);
  
  extern int cmd_add(int argc, const char **argv, const char *prefix);
+ extern int cmd_annotate(int argc, const char **argv, const char *prefix);
  extern int cmd_apply(int argc, const char **argv, const char *prefix);
  extern int cmd_archive(int argc, const char **argv, const char *prefix);
  extern int cmd_cat_file(int argc, const char **argv, const char *prefix);
diff --combined git.c
index e089b53571cd8776ab208fe14b87af53b7057739,32c356e04467e1564f6e4fd698350a11cb8deeae..235b6fea158b6901e55ac7b0560770359711da7c
--- 1/git.c
--- 2/git.c
+++ b/git.c
@@@ -219,6 -219,7 +219,7 @@@ static void handle_internal_command(in
                int option;
        } commands[] = {
                { "add", cmd_add, RUN_SETUP },
+               { "annotate", cmd_annotate, },
                { "apply", cmd_apply },
                { "archive", cmd_archive },
                { "cat-file", cmd_cat_file, RUN_SETUP },
                { "check-ref-format", cmd_check_ref_format },
                { "commit-tree", cmd_commit_tree, RUN_SETUP },
                { "count-objects", cmd_count_objects, RUN_SETUP },
 -              { "diff", cmd_diff, RUN_SETUP },
 +              { "diff", cmd_diff, RUN_SETUP | USE_PAGER },
                { "diff-files", cmd_diff_files, RUN_SETUP },
                { "diff-index", cmd_diff_index, RUN_SETUP },
                { "diff-stages", cmd_diff_stages, RUN_SETUP },
                { "show", cmd_show, RUN_SETUP | USE_PAGER },
                { "stripspace", cmd_stripspace },
                { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP },
 -              { "tar-tree", cmd_tar_tree, RUN_SETUP },
 +              { "tar-tree", cmd_tar_tree },
                { "unpack-objects", cmd_unpack_objects, RUN_SETUP },
                { "update-index", cmd_update_index, RUN_SETUP },
                { "update-ref", cmd_update_ref, RUN_SETUP },
diff --combined gitweb/gitweb.perl
index bc8d8eb238c41a682924c6121e494bd7a1b5153e,34b88ce8ebf563df0d0e0602fb6701b06849a2fe..9eb19bb82ed843c6de6040c28cbbbff8e1ea9595
@@@ -41,21 -41,26 +41,31 @@@ our $home_link_str = "++GITWEB_HOME_LIN
  # replace this with something more descriptive for clearer bookmarks
  our $site_name = "++GITWEB_SITENAME++" || $ENV{'SERVER_NAME'} || "Untitled";
  
+ # filename of html text to include at top of each page
+ our $site_header = "++GITWEB_SITE_HEADER++";
  # html text to include at home page
  our $home_text = "++GITWEB_HOMETEXT++";
+ # filename of html text to include at bottom of each page
+ our $site_footer = "++GITWEB_SITE_FOOTER++";
+ # URI of stylesheets
+ our @stylesheets = ("++GITWEB_CSS++");
+ our $stylesheet;
+ # default is not to define style sheet, but it can be overwritten later
+ undef $stylesheet;
  
 -# URI of GIT logo
 +# URI of default stylesheet
 +our $stylesheet = "++GITWEB_CSS++";
 +# URI of GIT logo (72x27 size)
  our $logo = "++GITWEB_LOGO++";
  # URI of GIT favicon, assumed to be image/png type
  our $favicon = "++GITWEB_FAVICON++";
  
 -our $githelp_url = "http://git.or.cz/";
 -our $githelp_label = "git homepage";
 +# 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_label = "git homepage";
  
  # source of projects list
  our $projects_list = "++GITWEB_LIST++";
@@@ -93,66 -98,21 +103,66 @@@ our %feature = 
        #
        # use gitweb_check_feature(<feature>) to check if <feature> is enabled
  
 +      # Enable the 'blame' blob view, showing the last commit that modified
 +      # each line in the file. This can be very CPU-intensive.
 +
 +      # To enable system wide have in $GITWEB_CONFIG
 +      # $feature{'blame'}{'default'} = [1];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'blame'}{'override'} = 1;
 +      # and in project config gitweb.blame = 0|1;
        'blame' => {
                'sub' => \&feature_blame,
                'override' => 0,
                'default' => [0]},
  
 +      # Enable the 'snapshot' link, providing a compressed tarball of any
 +      # tree. This can potentially generate high traffic if you have large
 +      # project.
 +
 +      # To disable system wide have in $GITWEB_CONFIG
 +      # $feature{'snapshot'}{'default'} = [undef];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'blame'}{'override'} = 1;
 +      # and in project config gitweb.snapshot = none|gzip|bzip2;
        'snapshot' => {
                'sub' => \&feature_snapshot,
                'override' => 0,
                #         => [content-encoding, suffix, program]
                'default' => ['x-gzip', 'gz', 'gzip']},
  
 +      # 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.
 +
 +      # To enable system wide have in $GITWEB_CONFIG
 +      # $feature{'pickaxe'}{'default'} = [1];
 +      # To have project specific config enable override in $GITWEB_CONFIG
 +      # $feature{'pickaxe'}{'override'} = 1;
 +      # and in project config gitweb.pickaxe = 0|1;
        'pickaxe' => {
                'sub' => \&feature_pickaxe,
                'override' => 0,
                'default' => [1]},
 +
 +      # Make gitweb use an alternative format of the URLs which can be
 +      # more readable and natural-looking: project name is embedded
 +      # directly in the path and the query string contains other
 +      # auxiliary information. All gitweb installations recognize
 +      # URL in either format; this configures in which formats gitweb
 +      # generates links.
 +
 +      # To enable system wide have in $GITWEB_CONFIG
 +      # $feature{'pathinfo'}{'default'} = [1];
 +      # Project specific override is not supported.
 +
 +      # Note that you will need to change the default location of CSS,
 +      # favicon, logo and possibly other files to an absolute URL. Also,
 +      # if gitweb.cgi serves as your indexfile, you will need to force
 +      # $my_uri to contain the script name in your $GITWEB_CONFIG.
 +      'pathinfo' => {
 +              'override' => 0,
 +              'default' => [0]},
  );
  
  sub gitweb_check_feature {
                $feature{$name}{'override'},
                @{$feature{$name}{'default'}});
        if (!$override) { return @defaults; }
 +      if (!defined $sub) {
 +              warn "feature $name is not overrideable";
 +              return @defaults;
 +      }
        return $sub->(@defaults);
  }
  
 -# To enable system wide have in $GITWEB_CONFIG
 -# $feature{'blame'}{'default'} = [1];
 -# To have project specific config enable override in $GITWEB_CONFIG
 -# $feature{'blame'}{'override'} = 1;
 -# and in project config gitweb.blame = 0|1;
 -
  sub feature_blame {
        my ($val) = git_get_project_config('blame', '--bool');
  
        return $_[0];
  }
  
 -# To disable system wide have in $GITWEB_CONFIG
 -# $feature{'snapshot'}{'default'} = [undef];
 -# To have project specific config enable override in $GITWEB_CONFIG
 -# $feature{'blame'}{'override'} = 1;
 -# and in project config  gitweb.snapshot = none|gzip|bzip2
 -
  sub feature_snapshot {
        my ($ctype, $suffix, $command) = @_;
  
@@@ -205,6 -173,12 +215,6 @@@ sub gitweb_have_snapshot 
        return $have_snapshot;
  }
  
 -# To enable system wide have in $GITWEB_CONFIG
 -# $feature{'pickaxe'}{'default'} = [1];
 -# To have project specific config enable override in $GITWEB_CONFIG
 -# $feature{'pickaxe'}{'override'} = 1;
 -# and in project config gitweb.pickaxe = 0|1;
 -
  sub feature_pickaxe {
        my ($val) = git_get_project_config('pickaxe', '--bool');
  
        return ($_[0]);
  }
  
+ # checking HEAD file with -e is fragile if the repository was
+ # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
+ # and then pruned.
+ sub check_head_link {
+       my ($dir) = @_;
+       my $headfile = "$dir/HEAD";
+       return ((-e $headfile) ||
+               (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
+ }
+ sub check_export_ok {
+       my ($dir) = @_;
+       return (check_head_link($dir) &&
+               (!$export_ok || -e "$dir/$export_ok"));
+ }
  # rename detection options for git-diff and git-diff-tree
  # - default is '-M', with the cost proportional to
  #   (number of removed files) * (number of new files).
@@@ -249,7 -239,7 +275,7 @@@ our $project = $cgi->param('p')
  if (defined $project) {
        if (!validate_pathname($project) ||
            !(-d "$projectroot/$project") ||
-           !(-e "$projectroot/$project/HEAD") ||
+           !check_head_link("$projectroot/$project") ||
            ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
            ($strict_export && !project_in_list($project))) {
                undef $project;
@@@ -326,7 -316,7 +352,7 @@@ sub evaluate_path_info 
        # find which part of PATH_INFO is project
        $project = $path_info;
        $project =~ s,/+$,,;
-       while ($project && !-e "$projectroot/$project/HEAD") {
+       while ($project && !check_head_link("$projectroot/$project")) {
                $project =~ s,/*[^/]*$,,;
        }
        # validate project
@@@ -412,10 -402,6 +438,10 @@@ exit
  
  sub href(%) {
        my %params = @_;
 +      my $href = $my_uri;
 +
 +      # XXX: Warning: If you touch this, check the search form for updating,
 +      # too.
  
        my @mapping = (
                project => "p",
  
        $params{'project'} = $project unless exists $params{'project'};
  
 +      my ($use_pathinfo) = gitweb_check_feature('pathinfo');
 +      if ($use_pathinfo) {
 +              # use PATH_INFO for project name
 +              $href .= "/$params{'project'}" if defined $params{'project'};
 +              delete $params{'project'};
 +
 +              # Summary just uses the project path URL
 +              if (defined $params{'action'} && $params{'action'} eq 'summary') {
 +                      delete $params{'action'};
 +              }
 +      }
 +
 +      # now encode the parameters explicitly
        my @result = ();
        for (my $i = 0; $i < @mapping; $i += 2) {
                my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
                        push @result, $symbol . "=" . esc_param($params{$name});
                }
        }
 -      return "$my_uri?" . join(';', @result);
 +      $href .= "?" . join(';', @result) if scalar @result;
 +
 +      return $href;
  }
  
  
@@@ -496,12 -467,6 +522,12 @@@ sub validate_refname 
        return $input;
  }
  
 +# very thin wrapper for decode("utf8", $str, Encode::FB_DEFAULT);
 +sub to_utf8 {
 +      my $str = shift;
 +      return decode("utf8", $str, Encode::FB_DEFAULT);
 +}
 +
  # quote unsafe chars, but keep the slash, even when it's not
  # correct, but quoted slashes look too horrible in bookmarks
  sub esc_param {
@@@ -524,7 -489,7 +550,7 @@@ sub esc_url 
  # replace invalid utf8 character with SUBSTITUTION sequence
  sub esc_html {
        my $str = shift;
 -      $str = decode("utf8", $str, Encode::FB_DEFAULT);
 +      $str = to_utf8($str);
        $str = escapeHTML($str);
        $str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
        $str =~ s/\033/^[/g; # "escape" ESCAPE (\e) character (e.g. commit 20a3847d8a5032ce41f90dcc68abfb36e6fee9b1)
@@@ -727,7 -692,7 +753,7 @@@ sub format_subject_html 
  
        if (length($short) < length($long)) {
                return $cgi->a({-href => $href, -class => "list subject",
 -                              -title => decode("utf8", $long, Encode::FB_DEFAULT)},
 +                              -title => to_utf8($long)},
                       esc_html($short) . $extra);
        } else {
                return $cgi->a({-href => $href, -class => "list subject"},
@@@ -878,8 -843,7 +904,7 @@@ sub git_get_projects_list 
  
                                my $subdir = substr($File::Find::name, $pfxlen + 1);
                                # we check related file in $projectroot
-                               if (-e "$projectroot/$subdir/HEAD" && (!$export_ok ||
-                                   -e "$projectroot/$subdir/$export_ok")) {
+                               if (check_export_ok("$projectroot/$subdir")) {
                                        push @list, { path => $subdir };
                                        $File::Find::prune = 1;
                                }
                        if (!defined $path) {
                                next;
                        }
-                       if (-e "$projectroot/$path/HEAD" && (!$export_ok ||
-                           -e "$projectroot/$path/$export_ok")) {
+                       if (check_export_ok("$projectroot/$path")) {
                                my $pr = {
                                        path => $path,
 -                                      owner => decode("utf8", $owner, Encode::FB_DEFAULT),
 +                                      owner => to_utf8($owner),
                                };
                                push @list, $pr
                        }
@@@ -933,7 -896,7 +957,7 @@@ sub git_get_project_owner 
                        $pr = unescape($pr);
                        $ow = unescape($ow);
                        if ($pr eq $project) {
 -                              $owner = decode("utf8", $ow, Encode::FB_DEFAULT);
 +                              $owner = to_utf8($ow);
                                last;
                        }
                }
@@@ -1011,6 -974,9 +1035,9 @@@ sub parse_date 
        $date{'hour_local'} = $hour;
        $date{'minute_local'} = $min;
        $date{'tz_local'} = $tz;
+       $date{'iso-tz'} = sprintf ("%04d-%02d-%02d %02d:%02d:%02d %s",
+                                  1900+$year, $mon+1, $mday,
+                                  $hour, $min, $sec, $tz);
        return %date;
  }
  
@@@ -1059,11 -1025,12 +1086,11 @@@ sub parse_commit 
        if (defined $commit_text) {
                @commit_lines = @$commit_text;
        } else {
 -              $/ = "\0";
 +              local $/ = "\0";
                open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", "--max-count=1", $commit_id
                        or return;
                @commit_lines = split '\n', <$fd>;
                close $fd or return;
 -              $/ = "\n";
                pop @commit_lines;
        }
        my $header = shift @commit_lines;
                        last;
                }
        }
 +      if ($co{'title'} eq "") {
 +              $co{'title'} = $co{'title_short'} = '(no commit message)';
 +      }
        # remove added spaces
        foreach my $line (@commit_lines) {
                $line =~ s/^    //;
@@@ -1297,7 -1261,7 +1324,7 @@@ sub get_file_owner 
        }
        my $owner = $gcos;
        $owner =~ s/[,;].*$//;
 -      return decode("utf8", $owner, Encode::FB_DEFAULT);
 +      return to_utf8($owner);
  }
  
  ## ......................................................................
@@@ -1416,8 -1380,17 +1443,17 @@@ sub git_header_html 
  <meta name="generator" content="gitweb/$version git/$git_version"/>
  <meta name="robots" content="index, nofollow"/>
  <title>$title</title>
- <link rel="stylesheet" type="text/css" href="$stylesheet"/>
  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";
+       } else {
+               foreach my $stylesheet (@stylesheets) {
+                       next unless $stylesheet;
+                       print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
+               }
+       }
        if (defined $project) {
                printf('<link rel="alternate" title="%s log" '.
                       'href="%s" type="application/rss+xml"/>'."\n",
        }
  
        print "</head>\n" .
-             "<body>\n" .
-             "<div class=\"page_header\">\n" .
+             "<body>\n";
+       if (-f $site_header) {
+               open (my $fd, $site_header);
+               print <$fd>;
+               close $fd;
+       }
+       print "<div class=\"page_header\">\n" .
 -            "<a href=\"" . esc_html($githelp_url) .
 -            "\" title=\"" . esc_html($githelp_label) .
 -            "\">" .
 -            "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
 -            "</a>\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));
                }
                $cgi->param("a", "search");
                $cgi->param("h", $search_hash);
 +              $cgi->param("p", $project);
                print $cgi->startform(-method => "get", -action => $my_uri) .
                      "<div class=\"search\">\n" .
                      $cgi->hidden(-name => "p") . "\n" .
@@@ -1488,8 -1469,15 +1531,15 @@@ sub git_footer_html 
                print $cgi->a({-href => href(project=>undef, action=>"project_index"),
                              -class => "rss_logo"}, "TXT") . "\n";
        }
-       print "</div>\n" .
-             "</body>\n" .
+       print "</div>\n" ;
+       if (-f $site_footer) {
+               open (my $fd, $site_footer);
+               print <$fd>;
+               close $fd;
+       }
+       print "</body>\n" .
              "</html>";
  }
  
@@@ -1614,16 -1602,17 +1664,16 @@@ sub git_print_page_path 
        my $type = shift;
        my $hb = shift;
  
 -      if (!defined $name) {
 -              print "<div class=\"page_path\">/</div>\n";
 -      } else {
 +
 +      print "<div class=\"page_path\">";
 +      print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
 +                    -title => 'tree root'}, "[$project]");
 +      print " / ";
 +      if (defined $name) {
                my @dirname = split '/', $name;
                my $basename = pop @dirname;
                my $fullname = '';
  
 -              print "<div class=\"page_path\">";
 -              print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
 -                            -title => 'tree root'}, "[$project]");
 -              print " / ";
                foreach my $dir (@dirname) {
                        $fullname .= ($fullname ? '/' : '') . $dir;
                        print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
                        print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
                                                     hash_base=>$hb),
                                      -title => $name}, esc_html($basename));
 +                      print " / ";
                } else {
                        print esc_html($basename);
                }
 -              print "<br/></div>\n";
        }
 +      print "<br/></div>\n";
  }
  
  # sub git_print_log (\@;%) {
@@@ -1721,13 -1709,13 +1771,13 @@@ sub git_print_tree_entry 
        if ($t->{'type'} eq "blob") {
                print "<td class=\"list\">" .
                        $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
 -                                             file_name=>"$basedir$t->{'name'}", %base_key),
 -                               -class => "list"}, esc_html($t->{'name'})) . "</td>\n";
 +                                             file_name=>"$basedir$t->{'name'}", %base_key),
 +                              -class => "list"}, esc_html($t->{'name'})) . "</td>\n";
                print "<td class=\"link\">";
                if ($have_blame) {
                        print $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
 -                                                   file_name=>"$basedir$t->{'name'}", %base_key)},
 -                                    "blame");
 +                                                         file_name=>"$basedir$t->{'name'}", %base_key)},
 +                                          "blame");
                }
                if (defined $hash_base) {
                        if ($have_blame) {
                }
                print " | " .
                        $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
 -                                             file_name=>"$basedir$t->{'name'}")},
 -                              "raw");
 +                                             file_name=>"$basedir$t->{'name'}")},
 +                              "raw");
                print "</td>\n";
  
        } elsif ($t->{'type'} eq "tree") {
@@@ -1808,7 -1796,7 +1858,7 @@@ sub git_difftree_body 
                        print "<td>";
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff{'file'}),
 -                                     -class => "list"}, esc_html($diff{'file'}));
 +                                    -class => "list"}, esc_html($diff{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
                                print " | ";
                        }
                        print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
 -                                                   file_name=>$diff{'file'})},
 -                                    "blame") . " | ";
 +                                                   file_name=>$diff{'file'})},
 +                                    "blame") . " | ";
                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
 -                                                   file_name=>$diff{'file'})},
 -                                    "history");
 +                                                   file_name=>$diff{'file'})},
 +                                    "history");
                        print "</td>\n";
  
                } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
                        }
                        print "<td>";
                        print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 -                                                   hash_base=>$hash, file_name=>$diff{'file'}),
 -                                     -class => "list"}, esc_html($diff{'file'}));
 +                                                   hash_base=>$hash, file_name=>$diff{'file'}),
 +                                    -class => "list"}, esc_html($diff{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chnge</td>\n";
                        print "<td class=\"link\">";
                                        print $cgi->a({-href => "#patch$patchno"}, "patch");
                                } else {
                                        print $cgi->a({-href => href(action=>"blobdiff",
 -                                                                   hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 -                                                                   hash_base=>$hash, hash_parent_base=>$parent,
 -                                                                   file_name=>$diff{'file'})},
 -                                                    "diff");
 +                                                                   hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                                   hash_base=>$hash, hash_parent_base=>$parent,
 +                                                                   file_name=>$diff{'file'})},
 +                                                    "diff");
                                }
                                print " | ";
                        }
                        print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
 -                                                   file_name=>$diff{'file'})},
 -                                    "blame") . " | ";
 +                                                   file_name=>$diff{'file'})},
 +                                    "blame") . " | ";
                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
 -                                                   file_name=>$diff{'file'})},
 -                                    "history");
 +                                                   file_name=>$diff{'file'})},
 +                                    "history");
                        print "</td>\n";
  
                } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
                                        print $cgi->a({-href => "#patch$patchno"}, "patch");
                                } else {
                                        print $cgi->a({-href => href(action=>"blobdiff",
 -                                                                   hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 -                                                                   hash_base=>$hash, hash_parent_base=>$parent,
 -                                                                   file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
 -                                                    "diff");
 +                                                                   hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                                   hash_base=>$hash, hash_parent_base=>$parent,
 +                                                                   file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
 +                                                    "diff");
                                }
                                print " | ";
                        }
                        print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
 -                                                   file_name=>$diff{'from_file'})},
 -                                    "blame") . " | ";
 +                                                   file_name=>$diff{'from_file'})},
 +                                    "blame") . " | ";
                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
 -                                                   file_name=>$diff{'from_file'})},
 -                                    "history");
 +                                                  file_name=>$diff{'from_file'})},
 +                                    "history");
                        print "</td>\n";
  
                } # we should not encounter Unmerged (U) or Unknown (X) status
@@@ -1976,14 -1964,14 +2026,14 @@@ sub git_patchset_body 
                                print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
                                                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
 -                                            $diffinfo->{'to_id'}) . "(new)" .
 +                                            $diffinfo->{'to_id'}) . " (new)" .
                                      "</div>\n"; # class="diff_info"
  
                        } elsif ($diffinfo->{'status'} eq "D") { # deleted
                                print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
                                      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
                                                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
 -                                            $diffinfo->{'from_id'}) . "(deleted)" .
 +                                            $diffinfo->{'from_id'}) . " (deleted)" .
                                      "</div>\n"; # class="diff_info"
  
                        } elsif ($diffinfo->{'status'} eq "R" || # renamed
@@@ -2087,10 -2075,8 +2137,10 @@@ sub git_shortlog_body 
                print "</td>\n" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
 -                    $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . " | " .
 -                    $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
 +                    $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
 +              if (gitweb_have_snapshot()) {
 +                      print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
 +              }
                print "</td>\n" .
                      "</tr>\n";
        }
@@@ -2518,7 -2504,8 +2568,8 @@@ sub git_blame2 
        if ($ftype !~ "blob") {
                die_error("400 Bad Request", "Object is not a blob");
        }
-       open ($fd, "-|", git_cmd(), "blame", '-l', '--', $file_name, $hash_base)
+       open ($fd, "-|", git_cmd(), "blame", '-p', '--',
+             $file_name, $hash_base)
                or die_error(undef, "Open git-blame failed");
        git_header_html();
        my $formats_nav =
  <table class="blame">
  <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
  HTML
-       while (<$fd>) {
-               /^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
-               my $full_rev = $1;
+       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 = $_;
                my $rev = substr($full_rev, 0, 8);
-               my $lineno = $2;
-               my $data = $3;
-               if (!defined $last_rev) {
-                       $last_rev = $full_rev;
-               } elsif ($last_rev ne $full_rev) {
-                       $last_rev = $full_rev;
+               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";
-               print "<td class=\"sha1\">" .
-                       $cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)},
-                               esc_html($rev)) . "</td>\n";
-               print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" .
-                     esc_html($lineno) . "</a></td>\n";
+               if ($group_size) {
+                       print "<td class=\"sha1\"";
+                       print " title=\"$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";
+               }
+               my $blamed = href(action => 'blame',
+                                 file_name => $meta->{'filename'},
+                                 hash_base => $full_rev);
+               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";
        }
@@@ -2833,7 -2847,7 +2911,7 @@@ sub git_tree 
        my $refs = git_get_references();
        my $ref = format_ref_marker($refs, $hash_base);
        git_header_html();
 -      my $base = "";
 +      my $basedir = '';
        my ($have_blame) = gitweb_check_feature('blame');
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
                my @views_nav = ();
                        # FIXME: Should be available when we have no hash base as well.
                        push @views_nav,
                                $cgi->a({-href => href(action=>"snapshot", hash=>$hash)},
 -                                      "snapshot");
 +                                      "snapshot");
                }
                git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
                print "<div class=\"title\">$hash</div>\n";
        }
        if (defined $file_name) {
 -              $base = esc_html("$file_name/");
 +              $basedir = $file_name;
 +              if ($basedir ne '' && substr($basedir, -1) ne '/') {
 +                      $basedir .= '/';
 +              }
        }
        git_print_page_path($file_name, 'tree', $hash_base);
        print "<div class=\"page_body\">\n";
        print "<table cellspacing=\"0\">\n";
        my $alternate = 1;
 +      # '..' (top directory) link if possible
 +      if (defined $hash_base &&
 +          defined $file_name && $file_name =~ m![^/]+$!) {
 +              if ($alternate) {
 +                      print "<tr class=\"dark\">\n";
 +              } else {
 +                      print "<tr class=\"light\">\n";
 +              }
 +              $alternate ^= 1;
 +
 +              my $up = $file_name;
 +              $up =~ s!/?[^/]+$!!;
 +              undef $up unless $up;
 +              # based on git_print_tree_entry
 +              print '<td class="mode">' . mode_str('040000') . "</td>\n";
 +              print '<td class="list">';
 +              print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
 +                                           file_name=>$up)},
 +                            "..");
 +              print "</td>\n";
 +              print "<td class=\"link\"></td>\n";
 +
 +              print "</tr>\n";
 +      }
        foreach my $line (@entries) {
                my %t = parse_ls_tree_line($line, -z => 1);
  
                }
                $alternate ^= 1;
  
 -              git_print_tree_entry(\%t, $base, $hash_base, $have_blame);
 +              git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
  
                print "</tr>\n";
        }
@@@ -3035,7 -3022,7 +3113,7 @@@ sub git_commit 
                                "blame");
        }
        git_header_html(undef, $expires);
 -      git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
 +      git_print_page_nav('commit', '',
                           $hash, $co{'tree'}, $hash,
                           join (' | ', @views_nav));
  
@@@ -3678,7 -3665,7 +3756,7 @@@ XM
                      "<![CDATA[\n";
                my $comment = $co{'comment'};
                foreach my $line (@$comment) {
 -                      $line = decode("utf8", $line, Encode::FB_DEFAULT);
 +                      $line = to_utf8($line);
                        print "$line<br/>\n";
                }
                print "<br/>\n";
                                next;
                        }
                        my $file = esc_html(unquote($7));
 -                      $file = decode("utf8", $file, Encode::FB_DEFAULT);
 +                      $file = to_utf8($file);
                        print "$file<br/>\n";
                }
                print "]]>\n" .