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