add Expires: +1d header to commit and commitdiff pages
[git.git] / gitweb.cgi
index e58f246..873b7ce 100755 (executable)
@@ -5,7 +5,7 @@
 # (C) 2005, Kay Sievers <kay.sievers@vrfy.org>
 # (C) 2005, Christian Gierke <ch@gierke.de>
 #
-# This program is licensed under the GPL v2, or a later version
+# This program is licensed under the GPLv2
 
 use strict;
 use warnings;
@@ -15,7 +15,7 @@ use CGI::Carp qw(fatalsToBrowser);
 use Fcntl ':mode';
 
 my $cgi = new CGI;
-my $version =          "241";
+my $version =          "247";
 my $my_url =           $cgi->url();
 my $my_uri =           $cgi->url(-absolute => 1);
 my $rss_link = "";
@@ -43,7 +43,7 @@ my $projects_list = "index/index.aux";
 # input validation and dispatch
 my $action = $cgi->param('a');
 if (defined $action) {
-       if ($action =~ m/[^0-9a-zA-Z\.\-_]+/) {
+       if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
                undef $action;
                die_error(undef, "Invalid action parameter.");
        }
@@ -58,7 +58,7 @@ if (defined $action) {
 
 my $order = $cgi->param('o');
 if (defined $order) {
-       if ($order =~ m/[^a-zA-Z0-9_]/) {
+       if ($order =~ m/[^0-9a-zA-Z_]/) {
                undef $order;
                die_error(undef, "Invalid order parameter.");
        }
@@ -66,13 +66,9 @@ if (defined $order) {
 
 my $project = $cgi->param('p');
 if (defined $project) {
-       if ($project =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
-               undef $project;
-               die_error(undef, "Non-canonical project parameter.");
-       }
-       if ($project =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~]/) {
-               undef $project;
-               die_error(undef, "Invalid character in project parameter.");
+       $project = validate_input($project);
+       if (!defined($project)) {
+               die_error(undef, "Invalid project parameter.");
        }
        if (!(-d "$projectroot/$project")) {
                undef $project;
@@ -91,54 +87,39 @@ if (defined $project) {
 
 my $file_name = $cgi->param('f');
 if (defined $file_name) {
-       if ($file_name =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
-               undef $file_name;
-               die_error(undef, "Non-canonical file parameter.");
-       }
-       if ($file_name =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~\:\!]/) {
-               undef $file_name;
-               die_error(undef, "Invalid character in file parameter.");
+       $file_name = validate_input($file_name);
+       if (!defined($file_name)) {
+               die_error(undef, "Invalid file parameter.");
        }
 }
 
 my $hash = $cgi->param('h');
 if (defined $hash) {
-       if (!($hash =~ m/^[0-9a-fA-F]{40}$/)) {
-               if ($hash =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
-                       undef $hash;
-                       die_error(undef, "Non-canonical hash parameter.");
-               }
-               if ($hash =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~\:\!]/) {
-                       undef $hash;
-                       die_error(undef, "Invalid character in hash parameter.");
-               }
-               # replace branch-name with hash
-               my $branchlist = git_read_refs("refs/heads");
-               foreach my $entry (@$branchlist) {
-                       my %branch = %$entry;
-                       if ($branch{'name'} eq $hash) {
-                               $hash = $branch{'id'};
-                               last;
-                       }
-               }
+       $hash = validate_input($hash);
+       if (!defined($hash)) {
+               die_error(undef, "Invalid hash parameter.");
        }
 }
 
 my $hash_parent = $cgi->param('hp');
-if (defined $hash_parent && !($hash_parent =~ m/^[0-9a-fA-F]{40}$/)) {
-       undef $hash_parent;
-       die_error(undef, "Invalid hash_parent parameter.");
+if (defined $hash_parent) {
+       $hash_parent = validate_input($hash_parent);
+       if (!defined($hash_parent)) {
+               die_error(undef, "Invalid hash parent parameter.");
+       }
 }
 
 my $hash_base = $cgi->param('hb');
-if (defined $hash_base && !($hash_base =~ m/^[0-9a-fA-F]{40}$/)) {
-       undef $hash_base;
-       die_error(undef, "Invalid parent hash parameter.");
+if (defined $hash_base) {
+       $hash_base = validate_input($hash_base);
+       if (!defined($hash_base)) {
+               die_error(undef, "Invalid hash base parameter.");
+       }
 }
 
 my $page = $cgi->param('pg');
 if (defined $page) {
-       if ($page =~ m/^[^0-9]+$/) {
+       if ($page =~ m/[^0-9]$/) {
                undef $page;
                die_error(undef, "Invalid page parameter.");
        }
@@ -153,11 +134,26 @@ if (defined $searchtext) {
        $searchtext = quotemeta $searchtext;
 }
 
+sub validate_input {
+       my $input = shift;
+
+       if ($input =~ m/^[0-9a-fA-F]{40}$/) {
+               return $input;
+       }
+       if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
+               return undef;
+       }
+       if ($input =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~]/) {
+               return undef;
+       }
+       return $input;
+}
+
 if (!defined $action || $action eq "summary") {
        git_summary();
        exit;
-} elsif ($action eq "branches") {
-       git_branches();
+} elsif ($action eq "heads") {
+       git_heads();
        exit;
 } elsif ($action eq "tags") {
        git_tags();
@@ -212,6 +208,7 @@ if (!defined $action || $action eq "summary") {
 
 sub git_header_html {
        my $status = shift || "200 OK";
+       my $expires = shift;
 
        my $title = "git";
        if (defined $project) {
@@ -220,7 +217,7 @@ sub git_header_html {
                        $title .= "/$action";
                }
        }
-       print $cgi->header(-type=>'text/html',  -charset => 'utf-8', -status=> $status);
+       print $cgi->header(-type=>'text/html',  -charset => 'utf-8', -status=> $status, -expires => $expires);
        print <<EOF;
 <?xml version="1.0" encoding="utf-8"?>
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -301,11 +298,19 @@ EOF
                if (!defined $searchtext) {
                        $searchtext = "";
                }
+               my $search_hash;
+               if (defined $hash) {
+                       $search_hash = $hash;
+               } else {
+                       $search_hash  = "HEAD";
+               }
                $cgi->param("a", "search");
+               $cgi->param("h", $search_hash);
                print $cgi->startform(-method => "get", -action => "$my_uri") .
                      "<div class=\"search\">\n" .
                      $cgi->hidden(-name => "p") . "\n" .
                      $cgi->hidden(-name => "a") . "\n" .
+                     $cgi->hidden(-name => "h") . "\n" .
                      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
                      "</div>" .
                      $cgi->end_form() . "\n";
@@ -447,21 +452,28 @@ sub git_read_commit {
 
        my @commit_lines;
        my %co;
-       my @parents;
 
        if (defined $commit_text) {
                @commit_lines = @$commit_text;
        } else {
-               open my $fd, "-|", "$gitbin/git-cat-file commit $commit_id" or return;
-               @commit_lines = map { chomp; $_ } <$fd>;
+               $/ = "\0";
+               open my $fd, "-|", "$gitbin/git-rev-list --header --parents --max-count=1 $commit_id" or return;
+               @commit_lines = split '\n', <$fd>;
                close $fd or return;
+               $/ = "\n";
+               pop @commit_lines;
+       }
+       my $header = shift @commit_lines;
+       if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
+               return;
        }
+       ($co{'id'}, my @parents) = split ' ', $header;
+       $co{'parents'} = \@parents;
+       $co{'parent'} = $parents[0];
        while (my $line = shift @commit_lines) {
                last if $line eq "\n";
                if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
                        $co{'tree'} = $1;
-               } elsif ($line =~ m/^parent ([0-9a-fA-F]{40})$/) {
-                       push @parents, $1;
                } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
                        $co{'author'} = $1;
                        $co{'author_epoch'} = $2;
@@ -480,15 +492,12 @@ sub git_read_commit {
                }
        }
        if (!defined $co{'tree'}) {
-               return undef
+               return;
        };
-       $co{'id'} = $commit_id;
-       $co{'parents'} = \@parents;
-       $co{'parent'} = $parents[0];
-       $co{'comment'} = \@commit_lines;
+
        foreach my $title (@commit_lines) {
                if ($title ne "") {
-                       $co{'title'} = chop_str($title, 80);
+                       $co{'title'} = chop_str($title, 80, 5);
                        # remove leading stuff of merges to make the interesting part visible
                        if (length($title) > 50) {
                                $title =~ s/^Automatic //;
@@ -506,10 +515,15 @@ sub git_read_commit {
                                        $title =~ s/\/pub\/scm//;
                                }
                        }
-                       $co{'title_short'} = chop_str($title, 50);
+                       $co{'title_short'} = chop_str($title, 50, 5);
                        last;
                }
        }
+       # remove added spaces
+       foreach my $line (@commit_lines) {
+               $line =~ s/^    //;
+       }
+       $co{'comment'} = \@commit_lines;
 
        my $age = time - $co{'committer_epoch'};
        $co{'age'} = $age;
@@ -988,7 +1002,7 @@ sub git_summary {
                                print $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit", -class => "list"},
                                      "<b>" . escapeHTML($co{'title'}) . "</b>");
                        }
-                             "</td>\n" .
+                       print "</td>\n" .
                              "<td class=\"link\">" .
                              $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"}, "commit") .
                              " | " . $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$commit"}, "commitdiff") .
@@ -1054,15 +1068,15 @@ sub git_summary {
                print "</table\n>";
        }
 
-       my $branchlist = git_read_refs("refs/heads");
-       if (defined @$branchlist) {
+       my $headlist = git_read_refs("refs/heads");
+       if (defined @$headlist) {
                print "<div>\n" .
-                     $cgi->a({-href => "$my_uri?p=$project;a=branches", -class => "title"}, "branches") .
+                     $cgi->a({-href => "$my_uri?p=$project;a=heads", -class => "title"}, "heads") .
                      "</div>\n";
                my $i = 16;
                print "<table cellspacing=\"0\">\n";
                my $alternate = 0;
-               foreach my $entry (@$branchlist) {
+               foreach my $entry (@$headlist) {
                        my %tag = %$entry;
                        if ($alternate) {
                                print "<tr class=\"dark\">\n";
@@ -1082,7 +1096,7 @@ sub git_summary {
                                      "</td>\n" .
                                      "</tr>";
                        } else {
-                               print "<td>" . $cgi->a({-href => "$my_uri?p=$project;a=branches"}, "...") . "</td>\n" .
+                               print "<td>" . $cgi->a({-href => "$my_uri?p=$project;a=heads"}, "...") . "</td>\n" .
                                "</tr>";
                                last;
                        }
@@ -1190,7 +1204,7 @@ sub git_tags {
        git_footer_html();
 }
 
-sub git_branches {
+sub git_heads {
        my $head = git_read_hash("$project/HEAD");
        git_header_html();
        print "<div class=\"page_nav\">\n" .
@@ -1268,7 +1282,6 @@ sub git_blob {
                $hash = git_get_hash_by_path($base, $file_name, "blob");
        }
        open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or die_error(undef, "Open failed.");
-       my $base = $file_name || "";
        git_header_html();
        if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
                print "<div class=\"page_nav\">\n" .
@@ -1278,9 +1291,13 @@ sub git_blob {
                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base"}, "commit") .
                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash_base"}, "commitdiff") .
                      " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash_base"}, "tree") . "<br/>\n";
-               print $cgi->a({-href => "$my_uri?p=$project;a=blob_plain;h=$hash"}, "plain") . "<br/>\n" .
-                     "</div>\n";
-               print "<div>" .
+               if (defined $file_name) {
+                       print $cgi->a({-href => "$my_uri?p=$project;a=blob_plain;h=$hash;f=$file_name"}, "plain") . "<br/>\n";
+               } else {
+                       print $cgi->a({-href => "$my_uri?p=$project;a=blob_plain;h=$hash"}, "plain") . "<br/>\n";
+               }
+               print "</div>\n".
+                      "<div>" .
                      $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash_base", -class => "title"}, escapeHTML($co{'title'})) .
                      "</div>\n";
        } else {
@@ -1310,7 +1327,11 @@ sub git_blob {
 }
 
 sub git_blob_plain {
-       print $cgi->header(-type => "text/plain", -charset => 'utf-8');
+       my $save_as = "$hash.txt";
+       if (defined $file_name) {
+               $save_as = $file_name;
+       }
+       print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"$save_as\"");
        open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or return;
        undef $/;
        print <$fd>;
@@ -1600,7 +1621,13 @@ sub git_commit {
        open my $fd, "-|", "$gitbin/git-diff-tree -r -M $root $parent $hash" or die_error(undef, "Open failed.");
        @difftree = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading diff-tree failed.");
-       git_header_html();
+
+       # non-textual hash id's can be cached
+       my $expires;
+       if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+               $expires = "+1d";
+       }
+       git_header_html(undef, $expires);
        print "<div class=\"page_nav\">\n" .
              $cgi->a({-href => "$my_uri?p=$project;a=summary"}, "summary") .
              " | " . $cgi->a({-href => "$my_uri?p=$project;a=shortlog;h=$hash"}, "shortlog") .
@@ -1634,7 +1661,7 @@ sub git_commit {
              "</tr>\n";
        print "<tr><td>committer</td><td>" . escapeHTML($co{'committer'}) . "</td></tr>\n";
        print "<tr><td></td><td> $cd{'rfc2822'}" . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
-       print "<tr><td>commit</td><td style=\"font-family:monospace\">$hash</td></tr>\n";
+       print "<tr><td>commit</td><td style=\"font-family:monospace\">$co{'id'}</td></tr>\n";
        print "<tr>" .
              "<td>tree</td>" .
              "<td style=\"font-family:monospace\">" .
@@ -1711,7 +1738,7 @@ sub git_commit {
                                $mode_chng = sprintf(" with mode: %04o", (oct $to_mode) & 0777);
                        }
                        print "<td>" .
-                             $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id;hp=$hash;f=$file", -class => "list"}, escapeHTML($file)) . "</td>\n" .
+                             $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file", -class => "list"}, escapeHTML($file)) . "</td>\n" .
                              "<td><span style=\"color: #008000;\">[new " . file_type($to_mode) . "$mode_chng]</span></td>\n" .
                              "<td class=\"link\">" . $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file"}, "blob") . "</td>\n";
                } elsif ($status eq "D") {
@@ -1832,7 +1859,12 @@ sub git_commitdiff {
        my (@difftree) = map { chomp; $_ } <$fd>;
        close $fd or die_error(undef, "Reading diff-tree failed.");
 
-       git_header_html();
+       # non-textual hash id's can be cached
+       my $expires;
+       if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
+               $expires = "+1d";
+       }
+       git_header_html(undef, $expires);
        print "<div class=\"page_nav\">\n" .
              $cgi->a({-href => "$my_uri?p=$project;a=summary"}, "summary") .
              " | " . $cgi->a({-href => "$my_uri?p=$project;a=shortlog;h=$hash"}, "shortlog") .
@@ -1932,7 +1964,7 @@ sub git_commitdiff_plain {
        }
        close $fd;
 
-       print $cgi->header(-type => "text/plain", -charset => 'utf-8');
+       print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"git-$hash.patch\"");
        my %co = git_read_commit($hash);
        my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
        my $comment = $co{'comment'};
@@ -2076,7 +2108,7 @@ sub git_search {
        my $alternate = 0;
        if ($commit_search) {
                $/ = "\0";
-               open my $fd, "-|", "$gitbin/git-rev-list --header $hash";
+               open my $fd, "-|", "$gitbin/git-rev-list --header --parents $hash" or next;
                while (my $commit_text = <$fd>) {
                        if (!grep m/$searchtext/i, $commit_text) {
                                next;
@@ -2088,8 +2120,7 @@ sub git_search {
                                next;
                        }
                        my @commit_lines = split "\n", $commit_text;
-                       my $commit = shift @commit_lines;
-                       my %co = git_read_commit($commit, \@commit_lines);
+                       my %co = git_read_commit(undef, \@commit_lines);
                        if (!%co) {
                                next;
                        }
@@ -2102,7 +2133,7 @@ sub git_search {
                        print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
                              "<td><i>" . escapeHTML(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
                              "<td>" .
-                             $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit", -class => "list"}, "<b>" . escapeHTML(chop_str($co{'title'}, 50)) . "</b><br/>");
+                             $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$co{'id'}", -class => "list"}, "<b>" . escapeHTML(chop_str($co{'title'}, 50)) . "</b><br/>");
                        my $comment = $co{'comment'};
                        foreach my $line (@$comment) {
                                if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
@@ -2117,8 +2148,8 @@ sub git_search {
                        }
                        print "</td>\n" .
                              "<td class=\"link\">" .
-                             $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"}, "commit") .
-                             " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$commit"}, "tree");
+                             $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$co{'id'}"}, "commit") .
+                             " | " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}"}, "tree");
                        print "</td>\n" .
                              "</tr>\n";
                }