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