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