v249
[git.git] / gitweb.cgi
1 #!/usr/bin/perl
2
3 # gitweb - simple web interface to track changes in git repositories
4 #
5 # (C) 2005, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke <ch@gierke.de>
7 #
8 # This program is licensed under the GPLv2
9
10 use strict;
11 use warnings;
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser);
15 use Fcntl ':mode';
16
17 my $cgi = new CGI;
18 my $version =           "249";
19 my $my_url =            $cgi->url();
20 my $my_uri =            $cgi->url(-absolute => 1);
21 my $rss_link = "";
22
23 # absolute fs-path which will be prepended to the project path
24 #my $projectroot =      "/pub/scm";
25 my $projectroot = "/home/kay/public_html/pub/scm";
26
27 # location of the git-core binaries
28 my $gitbin =            "/usr/bin";
29
30 # location for temporary files needed for diffs
31 my $git_temp =          "/tmp/gitweb";
32
33 # target of the home link on top of all pages
34 my $home_link =         $my_uri;
35
36 # html text to include at home page
37 my $home_text =         "indextext.html";
38
39 # source of projects list
40 #my $projects_list = $projectroot;
41 my $projects_list = "index/index.aux";
42
43 # input validation and dispatch
44 my $action = $cgi->param('a');
45 if (defined $action) {
46         if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
47                 undef $action;
48                 die_error(undef, "Invalid action parameter.");
49         }
50         if ($action eq "git-logo.png") {
51                 git_logo();
52                 exit;
53         } elsif ($action eq "opml") {
54                 git_opml();
55                 exit;
56         }
57 }
58
59 my $order = $cgi->param('o');
60 if (defined $order) {
61         if ($order =~ m/[^0-9a-zA-Z_]/) {
62                 undef $order;
63                 die_error(undef, "Invalid order parameter.");
64         }
65 }
66
67 my $project = $cgi->param('p');
68 if (defined $project) {
69         $project = validate_input($project);
70         if (!defined($project)) {
71                 die_error(undef, "Invalid project parameter.");
72         }
73         if (!(-d "$projectroot/$project")) {
74                 undef $project;
75                 die_error(undef, "No such directory.");
76         }
77         if (!(-e "$projectroot/$project/HEAD")) {
78                 undef $project;
79                 die_error(undef, "No such project.");
80         }
81         $rss_link = "<link rel=\"alternate\" title=\"" . esc($project) . " log\" href=\"" .
82                     esc("$my_uri?p=$project;a=rss") . "\" type=\"application/rss+xml\"/>";
83         $ENV{'GIT_DIR'} = "$projectroot/$project";
84 } else {
85         git_project_list();
86         exit;
87 }
88
89 my $file_name = $cgi->param('f');
90 if (defined $file_name) {
91         $file_name = validate_input($file_name);
92         if (!defined($file_name)) {
93                 die_error(undef, "Invalid file parameter.");
94         }
95 }
96
97 my $hash = $cgi->param('h');
98 if (defined $hash) {
99         $hash = validate_input($hash);
100         if (!defined($hash)) {
101                 die_error(undef, "Invalid hash parameter.");
102         }
103 }
104
105 my $hash_parent = $cgi->param('hp');
106 if (defined $hash_parent) {
107         $hash_parent = validate_input($hash_parent);
108         if (!defined($hash_parent)) {
109                 die_error(undef, "Invalid hash parent parameter.");
110         }
111 }
112
113 my $hash_base = $cgi->param('hb');
114 if (defined $hash_base) {
115         $hash_base = validate_input($hash_base);
116         if (!defined($hash_base)) {
117                 die_error(undef, "Invalid hash base parameter.");
118         }
119 }
120
121 my $page = $cgi->param('pg');
122 if (defined $page) {
123         if ($page =~ m/[^0-9]$/) {
124                 undef $page;
125                 die_error(undef, "Invalid page parameter.");
126         }
127 }
128
129 my $searchtext = $cgi->param('s');
130 if (defined $searchtext) {
131         if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
132                 undef $searchtext;
133                 die_error(undef, "Invalid search parameter.");
134         }
135         $searchtext = quotemeta $searchtext;
136 }
137
138 sub validate_input {
139         my $input = shift;
140
141         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
142                 return $input;
143         }
144         if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
145                 return undef;
146         }
147         if ($input =~ m/[^a-zA-Z0-9_ \.\/\-\+\#\~]/) {
148                 return undef;
149         }
150         return $input;
151 }
152
153 if (!defined $action || $action eq "summary") {
154         git_summary();
155         exit;
156 } elsif ($action eq "heads") {
157         git_heads();
158         exit;
159 } elsif ($action eq "tags") {
160         git_tags();
161         exit;
162 } elsif ($action eq "blob") {
163         git_blob();
164         exit;
165 } elsif ($action eq "blob_plain") {
166         git_blob_plain();
167         exit;
168 } elsif ($action eq "tree") {
169         git_tree();
170         exit;
171 } elsif ($action eq "rss") {
172         git_rss();
173         exit;
174 } elsif ($action eq "commit") {
175         git_commit();
176         exit;
177 } elsif ($action eq "log") {
178         git_log();
179         exit;
180 } elsif ($action eq "blobdiff") {
181         git_blobdiff();
182         exit;
183 } elsif ($action eq "blobdiff_plain") {
184         git_blobdiff_plain();
185         exit;
186 } elsif ($action eq "commitdiff") {
187         git_commitdiff();
188         exit;
189 } elsif ($action eq "commitdiff_plain") {
190         git_commitdiff_plain();
191         exit;
192 } elsif ($action eq "history") {
193         git_history();
194         exit;
195 } elsif ($action eq "search") {
196         git_search();
197         exit;
198 } elsif ($action eq "shortlog") {
199         git_shortlog();
200         exit;
201 } elsif ($action eq "tag") {
202         git_tag();
203         exit;
204 } else {
205         undef $action;
206         die_error(undef, "Unknown action.");
207         exit;
208 }
209
210 sub esc {
211         my $str = shift;
212         $str =~ s/ /%20/g;
213         $str =~ s/\+/%2B/g;
214         return $str;
215 }
216
217 sub git_header_html {
218         my $status = shift || "200 OK";
219         my $expires = shift;
220
221         my $title = "git";
222         if (defined $project) {
223                 $title .= " - $project";
224                 if (defined $action) {
225                         $title .= "/$action";
226                 }
227         }
228         print $cgi->header(-type=>'text/html',  -charset => 'utf-8', -status=> $status, -expires => $expires);
229         print <<EOF;
230 <?xml version="1.0" encoding="utf-8"?>
231 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
232 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
233 <!-- git web interface v$version, (C) 2005, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke <ch\@gierke.de> -->
234 <head>
235 <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
236 <meta name="robots" content="index, nofollow"/>
237 <title>$title</title>
238 $rss_link
239 <style type="text/css">
240 body { font-family: sans-serif; font-size: 12px; margin:0px; border:solid #d9d8d1; border-width:1px; margin:10px; }
241 a { color:#0000cc; }
242 a:hover, a:visited, a:active { color:#880000; }
243 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
244 div.page_header a:visited, a.header { color:#0000cc; }
245 div.page_header a:hover { color:#880000; }
246 div.page_nav { padding:8px; }
247 div.page_nav a:visited { color:#0000cc; }
248 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
249 div.page_footer { height:17px; padding:4px 8px; background-color: #d9d8d1; }
250 div.page_footer_text { float:left; color:#555555; font-style:italic; }
251 div.page_body { padding:8px; }
252 div.title, a.title {
253         display:block; padding:6px 8px;
254         font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
255 }
256 a.title:hover { background-color: #d9d8d1; }
257 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
258 div.log_body { padding:8px 8px 8px 150px; }
259 span.age { position:relative; float:left; width:142px; font-style:italic; }
260 div.log_link {
261         padding:0px 8px;
262         font-size:10px; font-family:sans-serif; font-style:normal;
263         position:relative; float:left; width:136px;
264 }
265 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
266 a.list { text-decoration:none; color:#000000; }
267 a.list:hover { text-decoration:underline; color:#880000; }
268 a.text { text-decoration:none; color:#0000cc; }
269 a.text:visited { text-decoration:none; color:#880000; }
270 a.text:hover { text-decoration:underline; color:#880000; }
271 table { padding:8px 4px; }
272 th { padding:2px 5px; font-size:12px; text-align:left; }
273 tr.light:hover { background-color:#edece6; }
274 tr.dark { background-color:#f6f6f0; }
275 tr.dark:hover { background-color:#edece6; }
276 td { padding:2px 5px; font-size:12px; vertical-align:top; }
277 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
278 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
279 div.diff_info { font-family:monospace; color:#000099; background-color:#edece6; font-style:italic; }
280 div.index_include { border:solid #d9d8d1; border-width:0px 0px 1px; padding:12px 8px; }
281 div.search { margin:4px 8px; position:absolute; top:56px; right:12px }
282 a.linenr { color:#999999; text-decoration:none }
283 a.rss_logo {
284         float:right; padding:3px 0px; width:35px; line-height:10px;
285         border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
286         color:#ffffff; background-color:#ff6600;
287         font-weight:bold; font-family:sans-serif; font-size:10px;
288         text-align:center; text-decoration:none;
289 }
290 a.rss_logo:hover { background-color:#ee5500; }
291 </style>
292 </head>
293 <body>
294 EOF
295         print "<div class=\"page_header\">\n" .
296               "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
297               "<img src=\"" . esc("$my_uri?a=git-logo.png") . "\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
298               "</a>\n";
299         print $cgi->a({-href => esc($home_link)}, "projects") . " / ";
300         if (defined $project) {
301                 print $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, escapeHTML($project));
302                 if (defined $action) {
303                         print " / $action";
304                 }
305                 print "\n";
306                 if (!defined $searchtext) {
307                         $searchtext = "";
308                 }
309                 my $search_hash;
310                 if (defined $hash) {
311                         $search_hash = $hash;
312                 } else {
313                         $search_hash  = "HEAD";
314                 }
315                 $cgi->param("a", "search");
316                 $cgi->param("h", $search_hash);
317                 print $cgi->startform(-method => "get", -action => $my_uri) .
318                       "<div class=\"search\">\n" .
319                       $cgi->hidden(-name => "p") . "\n" .
320                       $cgi->hidden(-name => "a") . "\n" .
321                       $cgi->hidden(-name => "h") . "\n" .
322                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
323                       "</div>" .
324                       $cgi->end_form() . "\n";
325         }
326         print "</div>\n";
327 }
328
329 sub git_footer_html {
330         print "<div class=\"page_footer\">\n";
331         if (defined $project) {
332                 my $descr = git_read_description($project);
333                 if (defined $descr) {
334                         print "<div class=\"page_footer_text\">" . escapeHTML($descr) . "</div>\n";
335                 }
336                 print $cgi->a({-href => esc("$my_uri?p=$project;a=rss"), -class => "rss_logo"}, "RSS") . "\n";
337         } else {
338                 print $cgi->a({-href => esc("$my_uri?a=opml"), -class => "rss_logo"}, "OPML") . "\n";
339         }
340         print "</div>\n" .
341               "</body>\n" .
342               "</html>";
343 }
344
345 sub die_error {
346         my $status = shift || "403 Forbidden";
347         my $error = shift || "Malformed query, file missing or permission denied"; 
348
349         git_header_html($status);
350         print "<div class=\"page_body\">\n" .
351               "<br/><br/>\n" .
352               "$status - $error\n" .
353               "<br/>\n" .
354               "</div>\n";
355         git_footer_html();
356         exit;
357 }
358
359 sub git_get_type {
360         my $hash = shift;
361
362         open my $fd, "-|", "$gitbin/git-cat-file -t $hash" or return;
363         my $type = <$fd>;
364         close $fd or return;
365         chomp $type;
366         return $type;
367 }
368
369 sub git_read_hash {
370         my $path = shift;
371
372         open my $fd, "$projectroot/$path" or return undef;
373         my $head = <$fd>;
374         close $fd;
375         chomp $head;
376         if ($head =~ m/^[0-9a-fA-F]{40}$/) {
377                 return $head;
378         }
379 }
380
381 sub git_read_description {
382         my $path = shift;
383
384         open my $fd, "$projectroot/$path/description" or return undef;
385         my $descr = <$fd>;
386         close $fd;
387         chomp $descr;
388         return $descr;
389 }
390
391 sub git_read_tag {
392         my $tag_id = shift;
393         my %tag;
394         my @comment;
395
396         open my $fd, "-|", "$gitbin/git-cat-file tag $tag_id" or return;
397         $tag{'id'} = $tag_id;
398         while (my $line = <$fd>) {
399                 chomp $line;
400                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
401                         $tag{'object'} = $1;
402                 } elsif ($line =~ m/^type (.+)$/) {
403                         $tag{'type'} = $1;
404                 } elsif ($line =~ m/^tag (.+)$/) {
405                         $tag{'name'} = $1;
406                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
407                         $tag{'author'} = $1;
408                         $tag{'epoch'} = $2;
409                         $tag{'tz'} = $3;
410                 } elsif ($line =~ m/--BEGIN/) {
411                         push @comment, $line;
412                         last;
413                 } elsif ($line eq "") {
414                         last;
415                 }
416         }
417         push @comment, <$fd>;
418         $tag{'comment'} = \@comment;
419         close $fd or return;
420         if (!defined $tag{'name'}) {
421                 return
422         };
423         return %tag
424 }
425
426 sub age_string {
427         my $age = shift;
428         my $age_str;
429
430         if ($age > 60*60*24*365*2) {
431                 $age_str = (int $age/60/60/24/365);
432                 $age_str .= " years ago";
433         } elsif ($age > 60*60*24*(365/12)*2) {
434                 $age_str = int $age/60/60/24/(365/12);
435                 $age_str .= " months ago";
436         } elsif ($age > 60*60*24*7*2) {
437                 $age_str = int $age/60/60/24/7;
438                 $age_str .= " weeks ago";
439         } elsif ($age > 60*60*24*2) {
440                 $age_str = int $age/60/60/24;
441                 $age_str .= " days ago";
442         } elsif ($age > 60*60*2) {
443                 $age_str = int $age/60/60;
444                 $age_str .= " hours ago";
445         } elsif ($age > 60*2) {
446                 $age_str = int $age/60;
447                 $age_str .= " min ago";
448         } elsif ($age > 2) {
449                 $age_str = int $age;
450                 $age_str .= " sec ago";
451         } else {
452                 $age_str .= " right now";
453         }
454         return $age_str;
455 }
456
457 sub git_read_commit {
458         my $commit_id = shift;
459         my $commit_text = shift;
460
461         my @commit_lines;
462         my %co;
463
464         if (defined $commit_text) {
465                 @commit_lines = @$commit_text;
466         } else {
467                 $/ = "\0";
468                 open my $fd, "-|", "$gitbin/git-rev-list --header --parents --max-count=1 $commit_id" or return;
469                 @commit_lines = split '\n', <$fd>;
470                 close $fd or return;
471                 $/ = "\n";
472                 pop @commit_lines;
473         }
474         my $header = shift @commit_lines;
475         if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
476                 return;
477         }
478         ($co{'id'}, my @parents) = split ' ', $header;
479         $co{'parents'} = \@parents;
480         $co{'parent'} = $parents[0];
481         while (my $line = shift @commit_lines) {
482                 last if $line eq "\n";
483                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
484                         $co{'tree'} = $1;
485                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
486                         $co{'author'} = $1;
487                         $co{'author_epoch'} = $2;
488                         $co{'author_tz'} = $3;
489                         if ($co{'author'} =~ m/^([^<]+) </) {
490                                 $co{'author_name'} = $1;
491                         } else {
492                                 $co{'author_name'} = $co{'author'};
493                         }
494                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
495                         $co{'committer'} = $1;
496                         $co{'committer_epoch'} = $2;
497                         $co{'committer_tz'} = $3;
498                         $co{'committer_name'} = $co{'committer'};
499                         $co{'committer_name'} =~ s/ <.*//;
500                 }
501         }
502         if (!defined $co{'tree'}) {
503                 return;
504         };
505
506         foreach my $title (@commit_lines) {
507                 if ($title ne "") {
508                         $co{'title'} = chop_str($title, 80, 5);
509                         # remove leading stuff of merges to make the interesting part visible
510                         if (length($title) > 50) {
511                                 $title =~ s/^Automatic //;
512                                 $title =~ s/^merge (of|with) /Merge ... /i;
513                                 if (length($title) > 50) {
514                                         $title =~ s/(http|rsync):\/\///;
515                                 }
516                                 if (length($title) > 50) {
517                                         $title =~ s/(master|www|rsync)\.//;
518                                 }
519                                 if (length($title) > 50) {
520                                         $title =~ s/kernel.org:?//;
521                                 }
522                                 if (length($title) > 50) {
523                                         $title =~ s/\/pub\/scm//;
524                                 }
525                         }
526                         $co{'title_short'} = chop_str($title, 50, 5);
527                         last;
528                 }
529         }
530         # remove added spaces
531         foreach my $line (@commit_lines) {
532                 $line =~ s/^    //;
533         }
534         $co{'comment'} = \@commit_lines;
535
536         my $age = time - $co{'committer_epoch'};
537         $co{'age'} = $age;
538         $co{'age_string'} = age_string($age);
539         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
540         if ($age > 60*60*24*7*2) {
541                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
542                 $co{'age_string_age'} = $co{'age_string'};
543         } else {
544                 $co{'age_string_date'} = $co{'age_string'};
545                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
546         }
547         return %co;
548 }
549
550 sub git_diff_print {
551         my $from = shift;
552         my $from_name = shift;
553         my $to = shift;
554         my $to_name = shift;
555         my $format = shift || "html";
556
557         my $from_tmp = "/dev/null";
558         my $to_tmp = "/dev/null";
559         my $pid = $$;
560
561         # create tmp from-file
562         if (defined $from) {
563                 $from_tmp = "$git_temp/gitweb_" . $$ . "_from";
564                 open my $fd2, "> $from_tmp";
565                 open my $fd, "-|", "$gitbin/git-cat-file blob $from";
566                 my @file = <$fd>;
567                 print $fd2 @file;
568                 close $fd2;
569                 close $fd;
570         }
571
572         # create tmp to-file
573         if (defined $to) {
574                 $to_tmp = "$git_temp/gitweb_" . $$ . "_to";
575                 open my $fd2, "> $to_tmp";
576                 open my $fd, "-|", "$gitbin/git-cat-file blob $to";
577                 my @file = <$fd>;
578                 print $fd2 @file;
579                 close $fd2;
580                 close $fd;
581         }
582
583         open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp";
584         if ($format eq "plain") {
585                 undef $/;
586                 print <$fd>;
587                 $/ = "\n";
588         } else {
589                 while (my $line = <$fd>) {
590                         chomp($line);
591                         my $char = substr($line, 0, 1);
592                         my $color = "";
593                         if ($char eq '+') {
594                                 $color = " style=\"color:#008800;\"";
595                         } elsif ($char eq "-") {
596                                 $color = " style=\"color:#cc0000;\"";
597                         } elsif ($char eq "@") {
598                                 $color = " style=\"color:#990099;\"";
599                         } elsif ($char eq "\\") {
600                                 # skip errors
601                                 next;
602                         }
603                         while ((my $pos = index($line, "\t")) != -1) {
604                                 if (my $count = (8 - (($pos-1) % 8))) {
605                                         my $spaces = ' ' x $count;
606                                         $line =~ s/\t/$spaces/;
607                                 }
608                         }
609                         print "<div class=\"pre\"$color>" . escapeHTML($line) . "</div>\n";
610                 }
611         }
612         close $fd;
613
614         if (defined $from) {
615                 unlink($from_tmp);
616         }
617         if (defined $to) {
618                 unlink($to_tmp);
619         }
620 }
621
622 sub mode_str {
623         my $mode = oct shift;
624
625         if (S_ISDIR($mode & S_IFMT)) {
626                 return 'drwxr-xr-x';
627         } elsif (S_ISLNK($mode)) {
628                 return 'lrwxrwxrwx';
629         } elsif (S_ISREG($mode)) {
630                 # git cares only about the executable bit
631                 if ($mode & S_IXUSR) {
632                         return '-rwxr-xr-x';
633                 } else {
634                         return '-rw-r--r--';
635                 };
636         } else {
637                 return '----------';
638         }
639 }
640
641 sub chop_str {
642         my $str = shift;
643         my $len = shift;
644         my $add_len = shift || 10;
645
646         # allow only $len chars, but don't cut a word if it would fit in $add_len
647         # if it doesn't fit, cut it if it's still longer than the dots we would add
648         $str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
649         my $body = $1;
650         my $tail = $2;
651         if (length($tail) > 4) {
652                 $tail = " ...";
653         }
654         return "$body$tail";
655 }
656
657 sub file_type {
658         my $mode = oct shift;
659
660         if (S_ISDIR($mode & S_IFMT)) {
661                 return "directory";
662         } elsif (S_ISLNK($mode)) {
663                 return "symlink";
664         } elsif (S_ISREG($mode)) {
665                 return "file";
666         } else {
667                 return "unknown";
668         }
669 }
670
671 sub format_log_line_html {
672         my $line = shift;
673
674         $line = escapeHTML($line);
675         $line =~ s/ /&nbsp;/g;
676         if ($line =~ m/([0-9a-fA-F]{40})/) {
677                 my $hash_text = $1;
678                 if (git_get_type($hash_text) eq "commit") {
679                         my $link = $cgi->a({-class => "text", -href => esc("$my_uri?p=$project;a=commit;h=$hash_text")}, $hash_text);
680                         $line =~ s/$hash_text/$link/;
681                 }
682         }
683         return $line;
684 }
685
686 sub date_str {
687         my $epoch = shift;
688         my $tz = shift || "-0000";
689
690         my %date;
691         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
692         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
693         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
694         $date{'hour'} = $hour;
695         $date{'minute'} = $min;
696         $date{'mday'} = $mday;
697         $date{'day'} = $days[$wday];
698         $date{'month'} = $months[$mon];
699         $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
700         $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
701
702         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
703         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
704         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
705         $date{'hour_local'} = $hour;
706         $date{'minute_local'} = $min;
707         $date{'tz_local'} = $tz;
708         return %date;
709 }
710
711 # git-logo (cached in browser for one day)
712 sub git_logo {
713         print $cgi->header(-type => 'image/png', -expires => '+1d');
714         # cat git-logo.png | hexdump -e '16/1 " %02x"  "\n"' | sed 's/ /\\x/g'
715         print   "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" .
716                 "\x00\x00\x00\x48\x00\x00\x00\x1b\x04\x03\x00\x00\x00\x2d\xd9\xd4" .
717                 "\x2d\x00\x00\x00\x18\x50\x4c\x54\x45\xff\xff\xff\x60\x60\x5d\xb0" .
718                 "\xaf\xaa\x00\x80\x00\xce\xcd\xc7\xc0\x00\x00\xe8\xe8\xe6\xf7\xf7" .
719                 "\xf6\x95\x0c\xa7\x47\x00\x00\x00\x73\x49\x44\x41\x54\x28\xcf\x63" .
720                 "\x48\x67\x20\x04\x4a\x5c\x18\x0a\x08\x2a\x62\x53\x61\x20\x02\x08" .
721                 "\x0d\x69\x45\xac\xa1\xa1\x01\x30\x0c\x93\x60\x36\x26\x52\x91\xb1" .
722                 "\x01\x11\xd6\xe1\x55\x64\x6c\x6c\xcc\x6c\x6c\x0c\xa2\x0c\x70\x2a" .
723                 "\x62\x06\x2a\xc1\x62\x1d\xb3\x01\x02\x53\xa4\x08\xe8\x00\x03\x18" .
724                 "\x26\x56\x11\xd4\xe1\x20\x97\x1b\xe0\xb4\x0e\x35\x24\x71\x29\x82" .
725                 "\x99\x30\xb8\x93\x0a\x11\xb9\x45\x88\xc1\x8d\xa0\xa2\x44\x21\x06" .
726                 "\x27\x41\x82\x40\x85\xc1\x45\x89\x20\x70\x01\x00\xa4\x3d\x21\xc5" .
727                 "\x12\x1c\x9a\xfe\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
728 }
729
730 sub get_file_owner {
731         my $path = shift;
732
733         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
734         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
735         if (!defined $gcos) {
736                 return undef;
737         }
738         my $owner = $gcos;
739         $owner =~ s/[,;].*$//;
740         return $owner;
741 }
742
743 sub git_read_projects {
744         my @list;
745
746         if (-d $projects_list) {
747                 # search in directory
748                 my $dir = $projects_list;
749                 opendir my $dh, $dir or return undef;
750                 while (my $dir = readdir($dh)) {
751                         if (-e "$projectroot/$dir/HEAD") {
752                                 my $pr = {
753                                         path => $dir,
754                                 };
755                                 push @list, $pr
756                         }
757                 }
758                 closedir($dh);
759         } elsif (-f $projects_list) {
760                 # read from file(url-encoded):
761                 # 'git%2Fgit.git Linus+Torvalds'
762                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
763                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
764                 open my $fd , $projects_list or return undef;
765                 while (my $line = <$fd>) {
766                         chomp $line;
767                         my ($path, $owner) = split ' ', $line;
768                         $path = unescape($path);
769                         $owner = unescape($owner);
770                         if (!defined $path) {
771                                 next;
772                         }
773                         if (-e "$projectroot/$path/HEAD") {
774                                 my $pr = {
775                                         path => $path,
776                                         owner => $owner,
777                                 };
778                                 push @list, $pr
779                         }
780                 }
781                 close $fd;
782         }
783         @list = sort {$a->{'path'} cmp $b->{'path'}} @list;
784         return @list;
785 }
786
787 sub git_project_list {
788         my @list = git_read_projects();
789         my @projects;
790         if (!@list) {
791                 die_error(undef, "No project found.");
792         }
793         foreach my $pr (@list) {
794                 my $head = git_read_hash("$pr->{'path'}/HEAD");
795                 if (!defined $head) {
796                         next;
797                 }
798                 $ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}";
799                 my %co = git_read_commit($head);
800                 if (!%co) {
801                         next;
802                 }
803                 $pr->{'commit'} = \%co;
804                 if (!defined $pr->{'descr'}) {
805                         my $descr = git_read_description($pr->{'path'}) || "";
806                         $pr->{'descr'} = chop_str($descr, 25, 5);
807                 }
808                 if (!defined $pr->{'owner'}) {
809                         $pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
810                 }
811                 push @projects, $pr;
812         }
813         git_header_html();
814         if (-f $home_text) {
815                 print "<div class=\"index_include\">\n";
816                 open (my $fd, $home_text);
817                 print <$fd>;
818                 close $fd;
819                 print "</div>\n";
820         }
821         print "<table cellspacing=\"0\">\n" .
822               "<tr>\n";
823         if (!defined($order) || (defined($order) && ($order eq "project"))) {
824                 @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
825                 print "<th>Project</th>\n";
826         } else {
827                 print "<th>" . $cgi->a({-class => "header", -href => esc("$my_uri?o=project")}, "Project") . "</th>\n";
828         }
829         if (defined($order) && ($order eq "descr")) {
830                 @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
831                 print "<th>Description</th>\n";
832         } else {
833                 print "<th>" . $cgi->a({-class => "header", -href => esc("$my_uri?o=descr")}, "Description") . "</th>\n";
834         }
835         if (defined($order) && ($order eq "owner")) {
836                 @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
837                 print "<th>Owner</th>\n";
838         } else {
839                 print "<th>" . $cgi->a({-class => "header", -href => esc("$my_uri?o=owner")}, "Owner") . "</th>\n";
840         }
841         if (defined($order) && ($order eq "age")) {
842                 @projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
843                 print "<th>Last Change</th>\n";
844         } else {
845                 print "<th>" . $cgi->a({-class => "header", -href => esc("$my_uri?o=age")}, "Last Change") . "</th>\n";
846         }
847         print "<th></th>\n" .
848               "</tr>\n";
849         my $alternate = 0;
850         foreach my $pr (@projects) {
851                 if ($alternate) {
852                         print "<tr class=\"dark\">\n";
853                 } else {
854                         print "<tr class=\"light\">\n";
855                 }
856                 $alternate ^= 1;
857                 print "<td>" . $cgi->a({-href => esc("$my_uri?p=$pr->{'path'};a=summary"), -class => "list"}, escapeHTML($pr->{'path'})) . "</td>\n" .
858                       "<td>$pr->{'descr'}</td>\n" .
859                       "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
860                 my $colored_age;
861                 if ($pr->{'commit'}{'age'} < 60*60*2) {
862                         $colored_age = "<span style =\"color: #009900;\"><b><i>$pr->{'commit'}{'age_string'}</i></b></span>";
863                 } elsif ($pr->{'commit'}{'age'} < 60*60*24*2) {
864                         $colored_age = "<span style =\"color: #009900;\"><i>$pr->{'commit'}{'age_string'}</i></span>";
865                 } else {
866                         $colored_age = "<i>$pr->{'commit'}{'age_string'}</i>";
867                 }
868                 print "<td>$colored_age</td>\n" .
869                       "<td class=\"link\">" .
870                       $cgi->a({-href => esc("$my_uri?p=$pr->{'path'};a=summary")}, "summary") .
871                       " | " . $cgi->a({-href => esc("$my_uri?p=$pr->{'path'};a=shortlog")}, "shortlog") .
872                       " | " . $cgi->a({-href => esc("$my_uri?p=$pr->{'path'};a=log")}, "log") .
873                       "</td>\n" .
874                       "</tr>\n";
875         }
876         print "</table>\n";
877         git_footer_html();
878 }
879
880 sub git_read_refs {
881         my $ref_dir = shift;
882         my @reflist;
883
884         my @refs;
885         opendir my $dh, "$projectroot/$project/$ref_dir";
886         while (my $dir = readdir($dh)) {
887                 if ($dir =~ m/^\./) {
888                         next;
889                 }
890                 if (-d "$projectroot/$project/$ref_dir/$dir") {
891                         opendir my $dh2, "$projectroot/$project/$ref_dir/$dir";
892                         my @subdirs = grep !m/^\./, readdir $dh2;
893                         closedir($dh2);
894                         foreach my $subdir (@subdirs) {
895                                 push @refs, "$dir/$subdir"
896                         }
897                         next;
898                 }
899                 push @refs, $dir;
900         }
901         closedir($dh);
902         foreach my $ref_file (@refs) {
903                 my $ref_id = git_read_hash("$project/$ref_dir/$ref_file");
904                 my $type = git_get_type($ref_id) || next;
905                 my %ref_item;
906                 my %co;
907                 $ref_item{'type'} = $type;
908                 $ref_item{'id'} = $ref_id;
909                 $ref_item{'epoch'} = 0;
910                 $ref_item{'age'} = "unknown";
911                 if ($type eq "tag") {
912                         my %tag = git_read_tag($ref_id);
913                         $ref_item{'comment'} = $tag{'comment'};
914                         if ($tag{'type'} eq "commit") {
915                                 %co = git_read_commit($tag{'object'});
916                                 $ref_item{'epoch'} = $co{'committer_epoch'};
917                                 $ref_item{'age'} = $co{'age_string'};
918                         } elsif (defined($tag{'epoch'})) {
919                                 my $age = time - $tag{'epoch'};
920                                 $ref_item{'epoch'} = $tag{'epoch'};
921                                 $ref_item{'age'} = age_string($age);
922                         }
923                         $ref_item{'reftype'} = $tag{'type'};
924                         $ref_item{'name'} = $tag{'name'};
925                         $ref_item{'refid'} = $tag{'object'};
926                 } elsif ($type eq "commit"){
927                         %co = git_read_commit($ref_id);
928                         $ref_item{'reftype'} = "commit";
929                         $ref_item{'name'} = $ref_file;
930                         $ref_item{'title'} = $co{'title'};
931                         $ref_item{'refid'} = $ref_id;
932                         $ref_item{'epoch'} = $co{'committer_epoch'};
933                         $ref_item{'age'} = $co{'age_string'};
934                 }
935
936                 push @reflist, \%ref_item;
937         }
938         # sort tags by age
939         @reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
940         return \@reflist;
941 }
942
943 sub git_summary {
944         my $descr = git_read_description($project) || "none";
945         my $head = git_read_hash("$project/HEAD");
946         my %co = git_read_commit($head);
947         my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
948
949         my $owner;
950         if (-f $projects_list) {
951                 open (my $fd , $projects_list);
952                 while (my $line = <$fd>) {
953                         chomp $line;
954                         my ($pr, $ow) = split ' ', $line;
955                         $pr = unescape($pr);
956                         $ow = unescape($ow);
957                         if ($pr eq $project) {
958                                 $owner = $ow;
959                                 last;
960                         }
961                 }
962                 close $fd;
963         }
964         if (!defined $owner) {
965                 $owner = get_file_owner("$projectroot/$project");
966         }
967
968         git_header_html();
969         print "<div class=\"page_nav\">\n" .
970               "summary".
971               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
972               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
973               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$head")}, "commit") .
974               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$head")}, "commitdiff") .
975               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree")}, "tree") .
976               "<br/><br/>\n" .
977               "</div>\n";
978         print "<div class=\"title\">&nbsp;</div>\n";
979         print "<table cellspacing=\"0\">\n" .
980               "<tr><td>description</td><td>" . escapeHTML($descr) . "</td></tr>\n" .
981               "<tr><td>owner</td><td>$owner</td></tr>\n" .
982               "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n" .
983               "</table>\n";
984         open my $fd, "-|", "$gitbin/git-rev-list --max-count=17 " . git_read_hash("$project/HEAD") or die_error(undef, "Open failed.");
985         my (@revlist) = map { chomp; $_ } <$fd>;
986         close $fd;
987         print "<div>\n" .
988               $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog"), -class => "title"}, "shortlog") .
989               "</div>\n";
990         my $i = 16;
991         print "<table cellspacing=\"0\">\n";
992         my $alternate = 0;
993         foreach my $commit (@revlist) {
994                 my %co = git_read_commit($commit);
995                 my %ad = date_str($co{'author_epoch'});
996                 if ($alternate) {
997                         print "<tr class=\"dark\">\n";
998                 } else {
999                         print "<tr class=\"light\">\n";
1000                 }
1001                 $alternate ^= 1;
1002                 if ($i-- > 0) {
1003                         print "<td><i>$co{'age_string'}</i></td>\n" .
1004                               "<td><i>" . escapeHTML(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
1005                               "<td>";
1006                         if (length($co{'title_short'}) < length($co{'title'})) {
1007                                 print $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit"), -class => "list", -title => "$co{'title'}"},
1008                                       "<b>" . escapeHTML($co{'title_short'}) . "</b>");
1009                         } else {
1010                                 print $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit"), -class => "list"},
1011                                       "<b>" . escapeHTML($co{'title'}) . "</b>");
1012                         }
1013                         print "</td>\n" .
1014                               "<td class=\"link\">" .
1015                               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit")}, "commit") .
1016                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
1017                               "</td>\n" .
1018                               "</tr>";
1019                 } else {
1020                         print "<td>" . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "...") . "</td>\n" .
1021                         "</tr>";
1022                         last;
1023                 }
1024         }
1025         print "</table\n>";
1026
1027         my $taglist = git_read_refs("refs/tags");
1028         if (defined @$taglist) {
1029                 print "<div>\n" .
1030                       $cgi->a({-href => esc("$my_uri?p=$project;a=tags"), -class => "title"}, "tags") .
1031                       "</div>\n";
1032                 my $i = 16;
1033                 print "<table cellspacing=\"0\">\n";
1034                 my $alternate = 0;
1035                 foreach my $entry (@$taglist) {
1036                         my %tag = %$entry;
1037                         my $comment_lines = $tag{'comment'};
1038                         my $comment = shift @$comment_lines;
1039                         if (defined($comment)) {
1040                                 $comment = chop_str($comment, 30, 5);
1041                         }
1042                         if ($alternate) {
1043                                 print "<tr class=\"dark\">\n";
1044                         } else {
1045                                 print "<tr class=\"light\">\n";
1046                         }
1047                         $alternate ^= 1;
1048                         if ($i-- > 0) {
1049                                 print "<td><i>$tag{'age'}</i></td>\n" .
1050                                       "<td>" .
1051                                       $cgi->a({-href => esc("$my_uri?p=$project;a=$tag{'reftype'};h=$tag{'refid'}"), -class => "list"},
1052                                       "<b>" . escapeHTML($tag{'name'}) . "</b>") .
1053                                       "</td>\n" .
1054                                       "<td>";
1055                                 if (defined($comment)) {
1056                                       print $cgi->a({-class => "list", -href => esc("$my_uri?p=$project;a=tag;h=$tag{'id'}")}, $comment);
1057                                 }
1058                                 print "</td>\n" .
1059                                       "<td class=\"link\">";
1060                                 if ($tag{'type'} eq "tag") {
1061                                       print $cgi->a({-href => esc("$my_uri?p=$project;a=tag;h=$tag{'id'}")}, "tag") . " | ";
1062                                 }
1063                                 print $cgi->a({-href => esc("$my_uri?p=$project;a=$tag{'reftype'};h=$tag{'refid'}")}, $tag{'reftype'});
1064                                 if ($tag{'reftype'} eq "commit") {
1065                                       print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
1066                                             " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$tag{'refid'}")}, "log");
1067                                 }
1068                                 print "</td>\n" .
1069                                       "</tr>";
1070                         } else {
1071                                 print "<td>" . $cgi->a({-href => esc("$my_uri?p=$project;a=tags")}, "...") . "</td>\n" .
1072                                 "</tr>";
1073                                 last;
1074                         }
1075                 }
1076                 print "</table\n>";
1077         }
1078
1079         my $headlist = git_read_refs("refs/heads");
1080         if (defined @$headlist) {
1081                 print "<div>\n" .
1082                       $cgi->a({-href => esc("$my_uri?p=$project;a=heads"), -class => "title"}, "heads") .
1083                       "</div>\n";
1084                 my $i = 16;
1085                 print "<table cellspacing=\"0\">\n";
1086                 my $alternate = 0;
1087                 foreach my $entry (@$headlist) {
1088                         my %tag = %$entry;
1089                         if ($alternate) {
1090                                 print "<tr class=\"dark\">\n";
1091                         } else {
1092                                 print "<tr class=\"light\">\n";
1093                         }
1094                         $alternate ^= 1;
1095                         if ($i-- > 0) {
1096                                 print "<td><i>$tag{'age'}</i></td>\n" .
1097                                       "<td>" .
1098                                       $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$tag{'name'}"), -class => "list"},
1099                                       "<b>" . escapeHTML($tag{'name'}) . "</b>") .
1100                                       "</td>\n" .
1101                                       "<td class=\"link\">" .
1102                                       $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
1103                                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$tag{'name'}")}, "log") .
1104                                       "</td>\n" .
1105                                       "</tr>";
1106                         } else {
1107                                 print "<td>" . $cgi->a({-href => esc("$my_uri?p=$project;a=heads")}, "...") . "</td>\n" .
1108                                 "</tr>";
1109                                 last;
1110                         }
1111                 }
1112                 print "</table\n>";
1113         }
1114         git_footer_html();
1115 }
1116
1117 sub git_tag {
1118         my $head = git_read_hash("$project/HEAD");
1119         git_header_html();
1120         print "<div class=\"page_nav\">\n" .
1121               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1122               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
1123               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
1124               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$head")}, "commit") .
1125               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$head")}, "commitdiff") .
1126               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;hb=$head")}, "tree") . "<br/>\n" .
1127               "<br/>\n" .
1128               "</div>\n";
1129         my %tag = git_read_tag($hash);
1130         print "<div>\n" .
1131               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash"), -class => "title"}, escapeHTML($tag{'name'})) . "\n" .
1132               "</div>\n";
1133         print "<div class=\"title_text\">\n" .
1134               "<table cellspacing=\"0\">\n" .
1135               "<tr>\n" .
1136               "<td>object</td>\n" .
1137               "<td>" . $cgi->a({-class => "list", -href => esc("$my_uri?p=$project;a=$tag{'type'};h=$tag{'object'}")}, $tag{'object'}) . "</td>\n" .
1138               "<td class=\"link\">" . $cgi->a({-href => esc("$my_uri?p=$project;a=$tag{'type'};h=$tag{'object'}")}, $tag{'type'}) . "</td>\n" .
1139               "</tr>\n";
1140         if (defined($tag{'author'})) {
1141                 my %ad = date_str($tag{'epoch'}, $tag{'tz'});
1142                 print "<tr><td>author</td><td>" . escapeHTML($tag{'author'}) . "</td></tr>\n";
1143                 print "<tr><td></td><td>" . $ad{'rfc2822'} . sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) . "</td></tr>\n";
1144         }
1145         print "</table>\n\n" .
1146               "</div>\n";
1147         print "<div class=\"page_body\">";
1148         my $comment = $tag{'comment'};
1149         foreach my $line (@$comment) {
1150                 print escapeHTML($line) . "<br/>\n";
1151         }
1152         print "</div>\n";
1153         git_footer_html();
1154 }
1155
1156 sub git_tags {
1157         my $head = git_read_hash("$project/HEAD");
1158         git_header_html();
1159         print "<div class=\"page_nav\">\n" .
1160               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1161               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
1162               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
1163               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$head")}, "commit") .
1164               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$head")}, "commitdiff") .
1165               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;hb=$head")}, "tree") . "<br/>\n" .
1166               "<br/>\n" .
1167               "</div>\n";
1168         my $taglist = git_read_refs("refs/tags");
1169         print "<div>\n" .
1170               $cgi->a({-href => esc("$my_uri?p=$project;a=summary"), -class => "title"}, "&nbsp;") .
1171               "</div>\n";
1172         print "<table cellspacing=\"0\">\n";
1173         my $alternate = 0;
1174         if (defined @$taglist) {
1175                 foreach my $entry (@$taglist) {
1176                         my %tag = %$entry;
1177                         my $comment_lines = $tag{'comment'};
1178                         my $comment = shift @$comment_lines;
1179                         if (defined($comment)) {
1180                                 $comment = chop_str($comment, 30, 5);
1181                         }
1182                         if ($alternate) {
1183                                 print "<tr class=\"dark\">\n";
1184                         } else {
1185                                 print "<tr class=\"light\">\n";
1186                         }
1187                         $alternate ^= 1;
1188                         print "<td><i>$tag{'age'}</i></td>\n" .
1189                               "<td>" .
1190                               $cgi->a({-href => esc("$my_uri?p=$project;a=$tag{'reftype'};h=$tag{'refid'}"), -class => "list"},
1191                               "<b>" . escapeHTML($tag{'name'}) . "</b>") .
1192                               "</td>\n" .
1193                               "<td>";
1194                         if (defined($comment)) {
1195                               print $cgi->a({-class => "list", -href => esc("$my_uri?p=$project;a=tag;h=$tag{'id'}")}, $comment);
1196                         }
1197                         print "</td>\n" .
1198                               "<td class=\"link\">";
1199                         if ($tag{'type'} eq "tag") {
1200                               print $cgi->a({-href => esc("$my_uri?p=$project;a=tag;h=$tag{'id'}")}, "tag") . " | ";
1201                         }
1202                         print $cgi->a({-href => esc("$my_uri?p=$project;a=$tag{'reftype'};h=$tag{'refid'}")}, $tag{'reftype'});
1203                         if ($tag{'reftype'} eq "commit") {
1204                               print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
1205                                     " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$tag{'refid'}")}, "log");
1206                         }
1207                         print "</td>\n" .
1208                               "</tr>";
1209                 }
1210         }
1211         print "</table\n>";
1212         git_footer_html();
1213 }
1214
1215 sub git_heads {
1216         my $head = git_read_hash("$project/HEAD");
1217         git_header_html();
1218         print "<div class=\"page_nav\">\n" .
1219               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1220               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
1221               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
1222               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$head")}, "commit") .
1223               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$head")}, "commitdiff") .
1224               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;hb=$head")}, "tree") . "<br/>\n" .
1225               "<br/>\n" .
1226               "</div>\n";
1227         my $taglist = git_read_refs("refs/heads");
1228         print "<div>\n" .
1229               $cgi->a({-href => esc("$my_uri?p=$project;a=summary"), -class => "title"}, "&nbsp;") .
1230               "</div>\n";
1231         print "<table cellspacing=\"0\">\n";
1232         my $alternate = 0;
1233         if (defined @$taglist) {
1234                 foreach my $entry (@$taglist) {
1235                         my %tag = %$entry;
1236                         if ($alternate) {
1237                                 print "<tr class=\"dark\">\n";
1238                         } else {
1239                                 print "<tr class=\"light\">\n";
1240                         }
1241                         $alternate ^= 1;
1242                         print "<td><i>$tag{'age'}</i></td>\n" .
1243                               "<td>" .
1244                               $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$tag{'name'}"), -class => "list"}, "<b>" . escapeHTML($tag{'name'}) . "</b>") .
1245                               "</td>\n" .
1246                               "<td class=\"link\">" .
1247                               $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$tag{'name'}")}, "shortlog") .
1248                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$tag{'name'}")}, "log") .
1249                               "</td>\n" .
1250                               "</tr>";
1251                 }
1252         }
1253         print "</table\n>";
1254         git_footer_html();
1255 }
1256
1257 sub git_get_hash_by_path {
1258         my $base = shift;
1259         my $path = shift || return undef;
1260
1261         my $tree = $base;
1262         my @parts = split '/', $path;
1263         while (my $part = shift @parts) {
1264                 open my $fd, "-|", "$gitbin/git-ls-tree $tree" or die_error(undef, "Open git-ls-tree failed.");
1265                 my (@entries) = map { chomp; $_ } <$fd>;
1266                 close $fd or return undef;
1267                 foreach my $line (@entries) {
1268                         #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
1269                         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
1270                         my $t_mode = $1;
1271                         my $t_type = $2;
1272                         my $t_hash = $3;
1273                         my $t_name = $4;
1274                         if ($t_name eq $part) {
1275                                 if (!(@parts)) {
1276                                         return $t_hash;
1277                                 }
1278                                 if ($t_type eq "tree") {
1279                                         $tree = $t_hash;
1280                                 }
1281                                 last;
1282                         }
1283                 }
1284         }
1285 }
1286
1287 sub git_blob {
1288         if (!defined $hash && defined $file_name) {
1289                 my $base = $hash_base || git_read_hash("$project/HEAD");
1290                 $hash = git_get_hash_by_path($base, $file_name, "blob");
1291         }
1292         open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or die_error(undef, "Open failed.");
1293         git_header_html();
1294         if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
1295                 print "<div class=\"page_nav\">\n" .
1296                       $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1297                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
1298                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
1299                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash_base")}, "commit") .
1300                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
1301                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") . "<br/>\n";
1302                 if (defined $file_name) {
1303                         print $cgi->a({-href => esc("$my_uri?p=$project;a=blob_plain;h=$hash;f=$file_name")}, "plain") . "<br/>\n";
1304                 } else {
1305                         print $cgi->a({-href => esc("$my_uri?p=$project;a=blob_plain;h=$hash")}, "plain") . "<br/>\n";
1306                 }
1307                 print "</div>\n".
1308                        "<div>" .
1309                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash_base"), -class => "title"}, escapeHTML($co{'title'})) .
1310                       "</div>\n";
1311         } else {
1312                 print "<div class=\"page_nav\">\n" .
1313                       "<br/><br/></div>\n" .
1314                       "<div class=\"title\">$hash</div>\n";
1315         }
1316         if (defined $file_name) {
1317                 print "<div class=\"page_path\"><b>$file_name</b></div>\n";
1318         }
1319         print "<div class=\"page_body\">\n";
1320         my $nr;
1321         while (my $line = <$fd>) {
1322                 chomp $line;
1323                 $nr++;
1324                 while ((my $pos = index($line, "\t")) != -1) {
1325                         if (my $count = (8 - ($pos % 8))) {
1326                                 my $spaces = ' ' x $count;
1327                                 $line =~ s/\t/$spaces/;
1328                         }
1329                 }
1330                 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n", $nr, $nr, $nr, escapeHTML($line);
1331         }
1332         close $fd or print "Reading blob failed.\n";
1333         print "</div>";
1334         git_footer_html();
1335 }
1336
1337 sub git_blob_plain {
1338         my $save_as = "$hash.txt";
1339         if (defined $file_name) {
1340                 $save_as = $file_name;
1341         }
1342         print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"$save_as\"");
1343         open my $fd, "-|", "$gitbin/git-cat-file blob $hash" or return;
1344         undef $/;
1345         print <$fd>;
1346         $/ = "\n";
1347         close $fd;
1348 }
1349
1350 sub git_tree {
1351         if (!defined $hash) {
1352                 $hash = git_read_hash("$project/HEAD");
1353                 if (defined $file_name) {
1354                         my $base = $hash_base || git_read_hash("$project/HEAD");
1355                         $hash = git_get_hash_by_path($base, $file_name, "tree");
1356                 }
1357                 if (!defined $hash_base) {
1358                         $hash_base = git_read_hash("$project/HEAD");
1359                 }
1360         }
1361         open my $fd, "-|", "$gitbin/git-ls-tree $hash" or die_error(undef, "Open git-ls-tree failed.");
1362         my (@entries) = map { chomp; $_ } <$fd>;
1363         close $fd or die_error(undef, "Reading tree failed.");
1364
1365         git_header_html();
1366         my $base_key = "";
1367         my $file_key = "";
1368         my $base = "";
1369         if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
1370                 $base_key = ";hb=$hash_base";
1371                 print "<div class=\"page_nav\">\n" .
1372                       $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1373                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash_base")}, "shortlog") .
1374                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash_base")}, "log") .
1375                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash_base")}, "commit") .
1376                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
1377                       " | tree" .
1378                       "<br/><br/>\n" .
1379                       "</div>\n";
1380                 print "<div>\n" .
1381                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash_base"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1382                       "</div>\n";
1383         } else {
1384                 print "<div class=\"page_nav\">\n";
1385                 print "<br/><br/></div>\n";
1386                 print "<div class=\"title\">$hash</div>\n";
1387         }
1388         if (defined $file_name) {
1389                 $base = "$file_name/";
1390                 print "<div class=\"page_path\"><b>/$file_name</b></div>\n";
1391         } else {
1392                 print "<div class=\"page_path\"><b>/</b></div>\n";
1393         }
1394         print "<div class=\"page_body\">\n";
1395         print "<table cellspacing=\"0\">\n";
1396         my $alternate = 0;
1397         foreach my $line (@entries) {
1398                 #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
1399                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
1400                 my $t_mode = $1;
1401                 my $t_type = $2;
1402                 my $t_hash = $3;
1403                 my $t_name = $4;
1404                 $file_key = ";f=$base$t_name";
1405                 if ($alternate) {
1406                         print "<tr class=\"dark\">\n";
1407                 } else {
1408                         print "<tr class=\"light\">\n";
1409                 }
1410                 $alternate ^= 1;
1411                 print "<td style=\"font-family:monospace\">" . mode_str($t_mode) . "</td>\n";
1412                 if ($t_type eq "blob") {
1413                         print "<td class=\"list\">" .
1414                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$t_hash" . $base_key . $file_key), -class => "list"}, $t_name) .
1415                               "</td>\n" .
1416                               "<td class=\"link\">" .
1417                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$t_hash" . $base_key . $file_key)}, "blob") .
1418                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=history;h=$hash_base" . $file_key)}, "history") .
1419                               "</td>\n";
1420                 } elsif ($t_type eq "tree") {
1421                         print "<td class=\"list\">" .
1422                               $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$t_hash" . $base_key . $file_key)}, $t_name) .
1423                               "</td>\n" .
1424                               "<td class=\"link\">" .
1425                               $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$t_hash" . $base_key . $file_key)}, "tree") .
1426                               "</td>\n";
1427                 }
1428                 print "</tr>\n";
1429         }
1430         print "</table>\n" .
1431               "</div>";
1432         git_footer_html();
1433 }
1434
1435 sub git_rss {
1436         # http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
1437         open my $fd, "-|", "$gitbin/git-rev-list --max-count=150 " . git_read_hash("$project/HEAD") or die_error(undef, "Open failed.");
1438         my (@revlist) = map { chomp; $_ } <$fd>;
1439         close $fd or die_error(undef, "Reading rev-list failed.");
1440         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
1441         print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
1442               "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n";
1443         print "<channel>\n";
1444         print "<title>$project</title>\n".
1445               "<link>" . escapeHTML("$my_url?p=$project;a=summary") . "</link>\n".
1446               "<description>$project log</description>\n".
1447               "<language>en</language>\n";
1448
1449         for (my $i = 0; $i <= $#revlist; $i++) {
1450                 my $commit = $revlist[$i];
1451                 my %co = git_read_commit($commit);
1452                 # we read 150, we always show 30 and the ones more recent than 48 hours
1453                 if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
1454                         last;
1455                 }
1456                 my %cd = date_str($co{'committer_epoch'});
1457                 open $fd, "-|", "$gitbin/git-diff-tree -r $co{'parent'} $co{'id'}" or next;
1458                 my @difftree = map { chomp; $_ } <$fd>;
1459                 close $fd or next;
1460                 print "<item>\n" .
1461                       "<title>" .
1462                       sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . escapeHTML($co{'title'}) .
1463                       "</title>\n" .
1464                       "<author>" . escapeHTML($co{'author'}) . "</author>\n" .
1465                       "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
1466                       "<guid isPermaLink=\"true\">" . escapeHTML("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
1467                       "<link>" . escapeHTML("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
1468                       "<description>" . escapeHTML($co{'title'}) . "</description>\n" .
1469                       "<content:encoded>" .
1470                       "<![CDATA[\n";
1471                 my $comment = $co{'comment'};
1472                 foreach my $line (@$comment) {
1473                         print "$line<br/>\n";
1474                 }
1475                 print "<br/>\n";
1476                 foreach my $line (@difftree) {
1477                         if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
1478                                 next;
1479                         }
1480                         my $file = $7;
1481                         print "$file<br/>\n";
1482                 }
1483                 print "]]>\n" .
1484                       "</content:encoded>\n" .
1485                       "</item>\n";
1486         }
1487         print "</channel></rss>";
1488 }
1489
1490 sub git_opml {
1491         my @list = git_read_projects();
1492
1493         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
1494         print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
1495               "<opml version=\"1.0\">\n".
1496               "<head>".
1497               "  <title>Git OPML Export</title>\n".
1498               "</head>\n".
1499               "<body>\n".
1500               "<outline text=\"git RSS feeds\">\n";
1501
1502         foreach my $pr (@list) {
1503                 my %proj = %$pr;
1504                 my $head = git_read_hash("$proj{'path'}/HEAD");
1505                 if (!defined $head) {
1506                         next;
1507                 }
1508                 $ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}";
1509                 my %co = git_read_commit($head);
1510                 if (!%co) {
1511                         next;
1512                 }
1513
1514                 my $path = escapeHTML(chop_str($proj{'path'}, 25, 5));
1515                 my $rss =  "$my_url?p=$proj{'path'};a=rss";
1516                 my $html =  "$my_url?p=$proj{'path'};a=summary";
1517                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
1518         }
1519         print "</outline>\n".
1520               "</body>\n".
1521               "</opml>\n";
1522 }
1523
1524 sub git_log {
1525         my $head = git_read_hash("$project/HEAD");
1526         if (!defined $hash) {
1527                 $hash = $head;
1528         }
1529         if (!defined $page) {
1530                 $page = 0;
1531         }
1532         git_header_html();
1533         print "<div class=\"page_nav\">\n";
1534         print $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1535               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash")}, "shortlog") .
1536               " | log" .
1537               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash")}, "commit") .
1538               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
1539               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$hash;hb=$hash")}, "tree") . "<br/>\n";
1540
1541         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
1542         open my $fd, "-|", "$gitbin/git-rev-list $limit $hash" or die_error(undef, "Open failed.");
1543         my (@revlist) = map { chomp; $_ } <$fd>;
1544         close $fd;
1545
1546         if ($hash ne $head || $page) {
1547                 print $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "HEAD");
1548         } else {
1549                 print "HEAD";
1550         }
1551         if ($page > 0) {
1552                 print " &sdot; " .
1553                 $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash;pg=" . ($page-1)), -accesskey => "p", -title => "Alt-p"}, "prev");
1554         } else {
1555                 print " &sdot; prev";
1556         }
1557         if ($#revlist >= (100 * ($page+1)-1)) {
1558                 print " &sdot; " .
1559                 $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash;pg=" . ($page+1)), -accesskey => "n", -title => "Alt-n"}, "next");
1560         } else {
1561                 print " &sdot; next";
1562         }
1563         print "<br/>\n" .
1564               "</div>\n";
1565         if (!@revlist) {
1566                 print "<div>\n" .
1567                       $cgi->a({-href => esc("$my_uri?p=$project;a=summary"), -class => "title"}, "&nbsp;") .
1568                       "</div>\n";
1569                 my %co = git_read_commit($hash);
1570                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
1571         }
1572         for (my $i = ($page * 100); $i <= $#revlist; $i++) {
1573                 my $commit = $revlist[$i];
1574                 my %co = git_read_commit($commit);
1575                 next if !%co;
1576                 my %ad = date_str($co{'author_epoch'});
1577                 print "<div>\n" .
1578                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit"), -class => "title"},
1579                       "<span class=\"age\">$co{'age_string'}</span>" . escapeHTML($co{'title'})) . "\n" .
1580                       "</div>\n";
1581                 print "<div class=\"title_text\">\n" .
1582                       "<div class=\"log_link\">\n" .
1583                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit")}, "commit") .
1584                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
1585                       "<br/>\n" .
1586                       "</div>\n" .
1587                       "<i>" . escapeHTML($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
1588                       "</div>\n" .
1589                       "<div class=\"log_body\">\n";
1590                 my $comment = $co{'comment'};
1591                 my $empty = 0;
1592                 foreach my $line (@$comment) {
1593                         if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
1594                                 next;
1595                         }
1596                         if ($line eq "") {
1597                                 if ($empty) {
1598                                         next;
1599                                 }
1600                                 $empty = 1;
1601                         } else {
1602                                 $empty = 0;
1603                         }
1604                         print format_log_line_html($line) . "<br/>\n";
1605                 }
1606                 if (!$empty) {
1607                         print "<br/>\n";
1608                 }
1609                 print "</div>\n";
1610         }
1611         git_footer_html();
1612 }
1613
1614 sub git_commit {
1615         my %co = git_read_commit($hash);
1616         if (!%co) {
1617                 die_error(undef, "Unknown commit object.");
1618         }
1619         my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
1620         my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
1621
1622         my @difftree;
1623         my $root = "";
1624         my $parent = $co{'parent'};
1625         if (!defined $parent) {
1626                 $root = " --root";
1627                 $parent = "";
1628         }
1629         open my $fd, "-|", "$gitbin/git-diff-tree -r -M $root $parent $hash" or die_error(undef, "Open failed.");
1630         @difftree = map { chomp; $_ } <$fd>;
1631         close $fd or die_error(undef, "Reading diff-tree failed.");
1632
1633         # non-textual hash id's can be cached
1634         my $expires;
1635         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
1636                 $expires = "+1d";
1637         }
1638         git_header_html(undef, $expires);
1639         print "<div class=\"page_nav\">\n" .
1640               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1641               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash")}, "shortlog") .
1642               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash")}, "log") .
1643               " | commit";
1644         if (defined $co{'parent'}) {
1645                 print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash")}, "commitdiff");
1646         }
1647         print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") . "\n" .
1648               "<br/><br/></div>\n";
1649         if (defined $co{'parent'}) {
1650                 print "<div>\n" .
1651                       $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1652                       "</div>\n";
1653         } else {
1654                 print "<div>\n" .
1655                       $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1656                       "</div>\n";
1657         }
1658         print "<div class=\"title_text\">\n" .
1659               "<table cellspacing=\"0\">\n";
1660         print "<tr><td>author</td><td>" . escapeHTML($co{'author'}) . "</td></tr>\n".
1661               "<tr>" .
1662               "<td></td><td> $ad{'rfc2822'}";
1663         if ($ad{'hour_local'} < 6) {
1664                 printf(" (<span style=\"color: #cc0000;\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
1665         } else {
1666                 printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
1667         }
1668         print "</td>" .
1669               "</tr>\n";
1670         print "<tr><td>committer</td><td>" . escapeHTML($co{'committer'}) . "</td></tr>\n";
1671         print "<tr><td></td><td> $cd{'rfc2822'}" . sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
1672         print "<tr><td>commit</td><td style=\"font-family:monospace\">$co{'id'}</td></tr>\n";
1673         print "<tr>" .
1674               "<td>tree</td>" .
1675               "<td style=\"font-family:monospace\">" .
1676               $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash"), class => "list"}, $co{'tree'}) .
1677               "</td>" .
1678               "<td class=\"link\">" . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
1679               "</td>" .
1680               "</tr>\n";
1681         my $parents  = $co{'parents'};
1682         foreach my $par (@$parents) {
1683                 print "<tr>" .
1684                       "<td>parent</td>" .
1685                       "<td style=\"font-family:monospace\">" . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$par"), class => "list"}, $par) . "</td>" .
1686                       "<td class=\"link\">" .
1687                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$par")}, "commit") .
1688                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash;hp=$par")}, "commitdiff") .
1689                       "</td>" .
1690                       "</tr>\n";
1691         }
1692         print "</table>". 
1693               "</div>\n";
1694         print "<div class=\"page_body\">\n";
1695         my $comment = $co{'comment'};
1696         my $empty = 0;
1697         my $signed = 0;
1698         foreach my $line (@$comment) {
1699                 # print only one empty line
1700                 if ($line eq "") {
1701                         if ($empty || $signed) {
1702                                 next;
1703                         }
1704                         $empty = 1;
1705                 } else {
1706                         $empty = 0;
1707                 }
1708                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
1709                         $signed = 1;
1710                         print "<span style=\"color: #888888\">" . escapeHTML($line) . "</span><br/>\n";
1711                 } else {
1712                         $signed = 0;
1713                         print format_log_line_html($line) . "<br/>\n";
1714                 }
1715         }
1716         print "</div>\n";
1717         print "<div class=\"list_head\">\n";
1718         if ($#difftree > 10) {
1719                 print(($#difftree + 1) . " files changed:\n");
1720         }
1721         print "</div>\n";
1722         print "<table cellspacing=\"0\">\n";
1723         my $alternate = 0;
1724         foreach my $line (@difftree) {
1725                 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M      ls-files.c'
1726                 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M      rev-tree.c'
1727                 if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
1728                         next;
1729                 }
1730                 my $from_mode = $1;
1731                 my $to_mode = $2;
1732                 my $from_id = $3;
1733                 my $to_id = $4;
1734                 my $status = $5;
1735                 my $similarity = $6;
1736                 my $file = $7;
1737                 if ($alternate) {
1738                         print "<tr class=\"dark\">\n";
1739                 } else {
1740                         print "<tr class=\"light\">\n";
1741                 }
1742                 $alternate ^= 1;
1743                 if ($status eq "A") {
1744                         my $mode_chng = "";
1745                         if (S_ISREG(oct $to_mode)) {
1746                                 $mode_chng = sprintf(" with mode: %04o", (oct $to_mode) & 0777);
1747                         }
1748                         print "<td>" .
1749                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file"), -class => "list"}, escapeHTML($file)) . "</td>\n" .
1750                               "<td><span style=\"color: #008000;\">[new " . file_type($to_mode) . "$mode_chng]</span></td>\n" .
1751                               "<td class=\"link\">" . $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, "blob") . "</td>\n";
1752                 } elsif ($status eq "D") {
1753                         print "<td>" .
1754                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$from_id;hb=$hash;f=$file"), -class => "list"}, escapeHTML($file)) . "</td>\n" .
1755                               "<td><span style=\"color: #c00000;\">[deleted " . file_type($from_mode). "]</span></td>\n" .
1756                               "<td class=\"link\">" .
1757                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, "blob") .
1758                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=history;h=$hash;f=$file")}, "history") .
1759                               "</td>\n"
1760                 } elsif ($status eq "M" || $status eq "T") {
1761                         my $mode_chnge = "";
1762                         if ($from_mode != $to_mode) {
1763                                 $mode_chnge = " <span style=\"color: #777777;\">[changed";
1764                                 if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) {
1765                                         $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode);
1766                                 }
1767                                 if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) {
1768                                         if (S_ISREG($from_mode) && S_ISREG($to_mode)) {
1769                                                 $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777);
1770                                         } elsif (S_ISREG($to_mode)) {
1771                                                 $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777);
1772                                         }
1773                                 }
1774                                 $mode_chnge .= "]</span>\n";
1775                         }
1776                         print "<td>";
1777                         if ($to_id ne $from_id) {
1778                                 print $cgi->a({-href => esc("$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file"), -class => "list"}, escapeHTML($file));
1779                         } else {
1780                                 print $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file"), -class => "list"}, escapeHTML($file));
1781                         }
1782                         print "</td>\n" .
1783                               "<td>$mode_chnge</td>\n" .
1784                               "<td class=\"link\">";
1785                         print $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, "blob");
1786                         if ($to_id ne $from_id) {
1787                                 print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$file")}, "diff");
1788                         }
1789                         print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=history;h=$hash;f=$file")}, "history") . "\n";
1790                         print "</td>\n";
1791                 } elsif ($status eq "R") {
1792                         my ($from_file, $to_file) = split "\t", $file;
1793                         my $mode_chng = "";
1794                         if ($from_mode != $to_mode) {
1795                                 $mode_chng = sprintf(", mode: %04o", (oct $to_mode) & 0777);
1796                         }
1797                         print "<td>" .
1798                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$to_file"), -class => "list"}, escapeHTML($to_file)) . "</td>\n" .
1799                               "<td><span style=\"color: #777777;\">[moved from " .
1800                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$from_id;hb=$hash;f=$from_file"), -class => "list"}, escapeHTML($from_file)) .
1801                               " with " . (int $similarity) . "% similarity$mode_chng]</span></td>\n" .
1802                               "<td class=\"link\">" .
1803                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$to_file")}, "blob");
1804                         if ($to_id ne $from_id) {
1805                                 print " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id;hb=$hash;f=$to_file")}, "diff");
1806                         }
1807                         print "</td>\n";
1808                 }
1809                 print "</tr>\n";
1810         }
1811         print "</table>\n";
1812         git_footer_html();
1813 }
1814
1815 sub git_blobdiff {
1816         mkdir($git_temp, 0700);
1817         git_header_html();
1818         if (defined $hash_base && (my %co = git_read_commit($hash_base))) {
1819                 print "<div class=\"page_nav\">\n" .
1820                       $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1821                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
1822                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
1823                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash_base")}, "commit") .
1824                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash_base")}, "commitdiff") .
1825                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash_base")}, "tree") .
1826                       "<br/>\n";
1827                 print $cgi->a({-href => esc("$my_uri?p=$project;a=blobdiff_plain;h=$hash;hp=$hash_parent")}, "plain") .
1828                       "</div>\n";
1829                 print "<div>\n" .
1830                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash_base"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1831                       "</div>\n";
1832         } else {
1833                 print "<div class=\"page_nav\">\n" .
1834                       "<br/><br/></div>\n" .
1835                       "<div class=\"title\">$hash vs $hash_parent</div>\n";
1836         }
1837         if (defined $file_name) {
1838                 print "<div class=\"page_path\"><b>/$file_name</b></div>\n";
1839         }
1840         print "<div class=\"page_body\">\n" .
1841               "<div class=\"diff_info\">blob:" .
1842               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$hash_parent;hb=$hash_base;f=$file_name")}, $hash_parent) .
1843               " -> blob:" .
1844               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$hash;hb=$hash_base;f=$file_name")}, $hash) .
1845               "</div>\n";
1846         git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
1847         print "</div>";
1848         git_footer_html();
1849 }
1850
1851 sub git_blobdiff_plain {
1852         mkdir($git_temp, 0700);
1853         print $cgi->header(-type => "text/plain", -charset => 'utf-8');
1854         git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain");
1855 }
1856
1857 sub git_commitdiff {
1858         mkdir($git_temp, 0700);
1859         my %co = git_read_commit($hash);
1860         if (!%co) {
1861                 die_error(undef, "Unknown commit object.");
1862         }
1863         if (!defined $hash_parent) {
1864                 $hash_parent = $co{'parent'};
1865         }
1866         open my $fd, "-|", "$gitbin/git-diff-tree -r $hash_parent $hash" or die_error(undef, "Open failed.");
1867         my (@difftree) = map { chomp; $_ } <$fd>;
1868         close $fd or die_error(undef, "Reading diff-tree failed.");
1869
1870         # non-textual hash id's can be cached
1871         my $expires;
1872         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
1873                 $expires = "+1d";
1874         }
1875         git_header_html(undef, $expires);
1876         print "<div class=\"page_nav\">\n" .
1877               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
1878               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash")}, "shortlog") .
1879               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash")}, "log") .
1880               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash")}, "commit") .
1881               " | commitdiff" .
1882               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") . "<br/>\n";
1883         print $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff_plain;h=$hash;hp=$hash_parent")}, "plain") . "\n" .
1884               "</div>\n";
1885         print "<div>\n" .
1886               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
1887               "</div>\n";
1888         print "<div class=\"page_body\">\n";
1889         my $comment = $co{'comment'};
1890         my $empty = 0;
1891         my $signed = 0;
1892         my @log = @$comment;
1893         # remove first and empty lines after that
1894         shift @log;
1895         while (defined $log[0] && $log[0] eq "") {
1896                 shift @log;
1897         }
1898         foreach my $line (@log) {
1899                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
1900                         next;
1901                 }
1902                 if ($line eq "") {
1903                         if ($empty) {
1904                                 next;
1905                         }
1906                         $empty = 1;
1907                 } else {
1908                         $empty = 0;
1909                 }
1910                 print format_log_line_html($line) . "<br/>\n";
1911         }
1912         print "<br/>\n";
1913         foreach my $line (@difftree) {
1914                 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M      ls-files.c'
1915                 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M      rev-tree.c'
1916                 $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
1917                 my $from_mode = $1;
1918                 my $to_mode = $2;
1919                 my $from_id = $3;
1920                 my $to_id = $4;
1921                 my $status = $5;
1922                 my $file = $6;
1923                 if ($status eq "A") {
1924                         print "<div class=\"diff_info\">" .  file_type($to_mode) . ":" .
1925                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id) . "(new)" .
1926                               "</div>\n";
1927                         git_diff_print(undef, "/dev/null", $to_id, "b/$file");
1928                 } elsif ($status eq "D") {
1929                         print "<div class=\"diff_info\">" . file_type($from_mode) . ":" .
1930                               $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) . "(deleted)" .
1931                               "</div>\n";
1932                         git_diff_print($from_id, "a/$file", undef, "/dev/null");
1933                 } elsif ($status eq "M") {
1934                         if ($from_id ne $to_id) {
1935                                 print "<div class=\"diff_info\">" .
1936                                       file_type($from_mode) . ":" . $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$from_id;hb=$hash;f=$file")}, $from_id) .
1937                                       " -> " .
1938                                       file_type($to_mode) . ":" . $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$to_id;hb=$hash;f=$file")}, $to_id);
1939                                 print "</div>\n";
1940                                 git_diff_print($from_id, "a/$file",  $to_id, "b/$file");
1941                         }
1942                 }
1943         }
1944         print "<br/>\n" .
1945               "</div>";
1946         git_footer_html();
1947 }
1948
1949 sub git_commitdiff_plain {
1950         mkdir($git_temp, 0700);
1951         open my $fd, "-|", "$gitbin/git-diff-tree -r $hash_parent $hash" or die_error(undef, "Open failed.");
1952         my (@difftree) = map { chomp; $_ } <$fd>;
1953         close $fd or die_error(undef, "Reading diff-tree failed.");
1954
1955         # try to figure out the next tag after this commit
1956         my $tagname;
1957         my %taghash;
1958         my $tags = git_read_refs("refs/tags");
1959         foreach my $entry (@$tags) {
1960                 my %tag = %$entry;
1961                 $taghash{$tag{'refid'}} = $tag{'name'};
1962         }
1963         open $fd, "-|", "$gitbin/git-rev-list HEAD";
1964         while (my $commit = <$fd>) {
1965                 chomp $commit;
1966                 if ($taghash{$commit}) {
1967                         $tagname = $taghash{$commit};
1968                 }
1969                 if ($commit eq $hash) {
1970                         last;
1971                 }
1972         }
1973         close $fd;
1974
1975         print $cgi->header(-type => "text/plain", -charset => 'utf-8', '-content-disposition' => "inline; filename=\"git-$hash.patch\"");
1976         my %co = git_read_commit($hash);
1977         my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
1978         my $comment = $co{'comment'};
1979         print "From: $co{'author'}\n" .
1980               "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n".
1981               "Subject: $co{'title'}\n";
1982         if (defined $tagname) {
1983               print "X-Git-Tag: $tagname\n";
1984         }
1985         print "X-Git-Url: $my_url?p=$project;a=commitdiff;h=$hash\n" .
1986               "\n";
1987
1988         foreach my $line (@$comment) {;
1989                 print "  $line\n";
1990         }
1991         print "---\n\n";
1992
1993         foreach my $line (@difftree) {
1994                 $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/;
1995                 my $from_id = $3;
1996                 my $to_id = $4;
1997                 my $status = $5;
1998                 my $file = $6;
1999                 if ($status eq "A") {
2000                         git_diff_print(undef, "/dev/null", $to_id, "b/$file", "plain");
2001                 } elsif ($status eq "D") {
2002                         git_diff_print($from_id, "a/$file", undef, "/dev/null", "plain");
2003                 } elsif ($status eq "M") {
2004                         git_diff_print($from_id, "a/$file",  $to_id, "b/$file", "plain");
2005                 }
2006         }
2007 }
2008
2009 sub git_history {
2010         if (!defined $hash) {
2011                 $hash = git_read_hash("$project/HEAD");
2012         }
2013         my %co = git_read_commit($hash);
2014         if (!%co) {
2015                 die_error(undef, "Unknown commit object.");
2016         }
2017         git_header_html();
2018         print "<div class=\"page_nav\">\n" .
2019               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
2020               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
2021               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log")}, "log") .
2022               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash")}, "commit") .
2023               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
2024               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
2025               "<br/><br/>\n" .
2026               "</div>\n";
2027         print "<div>\n" .
2028               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
2029               "</div>\n";
2030         print "<div class=\"page_path\"><b>/$file_name</b><br/></div>\n";
2031
2032         open my $fd, "-|", "$gitbin/git-rev-list $hash | $gitbin/git-diff-tree -r --stdin \'$file_name\'";
2033         my $commit;
2034         print "<table cellspacing=\"0\">\n";
2035         my $alternate = 0;
2036         while (my $line = <$fd>) {
2037                 if ($line =~ m/^([0-9a-fA-F]{40})/){
2038                         $commit = $1;
2039                         next;
2040                 }
2041                 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/ && (defined $commit)) {
2042                         my %co = git_read_commit($commit);
2043                         if (!%co) {
2044                                 next;
2045                         }
2046                         if ($alternate) {
2047                                 print "<tr class=\"dark\">\n";
2048                         } else {
2049                                 print "<tr class=\"light\">\n";
2050                         }
2051                         $alternate ^= 1;
2052                         print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2053                               "<td><i>" . escapeHTML(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
2054                               "<td>" . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit"), -class => "list"}, "<b>" .
2055                               escapeHTML(chop_str($co{'title'}, 50)) . "</b>") . "</td>\n" .
2056                               "<td class=\"link\">" .
2057                               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit")}, "commit") .
2058                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
2059                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=blob;hb=$commit;f=$file_name")}, "blob");
2060                         my $blob = git_get_hash_by_path($hash, $file_name);
2061                         my $blob_parent = git_get_hash_by_path($commit, $file_name);
2062                         if (defined $blob && defined $blob_parent && $blob ne $blob_parent) {
2063                                 print " | " .
2064                                 $cgi->a({-href => esc("$my_uri?p=$project;a=blobdiff;h=$blob;hp=$blob_parent;hb=$commit;f=$file_name")},
2065                                 "diff to current");
2066                         }
2067                         print "</td>\n" .
2068                               "</tr>\n";
2069                         undef $commit;
2070                 }
2071         }
2072         print "</table>\n";
2073         close $fd;
2074         git_footer_html();
2075 }
2076
2077 sub git_search {
2078         if (!defined $searchtext) {
2079                 die_error("", "Text field empty.");
2080         }
2081         if (!defined $hash) {
2082                 $hash = git_read_hash("$project/HEAD");
2083         }
2084         my %co = git_read_commit($hash);
2085         if (!%co) {
2086                 die_error(undef, "Unknown commit object.");
2087         }
2088         # pickaxe may take all resources of your box and run for several minutes
2089         # with every query - so decide by yourself how public you make this feature :)
2090         my $commit_search = 1;
2091         my $author_search = 0;
2092         my $committer_search = 0;
2093         my $pickaxe_search = 0;
2094         if ($searchtext =~ s/^author\\://i) {
2095                 $author_search = 1;
2096         } elsif ($searchtext =~ s/^committer\\://i) {
2097                 $committer_search = 1;
2098         } elsif ($searchtext =~ s/^pickaxe\\://i) {
2099                 $commit_search = 0;
2100                 $pickaxe_search = 1;
2101         }
2102         git_header_html();
2103         print "<div class=\"page_nav\">\n" .
2104               $cgi->a({-href => esc("$my_uri?p=$project;a=summary;h=$hash")}, "summary") .
2105               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "shortlog") .
2106               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash")}, "log") .
2107               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash")}, "commit") .
2108               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
2109               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$hash")}, "tree") .
2110               "<br/><br/>\n" .
2111               "</div>\n";
2112
2113         print "<div>\n" .
2114               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash"), -class => "title"}, escapeHTML($co{'title'})) . "\n" .
2115               "</div>\n";
2116         print "<table cellspacing=\"0\">\n";
2117         my $alternate = 0;
2118         if ($commit_search) {
2119                 $/ = "\0";
2120                 open my $fd, "-|", "$gitbin/git-rev-list --header --parents $hash" or next;
2121                 while (my $commit_text = <$fd>) {
2122                         if (!grep m/$searchtext/i, $commit_text) {
2123                                 next;
2124                         }
2125                         if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
2126                                 next;
2127                         }
2128                         if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
2129                                 next;
2130                         }
2131                         my @commit_lines = split "\n", $commit_text;
2132                         my %co = git_read_commit(undef, \@commit_lines);
2133                         if (!%co) {
2134                                 next;
2135                         }
2136                         if ($alternate) {
2137                                 print "<tr class=\"dark\">\n";
2138                         } else {
2139                                 print "<tr class=\"light\">\n";
2140                         }
2141                         $alternate ^= 1;
2142                         print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2143                               "<td><i>" . escapeHTML(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
2144                               "<td>" .
2145                               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" . escapeHTML(chop_str($co{'title'}, 50)) . "</b><br/>");
2146                         my $comment = $co{'comment'};
2147                         foreach my $line (@$comment) {
2148                                 if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
2149                                         my $lead = escapeHTML($1) || "";
2150                                         $lead = chop_str($lead, 30, 10);
2151                                         my $match = escapeHTML($2) || "";
2152                                         my $trail = escapeHTML($3) || "";
2153                                         $trail = chop_str($trail, 30, 10);
2154                                         my $text = "$lead<span style=\"color:#e00000\">$match</span>$trail";
2155                                         print chop_str($text, 80, 5) . "<br/>\n";
2156                                 }
2157                         }
2158                         print "</td>\n" .
2159                               "<td class=\"link\">" .
2160                               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$co{'id'}")}, "commit") .
2161                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
2162                         print "</td>\n" .
2163                               "</tr>\n";
2164                 }
2165                 close $fd;
2166         }
2167
2168         if ($pickaxe_search) {
2169                 $/ = "\n";
2170                 open my $fd, "-|", "$gitbin/git-rev-list $hash | $gitbin/git-diff-tree -r --stdin -S$searchtext";
2171                 undef %co;
2172                 my @files;
2173                 while (my $line = <$fd>) {
2174                         if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
2175                                 my %set;
2176                                 $set{'file'} = $6;
2177                                 $set{'from_id'} = $3;
2178                                 $set{'to_id'} = $4;
2179                                 $set{'id'} = $set{'to_id'};
2180                                 if ($set{'id'} =~ m/0{40}/) {
2181                                         $set{'id'} = $set{'from_id'};
2182                                 }
2183                                 if ($set{'id'} =~ m/0{40}/) {
2184                                         next;
2185                                 }
2186                                 push @files, \%set;
2187                         } elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
2188                                 if (%co) {
2189                                         if ($alternate) {
2190                                                 print "<tr class=\"dark\">\n";
2191                                         } else {
2192                                                 print "<tr class=\"light\">\n";
2193                                         }
2194                                         $alternate ^= 1;
2195                                         print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2196                                               "<td><i>" . escapeHTML(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
2197                                               "<td>" .
2198                                               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$co{'id'}"), -class => "list"}, "<b>" .
2199                                               escapeHTML(chop_str($co{'title'}, 50)) . "</b><br/>");
2200                                         while (my $setref = shift @files) {
2201                                                 my %set = %$setref;
2202                                                 print $cgi->a({-href => esc("$my_uri?p=$project;a=blob;h=$set{'id'};hb=$co{'id'};f=$set{'file'}"), class => "list"},
2203                                                       "<span style=\"color:#e00000\">" . escapeHTML($set{'file'}) . "</span>") .
2204                                                       "<br/>\n";
2205                                         }
2206                                         print "</td>\n" .
2207                                               "<td class=\"link\">" .
2208                                               $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$co{'id'}")}, "commit") .
2209                                               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$co{'tree'};hb=$co{'id'}")}, "tree");
2210                                         print "</td>\n" .
2211                                               "</tr>\n";
2212                                 }
2213                                 %co = git_read_commit($1);
2214                         }
2215                 }
2216                 close $fd;
2217         }
2218         print "</table>\n";
2219         git_footer_html();
2220 }
2221
2222 sub git_shortlog {
2223         my $head = git_read_hash("$project/HEAD");
2224         if (!defined $hash) {
2225                 $hash = $head;
2226         }
2227         if (!defined $page) {
2228                 $page = 0;
2229         }
2230         git_header_html();
2231         print "<div class=\"page_nav\">\n" .
2232               $cgi->a({-href => esc("$my_uri?p=$project;a=summary")}, "summary") .
2233               " | shortlog" .
2234               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=log;h=$hash")}, "log") .
2235               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$hash")}, "commit") .
2236               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$hash")}, "commitdiff") .
2237               " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=tree;h=$hash;hb=$hash")}, "tree") . "<br/>\n";
2238
2239         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
2240         open my $fd, "-|", "$gitbin/git-rev-list $limit $hash" or die_error(undef, "Open failed.");
2241         my (@revlist) = map { chomp; $_ } <$fd>;
2242         close $fd;
2243
2244         if ($hash ne $head || $page) {
2245                 print $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog")}, "HEAD");
2246         } else {
2247                 print "HEAD";
2248         }
2249         if ($page > 0) {
2250                 print " &sdot; " .
2251                 $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash;pg=" . ($page-1)), -accesskey => "p", -title => "Alt-p"}, "prev");
2252         } else {
2253                 print " &sdot; prev";
2254         }
2255         if ($#revlist >= (100 * ($page+1)-1)) {
2256                 print " &sdot; " .
2257                 $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash;pg=" . ($page+1)), -accesskey => "n", -title => "Alt-n"}, "next");
2258         } else {
2259                 print " &sdot; next";
2260         }
2261         print "<br/>\n" .
2262               "</div>\n";
2263         print "<div>\n" .
2264               $cgi->a({-href => esc("$my_uri?p=$project;a=summary"), -class => "title"}, "&nbsp;") .
2265               "</div>\n";
2266         print "<table cellspacing=\"0\">\n";
2267         my $alternate = 0;
2268         for (my $i = ($page * 100); $i <= $#revlist; $i++) {
2269                 my $commit = $revlist[$i];
2270                 my %co = git_read_commit($commit);
2271                 my %ad = date_str($co{'author_epoch'});
2272                 if ($alternate) {
2273                         print "<tr class=\"dark\">\n";
2274                 } else {
2275                         print "<tr class=\"light\">\n";
2276                 }
2277                 $alternate ^= 1;
2278                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2279                       "<td><i>" . escapeHTML(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
2280                       "<td>";
2281                 if (length($co{'title_short'}) < length($co{'title'})) {
2282                         print $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit"), -class => "list", -title => "$co{'title'}"},
2283                               "<b>" . escapeHTML($co{'title_short'}) . "</b>");
2284                 } else {
2285                         print $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit"), -class => "list"},
2286                               "<b>" . escapeHTML($co{'title_short'}) . "</b>");
2287                 }
2288                 print "</td>\n" .
2289                       "<td class=\"link\">" .
2290                       $cgi->a({-href => esc("$my_uri?p=$project;a=commit;h=$commit")}, "commit") .
2291                       " | " . $cgi->a({-href => esc("$my_uri?p=$project;a=commitdiff;h=$commit")}, "commitdiff") .
2292                       "</td>\n" .
2293                       "</tr>";
2294         }
2295         if ($#revlist >= (100 * ($page+1)-1)) {
2296                 print "<tr>\n" .
2297                       "<td>" .
2298                       $cgi->a({-href => esc("$my_uri?p=$project;a=shortlog;h=$hash;pg=" . ($page+1)), -title => "Alt-n"}, "next") .
2299                       "</td>\n" .
2300                       "</tr>\n";
2301         }
2302         print "</table\n>";
2303         git_footer_html();
2304 }