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