v107
[git.git] / gitweb.cgi
1 #!/usr/bin/perl
2
3 # gitweb.pl - 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 GPL v2, or a later version
9
10 use strict;
11 use warnings;
12 use CGI qw(:standard :escapeHTML);
13 use CGI::Carp qw(fatalsToBrowser);
14 use Fcntl ':mode';
15
16 my $cgi = new CGI;
17 my $version =           "107";
18 my $my_url =            $cgi->url();
19 my $my_uri =            $cgi->url(-absolute => 1);
20 my $rss_link = "";
21
22 # absolute fs-path which will be prepended to the project path
23 my $projectroot =       "/pub/scm";
24
25 # location of the git-core binaries
26 my $gitbin =            "/usr/bin";
27
28 # location for temporary files needed for diffs
29 my $gittmp =            "/tmp/gitweb";
30
31 # target of the home link on top of all pages
32 my $home_link =         $my_uri;
33 $home_link =            "/git";
34
35 # handler to return the list of projects
36 sub get_projects_list {
37         my @list;
38
39         # search in directory
40 #       my $dir = $projectroot;
41 #       opendir my $dh, $dir || return undef;
42 #       while (my $dir = readdir($dh)) {
43 #               if (-e "$projectroot/$dir/HEAD") {
44 #                       push @list, $dir;
45 #               }
46 #       }
47 #       closedir($dh);
48
49         # read from file
50         my $file = "index/index.txt";
51         open my $fd , $file || return undef;
52         while (my $line = <$fd>) {
53                 chomp $line;
54                 if (-e "$projectroot/$line/HEAD") {
55                         push @list, $line;
56                 }
57         }
58         close $fd;
59
60         @list = sort @list;
61         return \@list;
62 }
63
64 # input validation
65 my $project = $cgi->param('p');
66 if (defined $project) {
67         if ($project =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
68                 undef $project;
69                 die_error("", "Non-canonical project parameter.");
70         }
71         if ($project =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~]/) {
72                 undef $project;
73                 die_error("", "Invalid character in project parameter.");
74         }
75         if (!(-d "$projectroot/$project")) {
76                 undef $project;
77                 die_error("", "No such directory.");
78         }
79         if (!(-e "$projectroot/$project/HEAD")) {
80                 undef $project;
81                 die_error("", "No such project.");
82         }
83         $rss_link = "<link rel=\"alternate\" title=\"$project log\" href=\"$my_uri?p=$project;a=rss\" type=\"application/rss+xml\"/>";
84         $ENV{'SHA1_FILE_DIRECTORY'} = "$projectroot/$project/objects";
85 }
86
87 my $file_name = $cgi->param('f');
88 if (defined $file_name) {
89         if ($file_name =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
90                 undef $file_name;
91                 die_error("", "Non-canonical file parameter.");
92         }
93         if ($file_name =~ m/[^a-zA-Z0-9_\.\/\-\+\#\~]/) {
94                 undef $file_name;
95                 die_error("", "Invalid character in file parameter.");
96         }
97 }
98
99 my $action = $cgi->param('a');
100 if (defined $action) {
101         if ($action =~ m/[^0-9a-zA-Z\.\-]+/) {
102                 undef $action;
103                 die_error("", "Invalid action parameter.");
104         }
105 } else {
106         $action = "log";
107 }
108
109 my $hash = $cgi->param('h');
110 if (defined $hash && !($hash =~ m/^[0-9a-fA-F]{40}$/)) {
111         undef $hash;
112         die_error("", "Invalid hash parameter.");
113 }
114
115 my $hash_parent = $cgi->param('hp');
116 if (defined $hash_parent && !($hash_parent =~ m/^[0-9a-fA-F]{40}$/)) {
117         undef $hash_parent;
118         die_error("", "Invalid parent hash parameter.");
119 }
120
121 my $time_back = $cgi->param('t');
122 if (defined $time_back) {
123         if ($time_back =~ m/^[^0-9]+$/) {
124                 undef $time_back;
125                 die_error("", "Invalid time parameter.");
126         }
127 }
128
129 sub git_header_html {
130         my $status = shift || "200 OK";
131
132         my $title = "git";
133         if (defined $project) {
134                 $title .= " - $project";
135                 if (defined $action) {
136                         $title .= "/$action";
137                 }
138         }
139         print $cgi->header(-type=>'text/html',  -charset => 'utf-8', -status=> $status);
140         print <<EOF;
141 <?xml version="1.0" encoding="utf-8"?>
142 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
143 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
144 <!-- git web interface v$version, (C) 2005, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke <ch\@gierke.de> -->
145 <head>
146 <title>$title</title>
147 $rss_link
148 <style type="text/css">
149 body { font-family: sans-serif; font-size: 12px; margin:0px; }
150 a { color:#0000cc; }
151 a:hover { color:#880000; }
152 a:visited { color:#880000; }
153 a:active { color:#880000; }
154 div.page_header {
155         margin:15px 15px 0px; height:25px; padding:8px;
156         font-size:18px; font-weight:bold; background-color:#d9d8d1;
157 }
158 div.page_header a:visited { color:#0000cc; }
159 div.page_header a:hover { color:#880000; }
160 div.page_nav { margin:0px 15px; padding:8px; border:solid #d9d8d1; border-width:0px 1px; }
161 div.page_nav a:visited { color:#0000cc; }
162 div.page_footer { margin:0px 15px 15px; height:17px; padding:4px; padding-left:8px; background-color: #d9d8d1; }
163 div.page_footer_text { float:left; color:#555555; font-style:italic; }
164 div.page_body { margin:0px 15px; padding:8px; border:solid #d9d8d1; border-width:0px 1px; }
165 div.title, a.title {
166         display:block; margin:0px 15px; padding:6px 8px;
167         font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
168 }
169 a.title:hover { background-color: #d9d8d1; }
170 div.title_text { margin:0px 15px; padding:6px 8px; border: solid #d9d8d1; border-width:0px 1px 1px; }
171 div.log_body { margin:0px 15px; padding:8px; padding-left:150px; border:solid #d9d8d1; border-width:0px 1px; }
172 span.log_age { position:relative; float:left; width:142px; font-style:italic; }
173 div.log_link { font-size:10px; font-family:sans-serif; font-style:normal; position:relative; float:left; width:142px; }
174 div.list {
175         display:block; margin:0px 15px; padding:4px 6px 2px; border:solid #d9d8d1; border-width:1px 1px 0px;
176         font-weight:bold;
177 }
178 div.list_head {
179         display:block; margin:0px 15px; padding:4px 6px 4px; border:solid #d9d8d1; border-width:1px 1px 0px;
180         font-style:italic;
181 }
182 div.list a { text-decoration:none; color:#000000; }
183 div.list a:hover { color:#880000; }
184 div.link {
185         margin:0px 15px; padding:0px 6px 8px; border:solid #d9d8d1; border-width:0px 1px;
186         font-family:sans-serif; font-size:10px;
187 }
188 td { padding:5px 15px 0px 0px; font-size:12px; }
189 th { padding-right:10px; font-size:12px; text-align:left; }
190 span.diff_info { color:#000099; background-color:#edece6; font-style:italic; }
191 a.rss_logo { float:right; border:1px solid; line-height:15px;
192         border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e; width:35px;
193         color:#ffffff; background-color:#ff6600;
194         font-weight:bold; font-family:sans-serif; text-align:center; vertical-align:middle;
195         font-size:10px; display:block; text-decoration:none;
196 }
197 a.rss_logo:hover { background-color:#ee5500; }
198 </style>
199 </head>
200 <body>
201 EOF
202         print "<div class=\"page_header\">\n" .
203               "<a href=\"http://kernel.org/pub/software/scm/cogito\">" .
204               "<img src=\"$my_uri?a=git-logo.png\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/></a>";
205         print $cgi->a({-href => $home_link}, "projects") . " / ";
206         if (defined $project) {
207                 print $cgi->a({-href => "$my_uri?p=$project;a=log"}, escapeHTML($project));
208                 if (defined $action) {
209                         print " / $action";
210                 }
211         }
212         print "</div>\n";
213 }
214
215 sub git_footer_html {
216         print "<div class=\"page_footer\">\n";
217         if (defined $project) {
218                 my $descr = git_description($project);
219                 if (defined $descr) {
220                         print "<div class=\"page_footer_text\">" . escapeHTML($descr) . "</div>\n";
221                 }
222                 print $cgi->a({-href => "$my_uri?p=$project;a=rss", -class => "rss_logo"}, "RSS") . "\n";
223         }
224         print "</div>\n" .
225               "</body>\n" .
226               "</html>";
227 }
228
229 sub die_error {
230         my $status = shift || "403 Forbidden";
231         my $error = shift || "Malformed query, file missing or permission denied"; 
232
233         git_header_html($status);
234         print "<div class=\"page_body\">\n" .
235               "<br/><br/>\n";
236         print "$status - $error\n";
237         print "<br/></div>\n";
238         git_footer_html();
239         exit 0;
240 }
241
242 sub git_head {
243         my $path = shift;
244         open my $fd, "$projectroot/$path/HEAD" || return undef;
245         my $head = <$fd>;
246         close $fd;
247         chomp $head;
248         if ($head =~ m/^[0-9a-fA-F]{40}$/) {
249                 return $head;
250         } else {
251                 return undef;
252         }
253 }
254
255 sub git_description {
256         my $path = shift;
257         open my $fd, "$projectroot/$path/description" || return undef;
258         my $descr = <$fd>;
259         close $fd;
260         chomp $descr;
261         return $descr;
262 }
263
264 sub git_commit {
265         my $commit = shift;
266         my %co;
267         my @parents;
268
269         open my $fd, "-|", "$gitbin/git-cat-file commit $commit" || return;
270         while (my $line = <$fd>) {
271                 last if $line eq "\n";
272                 chomp $line;
273                 if ($line =~ m/^tree (.*)$/) {
274                         $co{'tree'} = $1;
275                 } elsif ($line =~ m/^parent (.*)$/) {
276                         push @parents, $1;
277                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
278                         $co{'author'} = $1;
279                         $co{'author_epoch'} = $2;
280                         $co{'author_tz'} = $3;
281                         $co{'author_name'} = $co{'author'};
282                         $co{'author_name'} =~ s/ <.*//;
283                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
284                         $co{'committer'} = $1;
285                         $co{'committer_epoch'} = $2;
286                         $co{'committer_tz'} = $3;
287                         $co{'committer_name'} = $co{'committer'};
288                         $co{'committer_name'} =~ s/ <.*//;
289                 }
290         }
291         $co{'parents'} = \@parents;
292         $co{'parent'} = $parents[0];
293         my (@comment) = map { chomp; $_ } <$fd>;
294         $co{'comment'} = \@comment;
295         $co{'title'} = $comment[0];
296         close $fd || return;
297         if (!defined $co{'tree'}) {
298                 return undef
299         };
300
301         my $age = time - $co{'committer_epoch'};
302         $co{'age'} = $age;
303         if ($age > 60*60*24*365*2) {
304                 $co{'age_string'} = (int $age/60/60/24/365);
305                 $co{'age_string'} .= " years ago";
306         } elsif ($age > 60*60*24*365/12*2) {
307                 $co{'age_string'} = int $age/60/60/24/365/12;
308                 $co{'age_string'} .= " months ago";
309         } elsif ($age > 60*60*24*7*2) {
310                 $co{'age_string'} = int $age/60/60/24/7;
311                 $co{'age_string'} .= " weeks ago";
312         } elsif ($age > 60*60*24*2) {
313                 $co{'age_string'} = int $age/60/60/24;
314                 $co{'age_string'} .= " days ago";
315         } elsif ($age > 60*60*2) {
316                 $co{'age_string'} = int $age/60/60;
317                 $co{'age_string'} .= " hours ago";
318         } elsif ($age > 60*2) {
319                 $co{'age_string'} = int $age/60;
320                 $co{'age_string'} .= " minutes ago";
321         } elsif ($age > 2) {
322                 $co{'age_string'} = int $age;
323                 $co{'age_string'} .= " seconds ago";
324         } else {
325                 $co{'age_string'} .= " right now";
326         }
327         return %co;
328 }
329
330 sub git_diff_html {
331         my $from = shift;
332         my $from_name = shift;
333         my $to = shift;
334         my $to_name = shift;
335
336         my $from_tmp = "/dev/null";
337         my $to_tmp = "/dev/null";
338         my $pid = $$;
339
340         # create tmp from-file
341         if (defined $from) {
342                 $from_tmp = "$gittmp/gitweb_" . $$ . "_from";
343                 open my $fd2, "> $from_tmp";
344                 open my $fd, "-|", "$gitbin/git-cat-file blob $from";
345                 my @file = <$fd>;
346                 print $fd2 @file;
347                 close $fd2;
348                 close $fd;
349         }
350
351         # create tmp to-file
352         if (defined $to) {
353                 $to_tmp = "$gittmp/gitweb_" . $$ . "_to";
354                 open my $fd2, "> $to_tmp";
355                 open my $fd, "-|", "$gitbin/git-cat-file blob $to";
356                 my @file = <$fd>;
357                 print $fd2 @file;
358                 close $fd2;
359                 close $fd;
360         }
361
362         open my $fd, "-|", "/usr/bin/diff -u -p -L $from_name -L $to_name $from_tmp $to_tmp";
363         while (my $line = <$fd>) {
364                 my $char = substr($line,0,1);
365                 # skip errors
366                 next if $char eq '\\';
367                 # color the diff
368                 print '<span style="color: #008800;">' if $char eq '+';
369                 print '<span style="color: #CC0000;">' if $char eq '-';
370                 print '<span style="color: #990099;">' if $char eq '@';
371                 print escapeHTML($line);
372                 print '</span>' if $char eq '+' or $char eq '-' or $char eq '@';
373         }
374         close $fd;
375
376         if (defined $from) {
377                 unlink($from_tmp);
378         }
379         if (defined $to) {
380                 unlink($to_tmp);
381         }
382 }
383
384 sub mode_str {
385         my $mode = oct shift;
386
387         if (S_ISDIR($mode & S_IFMT)) {
388                 return 'drwxr-xr-x';
389         } elsif (S_ISLNK($mode)) {
390                 return 'lrwxrwxrwx';
391         } elsif (S_ISREG($mode)) {
392                 # git cares only about the executable bit
393                 if ($mode & S_IXUSR) {
394                         return '-rwxr-xr-x';
395                 } else {
396                         return '-rw-r--r--';
397                 };
398         } else {
399                 return '----------';
400         }
401 }
402
403 sub file_type {
404         my $mode = oct shift;
405
406         if (S_ISDIR($mode & S_IFMT)) {
407                 return "directory";
408         } elsif (S_ISLNK($mode)) {
409                 return "symlink";
410         } elsif (S_ISREG($mode)) {
411                 return "file";
412         } else {
413                 return "unknown";
414         }
415 }
416
417 sub date_str {
418         my $epoch = shift;
419         my $tz = shift || "-0000";
420
421         my %date;
422         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
423         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
424         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
425         $date{'hour'} = $hour;
426         $date{'minute'} = $min;
427         $date{'mday'} = $mday;
428         $date{'day'} = $days[$wday];
429         $date{'month'} = $months[$mon];
430         $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000", $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
431         $date{'mday-time'} = sprintf "%d %s %02d:%02d", $mday, $months[$mon], $hour ,$min;
432
433         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
434         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
435         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
436         $date{'hour_local'} = $hour;
437         $date{'minute_local'} = $min;
438         $date{'tz_local'} = $tz;
439         return %date;
440 }
441
442 # git-logo (cached in browser for one day)
443 if (defined $action && $action eq "git-logo.png") {
444         print $cgi->header(-type => 'image/png', -expires => '+1d');
445         # cat git-logo.png | hexdump -e '16/1 " %02x"  "\n"' | sed 's/ /\\x/g'
446         print   "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52" .
447                 "\x00\x00\x00\x48\x00\x00\x00\x1b\x04\x03\x00\x00\x00\x2d\xd9\xd4" .
448                 "\x2d\x00\x00\x00\x18\x50\x4c\x54\x45\xff\xff\xff\x60\x60\x5d\xb0" .
449                 "\xaf\xaa\x00\x80\x00\xce\xcd\xc7\xc0\x00\x00\xe8\xe8\xe6\xf7\xf7" .
450                 "\xf6\x95\x0c\xa7\x47\x00\x00\x00\x73\x49\x44\x41\x54\x28\xcf\x63" .
451                 "\x48\x67\x20\x04\x4a\x5c\x18\x0a\x08\x2a\x62\x53\x61\x20\x02\x08" .
452                 "\x0d\x69\x45\xac\xa1\xa1\x01\x30\x0c\x93\x60\x36\x26\x52\x91\xb1" .
453                 "\x01\x11\xd6\xe1\x55\x64\x6c\x6c\xcc\x6c\x6c\x0c\xa2\x0c\x70\x2a" .
454                 "\x62\x06\x2a\xc1\x62\x1d\xb3\x01\x02\x53\xa4\x08\xe8\x00\x03\x18" .
455                 "\x26\x56\x11\xd4\xe1\x20\x97\x1b\xe0\xb4\x0e\x35\x24\x71\x29\x82" .
456                 "\x99\x30\xb8\x93\x0a\x11\xb9\x45\x88\xc1\x8d\xa0\xa2\x44\x21\x06" .
457                 "\x27\x41\x82\x40\x85\xc1\x45\x89\x20\x70\x01\x00\xa4\x3d\x21\xc5" .
458                 "\x12\x1c\x9a\xfe\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
459         exit;
460 }
461
462 # project browser
463 if (!defined $project) {
464         my $projects = get_projects_list();
465         git_header_html();
466         print "<div class=\"page_body\">\n";
467         print "<table cellspacing=\"0\">\n";
468         print "<tr>\n" .
469               "<th>Project</th>\n" .
470               "<th>Description</th>\n" .
471               "<th>Owner</th>\n" .
472               "<th>last change</th>\n" .
473               "</tr>\n" .
474               "<br/>";
475         foreach my $proj (@$projects) {
476                 my $head = git_head($proj);
477                 if (!defined $head) {
478                         next;
479                 }
480                 $ENV{'SHA1_FILE_DIRECTORY'} = "$projectroot/$proj/objects";
481                 my %co = git_commit($head);
482                 if (!%co) {
483                         next;
484                 }
485                 my $descr = git_description($proj) || "";
486                 my $owner = "";
487                 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat("$projectroot/$proj");
488                 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
489                 if (defined $gcos) {
490                         $owner = $gcos;
491                         $owner =~ s/[,;].*$//;
492                 }
493                 print "<tr>\n" .
494                       "<td>" . $cgi->a({-href => "$my_uri?p=$proj;a=log"}, escapeHTML($proj)) . "</td>\n" .
495                       "<td>$descr</td>\n" .
496                       "<td><i>$owner</i></td>\n";
497                 if ($co{'age'} < 60*60*2) {
498                         print "<td><span style =\"color: #009900;\"><b><i>" . $co{'age_string'} . "</i></b></span></td>\n";
499                 } elsif ($co{'age'} < 60*60*24*2) {
500                         print "<td><span style =\"color: #009900;\"><i>" . $co{'age_string'} . "</i></span></td>\n";
501                 } else {
502                         print "<td><i>" . $co{'age_string'} . "</i></td>\n";
503                 }
504                 print "</tr>\n";
505                 undef %co;
506         }
507         print "</table>\n" .
508               "<br/>\n" .
509               "</div>\n";
510         git_footer_html();
511         exit;
512 }
513
514 # action dispatch
515 if ($action eq "blob") {
516         open my $fd, "-|", "$gitbin/git-cat-file blob $hash" || die_error("", "Open failed.");
517         git_header_html();
518         print "<div class=\"page_nav\">\n";
519         print "<br/><br/></div>\n";
520         print "<div class=\"title\">$hash</div>\n";
521         print "<div class=\"page_body\"><pre>\n";
522         my $nr;
523         while (my $line = <$fd>) {
524                 $nr++;
525                 printf "<span style =\"color: #999999;\">%4i\t</span>%s", $nr, escapeHTML($line);;
526         }
527         close $fd || print "Reading blob failed.\n";
528         print "</pre><br/>\n";
529         print "</div>";
530         git_footer_html();
531 } elsif ($action eq "tree") {
532         if (!defined $hash) {
533                 $hash = git_head($project);
534         }
535         open my $fd, "-|", "$gitbin/git-ls-tree $hash" || die_error("", "Open failed.");
536         my (@entries) = map { chomp; $_ } <$fd>;
537         close $fd || die_error("", "Reading tree failed.");
538
539         git_header_html();
540         my %co = git_commit($hash);
541         if (%co) {
542                 print "<div class=\"page_nav\"> view\n" .
543                       $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash"}, "commit") . " | " .
544                       $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash"}, "diffs") . " | " .
545                       $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$hash"}, "tree") .
546                       "<br/><br/>\n" .
547                       "</div>\n";
548                 print "<div>\n" .
549                       $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
550                       "</div>\n";
551         } else {
552                 print "<div class=\"page_nav\">\n";
553                 print "<br/><br/></div>\n";
554                 print "<div class=\"title\">$hash</div>\n";
555         }
556         print "<div class=\"page_body\">\n";
557         print "<br/><pre>\n";
558         foreach my $line (@entries) {
559                 #'100644        blob    0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa        panic.c'
560                 $line =~ m/^([0-9]+)\t(.*)\t(.*)\t(.*)$/;
561                 my $t_mode = $1;
562                 my $t_type = $2;
563                 my $t_hash = $3;
564                 my $t_name = $4;
565                 if ($t_type eq "blob") {
566                         print mode_str($t_mode). " " . $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$t_hash"}, $t_name);
567                         if (S_ISLNK(oct $t_mode)) {
568                                 open my $fd, "-|", "$gitbin/git-cat-file blob $t_hash";
569                                 my $target = <$fd>;
570                                 close $fd;
571                                 print "\t -> $target";
572                         }
573                         print "\n";
574                 } elsif ($t_type eq "tree") {
575                         print mode_str($t_mode). " " . $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$t_hash"}, $t_name) . "\n";
576                 }
577         }
578         print "</pre>\n";
579         print "<br/></div>";
580         git_footer_html();
581 } elsif ($action eq "rss") {
582         open my $fd, "-|", "$gitbin/git-rev-list --max-count=20 " . git_head($project) || die_error("", "Open failed.");
583         my (@revlist) = map { chomp; $_ } <$fd>;
584         close $fd || die_error("", "Reading rev-list failed.");
585
586         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
587         print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
588               "<rss version=\"0.91\">\n";
589         print "<channel>\n";
590         print "<title>$project</title>\n".
591               "<link> " . $my_url . "/$project/log</link>\n".
592               "<description>$project log</description>\n".
593               "<language>en</language>\n";
594
595         foreach my $commit (@revlist) {
596                 my %co = git_commit($commit);
597                 my %ad = date_str($co{'author_epoch'});
598                 print "<item>\n" .
599                       "\t<title>" . sprintf("%d %s %02d:%02d", $ad{'mday'}, $ad{'month'}, $ad{'hour'}, $ad{'min'}) . " - " . escapeHTML($co{'title'}) . "</title>\n" .
600                       "\t<link> " . $my_url . "?p=$project;a=commit;h=$commit</link>\n" .
601                       "\t<description>";
602                 my $comment = $co{'comment'};
603                 foreach my $line (@$comment) {
604                         print escapeHTML($line) . "<br/>\n";
605                 }
606                 print "\t</description>\n" .
607                       "</item>\n";
608                 undef %ad;
609                 undef %co;
610         }
611         print "</channel></rss>";
612 } elsif ($action eq "log") {
613         my $head = git_head($project);
614         my $limit_option = "";
615         if (!defined $time_back) {
616                 $limit_option = "--max-count=10";
617         } elsif ($time_back > 0) {
618                 my $date = time - $time_back*24*60*60;
619                 $limit_option = "--max-age=$date";
620         }
621         open my $fd, "-|", "$gitbin/git-rev-list $limit_option $head" || die_error("", "Open failed.");
622         my (@revlist) = map { chomp; $_ } <$fd>;
623         close $fd || die_error("", "Reading rev-list failed.");
624
625         git_header_html();
626         print "<div class=\"page_nav\">\n";
627         print "view  ";
628         print $cgi->a({-href => "$my_uri?p=$project;a=log"}, "last 10") . " | " .
629               $cgi->a({-href => "$my_uri?p=$project;a=log;t=1"}, "day") . " | " .
630               $cgi->a({-href => "$my_uri?p=$project;a=log;t=7"}, "week") . " | " .
631               $cgi->a({-href => "$my_uri?p=$project;a=log;t=31"}, "month") . " | " .
632               $cgi->a({-href => "$my_uri?p=$project;a=log;t=365"}, "year") . " | " .
633               $cgi->a({-href => "$my_uri?p=$project;a=log;t=0"}, "all") . "<br/>\n";
634         print "<br/><br/>\n" .
635               "</div>\n";
636
637         if (!@revlist) {
638                 my %co = git_commit($head);
639                 print "<div class=\"page_body\"> Last change " . $co{'age_string'} . ".<br/><br/></div>\n";
640         }
641
642         foreach my $commit (@revlist) {
643                 my %co = git_commit($commit);
644                 next if !%co;
645                 my %ad = date_str($co{'author_epoch'});
646                 print "<div>\n" .
647                       $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit", -class => "title"}, 
648                       "<span class=\"log_age\">" . $co{'age_string'} . "</span>" . escapeHTML($co{'title'})) . "\n" .
649                       "</div>\n";
650                 print "<div class=\"title_text\">\n" .
651                       "<div class=\"log_link\">\n" .
652                       "view " . $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"}, "commit") . " | " .
653                       $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$commit"}, "diff") . "<br/>\n" .
654                       "</div>\n" .
655                       "<i>" . escapeHTML($co{'author_name'}) .  " [" . $ad{'rfc2822'} . "]</i><br/>\n" .
656                       "</div>\n" .
657                       "<div class=\"log_body\">\n";
658                 my $comment = $co{'comment'};
659                 foreach my $line (@$comment) {
660                         last if ($line =~ m/^(signed-off|acked)-by:/i);
661                                 print escapeHTML($line) . "<br/>\n";
662                 }
663                 print "<br/>\n" .
664                       "</div>\n";
665                 undef %ad;
666                 undef %co;
667         }
668         git_footer_html();
669 } elsif ($action eq "commit") {
670         my %co = git_commit($hash);
671         if (!%co) {
672                 die_error("", "Unknown commit object.");
673         }
674         my %ad = date_str($co{'author_epoch'}, $co{'author_tz'});
675         my %cd = date_str($co{'committer_epoch'}, $co{'committer_tz'});
676
677         my @difftree;
678         if (defined $co{'parent'}) {
679                 open my $fd, "-|", "$gitbin/git-diff-tree -r " . $co{'parent'} . " $hash" || die_error("", "Open failed.");
680                 @difftree = map { chomp; $_ } <$fd>;
681                 close $fd || die_error("", "Reading diff-tree failed.");
682         } else {
683                 # fake git-diff-tree output for initial revision
684                 open my $fd, "-|", "$gitbin/git-ls-tree -r $hash" || die_error("", "Open failed.");
685                 @difftree = map { chomp;  "+" . $_ } <$fd>;
686                 close $fd || die_error("", "Reading ls-tree failed.");
687         }
688         git_header_html();
689         print "<div class=\"page_nav\"> view\n" .
690               $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash"}, "commit") . " | \n" .
691               $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash"}, "diffs") . " | \n" .
692               $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$hash"}, "tree") . "\n" .
693               "<br/><br/></div>\n";
694         if (defined $co{'parent'}) {
695                 print "<div>\n" .
696                       $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
697                       "</div>\n";
698         } else {
699                 print "<div>\n" .
700                       $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
701                       "</div>\n";
702         }
703         print "<div class=\"title_text\">\n" .
704               "<table cellspacing=\"0\">\n";
705         print "<tr><td>author</td><td>" . escapeHTML($co{'author'}) . "</td></tr>\n".
706               "<tr><td></td><td> " . $ad{'rfc2822'};
707         if ($ad{'hour_local'} < 6) {
708                 printf(" (<span style=\"color: #cc0000;\">%02d:%02d</span> %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
709         } else {
710                 printf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
711         }
712         print "</td></tr>\n";
713         print "<tr><td>committer</td><td>" . escapeHTML($co{'committer'}) . "</td></tr>\n";
714         print "<tr><td></td><td> " . $cd{'rfc2822'} .
715               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) . "</td></tr>\n";
716         print "<tr><td>commit</td><td style=\"font-family: monospace;\">$hash</td></tr>\n";
717         print "<tr><td>tree</td><td style=\"font-family: monospace;\">" .
718               $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$hash"}, $co{'tree'}) . "</td></tr>\n";
719         my $parents  = $co{'parents'};
720         foreach my $par (@$parents) {
721                 print "<tr><td>parent</td><td style=\"font-family: monospace;\">" .
722                       $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$par"}, $par) . "</td></tr>\n";
723         }
724         print "</table>". 
725               "</div>\n";
726         print "<div class=\"page_body\">\n";
727         my $comment = $co{'comment'};
728         foreach my $line (@$comment) {
729                 if ($line =~ m/(signed-off|acked)-by:/i) {
730                         print "<span style=\"color: #888888\">" . escapeHTML($line) . "</span><br/>\n";
731                 } else {
732                         print escapeHTML($line) . "<br/>\n";
733                 }
734         }
735         print "</div>\n";
736         if ($#difftree > 10) {
737                 print "<div class=\"list_head\">" . ($#difftree + 1) . " files changed:<br/></div>\n";
738         }
739         foreach my $line (@difftree) {
740                 # '*100644->100644      blob    9f91a116d91926df3ba936a80f020a6ab1084d2b->bb90a0c3a91eb52020d0db0e8b4f94d30e02d596      net/ipv4/route.c'
741                 # '+100644      blob    4a83ab6cd565d21ab0385bac6643826b83c2fcd4        arch/arm/lib/bitops.h'
742                 # '*100664->100644      blob    b1a8e3dd5556b61dd771d32307c6ee5d7150fa43->b1a8e3dd5556b61dd771d32307c6ee5d7150fa43      show-files.c'
743                 # '*100664->100644      blob    d08e895238bac36d8220586fdc28c27e1a7a76d3->d08e895238bac36d8220586fdc28c27e1a7a76d3      update-cache.c'
744                 $line =~ m/^(.)(.*)\t(.*)\t(.*)\t(.*)$/;
745                 my $op = $1;
746                 my $mode = $2;
747                 my $type = $3;
748                 my $id = $4;
749                 my $file = $5;
750                 if ($type eq "blob") {
751                         if ($op eq "+") {
752                                 my $mode_chng = "";
753                                 if (S_ISREG(oct $mode)) {
754                                         $mode_chng = sprintf(" with mode: %04o", (oct $mode) & 0777);
755                                 }
756                                 print "<div class=\"list\">\n" .
757                                       $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id"},
758                                       escapeHTML($file) . " <span style=\"color: #008000;\">[new " . file_type($mode) . $mode_chng . "]</span>") . "\n" .
759                                       "</div>";
760                                 print "<div class=\"link\">\n" .
761                                       "view " .
762                                       $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id"}, "file") . "<br/>\n" .
763                                       "</div>\n";
764                         } elsif ($op eq "-") {
765                                 print "<div class=\"list\">\n" .
766                                       $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id"},
767                                       escapeHTML($file) .  " <span style=\"color: #c00000;\">[deleted " . file_type($mode) . "]</span>") . "\n" .
768                                       "</div>";
769                                 print "<div class=\"link\">\n" .
770                                       "view " .
771                                       $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id"}, "file") . " | " .
772                                       $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash;f=$file"}, "history") . "<br/>\n" .
773                                       "</div>\n";
774                         } elsif ($op eq "*") {
775                                 $id =~ m/([0-9a-fA-F]+)->([0-9a-fA-F]+)/;
776                                 my $from_id = $1;
777                                 my $to_id = $2;
778                                 $mode =~ m/^([0-7]{6})->([0-7]{6})$/;
779                                 my $from_mode = $1;
780                                 my $to_mode = $2;
781                                 my $mode_chnge = "";
782                                 if ($from_mode != $to_mode) {
783                                         $mode_chnge = " <span style=\"color: #888888;\">[changed";
784                                         if (((oct $from_mode) & S_IFMT) != ((oct $to_mode) & S_IFMT)) {
785                                                 $mode_chnge .= " from " . file_type($from_mode) . " to " . file_type($to_mode);
786                                         }
787                                         if (((oct $from_mode) & 0777) != ((oct $to_mode) & 0777)) {
788                                                 if (S_ISREG($from_mode) && S_ISREG($to_mode)) {
789                                                         $mode_chnge .= sprintf(" mode: %04o->%04o", (oct $from_mode) & 0777, (oct $to_mode) & 0777);
790                                                 } elsif (S_ISREG($to_mode)) {
791                                                         $mode_chnge .= sprintf(" mode: %04o", (oct $to_mode) & 0777);
792                                                 }
793                                         }
794                                         $mode_chnge .= "]</span>\n";
795                                 }
796                                 print "<div class=\"list\">\n";
797                                 if ($to_id ne $from_id) {
798                                         print $cgi->a({-href => "$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id"},
799                                               escapeHTML($file) . $mode_chnge) . "\n" .
800                                               "</div>\n";
801                                 } else {
802                                         print $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id"},
803                                               escapeHTML($file) . $mode_chnge) . "\n" .
804                                               "</div>\n";
805                                 }
806                                 print "<div class=\"link\">\n" .
807                                       "view ";
808                                 if ($to_id ne $from_id) {
809                                         print $cgi->a({-href => "$my_uri?p=$project;a=blobdiff;h=$to_id;hp=$from_id"}, "diff") . " | ";
810                                 }
811                                 print $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id"}, "file") . " | " .
812                                       $cgi->a({-href => "$my_uri?p=$project;a=history;h=$hash;f=$file"}, "history") . "<br/>\n" .
813                                       "</div>\n";
814                         }
815                 }
816         }
817         git_footer_html();
818 } elsif ($action eq "blobdiff") {
819         mkdir($gittmp, 0700);
820         git_header_html();
821         print "<div class=\"page_nav\">\n";
822         print "<br/><br/></div>\n";
823         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
824         print "<div class=\"page_body\">\n" .
825               "<pre>\n";
826         print "<span class=\"diff_info\">blob:" .
827               $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$hash_parent"}, $hash_parent) .
828               " -> blob:" .
829               $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$hash"}, $hash) .
830               "</span>\n";
831         git_diff_html($hash_parent, $hash_parent, $hash, $hash);
832         print "</pre>\n" .
833               "<br/></div>";
834         git_footer_html();
835 } elsif ($action eq "commitdiff") {
836         mkdir($gittmp, 0700);
837         my %co = git_commit($hash);
838         if (!%co) {
839                 die_error("", "Unknown commit object.");
840         }
841         open my $fd, "-|", "$gitbin/git-diff-tree -r " . $co{'parent'} . " $hash" || die_error("", "Open failed.");
842         my (@difftree) = map { chomp; $_ } <$fd>;
843         close $fd || die_error("", "Reading diff-tree failed.");
844
845         git_header_html();
846         print "<div class=\"page_nav\"> view\n" .
847               $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash"}, "commit") . " | \n" .
848               $cgi->a({-href => "$my_uri?p=$project;a=commitdiff;h=$hash"}, "diffs") . " | \n" .
849               $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$hash"}, "tree") . "\n" .
850               "<br/><br/></div>\n";
851         print "<div>\n" .
852               $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash", -class => "title"}, escapeHTML($co{'title'})) . "\n" .
853               "</div>\n";
854         print "<div class=\"page_body\">\n" .
855               "<pre>\n";
856         foreach my $line (@difftree) {
857                 # '*100644->100644      blob    8e5f9bbdf4de94a1bc4b4da8cb06677ce0a57716->8da3a306d0c0c070d87048d14a033df02f40a154      Makefile'
858                 $line =~ m/^(.)(.*)\t(.*)\t(.*)\t(.*)$/;
859                 my $op = $1;
860                 my $mode = $2;
861                 my $type = $3;
862                 my $id = $4;
863                 my $file = $5;
864                 if ($type eq "blob") {
865                         if ($op eq "+") {
866                                 print "<span class=\"diff_info\">" .  file_type($mode) . ":" .
867                                       $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id"}, $id) . "(new)" .
868                                       "</span>\n";
869                                 git_diff_html(undef, "/dev/null", $id, "b/$file");
870                         } elsif ($op eq "-") {
871                                 print "<span class=\"diff_info\">" . file_type($mode) . ":" .
872                                       $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$id"}, $id) . "(deleted)" .
873                                       "</span>\n";
874                                 git_diff_html($id, "a/$file", undef, "/dev/null");
875                         } elsif ($op eq "*") {
876                                 $id =~ m/([0-9a-fA-F]+)->([0-9a-fA-F]+)/;
877                                 my $from_id = $1;
878                                 my $to_id = $2;
879                                 $mode =~ m/([0-7]+)->([0-7]+)/;
880                                 my $from_mode = $1;
881                                 my $to_mode = $2;
882                                 if ($from_id ne $to_id) {
883                                         print "<span class=\"diff_info\">" .
884                                               file_type($from_mode) . ":" . $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$from_id"}, $from_id) .
885                                               " -> " .
886                                               file_type($to_mode) . ":" . $cgi->a({-href => "$my_uri?p=$project;a=blob;h=$to_id"}, $to_id);
887                                         print "</span>\n";
888                                         git_diff_html($from_id, "a/$file",  $to_id, "b/$file");
889                                 }
890                         }
891                 }
892         }
893         print "</pre><br/>\n";
894         print "</div>";
895         git_footer_html();
896 } elsif ($action eq "history") {
897         if (!defined $hash) {
898                 $hash = git_head($project);
899         }
900         git_header_html();
901         print "<div class=\"page_nav\">\n";
902         print "<br/><br/></div>\n";
903         print "<div>\n" .
904               $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$hash", -class => "title"}, escapeHTML($file_name)) . "\n" .
905               "</div>\n";
906         open my $fd, "-|", "$gitbin/git-rev-list $hash | $gitbin/git-diff-tree -r --stdin $file_name";
907         my $commit;
908         while (my $line = <$fd>) {
909                 if ($line =~ m/^([0-9a-fA-F]{40}) /){
910                         $commit = $1;
911                         next;
912                 }
913                 if ($line =~ m/^(.)(.*)\t(.*)\t(.*)\t(.*)$/ && (defined $commit)) {
914                         my $type = $3;
915                         my $file = $5;
916                         if ($file ne $file_name || $type ne "blob") {
917                                 next;
918                         }
919                         my %co = git_commit($commit);
920                         if (!%co) {
921                                 next;
922                         }
923                         print "<div class=\"list\">\n" .
924                               $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"},
925                               "<span class=\"log_age\">" . $co{'age_string'} . "</span>" . escapeHTML($co{'title'})) . "\n" .
926                               "</div>\n";
927                         print "<div class=\"link\">\n" .
928                               "view " .
929                               $cgi->a({-href => "$my_uri?p=$project;a=commit;h=$commit"}, "commit") . " | " .
930                               $cgi->a({-href => "$my_uri?p=$project;a=tree;h=$commit"}, "tree") . "<br/><br/>\n" .
931                               "</div>\n";
932                         undef %co;
933                         undef $commit;
934                 }
935         }
936         close $fd;
937         git_footer_html();
938 } else {
939         undef $action;
940         die_error("", "Unknown action.");
941 }