Merge branch 'maint'
[git] / gitweb / gitweb.perl
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 use File::Basename qw(basename);
19 binmode STDOUT, ':utf8';
20
21 BEGIN {
22         CGI->compile() if $ENV{'MOD_PERL'};
23 }
24
25 our $cgi = new CGI;
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute => 1);
29
30 # if we're called with PATH_INFO, we have to strip that
31 # from the URL to find our real URL
32 if (my $path_info = $ENV{"PATH_INFO"}) {
33         $my_url =~ s,\Q$path_info\E$,,;
34         $my_uri =~ s,\Q$path_info\E$,,;
35 }
36
37 # core git executable to use
38 # this can just be "git" if your webserver has a sensible PATH
39 our $GIT = "++GIT_BINDIR++/git";
40
41 # absolute fs-path which will be prepended to the project path
42 #our $projectroot = "/pub/scm";
43 our $projectroot = "++GITWEB_PROJECTROOT++";
44
45 # fs traversing limit for getting project list
46 # the number is relative to the projectroot
47 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
48
49 # target of the home link on top of all pages
50 our $home_link = $my_uri || "/";
51
52 # string of the home link on top of all pages
53 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
54
55 # name of your site or organization to appear in page titles
56 # replace this with something more descriptive for clearer bookmarks
57 our $site_name = "++GITWEB_SITENAME++"
58                  || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
59
60 # filename of html text to include at top of each page
61 our $site_header = "++GITWEB_SITE_HEADER++";
62 # html text to include at home page
63 our $home_text = "++GITWEB_HOMETEXT++";
64 # filename of html text to include at bottom of each page
65 our $site_footer = "++GITWEB_SITE_FOOTER++";
66
67 # URI of stylesheets
68 our @stylesheets = ("++GITWEB_CSS++");
69 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
70 our $stylesheet = undef;
71 # URI of GIT logo (72x27 size)
72 our $logo = "++GITWEB_LOGO++";
73 # URI of GIT favicon, assumed to be image/png type
74 our $favicon = "++GITWEB_FAVICON++";
75
76 # URI and label (title) of GIT logo link
77 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
78 #our $logo_label = "git documentation";
79 our $logo_url = "http://git.or.cz/";
80 our $logo_label = "git homepage";
81
82 # source of projects list
83 our $projects_list = "++GITWEB_LIST++";
84
85 # the width (in characters) of the projects list "Description" column
86 our $projects_list_description_width = 25;
87
88 # default order of projects list
89 # valid values are none, project, descr, owner, and age
90 our $default_projects_order = "project";
91
92 # show repository only if this file exists
93 # (only effective if this variable evaluates to true)
94 our $export_ok = "++GITWEB_EXPORT_OK++";
95
96 # only allow viewing of repositories also shown on the overview page
97 our $strict_export = "++GITWEB_STRICT_EXPORT++";
98
99 # list of git base URLs used for URL to where fetch project from,
100 # i.e. full URL is "$git_base_url/$project"
101 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
102
103 # default blob_plain mimetype and default charset for text/plain blob
104 our $default_blob_plain_mimetype = 'text/plain';
105 our $default_text_plain_charset  = undef;
106
107 # file to use for guessing MIME types before trying /etc/mime.types
108 # (relative to the current git repository)
109 our $mimetypes_file = undef;
110
111 # assume this charset if line contains non-UTF-8 characters;
112 # it should be valid encoding (see Encoding::Supported(3pm) for list),
113 # for which encoding all byte sequences are valid, for example
114 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
115 # could be even 'utf-8' for the old behavior)
116 our $fallback_encoding = 'latin1';
117
118 # rename detection options for git-diff and git-diff-tree
119 # - default is '-M', with the cost proportional to
120 #   (number of removed files) * (number of new files).
121 # - more costly is '-C' (which implies '-M'), with the cost proportional to
122 #   (number of changed files + number of removed files) * (number of new files)
123 # - even more costly is '-C', '--find-copies-harder' with cost
124 #   (number of files in the original tree) * (number of new files)
125 # - one might want to include '-B' option, e.g. '-B', '-M'
126 our @diff_opts = ('-M'); # taken from git_commit
127
128 # information about snapshot formats that gitweb is capable of serving
129 our %known_snapshot_formats = (
130         # name => {
131         #       'display' => display name,
132         #       'type' => mime type,
133         #       'suffix' => filename suffix,
134         #       'format' => --format for git-archive,
135         #       'compressor' => [compressor command and arguments]
136         #                       (array reference, optional)}
137         #
138         'tgz' => {
139                 'display' => 'tar.gz',
140                 'type' => 'application/x-gzip',
141                 'suffix' => '.tar.gz',
142                 'format' => 'tar',
143                 'compressor' => ['gzip']},
144
145         'tbz2' => {
146                 'display' => 'tar.bz2',
147                 'type' => 'application/x-bzip2',
148                 'suffix' => '.tar.bz2',
149                 'format' => 'tar',
150                 'compressor' => ['bzip2']},
151
152         'zip' => {
153                 'display' => 'zip',
154                 'type' => 'application/x-zip',
155                 'suffix' => '.zip',
156                 'format' => 'zip'},
157 );
158
159 # Aliases so we understand old gitweb.snapshot values in repository
160 # configuration.
161 our %known_snapshot_format_aliases = (
162         'gzip'  => 'tgz',
163         'bzip2' => 'tbz2',
164
165         # backward compatibility: legacy gitweb config support
166         'x-gzip' => undef, 'gz' => undef,
167         'x-bzip2' => undef, 'bz2' => undef,
168         'x-zip' => undef, '' => undef,
169 );
170
171 # You define site-wide feature defaults here; override them with
172 # $GITWEB_CONFIG as necessary.
173 our %feature = (
174         # feature => {
175         #       'sub' => feature-sub (subroutine),
176         #       'override' => allow-override (boolean),
177         #       'default' => [ default options...] (array reference)}
178         #
179         # if feature is overridable (it means that allow-override has true value),
180         # then feature-sub will be called with default options as parameters;
181         # return value of feature-sub indicates if to enable specified feature
182         #
183         # if there is no 'sub' key (no feature-sub), then feature cannot be
184         # overriden
185         #
186         # use gitweb_check_feature(<feature>) to check if <feature> is enabled
187
188         # Enable the 'blame' blob view, showing the last commit that modified
189         # each line in the file. This can be very CPU-intensive.
190
191         # To enable system wide have in $GITWEB_CONFIG
192         # $feature{'blame'}{'default'} = [1];
193         # To have project specific config enable override in $GITWEB_CONFIG
194         # $feature{'blame'}{'override'} = 1;
195         # and in project config gitweb.blame = 0|1;
196         'blame' => {
197                 'sub' => \&feature_blame,
198                 'override' => 0,
199                 'default' => [0]},
200
201         # Enable the 'snapshot' link, providing a compressed archive of any
202         # tree. This can potentially generate high traffic if you have large
203         # project.
204
205         # Value is a list of formats defined in %known_snapshot_formats that
206         # you wish to offer.
207         # To disable system wide have in $GITWEB_CONFIG
208         # $feature{'snapshot'}{'default'} = [];
209         # To have project specific config enable override in $GITWEB_CONFIG
210         # $feature{'snapshot'}{'override'} = 1;
211         # and in project config, a comma-separated list of formats or "none"
212         # to disable.  Example: gitweb.snapshot = tbz2,zip;
213         'snapshot' => {
214                 'sub' => \&feature_snapshot,
215                 'override' => 0,
216                 'default' => ['tgz']},
217
218         # Enable text search, which will list the commits which match author,
219         # committer or commit text to a given string.  Enabled by default.
220         # Project specific override is not supported.
221         'search' => {
222                 'override' => 0,
223                 'default' => [1]},
224
225         # Enable grep search, which will list the files in currently selected
226         # tree containing the given string. Enabled by default. This can be
227         # potentially CPU-intensive, of course.
228
229         # To enable system wide have in $GITWEB_CONFIG
230         # $feature{'grep'}{'default'} = [1];
231         # To have project specific config enable override in $GITWEB_CONFIG
232         # $feature{'grep'}{'override'} = 1;
233         # and in project config gitweb.grep = 0|1;
234         'grep' => {
235                 'override' => 0,
236                 'default' => [1]},
237
238         # Enable the pickaxe search, which will list the commits that modified
239         # a given string in a file. This can be practical and quite faster
240         # alternative to 'blame', but still potentially CPU-intensive.
241
242         # To enable system wide have in $GITWEB_CONFIG
243         # $feature{'pickaxe'}{'default'} = [1];
244         # To have project specific config enable override in $GITWEB_CONFIG
245         # $feature{'pickaxe'}{'override'} = 1;
246         # and in project config gitweb.pickaxe = 0|1;
247         'pickaxe' => {
248                 'sub' => \&feature_pickaxe,
249                 'override' => 0,
250                 'default' => [1]},
251
252         # Make gitweb use an alternative format of the URLs which can be
253         # more readable and natural-looking: project name is embedded
254         # directly in the path and the query string contains other
255         # auxiliary information. All gitweb installations recognize
256         # URL in either format; this configures in which formats gitweb
257         # generates links.
258
259         # To enable system wide have in $GITWEB_CONFIG
260         # $feature{'pathinfo'}{'default'} = [1];
261         # Project specific override is not supported.
262
263         # Note that you will need to change the default location of CSS,
264         # favicon, logo and possibly other files to an absolute URL. Also,
265         # if gitweb.cgi serves as your indexfile, you will need to force
266         # $my_uri to contain the script name in your $GITWEB_CONFIG.
267         'pathinfo' => {
268                 'override' => 0,
269                 'default' => [0]},
270
271         # Make gitweb consider projects in project root subdirectories
272         # to be forks of existing projects. Given project $projname.git,
273         # projects matching $projname/*.git will not be shown in the main
274         # projects list, instead a '+' mark will be added to $projname
275         # there and a 'forks' view will be enabled for the project, listing
276         # all the forks. If project list is taken from a file, forks have
277         # to be listed after the main project.
278
279         # To enable system wide have in $GITWEB_CONFIG
280         # $feature{'forks'}{'default'} = [1];
281         # Project specific override is not supported.
282         'forks' => {
283                 'override' => 0,
284                 'default' => [0]},
285 );
286
287 sub gitweb_check_feature {
288         my ($name) = @_;
289         return unless exists $feature{$name};
290         my ($sub, $override, @defaults) = (
291                 $feature{$name}{'sub'},
292                 $feature{$name}{'override'},
293                 @{$feature{$name}{'default'}});
294         if (!$override) { return @defaults; }
295         if (!defined $sub) {
296                 warn "feature $name is not overrideable";
297                 return @defaults;
298         }
299         return $sub->(@defaults);
300 }
301
302 sub feature_blame {
303         my ($val) = git_get_project_config('blame', '--bool');
304
305         if ($val eq 'true') {
306                 return 1;
307         } elsif ($val eq 'false') {
308                 return 0;
309         }
310
311         return $_[0];
312 }
313
314 sub feature_snapshot {
315         my (@fmts) = @_;
316
317         my ($val) = git_get_project_config('snapshot');
318
319         if ($val) {
320                 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
321         }
322
323         return @fmts;
324 }
325
326 sub feature_grep {
327         my ($val) = git_get_project_config('grep', '--bool');
328
329         if ($val eq 'true') {
330                 return (1);
331         } elsif ($val eq 'false') {
332                 return (0);
333         }
334
335         return ($_[0]);
336 }
337
338 sub feature_pickaxe {
339         my ($val) = git_get_project_config('pickaxe', '--bool');
340
341         if ($val eq 'true') {
342                 return (1);
343         } elsif ($val eq 'false') {
344                 return (0);
345         }
346
347         return ($_[0]);
348 }
349
350 # checking HEAD file with -e is fragile if the repository was
351 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
352 # and then pruned.
353 sub check_head_link {
354         my ($dir) = @_;
355         my $headfile = "$dir/HEAD";
356         return ((-e $headfile) ||
357                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
358 }
359
360 sub check_export_ok {
361         my ($dir) = @_;
362         return (check_head_link($dir) &&
363                 (!$export_ok || -e "$dir/$export_ok"));
364 }
365
366 # process alternate names for backward compatibility
367 # filter out unsupported (unknown) snapshot formats
368 sub filter_snapshot_fmts {
369         my @fmts = @_;
370
371         @fmts = map {
372                 exists $known_snapshot_format_aliases{$_} ?
373                        $known_snapshot_format_aliases{$_} : $_} @fmts;
374         @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
375
376 }
377
378 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
379 if (-e $GITWEB_CONFIG) {
380         do $GITWEB_CONFIG;
381 } else {
382         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
383         do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
384 }
385
386 # version of the core git binary
387 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
388
389 $projects_list ||= $projectroot;
390
391 # ======================================================================
392 # input validation and dispatch
393 our $action = $cgi->param('a');
394 if (defined $action) {
395         if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
396                 die_error(400, "Invalid action parameter");
397         }
398 }
399
400 # parameters which are pathnames
401 our $project = $cgi->param('p');
402 if (defined $project) {
403         if (!validate_pathname($project) ||
404             !(-d "$projectroot/$project") ||
405             !check_head_link("$projectroot/$project") ||
406             ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
407             ($strict_export && !project_in_list($project))) {
408                 undef $project;
409                 die_error(404, "No such project");
410         }
411 }
412
413 our $file_name = $cgi->param('f');
414 if (defined $file_name) {
415         if (!validate_pathname($file_name)) {
416                 die_error(400, "Invalid file parameter");
417         }
418 }
419
420 our $file_parent = $cgi->param('fp');
421 if (defined $file_parent) {
422         if (!validate_pathname($file_parent)) {
423                 die_error(400, "Invalid file parent parameter");
424         }
425 }
426
427 # parameters which are refnames
428 our $hash = $cgi->param('h');
429 if (defined $hash) {
430         if (!validate_refname($hash)) {
431                 die_error(400, "Invalid hash parameter");
432         }
433 }
434
435 our $hash_parent = $cgi->param('hp');
436 if (defined $hash_parent) {
437         if (!validate_refname($hash_parent)) {
438                 die_error(400, "Invalid hash parent parameter");
439         }
440 }
441
442 our $hash_base = $cgi->param('hb');
443 if (defined $hash_base) {
444         if (!validate_refname($hash_base)) {
445                 die_error(400, "Invalid hash base parameter");
446         }
447 }
448
449 my %allowed_options = (
450         "--no-merges" => [ qw(rss atom log shortlog history) ],
451 );
452
453 our @extra_options = $cgi->param('opt');
454 if (defined @extra_options) {
455         foreach my $opt (@extra_options) {
456                 if (not exists $allowed_options{$opt}) {
457                         die_error(400, "Invalid option parameter");
458                 }
459                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
460                         die_error(400, "Invalid option parameter for this action");
461                 }
462         }
463 }
464
465 our $hash_parent_base = $cgi->param('hpb');
466 if (defined $hash_parent_base) {
467         if (!validate_refname($hash_parent_base)) {
468                 die_error(400, "Invalid hash parent base parameter");
469         }
470 }
471
472 # other parameters
473 our $page = $cgi->param('pg');
474 if (defined $page) {
475         if ($page =~ m/[^0-9]/) {
476                 die_error(400, "Invalid page parameter");
477         }
478 }
479
480 our $searchtype = $cgi->param('st');
481 if (defined $searchtype) {
482         if ($searchtype =~ m/[^a-z]/) {
483                 die_error(400, "Invalid searchtype parameter");
484         }
485 }
486
487 our $search_use_regexp = $cgi->param('sr');
488
489 our $searchtext = $cgi->param('s');
490 our $search_regexp;
491 if (defined $searchtext) {
492         if (length($searchtext) < 2) {
493                 die_error(403, "At least two characters are required for search parameter");
494         }
495         $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
496 }
497
498 # now read PATH_INFO and use it as alternative to parameters
499 sub evaluate_path_info {
500         return if defined $project;
501         my $path_info = $ENV{"PATH_INFO"};
502         return if !$path_info;
503         $path_info =~ s,^/+,,;
504         return if !$path_info;
505         # find which part of PATH_INFO is project
506         $project = $path_info;
507         $project =~ s,/+$,,;
508         while ($project && !check_head_link("$projectroot/$project")) {
509                 $project =~ s,/*[^/]*$,,;
510         }
511         # validate project
512         $project = validate_pathname($project);
513         if (!$project ||
514             ($export_ok && !-e "$projectroot/$project/$export_ok") ||
515             ($strict_export && !project_in_list($project))) {
516                 undef $project;
517                 return;
518         }
519         # do not change any parameters if an action is given using the query string
520         return if $action;
521         $path_info =~ s,^\Q$project\E/*,,;
522         my ($refname, $pathname) = split(/:/, $path_info, 2);
523         if (defined $pathname) {
524                 # we got "project.git/branch:filename" or "project.git/branch:dir/"
525                 # we could use git_get_type(branch:pathname), but it needs $git_dir
526                 $pathname =~ s,^/+,,;
527                 if (!$pathname || substr($pathname, -1) eq "/") {
528                         $action  ||= "tree";
529                         $pathname =~ s,/$,,;
530                 } else {
531                         $action  ||= "blob_plain";
532                 }
533                 $hash_base ||= validate_refname($refname);
534                 $file_name ||= validate_pathname($pathname);
535         } elsif (defined $refname) {
536                 # we got "project.git/branch"
537                 $action ||= "shortlog";
538                 $hash   ||= validate_refname($refname);
539         }
540 }
541 evaluate_path_info();
542
543 # path to the current git repository
544 our $git_dir;
545 $git_dir = "$projectroot/$project" if $project;
546
547 # dispatch
548 my %actions = (
549         "blame" => \&git_blame,
550         "blobdiff" => \&git_blobdiff,
551         "blobdiff_plain" => \&git_blobdiff_plain,
552         "blob" => \&git_blob,
553         "blob_plain" => \&git_blob_plain,
554         "commitdiff" => \&git_commitdiff,
555         "commitdiff_plain" => \&git_commitdiff_plain,
556         "commit" => \&git_commit,
557         "forks" => \&git_forks,
558         "heads" => \&git_heads,
559         "history" => \&git_history,
560         "log" => \&git_log,
561         "rss" => \&git_rss,
562         "atom" => \&git_atom,
563         "search" => \&git_search,
564         "search_help" => \&git_search_help,
565         "shortlog" => \&git_shortlog,
566         "summary" => \&git_summary,
567         "tag" => \&git_tag,
568         "tags" => \&git_tags,
569         "tree" => \&git_tree,
570         "snapshot" => \&git_snapshot,
571         "object" => \&git_object,
572         # those below don't need $project
573         "opml" => \&git_opml,
574         "project_list" => \&git_project_list,
575         "project_index" => \&git_project_index,
576 );
577
578 if (!defined $action) {
579         if (defined $hash) {
580                 $action = git_get_type($hash);
581         } elsif (defined $hash_base && defined $file_name) {
582                 $action = git_get_type("$hash_base:$file_name");
583         } elsif (defined $project) {
584                 $action = 'summary';
585         } else {
586                 $action = 'project_list';
587         }
588 }
589 if (!defined($actions{$action})) {
590         die_error(400, "Unknown action");
591 }
592 if ($action !~ m/^(opml|project_list|project_index)$/ &&
593     !$project) {
594         die_error(400, "Project needed");
595 }
596 $actions{$action}->();
597 exit;
598
599 ## ======================================================================
600 ## action links
601
602 sub href (%) {
603         my %params = @_;
604         # default is to use -absolute url() i.e. $my_uri
605         my $href = $params{-full} ? $my_url : $my_uri;
606
607         # XXX: Warning: If you touch this, check the search form for updating,
608         # too.
609
610         my @mapping = (
611                 project => "p",
612                 action => "a",
613                 file_name => "f",
614                 file_parent => "fp",
615                 hash => "h",
616                 hash_parent => "hp",
617                 hash_base => "hb",
618                 hash_parent_base => "hpb",
619                 page => "pg",
620                 order => "o",
621                 searchtext => "s",
622                 searchtype => "st",
623                 snapshot_format => "sf",
624                 extra_options => "opt",
625                 search_use_regexp => "sr",
626         );
627         my %mapping = @mapping;
628
629         $params{'project'} = $project unless exists $params{'project'};
630
631         if ($params{-replay}) {
632                 while (my ($name, $symbol) = each %mapping) {
633                         if (!exists $params{$name}) {
634                                 # to allow for multivalued params we use arrayref form
635                                 $params{$name} = [ $cgi->param($symbol) ];
636                         }
637                 }
638         }
639
640         my ($use_pathinfo) = gitweb_check_feature('pathinfo');
641         if ($use_pathinfo) {
642                 # use PATH_INFO for project name
643                 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
644                 delete $params{'project'};
645
646                 # Summary just uses the project path URL
647                 if (defined $params{'action'} && $params{'action'} eq 'summary') {
648                         delete $params{'action'};
649                 }
650         }
651
652         # now encode the parameters explicitly
653         my @result = ();
654         for (my $i = 0; $i < @mapping; $i += 2) {
655                 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
656                 if (defined $params{$name}) {
657                         if (ref($params{$name}) eq "ARRAY") {
658                                 foreach my $par (@{$params{$name}}) {
659                                         push @result, $symbol . "=" . esc_param($par);
660                                 }
661                         } else {
662                                 push @result, $symbol . "=" . esc_param($params{$name});
663                         }
664                 }
665         }
666         $href .= "?" . join(';', @result) if scalar @result;
667
668         return $href;
669 }
670
671
672 ## ======================================================================
673 ## validation, quoting/unquoting and escaping
674
675 sub validate_pathname {
676         my $input = shift || return undef;
677
678         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
679         # at the beginning, at the end, and between slashes.
680         # also this catches doubled slashes
681         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
682                 return undef;
683         }
684         # no null characters
685         if ($input =~ m!\0!) {
686                 return undef;
687         }
688         return $input;
689 }
690
691 sub validate_refname {
692         my $input = shift || return undef;
693
694         # textual hashes are O.K.
695         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
696                 return $input;
697         }
698         # it must be correct pathname
699         $input = validate_pathname($input)
700                 or return undef;
701         # restrictions on ref name according to git-check-ref-format
702         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
703                 return undef;
704         }
705         return $input;
706 }
707
708 # decode sequences of octets in utf8 into Perl's internal form,
709 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
710 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
711 sub to_utf8 {
712         my $str = shift;
713         if (utf8::valid($str)) {
714                 utf8::decode($str);
715                 return $str;
716         } else {
717                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
718         }
719 }
720
721 # quote unsafe chars, but keep the slash, even when it's not
722 # correct, but quoted slashes look too horrible in bookmarks
723 sub esc_param {
724         my $str = shift;
725         $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
726         $str =~ s/\+/%2B/g;
727         $str =~ s/ /\+/g;
728         return $str;
729 }
730
731 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
732 sub esc_url {
733         my $str = shift;
734         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
735         $str =~ s/\+/%2B/g;
736         $str =~ s/ /\+/g;
737         return $str;
738 }
739
740 # replace invalid utf8 character with SUBSTITUTION sequence
741 sub esc_html ($;%) {
742         my $str = shift;
743         my %opts = @_;
744
745         $str = to_utf8($str);
746         $str = $cgi->escapeHTML($str);
747         if ($opts{'-nbsp'}) {
748                 $str =~ s/ /&nbsp;/g;
749         }
750         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
751         return $str;
752 }
753
754 # quote control characters and escape filename to HTML
755 sub esc_path {
756         my $str = shift;
757         my %opts = @_;
758
759         $str = to_utf8($str);
760         $str = $cgi->escapeHTML($str);
761         if ($opts{'-nbsp'}) {
762                 $str =~ s/ /&nbsp;/g;
763         }
764         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
765         return $str;
766 }
767
768 # Make control characters "printable", using character escape codes (CEC)
769 sub quot_cec {
770         my $cntrl = shift;
771         my %opts = @_;
772         my %es = ( # character escape codes, aka escape sequences
773                 "\t" => '\t',   # tab            (HT)
774                 "\n" => '\n',   # line feed      (LF)
775                 "\r" => '\r',   # carrige return (CR)
776                 "\f" => '\f',   # form feed      (FF)
777                 "\b" => '\b',   # backspace      (BS)
778                 "\a" => '\a',   # alarm (bell)   (BEL)
779                 "\e" => '\e',   # escape         (ESC)
780                 "\013" => '\v', # vertical tab   (VT)
781                 "\000" => '\0', # nul character  (NUL)
782         );
783         my $chr = ( (exists $es{$cntrl})
784                     ? $es{$cntrl}
785                     : sprintf('\%2x', ord($cntrl)) );
786         if ($opts{-nohtml}) {
787                 return $chr;
788         } else {
789                 return "<span class=\"cntrl\">$chr</span>";
790         }
791 }
792
793 # Alternatively use unicode control pictures codepoints,
794 # Unicode "printable representation" (PR)
795 sub quot_upr {
796         my $cntrl = shift;
797         my %opts = @_;
798
799         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
800         if ($opts{-nohtml}) {
801                 return $chr;
802         } else {
803                 return "<span class=\"cntrl\">$chr</span>";
804         }
805 }
806
807 # git may return quoted and escaped filenames
808 sub unquote {
809         my $str = shift;
810
811         sub unq {
812                 my $seq = shift;
813                 my %es = ( # character escape codes, aka escape sequences
814                         't' => "\t",   # tab            (HT, TAB)
815                         'n' => "\n",   # newline        (NL)
816                         'r' => "\r",   # return         (CR)
817                         'f' => "\f",   # form feed      (FF)
818                         'b' => "\b",   # backspace      (BS)
819                         'a' => "\a",   # alarm (bell)   (BEL)
820                         'e' => "\e",   # escape         (ESC)
821                         'v' => "\013", # vertical tab   (VT)
822                 );
823
824                 if ($seq =~ m/^[0-7]{1,3}$/) {
825                         # octal char sequence
826                         return chr(oct($seq));
827                 } elsif (exists $es{$seq}) {
828                         # C escape sequence, aka character escape code
829                         return $es{$seq};
830                 }
831                 # quoted ordinary character
832                 return $seq;
833         }
834
835         if ($str =~ m/^"(.*)"$/) {
836                 # needs unquoting
837                 $str = $1;
838                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
839         }
840         return $str;
841 }
842
843 # escape tabs (convert tabs to spaces)
844 sub untabify {
845         my $line = shift;
846
847         while ((my $pos = index($line, "\t")) != -1) {
848                 if (my $count = (8 - ($pos % 8))) {
849                         my $spaces = ' ' x $count;
850                         $line =~ s/\t/$spaces/;
851                 }
852         }
853
854         return $line;
855 }
856
857 sub project_in_list {
858         my $project = shift;
859         my @list = git_get_projects_list();
860         return @list && scalar(grep { $_->{'path'} eq $project } @list);
861 }
862
863 ## ----------------------------------------------------------------------
864 ## HTML aware string manipulation
865
866 # Try to chop given string on a word boundary between position
867 # $len and $len+$add_len. If there is no word boundary there,
868 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
869 # (marking chopped part) would be longer than given string.
870 sub chop_str {
871         my $str = shift;
872         my $len = shift;
873         my $add_len = shift || 10;
874         my $where = shift || 'right'; # 'left' | 'center' | 'right'
875
876         # Make sure perl knows it is utf8 encoded so we don't
877         # cut in the middle of a utf8 multibyte char.
878         $str = to_utf8($str);
879
880         # allow only $len chars, but don't cut a word if it would fit in $add_len
881         # if it doesn't fit, cut it if it's still longer than the dots we would add
882         # remove chopped character entities entirely
883
884         # when chopping in the middle, distribute $len into left and right part
885         # return early if chopping wouldn't make string shorter
886         if ($where eq 'center') {
887                 return $str if ($len + 5 >= length($str)); # filler is length 5
888                 $len = int($len/2);
889         } else {
890                 return $str if ($len + 4 >= length($str)); # filler is length 4
891         }
892
893         # regexps: ending and beginning with word part up to $add_len
894         my $endre = qr/.{$len}\w{0,$add_len}/;
895         my $begre = qr/\w{0,$add_len}.{$len}/;
896
897         if ($where eq 'left') {
898                 $str =~ m/^(.*?)($begre)$/;
899                 my ($lead, $body) = ($1, $2);
900                 if (length($lead) > 4) {
901                         $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
902                         $lead = " ...";
903                 }
904                 return "$lead$body";
905
906         } elsif ($where eq 'center') {
907                 $str =~ m/^($endre)(.*)$/;
908                 my ($left, $str)  = ($1, $2);
909                 $str =~ m/^(.*?)($begre)$/;
910                 my ($mid, $right) = ($1, $2);
911                 if (length($mid) > 5) {
912                         $left  =~ s/&[^;]*$//;
913                         $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
914                         $mid = " ... ";
915                 }
916                 return "$left$mid$right";
917
918         } else {
919                 $str =~ m/^($endre)(.*)$/;
920                 my $body = $1;
921                 my $tail = $2;
922                 if (length($tail) > 4) {
923                         $body =~ s/&[^;]*$//;
924                         $tail = "... ";
925                 }
926                 return "$body$tail";
927         }
928 }
929
930 # takes the same arguments as chop_str, but also wraps a <span> around the
931 # result with a title attribute if it does get chopped. Additionally, the
932 # string is HTML-escaped.
933 sub chop_and_escape_str {
934         my ($str) = @_;
935
936         my $chopped = chop_str(@_);
937         if ($chopped eq $str) {
938                 return esc_html($chopped);
939         } else {
940                 $str =~ s/([[:cntrl:]])/?/g;
941                 return $cgi->span({-title=>$str}, esc_html($chopped));
942         }
943 }
944
945 ## ----------------------------------------------------------------------
946 ## functions returning short strings
947
948 # CSS class for given age value (in seconds)
949 sub age_class {
950         my $age = shift;
951
952         if (!defined $age) {
953                 return "noage";
954         } elsif ($age < 60*60*2) {
955                 return "age0";
956         } elsif ($age < 60*60*24*2) {
957                 return "age1";
958         } else {
959                 return "age2";
960         }
961 }
962
963 # convert age in seconds to "nn units ago" string
964 sub age_string {
965         my $age = shift;
966         my $age_str;
967
968         if ($age > 60*60*24*365*2) {
969                 $age_str = (int $age/60/60/24/365);
970                 $age_str .= " years ago";
971         } elsif ($age > 60*60*24*(365/12)*2) {
972                 $age_str = int $age/60/60/24/(365/12);
973                 $age_str .= " months ago";
974         } elsif ($age > 60*60*24*7*2) {
975                 $age_str = int $age/60/60/24/7;
976                 $age_str .= " weeks ago";
977         } elsif ($age > 60*60*24*2) {
978                 $age_str = int $age/60/60/24;
979                 $age_str .= " days ago";
980         } elsif ($age > 60*60*2) {
981                 $age_str = int $age/60/60;
982                 $age_str .= " hours ago";
983         } elsif ($age > 60*2) {
984                 $age_str = int $age/60;
985                 $age_str .= " min ago";
986         } elsif ($age > 2) {
987                 $age_str = int $age;
988                 $age_str .= " sec ago";
989         } else {
990                 $age_str .= " right now";
991         }
992         return $age_str;
993 }
994
995 use constant {
996         S_IFINVALID => 0030000,
997         S_IFGITLINK => 0160000,
998 };
999
1000 # submodule/subproject, a commit object reference
1001 sub S_ISGITLINK($) {
1002         my $mode = shift;
1003
1004         return (($mode & S_IFMT) == S_IFGITLINK)
1005 }
1006
1007 # convert file mode in octal to symbolic file mode string
1008 sub mode_str {
1009         my $mode = oct shift;
1010
1011         if (S_ISGITLINK($mode)) {
1012                 return 'm---------';
1013         } elsif (S_ISDIR($mode & S_IFMT)) {
1014                 return 'drwxr-xr-x';
1015         } elsif (S_ISLNK($mode)) {
1016                 return 'lrwxrwxrwx';
1017         } elsif (S_ISREG($mode)) {
1018                 # git cares only about the executable bit
1019                 if ($mode & S_IXUSR) {
1020                         return '-rwxr-xr-x';
1021                 } else {
1022                         return '-rw-r--r--';
1023                 };
1024         } else {
1025                 return '----------';
1026         }
1027 }
1028
1029 # convert file mode in octal to file type string
1030 sub file_type {
1031         my $mode = shift;
1032
1033         if ($mode !~ m/^[0-7]+$/) {
1034                 return $mode;
1035         } else {
1036                 $mode = oct $mode;
1037         }
1038
1039         if (S_ISGITLINK($mode)) {
1040                 return "submodule";
1041         } elsif (S_ISDIR($mode & S_IFMT)) {
1042                 return "directory";
1043         } elsif (S_ISLNK($mode)) {
1044                 return "symlink";
1045         } elsif (S_ISREG($mode)) {
1046                 return "file";
1047         } else {
1048                 return "unknown";
1049         }
1050 }
1051
1052 # convert file mode in octal to file type description string
1053 sub file_type_long {
1054         my $mode = shift;
1055
1056         if ($mode !~ m/^[0-7]+$/) {
1057                 return $mode;
1058         } else {
1059                 $mode = oct $mode;
1060         }
1061
1062         if (S_ISGITLINK($mode)) {
1063                 return "submodule";
1064         } elsif (S_ISDIR($mode & S_IFMT)) {
1065                 return "directory";
1066         } elsif (S_ISLNK($mode)) {
1067                 return "symlink";
1068         } elsif (S_ISREG($mode)) {
1069                 if ($mode & S_IXUSR) {
1070                         return "executable";
1071                 } else {
1072                         return "file";
1073                 };
1074         } else {
1075                 return "unknown";
1076         }
1077 }
1078
1079
1080 ## ----------------------------------------------------------------------
1081 ## functions returning short HTML fragments, or transforming HTML fragments
1082 ## which don't belong to other sections
1083
1084 # format line of commit message.
1085 sub format_log_line_html {
1086         my $line = shift;
1087
1088         $line = esc_html($line, -nbsp=>1);
1089         if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1090                 my $hash_text = $1;
1091                 my $link =
1092                         $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1093                                 -class => "text"}, $hash_text);
1094                 $line =~ s/$hash_text/$link/;
1095         }
1096         return $line;
1097 }
1098
1099 # format marker of refs pointing to given object
1100
1101 # the destination action is chosen based on object type and current context:
1102 # - for annotated tags, we choose the tag view unless it's the current view
1103 #   already, in which case we go to shortlog view
1104 # - for other refs, we keep the current view if we're in history, shortlog or
1105 #   log view, and select shortlog otherwise
1106 sub format_ref_marker {
1107         my ($refs, $id) = @_;
1108         my $markers = '';
1109
1110         if (defined $refs->{$id}) {
1111                 foreach my $ref (@{$refs->{$id}}) {
1112                         # this code exploits the fact that non-lightweight tags are the
1113                         # only indirect objects, and that they are the only objects for which
1114                         # we want to use tag instead of shortlog as action
1115                         my ($type, $name) = qw();
1116                         my $indirect = ($ref =~ s/\^\{\}$//);
1117                         # e.g. tags/v2.6.11 or heads/next
1118                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1119                                 $type = $1;
1120                                 $name = $2;
1121                         } else {
1122                                 $type = "ref";
1123                                 $name = $ref;
1124                         }
1125
1126                         my $class = $type;
1127                         $class .= " indirect" if $indirect;
1128
1129                         my $dest_action = "shortlog";
1130
1131                         if ($indirect) {
1132                                 $dest_action = "tag" unless $action eq "tag";
1133                         } elsif ($action =~ /^(history|(short)?log)$/) {
1134                                 $dest_action = $action;
1135                         }
1136
1137                         my $dest = "";
1138                         $dest .= "refs/" unless $ref =~ m!^refs/!;
1139                         $dest .= $ref;
1140
1141                         my $link = $cgi->a({
1142                                 -href => href(
1143                                         action=>$dest_action,
1144                                         hash=>$dest
1145                                 )}, $name);
1146
1147                         $markers .= " <span class=\"$class\" title=\"$ref\">" .
1148                                 $link . "</span>";
1149                 }
1150         }
1151
1152         if ($markers) {
1153                 return ' <span class="refs">'. $markers . '</span>';
1154         } else {
1155                 return "";
1156         }
1157 }
1158
1159 # format, perhaps shortened and with markers, title line
1160 sub format_subject_html {
1161         my ($long, $short, $href, $extra) = @_;
1162         $extra = '' unless defined($extra);
1163
1164         if (length($short) < length($long)) {
1165                 return $cgi->a({-href => $href, -class => "list subject",
1166                                 -title => to_utf8($long)},
1167                        esc_html($short) . $extra);
1168         } else {
1169                 return $cgi->a({-href => $href, -class => "list subject"},
1170                        esc_html($long)  . $extra);
1171         }
1172 }
1173
1174 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1175 sub format_git_diff_header_line {
1176         my $line = shift;
1177         my $diffinfo = shift;
1178         my ($from, $to) = @_;
1179
1180         if ($diffinfo->{'nparents'}) {
1181                 # combined diff
1182                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1183                 if ($to->{'href'}) {
1184                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1185                                          esc_path($to->{'file'}));
1186                 } else { # file was deleted (no href)
1187                         $line .= esc_path($to->{'file'});
1188                 }
1189         } else {
1190                 # "ordinary" diff
1191                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1192                 if ($from->{'href'}) {
1193                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1194                                          'a/' . esc_path($from->{'file'}));
1195                 } else { # file was added (no href)
1196                         $line .= 'a/' . esc_path($from->{'file'});
1197                 }
1198                 $line .= ' ';
1199                 if ($to->{'href'}) {
1200                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1201                                          'b/' . esc_path($to->{'file'}));
1202                 } else { # file was deleted
1203                         $line .= 'b/' . esc_path($to->{'file'});
1204                 }
1205         }
1206
1207         return "<div class=\"diff header\">$line</div>\n";
1208 }
1209
1210 # format extended diff header line, before patch itself
1211 sub format_extended_diff_header_line {
1212         my $line = shift;
1213         my $diffinfo = shift;
1214         my ($from, $to) = @_;
1215
1216         # match <path>
1217         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1218                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1219                                        esc_path($from->{'file'}));
1220         }
1221         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1222                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1223                                  esc_path($to->{'file'}));
1224         }
1225         # match single <mode>
1226         if ($line =~ m/\s(\d{6})$/) {
1227                 $line .= '<span class="info"> (' .
1228                          file_type_long($1) .
1229                          ')</span>';
1230         }
1231         # match <hash>
1232         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1233                 # can match only for combined diff
1234                 $line = 'index ';
1235                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1236                         if ($from->{'href'}[$i]) {
1237                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1238                                                   -class=>"hash"},
1239                                                  substr($diffinfo->{'from_id'}[$i],0,7));
1240                         } else {
1241                                 $line .= '0' x 7;
1242                         }
1243                         # separator
1244                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1245                 }
1246                 $line .= '..';
1247                 if ($to->{'href'}) {
1248                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1249                                          substr($diffinfo->{'to_id'},0,7));
1250                 } else {
1251                         $line .= '0' x 7;
1252                 }
1253
1254         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1255                 # can match only for ordinary diff
1256                 my ($from_link, $to_link);
1257                 if ($from->{'href'}) {
1258                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1259                                              substr($diffinfo->{'from_id'},0,7));
1260                 } else {
1261                         $from_link = '0' x 7;
1262                 }
1263                 if ($to->{'href'}) {
1264                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1265                                            substr($diffinfo->{'to_id'},0,7));
1266                 } else {
1267                         $to_link = '0' x 7;
1268                 }
1269                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1270                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1271         }
1272
1273         return $line . "<br/>\n";
1274 }
1275
1276 # format from-file/to-file diff header
1277 sub format_diff_from_to_header {
1278         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1279         my $line;
1280         my $result = '';
1281
1282         $line = $from_line;
1283         #assert($line =~ m/^---/) if DEBUG;
1284         # no extra formatting for "^--- /dev/null"
1285         if (! $diffinfo->{'nparents'}) {
1286                 # ordinary (single parent) diff
1287                 if ($line =~ m!^--- "?a/!) {
1288                         if ($from->{'href'}) {
1289                                 $line = '--- a/' .
1290                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1291                                                 esc_path($from->{'file'}));
1292                         } else {
1293                                 $line = '--- a/' .
1294                                         esc_path($from->{'file'});
1295                         }
1296                 }
1297                 $result .= qq!<div class="diff from_file">$line</div>\n!;
1298
1299         } else {
1300                 # combined diff (merge commit)
1301                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1302                         if ($from->{'href'}[$i]) {
1303                                 $line = '--- ' .
1304                                         $cgi->a({-href=>href(action=>"blobdiff",
1305                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
1306                                                              hash_parent_base=>$parents[$i],
1307                                                              file_parent=>$from->{'file'}[$i],
1308                                                              hash=>$diffinfo->{'to_id'},
1309                                                              hash_base=>$hash,
1310                                                              file_name=>$to->{'file'}),
1311                                                  -class=>"path",
1312                                                  -title=>"diff" . ($i+1)},
1313                                                 $i+1) .
1314                                         '/' .
1315                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1316                                                 esc_path($from->{'file'}[$i]));
1317                         } else {
1318                                 $line = '--- /dev/null';
1319                         }
1320                         $result .= qq!<div class="diff from_file">$line</div>\n!;
1321                 }
1322         }
1323
1324         $line = $to_line;
1325         #assert($line =~ m/^\+\+\+/) if DEBUG;
1326         # no extra formatting for "^+++ /dev/null"
1327         if ($line =~ m!^\+\+\+ "?b/!) {
1328                 if ($to->{'href'}) {
1329                         $line = '+++ b/' .
1330                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1331                                         esc_path($to->{'file'}));
1332                 } else {
1333                         $line = '+++ b/' .
1334                                 esc_path($to->{'file'});
1335                 }
1336         }
1337         $result .= qq!<div class="diff to_file">$line</div>\n!;
1338
1339         return $result;
1340 }
1341
1342 # create note for patch simplified by combined diff
1343 sub format_diff_cc_simplified {
1344         my ($diffinfo, @parents) = @_;
1345         my $result = '';
1346
1347         $result .= "<div class=\"diff header\">" .
1348                    "diff --cc ";
1349         if (!is_deleted($diffinfo)) {
1350                 $result .= $cgi->a({-href => href(action=>"blob",
1351                                                   hash_base=>$hash,
1352                                                   hash=>$diffinfo->{'to_id'},
1353                                                   file_name=>$diffinfo->{'to_file'}),
1354                                     -class => "path"},
1355                                    esc_path($diffinfo->{'to_file'}));
1356         } else {
1357                 $result .= esc_path($diffinfo->{'to_file'});
1358         }
1359         $result .= "</div>\n" . # class="diff header"
1360                    "<div class=\"diff nodifferences\">" .
1361                    "Simple merge" .
1362                    "</div>\n"; # class="diff nodifferences"
1363
1364         return $result;
1365 }
1366
1367 # format patch (diff) line (not to be used for diff headers)
1368 sub format_diff_line {
1369         my $line = shift;
1370         my ($from, $to) = @_;
1371         my $diff_class = "";
1372
1373         chomp $line;
1374
1375         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1376                 # combined diff
1377                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1378                 if ($line =~ m/^\@{3}/) {
1379                         $diff_class = " chunk_header";
1380                 } elsif ($line =~ m/^\\/) {
1381                         $diff_class = " incomplete";
1382                 } elsif ($prefix =~ tr/+/+/) {
1383                         $diff_class = " add";
1384                 } elsif ($prefix =~ tr/-/-/) {
1385                         $diff_class = " rem";
1386                 }
1387         } else {
1388                 # assume ordinary diff
1389                 my $char = substr($line, 0, 1);
1390                 if ($char eq '+') {
1391                         $diff_class = " add";
1392                 } elsif ($char eq '-') {
1393                         $diff_class = " rem";
1394                 } elsif ($char eq '@') {
1395                         $diff_class = " chunk_header";
1396                 } elsif ($char eq "\\") {
1397                         $diff_class = " incomplete";
1398                 }
1399         }
1400         $line = untabify($line);
1401         if ($from && $to && $line =~ m/^\@{2} /) {
1402                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1403                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1404
1405                 $from_lines = 0 unless defined $from_lines;
1406                 $to_lines   = 0 unless defined $to_lines;
1407
1408                 if ($from->{'href'}) {
1409                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1410                                              -class=>"list"}, $from_text);
1411                 }
1412                 if ($to->{'href'}) {
1413                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1414                                              -class=>"list"}, $to_text);
1415                 }
1416                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1417                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1418                 return "<div class=\"diff$diff_class\">$line</div>\n";
1419         } elsif ($from && $to && $line =~ m/^\@{3}/) {
1420                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1421                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1422
1423                 @from_text = split(' ', $ranges);
1424                 for (my $i = 0; $i < @from_text; ++$i) {
1425                         ($from_start[$i], $from_nlines[$i]) =
1426                                 (split(',', substr($from_text[$i], 1)), 0);
1427                 }
1428
1429                 $to_text   = pop @from_text;
1430                 $to_start  = pop @from_start;
1431                 $to_nlines = pop @from_nlines;
1432
1433                 $line = "<span class=\"chunk_info\">$prefix ";
1434                 for (my $i = 0; $i < @from_text; ++$i) {
1435                         if ($from->{'href'}[$i]) {
1436                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1437                                                   -class=>"list"}, $from_text[$i]);
1438                         } else {
1439                                 $line .= $from_text[$i];
1440                         }
1441                         $line .= " ";
1442                 }
1443                 if ($to->{'href'}) {
1444                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1445                                           -class=>"list"}, $to_text);
1446                 } else {
1447                         $line .= $to_text;
1448                 }
1449                 $line .= " $prefix</span>" .
1450                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1451                 return "<div class=\"diff$diff_class\">$line</div>\n";
1452         }
1453         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1454 }
1455
1456 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1457 # linked.  Pass the hash of the tree/commit to snapshot.
1458 sub format_snapshot_links {
1459         my ($hash) = @_;
1460         my @snapshot_fmts = gitweb_check_feature('snapshot');
1461         @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1462         my $num_fmts = @snapshot_fmts;
1463         if ($num_fmts > 1) {
1464                 # A parenthesized list of links bearing format names.
1465                 # e.g. "snapshot (_tar.gz_ _zip_)"
1466                 return "snapshot (" . join(' ', map
1467                         $cgi->a({
1468                                 -href => href(
1469                                         action=>"snapshot",
1470                                         hash=>$hash,
1471                                         snapshot_format=>$_
1472                                 )
1473                         }, $known_snapshot_formats{$_}{'display'})
1474                 , @snapshot_fmts) . ")";
1475         } elsif ($num_fmts == 1) {
1476                 # A single "snapshot" link whose tooltip bears the format name.
1477                 # i.e. "_snapshot_"
1478                 my ($fmt) = @snapshot_fmts;
1479                 return
1480                         $cgi->a({
1481                                 -href => href(
1482                                         action=>"snapshot",
1483                                         hash=>$hash,
1484                                         snapshot_format=>$fmt
1485                                 ),
1486                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1487                         }, "snapshot");
1488         } else { # $num_fmts == 0
1489                 return undef;
1490         }
1491 }
1492
1493 ## ......................................................................
1494 ## functions returning values to be passed, perhaps after some
1495 ## transformation, to other functions; e.g. returning arguments to href()
1496
1497 # returns hash to be passed to href to generate gitweb URL
1498 # in -title key it returns description of link
1499 sub get_feed_info {
1500         my $format = shift || 'Atom';
1501         my %res = (action => lc($format));
1502
1503         # feed links are possible only for project views
1504         return unless (defined $project);
1505         # some views should link to OPML, or to generic project feed,
1506         # or don't have specific feed yet (so they should use generic)
1507         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1508
1509         my $branch;
1510         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1511         # from tag links; this also makes possible to detect branch links
1512         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1513             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
1514                 $branch = $1;
1515         }
1516         # find log type for feed description (title)
1517         my $type = 'log';
1518         if (defined $file_name) {
1519                 $type  = "history of $file_name";
1520                 $type .= "/" if ($action eq 'tree');
1521                 $type .= " on '$branch'" if (defined $branch);
1522         } else {
1523                 $type = "log of $branch" if (defined $branch);
1524         }
1525
1526         $res{-title} = $type;
1527         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1528         $res{'file_name'} = $file_name;
1529
1530         return %res;
1531 }
1532
1533 ## ----------------------------------------------------------------------
1534 ## git utility subroutines, invoking git commands
1535
1536 # returns path to the core git executable and the --git-dir parameter as list
1537 sub git_cmd {
1538         return $GIT, '--git-dir='.$git_dir;
1539 }
1540
1541 # quote the given arguments for passing them to the shell
1542 # quote_command("command", "arg 1", "arg with ' and ! characters")
1543 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1544 # Try to avoid using this function wherever possible.
1545 sub quote_command {
1546         return join(' ',
1547                     map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1548 }
1549
1550 # get HEAD ref of given project as hash
1551 sub git_get_head_hash {
1552         my $project = shift;
1553         my $o_git_dir = $git_dir;
1554         my $retval = undef;
1555         $git_dir = "$projectroot/$project";
1556         if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1557                 my $head = <$fd>;
1558                 close $fd;
1559                 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1560                         $retval = $1;
1561                 }
1562         }
1563         if (defined $o_git_dir) {
1564                 $git_dir = $o_git_dir;
1565         }
1566         return $retval;
1567 }
1568
1569 # get type of given object
1570 sub git_get_type {
1571         my $hash = shift;
1572
1573         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1574         my $type = <$fd>;
1575         close $fd or return;
1576         chomp $type;
1577         return $type;
1578 }
1579
1580 # repository configuration
1581 our $config_file = '';
1582 our %config;
1583
1584 # store multiple values for single key as anonymous array reference
1585 # single values stored directly in the hash, not as [ <value> ]
1586 sub hash_set_multi {
1587         my ($hash, $key, $value) = @_;
1588
1589         if (!exists $hash->{$key}) {
1590                 $hash->{$key} = $value;
1591         } elsif (!ref $hash->{$key}) {
1592                 $hash->{$key} = [ $hash->{$key}, $value ];
1593         } else {
1594                 push @{$hash->{$key}}, $value;
1595         }
1596 }
1597
1598 # return hash of git project configuration
1599 # optionally limited to some section, e.g. 'gitweb'
1600 sub git_parse_project_config {
1601         my $section_regexp = shift;
1602         my %config;
1603
1604         local $/ = "\0";
1605
1606         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1607                 or return;
1608
1609         while (my $keyval = <$fh>) {
1610                 chomp $keyval;
1611                 my ($key, $value) = split(/\n/, $keyval, 2);
1612
1613                 hash_set_multi(\%config, $key, $value)
1614                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1615         }
1616         close $fh;
1617
1618         return %config;
1619 }
1620
1621 # convert config value to boolean, 'true' or 'false'
1622 # no value, number > 0, 'true' and 'yes' values are true
1623 # rest of values are treated as false (never as error)
1624 sub config_to_bool {
1625         my $val = shift;
1626
1627         # strip leading and trailing whitespace
1628         $val =~ s/^\s+//;
1629         $val =~ s/\s+$//;
1630
1631         return (!defined $val ||               # section.key
1632                 ($val =~ /^\d+$/ && $val) ||   # section.key = 1
1633                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
1634 }
1635
1636 # convert config value to simple decimal number
1637 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1638 # to be multiplied by 1024, 1048576, or 1073741824
1639 sub config_to_int {
1640         my $val = shift;
1641
1642         # strip leading and trailing whitespace
1643         $val =~ s/^\s+//;
1644         $val =~ s/\s+$//;
1645
1646         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1647                 $unit = lc($unit);
1648                 # unknown unit is treated as 1
1649                 return $num * ($unit eq 'g' ? 1073741824 :
1650                                $unit eq 'm' ?    1048576 :
1651                                $unit eq 'k' ?       1024 : 1);
1652         }
1653         return $val;
1654 }
1655
1656 # convert config value to array reference, if needed
1657 sub config_to_multi {
1658         my $val = shift;
1659
1660         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1661 }
1662
1663 sub git_get_project_config {
1664         my ($key, $type) = @_;
1665
1666         # key sanity check
1667         return unless ($key);
1668         $key =~ s/^gitweb\.//;
1669         return if ($key =~ m/\W/);
1670
1671         # type sanity check
1672         if (defined $type) {
1673                 $type =~ s/^--//;
1674                 $type = undef
1675                         unless ($type eq 'bool' || $type eq 'int');
1676         }
1677
1678         # get config
1679         if (!defined $config_file ||
1680             $config_file ne "$git_dir/config") {
1681                 %config = git_parse_project_config('gitweb');
1682                 $config_file = "$git_dir/config";
1683         }
1684
1685         # ensure given type
1686         if (!defined $type) {
1687                 return $config{"gitweb.$key"};
1688         } elsif ($type eq 'bool') {
1689                 # backward compatibility: 'git config --bool' returns true/false
1690                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1691         } elsif ($type eq 'int') {
1692                 return config_to_int($config{"gitweb.$key"});
1693         }
1694         return $config{"gitweb.$key"};
1695 }
1696
1697 # get hash of given path at given ref
1698 sub git_get_hash_by_path {
1699         my $base = shift;
1700         my $path = shift || return undef;
1701         my $type = shift;
1702
1703         $path =~ s,/+$,,;
1704
1705         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1706                 or die_error(500, "Open git-ls-tree failed");
1707         my $line = <$fd>;
1708         close $fd or return undef;
1709
1710         if (!defined $line) {
1711                 # there is no tree or hash given by $path at $base
1712                 return undef;
1713         }
1714
1715         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
1716         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1717         if (defined $type && $type ne $2) {
1718                 # type doesn't match
1719                 return undef;
1720         }
1721         return $3;
1722 }
1723
1724 # get path of entry with given hash at given tree-ish (ref)
1725 # used to get 'from' filename for combined diff (merge commit) for renames
1726 sub git_get_path_by_hash {
1727         my $base = shift || return;
1728         my $hash = shift || return;
1729
1730         local $/ = "\0";
1731
1732         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1733                 or return undef;
1734         while (my $line = <$fd>) {
1735                 chomp $line;
1736
1737                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
1738                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
1739                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1740                         close $fd;
1741                         return $1;
1742                 }
1743         }
1744         close $fd;
1745         return undef;
1746 }
1747
1748 ## ......................................................................
1749 ## git utility functions, directly accessing git repository
1750
1751 sub git_get_project_description {
1752         my $path = shift;
1753
1754         $git_dir = "$projectroot/$path";
1755         open my $fd, "$git_dir/description"
1756                 or return git_get_project_config('description');
1757         my $descr = <$fd>;
1758         close $fd;
1759         if (defined $descr) {
1760                 chomp $descr;
1761         }
1762         return $descr;
1763 }
1764
1765 sub git_get_project_url_list {
1766         my $path = shift;
1767
1768         $git_dir = "$projectroot/$path";
1769         open my $fd, "$git_dir/cloneurl"
1770                 or return wantarray ?
1771                 @{ config_to_multi(git_get_project_config('url')) } :
1772                    config_to_multi(git_get_project_config('url'));
1773         my @git_project_url_list = map { chomp; $_ } <$fd>;
1774         close $fd;
1775
1776         return wantarray ? @git_project_url_list : \@git_project_url_list;
1777 }
1778
1779 sub git_get_projects_list {
1780         my ($filter) = @_;
1781         my @list;
1782
1783         $filter ||= '';
1784         $filter =~ s/\.git$//;
1785
1786         my ($check_forks) = gitweb_check_feature('forks');
1787
1788         if (-d $projects_list) {
1789                 # search in directory
1790                 my $dir = $projects_list . ($filter ? "/$filter" : '');
1791                 # remove the trailing "/"
1792                 $dir =~ s!/+$!!;
1793                 my $pfxlen = length("$dir");
1794                 my $pfxdepth = ($dir =~ tr!/!!);
1795
1796                 File::Find::find({
1797                         follow_fast => 1, # follow symbolic links
1798                         follow_skip => 2, # ignore duplicates
1799                         dangling_symlinks => 0, # ignore dangling symlinks, silently
1800                         wanted => sub {
1801                                 # skip project-list toplevel, if we get it.
1802                                 return if (m!^[/.]$!);
1803                                 # only directories can be git repositories
1804                                 return unless (-d $_);
1805                                 # don't traverse too deep (Find is super slow on os x)
1806                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1807                                         $File::Find::prune = 1;
1808                                         return;
1809                                 }
1810
1811                                 my $subdir = substr($File::Find::name, $pfxlen + 1);
1812                                 # we check related file in $projectroot
1813                                 if ($check_forks and $subdir =~ m#/.#) {
1814                                         $File::Find::prune = 1;
1815                                 } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
1816                                         push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1817                                         $File::Find::prune = 1;
1818                                 }
1819                         },
1820                 }, "$dir");
1821
1822         } elsif (-f $projects_list) {
1823                 # read from file(url-encoded):
1824                 # 'git%2Fgit.git Linus+Torvalds'
1825                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1826                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1827                 my %paths;
1828                 open my ($fd), $projects_list or return;
1829         PROJECT:
1830                 while (my $line = <$fd>) {
1831                         chomp $line;
1832                         my ($path, $owner) = split ' ', $line;
1833                         $path = unescape($path);
1834                         $owner = unescape($owner);
1835                         if (!defined $path) {
1836                                 next;
1837                         }
1838                         if ($filter ne '') {
1839                                 # looking for forks;
1840                                 my $pfx = substr($path, 0, length($filter));
1841                                 if ($pfx ne $filter) {
1842                                         next PROJECT;
1843                                 }
1844                                 my $sfx = substr($path, length($filter));
1845                                 if ($sfx !~ /^\/.*\.git$/) {
1846                                         next PROJECT;
1847                                 }
1848                         } elsif ($check_forks) {
1849                         PATH:
1850                                 foreach my $filter (keys %paths) {
1851                                         # looking for forks;
1852                                         my $pfx = substr($path, 0, length($filter));
1853                                         if ($pfx ne $filter) {
1854                                                 next PATH;
1855                                         }
1856                                         my $sfx = substr($path, length($filter));
1857                                         if ($sfx !~ /^\/.*\.git$/) {
1858                                                 next PATH;
1859                                         }
1860                                         # is a fork, don't include it in
1861                                         # the list
1862                                         next PROJECT;
1863                                 }
1864                         }
1865                         if (check_export_ok("$projectroot/$path")) {
1866                                 my $pr = {
1867                                         path => $path,
1868                                         owner => to_utf8($owner),
1869                                 };
1870                                 push @list, $pr;
1871                                 (my $forks_path = $path) =~ s/\.git$//;
1872                                 $paths{$forks_path}++;
1873                         }
1874                 }
1875                 close $fd;
1876         }
1877         return @list;
1878 }
1879
1880 our $gitweb_project_owner = undef;
1881 sub git_get_project_list_from_file {
1882
1883         return if (defined $gitweb_project_owner);
1884
1885         $gitweb_project_owner = {};
1886         # read from file (url-encoded):
1887         # 'git%2Fgit.git Linus+Torvalds'
1888         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1889         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1890         if (-f $projects_list) {
1891                 open (my $fd , $projects_list);
1892                 while (my $line = <$fd>) {
1893                         chomp $line;
1894                         my ($pr, $ow) = split ' ', $line;
1895                         $pr = unescape($pr);
1896                         $ow = unescape($ow);
1897                         $gitweb_project_owner->{$pr} = to_utf8($ow);
1898                 }
1899                 close $fd;
1900         }
1901 }
1902
1903 sub git_get_project_owner {
1904         my $project = shift;
1905         my $owner;
1906
1907         return undef unless $project;
1908         $git_dir = "$projectroot/$project";
1909
1910         if (!defined $gitweb_project_owner) {
1911                 git_get_project_list_from_file();
1912         }
1913
1914         if (exists $gitweb_project_owner->{$project}) {
1915                 $owner = $gitweb_project_owner->{$project};
1916         }
1917         if (!defined $owner){
1918                 $owner = git_get_project_config('owner');
1919         }
1920         if (!defined $owner) {
1921                 $owner = get_file_owner("$git_dir");
1922         }
1923
1924         return $owner;
1925 }
1926
1927 sub git_get_last_activity {
1928         my ($path) = @_;
1929         my $fd;
1930
1931         $git_dir = "$projectroot/$path";
1932         open($fd, "-|", git_cmd(), 'for-each-ref',
1933              '--format=%(committer)',
1934              '--sort=-committerdate',
1935              '--count=1',
1936              'refs/heads') or return;
1937         my $most_recent = <$fd>;
1938         close $fd or return;
1939         if (defined $most_recent &&
1940             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1941                 my $timestamp = $1;
1942                 my $age = time - $timestamp;
1943                 return ($age, age_string($age));
1944         }
1945         return (undef, undef);
1946 }
1947
1948 sub git_get_references {
1949         my $type = shift || "";
1950         my %refs;
1951         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1952         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1953         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
1954                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
1955                 or return;
1956
1957         while (my $line = <$fd>) {
1958                 chomp $line;
1959                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
1960                         if (defined $refs{$1}) {
1961                                 push @{$refs{$1}}, $2;
1962                         } else {
1963                                 $refs{$1} = [ $2 ];
1964                         }
1965                 }
1966         }
1967         close $fd or return;
1968         return \%refs;
1969 }
1970
1971 sub git_get_rev_name_tags {
1972         my $hash = shift || return undef;
1973
1974         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
1975                 or return;
1976         my $name_rev = <$fd>;
1977         close $fd;
1978
1979         if ($name_rev =~ m|^$hash tags/(.*)$|) {
1980                 return $1;
1981         } else {
1982                 # catches also '$hash undefined' output
1983                 return undef;
1984         }
1985 }
1986
1987 ## ----------------------------------------------------------------------
1988 ## parse to hash functions
1989
1990 sub parse_date {
1991         my $epoch = shift;
1992         my $tz = shift || "-0000";
1993
1994         my %date;
1995         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
1996         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
1997         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
1998         $date{'hour'} = $hour;
1999         $date{'minute'} = $min;
2000         $date{'mday'} = $mday;
2001         $date{'day'} = $days[$wday];
2002         $date{'month'} = $months[$mon];
2003         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2004                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2005         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2006                              $mday, $months[$mon], $hour ,$min;
2007         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2008                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2009
2010         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2011         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2012         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2013         $date{'hour_local'} = $hour;
2014         $date{'minute_local'} = $min;
2015         $date{'tz_local'} = $tz;
2016         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2017                                   1900+$year, $mon+1, $mday,
2018                                   $hour, $min, $sec, $tz);
2019         return %date;
2020 }
2021
2022 sub parse_tag {
2023         my $tag_id = shift;
2024         my %tag;
2025         my @comment;
2026
2027         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2028         $tag{'id'} = $tag_id;
2029         while (my $line = <$fd>) {
2030                 chomp $line;
2031                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2032                         $tag{'object'} = $1;
2033                 } elsif ($line =~ m/^type (.+)$/) {
2034                         $tag{'type'} = $1;
2035                 } elsif ($line =~ m/^tag (.+)$/) {
2036                         $tag{'name'} = $1;
2037                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2038                         $tag{'author'} = $1;
2039                         $tag{'epoch'} = $2;
2040                         $tag{'tz'} = $3;
2041                 } elsif ($line =~ m/--BEGIN/) {
2042                         push @comment, $line;
2043                         last;
2044                 } elsif ($line eq "") {
2045                         last;
2046                 }
2047         }
2048         push @comment, <$fd>;
2049         $tag{'comment'} = \@comment;
2050         close $fd or return;
2051         if (!defined $tag{'name'}) {
2052                 return
2053         };
2054         return %tag
2055 }
2056
2057 sub parse_commit_text {
2058         my ($commit_text, $withparents) = @_;
2059         my @commit_lines = split '\n', $commit_text;
2060         my %co;
2061
2062         pop @commit_lines; # Remove '\0'
2063
2064         if (! @commit_lines) {
2065                 return;
2066         }
2067
2068         my $header = shift @commit_lines;
2069         if ($header !~ m/^[0-9a-fA-F]{40}/) {
2070                 return;
2071         }
2072         ($co{'id'}, my @parents) = split ' ', $header;
2073         while (my $line = shift @commit_lines) {
2074                 last if $line eq "\n";
2075                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2076                         $co{'tree'} = $1;
2077                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2078                         push @parents, $1;
2079                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2080                         $co{'author'} = $1;
2081                         $co{'author_epoch'} = $2;
2082                         $co{'author_tz'} = $3;
2083                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2084                                 $co{'author_name'}  = $1;
2085                                 $co{'author_email'} = $2;
2086                         } else {
2087                                 $co{'author_name'} = $co{'author'};
2088                         }
2089                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2090                         $co{'committer'} = $1;
2091                         $co{'committer_epoch'} = $2;
2092                         $co{'committer_tz'} = $3;
2093                         $co{'committer_name'} = $co{'committer'};
2094                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2095                                 $co{'committer_name'}  = $1;
2096                                 $co{'committer_email'} = $2;
2097                         } else {
2098                                 $co{'committer_name'} = $co{'committer'};
2099                         }
2100                 }
2101         }
2102         if (!defined $co{'tree'}) {
2103                 return;
2104         };
2105         $co{'parents'} = \@parents;
2106         $co{'parent'} = $parents[0];
2107
2108         foreach my $title (@commit_lines) {
2109                 $title =~ s/^    //;
2110                 if ($title ne "") {
2111                         $co{'title'} = chop_str($title, 80, 5);
2112                         # remove leading stuff of merges to make the interesting part visible
2113                         if (length($title) > 50) {
2114                                 $title =~ s/^Automatic //;
2115                                 $title =~ s/^merge (of|with) /Merge ... /i;
2116                                 if (length($title) > 50) {
2117                                         $title =~ s/(http|rsync):\/\///;
2118                                 }
2119                                 if (length($title) > 50) {
2120                                         $title =~ s/(master|www|rsync)\.//;
2121                                 }
2122                                 if (length($title) > 50) {
2123                                         $title =~ s/kernel.org:?//;
2124                                 }
2125                                 if (length($title) > 50) {
2126                                         $title =~ s/\/pub\/scm//;
2127                                 }
2128                         }
2129                         $co{'title_short'} = chop_str($title, 50, 5);
2130                         last;
2131                 }
2132         }
2133         if (! defined $co{'title'} || $co{'title'} eq "") {
2134                 $co{'title'} = $co{'title_short'} = '(no commit message)';
2135         }
2136         # remove added spaces
2137         foreach my $line (@commit_lines) {
2138                 $line =~ s/^    //;
2139         }
2140         $co{'comment'} = \@commit_lines;
2141
2142         my $age = time - $co{'committer_epoch'};
2143         $co{'age'} = $age;
2144         $co{'age_string'} = age_string($age);
2145         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2146         if ($age > 60*60*24*7*2) {
2147                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2148                 $co{'age_string_age'} = $co{'age_string'};
2149         } else {
2150                 $co{'age_string_date'} = $co{'age_string'};
2151                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2152         }
2153         return %co;
2154 }
2155
2156 sub parse_commit {
2157         my ($commit_id) = @_;
2158         my %co;
2159
2160         local $/ = "\0";
2161
2162         open my $fd, "-|", git_cmd(), "rev-list",
2163                 "--parents",
2164                 "--header",
2165                 "--max-count=1",
2166                 $commit_id,
2167                 "--",
2168                 or die_error(500, "Open git-rev-list failed");
2169         %co = parse_commit_text(<$fd>, 1);
2170         close $fd;
2171
2172         return %co;
2173 }
2174
2175 sub parse_commits {
2176         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2177         my @cos;
2178
2179         $maxcount ||= 1;
2180         $skip ||= 0;
2181
2182         local $/ = "\0";
2183
2184         open my $fd, "-|", git_cmd(), "rev-list",
2185                 "--header",
2186                 @args,
2187                 ("--max-count=" . $maxcount),
2188                 ("--skip=" . $skip),
2189                 @extra_options,
2190                 $commit_id,
2191                 "--",
2192                 ($filename ? ($filename) : ())
2193                 or die_error(500, "Open git-rev-list failed");
2194         while (my $line = <$fd>) {
2195                 my %co = parse_commit_text($line);
2196                 push @cos, \%co;
2197         }
2198         close $fd;
2199
2200         return wantarray ? @cos : \@cos;
2201 }
2202
2203 # parse line of git-diff-tree "raw" output
2204 sub parse_difftree_raw_line {
2205         my $line = shift;
2206         my %res;
2207
2208         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2209         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2210         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2211                 $res{'from_mode'} = $1;
2212                 $res{'to_mode'} = $2;
2213                 $res{'from_id'} = $3;
2214                 $res{'to_id'} = $4;
2215                 $res{'status'} = $5;
2216                 $res{'similarity'} = $6;
2217                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2218                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2219                 } else {
2220                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2221                 }
2222         }
2223         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2224         # combined diff (for merge commit)
2225         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2226                 $res{'nparents'}  = length($1);
2227                 $res{'from_mode'} = [ split(' ', $2) ];
2228                 $res{'to_mode'} = pop @{$res{'from_mode'}};
2229                 $res{'from_id'} = [ split(' ', $3) ];
2230                 $res{'to_id'} = pop @{$res{'from_id'}};
2231                 $res{'status'} = [ split('', $4) ];
2232                 $res{'to_file'} = unquote($5);
2233         }
2234         # 'c512b523472485aef4fff9e57b229d9d243c967f'
2235         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2236                 $res{'commit'} = $1;
2237         }
2238
2239         return wantarray ? %res : \%res;
2240 }
2241
2242 # wrapper: return parsed line of git-diff-tree "raw" output
2243 # (the argument might be raw line, or parsed info)
2244 sub parsed_difftree_line {
2245         my $line_or_ref = shift;
2246
2247         if (ref($line_or_ref) eq "HASH") {
2248                 # pre-parsed (or generated by hand)
2249                 return $line_or_ref;
2250         } else {
2251                 return parse_difftree_raw_line($line_or_ref);
2252         }
2253 }
2254
2255 # parse line of git-ls-tree output
2256 sub parse_ls_tree_line ($;%) {
2257         my $line = shift;
2258         my %opts = @_;
2259         my %res;
2260
2261         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2262         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2263
2264         $res{'mode'} = $1;
2265         $res{'type'} = $2;
2266         $res{'hash'} = $3;
2267         if ($opts{'-z'}) {
2268                 $res{'name'} = $4;
2269         } else {
2270                 $res{'name'} = unquote($4);
2271         }
2272
2273         return wantarray ? %res : \%res;
2274 }
2275
2276 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2277 sub parse_from_to_diffinfo {
2278         my ($diffinfo, $from, $to, @parents) = @_;
2279
2280         if ($diffinfo->{'nparents'}) {
2281                 # combined diff
2282                 $from->{'file'} = [];
2283                 $from->{'href'} = [];
2284                 fill_from_file_info($diffinfo, @parents)
2285                         unless exists $diffinfo->{'from_file'};
2286                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2287                         $from->{'file'}[$i] =
2288                                 defined $diffinfo->{'from_file'}[$i] ?
2289                                         $diffinfo->{'from_file'}[$i] :
2290                                         $diffinfo->{'to_file'};
2291                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2292                                 $from->{'href'}[$i] = href(action=>"blob",
2293                                                            hash_base=>$parents[$i],
2294                                                            hash=>$diffinfo->{'from_id'}[$i],
2295                                                            file_name=>$from->{'file'}[$i]);
2296                         } else {
2297                                 $from->{'href'}[$i] = undef;
2298                         }
2299                 }
2300         } else {
2301                 # ordinary (not combined) diff
2302                 $from->{'file'} = $diffinfo->{'from_file'};
2303                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2304                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2305                                                hash=>$diffinfo->{'from_id'},
2306                                                file_name=>$from->{'file'});
2307                 } else {
2308                         delete $from->{'href'};
2309                 }
2310         }
2311
2312         $to->{'file'} = $diffinfo->{'to_file'};
2313         if (!is_deleted($diffinfo)) { # file exists in result
2314                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2315                                      hash=>$diffinfo->{'to_id'},
2316                                      file_name=>$to->{'file'});
2317         } else {
2318                 delete $to->{'href'};
2319         }
2320 }
2321
2322 ## ......................................................................
2323 ## parse to array of hashes functions
2324
2325 sub git_get_heads_list {
2326         my $limit = shift;
2327         my @headslist;
2328
2329         open my $fd, '-|', git_cmd(), 'for-each-ref',
2330                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2331                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2332                 'refs/heads'
2333                 or return;
2334         while (my $line = <$fd>) {
2335                 my %ref_item;
2336
2337                 chomp $line;
2338                 my ($refinfo, $committerinfo) = split(/\0/, $line);
2339                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2340                 my ($committer, $epoch, $tz) =
2341                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2342                 $ref_item{'fullname'}  = $name;
2343                 $name =~ s!^refs/heads/!!;
2344
2345                 $ref_item{'name'}  = $name;
2346                 $ref_item{'id'}    = $hash;
2347                 $ref_item{'title'} = $title || '(no commit message)';
2348                 $ref_item{'epoch'} = $epoch;
2349                 if ($epoch) {
2350                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2351                 } else {
2352                         $ref_item{'age'} = "unknown";
2353                 }
2354
2355                 push @headslist, \%ref_item;
2356         }
2357         close $fd;
2358
2359         return wantarray ? @headslist : \@headslist;
2360 }
2361
2362 sub git_get_tags_list {
2363         my $limit = shift;
2364         my @tagslist;
2365
2366         open my $fd, '-|', git_cmd(), 'for-each-ref',
2367                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2368                 '--format=%(objectname) %(objecttype) %(refname) '.
2369                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2370                 'refs/tags'
2371                 or return;
2372         while (my $line = <$fd>) {
2373                 my %ref_item;
2374
2375                 chomp $line;
2376                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2377                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2378                 my ($creator, $epoch, $tz) =
2379                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2380                 $ref_item{'fullname'} = $name;
2381                 $name =~ s!^refs/tags/!!;
2382
2383                 $ref_item{'type'} = $type;
2384                 $ref_item{'id'} = $id;
2385                 $ref_item{'name'} = $name;
2386                 if ($type eq "tag") {
2387                         $ref_item{'subject'} = $title;
2388                         $ref_item{'reftype'} = $reftype;
2389                         $ref_item{'refid'}   = $refid;
2390                 } else {
2391                         $ref_item{'reftype'} = $type;
2392                         $ref_item{'refid'}   = $id;
2393                 }
2394
2395                 if ($type eq "tag" || $type eq "commit") {
2396                         $ref_item{'epoch'} = $epoch;
2397                         if ($epoch) {
2398                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2399                         } else {
2400                                 $ref_item{'age'} = "unknown";
2401                         }
2402                 }
2403
2404                 push @tagslist, \%ref_item;
2405         }
2406         close $fd;
2407
2408         return wantarray ? @tagslist : \@tagslist;
2409 }
2410
2411 ## ----------------------------------------------------------------------
2412 ## filesystem-related functions
2413
2414 sub get_file_owner {
2415         my $path = shift;
2416
2417         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2418         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2419         if (!defined $gcos) {
2420                 return undef;
2421         }
2422         my $owner = $gcos;
2423         $owner =~ s/[,;].*$//;
2424         return to_utf8($owner);
2425 }
2426
2427 ## ......................................................................
2428 ## mimetype related functions
2429
2430 sub mimetype_guess_file {
2431         my $filename = shift;
2432         my $mimemap = shift;
2433         -r $mimemap or return undef;
2434
2435         my %mimemap;
2436         open(MIME, $mimemap) or return undef;
2437         while (<MIME>) {
2438                 next if m/^#/; # skip comments
2439                 my ($mime, $exts) = split(/\t+/);
2440                 if (defined $exts) {
2441                         my @exts = split(/\s+/, $exts);
2442                         foreach my $ext (@exts) {
2443                                 $mimemap{$ext} = $mime;
2444                         }
2445                 }
2446         }
2447         close(MIME);
2448
2449         $filename =~ /\.([^.]*)$/;
2450         return $mimemap{$1};
2451 }
2452
2453 sub mimetype_guess {
2454         my $filename = shift;
2455         my $mime;
2456         $filename =~ /\./ or return undef;
2457
2458         if ($mimetypes_file) {
2459                 my $file = $mimetypes_file;
2460                 if ($file !~ m!^/!) { # if it is relative path
2461                         # it is relative to project
2462                         $file = "$projectroot/$project/$file";
2463                 }
2464                 $mime = mimetype_guess_file($filename, $file);
2465         }
2466         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2467         return $mime;
2468 }
2469
2470 sub blob_mimetype {
2471         my $fd = shift;
2472         my $filename = shift;
2473
2474         if ($filename) {
2475                 my $mime = mimetype_guess($filename);
2476                 $mime and return $mime;
2477         }
2478
2479         # just in case
2480         return $default_blob_plain_mimetype unless $fd;
2481
2482         if (-T $fd) {
2483                 return 'text/plain';
2484         } elsif (! $filename) {
2485                 return 'application/octet-stream';
2486         } elsif ($filename =~ m/\.png$/i) {
2487                 return 'image/png';
2488         } elsif ($filename =~ m/\.gif$/i) {
2489                 return 'image/gif';
2490         } elsif ($filename =~ m/\.jpe?g$/i) {
2491                 return 'image/jpeg';
2492         } else {
2493                 return 'application/octet-stream';
2494         }
2495 }
2496
2497 sub blob_contenttype {
2498         my ($fd, $file_name, $type) = @_;
2499
2500         $type ||= blob_mimetype($fd, $file_name);
2501         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2502                 $type .= "; charset=$default_text_plain_charset";
2503         }
2504
2505         return $type;
2506 }
2507
2508 ## ======================================================================
2509 ## functions printing HTML: header, footer, error page
2510
2511 sub git_header_html {
2512         my $status = shift || "200 OK";
2513         my $expires = shift;
2514
2515         my $title = "$site_name";
2516         if (defined $project) {
2517                 $title .= " - " . to_utf8($project);
2518                 if (defined $action) {
2519                         $title .= "/$action";
2520                         if (defined $file_name) {
2521                                 $title .= " - " . esc_path($file_name);
2522                                 if ($action eq "tree" && $file_name !~ m|/$|) {
2523                                         $title .= "/";
2524                                 }
2525                         }
2526                 }
2527         }
2528         my $content_type;
2529         # require explicit support from the UA if we are to send the page as
2530         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2531         # we have to do this because MSIE sometimes globs '*/*', pretending to
2532         # support xhtml+xml but choking when it gets what it asked for.
2533         if (defined $cgi->http('HTTP_ACCEPT') &&
2534             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2535             $cgi->Accept('application/xhtml+xml') != 0) {
2536                 $content_type = 'application/xhtml+xml';
2537         } else {
2538                 $content_type = 'text/html';
2539         }
2540         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2541                            -status=> $status, -expires => $expires);
2542         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2543         print <<EOF;
2544 <?xml version="1.0" encoding="utf-8"?>
2545 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2546 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2547 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2548 <!-- git core binaries version $git_version -->
2549 <head>
2550 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2551 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2552 <meta name="robots" content="index, nofollow"/>
2553 <title>$title</title>
2554 EOF
2555 # print out each stylesheet that exist
2556         if (defined $stylesheet) {
2557 #provides backwards capability for those people who define style sheet in a config file
2558                 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2559         } else {
2560                 foreach my $stylesheet (@stylesheets) {
2561                         next unless $stylesheet;
2562                         print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2563                 }
2564         }
2565         if (defined $project) {
2566                 my %href_params = get_feed_info();
2567                 if (!exists $href_params{'-title'}) {
2568                         $href_params{'-title'} = 'log';
2569                 }
2570
2571                 foreach my $format qw(RSS Atom) {
2572                         my $type = lc($format);
2573                         my %link_attr = (
2574                                 '-rel' => 'alternate',
2575                                 '-title' => "$project - $href_params{'-title'} - $format feed",
2576                                 '-type' => "application/$type+xml"
2577                         );
2578
2579                         $href_params{'action'} = $type;
2580                         $link_attr{'-href'} = href(%href_params);
2581                         print "<link ".
2582                               "rel=\"$link_attr{'-rel'}\" ".
2583                               "title=\"$link_attr{'-title'}\" ".
2584                               "href=\"$link_attr{'-href'}\" ".
2585                               "type=\"$link_attr{'-type'}\" ".
2586                               "/>\n";
2587
2588                         $href_params{'extra_options'} = '--no-merges';
2589                         $link_attr{'-href'} = href(%href_params);
2590                         $link_attr{'-title'} .= ' (no merges)';
2591                         print "<link ".
2592                               "rel=\"$link_attr{'-rel'}\" ".
2593                               "title=\"$link_attr{'-title'}\" ".
2594                               "href=\"$link_attr{'-href'}\" ".
2595                               "type=\"$link_attr{'-type'}\" ".
2596                               "/>\n";
2597                 }
2598
2599         } else {
2600                 printf('<link rel="alternate" title="%s projects list" '.
2601                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
2602                        $site_name, href(project=>undef, action=>"project_index"));
2603                 printf('<link rel="alternate" title="%s projects feeds" '.
2604                        'href="%s" type="text/x-opml" />'."\n",
2605                        $site_name, href(project=>undef, action=>"opml"));
2606         }
2607         if (defined $favicon) {
2608                 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2609         }
2610
2611         print "</head>\n" .
2612               "<body>\n";
2613
2614         if (-f $site_header) {
2615                 open (my $fd, $site_header);
2616                 print <$fd>;
2617                 close $fd;
2618         }
2619
2620         print "<div class=\"page_header\">\n" .
2621               $cgi->a({-href => esc_url($logo_url),
2622                        -title => $logo_label},
2623                       qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2624         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2625         if (defined $project) {
2626                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2627                 if (defined $action) {
2628                         print " / $action";
2629                 }
2630                 print "\n";
2631         }
2632         print "</div>\n";
2633
2634         my ($have_search) = gitweb_check_feature('search');
2635         if (defined $project && $have_search) {
2636                 if (!defined $searchtext) {
2637                         $searchtext = "";
2638                 }
2639                 my $search_hash;
2640                 if (defined $hash_base) {
2641                         $search_hash = $hash_base;
2642                 } elsif (defined $hash) {
2643                         $search_hash = $hash;
2644                 } else {
2645                         $search_hash = "HEAD";
2646                 }
2647                 my $action = $my_uri;
2648                 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2649                 if ($use_pathinfo) {
2650                         $action .= "/".esc_url($project);
2651                 }
2652                 print $cgi->startform(-method => "get", -action => $action) .
2653                       "<div class=\"search\">\n" .
2654                       (!$use_pathinfo &&
2655                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2656                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2657                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2658                       $cgi->popup_menu(-name => 'st', -default => 'commit',
2659                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2660                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2661                       " search:\n",
2662                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2663                       "<span title=\"Extended regular expression\">" .
2664                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2665                                      -checked => $search_use_regexp) .
2666                       "</span>" .
2667                       "</div>" .
2668                       $cgi->end_form() . "\n";
2669         }
2670 }
2671
2672 sub git_footer_html {
2673         my $feed_class = 'rss_logo';
2674
2675         print "<div class=\"page_footer\">\n";
2676         if (defined $project) {
2677                 my $descr = git_get_project_description($project);
2678                 if (defined $descr) {
2679                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2680                 }
2681
2682                 my %href_params = get_feed_info();
2683                 if (!%href_params) {
2684                         $feed_class .= ' generic';
2685                 }
2686                 $href_params{'-title'} ||= 'log';
2687
2688                 foreach my $format qw(RSS Atom) {
2689                         $href_params{'action'} = lc($format);
2690                         print $cgi->a({-href => href(%href_params),
2691                                       -title => "$href_params{'-title'} $format feed",
2692                                       -class => $feed_class}, $format)."\n";
2693                 }
2694
2695         } else {
2696                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2697                               -class => $feed_class}, "OPML") . " ";
2698                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2699                               -class => $feed_class}, "TXT") . "\n";
2700         }
2701         print "</div>\n"; # class="page_footer"
2702
2703         if (-f $site_footer) {
2704                 open (my $fd, $site_footer);
2705                 print <$fd>;
2706                 close $fd;
2707         }
2708
2709         print "</body>\n" .
2710               "</html>";
2711 }
2712
2713 # die_error(<http_status_code>, <error_message>)
2714 # Example: die_error(404, 'Hash not found')
2715 # By convention, use the following status codes (as defined in RFC 2616):
2716 # 400: Invalid or missing CGI parameters, or
2717 #      requested object exists but has wrong type.
2718 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2719 #      this server or project.
2720 # 404: Requested object/revision/project doesn't exist.
2721 # 500: The server isn't configured properly, or
2722 #      an internal error occurred (e.g. failed assertions caused by bugs), or
2723 #      an unknown error occurred (e.g. the git binary died unexpectedly).
2724 sub die_error {
2725         my $status = shift || 500;
2726         my $error = shift || "Internal server error";
2727
2728         my %http_responses = (400 => '400 Bad Request',
2729                               403 => '403 Forbidden',
2730                               404 => '404 Not Found',
2731                               500 => '500 Internal Server Error');
2732         git_header_html($http_responses{$status});
2733         print <<EOF;
2734 <div class="page_body">
2735 <br /><br />
2736 $status - $error
2737 <br />
2738 </div>
2739 EOF
2740         git_footer_html();
2741         exit;
2742 }
2743
2744 ## ----------------------------------------------------------------------
2745 ## functions printing or outputting HTML: navigation
2746
2747 sub git_print_page_nav {
2748         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2749         $extra = '' if !defined $extra; # pager or formats
2750
2751         my @navs = qw(summary shortlog log commit commitdiff tree);
2752         if ($suppress) {
2753                 @navs = grep { $_ ne $suppress } @navs;
2754         }
2755
2756         my %arg = map { $_ => {action=>$_} } @navs;
2757         if (defined $head) {
2758                 for (qw(commit commitdiff)) {
2759                         $arg{$_}{'hash'} = $head;
2760                 }
2761                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2762                         for (qw(shortlog log)) {
2763                                 $arg{$_}{'hash'} = $head;
2764                         }
2765                 }
2766         }
2767         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2768         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2769
2770         print "<div class=\"page_nav\">\n" .
2771                 (join " | ",
2772                  map { $_ eq $current ?
2773                        $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2774                  } @navs);
2775         print "<br/>\n$extra<br/>\n" .
2776               "</div>\n";
2777 }
2778
2779 sub format_paging_nav {
2780         my ($action, $hash, $head, $page, $has_next_link) = @_;
2781         my $paging_nav;
2782
2783
2784         if ($hash ne $head || $page) {
2785                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2786         } else {
2787                 $paging_nav .= "HEAD";
2788         }
2789
2790         if ($page > 0) {
2791                 $paging_nav .= " &sdot; " .
2792                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
2793                                  -accesskey => "p", -title => "Alt-p"}, "prev");
2794         } else {
2795                 $paging_nav .= " &sdot; prev";
2796         }
2797
2798         if ($has_next_link) {
2799                 $paging_nav .= " &sdot; " .
2800                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
2801                                  -accesskey => "n", -title => "Alt-n"}, "next");
2802         } else {
2803                 $paging_nav .= " &sdot; next";
2804         }
2805
2806         return $paging_nav;
2807 }
2808
2809 ## ......................................................................
2810 ## functions printing or outputting HTML: div
2811
2812 sub git_print_header_div {
2813         my ($action, $title, $hash, $hash_base) = @_;
2814         my %args = ();
2815
2816         $args{'action'} = $action;
2817         $args{'hash'} = $hash if $hash;
2818         $args{'hash_base'} = $hash_base if $hash_base;
2819
2820         print "<div class=\"header\">\n" .
2821               $cgi->a({-href => href(%args), -class => "title"},
2822               $title ? $title : $action) .
2823               "\n</div>\n";
2824 }
2825
2826 #sub git_print_authorship (\%) {
2827 sub git_print_authorship {
2828         my $co = shift;
2829
2830         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2831         print "<div class=\"author_date\">" .
2832               esc_html($co->{'author_name'}) .
2833               " [$ad{'rfc2822'}";
2834         if ($ad{'hour_local'} < 6) {
2835                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2836                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2837         } else {
2838                 printf(" (%02d:%02d %s)",
2839                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2840         }
2841         print "]</div>\n";
2842 }
2843
2844 sub git_print_page_path {
2845         my $name = shift;
2846         my $type = shift;
2847         my $hb = shift;
2848
2849
2850         print "<div class=\"page_path\">";
2851         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2852                       -title => 'tree root'}, to_utf8("[$project]"));
2853         print " / ";
2854         if (defined $name) {
2855                 my @dirname = split '/', $name;
2856                 my $basename = pop @dirname;
2857                 my $fullname = '';
2858
2859                 foreach my $dir (@dirname) {
2860                         $fullname .= ($fullname ? '/' : '') . $dir;
2861                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2862                                                      hash_base=>$hb),
2863                                       -title => $fullname}, esc_path($dir));
2864                         print " / ";
2865                 }
2866                 if (defined $type && $type eq 'blob') {
2867                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2868                                                      hash_base=>$hb),
2869                                       -title => $name}, esc_path($basename));
2870                 } elsif (defined $type && $type eq 'tree') {
2871                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2872                                                      hash_base=>$hb),
2873                                       -title => $name}, esc_path($basename));
2874                         print " / ";
2875                 } else {
2876                         print esc_path($basename);
2877                 }
2878         }
2879         print "<br/></div>\n";
2880 }
2881
2882 # sub git_print_log (\@;%) {
2883 sub git_print_log ($;%) {
2884         my $log = shift;
2885         my %opts = @_;
2886
2887         if ($opts{'-remove_title'}) {
2888                 # remove title, i.e. first line of log
2889                 shift @$log;
2890         }
2891         # remove leading empty lines
2892         while (defined $log->[0] && $log->[0] eq "") {
2893                 shift @$log;
2894         }
2895
2896         # print log
2897         my $signoff = 0;
2898         my $empty = 0;
2899         foreach my $line (@$log) {
2900                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2901                         $signoff = 1;
2902                         $empty = 0;
2903                         if (! $opts{'-remove_signoff'}) {
2904                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2905                                 next;
2906                         } else {
2907                                 # remove signoff lines
2908                                 next;
2909                         }
2910                 } else {
2911                         $signoff = 0;
2912                 }
2913
2914                 # print only one empty line
2915                 # do not print empty line after signoff
2916                 if ($line eq "") {
2917                         next if ($empty || $signoff);
2918                         $empty = 1;
2919                 } else {
2920                         $empty = 0;
2921                 }
2922
2923                 print format_log_line_html($line) . "<br/>\n";
2924         }
2925
2926         if ($opts{'-final_empty_line'}) {
2927                 # end with single empty line
2928                 print "<br/>\n" unless $empty;
2929         }
2930 }
2931
2932 # return link target (what link points to)
2933 sub git_get_link_target {
2934         my $hash = shift;
2935         my $link_target;
2936
2937         # read link
2938         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2939                 or return;
2940         {
2941                 local $/;
2942                 $link_target = <$fd>;
2943         }
2944         close $fd
2945                 or return;
2946
2947         return $link_target;
2948 }
2949
2950 # given link target, and the directory (basedir) the link is in,
2951 # return target of link relative to top directory (top tree);
2952 # return undef if it is not possible (including absolute links).
2953 sub normalize_link_target {
2954         my ($link_target, $basedir, $hash_base) = @_;
2955
2956         # we can normalize symlink target only if $hash_base is provided
2957         return unless $hash_base;
2958
2959         # absolute symlinks (beginning with '/') cannot be normalized
2960         return if (substr($link_target, 0, 1) eq '/');
2961
2962         # normalize link target to path from top (root) tree (dir)
2963         my $path;
2964         if ($basedir) {
2965                 $path = $basedir . '/' . $link_target;
2966         } else {
2967                 # we are in top (root) tree (dir)
2968                 $path = $link_target;
2969         }
2970
2971         # remove //, /./, and /../
2972         my @path_parts;
2973         foreach my $part (split('/', $path)) {
2974                 # discard '.' and ''
2975                 next if (!$part || $part eq '.');
2976                 # handle '..'
2977                 if ($part eq '..') {
2978                         if (@path_parts) {
2979                                 pop @path_parts;
2980                         } else {
2981                                 # link leads outside repository (outside top dir)
2982                                 return;
2983                         }
2984                 } else {
2985                         push @path_parts, $part;
2986                 }
2987         }
2988         $path = join('/', @path_parts);
2989
2990         return $path;
2991 }
2992
2993 # print tree entry (row of git_tree), but without encompassing <tr> element
2994 sub git_print_tree_entry {
2995         my ($t, $basedir, $hash_base, $have_blame) = @_;
2996
2997         my %base_key = ();
2998         $base_key{'hash_base'} = $hash_base if defined $hash_base;
2999
3000         # The format of a table row is: mode list link.  Where mode is
3001         # the mode of the entry, list is the name of the entry, an href,
3002         # and link is the action links of the entry.
3003
3004         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3005         if ($t->{'type'} eq "blob") {
3006                 print "<td class=\"list\">" .
3007                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3008                                                file_name=>"$basedir$t->{'name'}", %base_key),
3009                                 -class => "list"}, esc_path($t->{'name'}));
3010                 if (S_ISLNK(oct $t->{'mode'})) {
3011                         my $link_target = git_get_link_target($t->{'hash'});
3012                         if ($link_target) {
3013                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3014                                 if (defined $norm_target) {
3015                                         print " -> " .
3016                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3017                                                                      file_name=>$norm_target),
3018                                                        -title => $norm_target}, esc_path($link_target));
3019                                 } else {
3020                                         print " -> " . esc_path($link_target);
3021                                 }
3022                         }
3023                 }
3024                 print "</td>\n";
3025                 print "<td class=\"link\">";
3026                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3027                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3028                               "blob");
3029                 if ($have_blame) {
3030                         print " | " .
3031                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3032                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3033                                       "blame");
3034                 }
3035                 if (defined $hash_base) {
3036                         print " | " .
3037                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3038                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3039                                       "history");
3040                 }
3041                 print " | " .
3042                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3043                                                file_name=>"$basedir$t->{'name'}")},
3044                                 "raw");
3045                 print "</td>\n";
3046
3047         } elsif ($t->{'type'} eq "tree") {
3048                 print "<td class=\"list\">";
3049                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3050                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3051                               esc_path($t->{'name'}));
3052                 print "</td>\n";
3053                 print "<td class=\"link\">";
3054                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3055                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3056                               "tree");
3057                 if (defined $hash_base) {
3058                         print " | " .
3059                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3060                                                      file_name=>"$basedir$t->{'name'}")},
3061                                       "history");
3062                 }
3063                 print "</td>\n";
3064         } else {
3065                 # unknown object: we can only present history for it
3066                 # (this includes 'commit' object, i.e. submodule support)
3067                 print "<td class=\"list\">" .
3068                       esc_path($t->{'name'}) .
3069                       "</td>\n";
3070                 print "<td class=\"link\">";
3071                 if (defined $hash_base) {
3072                         print $cgi->a({-href => href(action=>"history",
3073                                                      hash_base=>$hash_base,
3074                                                      file_name=>"$basedir$t->{'name'}")},
3075                                       "history");
3076                 }
3077                 print "</td>\n";
3078         }
3079 }
3080
3081 ## ......................................................................
3082 ## functions printing large fragments of HTML
3083
3084 # get pre-image filenames for merge (combined) diff
3085 sub fill_from_file_info {
3086         my ($diff, @parents) = @_;
3087
3088         $diff->{'from_file'} = [ ];
3089         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3090         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3091                 if ($diff->{'status'}[$i] eq 'R' ||
3092                     $diff->{'status'}[$i] eq 'C') {
3093                         $diff->{'from_file'}[$i] =
3094                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3095                 }
3096         }
3097
3098         return $diff;
3099 }
3100
3101 # is current raw difftree line of file deletion
3102 sub is_deleted {
3103         my $diffinfo = shift;
3104
3105         return $diffinfo->{'to_id'} eq ('0' x 40);
3106 }
3107
3108 # does patch correspond to [previous] difftree raw line
3109 # $diffinfo  - hashref of parsed raw diff format
3110 # $patchinfo - hashref of parsed patch diff format
3111 #              (the same keys as in $diffinfo)
3112 sub is_patch_split {
3113         my ($diffinfo, $patchinfo) = @_;
3114
3115         return defined $diffinfo && defined $patchinfo
3116                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3117 }
3118
3119
3120 sub git_difftree_body {
3121         my ($difftree, $hash, @parents) = @_;
3122         my ($parent) = $parents[0];
3123         my ($have_blame) = gitweb_check_feature('blame');
3124         print "<div class=\"list_head\">\n";
3125         if ($#{$difftree} > 10) {
3126                 print(($#{$difftree} + 1) . " files changed:\n");
3127         }
3128         print "</div>\n";
3129
3130         print "<table class=\"" .
3131               (@parents > 1 ? "combined " : "") .
3132               "diff_tree\">\n";
3133
3134         # header only for combined diff in 'commitdiff' view
3135         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3136         if ($has_header) {
3137                 # table header
3138                 print "<thead><tr>\n" .
3139                        "<th></th><th></th>\n"; # filename, patchN link
3140                 for (my $i = 0; $i < @parents; $i++) {
3141                         my $par = $parents[$i];
3142                         print "<th>" .
3143                               $cgi->a({-href => href(action=>"commitdiff",
3144                                                      hash=>$hash, hash_parent=>$par),
3145                                        -title => 'commitdiff to parent number ' .
3146                                                   ($i+1) . ': ' . substr($par,0,7)},
3147                                       $i+1) .
3148                               "&nbsp;</th>\n";
3149                 }
3150                 print "</tr></thead>\n<tbody>\n";
3151         }
3152
3153         my $alternate = 1;
3154         my $patchno = 0;
3155         foreach my $line (@{$difftree}) {
3156                 my $diff = parsed_difftree_line($line);
3157
3158                 if ($alternate) {
3159                         print "<tr class=\"dark\">\n";
3160                 } else {
3161                         print "<tr class=\"light\">\n";
3162                 }
3163                 $alternate ^= 1;
3164
3165                 if (exists $diff->{'nparents'}) { # combined diff
3166
3167                         fill_from_file_info($diff, @parents)
3168                                 unless exists $diff->{'from_file'};
3169
3170                         if (!is_deleted($diff)) {
3171                                 # file exists in the result (child) commit
3172                                 print "<td>" .
3173                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3174                                                              file_name=>$diff->{'to_file'},
3175                                                              hash_base=>$hash),
3176                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3177                                       "</td>\n";
3178                         } else {
3179                                 print "<td>" .
3180                                       esc_path($diff->{'to_file'}) .
3181                                       "</td>\n";
3182                         }
3183
3184                         if ($action eq 'commitdiff') {
3185                                 # link to patch
3186                                 $patchno++;
3187                                 print "<td class=\"link\">" .
3188                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3189                                       " | " .
3190                                       "</td>\n";
3191                         }
3192
3193                         my $has_history = 0;
3194                         my $not_deleted = 0;
3195                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3196                                 my $hash_parent = $parents[$i];
3197                                 my $from_hash = $diff->{'from_id'}[$i];
3198                                 my $from_path = $diff->{'from_file'}[$i];
3199                                 my $status = $diff->{'status'}[$i];
3200
3201                                 $has_history ||= ($status ne 'A');
3202                                 $not_deleted ||= ($status ne 'D');
3203
3204                                 if ($status eq 'A') {
3205                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3206                                 } elsif ($status eq 'D') {
3207                                         print "<td class=\"link\">" .
3208                                               $cgi->a({-href => href(action=>"blob",
3209                                                                      hash_base=>$hash,
3210                                                                      hash=>$from_hash,
3211                                                                      file_name=>$from_path)},
3212                                                       "blob" . ($i+1)) .
3213                                               " | </td>\n";
3214                                 } else {
3215                                         if ($diff->{'to_id'} eq $from_hash) {
3216                                                 print "<td class=\"link nochange\">";
3217                                         } else {
3218                                                 print "<td class=\"link\">";
3219                                         }
3220                                         print $cgi->a({-href => href(action=>"blobdiff",
3221                                                                      hash=>$diff->{'to_id'},
3222                                                                      hash_parent=>$from_hash,
3223                                                                      hash_base=>$hash,
3224                                                                      hash_parent_base=>$hash_parent,
3225                                                                      file_name=>$diff->{'to_file'},
3226                                                                      file_parent=>$from_path)},
3227                                                       "diff" . ($i+1)) .
3228                                               " | </td>\n";
3229                                 }
3230                         }
3231
3232                         print "<td class=\"link\">";
3233                         if ($not_deleted) {
3234                                 print $cgi->a({-href => href(action=>"blob",
3235                                                              hash=>$diff->{'to_id'},
3236                                                              file_name=>$diff->{'to_file'},
3237                                                              hash_base=>$hash)},
3238                                               "blob");
3239                                 print " | " if ($has_history);
3240                         }
3241                         if ($has_history) {
3242                                 print $cgi->a({-href => href(action=>"history",
3243                                                              file_name=>$diff->{'to_file'},
3244                                                              hash_base=>$hash)},
3245                                               "history");
3246                         }
3247                         print "</td>\n";
3248
3249                         print "</tr>\n";
3250                         next; # instead of 'else' clause, to avoid extra indent
3251                 }
3252                 # else ordinary diff
3253
3254                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3255                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3256                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3257                         $to_mode_oct = oct $diff->{'to_mode'};
3258                         if (S_ISREG($to_mode_oct)) { # only for regular file
3259                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3260                         }
3261                         $to_file_type = file_type($diff->{'to_mode'});
3262                 }
3263                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3264                         $from_mode_oct = oct $diff->{'from_mode'};
3265                         if (S_ISREG($to_mode_oct)) { # only for regular file
3266                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3267                         }
3268                         $from_file_type = file_type($diff->{'from_mode'});
3269                 }
3270
3271                 if ($diff->{'status'} eq "A") { # created
3272                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3273                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3274                         $mode_chng   .= "]</span>";
3275                         print "<td>";
3276                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3277                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3278                                       -class => "list"}, esc_path($diff->{'file'}));
3279                         print "</td>\n";
3280                         print "<td>$mode_chng</td>\n";
3281                         print "<td class=\"link\">";
3282                         if ($action eq 'commitdiff') {
3283                                 # link to patch
3284                                 $patchno++;
3285                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3286                                 print " | ";
3287                         }
3288                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3289                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3290                                       "blob");
3291                         print "</td>\n";
3292
3293                 } elsif ($diff->{'status'} eq "D") { # deleted
3294                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3295                         print "<td>";
3296                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3297                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3298                                        -class => "list"}, esc_path($diff->{'file'}));
3299                         print "</td>\n";
3300                         print "<td>$mode_chng</td>\n";
3301                         print "<td class=\"link\">";
3302                         if ($action eq 'commitdiff') {
3303                                 # link to patch
3304                                 $patchno++;
3305                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3306                                 print " | ";
3307                         }
3308                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3309                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3310                                       "blob") . " | ";
3311                         if ($have_blame) {
3312                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3313                                                              file_name=>$diff->{'file'})},
3314                                               "blame") . " | ";
3315                         }
3316                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3317                                                      file_name=>$diff->{'file'})},
3318                                       "history");
3319                         print "</td>\n";
3320
3321                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3322                         my $mode_chnge = "";
3323                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3324                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3325                                 if ($from_file_type ne $to_file_type) {
3326                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3327                                 }
3328                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3329                                         if ($from_mode_str && $to_mode_str) {
3330                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3331                                         } elsif ($to_mode_str) {
3332                                                 $mode_chnge .= " mode: $to_mode_str";
3333                                         }
3334                                 }
3335                                 $mode_chnge .= "]</span>\n";
3336                         }
3337                         print "<td>";
3338                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3339                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3340                                       -class => "list"}, esc_path($diff->{'file'}));
3341                         print "</td>\n";
3342                         print "<td>$mode_chnge</td>\n";
3343                         print "<td class=\"link\">";
3344                         if ($action eq 'commitdiff') {
3345                                 # link to patch
3346                                 $patchno++;
3347                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3348                                       " | ";
3349                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3350                                 # "commit" view and modified file (not onlu mode changed)
3351                                 print $cgi->a({-href => href(action=>"blobdiff",
3352                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3353                                                              hash_base=>$hash, hash_parent_base=>$parent,
3354                                                              file_name=>$diff->{'file'})},
3355                                               "diff") .
3356                                       " | ";
3357                         }
3358                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3359                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3360                                        "blob") . " | ";
3361                         if ($have_blame) {
3362                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3363                                                              file_name=>$diff->{'file'})},
3364                                               "blame") . " | ";
3365                         }
3366                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3367                                                      file_name=>$diff->{'file'})},
3368                                       "history");
3369                         print "</td>\n";
3370
3371                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3372                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3373                         my $nstatus = $status_name{$diff->{'status'}};
3374                         my $mode_chng = "";
3375                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3376                                 # mode also for directories, so we cannot use $to_mode_str
3377                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3378                         }
3379                         print "<td>" .
3380                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3381                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3382                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3383                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3384                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3385                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3386                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3387                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3388                               "<td class=\"link\">";
3389                         if ($action eq 'commitdiff') {
3390                                 # link to patch
3391                                 $patchno++;
3392                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3393                                       " | ";
3394                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3395                                 # "commit" view and modified file (not only pure rename or copy)
3396                                 print $cgi->a({-href => href(action=>"blobdiff",
3397                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3398                                                              hash_base=>$hash, hash_parent_base=>$parent,
3399                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3400                                               "diff") .
3401                                       " | ";
3402                         }
3403                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3404                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3405                                       "blob") . " | ";
3406                         if ($have_blame) {
3407                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3408                                                              file_name=>$diff->{'to_file'})},
3409                                               "blame") . " | ";
3410                         }
3411                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3412                                                     file_name=>$diff->{'to_file'})},
3413                                       "history");
3414                         print "</td>\n";
3415
3416                 } # we should not encounter Unmerged (U) or Unknown (X) status
3417                 print "</tr>\n";
3418         }
3419         print "</tbody>" if $has_header;
3420         print "</table>\n";
3421 }
3422
3423 sub git_patchset_body {
3424         my ($fd, $difftree, $hash, @hash_parents) = @_;
3425         my ($hash_parent) = $hash_parents[0];
3426
3427         my $is_combined = (@hash_parents > 1);
3428         my $patch_idx = 0;
3429         my $patch_number = 0;
3430         my $patch_line;
3431         my $diffinfo;
3432         my $to_name;
3433         my (%from, %to);
3434
3435         print "<div class=\"patchset\">\n";
3436
3437         # skip to first patch
3438         while ($patch_line = <$fd>) {
3439                 chomp $patch_line;
3440
3441                 last if ($patch_line =~ m/^diff /);
3442         }
3443
3444  PATCH:
3445         while ($patch_line) {
3446
3447                 # parse "git diff" header line
3448                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3449                         # $1 is from_name, which we do not use
3450                         $to_name = unquote($2);
3451                         $to_name =~ s!^b/!!;
3452                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3453                         # $1 is 'cc' or 'combined', which we do not use
3454                         $to_name = unquote($2);
3455                 } else {
3456                         $to_name = undef;
3457                 }
3458
3459                 # check if current patch belong to current raw line
3460                 # and parse raw git-diff line if needed
3461                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3462                         # this is continuation of a split patch
3463                         print "<div class=\"patch cont\">\n";
3464                 } else {
3465                         # advance raw git-diff output if needed
3466                         $patch_idx++ if defined $diffinfo;
3467
3468                         # read and prepare patch information
3469                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3470
3471                         # compact combined diff output can have some patches skipped
3472                         # find which patch (using pathname of result) we are at now;
3473                         if ($is_combined) {
3474                                 while ($to_name ne $diffinfo->{'to_file'}) {
3475                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3476                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3477                                               "</div>\n";  # class="patch"
3478
3479                                         $patch_idx++;
3480                                         $patch_number++;
3481
3482                                         last if $patch_idx > $#$difftree;
3483                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3484                                 }
3485                         }
3486
3487                         # modifies %from, %to hashes
3488                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3489
3490                         # this is first patch for raw difftree line with $patch_idx index
3491                         # we index @$difftree array from 0, but number patches from 1
3492                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3493                 }
3494
3495                 # git diff header
3496                 #assert($patch_line =~ m/^diff /) if DEBUG;
3497                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3498                 $patch_number++;
3499                 # print "git diff" header
3500                 print format_git_diff_header_line($patch_line, $diffinfo,
3501                                                   \%from, \%to);
3502
3503                 # print extended diff header
3504                 print "<div class=\"diff extended_header\">\n";
3505         EXTENDED_HEADER:
3506                 while ($patch_line = <$fd>) {
3507                         chomp $patch_line;
3508
3509                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3510
3511                         print format_extended_diff_header_line($patch_line, $diffinfo,
3512                                                                \%from, \%to);
3513                 }
3514                 print "</div>\n"; # class="diff extended_header"
3515
3516                 # from-file/to-file diff header
3517                 if (! $patch_line) {
3518                         print "</div>\n"; # class="patch"
3519                         last PATCH;
3520                 }
3521                 next PATCH if ($patch_line =~ m/^diff /);
3522                 #assert($patch_line =~ m/^---/) if DEBUG;
3523
3524                 my $last_patch_line = $patch_line;
3525                 $patch_line = <$fd>;
3526                 chomp $patch_line;
3527                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3528
3529                 print format_diff_from_to_header($last_patch_line, $patch_line,
3530                                                  $diffinfo, \%from, \%to,
3531                                                  @hash_parents);
3532
3533                 # the patch itself
3534         LINE:
3535                 while ($patch_line = <$fd>) {
3536                         chomp $patch_line;
3537
3538                         next PATCH if ($patch_line =~ m/^diff /);
3539
3540                         print format_diff_line($patch_line, \%from, \%to);
3541                 }
3542
3543         } continue {
3544                 print "</div>\n"; # class="patch"
3545         }
3546
3547         # for compact combined (--cc) format, with chunk and patch simpliciaction
3548         # patchset might be empty, but there might be unprocessed raw lines
3549         for (++$patch_idx if $patch_number > 0;
3550              $patch_idx < @$difftree;
3551              ++$patch_idx) {
3552                 # read and prepare patch information
3553                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3554
3555                 # generate anchor for "patch" links in difftree / whatchanged part
3556                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3557                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3558                       "</div>\n";  # class="patch"
3559
3560                 $patch_number++;
3561         }
3562
3563         if ($patch_number == 0) {
3564                 if (@hash_parents > 1) {
3565                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3566                 } else {
3567                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3568                 }
3569         }
3570
3571         print "</div>\n"; # class="patchset"
3572 }
3573
3574 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3575
3576 # fills project list info (age, description, owner, forks) for each
3577 # project in the list, removing invalid projects from returned list
3578 # NOTE: modifies $projlist, but does not remove entries from it
3579 sub fill_project_list_info {
3580         my ($projlist, $check_forks) = @_;
3581         my @projects;
3582
3583  PROJECT:
3584         foreach my $pr (@$projlist) {
3585                 my (@activity) = git_get_last_activity($pr->{'path'});
3586                 unless (@activity) {
3587                         next PROJECT;
3588                 }
3589                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3590                 if (!defined $pr->{'descr'}) {
3591                         my $descr = git_get_project_description($pr->{'path'}) || "";
3592                         $descr = to_utf8($descr);
3593                         $pr->{'descr_long'} = $descr;
3594                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3595                 }
3596                 if (!defined $pr->{'owner'}) {
3597                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3598                 }
3599                 if ($check_forks) {
3600                         my $pname = $pr->{'path'};
3601                         if (($pname =~ s/\.git$//) &&
3602                             ($pname !~ /\/$/) &&
3603                             (-d "$projectroot/$pname")) {
3604                                 $pr->{'forks'} = "-d $projectroot/$pname";
3605                         }       else {
3606                                 $pr->{'forks'} = 0;
3607                         }
3608                 }
3609                 push @projects, $pr;
3610         }
3611
3612         return @projects;
3613 }
3614
3615 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3616 # (changing $list), or generating 'sort by $name' replay link otherwise
3617 sub print_sort_th {
3618         my ($str_sort, $name, $order, $key, $header, $list) = @_;
3619         $key    ||= $name;
3620         $header ||= ucfirst($name);
3621
3622         if ($order eq $name) {
3623                 if ($str_sort) {
3624                         @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
3625                 } else {
3626                         @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
3627                 }
3628                 print "<th>$header</th>\n";
3629         } else {
3630                 print "<th>" .
3631                       $cgi->a({-href => href(-replay=>1, order=>$name),
3632                                -class => "header"}, $header) .
3633                       "</th>\n";
3634         }
3635 }
3636
3637 sub print_sort_th_str {
3638         print_sort_th(1, @_);
3639 }
3640
3641 sub print_sort_th_num {
3642         print_sort_th(0, @_);
3643 }
3644
3645 sub git_project_list_body {
3646         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3647
3648         my ($check_forks) = gitweb_check_feature('forks');
3649         my @projects = fill_project_list_info($projlist, $check_forks);
3650
3651         $order ||= $default_projects_order;
3652         $from = 0 unless defined $from;
3653         $to = $#projects if (!defined $to || $#projects < $to);
3654
3655         print "<table class=\"project_list\">\n";
3656         unless ($no_header) {
3657                 print "<tr>\n";
3658                 if ($check_forks) {
3659                         print "<th></th>\n";
3660                 }
3661                 print_sort_th_str('project', $order, 'path',
3662                                   'Project', \@projects);
3663                 print_sort_th_str('descr', $order, 'descr_long',
3664                                   'Description', \@projects);
3665                 print_sort_th_str('owner', $order, 'owner',
3666                                   'Owner', \@projects);
3667                 print_sort_th_num('age', $order, 'age',
3668                                   'Last Change', \@projects);
3669                 print "<th></th>\n" . # for links
3670                       "</tr>\n";
3671         }
3672         my $alternate = 1;
3673         for (my $i = $from; $i <= $to; $i++) {
3674                 my $pr = $projects[$i];
3675                 if ($alternate) {
3676                         print "<tr class=\"dark\">\n";
3677                 } else {
3678                         print "<tr class=\"light\">\n";
3679                 }
3680                 $alternate ^= 1;
3681                 if ($check_forks) {
3682                         print "<td>";
3683                         if ($pr->{'forks'}) {
3684                                 print "<!-- $pr->{'forks'} -->\n";
3685                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3686                         }
3687                         print "</td>\n";
3688                 }
3689                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3690                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3691                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3692                                         -class => "list", -title => $pr->{'descr_long'}},
3693                                         esc_html($pr->{'descr'})) . "</td>\n" .
3694                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3695                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3696                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3697                       "<td class=\"link\">" .
3698                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3699                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3700                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3701                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3702                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3703                       "</td>\n" .
3704                       "</tr>\n";
3705         }
3706         if (defined $extra) {
3707                 print "<tr>\n";
3708                 if ($check_forks) {
3709                         print "<td></td>\n";
3710                 }
3711                 print "<td colspan=\"5\">$extra</td>\n" .
3712                       "</tr>\n";
3713         }
3714         print "</table>\n";
3715 }
3716
3717 sub git_shortlog_body {
3718         # uses global variable $project
3719         my ($commitlist, $from, $to, $refs, $extra) = @_;
3720
3721         $from = 0 unless defined $from;
3722         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3723
3724         print "<table class=\"shortlog\">\n";
3725         my $alternate = 1;
3726         for (my $i = $from; $i <= $to; $i++) {
3727                 my %co = %{$commitlist->[$i]};
3728                 my $commit = $co{'id'};
3729                 my $ref = format_ref_marker($refs, $commit);
3730                 if ($alternate) {
3731                         print "<tr class=\"dark\">\n";
3732                 } else {
3733                         print "<tr class=\"light\">\n";
3734                 }
3735                 $alternate ^= 1;
3736                 my $author = chop_and_escape_str($co{'author_name'}, 10);
3737                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3738                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3739                       "<td><i>" . $author . "</i></td>\n" .
3740                       "<td>";
3741                 print format_subject_html($co{'title'}, $co{'title_short'},
3742                                           href(action=>"commit", hash=>$commit), $ref);
3743                 print "</td>\n" .
3744                       "<td class=\"link\">" .
3745                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3746                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3747                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3748                 my $snapshot_links = format_snapshot_links($commit);
3749                 if (defined $snapshot_links) {
3750                         print " | " . $snapshot_links;
3751                 }
3752                 print "</td>\n" .
3753                       "</tr>\n";
3754         }
3755         if (defined $extra) {
3756                 print "<tr>\n" .
3757                       "<td colspan=\"4\">$extra</td>\n" .
3758                       "</tr>\n";
3759         }
3760         print "</table>\n";
3761 }
3762
3763 sub git_history_body {
3764         # Warning: assumes constant type (blob or tree) during history
3765         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3766
3767         $from = 0 unless defined $from;
3768         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3769
3770         print "<table class=\"history\">\n";
3771         my $alternate = 1;
3772         for (my $i = $from; $i <= $to; $i++) {
3773                 my %co = %{$commitlist->[$i]};
3774                 if (!%co) {
3775                         next;
3776                 }
3777                 my $commit = $co{'id'};
3778
3779                 my $ref = format_ref_marker($refs, $commit);
3780
3781                 if ($alternate) {
3782                         print "<tr class=\"dark\">\n";
3783                 } else {
3784                         print "<tr class=\"light\">\n";
3785                 }
3786                 $alternate ^= 1;
3787         # shortlog uses      chop_str($co{'author_name'}, 10)
3788                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3789                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3790                       "<td><i>" . $author . "</i></td>\n" .
3791                       "<td>";
3792                 # originally git_history used chop_str($co{'title'}, 50)
3793                 print format_subject_html($co{'title'}, $co{'title_short'},
3794                                           href(action=>"commit", hash=>$commit), $ref);
3795                 print "</td>\n" .
3796                       "<td class=\"link\">" .
3797                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3798                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3799
3800                 if ($ftype eq 'blob') {
3801                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3802                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3803                         if (defined $blob_current && defined $blob_parent &&
3804                                         $blob_current ne $blob_parent) {
3805                                 print " | " .
3806                                         $cgi->a({-href => href(action=>"blobdiff",
3807                                                                hash=>$blob_current, hash_parent=>$blob_parent,
3808                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
3809                                                                file_name=>$file_name)},
3810                                                 "diff to current");
3811                         }
3812                 }
3813                 print "</td>\n" .
3814                       "</tr>\n";
3815         }
3816         if (defined $extra) {
3817                 print "<tr>\n" .
3818                       "<td colspan=\"4\">$extra</td>\n" .
3819                       "</tr>\n";
3820         }
3821         print "</table>\n";
3822 }
3823
3824 sub git_tags_body {
3825         # uses global variable $project
3826         my ($taglist, $from, $to, $extra) = @_;
3827         $from = 0 unless defined $from;
3828         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3829
3830         print "<table class=\"tags\">\n";
3831         my $alternate = 1;
3832         for (my $i = $from; $i <= $to; $i++) {
3833                 my $entry = $taglist->[$i];
3834                 my %tag = %$entry;
3835                 my $comment = $tag{'subject'};
3836                 my $comment_short;
3837                 if (defined $comment) {
3838                         $comment_short = chop_str($comment, 30, 5);
3839                 }
3840                 if ($alternate) {
3841                         print "<tr class=\"dark\">\n";
3842                 } else {
3843                         print "<tr class=\"light\">\n";
3844                 }
3845                 $alternate ^= 1;
3846                 if (defined $tag{'age'}) {
3847                         print "<td><i>$tag{'age'}</i></td>\n";
3848                 } else {
3849                         print "<td></td>\n";
3850                 }
3851                 print "<td>" .
3852                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3853                                -class => "list name"}, esc_html($tag{'name'})) .
3854                       "</td>\n" .
3855                       "<td>";
3856                 if (defined $comment) {
3857                         print format_subject_html($comment, $comment_short,
3858                                                   href(action=>"tag", hash=>$tag{'id'}));
3859                 }
3860                 print "</td>\n" .
3861                       "<td class=\"selflink\">";
3862                 if ($tag{'type'} eq "tag") {
3863                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3864                 } else {
3865                         print "&nbsp;";
3866                 }
3867                 print "</td>\n" .
3868                       "<td class=\"link\">" . " | " .
3869                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3870                 if ($tag{'reftype'} eq "commit") {
3871                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3872                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3873                 } elsif ($tag{'reftype'} eq "blob") {
3874                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3875                 }
3876                 print "</td>\n" .
3877                       "</tr>";
3878         }
3879         if (defined $extra) {
3880                 print "<tr>\n" .
3881                       "<td colspan=\"5\">$extra</td>\n" .
3882                       "</tr>\n";
3883         }
3884         print "</table>\n";
3885 }
3886
3887 sub git_heads_body {
3888         # uses global variable $project
3889         my ($headlist, $head, $from, $to, $extra) = @_;
3890         $from = 0 unless defined $from;
3891         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3892
3893         print "<table class=\"heads\">\n";
3894         my $alternate = 1;
3895         for (my $i = $from; $i <= $to; $i++) {
3896                 my $entry = $headlist->[$i];
3897                 my %ref = %$entry;
3898                 my $curr = $ref{'id'} eq $head;
3899                 if ($alternate) {
3900                         print "<tr class=\"dark\">\n";
3901                 } else {
3902                         print "<tr class=\"light\">\n";
3903                 }
3904                 $alternate ^= 1;
3905                 print "<td><i>$ref{'age'}</i></td>\n" .
3906                       ($curr ? "<td class=\"current_head\">" : "<td>") .
3907                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
3908                                -class => "list name"},esc_html($ref{'name'})) .
3909                       "</td>\n" .
3910                       "<td class=\"link\">" .
3911                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
3912                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
3913                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
3914                       "</td>\n" .
3915                       "</tr>";
3916         }
3917         if (defined $extra) {
3918                 print "<tr>\n" .
3919                       "<td colspan=\"3\">$extra</td>\n" .
3920                       "</tr>\n";
3921         }
3922         print "</table>\n";
3923 }
3924
3925 sub git_search_grep_body {
3926         my ($commitlist, $from, $to, $extra) = @_;
3927         $from = 0 unless defined $from;
3928         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3929
3930         print "<table class=\"commit_search\">\n";
3931         my $alternate = 1;
3932         for (my $i = $from; $i <= $to; $i++) {
3933                 my %co = %{$commitlist->[$i]};
3934                 if (!%co) {
3935                         next;
3936                 }
3937                 my $commit = $co{'id'};
3938                 if ($alternate) {
3939                         print "<tr class=\"dark\">\n";
3940                 } else {
3941                         print "<tr class=\"light\">\n";
3942                 }
3943                 $alternate ^= 1;
3944                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
3945                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3946                       "<td><i>" . $author . "</i></td>\n" .
3947                       "<td>" .
3948                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3949                                -class => "list subject"},
3950                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
3951                 my $comment = $co{'comment'};
3952                 foreach my $line (@$comment) {
3953                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3954                                 my ($lead, $match, $trail) = ($1, $2, $3);
3955                                 $match = chop_str($match, 70, 5, 'center');
3956                                 my $contextlen = int((80 - length($match))/2);
3957                                 $contextlen = 30 if ($contextlen > 30);
3958                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
3959                                 $trail = chop_str($trail, $contextlen, 10, 'right');
3960
3961                                 $lead  = esc_html($lead);
3962                                 $match = esc_html($match);
3963                                 $trail = esc_html($trail);
3964
3965                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
3966                         }
3967                 }
3968                 print "</td>\n" .
3969                       "<td class=\"link\">" .
3970                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3971                       " | " .
3972                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
3973                       " | " .
3974                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3975                 print "</td>\n" .
3976                       "</tr>\n";
3977         }
3978         if (defined $extra) {
3979                 print "<tr>\n" .
3980                       "<td colspan=\"3\">$extra</td>\n" .
3981                       "</tr>\n";
3982         }
3983         print "</table>\n";
3984 }
3985
3986 ## ======================================================================
3987 ## ======================================================================
3988 ## actions
3989
3990 sub git_project_list {
3991         my $order = $cgi->param('o');
3992         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3993                 die_error(400, "Unknown order parameter");
3994         }
3995
3996         my @list = git_get_projects_list();
3997         if (!@list) {
3998                 die_error(404, "No projects found");
3999         }
4000
4001         git_header_html();
4002         if (-f $home_text) {
4003                 print "<div class=\"index_include\">\n";
4004                 open (my $fd, $home_text);
4005                 print <$fd>;
4006                 close $fd;
4007                 print "</div>\n";
4008         }
4009         git_project_list_body(\@list, $order);
4010         git_footer_html();
4011 }
4012
4013 sub git_forks {
4014         my $order = $cgi->param('o');
4015         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4016                 die_error(400, "Unknown order parameter");
4017         }
4018
4019         my @list = git_get_projects_list($project);
4020         if (!@list) {
4021                 die_error(404, "No forks found");
4022         }
4023
4024         git_header_html();
4025         git_print_page_nav('','');
4026         git_print_header_div('summary', "$project forks");
4027         git_project_list_body(\@list, $order);
4028         git_footer_html();
4029 }
4030
4031 sub git_project_index {
4032         my @projects = git_get_projects_list($project);
4033
4034         print $cgi->header(
4035                 -type => 'text/plain',
4036                 -charset => 'utf-8',
4037                 -content_disposition => 'inline; filename="index.aux"');
4038
4039         foreach my $pr (@projects) {
4040                 if (!exists $pr->{'owner'}) {
4041                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4042                 }
4043
4044                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4045                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4046                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4047                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4048                 $path  =~ s/ /\+/g;
4049                 $owner =~ s/ /\+/g;
4050
4051                 print "$path $owner\n";
4052         }
4053 }
4054
4055 sub git_summary {
4056         my $descr = git_get_project_description($project) || "none";
4057         my %co = parse_commit("HEAD");
4058         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4059         my $head = $co{'id'};
4060
4061         my $owner = git_get_project_owner($project);
4062
4063         my $refs = git_get_references();
4064         # These get_*_list functions return one more to allow us to see if
4065         # there are more ...
4066         my @taglist  = git_get_tags_list(16);
4067         my @headlist = git_get_heads_list(16);
4068         my @forklist;
4069         my ($check_forks) = gitweb_check_feature('forks');
4070
4071         if ($check_forks) {
4072                 @forklist = git_get_projects_list($project);
4073         }
4074
4075         git_header_html();
4076         git_print_page_nav('summary','', $head);
4077
4078         print "<div class=\"title\">&nbsp;</div>\n";
4079         print "<table class=\"projects_list\">\n" .
4080               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4081               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4082         if (defined $cd{'rfc2822'}) {
4083                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4084         }
4085
4086         # use per project git URL list in $projectroot/$project/cloneurl
4087         # or make project git URL from git base URL and project name
4088         my $url_tag = "URL";
4089         my @url_list = git_get_project_url_list($project);
4090         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4091         foreach my $git_url (@url_list) {
4092                 next unless $git_url;
4093                 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4094                 $url_tag = "";
4095         }
4096         print "</table>\n";
4097
4098         if (-s "$projectroot/$project/README.html") {
4099                 if (open my $fd, "$projectroot/$project/README.html") {
4100                         print "<div class=\"title\">readme</div>\n" .
4101                               "<div class=\"readme\">\n";
4102                         print $_ while (<$fd>);
4103                         print "\n</div>\n"; # class="readme"
4104                         close $fd;
4105                 }
4106         }
4107
4108         # we need to request one more than 16 (0..15) to check if
4109         # those 16 are all
4110         my @commitlist = $head ? parse_commits($head, 17) : ();
4111         if (@commitlist) {
4112                 git_print_header_div('shortlog');
4113                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4114                                   $#commitlist <=  15 ? undef :
4115                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4116         }
4117
4118         if (@taglist) {
4119                 git_print_header_div('tags');
4120                 git_tags_body(\@taglist, 0, 15,
4121                               $#taglist <=  15 ? undef :
4122                               $cgi->a({-href => href(action=>"tags")}, "..."));
4123         }
4124
4125         if (@headlist) {
4126                 git_print_header_div('heads');
4127                 git_heads_body(\@headlist, $head, 0, 15,
4128                                $#headlist <= 15 ? undef :
4129                                $cgi->a({-href => href(action=>"heads")}, "..."));
4130         }
4131
4132         if (@forklist) {
4133                 git_print_header_div('forks');
4134                 git_project_list_body(\@forklist, undef, 0, 15,
4135                                       $#forklist <= 15 ? undef :
4136                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4137                                       'noheader');
4138         }
4139
4140         git_footer_html();
4141 }
4142
4143 sub git_tag {
4144         my $head = git_get_head_hash($project);
4145         git_header_html();
4146         git_print_page_nav('','', $head,undef,$head);
4147         my %tag = parse_tag($hash);
4148
4149         if (! %tag) {
4150                 die_error(404, "Unknown tag object");
4151         }
4152
4153         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4154         print "<div class=\"title_text\">\n" .
4155               "<table class=\"object_header\">\n" .
4156               "<tr>\n" .
4157               "<td>object</td>\n" .
4158               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4159                                $tag{'object'}) . "</td>\n" .
4160               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4161                                               $tag{'type'}) . "</td>\n" .
4162               "</tr>\n";
4163         if (defined($tag{'author'})) {
4164                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4165                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4166                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4167                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4168                         "</td></tr>\n";
4169         }
4170         print "</table>\n\n" .
4171               "</div>\n";
4172         print "<div class=\"page_body\">";
4173         my $comment = $tag{'comment'};
4174         foreach my $line (@$comment) {
4175                 chomp $line;
4176                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4177         }
4178         print "</div>\n";
4179         git_footer_html();
4180 }
4181
4182 sub git_blame {
4183         my $fd;
4184         my $ftype;
4185
4186         gitweb_check_feature('blame')
4187             or die_error(403, "Blame view not allowed");
4188
4189         die_error(400, "No file name given") unless $file_name;
4190         $hash_base ||= git_get_head_hash($project);
4191         die_error(404, "Couldn't find base commit") unless ($hash_base);
4192         my %co = parse_commit($hash_base)
4193                 or die_error(404, "Commit not found");
4194         if (!defined $hash) {
4195                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4196                         or die_error(404, "Error looking up file");
4197         }
4198         $ftype = git_get_type($hash);
4199         if ($ftype !~ "blob") {
4200                 die_error(400, "Object is not a blob");
4201         }
4202         open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4203               $file_name, $hash_base)
4204                 or die_error(500, "Open git-blame failed");
4205         git_header_html();
4206         my $formats_nav =
4207                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4208                         "blob") .
4209                 " | " .
4210                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4211                         "history") .
4212                 " | " .
4213                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4214                         "HEAD");
4215         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4216         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4217         git_print_page_path($file_name, $ftype, $hash_base);
4218         my @rev_color = (qw(light2 dark2));
4219         my $num_colors = scalar(@rev_color);
4220         my $current_color = 0;
4221         my $last_rev;
4222         print <<HTML;
4223 <div class="page_body">
4224 <table class="blame">
4225 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4226 HTML
4227         my %metainfo = ();
4228         while (1) {
4229                 $_ = <$fd>;
4230                 last unless defined $_;
4231                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4232                     /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4233                 if (!exists $metainfo{$full_rev}) {
4234                         $metainfo{$full_rev} = {};
4235                 }
4236                 my $meta = $metainfo{$full_rev};
4237                 while (<$fd>) {
4238                         last if (s/^\t//);
4239                         if (/^(\S+) (.*)$/) {
4240                                 $meta->{$1} = $2;
4241                         }
4242                 }
4243                 my $data = $_;
4244                 chomp $data;
4245                 my $rev = substr($full_rev, 0, 8);
4246                 my $author = $meta->{'author'};
4247                 my %date = parse_date($meta->{'author-time'},
4248                                       $meta->{'author-tz'});
4249                 my $date = $date{'iso-tz'};
4250                 if ($group_size) {
4251                         $current_color = ++$current_color % $num_colors;
4252                 }
4253                 print "<tr class=\"$rev_color[$current_color]\">\n";
4254                 if ($group_size) {
4255                         print "<td class=\"sha1\"";
4256                         print " title=\"". esc_html($author) . ", $date\"";
4257                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4258                         print ">";
4259                         print $cgi->a({-href => href(action=>"commit",
4260                                                      hash=>$full_rev,
4261                                                      file_name=>$file_name)},
4262                                       esc_html($rev));
4263                         print "</td>\n";
4264                 }
4265                 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4266                         or die_error(500, "Open git-rev-parse failed");
4267                 my $parent_commit = <$dd>;
4268                 close $dd;
4269                 chomp($parent_commit);
4270                 my $blamed = href(action => 'blame',
4271                                   file_name => $meta->{'filename'},
4272                                   hash_base => $parent_commit);
4273                 print "<td class=\"linenr\">";
4274                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4275                                 -id => "l$lineno",
4276                                 -class => "linenr" },
4277                               esc_html($lineno));
4278                 print "</td>";
4279                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4280                 print "</tr>\n";
4281         }
4282         print "</table>\n";
4283         print "</div>";
4284         close $fd
4285                 or print "Reading blob failed\n";
4286         git_footer_html();
4287 }
4288
4289 sub git_tags {
4290         my $head = git_get_head_hash($project);
4291         git_header_html();
4292         git_print_page_nav('','', $head,undef,$head);
4293         git_print_header_div('summary', $project);
4294
4295         my @tagslist = git_get_tags_list();
4296         if (@tagslist) {
4297                 git_tags_body(\@tagslist);
4298         }
4299         git_footer_html();
4300 }
4301
4302 sub git_heads {
4303         my $head = git_get_head_hash($project);
4304         git_header_html();
4305         git_print_page_nav('','', $head,undef,$head);
4306         git_print_header_div('summary', $project);
4307
4308         my @headslist = git_get_heads_list();
4309         if (@headslist) {
4310                 git_heads_body(\@headslist, $head);
4311         }
4312         git_footer_html();
4313 }
4314
4315 sub git_blob_plain {
4316         my $type = shift;
4317         my $expires;
4318
4319         if (!defined $hash) {
4320                 if (defined $file_name) {
4321                         my $base = $hash_base || git_get_head_hash($project);
4322                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4323                                 or die_error(404, "Cannot find file");
4324                 } else {
4325                         die_error(400, "No file name defined");
4326                 }
4327         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4328                 # blobs defined by non-textual hash id's can be cached
4329                 $expires = "+1d";
4330         }
4331
4332         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4333                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4334
4335         # content-type (can include charset)
4336         $type = blob_contenttype($fd, $file_name, $type);
4337
4338         # "save as" filename, even when no $file_name is given
4339         my $save_as = "$hash";
4340         if (defined $file_name) {
4341                 $save_as = $file_name;
4342         } elsif ($type =~ m/^text\//) {
4343                 $save_as .= '.txt';
4344         }
4345
4346         print $cgi->header(
4347                 -type => $type,
4348                 -expires => $expires,
4349                 -content_disposition => 'inline; filename="' . $save_as . '"');
4350         undef $/;
4351         binmode STDOUT, ':raw';
4352         print <$fd>;
4353         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4354         $/ = "\n";
4355         close $fd;
4356 }
4357
4358 sub git_blob {
4359         my $expires;
4360
4361         if (!defined $hash) {
4362                 if (defined $file_name) {
4363                         my $base = $hash_base || git_get_head_hash($project);
4364                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4365                                 or die_error(404, "Cannot find file");
4366                 } else {
4367                         die_error(400, "No file name defined");
4368                 }
4369         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4370                 # blobs defined by non-textual hash id's can be cached
4371                 $expires = "+1d";
4372         }
4373
4374         my ($have_blame) = gitweb_check_feature('blame');
4375         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4376                 or die_error(500, "Couldn't cat $file_name, $hash");
4377         my $mimetype = blob_mimetype($fd, $file_name);
4378         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4379                 close $fd;
4380                 return git_blob_plain($mimetype);
4381         }
4382         # we can have blame only for text/* mimetype
4383         $have_blame &&= ($mimetype =~ m!^text/!);
4384
4385         git_header_html(undef, $expires);
4386         my $formats_nav = '';
4387         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4388                 if (defined $file_name) {
4389                         if ($have_blame) {
4390                                 $formats_nav .=
4391                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
4392                                                 "blame") .
4393                                         " | ";
4394                         }
4395                         $formats_nav .=
4396                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4397                                         "history") .
4398                                 " | " .
4399                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4400                                         "raw") .
4401                                 " | " .
4402                                 $cgi->a({-href => href(action=>"blob",
4403                                                        hash_base=>"HEAD", file_name=>$file_name)},
4404                                         "HEAD");
4405                 } else {
4406                         $formats_nav .=
4407                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4408                                         "raw");
4409                 }
4410                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4411                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4412         } else {
4413                 print "<div class=\"page_nav\">\n" .
4414                       "<br/><br/></div>\n" .
4415                       "<div class=\"title\">$hash</div>\n";
4416         }
4417         git_print_page_path($file_name, "blob", $hash_base);
4418         print "<div class=\"page_body\">\n";
4419         if ($mimetype =~ m!^image/!) {
4420                 print qq!<img type="$mimetype"!;
4421                 if ($file_name) {
4422                         print qq! alt="$file_name" title="$file_name"!;
4423                 }
4424                 print qq! src="! .
4425                       href(action=>"blob_plain", hash=>$hash,
4426                            hash_base=>$hash_base, file_name=>$file_name) .
4427                       qq!" />\n!;
4428         } else {
4429                 my $nr;
4430                 while (my $line = <$fd>) {
4431                         chomp $line;
4432                         $nr++;
4433                         $line = untabify($line);
4434                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4435                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4436                 }
4437         }
4438         close $fd
4439                 or print "Reading blob failed.\n";
4440         print "</div>";
4441         git_footer_html();
4442 }
4443
4444 sub git_tree {
4445         if (!defined $hash_base) {
4446                 $hash_base = "HEAD";
4447         }
4448         if (!defined $hash) {
4449                 if (defined $file_name) {
4450                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4451                 } else {
4452                         $hash = $hash_base;
4453                 }
4454         }
4455         die_error(404, "No such tree") unless defined($hash);
4456         $/ = "\0";
4457         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4458                 or die_error(500, "Open git-ls-tree failed");
4459         my @entries = map { chomp; $_ } <$fd>;
4460         close $fd or die_error(404, "Reading tree failed");
4461         $/ = "\n";
4462
4463         my $refs = git_get_references();
4464         my $ref = format_ref_marker($refs, $hash_base);
4465         git_header_html();
4466         my $basedir = '';
4467         my ($have_blame) = gitweb_check_feature('blame');
4468         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4469                 my @views_nav = ();
4470                 if (defined $file_name) {
4471                         push @views_nav,
4472                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4473                                         "history"),
4474                                 $cgi->a({-href => href(action=>"tree",
4475                                                        hash_base=>"HEAD", file_name=>$file_name)},
4476                                         "HEAD"),
4477                 }
4478                 my $snapshot_links = format_snapshot_links($hash);
4479                 if (defined $snapshot_links) {
4480                         # FIXME: Should be available when we have no hash base as well.
4481                         push @views_nav, $snapshot_links;
4482                 }
4483                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4484                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4485         } else {
4486                 undef $hash_base;
4487                 print "<div class=\"page_nav\">\n";
4488                 print "<br/><br/></div>\n";
4489                 print "<div class=\"title\">$hash</div>\n";
4490         }
4491         if (defined $file_name) {
4492                 $basedir = $file_name;
4493                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4494                         $basedir .= '/';
4495                 }
4496                 git_print_page_path($file_name, 'tree', $hash_base);
4497         }
4498         print "<div class=\"page_body\">\n";
4499         print "<table class=\"tree\">\n";
4500         my $alternate = 1;
4501         # '..' (top directory) link if possible
4502         if (defined $hash_base &&
4503             defined $file_name && $file_name =~ m![^/]+$!) {
4504                 if ($alternate) {
4505                         print "<tr class=\"dark\">\n";
4506                 } else {
4507                         print "<tr class=\"light\">\n";
4508                 }
4509                 $alternate ^= 1;
4510
4511                 my $up = $file_name;
4512                 $up =~ s!/?[^/]+$!!;
4513                 undef $up unless $up;
4514                 # based on git_print_tree_entry
4515                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4516                 print '<td class="list">';
4517                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4518                                              file_name=>$up)},
4519                               "..");
4520                 print "</td>\n";
4521                 print "<td class=\"link\"></td>\n";
4522
4523                 print "</tr>\n";
4524         }
4525         foreach my $line (@entries) {
4526                 my %t = parse_ls_tree_line($line, -z => 1);
4527
4528                 if ($alternate) {
4529                         print "<tr class=\"dark\">\n";
4530                 } else {
4531                         print "<tr class=\"light\">\n";
4532                 }
4533                 $alternate ^= 1;
4534
4535                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4536
4537                 print "</tr>\n";
4538         }
4539         print "</table>\n" .
4540               "</div>";
4541         git_footer_html();
4542 }
4543
4544 sub git_snapshot {
4545         my @supported_fmts = gitweb_check_feature('snapshot');
4546         @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4547
4548         my $format = $cgi->param('sf');
4549         if (!@supported_fmts) {
4550                 die_error(403, "Snapshots not allowed");
4551         }
4552         # default to first supported snapshot format
4553         $format ||= $supported_fmts[0];
4554         if ($format !~ m/^[a-z0-9]+$/) {
4555                 die_error(400, "Invalid snapshot format parameter");
4556         } elsif (!exists($known_snapshot_formats{$format})) {
4557                 die_error(400, "Unknown snapshot format");
4558         } elsif (!grep($_ eq $format, @supported_fmts)) {
4559                 die_error(403, "Unsupported snapshot format");
4560         }
4561
4562         if (!defined $hash) {
4563                 $hash = git_get_head_hash($project);
4564         }
4565
4566         my $name = $project;
4567         $name =~ s,([^/])/*\.git$,$1,;
4568         $name = basename($name);
4569         my $filename = to_utf8($name);
4570         $name =~ s/\047/\047\\\047\047/g;
4571         my $cmd;
4572         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4573         $cmd = quote_command(
4574                 git_cmd(), 'archive',
4575                 "--format=$known_snapshot_formats{$format}{'format'}",
4576                 "--prefix=$name/", $hash);
4577         if (exists $known_snapshot_formats{$format}{'compressor'}) {
4578                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4579         }
4580
4581         print $cgi->header(
4582                 -type => $known_snapshot_formats{$format}{'type'},
4583                 -content_disposition => 'inline; filename="' . "$filename" . '"',
4584                 -status => '200 OK');
4585
4586         open my $fd, "-|", $cmd
4587                 or die_error(500, "Execute git-archive failed");
4588         binmode STDOUT, ':raw';
4589         print <$fd>;
4590         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4591         close $fd;
4592 }
4593
4594 sub git_log {
4595         my $head = git_get_head_hash($project);
4596         if (!defined $hash) {
4597                 $hash = $head;
4598         }
4599         if (!defined $page) {
4600                 $page = 0;
4601         }
4602         my $refs = git_get_references();
4603
4604         my @commitlist = parse_commits($hash, 101, (100 * $page));
4605
4606         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4607
4608         git_header_html();
4609         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4610
4611         if (!@commitlist) {
4612                 my %co = parse_commit($hash);
4613
4614                 git_print_header_div('summary', $project);
4615                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4616         }
4617         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4618         for (my $i = 0; $i <= $to; $i++) {
4619                 my %co = %{$commitlist[$i]};
4620                 next if !%co;
4621                 my $commit = $co{'id'};
4622                 my $ref = format_ref_marker($refs, $commit);
4623                 my %ad = parse_date($co{'author_epoch'});
4624                 git_print_header_div('commit',
4625                                "<span class=\"age\">$co{'age_string'}</span>" .
4626                                esc_html($co{'title'}) . $ref,
4627                                $commit);
4628                 print "<div class=\"title_text\">\n" .
4629                       "<div class=\"log_link\">\n" .
4630                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4631                       " | " .
4632                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4633                       " | " .
4634                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4635                       "<br/>\n" .
4636                       "</div>\n" .
4637                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4638                       "</div>\n";
4639
4640                 print "<div class=\"log_body\">\n";
4641                 git_print_log($co{'comment'}, -final_empty_line=> 1);
4642                 print "</div>\n";
4643         }
4644         if ($#commitlist >= 100) {
4645                 print "<div class=\"page_nav\">\n";
4646                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4647                                -accesskey => "n", -title => "Alt-n"}, "next");
4648                 print "</div>\n";
4649         }
4650         git_footer_html();
4651 }
4652
4653 sub git_commit {
4654         $hash ||= $hash_base || "HEAD";
4655         my %co = parse_commit($hash)
4656             or die_error(404, "Unknown commit object");
4657         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4658         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4659
4660         my $parent  = $co{'parent'};
4661         my $parents = $co{'parents'}; # listref
4662
4663         # we need to prepare $formats_nav before any parameter munging
4664         my $formats_nav;
4665         if (!defined $parent) {
4666                 # --root commitdiff
4667                 $formats_nav .= '(initial)';
4668         } elsif (@$parents == 1) {
4669                 # single parent commit
4670                 $formats_nav .=
4671                         '(parent: ' .
4672                         $cgi->a({-href => href(action=>"commit",
4673                                                hash=>$parent)},
4674                                 esc_html(substr($parent, 0, 7))) .
4675                         ')';
4676         } else {
4677                 # merge commit
4678                 $formats_nav .=
4679                         '(merge: ' .
4680                         join(' ', map {
4681                                 $cgi->a({-href => href(action=>"commit",
4682                                                        hash=>$_)},
4683                                         esc_html(substr($_, 0, 7)));
4684                         } @$parents ) .
4685                         ')';
4686         }
4687
4688         if (!defined $parent) {
4689                 $parent = "--root";
4690         }
4691         my @difftree;
4692         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4693                 @diff_opts,
4694                 (@$parents <= 1 ? $parent : '-c'),
4695                 $hash, "--"
4696                 or die_error(500, "Open git-diff-tree failed");
4697         @difftree = map { chomp; $_ } <$fd>;
4698         close $fd or die_error(404, "Reading git-diff-tree failed");
4699
4700         # non-textual hash id's can be cached
4701         my $expires;
4702         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4703                 $expires = "+1d";
4704         }
4705         my $refs = git_get_references();
4706         my $ref = format_ref_marker($refs, $co{'id'});
4707
4708         git_header_html(undef, $expires);
4709         git_print_page_nav('commit', '',
4710                            $hash, $co{'tree'}, $hash,
4711                            $formats_nav);
4712
4713         if (defined $co{'parent'}) {
4714                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4715         } else {
4716                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4717         }
4718         print "<div class=\"title_text\">\n" .
4719               "<table class=\"object_header\">\n";
4720         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4721               "<tr>" .
4722               "<td></td><td> $ad{'rfc2822'}";
4723         if ($ad{'hour_local'} < 6) {
4724                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4725                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4726         } else {
4727                 printf(" (%02d:%02d %s)",
4728                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4729         }
4730         print "</td>" .
4731               "</tr>\n";
4732         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4733         print "<tr><td></td><td> $cd{'rfc2822'}" .
4734               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4735               "</td></tr>\n";
4736         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4737         print "<tr>" .
4738               "<td>tree</td>" .
4739               "<td class=\"sha1\">" .
4740               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4741                        class => "list"}, $co{'tree'}) .
4742               "</td>" .
4743               "<td class=\"link\">" .
4744               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4745                       "tree");
4746         my $snapshot_links = format_snapshot_links($hash);
4747         if (defined $snapshot_links) {
4748                 print " | " . $snapshot_links;
4749         }
4750         print "</td>" .
4751               "</tr>\n";
4752
4753         foreach my $par (@$parents) {
4754                 print "<tr>" .
4755                       "<td>parent</td>" .
4756                       "<td class=\"sha1\">" .
4757                       $cgi->a({-href => href(action=>"commit", hash=>$par),
4758                                class => "list"}, $par) .
4759                       "</td>" .
4760                       "<td class=\"link\">" .
4761                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4762                       " | " .
4763                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4764                       "</td>" .
4765                       "</tr>\n";
4766         }
4767         print "</table>".
4768               "</div>\n";
4769
4770         print "<div class=\"page_body\">\n";
4771         git_print_log($co{'comment'});
4772         print "</div>\n";
4773
4774         git_difftree_body(\@difftree, $hash, @$parents);
4775
4776         git_footer_html();
4777 }
4778
4779 sub git_object {
4780         # object is defined by:
4781         # - hash or hash_base alone
4782         # - hash_base and file_name
4783         my $type;
4784
4785         # - hash or hash_base alone
4786         if ($hash || ($hash_base && !defined $file_name)) {
4787                 my $object_id = $hash || $hash_base;
4788
4789                 open my $fd, "-|", quote_command(
4790                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4791                         or die_error(404, "Object does not exist");
4792                 $type = <$fd>;
4793                 chomp $type;
4794                 close $fd
4795                         or die_error(404, "Object does not exist");
4796
4797         # - hash_base and file_name
4798         } elsif ($hash_base && defined $file_name) {
4799                 $file_name =~ s,/+$,,;
4800
4801                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4802                         or die_error(404, "Base object does not exist");
4803
4804                 # here errors should not hapen
4805                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4806                         or die_error(500, "Open git-ls-tree failed");
4807                 my $line = <$fd>;
4808                 close $fd;
4809
4810                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4811                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4812                         die_error(404, "File or directory for given base does not exist");
4813                 }
4814                 $type = $2;
4815                 $hash = $3;
4816         } else {
4817                 die_error(400, "Not enough information to find object");
4818         }
4819
4820         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4821                                           hash=>$hash, hash_base=>$hash_base,
4822                                           file_name=>$file_name),
4823                              -status => '302 Found');
4824 }
4825
4826 sub git_blobdiff {
4827         my $format = shift || 'html';
4828
4829         my $fd;
4830         my @difftree;
4831         my %diffinfo;
4832         my $expires;
4833
4834         # preparing $fd and %diffinfo for git_patchset_body
4835         # new style URI
4836         if (defined $hash_base && defined $hash_parent_base) {
4837                 if (defined $file_name) {
4838                         # read raw output
4839                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4840                                 $hash_parent_base, $hash_base,
4841                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
4842                                 or die_error(500, "Open git-diff-tree failed");
4843                         @difftree = map { chomp; $_ } <$fd>;
4844                         close $fd
4845                                 or die_error(404, "Reading git-diff-tree failed");
4846                         @difftree
4847                                 or die_error(404, "Blob diff not found");
4848
4849                 } elsif (defined $hash &&
4850                          $hash =~ /[0-9a-fA-F]{40}/) {
4851                         # try to find filename from $hash
4852
4853                         # read filtered raw output
4854                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4855                                 $hash_parent_base, $hash_base, "--"
4856                                 or die_error(500, "Open git-diff-tree failed");
4857                         @difftree =
4858                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
4859                                 # $hash == to_id
4860                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4861                                 map { chomp; $_ } <$fd>;
4862                         close $fd
4863                                 or die_error(404, "Reading git-diff-tree failed");
4864                         @difftree
4865                                 or die_error(404, "Blob diff not found");
4866
4867                 } else {
4868                         die_error(400, "Missing one of the blob diff parameters");
4869                 }
4870
4871                 if (@difftree > 1) {
4872                         die_error(400, "Ambiguous blob diff specification");
4873                 }
4874
4875                 %diffinfo = parse_difftree_raw_line($difftree[0]);
4876                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4877                 $file_name   ||= $diffinfo{'to_file'};
4878
4879                 $hash_parent ||= $diffinfo{'from_id'};
4880                 $hash        ||= $diffinfo{'to_id'};
4881
4882                 # non-textual hash id's can be cached
4883                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4884                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4885                         $expires = '+1d';
4886                 }
4887
4888                 # open patch output
4889                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4890                         '-p', ($format eq 'html' ? "--full-index" : ()),
4891                         $hash_parent_base, $hash_base,
4892                         "--", (defined $file_parent ? $file_parent : ()), $file_name
4893                         or die_error(500, "Open git-diff-tree failed");
4894         }
4895
4896         # old/legacy style URI
4897         if (!%diffinfo && # if new style URI failed
4898             defined $hash && defined $hash_parent) {
4899                 # fake git-diff-tree raw output
4900                 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4901                 $diffinfo{'from_id'} = $hash_parent;
4902                 $diffinfo{'to_id'}   = $hash;
4903                 if (defined $file_name) {
4904                         if (defined $file_parent) {
4905                                 $diffinfo{'status'} = '2';
4906                                 $diffinfo{'from_file'} = $file_parent;
4907                                 $diffinfo{'to_file'}   = $file_name;
4908                         } else { # assume not renamed
4909                                 $diffinfo{'status'} = '1';
4910                                 $diffinfo{'from_file'} = $file_name;
4911                                 $diffinfo{'to_file'}   = $file_name;
4912                         }
4913                 } else { # no filename given
4914                         $diffinfo{'status'} = '2';
4915                         $diffinfo{'from_file'} = $hash_parent;
4916                         $diffinfo{'to_file'}   = $hash;
4917                 }
4918
4919                 # non-textual hash id's can be cached
4920                 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4921                     $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4922                         $expires = '+1d';
4923                 }
4924
4925                 # open patch output
4926                 open $fd, "-|", git_cmd(), "diff", @diff_opts,
4927                         '-p', ($format eq 'html' ? "--full-index" : ()),
4928                         $hash_parent, $hash, "--"
4929                         or die_error(500, "Open git-diff failed");
4930         } else  {
4931                 die_error(400, "Missing one of the blob diff parameters")
4932                         unless %diffinfo;
4933         }
4934
4935         # header
4936         if ($format eq 'html') {
4937                 my $formats_nav =
4938                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
4939                                 "raw");
4940                 git_header_html(undef, $expires);
4941                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4942                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4943                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4944                 } else {
4945                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4946                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4947                 }
4948                 if (defined $file_name) {
4949                         git_print_page_path($file_name, "blob", $hash_base);
4950                 } else {
4951                         print "<div class=\"page_path\"></div>\n";
4952                 }
4953
4954         } elsif ($format eq 'plain') {
4955                 print $cgi->header(
4956                         -type => 'text/plain',
4957                         -charset => 'utf-8',
4958                         -expires => $expires,
4959                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
4960
4961                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4962
4963         } else {
4964                 die_error(400, "Unknown blobdiff format");
4965         }
4966
4967         # patch
4968         if ($format eq 'html') {
4969                 print "<div class=\"page_body\">\n";
4970
4971                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
4972                 close $fd;
4973
4974                 print "</div>\n"; # class="page_body"
4975                 git_footer_html();
4976
4977         } else {
4978                 while (my $line = <$fd>) {
4979                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
4980                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
4981
4982                         print $line;
4983
4984                         last if $line =~ m!^\+\+\+!;
4985                 }
4986                 local $/ = undef;
4987                 print <$fd>;
4988                 close $fd;
4989         }
4990 }
4991
4992 sub git_blobdiff_plain {
4993         git_blobdiff('plain');
4994 }
4995
4996 sub git_commitdiff {
4997         my $format = shift || 'html';
4998         $hash ||= $hash_base || "HEAD";
4999         my %co = parse_commit($hash)
5000             or die_error(404, "Unknown commit object");
5001
5002         # choose format for commitdiff for merge
5003         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5004                 $hash_parent = '--cc';
5005         }
5006         # we need to prepare $formats_nav before almost any parameter munging
5007         my $formats_nav;
5008         if ($format eq 'html') {
5009                 $formats_nav =
5010                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5011                                 "raw");
5012
5013                 if (defined $hash_parent &&
5014                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
5015                         # commitdiff with two commits given
5016                         my $hash_parent_short = $hash_parent;
5017                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5018                                 $hash_parent_short = substr($hash_parent, 0, 7);
5019                         }
5020                         $formats_nav .=
5021                                 ' (from';
5022                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5023                                 if ($co{'parents'}[$i] eq $hash_parent) {
5024                                         $formats_nav .= ' parent ' . ($i+1);
5025                                         last;
5026                                 }
5027                         }
5028                         $formats_nav .= ': ' .
5029                                 $cgi->a({-href => href(action=>"commitdiff",
5030                                                        hash=>$hash_parent)},
5031                                         esc_html($hash_parent_short)) .
5032                                 ')';
5033                 } elsif (!$co{'parent'}) {
5034                         # --root commitdiff
5035                         $formats_nav .= ' (initial)';
5036                 } elsif (scalar @{$co{'parents'}} == 1) {
5037                         # single parent commit
5038                         $formats_nav .=
5039                                 ' (parent: ' .
5040                                 $cgi->a({-href => href(action=>"commitdiff",
5041                                                        hash=>$co{'parent'})},
5042                                         esc_html(substr($co{'parent'}, 0, 7))) .
5043                                 ')';
5044                 } else {
5045                         # merge commit
5046                         if ($hash_parent eq '--cc') {
5047                                 $formats_nav .= ' | ' .
5048                                         $cgi->a({-href => href(action=>"commitdiff",
5049                                                                hash=>$hash, hash_parent=>'-c')},
5050                                                 'combined');
5051                         } else { # $hash_parent eq '-c'
5052                                 $formats_nav .= ' | ' .
5053                                         $cgi->a({-href => href(action=>"commitdiff",
5054                                                                hash=>$hash, hash_parent=>'--cc')},
5055                                                 'compact');
5056                         }
5057                         $formats_nav .=
5058                                 ' (merge: ' .
5059                                 join(' ', map {
5060                                         $cgi->a({-href => href(action=>"commitdiff",
5061                                                                hash=>$_)},
5062                                                 esc_html(substr($_, 0, 7)));
5063                                 } @{$co{'parents'}} ) .
5064                                 ')';
5065                 }
5066         }
5067
5068         my $hash_parent_param = $hash_parent;
5069         if (!defined $hash_parent_param) {
5070                 # --cc for multiple parents, --root for parentless
5071                 $hash_parent_param =
5072                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5073         }
5074
5075         # read commitdiff
5076         my $fd;
5077         my @difftree;
5078         if ($format eq 'html') {
5079                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5080                         "--no-commit-id", "--patch-with-raw", "--full-index",
5081                         $hash_parent_param, $hash, "--"
5082                         or die_error(500, "Open git-diff-tree failed");
5083
5084                 while (my $line = <$fd>) {
5085                         chomp $line;
5086                         # empty line ends raw part of diff-tree output
5087                         last unless $line;
5088                         push @difftree, scalar parse_difftree_raw_line($line);
5089                 }
5090
5091         } elsif ($format eq 'plain') {
5092                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5093                         '-p', $hash_parent_param, $hash, "--"
5094                         or die_error(500, "Open git-diff-tree failed");
5095
5096         } else {
5097                 die_error(400, "Unknown commitdiff format");
5098         }
5099
5100         # non-textual hash id's can be cached
5101         my $expires;
5102         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5103                 $expires = "+1d";
5104         }
5105
5106         # write commit message
5107         if ($format eq 'html') {
5108                 my $refs = git_get_references();
5109                 my $ref = format_ref_marker($refs, $co{'id'});
5110
5111                 git_header_html(undef, $expires);
5112                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5113                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5114                 git_print_authorship(\%co);
5115                 print "<div class=\"page_body\">\n";
5116                 if (@{$co{'comment'}} > 1) {
5117                         print "<div class=\"log\">\n";
5118                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5119                         print "</div>\n"; # class="log"
5120                 }
5121
5122         } elsif ($format eq 'plain') {
5123                 my $refs = git_get_references("tags");
5124                 my $tagname = git_get_rev_name_tags($hash);
5125                 my $filename = basename($project) . "-$hash.patch";
5126
5127                 print $cgi->header(
5128                         -type => 'text/plain',
5129                         -charset => 'utf-8',
5130                         -expires => $expires,
5131                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5132                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5133                 print "From: " . to_utf8($co{'author'}) . "\n";
5134                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5135                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5136
5137                 print "X-Git-Tag: $tagname\n" if $tagname;
5138                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5139
5140                 foreach my $line (@{$co{'comment'}}) {
5141                         print to_utf8($line) . "\n";
5142                 }
5143                 print "---\n\n";
5144         }
5145
5146         # write patch
5147         if ($format eq 'html') {
5148                 my $use_parents = !defined $hash_parent ||
5149                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5150                 git_difftree_body(\@difftree, $hash,
5151                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5152                 print "<br/>\n";
5153
5154                 git_patchset_body($fd, \@difftree, $hash,
5155                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5156                 close $fd;
5157                 print "</div>\n"; # class="page_body"
5158                 git_footer_html();
5159
5160         } elsif ($format eq 'plain') {
5161                 local $/ = undef;
5162                 print <$fd>;
5163                 close $fd
5164                         or print "Reading git-diff-tree failed\n";
5165         }
5166 }
5167
5168 sub git_commitdiff_plain {
5169         git_commitdiff('plain');
5170 }
5171
5172 sub git_history {
5173         if (!defined $hash_base) {
5174                 $hash_base = git_get_head_hash($project);
5175         }
5176         if (!defined $page) {
5177                 $page = 0;
5178         }
5179         my $ftype;
5180         my %co = parse_commit($hash_base)
5181             or die_error(404, "Unknown commit object");
5182
5183         my $refs = git_get_references();
5184         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5185
5186         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5187                                        $file_name, "--full-history")
5188             or die_error(404, "No such file or directory on given branch");
5189
5190         if (!defined $hash && defined $file_name) {
5191                 # some commits could have deleted file in question,
5192                 # and not have it in tree, but one of them has to have it
5193                 for (my $i = 0; $i <= @commitlist; $i++) {
5194                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5195                         last if defined $hash;
5196                 }
5197         }
5198         if (defined $hash) {
5199                 $ftype = git_get_type($hash);
5200         }
5201         if (!defined $ftype) {
5202                 die_error(500, "Unknown type of object");
5203         }
5204
5205         my $paging_nav = '';
5206         if ($page > 0) {
5207                 $paging_nav .=
5208                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5209                                                file_name=>$file_name)},
5210                                 "first");
5211                 $paging_nav .= " &sdot; " .
5212                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5213                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5214         } else {
5215                 $paging_nav .= "first";
5216                 $paging_nav .= " &sdot; prev";
5217         }
5218         my $next_link = '';
5219         if ($#commitlist >= 100) {
5220                 $next_link =
5221                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5222                                  -accesskey => "n", -title => "Alt-n"}, "next");
5223                 $paging_nav .= " &sdot; $next_link";
5224         } else {
5225                 $paging_nav .= " &sdot; next";
5226         }
5227
5228         git_header_html();
5229         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5230         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5231         git_print_page_path($file_name, $ftype, $hash_base);
5232
5233         git_history_body(\@commitlist, 0, 99,
5234                          $refs, $hash_base, $ftype, $next_link);
5235
5236         git_footer_html();
5237 }
5238
5239 sub git_search {
5240         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5241         if (!defined $searchtext) {
5242                 die_error(400, "Text field is empty");
5243         }
5244         if (!defined $hash) {
5245                 $hash = git_get_head_hash($project);
5246         }
5247         my %co = parse_commit($hash);
5248         if (!%co) {
5249                 die_error(404, "Unknown commit object");
5250         }
5251         if (!defined $page) {
5252                 $page = 0;
5253         }
5254
5255         $searchtype ||= 'commit';
5256         if ($searchtype eq 'pickaxe') {
5257                 # pickaxe may take all resources of your box and run for several minutes
5258                 # with every query - so decide by yourself how public you make this feature
5259                 gitweb_check_feature('pickaxe')
5260                     or die_error(403, "Pickaxe is disabled");
5261         }
5262         if ($searchtype eq 'grep') {
5263                 gitweb_check_feature('grep')
5264                     or die_error(403, "Grep is disabled");
5265         }
5266
5267         git_header_html();
5268
5269         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5270                 my $greptype;
5271                 if ($searchtype eq 'commit') {
5272                         $greptype = "--grep=";
5273                 } elsif ($searchtype eq 'author') {
5274                         $greptype = "--author=";
5275                 } elsif ($searchtype eq 'committer') {
5276                         $greptype = "--committer=";
5277                 }
5278                 $greptype .= $searchtext;
5279                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5280                                                $greptype, '--regexp-ignore-case',
5281                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5282
5283                 my $paging_nav = '';
5284                 if ($page > 0) {
5285                         $paging_nav .=
5286                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5287                                                        searchtext=>$searchtext,
5288                                                        searchtype=>$searchtype)},
5289                                         "first");
5290                         $paging_nav .= " &sdot; " .
5291                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5292                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5293                 } else {
5294                         $paging_nav .= "first";
5295                         $paging_nav .= " &sdot; prev";
5296                 }
5297                 my $next_link = '';
5298                 if ($#commitlist >= 100) {
5299                         $next_link =
5300                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5301                                          -accesskey => "n", -title => "Alt-n"}, "next");
5302                         $paging_nav .= " &sdot; $next_link";
5303                 } else {
5304                         $paging_nav .= " &sdot; next";
5305                 }
5306
5307                 if ($#commitlist >= 100) {
5308                 }
5309
5310                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5311                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5312                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5313         }
5314
5315         if ($searchtype eq 'pickaxe') {
5316                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5317                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5318
5319                 print "<table class=\"pickaxe search\">\n";
5320                 my $alternate = 1;
5321                 $/ = "\n";
5322                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5323                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5324                         ($search_use_regexp ? '--pickaxe-regex' : ());
5325                 undef %co;
5326                 my @files;
5327                 while (my $line = <$fd>) {
5328                         chomp $line;
5329                         next unless $line;
5330
5331                         my %set = parse_difftree_raw_line($line);
5332                         if (defined $set{'commit'}) {
5333                                 # finish previous commit
5334                                 if (%co) {
5335                                         print "</td>\n" .
5336                                               "<td class=\"link\">" .
5337                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5338                                               " | " .
5339                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5340                                         print "</td>\n" .
5341                                               "</tr>\n";
5342                                 }
5343
5344                                 if ($alternate) {
5345                                         print "<tr class=\"dark\">\n";
5346                                 } else {
5347                                         print "<tr class=\"light\">\n";
5348                                 }
5349                                 $alternate ^= 1;
5350                                 %co = parse_commit($set{'commit'});
5351                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5352                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5353                                       "<td><i>$author</i></td>\n" .
5354                                       "<td>" .
5355                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5356                                               -class => "list subject"},
5357                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5358                         } elsif (defined $set{'to_id'}) {
5359                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5360
5361                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5362                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5363                                               -class => "list"},
5364                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5365                                       "<br/>\n";
5366                         }
5367                 }
5368                 close $fd;
5369
5370                 # finish last commit (warning: repetition!)
5371                 if (%co) {
5372                         print "</td>\n" .
5373                               "<td class=\"link\">" .
5374                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5375                               " | " .
5376                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5377                         print "</td>\n" .
5378                               "</tr>\n";
5379                 }
5380
5381                 print "</table>\n";
5382         }
5383
5384         if ($searchtype eq 'grep') {
5385                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5386                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5387
5388                 print "<table class=\"grep_search\">\n";
5389                 my $alternate = 1;
5390                 my $matches = 0;
5391                 $/ = "\n";
5392                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5393                         $search_use_regexp ? ('-E', '-i') : '-F',
5394                         $searchtext, $co{'tree'};
5395                 my $lastfile = '';
5396                 while (my $line = <$fd>) {
5397                         chomp $line;
5398                         my ($file, $lno, $ltext, $binary);
5399                         last if ($matches++ > 1000);
5400                         if ($line =~ /^Binary file (.+) matches$/) {
5401                                 $file = $1;
5402                                 $binary = 1;
5403                         } else {
5404                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5405                         }
5406                         if ($file ne $lastfile) {
5407                                 $lastfile and print "</td></tr>\n";
5408                                 if ($alternate++) {
5409                                         print "<tr class=\"dark\">\n";
5410                                 } else {
5411                                         print "<tr class=\"light\">\n";
5412                                 }
5413                                 print "<td class=\"list\">".
5414                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5415                                                                file_name=>"$file"),
5416                                                 -class => "list"}, esc_path($file));
5417                                 print "</td><td>\n";
5418                                 $lastfile = $file;
5419                         }
5420                         if ($binary) {
5421                                 print "<div class=\"binary\">Binary file</div>\n";
5422                         } else {
5423                                 $ltext = untabify($ltext);
5424                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5425                                         $ltext = esc_html($1, -nbsp=>1);
5426                                         $ltext .= '<span class="match">';
5427                                         $ltext .= esc_html($2, -nbsp=>1);
5428                                         $ltext .= '</span>';
5429                                         $ltext .= esc_html($3, -nbsp=>1);
5430                                 } else {
5431                                         $ltext = esc_html($ltext, -nbsp=>1);
5432                                 }
5433                                 print "<div class=\"pre\">" .
5434                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5435                                                                file_name=>"$file").'#l'.$lno,
5436                                                 -class => "linenr"}, sprintf('%4i', $lno))
5437                                         . ' ' .  $ltext . "</div>\n";
5438                         }
5439                 }
5440                 if ($lastfile) {
5441                         print "</td></tr>\n";
5442                         if ($matches > 1000) {
5443                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5444                         }
5445                 } else {
5446                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5447                 }
5448                 close $fd;
5449
5450                 print "</table>\n";
5451         }
5452         git_footer_html();
5453 }
5454
5455 sub git_search_help {
5456         git_header_html();
5457         git_print_page_nav('','', $hash,$hash,$hash);
5458         print <<EOT;
5459 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5460 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5461 the pattern entered is recognized as the POSIX extended
5462 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5463 insensitive).</p>
5464 <dl>
5465 <dt><b>commit</b></dt>
5466 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5467 EOT
5468         my ($have_grep) = gitweb_check_feature('grep');
5469         if ($have_grep) {
5470                 print <<EOT;
5471 <dt><b>grep</b></dt>
5472 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5473     a different one) are searched for the given pattern. On large trees, this search can take
5474 a while and put some strain on the server, so please use it with some consideration. Note that
5475 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5476 case-sensitive.</dd>
5477 EOT
5478         }
5479         print <<EOT;
5480 <dt><b>author</b></dt>
5481 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5482 <dt><b>committer</b></dt>
5483 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5484 EOT
5485         my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5486         if ($have_pickaxe) {
5487                 print <<EOT;
5488 <dt><b>pickaxe</b></dt>
5489 <dd>All commits that caused the string to appear or disappear from any file (changes that
5490 added, removed or "modified" the string) will be listed. This search can take a while and
5491 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5492 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5493 EOT
5494         }
5495         print "</dl>\n";
5496         git_footer_html();
5497 }
5498
5499 sub git_shortlog {
5500         my $head = git_get_head_hash($project);
5501         if (!defined $hash) {
5502                 $hash = $head;
5503         }
5504         if (!defined $page) {
5505                 $page = 0;
5506         }
5507         my $refs = git_get_references();
5508
5509         my $commit_hash = $hash;
5510         if (defined $hash_parent) {
5511                 $commit_hash = "$hash_parent..$hash";
5512         }
5513         my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5514
5515         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5516         my $next_link = '';
5517         if ($#commitlist >= 100) {
5518                 $next_link =
5519                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5520                                  -accesskey => "n", -title => "Alt-n"}, "next");
5521         }
5522
5523         git_header_html();
5524         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5525         git_print_header_div('summary', $project);
5526
5527         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5528
5529         git_footer_html();
5530 }
5531
5532 ## ......................................................................
5533 ## feeds (RSS, Atom; OPML)
5534
5535 sub git_feed {
5536         my $format = shift || 'atom';
5537         my ($have_blame) = gitweb_check_feature('blame');
5538
5539         # Atom: http://www.atomenabled.org/developers/syndication/
5540         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5541         if ($format ne 'rss' && $format ne 'atom') {
5542                 die_error(400, "Unknown web feed format");
5543         }
5544
5545         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5546         my $head = $hash || 'HEAD';
5547         my @commitlist = parse_commits($head, 150, 0, $file_name);
5548
5549         my %latest_commit;
5550         my %latest_date;
5551         my $content_type = "application/$format+xml";
5552         if (defined $cgi->http('HTTP_ACCEPT') &&
5553                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5554                 # browser (feed reader) prefers text/xml
5555                 $content_type = 'text/xml';
5556         }
5557         if (defined($commitlist[0])) {
5558                 %latest_commit = %{$commitlist[0]};
5559                 %latest_date   = parse_date($latest_commit{'author_epoch'});
5560                 print $cgi->header(
5561                         -type => $content_type,
5562                         -charset => 'utf-8',
5563                         -last_modified => $latest_date{'rfc2822'});
5564         } else {
5565                 print $cgi->header(
5566                         -type => $content_type,
5567                         -charset => 'utf-8');
5568         }
5569
5570         # Optimization: skip generating the body if client asks only
5571         # for Last-Modified date.
5572         return if ($cgi->request_method() eq 'HEAD');
5573
5574         # header variables
5575         my $title = "$site_name - $project/$action";
5576         my $feed_type = 'log';
5577         if (defined $hash) {
5578                 $title .= " - '$hash'";
5579                 $feed_type = 'branch log';
5580                 if (defined $file_name) {
5581                         $title .= " :: $file_name";
5582                         $feed_type = 'history';
5583                 }
5584         } elsif (defined $file_name) {
5585                 $title .= " - $file_name";
5586                 $feed_type = 'history';
5587         }
5588         $title .= " $feed_type";
5589         my $descr = git_get_project_description($project);
5590         if (defined $descr) {
5591                 $descr = esc_html($descr);
5592         } else {
5593                 $descr = "$project " .
5594                          ($format eq 'rss' ? 'RSS' : 'Atom') .
5595                          " feed";
5596         }
5597         my $owner = git_get_project_owner($project);
5598         $owner = esc_html($owner);
5599
5600         #header
5601         my $alt_url;
5602         if (defined $file_name) {
5603                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5604         } elsif (defined $hash) {
5605                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5606         } else {
5607                 $alt_url = href(-full=>1, action=>"summary");
5608         }
5609         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5610         if ($format eq 'rss') {
5611                 print <<XML;
5612 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5613 <channel>
5614 XML
5615                 print "<title>$title</title>\n" .
5616                       "<link>$alt_url</link>\n" .
5617                       "<description>$descr</description>\n" .
5618                       "<language>en</language>\n";
5619         } elsif ($format eq 'atom') {
5620                 print <<XML;
5621 <feed xmlns="http://www.w3.org/2005/Atom">
5622 XML
5623                 print "<title>$title</title>\n" .
5624                       "<subtitle>$descr</subtitle>\n" .
5625                       '<link rel="alternate" type="text/html" href="' .
5626                       $alt_url . '" />' . "\n" .
5627                       '<link rel="self" type="' . $content_type . '" href="' .
5628                       $cgi->self_url() . '" />' . "\n" .
5629                       "<id>" . href(-full=>1) . "</id>\n" .
5630                       # use project owner for feed author
5631                       "<author><name>$owner</name></author>\n";
5632                 if (defined $favicon) {
5633                         print "<icon>" . esc_url($favicon) . "</icon>\n";
5634                 }
5635                 if (defined $logo_url) {
5636                         # not twice as wide as tall: 72 x 27 pixels
5637                         print "<logo>" . esc_url($logo) . "</logo>\n";
5638                 }
5639                 if (! %latest_date) {
5640                         # dummy date to keep the feed valid until commits trickle in:
5641                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
5642                 } else {
5643                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
5644                 }
5645         }
5646
5647         # contents
5648         for (my $i = 0; $i <= $#commitlist; $i++) {
5649                 my %co = %{$commitlist[$i]};
5650                 my $commit = $co{'id'};
5651                 # we read 150, we always show 30 and the ones more recent than 48 hours
5652                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5653                         last;
5654                 }
5655                 my %cd = parse_date($co{'author_epoch'});
5656
5657                 # get list of changed files
5658                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5659                         $co{'parent'} || "--root",
5660                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
5661                         or next;
5662                 my @difftree = map { chomp; $_ } <$fd>;
5663                 close $fd
5664                         or next;
5665
5666                 # print element (entry, item)
5667                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5668                 if ($format eq 'rss') {
5669                         print "<item>\n" .
5670                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
5671                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
5672                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5673                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5674                               "<link>$co_url</link>\n" .
5675                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
5676                               "<content:encoded>" .
5677                               "<![CDATA[\n";
5678                 } elsif ($format eq 'atom') {
5679                         print "<entry>\n" .
5680                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5681                               "<updated>$cd{'iso-8601'}</updated>\n" .
5682                               "<author>\n" .
5683                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5684                         if ($co{'author_email'}) {
5685                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5686                         }
5687                         print "</author>\n" .
5688                               # use committer for contributor
5689                               "<contributor>\n" .
5690                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5691                         if ($co{'committer_email'}) {
5692                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5693                         }
5694                         print "</contributor>\n" .
5695                               "<published>$cd{'iso-8601'}</published>\n" .
5696                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5697                               "<id>$co_url</id>\n" .
5698                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5699                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5700                 }
5701                 my $comment = $co{'comment'};
5702                 print "<pre>\n";
5703                 foreach my $line (@$comment) {
5704                         $line = esc_html($line);
5705                         print "$line\n";
5706                 }
5707                 print "</pre><ul>\n";
5708                 foreach my $difftree_line (@difftree) {
5709                         my %difftree = parse_difftree_raw_line($difftree_line);
5710                         next if !$difftree{'from_id'};
5711
5712                         my $file = $difftree{'file'} || $difftree{'to_file'};
5713
5714                         print "<li>" .
5715                               "[" .
5716                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5717                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5718                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5719                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
5720                                       -title => "diff"}, 'D');
5721                         if ($have_blame) {
5722                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
5723                                                              file_name=>$file, hash_base=>$commit),
5724                                               -title => "blame"}, 'B');
5725                         }
5726                         # if this is not a feed of a file history
5727                         if (!defined $file_name || $file_name ne $file) {
5728                                 print $cgi->a({-href => href(-full=>1, action=>"history",
5729                                                              file_name=>$file, hash=>$commit),
5730                                               -title => "history"}, 'H');
5731                         }
5732                         $file = esc_path($file);
5733                         print "] ".
5734                               "$file</li>\n";
5735                 }
5736                 if ($format eq 'rss') {
5737                         print "</ul>]]>\n" .
5738                               "</content:encoded>\n" .
5739                               "</item>\n";
5740                 } elsif ($format eq 'atom') {
5741                         print "</ul>\n</div>\n" .
5742                               "</content>\n" .
5743                               "</entry>\n";
5744                 }
5745         }
5746
5747         # end of feed
5748         if ($format eq 'rss') {
5749                 print "</channel>\n</rss>\n";
5750         }       elsif ($format eq 'atom') {
5751                 print "</feed>\n";
5752         }
5753 }
5754
5755 sub git_rss {
5756         git_feed('rss');
5757 }
5758
5759 sub git_atom {
5760         git_feed('atom');
5761 }
5762
5763 sub git_opml {
5764         my @list = git_get_projects_list();
5765
5766         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5767         print <<XML;
5768 <?xml version="1.0" encoding="utf-8"?>
5769 <opml version="1.0">
5770 <head>
5771   <title>$site_name OPML Export</title>
5772 </head>
5773 <body>
5774 <outline text="git RSS feeds">
5775 XML
5776
5777         foreach my $pr (@list) {
5778                 my %proj = %$pr;
5779                 my $head = git_get_head_hash($proj{'path'});
5780                 if (!defined $head) {
5781                         next;
5782                 }
5783                 $git_dir = "$projectroot/$proj{'path'}";
5784                 my %co = parse_commit($head);
5785                 if (!%co) {
5786                         next;
5787                 }
5788
5789                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5790                 my $rss  = "$my_url?p=$proj{'path'};a=rss";
5791                 my $html = "$my_url?p=$proj{'path'};a=summary";
5792                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5793         }
5794         print <<XML;
5795 </outline>
5796 </body>
5797 </opml>
5798 XML
5799 }