gitweb: Divide page path into directories -- path's "breadcrumbs"
[gitweb.git] / gitweb / gitweb.perl
index 352236bc44ca8a3dc4951a449998c4938e15af5d..d89f709d1361e45f8d3f6ce9eb74430d9ff0d94d 100755 (executable)
@@ -48,6 +48,8 @@
 our $stylesheet = "++GITWEB_CSS++";
 # URI of GIT logo
 our $logo = "++GITWEB_LOGO++";
+# URI of GIT favicon, assumed to be image/png type
+our $favicon = "++GITWEB_FAVICON++";
 
 # source of projects list
 our $projects_list = "++GITWEB_LIST++";
@@ -152,7 +154,7 @@ sub feature_snapshot {
 our @diff_opts = ('-M'); # taken from git_commit
 
 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
-require $GITWEB_CONFIG if -e $GITWEB_CONFIG;
+do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
 
 # version of the core git binary
 our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
@@ -1027,9 +1029,30 @@ sub parse_difftree_raw_line {
                }
        }
        # 'c512b523472485aef4fff9e57b229d9d243c967f'
-       #elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
-       #       $res{'commit'} = $1;
-       #}
+       elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
+               $res{'commit'} = $1;
+       }
+
+       return wantarray ? %res : \%res;
+}
+
+# parse line of git-ls-tree output
+sub parse_ls_tree_line ($;%) {
+       my $line = shift;
+       my %opts = @_;
+       my %res;
+
+       #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
+       $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
+
+       $res{'mode'} = $1;
+       $res{'type'} = $2;
+       $res{'hash'} = $3;
+       if ($opts{'-z'}) {
+               $res{'name'} = $4;
+       } else {
+               $res{'name'} = unquote($4);
+       }
 
        return wantarray ? %res : \%res;
 }
@@ -1201,6 +1224,9 @@ sub git_header_html {
                       'href="%s" type="application/rss+xml"/>'."\n",
                       esc_param($project), href(action=>"rss"));
        }
+       if (defined $favicon) {
+               print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
+       }
 
        print "</head>\n" .
              "<body>\n" .
@@ -1354,6 +1380,24 @@ sub git_print_header_div {
              "\n</div>\n";
 }
 
+#sub git_print_authorship (\%) {
+sub git_print_authorship {
+       my $co = shift;
+
+       my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
+       print "<div class=\"author_date\">" .
+             esc_html($co->{'author_name'}) .
+             " [$ad{'rfc2822'}";
+       if ($ad{'hour_local'} < 6) {
+               printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
+                      $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+       } else {
+               printf(" (%02d:%02d %s)",
+                      $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
+       }
+       print "]</div>\n";
+}
+
 sub git_print_page_path {
        my $name = shift;
        my $type = shift;
@@ -1361,25 +1405,44 @@ sub git_print_page_path {
 
        if (!defined $name) {
                print "<div class=\"page_path\">/</div>\n";
-       } elsif (defined $type && $type eq 'blob') {
+       } else {
+               my @dirname = split '/', $name;
+               my $basename = pop @dirname;
+               my $fullname = '';
+
                print "<div class=\"page_path\">";
-               if (defined $hb) {
+               foreach my $dir (@dirname) {
+                       $fullname .= $dir . '/';
+                       print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
+                                                    hash_base=>$hb),
+                                     -title => $fullname}, esc_html($dir));
+                       print "/";
+               }
+               if (defined $type && $type eq 'blob') {
                        print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
-                                                    hash_base=>$hb)},
-                                     esc_html($name));
+                                                    hash_base=>$hb),
+                                     -title => $name}, esc_html($basename));
+               } elsif (defined $type && $type eq 'tree') {
+                       print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
+                                                    hash_base=>$hb),
+                                     -title => $name}, esc_html($basename));
+                       print "/";
                } else {
-                       print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)},
-                                     esc_html($name));
+                       print esc_html($basename);
                }
                print "<br/></div>\n";
-       } else {
-               print "<div class=\"page_path\">" . esc_html($name) . "<br/></div>\n";
        }
 }
 
-sub git_print_log {
+# sub git_print_log (\@;%) {
+sub git_print_log ($;%) {
        my $log = shift;
+       my %opts = @_;
 
+       if ($opts{'-remove_title'}) {
+               # remove title, i.e. first line of log
+               shift @$log;
+       }
        # remove leading empty lines
        while (defined $log->[0] && $log->[0] eq "") {
                shift @$log;
@@ -1389,6 +1452,20 @@ sub git_print_log {
        my $signoff = 0;
        my $empty = 0;
        foreach my $line (@$log) {
+               if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
+                       $signoff = 1;
+                       $empty = 0;
+                       if (! $opts{'-remove_signoff'}) {
+                               print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
+                               next;
+                       } else {
+                               # remove signoff lines
+                               next;
+                       }
+               } else {
+                       $signoff = 0;
+               }
+
                # print only one empty line
                # do not print empty line after signoff
                if ($line eq "") {
@@ -1397,13 +1474,13 @@ sub git_print_log {
                } else {
                        $empty = 0;
                }
-               if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
-                       $signoff = 1;
-                       print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
-               } else {
-                       $signoff = 0;
-                       print format_log_line_html($line) . "<br/>\n";
-               }
+
+               print format_log_line_html($line) . "<br/>\n";
+       }
+
+       if ($opts{'-final_empty_line'}) {
+               # end with single empty line
+               print "<br/>\n" unless $empty;
        }
 }
 
@@ -1411,30 +1488,65 @@ sub git_print_simplified_log {
        my $log = shift;
        my $remove_title = shift;
 
-       shift @$log if $remove_title;
-       # remove leading empty lines
-       while (defined $log->[0] && $log->[0] eq "") {
-               shift @$log;
-       }
+       git_print_log($log,
+               -final_empty_line=> 1,
+               -remove_title => $remove_title);
+}
 
-       # simplify and print log
-       my $empty = 0;
-       foreach my $line (@$log) {
-               # remove signoff lines
-               if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
-                       next;
+# print tree entry (row of git_tree), but without encompassing <tr> element
+sub git_print_tree_entry {
+       my ($t, $basedir, $hash_base, $have_blame) = @_;
+
+       my %base_key = ();
+       $base_key{hash_base} = $hash_base if defined $hash_base;
+
+       print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
+       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" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             "blob");
+               if ($have_blame) {
+                       print " | " .
+                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
+                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
+                                       "blame");
                }
-               # print only one empty line
-               if ($line eq "") {
-                       next if $empty;
-                       $empty = 1;
-               } else {
-                       $empty = 0;
+               if (defined $hash_base) {
+                       print " | " .
+                             $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
+                                                    hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
+                                     "history");
                }
-               print format_log_line_html($line) . "<br/>\n";
+               print " | " .
+                     $cgi->a({-href => href(action=>"blob_plain",
+                                            hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
+                             "raw") .
+                     "</td>\n";
+
+       } elsif ($t->{'type'} eq "tree") {
+               print "<td class=\"list\">" .
+                     $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             esc_html($t->{'name'})) .
+                     "</td>\n" .
+                     "<td class=\"link\">" .
+                     $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
+                                            file_name=>"$basedir$t->{'name'}", %base_key)},
+                             "tree");
+               if (defined $hash_base) {
+                       print " | " .
+                             $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
+                                                    file_name=>"$basedir$t->{'name'}")},
+                                     "history");
+               }
+               print "</td>\n";
        }
-       # end with single empty line
-       print "<br/>\n" unless $empty;
 }
 
 ## ......................................................................
@@ -1451,6 +1563,7 @@ sub git_difftree_body {
 
        print "<table class=\"diff_tree\">\n";
        my $alternate = 0;
+       my $patchno = 0;
        foreach my $line (@{$difftree}) {
                my %diff = parse_difftree_raw_line($line);
 
@@ -1491,8 +1604,14 @@ sub git_difftree_body {
                              "<td class=\"link\">" .
                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
                                                     hash_base=>$hash, file_name=>$diff{'file'})},
-                                     "blob") .
-                             "</td>\n";
+                                     "blob");
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print " | " .
+                                     $cgi->a({-href => "#patch$patchno"}, "patch");
+                       }
+                       print "</td>\n";
 
                } elsif ($diff{'status'} eq "D") { # deleted
                        my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
@@ -1506,8 +1625,14 @@ sub git_difftree_body {
                              $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
                                                     hash_base=>$parent, file_name=>$diff{'file'})},
                                      "blob") .
-                             " | " .
-                             $cgi->a({-href => href(action=>"history", hash_base=>$parent,
+                             " | ";
+                       if ($action eq 'commitdiff') {
+                               # link to patch
+                               $patchno++;
+                               print " | " .
+                                     $cgi->a({-href => "#patch$patchno"}, "patch");
+                       }
+                       print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
                                                     file_name=>$diff{'file'})},
                                      "history") .
                              "</td>\n";
@@ -1543,16 +1668,23 @@ sub git_difftree_body {
                        print "</td>\n" .
                              "<td>$mode_chnge</td>\n" .
                              "<td class=\"link\">" .
-                               $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
-                                                      hash_base=>$hash, file_name=>$diff{'file'})},
-                                       "blob");
+                             $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
+                                                    hash_base=>$hash, file_name=>$diff{'file'})},
+                                     "blob");
                        if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
-                               print " | " .
-                                       $cgi->a({-href => href(action=>"blobdiff",
-                                                              hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
-                                                              hash_base=>$hash, hash_parent_base=>$parent,
-                                                              file_name=>$diff{'file'})},
-                                               "diff");
+                               if ($action eq 'commitdiff') {
+                                       # link to patch
+                                       $patchno++;
+                                       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");
+                               }
                        }
                        print " | " .
                                $cgi->a({-href => href(action=>"history",
@@ -1582,12 +1714,19 @@ sub git_difftree_body {
                                                     hash=>$diff{'to_id'}, file_name=>$diff{'to_file'})},
                                      "blob");
                        if ($diff{'to_id'} ne $diff{'from_id'}) {
-                               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");
+                               if ($action eq 'commitdiff') {
+                                       # link to patch
+                                       $patchno++;
+                                       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");
+                               }
                        }
                        print "</td>\n";
 
@@ -1620,7 +1759,7 @@ sub git_patchset_body {
                                # first patch in patchset
                                $patch_found = 1;
                        }
-                       print "<div class=\"patch\">\n";
+                       print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
 
                        if (ref($difftree->[$patch_idx]) eq "HASH") {
                                $diffinfo = $difftree->[$patch_idx];
@@ -2130,7 +2269,8 @@ sub git_blame2 {
        my $fd;
        my $ftype;
 
-       if (!gitweb_check_feature('blame')) {
+       my ($have_blame) = gitweb_check_feature('blame');
+       if (!$have_blame) {
                die_error('403 Permission denied', "Permission denied");
        }
        die_error('404 Not Found', "File name not defined") if (!$file_name);
@@ -2199,7 +2339,8 @@ sub git_blame2 {
 sub git_blame {
        my $fd;
 
-       if (!gitweb_check_feature('blame')) {
+       my ($have_blame) = gitweb_check_feature('blame');
+       if (!$have_blame) {
                die_error('403 Permission denied', "Permission denied");
        }
        die_error('404 Not Found', "File name not defined") if (!$file_name);
@@ -2373,7 +2514,7 @@ sub git_blob {
                        die_error(undef, "No file name defined");
                }
        }
-       my $have_blame = gitweb_check_feature('blame');
+       my ($have_blame) = gitweb_check_feature('blame');
        open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
                or die_error(undef, "Couldn't cat $file_name, $hash");
        my $mimetype = blob_mimetype($fd, $file_name);
@@ -2448,14 +2589,13 @@ sub git_tree {
        my $refs = git_get_references();
        my $ref = format_ref_marker($refs, $hash_base);
        git_header_html();
-       my %base_key = ();
        my $base = "";
-       my $have_blame = gitweb_check_feature('blame');
+       my ($have_blame) = gitweb_check_feature('blame');
        if (defined $hash_base && (my %co = parse_commit($hash_base))) {
-               $base_key{hash_base} = $hash_base;
                git_print_page_nav('tree','', $hash_base);
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
        } else {
+               undef $hash_base;
                print "<div class=\"page_nav\">\n";
                print "<br/><br/></div>\n";
                print "<div class=\"title\">$hash</div>\n";
@@ -2468,54 +2608,17 @@ sub git_tree {
        print "<table cellspacing=\"0\">\n";
        my $alternate = 0;
        foreach my $line (@entries) {
-               #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
-               $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
-               my $t_mode = $1;
-               my $t_type = $2;
-               my $t_hash = $3;
-               my $t_name = validate_input($4);
+               my %t = parse_ls_tree_line($line, -z => 1);
+
                if ($alternate) {
                        print "<tr class=\"dark\">\n";
                } else {
                        print "<tr class=\"light\">\n";
                }
                $alternate ^= 1;
-               print "<td class=\"mode\">" . mode_str($t_mode) . "</td>\n";
-               if ($t_type eq "blob") {
-                       print "<td class=\"list\">" .
-                             $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key),
-                                     -class => "list"}, esc_html($t_name)) .
-                             "</td>\n" .
-                             "<td class=\"link\">" .
-                             $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
-                                     "blob");
-                       if ($have_blame) {
-                               print " | " .
-                                       $cgi->a({-href => href(action=>"blame", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
-                                               "blame");
-                       }
-                       print " | " .
-                             $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
-                                                    hash=>$t_hash, file_name=>"$base$t_name")},
-                                     "history") .
-                             " | " .
-                             $cgi->a({-href => href(action=>"blob_plain",
-                                                    hash=>$t_hash, file_name=>"$base$t_name")},
-                                     "raw") .
-                             "</td>\n";
-               } elsif ($t_type eq "tree") {
-                       print "<td class=\"list\">" .
-                             $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
-                                     esc_html($t_name)) .
-                             "</td>\n" .
-                             "<td class=\"link\">" .
-                             $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
-                                     "tree") .
-                             " | " .
-                             $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, file_name=>"$base$t_name")},
-                                     "history") .
-                             "</td>\n";
-               }
+
+               git_print_tree_entry(\%t, $base, $hash_base, $have_blame);
+
                print "</tr>\n";
        }
        print "</table>\n" .
@@ -2696,7 +2799,7 @@ sub git_commit {
                      "<td class=\"link\">" .
                      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
                      " | " .
-                     $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") .
+                     $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
                      "</td>" .
                      "</tr>\n";
        }
@@ -2734,10 +2837,6 @@ sub git_blobdiff {
                        @difftree
                                or die_error('404 Not Found', "Blob diff not found");
 
-               } elsif (defined $hash) { # try to find filename from $hash
-                       if ($hash !~ /[0-9a-fA-F]{40}/) {
-                               $hash = git_to_hash($hash);
-                       }
                } elsif (defined $hash &&
                         $hash =~ /[0-9a-fA-F]{40}/) {
                        # try to find filename from $hash
@@ -2935,6 +3034,7 @@ sub git_commitdiff {
                git_header_html(undef, $expires);
                git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
                git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
+               git_print_authorship(\%co);
                print "<div class=\"page_body\">\n";
                print "<div class=\"log\">\n";
                git_print_simplified_log($co{'comment'}, 1); # skip title
@@ -2967,8 +3067,8 @@ sub git_commitdiff {
 
        # write patch
        if ($format eq 'html') {
-               #git_difftree_body(\@difftree, $hash, $hash_parent);
-               #print "<br/>\n";
+               git_difftree_body(\@difftree, $hash, $hash_parent);
+               print "<br/>\n";
 
                git_patchset_body($fd, \@difftree, $hash, $hash_parent);
                close $fd;