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