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