Merge branch 'maint'
authorJunio C Hamano <junkio@cox.net>
Tue, 15 May 2007 01:50:01 +0000 (18:50 -0700)
committerJunio C Hamano <junkio@cox.net>
Tue, 15 May 2007 01:50:01 +0000 (18:50 -0700)
* maint:
Prepare for 1.5.1.5 Release Notes
gitweb: Add a few comments about %feature hash
git-am: Clean up the asciidoc documentation
Documentation: format-patch has no --mbox option
builtin-log.c: Fix typo in comment
Fix git-clone buglet for remote case.

1  2 
builtin-log.c
gitweb/gitweb.perl
diff --combined builtin-log.c
index 38bf52f1006fd991175621734e11e4d23f4b23de,51294900dd12bac253a1f8a67f093f1a1dfbcb4b..37447123f924149f012a298eaf31dacb0c87b724
  #include "builtin.h"
  #include "tag.h"
  #include "reflog-walk.h"
 +#include "patch-ids.h"
 +#include "refs.h"
  
  static int default_show_root = 1;
  
  /* this is in builtin-diff.c */
  void add_head(struct rev_info *revs);
  
 +static void add_name_decoration(const char *prefix, const char *name, struct object *obj)
 +{
 +      int plen = strlen(prefix);
 +      int nlen = strlen(name);
 +      struct name_decoration *res = xmalloc(sizeof(struct name_decoration) + plen + nlen);
 +      memcpy(res->name, prefix, plen);
 +      memcpy(res->name + plen, name, nlen + 1);
 +      res->next = add_decoration(&name_decoration, obj, res);
 +}
 +
 +static int add_ref_decoration(const char *refname, const unsigned char *sha1, int flags, void *cb_data)
 +{
 +      struct object *obj = parse_object(sha1);
 +      if (!obj)
 +              return 0;
 +      add_name_decoration("", refname, obj);
 +      while (obj->type == OBJ_TAG) {
 +              obj = ((struct tag *)obj)->tagged;
 +              if (!obj)
 +                      break;
 +              add_name_decoration("tag: ", refname, obj);
 +      }
 +      return 0;
 +}
 +
  static void cmd_log_init(int argc, const char **argv, const char *prefix,
                      struct rev_info *rev)
  {
        int i;
 +      int decorate = 0;
  
        rev->abbrev = DEFAULT_ABBREV;
        rev->commit_format = CMIT_FMT_DEFAULT;
                                git_log_output_encoding = xstrdup(arg);
                        else
                                git_log_output_encoding = "";
 -              }
 -              else
 +              } else if (!strcmp(arg, "--decorate")) {
 +                      if (!decorate)
 +                              for_each_ref(add_ref_decoration, NULL);
 +                      decorate = 1;
 +              } else
                        die("unrecognized argument: %s", arg);
        }
  }
@@@ -364,12 -333,25 +364,12 @@@ static int reopen_stdout(struct commit 
  
  }
  
 -static int get_patch_id(struct commit *commit, struct diff_options *options,
 -              unsigned char *sha1)
 -{
 -      if (commit->parents)
 -              diff_tree_sha1(commit->parents->item->object.sha1,
 -                             commit->object.sha1, "", options);
 -      else
 -              diff_root_tree_sha1(commit->object.sha1, "", options);
 -      diffcore_std(options);
 -      return diff_flush_patch_id(options, sha1);
 -}
 -
 -static void get_patch_ids(struct rev_info *rev, struct diff_options *options, const char *prefix)
 +static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids, const char *prefix)
  {
        struct rev_info check_rev;
        struct commit *commit;
        struct object *o1, *o2;
        unsigned flags1, flags2;
 -      unsigned char sha1[20];
  
        if (rev->pending.nr != 2)
                die("Need exactly one range.");
        if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING))
                die("Not a range.");
  
 -      diff_setup(options);
 -      options->recursive = 1;
 -      if (diff_setup_done(options) < 0)
 -              die("diff_setup_done failed");
 +      init_patch_ids(ids);
  
        /* given a range a..b get all patch ids for b..a */
        init_revisions(&check_rev, prefix);
                if (commit->parents && commit->parents->next)
                        continue;
  
 -              if (!get_patch_id(commit, options, sha1))
 -                      created_object(sha1, xcalloc(1, sizeof(struct object)));
 +              add_commit_patch_id(commit, ids);
        }
  
        /* reset for next revision walk */
@@@ -431,11 -417,10 +431,11 @@@ int cmd_format_patch(int argc, const ch
        int numbered = 0;
        int start_number = -1;
        int keep_subject = 0;
 +      int subject_prefix = 0;
        int ignore_if_in_upstream = 0;
        int thread = 0;
        const char *in_reply_to = NULL;
 -      struct diff_options patch_id_opts;
 +      struct patch_ids ids;
        char *add_signoff = NULL;
        char message_id[1024];
        char ref_message_id[1024];
  
        /*
         * Parse the arguments before setup_revisions(), or something
-        * like "git fmt-patch -o a123 HEAD^.." may fail; a123 is
+        * like "git format-patch -o a123 HEAD^.." may fail; a123 is
         * possibly a valid SHA1.
         */
        for (i = 1, j = 1; i < argc; i++) {
                        if (i == argc)
                                die("Need a Message-Id for --in-reply-to");
                        in_reply_to = argv[i];
 -              }
 -              else if (!prefixcmp(argv[i], "--suffix="))
 +              } else if (!prefixcmp(argv[i], "--subject-prefix=")) {
 +                      subject_prefix = 1;
 +                      rev.subject_prefix = argv[i] + 17;
 +              } else if (!prefixcmp(argv[i], "--suffix="))
                        fmt_patch_suffix = argv[i] + 9;
                else
                        argv[j++] = argv[i];
                start_number = 1;
        if (numbered && keep_subject)
                die ("-n and -k are mutually exclusive.");
 +      if (keep_subject && subject_prefix)
 +              die ("--subject-prefix and -k are mutually exclusive.");
  
        argc = setup_revisions(argc, argv, &rev, "HEAD");
        if (argc > 1)
        }
  
        if (ignore_if_in_upstream)
 -              get_patch_ids(&rev, &patch_id_opts, prefix);
 +              get_patch_ids(&rev, &ids, prefix);
  
        if (!use_stdout)
                realstdout = fdopen(dup(1), "w");
  
        prepare_revision_walk(&rev);
        while ((commit = get_revision(&rev)) != NULL) {
 -              unsigned char sha1[20];
 -
                /* ignore merges */
                if (commit->parents && commit->parents->next)
                        continue;
  
                if (ignore_if_in_upstream &&
 -                              !get_patch_id(commit, &patch_id_opts, sha1) &&
 -                              lookup_object(sha1))
 +                              has_commit_patch_id(commit, &ids))
                        continue;
  
                nr++;
                        fclose(stdout);
        }
        free(list);
 +      if (ignore_if_in_upstream)
 +              free_patch_ids(&ids);
        return 0;
  }
  
@@@ -664,7 -646,7 +664,7 @@@ static const char cherry_usage[] 
  int cmd_cherry(int argc, const char **argv, const char *prefix)
  {
        struct rev_info revs;
 -      struct diff_options patch_id_opts;
 +      struct patch_ids ids;
        struct commit *commit;
        struct commit_list *list = NULL;
        const char *upstream;
                        return 0;
        }
  
 -      get_patch_ids(&revs, &patch_id_opts, prefix);
 +      get_patch_ids(&revs, &ids, prefix);
  
        if (limit && add_pending_commit(limit, &revs, UNINTERESTING))
                die("Unknown commit %s", limit);
        }
  
        while (list) {
 -              unsigned char sha1[20];
                char sign = '+';
  
                commit = list->item;
 -              if (!get_patch_id(commit, &patch_id_opts, sha1) &&
 -                  lookup_object(sha1))
 +              if (has_commit_patch_id(commit, &ids))
                        sign = '-';
  
                if (verbose) {
                list = list->next;
        }
  
 +      free_patch_ids(&ids);
        return 0;
  }
diff --combined gitweb/gitweb.perl
index c2eeca9fa0bdbaa514cda5dd51f28376b2793be9,7b520f5fc1f292d66a1958fbe09b44debc8bab7b..a13043deea5d7569ae15763b8de27bcafe14a1c4
@@@ -19,7 -19,7 +19,7 @@@ use File::Basename qw(basename)
  binmode STDOUT, ':utf8';
  
  BEGIN {
 -      CGI->compile() if $ENV{MOD_PERL};
 +      CGI->compile() if $ENV{'MOD_PERL'};
  }
  
  our $cgi = new CGI;
@@@ -71,10 -71,6 +71,10 @@@ our $logo_label = "git homepage"
  # source of projects list
  our $projects_list = "++GITWEB_LIST++";
  
 +# default order of projects list
 +# valid values are none, project, descr, owner, and age
 +our $default_projects_order = "project";
 +
  # show repository only if this file exists
  # (only effective if this variable evaluates to true)
  our $export_ok = "++GITWEB_EXPORT_OK++";
@@@ -102,10 -98,13 +102,13 @@@ our %feature = 
        #       'override' => allow-override (boolean),
        #       'default' => [ default options...] (array reference)}
        #
-       # if feature is overridable (it means that allow-override has true value,
+       # if feature is overridable (it means that allow-override has true value),
        # then feature-sub will be called with default options as parameters;
        # 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
+       #
        # use gitweb_check_feature(<feature>) to check if <feature> is enabled
  
        # Enable the 'blame' blob view, showing the last commit that modified
  
        # Enable text search, which will list the commits which match author,
        # committer or commit text to a given string.  Enabled by default.
+       # Project specific override is not supported.
        'search' => {
                'override' => 0,
                'default' => [1]},
        # projects matching $projname/*.git will not be shown in the main
        # projects list, instead a '+' mark will be added to $projname
        # there and a 'forks' view will be enabled for the project, listing
 -      # all the forks. This feature is supported only if project list
 -      # is taken from a directory, not file.
 +      # all the forks. If project list is taken from a file, forks have
 +      # to be listed after the main project.
  
        # To enable system wide have in $GITWEB_CONFIG
        # $feature{'forks'}{'default'} = [1];
@@@ -458,16 -458,10 +462,16 @@@ my %actions = 
        "project_index" => \&git_project_index,
  );
  
 -if (defined $project) {
 -      $action ||= 'summary';
 -} else {
 -      $action ||= 'project_list';
 +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(undef, "Unknown action");
@@@ -728,9 -722,7 +732,9 @@@ sub chop_str 
  sub age_class {
        my $age = shift;
  
 -      if ($age < 60*60*2) {
 +      if (!defined $age) {
 +              return "noage";
 +      } elsif ($age < 60*60*2) {
                return "age0";
        } elsif ($age < 60*60*24*2) {
                return "age1";
@@@ -905,34 -897,19 +909,34 @@@ sub format_subject_html 
  sub format_diff_line {
        my $line = shift;
        my ($from, $to) = @_;
 -      my $char = substr($line, 0, 1);
        my $diff_class = "";
  
        chomp $line;
  
 -      if ($char eq '+') {
 -              $diff_class = " add";
 -      } elsif ($char eq "-") {
 -              $diff_class = " rem";
 -      } elsif ($char eq "@") {
 -              $diff_class = " chunk_header";
 -      } elsif ($char eq "\\") {
 -              $diff_class = " incomplete";
 +      if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
 +              # combined diff
 +              my $prefix = substr($line, 0, scalar @{$from->{'href'}});
 +              if ($line =~ m/^\@{3}/) {
 +                      $diff_class = " chunk_header";
 +              } elsif ($line =~ m/^\\/) {
 +                      $diff_class = " incomplete";
 +              } elsif ($prefix =~ tr/+/+/) {
 +                      $diff_class = " add";
 +              } elsif ($prefix =~ tr/-/-/) {
 +                      $diff_class = " rem";
 +              }
 +      } else {
 +              # assume ordinary diff
 +              my $char = substr($line, 0, 1);
 +              if ($char eq '+') {
 +                      $diff_class = " add";
 +              } elsif ($char eq '-') {
 +                      $diff_class = " rem";
 +              } elsif ($char eq '@') {
 +                      $diff_class = " chunk_header";
 +              } elsif ($char eq "\\") {
 +                      $diff_class = " incomplete";
 +              }
        }
        $line = untabify($line);
        if ($from && $to && $line =~ m/^\@{2} /) {
                $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
                        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
                return "<div class=\"diff$diff_class\">$line</div>\n";
 +      } elsif ($from && $to && $line =~ m/^\@{3}/) {
 +              my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
 +              my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
 +
 +              @from_text = split(' ', $ranges);
 +              for (my $i = 0; $i < @from_text; ++$i) {
 +                      ($from_start[$i], $from_nlines[$i]) =
 +                              (split(',', substr($from_text[$i], 1)), 0);
 +              }
 +
 +              $to_text   = pop @from_text;
 +              $to_start  = pop @from_start;
 +              $to_nlines = pop @from_nlines;
 +
 +              $line = "<span class=\"chunk_info\">$prefix ";
 +              for (my $i = 0; $i < @from_text; ++$i) {
 +                      if ($from->{'href'}[$i]) {
 +                              $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
 +                                                -class=>"list"}, $from_text[$i]);
 +                      } else {
 +                              $line .= $from_text[$i];
 +                      }
 +                      $line .= " ";
 +              }
 +              if ($to->{'href'}) {
 +                      $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
 +                                        -class=>"list"}, $to_text);
 +              } else {
 +                      $line .= $to_text;
 +              }
 +              $line .= " $prefix</span>" .
 +                       "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
 +              return "<div class=\"diff$diff_class\">$line</div>\n";
        }
        return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
  }
@@@ -1062,11 -1006,6 +1066,11 @@@ sub git_get_hash_by_path 
        my $line = <$fd>;
        close $fd or return undef;
  
 +      if (!defined $line) {
 +              # there is no tree or hash given by $path at $base
 +              return undef;
 +      }
 +
        #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
        $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
        if (defined $type && $type ne $2) {
        return $3;
  }
  
 +# get path of entry with given hash at given tree-ish (ref)
 +# used to get 'from' filename for combined diff (merge commit) for renames
 +sub git_get_path_by_hash {
 +      my $base = shift || return;
 +      my $hash = shift || return;
 +
 +      local $/ = "\0";
 +
 +      open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
 +              or return undef;
 +      while (my $line = <$fd>) {
 +              chomp $line;
 +
 +              #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
 +              #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
 +              if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
 +                      close $fd;
 +                      return $1;
 +              }
 +      }
 +      close $fd;
 +      return undef;
 +}
 +
  ## ......................................................................
  ## git utility functions, directly accessing git repository
  
@@@ -1130,8 -1045,6 +1134,8 @@@ sub git_get_projects_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" : '');
                $dir =~ s!/+$!!;
                my $pfxlen = length("$dir");
  
 -              my ($check_forks) = gitweb_check_feature('forks');
 -
                File::Find::find({
                        follow_fast => 1, # follow symbolic links
                        dangling_symlinks => 0, # ignore dangling symlinks, silently
                # '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;
 +      PROJECT:
                while (my $line = <$fd>) {
                        chomp $line;
                        my ($path, $owner) = split ' ', $line;
                                # looking for forks;
                                my $pfx = substr($path, 0, length($filter));
                                if ($pfx ne $filter) {
 -                                      next;
 +                                      next PROJECT;
                                }
                                my $sfx = substr($path, length($filter));
                                if ($sfx !~ /^\/.*\.git$/) {
 -                                      next;
 +                                      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 (check_export_ok("$projectroot/$path")) {
                                        path => $path,
                                        owner => decode_utf8($owner),
                                };
 -                              push @list, $pr
 +                              push @list, $pr;
 +                              (my $forks_path = $path) =~ s/\.git$//;
 +                              $paths{$forks_path}++;
                        }
                }
                close $fd;
        }
 -      @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
        return @list;
  }
  
@@@ -1260,8 -1156,7 +1264,8 @@@ sub git_get_last_activity 
             'refs/heads') or return;
        my $most_recent = <$fd>;
        close $fd or return;
 -      if ($most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
 +      if (defined $most_recent &&
 +          $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
                my $timestamp = $1;
                my $age = time - $timestamp;
                return ($age, age_string($age));
@@@ -1384,12 -1279,8 +1388,12 @@@ sub parse_commit_text 
  
        pop @commit_lines; # Remove '\0'
  
 +      if (! @commit_lines) {
 +              return;
 +      }
 +
        my $header = shift @commit_lines;
 -      if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
 +      if ($header !~ m/^[0-9a-fA-F]{40}/) {
                return;
        }
        ($co{'id'}, my @parents) = split ' ', $header;
@@@ -1585,17 -1476,6 +1589,17 @@@ sub parse_difftree_raw_line 
                        $res{'file'} = unquote($7);
                }
        }
 +      # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
 +      # combined diff (for merge commit)
 +      elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
 +              $res{'nparents'}  = length($1);
 +              $res{'from_mode'} = [ split(' ', $2) ];
 +              $res{'to_mode'} = pop @{$res{'from_mode'}};
 +              $res{'from_id'} = [ split(' ', $3) ];
 +              $res{'to_id'} = pop @{$res{'from_id'}};
 +              $res{'status'} = [ split('', $4) ];
 +              $res{'to_file'} = unquote($5);
 +      }
        # 'c512b523472485aef4fff9e57b229d9d243c967f'
        elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
                $res{'commit'} = $1;
@@@ -1918,7 -1798,7 +1922,7 @@@ EO
                      $cgi->hidden(-name => "a") . "\n" .
                      $cgi->hidden(-name => "h") . "\n" .
                      $cgi->popup_menu(-name => 'st', -default => 'commit',
 -                                     -values => ['commit', 'author', 'committer', 'pickaxe']) .
 +                                     -values => ['commit', 'author', 'committer', 'pickaxe']) .
                      $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
                      " search:\n",
                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
@@@ -1988,16 -1868,16 +1992,16 @@@ sub git_print_page_nav 
        my %arg = map { $_ => {action=>$_} } @navs;
        if (defined $head) {
                for (qw(commit commitdiff)) {
 -                      $arg{$_}{hash} = $head;
 +                      $arg{$_}{'hash'} = $head;
                }
                if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
                        for (qw(shortlog log)) {
 -                              $arg{$_}{hash} = $head;
 +                              $arg{$_}{'hash'} = $head;
                        }
                }
        }
 -      $arg{tree}{hash} = $treehead if defined $treehead;
 -      $arg{tree}{hash_base} = $treebase if defined $treebase;
 +      $arg{'tree'}{'hash'} = $treehead if defined $treehead;
 +      $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
  
        print "<div class=\"page_nav\">\n" .
                (join " | ",
@@@ -2045,9 -1925,9 +2049,9 @@@ sub git_print_header_div 
        my ($action, $title, $hash, $hash_base) = @_;
        my %args = ();
  
 -      $args{action} = $action;
 -      $args{hash} = $hash if $hash;
 -      $args{hash_base} = $hash_base if $hash_base;
 +      $args{'action'} = $action;
 +      $args{'hash'} = $hash if $hash;
 +      $args{'hash_base'} = $hash_base if $hash_base;
  
        print "<div class=\"header\">\n" .
              $cgi->a({-href => href(%args), -class => "title"},
@@@ -2299,42 -2179,8 +2303,42 @@@ sub git_print_tree_entry 
  ## ......................................................................
  ## functions printing large fragments of HTML
  
 +sub fill_from_file_info {
 +      my ($diff, @parents) = @_;
 +
 +      $diff->{'from_file'} = [ ];
 +      $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
 +      for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
 +              if ($diff->{'status'}[$i] eq 'R' ||
 +                  $diff->{'status'}[$i] eq 'C') {
 +                      $diff->{'from_file'}[$i] =
 +                              git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
 +              }
 +      }
 +
 +      return $diff;
 +}
 +
 +# parameters can be strings, or references to arrays of strings
 +sub from_ids_eq {
 +      my ($a, $b) = @_;
 +
 +      if (ref($a) eq "ARRAY" && ref($b) eq "ARRAY" && @$a == @$b) {
 +              for (my $i = 0; $i < @$a; ++$i) {
 +                      return 0 unless ($a->[$i] eq $b->[$i]);
 +              }
 +              return 1;
 +      } elsif (!ref($a) && !ref($b)) {
 +              return $a eq $b;
 +      } else {
 +              return 0;
 +      }
 +}
 +
 +
  sub git_difftree_body {
 -      my ($difftree, $hash, $parent) = @_;
 +      my ($difftree, $hash, @parents) = @_;
 +      my ($parent) = $parents[0];
        my ($have_blame) = gitweb_check_feature('blame');
        print "<div class=\"list_head\">\n";
        if ($#{$difftree} > 10) {
        }
        print "</div>\n";
  
 -      print "<table class=\"diff_tree\">\n";
 +      print "<table class=\"" .
 +            (@parents > 1 ? "combined " : "") .
 +            "diff_tree\">\n";
        my $alternate = 1;
        my $patchno = 0;
        foreach my $line (@{$difftree}) {
 -              my %diff = parse_difftree_raw_line($line);
 +              my $diff;
 +              if (ref($line) eq "HASH") {
 +                      # pre-parsed (or generated by hand)
 +                      $diff = $line;
 +              } else {
 +                      $diff = parse_difftree_raw_line($line);
 +              }
  
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                }
                $alternate ^= 1;
  
 +              if (exists $diff->{'nparents'}) { # combined diff
 +
 +                      fill_from_file_info($diff, @parents)
 +                              unless exists $diff->{'from_file'};
 +
 +                      if ($diff->{'to_id'} ne ('0' x 40)) {
 +                              # file exists in the result (child) commit
 +                              print "<td>" .
 +                                    $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
 +                                                           file_name=>$diff->{'to_file'},
 +                                                           hash_base=>$hash),
 +                                            -class => "list"}, esc_path($diff->{'to_file'})) .
 +                                    "</td>\n";
 +                      } else {
 +                              print "<td>" .
 +                                    esc_path($diff->{'to_file'}) .
 +                                    "</td>\n";
 +                      }
 +
 +                      if ($action eq 'commitdiff') {
 +                              # link to patch
 +                              $patchno++;
 +                              print "<td class=\"link\">" .
 +                                    $cgi->a({-href => "#patch$patchno"}, "patch") .
 +                                    " | " .
 +                                    "</td>\n";
 +                      }
 +
 +                      my $has_history = 0;
 +                      my $not_deleted = 0;
 +                      for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
 +                              my $hash_parent = $parents[$i];
 +                              my $from_hash = $diff->{'from_id'}[$i];
 +                              my $from_path = $diff->{'from_file'}[$i];
 +                              my $status = $diff->{'status'}[$i];
 +
 +                              $has_history ||= ($status ne 'A');
 +                              $not_deleted ||= ($status ne 'D');
 +
 +                              if ($status eq 'A') {
 +                                      print "<td  class=\"link\" align=\"right\"> | </td>\n";
 +                              } elsif ($status eq 'D') {
 +                                      print "<td class=\"link\">" .
 +                                            $cgi->a({-href => href(action=>"blob",
 +                                                                   hash_base=>$hash,
 +                                                                   hash=>$from_hash,
 +                                                                   file_name=>$from_path)},
 +                                                    "blob" . ($i+1)) .
 +                                            " | </td>\n";
 +                              } else {
 +                                      if ($diff->{'to_id'} eq $from_hash) {
 +                                              print "<td class=\"link nochange\">";
 +                                      } else {
 +                                              print "<td class=\"link\">";
 +                                      }
 +                                      print $cgi->a({-href => href(action=>"blobdiff",
 +                                                                   hash=>$diff->{'to_id'},
 +                                                                   hash_parent=>$from_hash,
 +                                                                   hash_base=>$hash,
 +                                                                   hash_parent_base=>$hash_parent,
 +                                                                   file_name=>$diff->{'to_file'},
 +                                                                   file_parent=>$from_path)},
 +                                                    "diff" . ($i+1)) .
 +                                            " | </td>\n";
 +                              }
 +                      }
 +
 +                      print "<td class=\"link\">";
 +                      if ($not_deleted) {
 +                              print $cgi->a({-href => href(action=>"blob",
 +                                                           hash=>$diff->{'to_id'},
 +                                                           file_name=>$diff->{'to_file'},
 +                                                           hash_base=>$hash)},
 +                                            "blob");
 +                              print " | " if ($has_history);
 +                      }
 +                      if ($has_history) {
 +                              print $cgi->a({-href => href(action=>"history",
 +                                                           file_name=>$diff->{'to_file'},
 +                                                           hash_base=>$hash)},
 +                                            "history");
 +                      }
 +                      print "</td>\n";
 +
 +                      print "</tr>\n";
 +                      next; # instead of 'else' clause, to avoid extra indent
 +              }
 +              # else ordinary diff
 +
                my ($to_mode_oct, $to_mode_str, $to_file_type);
                my ($from_mode_oct, $from_mode_str, $from_file_type);
 -              if ($diff{'to_mode'} ne ('0' x 6)) {
 -                      $to_mode_oct = oct $diff{'to_mode'};
 +              if ($diff->{'to_mode'} ne ('0' x 6)) {
 +                      $to_mode_oct = oct $diff->{'to_mode'};
                        if (S_ISREG($to_mode_oct)) { # only for regular file
                                $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
                        }
 -                      $to_file_type = file_type($diff{'to_mode'});
 +                      $to_file_type = file_type($diff->{'to_mode'});
                }
 -              if ($diff{'from_mode'} ne ('0' x 6)) {
 -                      $from_mode_oct = oct $diff{'from_mode'};
 +              if ($diff->{'from_mode'} ne ('0' x 6)) {
 +                      $from_mode_oct = oct $diff->{'from_mode'};
                        if (S_ISREG($to_mode_oct)) { # only for regular file
                                $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
                        }
 -                      $from_file_type = file_type($diff{'from_mode'});
 +                      $from_file_type = file_type($diff->{'from_mode'});
                }
  
 -              if ($diff{'status'} eq "A") { # created
 +              if ($diff->{'status'} eq "A") { # created
                        my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
                        $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
                        $mode_chng   .= "]</span>";
                        print "<td>";
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 -                                                   hash_base=>$hash, file_name=>$diff{'file'}),
 -                                    -class => "list"}, esc_path($diff{'file'}));
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
 +                                                   hash_base=>$hash, file_name=>$diff->{'file'}),
 +                                    -class => "list"}, esc_path($diff->{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
                                print $cgi->a({-href => "#patch$patchno"}, "patch");
                                print " | ";
                        }
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 -                                                   hash_base=>$hash, file_name=>$diff{'file'})},
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
 +                                                   hash_base=>$hash, file_name=>$diff->{'file'})},
                                      "blob");
                        print "</td>\n";
  
 -              } elsif ($diff{'status'} eq "D") { # deleted
 +              } elsif ($diff->{'status'} eq "D") { # deleted
                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
                        print "<td>";
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
 -                                                   hash_base=>$parent, file_name=>$diff{'file'}),
 -                                     -class => "list"}, esc_path($diff{'file'}));
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
 +                                                   hash_base=>$parent, file_name=>$diff->{'file'}),
 +                                     -class => "list"}, esc_path($diff->{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chng</td>\n";
                        print "<td class=\"link\">";
                                print $cgi->a({-href => "#patch$patchno"}, "patch");
                                print " | ";
                        }
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
 -                                                   hash_base=>$parent, file_name=>$diff{'file'})},
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
 +                                                   hash_base=>$parent, file_name=>$diff->{'file'})},
                                      "blob") . " | ";
                        if ($have_blame) {
                                print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
 -                                                           file_name=>$diff{'file'})},
 +                                                           file_name=>$diff->{'file'})},
                                              "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
 -                                                   file_name=>$diff{'file'})},
 +                                                   file_name=>$diff->{'file'})},
                                      "history");
                        print "</td>\n";
  
 -              } elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
 +              } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
                        my $mode_chnge = "";
 -                      if ($diff{'from_mode'} != $diff{'to_mode'}) {
 +                      if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
                                $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
                                if ($from_file_type ne $to_file_type) {
                                        $mode_chnge .= " from $from_file_type to $to_file_type";
                                $mode_chnge .= "]</span>\n";
                        }
                        print "<td>";
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 -                                                   hash_base=>$hash, file_name=>$diff{'file'}),
 -                                    -class => "list"}, esc_path($diff{'file'}));
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
 +                                                   hash_base=>$hash, file_name=>$diff->{'file'}),
 +                                    -class => "list"}, esc_path($diff->{'file'}));
                        print "</td>\n";
                        print "<td>$mode_chnge</td>\n";
                        print "<td class=\"link\">";
                                $patchno++;
                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
                                      " | ";
 -                      } elsif ($diff{'to_id'} ne $diff{'from_id'}) {
 +                      } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
                                # "commit" view and modified file (not onlu mode changed)
                                print $cgi->a({-href => href(action=>"blobdiff",
 -                                                           hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                           hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
                                                             hash_base=>$hash, hash_parent_base=>$parent,
 -                                                           file_name=>$diff{'file'})},
 +                                                           file_name=>$diff->{'file'})},
                                              "diff") .
                                      " | ";
                        }
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 -                                                   hash_base=>$hash, file_name=>$diff{'file'})},
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
 +                                                   hash_base=>$hash, file_name=>$diff->{'file'})},
                                       "blob") . " | ";
                        if ($have_blame) {
                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
 -                                                           file_name=>$diff{'file'})},
 +                                                           file_name=>$diff->{'file'})},
                                              "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
 -                                                   file_name=>$diff{'file'})},
 +                                                   file_name=>$diff->{'file'})},
                                      "history");
                        print "</td>\n";
  
 -              } elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
 +              } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
                        my %status_name = ('R' => 'moved', 'C' => 'copied');
 -                      my $nstatus = $status_name{$diff{'status'}};
 +                      my $nstatus = $status_name{$diff->{'status'}};
                        my $mode_chng = "";
 -                      if ($diff{'from_mode'} != $diff{'to_mode'}) {
 +                      if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
                                # mode also for directories, so we cannot use $to_mode_str
                                $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
                        }
                        print "<td>" .
                              $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 -                                                   hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
 -                                    -class => "list"}, esc_path($diff{'to_file'})) . "</td>\n" .
 +                                                   hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
 +                                    -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
                              "<td><span class=\"file_status $nstatus\">[$nstatus from " .
                              $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
 -                                                   hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}),
 -                                    -class => "list"}, esc_path($diff{'from_file'})) .
 -                            " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
 +                                                   hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
 +                                    -class => "list"}, esc_path($diff->{'from_file'})) .
 +                            " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
                              "<td class=\"link\">";
                        if ($action eq 'commitdiff') {
                                # link to patch
                                $patchno++;
                                print $cgi->a({-href => "#patch$patchno"}, "patch") .
                                      " | ";
 -                      } elsif ($diff{'to_id'} ne $diff{'from_id'}) {
 +                      } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
                                # "commit" view and modified file (not only pure rename or copy)
                                print $cgi->a({-href => href(action=>"blobdiff",
 -                                                           hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 +                                                           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'})},
 +                                                           file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
                                              "diff") .
                                      " | ";
                        }
 -                      print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 -                                                   hash_base=>$parent, file_name=>$diff{'to_file'})},
 +                      print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
 +                                                   hash_base=>$parent, file_name=>$diff->{'to_file'})},
                                      "blob") . " | ";
                        if ($have_blame) {
                                print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
 -                                                           file_name=>$diff{'to_file'})},
 +                                                           file_name=>$diff->{'to_file'})},
                                              "blame") . " | ";
                        }
                        print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
 -                                                  file_name=>$diff{'to_file'})},
 +                                                  file_name=>$diff->{'to_file'})},
                                      "history");
                        print "</td>\n";
  
  }
  
  sub git_patchset_body {
 -      my ($fd, $difftree, $hash, $hash_parent) = @_;
 +      my ($fd, $difftree, $hash, @hash_parents) = @_;
 +      my ($hash_parent) = $hash_parents[0];
  
        my $patch_idx = 0;
 +      my $patch_number = 0;
        my $patch_line;
        my $diffinfo;
        my (%from, %to);
                # git diff header
                #assert($patch_line =~ m/^diff /) if DEBUG;
                #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
 +              $patch_number++;
                push @diff_header, $patch_line;
  
                # extended diff header
                        if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
                                $from_id = $1;
                                $to_id   = $2;
 +                      } elsif ($patch_line =~ m/^index ((?:[0-9a-fA-F]{40},)+[0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
 +                              $from_id = [ split(',', $1) ];
 +                              $to_id   = $2;
                        }
  
                        push @diff_header, $patch_line;
                # check if current patch belong to current raw line
                # and parse raw git-diff line if needed
                if (defined $diffinfo &&
 -                  $diffinfo->{'from_id'} eq $from_id &&
 -                  $diffinfo->{'to_id'}   eq $to_id) {
 -                      # this is split patch
 +                  defined $from_id && defined $to_id &&
 +                  from_ids_eq($diffinfo->{'from_id'}, $from_id) &&
 +                  $diffinfo->{'to_id'} eq $to_id) {
 +                      # this is continuation of a split patch
                        print "<div class=\"patch cont\">\n";
                } else {
                        # advance raw git-diff output if needed
                        } else {
                                $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
                        }
 -                      $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
 -                      $to{'file'}   = $diffinfo->{'to_file'}   || $diffinfo->{'file'};
 -                      if ($diffinfo->{'status'} ne "A") { # not new (added) file
 -                              $from{'href'} = href(action=>"blob", hash_base=>$hash_parent,
 -                                                   hash=>$diffinfo->{'from_id'},
 -                                                   file_name=>$from{'file'});
 +                      if ($diffinfo->{'nparents'}) {
 +                              # combined diff
 +                              $from{'file'} = [];
 +                              $from{'href'} = [];
 +                              fill_from_file_info($diffinfo, @hash_parents)
 +                                      unless exists $diffinfo->{'from_file'};
 +                              for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
 +                                      $from{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'};
 +                                      if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
 +                                              $from{'href'}[$i] = href(action=>"blob",
 +                                                                       hash_base=>$hash_parents[$i],
 +                                                                       hash=>$diffinfo->{'from_id'}[$i],
 +                                                                       file_name=>$from{'file'}[$i]);
 +                                      } else {
 +                                              $from{'href'}[$i] = undef;
 +                                      }
 +                              }
                        } else {
 -                              delete $from{'href'};
 +                              $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
 +                              if ($diffinfo->{'status'} ne "A") { # not new (added) file
 +                                      $from{'href'} = href(action=>"blob", hash_base=>$hash_parent,
 +                                                           hash=>$diffinfo->{'from_id'},
 +                                                           file_name=>$from{'file'});
 +                              } else {
 +                                      delete $from{'href'};
 +                              }
                        }
 +                      $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
                        if ($diffinfo->{'status'} ne "D") { # not deleted file
                                $to{'href'} = href(action=>"blob", hash_base=>$hash,
                                                   hash=>$diffinfo->{'to_id'},
  
                # print "git diff" header
                $patch_line = shift @diff_header;
 -              $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!;
 -              if ($from{'href'}) {
 -                      $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"},
 -                                             'a/' . esc_path($from{'file'}));
 -              } else { # file was added
 -                      $patch_line .= 'a/' . esc_path($from{'file'});
 -              }
 -              $patch_line .= ' ';
 -              if ($to{'href'}) {
 -                      $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
 -                                             'b/' . esc_path($to{'file'}));
 -              } else { # file was deleted
 -                      $patch_line .= 'b/' . esc_path($to{'file'});
 +              if ($diffinfo->{'nparents'}) {
 +
 +                      # combined diff
 +                      $patch_line =~ s!^(diff (.*?) )"?.*$!$1!;
 +                      if ($to{'href'}) {
 +                              $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
 +                                                     esc_path($to{'file'}));
 +                      } else { # file was deleted
 +                              $patch_line .= esc_path($to{'file'});
 +                      }
 +
 +              } else {
 +
 +                      $patch_line =~ s!^(diff (.*?) )"?a/.*$!$1!;
 +                      if ($from{'href'}) {
 +                              $patch_line .= $cgi->a({-href => $from{'href'}, -class => "path"},
 +                                                     'a/' . esc_path($from{'file'}));
 +                      } else { # file was added
 +                              $patch_line .= 'a/' . esc_path($from{'file'});
 +                      }
 +                      $patch_line .= ' ';
 +                      if ($to{'href'}) {
 +                              $patch_line .= $cgi->a({-href => $to{'href'}, -class => "path"},
 +                                                     'b/' . esc_path($to{'file'}));
 +                      } else { # file was deleted
 +                              $patch_line .= 'b/' . esc_path($to{'file'});
 +                      }
 +
                }
                print "<div class=\"diff header\">$patch_line</div>\n";
  
                                $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"path"},
                                                       esc_path($to{'file'}));
                        }
 -                      # match <mode>
 +                      # match single <mode>
                        if ($patch_line =~ m/\s(\d{6})$/) {
                                $patch_line .= '<span class="info"> (' .
                                               file_type_long($1) .
                                               ')</span>';
                        }
                        # match <hash>
 -                      if ($patch_line =~ m/^index/) {
 +                      if ($patch_line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
 +                              # can match only for combined diff
 +                              $patch_line = 'index ';
 +                              for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
 +                                      if ($from{'href'}[$i]) {
 +                                              $patch_line .= $cgi->a({-href=>$from{'href'}[$i],
 +                                                                      -class=>"hash"},
 +                                                                     substr($diffinfo->{'from_id'}[$i],0,7));
 +                                      } else {
 +                                              $patch_line .= '0' x 7;
 +                                      }
 +                                      # separator
 +                                      $patch_line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
 +                              }
 +                              $patch_line .= '..';
 +                              if ($to{'href'}) {
 +                                      $patch_line .= $cgi->a({-href=>$to{'href'}, -class=>"hash"},
 +                                                             substr($diffinfo->{'to_id'},0,7));
 +                              } else {
 +                                      $patch_line .= '0' x 7;
 +                              }
 +
 +                      } elsif ($patch_line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
 +                              # can match only for ordinary diff
                                my ($from_link, $to_link);
                                if ($from{'href'}) {
                                        $from_link = $cgi->a({-href=>$from{'href'}, -class=>"hash"},
                }
                next PATCH if ($patch_line =~ m/^diff /);
                #assert($patch_line =~ m/^---/) if DEBUG;
 -              if ($from{'href'} && $patch_line =~ m!^--- "?a/!) {
 +              if (!$diffinfo->{'nparents'} && # not from-file line for combined diff
 +                  $from{'href'} && $patch_line =~ m!^--- "?a/!) {
                        $patch_line = '--- a/' .
                                      $cgi->a({-href=>$from{'href'}, -class=>"path"},
                                              esc_path($from{'file'}));
        } continue {
                print "</div>\n"; # class="patch"
        }
 +      print "<div class=\"diff nodifferences\">No differences found</div>\n" if (!$patch_number);
  
        print "</div>\n"; # class="patchset"
  }
@@@ -2913,7 -2596,7 +2917,7 @@@ sub git_project_list_body 
                push @projects, $pr;
        }
  
 -      $order ||= "project";
 +      $order ||= $default_projects_order;
        $from = 0 unless defined $from;
        $to = $#projects if (!defined $to || $#projects < $to);
  
                                        esc_html($pr->{'descr'})) . "</td>\n" .
                      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
                print "<td class=\"". age_class($pr->{'age'}) . "\">" .
 -                    $pr->{'age_string'} . "</td>\n" .
 +                    (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
                      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
@@@ -3272,7 -2955,7 +3276,7 @@@ sub git_search_grep_body 
  
  sub git_project_list {
        my $order = $cgi->param('o');
 -      if (defined $order && $order !~ m/project|descr|owner|age/) {
 +      if (defined $order && $order !~ m/none|project|descr|owner|age/) {
                die_error(undef, "Unknown order parameter");
        }
  
  
  sub git_forks {
        my $order = $cgi->param('o');
 -      if (defined $order && $order !~ m/project|descr|owner|age/) {
 +      if (defined $order && $order !~ m/none|project|descr|owner|age/) {
                die_error(undef, "Unknown order parameter");
        }
  
@@@ -3338,7 -3021,7 +3342,7 @@@ sub git_project_index 
  sub git_summary {
        my $descr = git_get_project_description($project) || "none";
        my %co = parse_commit("HEAD");
 -      my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
 +      my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
        my $head = $co{'id'};
  
        my $owner = git_get_project_owner($project);
        print "<div class=\"title\">&nbsp;</div>\n";
        print "<table cellspacing=\"0\">\n" .
              "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
 -            "<tr><td>owner</td><td>$owner</td></tr>\n" .
 -            "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
 +            "<tr><td>owner</td><td>$owner</td></tr>\n";
 +      if (defined $cd{'rfc2822'}) {
 +              print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
 +      }
 +
        # use per project git URL list in $projectroot/$project/cloneurl
        # or make project git URL from git base URL and project name
        my $url_tag = "URL";
  
        # we need to request one more than 16 (0..15) to check if
        # those 16 are all
 -      my @commitlist = parse_commits($head, 17);
 -      git_print_header_div('shortlog');
 -      git_shortlog_body(\@commitlist, 0, 15, $refs,
 -                        $#commitlist <=  15 ? undef :
 -                        $cgi->a({-href => href(action=>"shortlog")}, "..."));
 +      my @commitlist = $head ? parse_commits($head, 17) : ();
 +      if (@commitlist) {
 +              git_print_header_div('shortlog');
 +              git_shortlog_body(\@commitlist, 0, 15, $refs,
 +                                $#commitlist <=  15 ? undef :
 +                                $cgi->a({-href => href(action=>"shortlog")}, "..."));
 +      }
  
        if (@taglist) {
                git_print_header_div('tags');
                git_project_list_body(\@forklist, undef, 0, 15,
                                      $#forklist <= 15 ? undef :
                                      $cgi->a({-href => href(action=>"forks")}, "..."),
 -                                    'noheader');
 +                                    'noheader');
        }
  
        git_footer_html();
@@@ -3426,11 -3104,6 +3430,11 @@@ sub git_tag 
        git_header_html();
        git_print_page_nav('','', $head,undef,$head);
        my %tag = parse_tag($hash);
 +
 +      if (! %tag) {
 +              die_error(undef, "Unknown tag object");
 +      }
 +
        git_print_header_div('commit', esc_html($tag{'name'}), $hash);
        print "<div class=\"title_text\">\n" .
              "<table cellspacing=\"0\">\n" .
@@@ -3527,7 -3200,7 +3531,7 @@@ HTM
                my $rev = substr($full_rev, 0, 8);
                my $author = $meta->{'author'};
                my %date = parse_date($meta->{'author-time'},
 -                                    $meta->{'author-tz'});
 +                                    $meta->{'author-tz'});
                my $date = $date{'iso-tz'};
                if ($group_size) {
                        $current_color = ++$current_color % $num_colors;
                        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));
 +                                                   hash=>$full_rev,
 +                                                   file_name=>$file_name)},
 +                                    esc_html($rev));
                        print "</td>\n";
                }
                open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
                close $dd;
                chomp($parent_commit);
                my $blamed = href(action => 'blame',
 -                                file_name => $meta->{'filename'},
 -                                hash_base => $parent_commit);
 +                                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));
 +                              -id => "l$lineno",
 +                              -class => "linenr" },
 +                            esc_html($lineno));
                print "</td>";
                print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
                print "</tr>\n";
@@@ -3946,7 -3619,7 +3950,7 @@@ sub git_snapshot 
        my $name = $project;
        $name =~ s/\047/\047\\\047\047/g;
        open my $fd, "-|",
 -      "$git archive --format=tar --prefix=\'$name\'/ $hash | $command"
 +              "$git archive --format=tar --prefix=\'$name\'/ $hash | $command"
                or die_error(undef, "Execute git-tar-tree failed");
        binmode STDOUT, ':raw';
        print <$fd>;
@@@ -4055,13 -3728,14 +4059,13 @@@ sub git_commit 
                $parent = "--root";
        }
        my @difftree;
 -      if (@$parents <= 1) {
 -              # difftree output is not printed for merges
 -              open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
 -                      @diff_opts, $parent, $hash, "--"
 -                              or die_error(undef, "Open git-diff-tree failed");
 -              @difftree = map { chomp; $_ } <$fd>;
 -              close $fd or die_error(undef, "Reading git-diff-tree failed");
 -      }
 +      open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
 +              @diff_opts,
 +              (@$parents <= 1 ? $parent : '-c'),
 +              $hash, "--"
 +              or die_error(undef, "Open git-diff-tree failed");
 +      @difftree = map { chomp; $_ } <$fd>;
 +      close $fd or die_error(undef, "Reading git-diff-tree failed");
  
        # non-textual hash id's can be cached
        my $expires;
        git_print_log($co{'comment'});
        print "</div>\n";
  
 -      if (@$parents <= 1) {
 -              # do not output difftree/whatchanged for merges
 -              git_difftree_body(\@difftree, $hash, $parent);
 -      }
 +      git_difftree_body(\@difftree, $hash, @$parents);
  
        git_footer_html();
  }
@@@ -4416,10 -4093,8 +4420,10 @@@ sub git_commitdiff 
                }
        }
  
 +      my $hash_parent_param = $hash_parent;
        if (!defined $hash_parent) {
 -              $hash_parent = $co{'parent'} || '--root';
 +              $hash_parent_param =
 +                      @{$co{'parents'}} > 1 ? '-c' : $co{'parent'} || '--root';
        }
  
        # read commitdiff
        if ($format eq 'html') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
                        "--no-commit-id", "--patch-with-raw", "--full-index",
 -                      $hash_parent, $hash, "--"
 +                      $hash_parent_param, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
  
                while (my $line = <$fd>) {
                        chomp $line;
                        # empty line ends raw part of diff-tree output
                        last unless $line;
 -                      push @difftree, $line;
 +                      push @difftree, scalar parse_difftree_raw_line($line);
                }
  
        } elsif ($format eq 'plain') {
                open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 -                      '-p', $hash_parent, $hash, "--"
 +                      '-p', $hash_parent_param, $hash, "--"
                        or die_error(undef, "Open git-diff-tree failed");
  
        } else {
  
        # write patch
        if ($format eq 'html') {
 -              git_difftree_body(\@difftree, $hash, $hash_parent);
 +              git_difftree_body(\@difftree, $hash, $hash_parent || @{$co{'parents'}});
                print "<br/>\n";
  
 -              git_patchset_body($fd, \@difftree, $hash, $hash_parent);
 +              git_patchset_body($fd, \@difftree, $hash, $hash_parent || @{$co{'parents'}});
                close $fd;
                print "</div>\n"; # class="page_body"
                git_footer_html();
@@@ -4629,13 -4304,13 +4633,13 @@@ sub git_search 
                if ($page > 0) {
                        $paging_nav .=
                                $cgi->a({-href => href(action=>"search", hash=>$hash,
 -                                                     searchtext=>$searchtext, searchtype=>$searchtype)},
 -                                      "first");
 +                                                     searchtext=>$searchtext, searchtype=>$searchtype)},
 +                                      "first");
                        $paging_nav .= " &sdot; " .
                                $cgi->a({-href => href(action=>"search", hash=>$hash,
 -                                                     searchtext=>$searchtext, searchtype=>$searchtype,
 -                                                     page=>$page-1),
 -                                       -accesskey => "p", -title => "Alt-p"}, "prev");
 +                                                     searchtext=>$searchtext, searchtype=>$searchtype,
 +                                                     page=>$page-1),
 +                                       -accesskey => "p", -title => "Alt-p"}, "prev");
                } else {
                        $paging_nav .= "first";
                        $paging_nav .= " &sdot; prev";
                if ($#commitlist >= 100) {
                        $paging_nav .= " &sdot; " .
                                $cgi->a({-href => href(action=>"search", hash=>$hash,
 -                                                     searchtext=>$searchtext, searchtype=>$searchtype,
 -                                                     page=>$page+1),
 -                                       -accesskey => "n", -title => "Alt-n"}, "next");
 +                                                     searchtext=>$searchtext, searchtype=>$searchtype,
 +                                                     page=>$page+1),
 +                                       -accesskey => "n", -title => "Alt-n"}, "next");
                } else {
                        $paging_nav .= " &sdot; next";
                }
                if ($#commitlist >= 100) {
                        $next_link =
                                $cgi->a({-href => href(action=>"search", hash=>$hash,
 -                                                     searchtext=>$searchtext, searchtype=>$searchtype,
 -                                                     page=>$page+1),
 -                                       -accesskey => "n", -title => "Alt-n"}, "next");
 +                                                     searchtext=>$searchtext, searchtype=>$searchtype,
 +                                                     page=>$page+1),
 +                                       -accesskey => "n", -title => "Alt-n"}, "next");
                }
  
                git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);