Merge branch 'mm/maint-hint-failed-merge' 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=\"" . href(-replay => 1)
5068                                 . "#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5069                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5070                 }
5071         }
5072         close $fd
5073                 or print "Reading blob failed.\n";
5074         print "</div>";
5075         git_footer_html();
5076 }
5077
5078 sub git_tree {
5079         if (!defined $hash_base) {
5080                 $hash_base = "HEAD";
5081         }
5082         if (!defined $hash) {
5083                 if (defined $file_name) {
5084                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5085                 } else {
5086                         $hash = $hash_base;
5087                 }
5088         }
5089         die_error(404, "No such tree") unless defined($hash);
5090
5091         my @entries = ();
5092         {
5093                 local $/ = "\0";
5094                 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
5095                         or die_error(500, "Open git-ls-tree failed");
5096                 @entries = map { chomp; $_ } <$fd>;
5097                 close $fd
5098                         or die_error(404, "Reading tree failed");
5099         }
5100
5101         my $refs = git_get_references();
5102         my $ref = format_ref_marker($refs, $hash_base);
5103         git_header_html();
5104         my $basedir = '';
5105         my $have_blame = gitweb_check_feature('blame');
5106         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5107                 my @views_nav = ();
5108                 if (defined $file_name) {
5109                         push @views_nav,
5110                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
5111                                         "history"),
5112                                 $cgi->a({-href => href(action=>"tree",
5113                                                        hash_base=>"HEAD", file_name=>$file_name)},
5114                                         "HEAD"),
5115                 }
5116                 my $snapshot_links = format_snapshot_links($hash);
5117                 if (defined $snapshot_links) {
5118                         # FIXME: Should be available when we have no hash base as well.
5119                         push @views_nav, $snapshot_links;
5120                 }
5121                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
5122                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5123         } else {
5124                 undef $hash_base;
5125                 print "<div class=\"page_nav\">\n";
5126                 print "<br/><br/></div>\n";
5127                 print "<div class=\"title\">$hash</div>\n";
5128         }
5129         if (defined $file_name) {
5130                 $basedir = $file_name;
5131                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5132                         $basedir .= '/';
5133                 }
5134                 git_print_page_path($file_name, 'tree', $hash_base);
5135         }
5136         print "<div class=\"page_body\">\n";
5137         print "<table class=\"tree\">\n";
5138         my $alternate = 1;
5139         # '..' (top directory) link if possible
5140         if (defined $hash_base &&
5141             defined $file_name && $file_name =~ m![^/]+$!) {
5142                 if ($alternate) {
5143                         print "<tr class=\"dark\">\n";
5144                 } else {
5145                         print "<tr class=\"light\">\n";
5146                 }
5147                 $alternate ^= 1;
5148
5149                 my $up = $file_name;
5150                 $up =~ s!/?[^/]+$!!;
5151                 undef $up unless $up;
5152                 # based on git_print_tree_entry
5153                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5154                 print '<td class="list">';
5155                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
5156                                              file_name=>$up)},
5157                               "..");
5158                 print "</td>\n";
5159                 print "<td class=\"link\"></td>\n";
5160
5161                 print "</tr>\n";
5162         }
5163         foreach my $line (@entries) {
5164                 my %t = parse_ls_tree_line($line, -z => 1);
5165
5166                 if ($alternate) {
5167                         print "<tr class=\"dark\">\n";
5168                 } else {
5169                         print "<tr class=\"light\">\n";
5170                 }
5171                 $alternate ^= 1;
5172
5173                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5174
5175                 print "</tr>\n";
5176         }
5177         print "</table>\n" .
5178               "</div>";
5179         git_footer_html();
5180 }
5181
5182 sub git_snapshot {
5183         my $format = $input_params{'snapshot_format'};
5184         if (!@snapshot_fmts) {
5185                 die_error(403, "Snapshots not allowed");
5186         }
5187         # default to first supported snapshot format
5188         $format ||= $snapshot_fmts[0];
5189         if ($format !~ m/^[a-z0-9]+$/) {
5190                 die_error(400, "Invalid snapshot format parameter");
5191         } elsif (!exists($known_snapshot_formats{$format})) {
5192                 die_error(400, "Unknown snapshot format");
5193         } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5194                 die_error(403, "Snapshot format not allowed");
5195         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5196                 die_error(403, "Unsupported snapshot format");
5197         }
5198
5199         if (!defined $hash) {
5200                 $hash = git_get_head_hash($project);
5201         }
5202
5203         my $name = $project;
5204         $name =~ s,([^/])/*\.git$,$1,;
5205         $name = basename($name);
5206         my $filename = to_utf8($name);
5207         $name =~ s/\047/\047\\\047\047/g;
5208         my $cmd;
5209         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5210         $cmd = quote_command(
5211                 git_cmd(), 'archive',
5212                 "--format=$known_snapshot_formats{$format}{'format'}",
5213                 "--prefix=$name/", $hash);
5214         if (exists $known_snapshot_formats{$format}{'compressor'}) {
5215                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5216         }
5217
5218         print $cgi->header(
5219                 -type => $known_snapshot_formats{$format}{'type'},
5220                 -content_disposition => 'inline; filename="' . "$filename" . '"',
5221                 -status => '200 OK');
5222
5223         open my $fd, "-|", $cmd
5224                 or die_error(500, "Execute git-archive failed");
5225         binmode STDOUT, ':raw';
5226         print <$fd>;
5227         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5228         close $fd;
5229 }
5230
5231 sub git_log {
5232         my $head = git_get_head_hash($project);
5233         if (!defined $hash) {
5234                 $hash = $head;
5235         }
5236         if (!defined $page) {
5237                 $page = 0;
5238         }
5239         my $refs = git_get_references();
5240
5241         my @commitlist = parse_commits($hash, 101, (100 * $page));
5242
5243         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5244
5245         my ($patch_max) = gitweb_get_feature('patches');
5246         if ($patch_max) {
5247                 if ($patch_max < 0 || @commitlist <= $patch_max) {
5248                         $paging_nav .= " &sdot; " .
5249                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5250                                         "patches");
5251                 }
5252         }
5253
5254         git_header_html();
5255         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5256
5257         if (!@commitlist) {
5258                 my %co = parse_commit($hash);
5259
5260                 git_print_header_div('summary', $project);
5261                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5262         }
5263         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5264         for (my $i = 0; $i <= $to; $i++) {
5265                 my %co = %{$commitlist[$i]};
5266                 next if !%co;
5267                 my $commit = $co{'id'};
5268                 my $ref = format_ref_marker($refs, $commit);
5269                 my %ad = parse_date($co{'author_epoch'});
5270                 git_print_header_div('commit',
5271                                "<span class=\"age\">$co{'age_string'}</span>" .
5272                                esc_html($co{'title'}) . $ref,
5273                                $commit);
5274                 print "<div class=\"title_text\">\n" .
5275                       "<div class=\"log_link\">\n" .
5276                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5277                       " | " .
5278                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5279                       " | " .
5280                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5281                       "<br/>\n" .
5282                       "</div>\n";
5283                       git_print_authorship(\%co, -tag => 'span');
5284                       print "<br/>\n</div>\n";
5285
5286                 print "<div class=\"log_body\">\n";
5287                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5288                 print "</div>\n";
5289         }
5290         if ($#commitlist >= 100) {
5291                 print "<div class=\"page_nav\">\n";
5292                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5293                                -accesskey => "n", -title => "Alt-n"}, "next");
5294                 print "</div>\n";
5295         }
5296         git_footer_html();
5297 }
5298
5299 sub git_commit {
5300         $hash ||= $hash_base || "HEAD";
5301         my %co = parse_commit($hash)
5302             or die_error(404, "Unknown commit object");
5303
5304         my $parent  = $co{'parent'};
5305         my $parents = $co{'parents'}; # listref
5306
5307         # we need to prepare $formats_nav before any parameter munging
5308         my $formats_nav;
5309         if (!defined $parent) {
5310                 # --root commitdiff
5311                 $formats_nav .= '(initial)';
5312         } elsif (@$parents == 1) {
5313                 # single parent commit
5314                 $formats_nav .=
5315                         '(parent: ' .
5316                         $cgi->a({-href => href(action=>"commit",
5317                                                hash=>$parent)},
5318                                 esc_html(substr($parent, 0, 7))) .
5319                         ')';
5320         } else {
5321                 # merge commit
5322                 $formats_nav .=
5323                         '(merge: ' .
5324                         join(' ', map {
5325                                 $cgi->a({-href => href(action=>"commit",
5326                                                        hash=>$_)},
5327                                         esc_html(substr($_, 0, 7)));
5328                         } @$parents ) .
5329                         ')';
5330         }
5331         if (gitweb_check_feature('patches') && @$parents <= 1) {
5332                 $formats_nav .= " | " .
5333                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
5334                                 "patch");
5335         }
5336
5337         if (!defined $parent) {
5338                 $parent = "--root";
5339         }
5340         my @difftree;
5341         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5342                 @diff_opts,
5343                 (@$parents <= 1 ? $parent : '-c'),
5344                 $hash, "--"
5345                 or die_error(500, "Open git-diff-tree failed");
5346         @difftree = map { chomp; $_ } <$fd>;
5347         close $fd or die_error(404, "Reading git-diff-tree failed");
5348
5349         # non-textual hash id's can be cached
5350         my $expires;
5351         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5352                 $expires = "+1d";
5353         }
5354         my $refs = git_get_references();
5355         my $ref = format_ref_marker($refs, $co{'id'});
5356
5357         git_header_html(undef, $expires);
5358         git_print_page_nav('commit', '',
5359                            $hash, $co{'tree'}, $hash,
5360                            $formats_nav);
5361
5362         if (defined $co{'parent'}) {
5363                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5364         } else {
5365                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5366         }
5367         print "<div class=\"title_text\">\n" .
5368               "<table class=\"object_header\">\n";
5369         git_print_authorship_rows(\%co);
5370         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5371         print "<tr>" .
5372               "<td>tree</td>" .
5373               "<td class=\"sha1\">" .
5374               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5375                        class => "list"}, $co{'tree'}) .
5376               "</td>" .
5377               "<td class=\"link\">" .
5378               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5379                       "tree");
5380         my $snapshot_links = format_snapshot_links($hash);
5381         if (defined $snapshot_links) {
5382                 print " | " . $snapshot_links;
5383         }
5384         print "</td>" .
5385               "</tr>\n";
5386
5387         foreach my $par (@$parents) {
5388                 print "<tr>" .
5389                       "<td>parent</td>" .
5390                       "<td class=\"sha1\">" .
5391                       $cgi->a({-href => href(action=>"commit", hash=>$par),
5392                                class => "list"}, $par) .
5393                       "</td>" .
5394                       "<td class=\"link\">" .
5395                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5396                       " | " .
5397                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5398                       "</td>" .
5399                       "</tr>\n";
5400         }
5401         print "</table>".
5402               "</div>\n";
5403
5404         print "<div class=\"page_body\">\n";
5405         git_print_log($co{'comment'});
5406         print "</div>\n";
5407
5408         git_difftree_body(\@difftree, $hash, @$parents);
5409
5410         git_footer_html();
5411 }
5412
5413 sub git_object {
5414         # object is defined by:
5415         # - hash or hash_base alone
5416         # - hash_base and file_name
5417         my $type;
5418
5419         # - hash or hash_base alone
5420         if ($hash || ($hash_base && !defined $file_name)) {
5421                 my $object_id = $hash || $hash_base;
5422
5423                 open my $fd, "-|", quote_command(
5424                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5425                         or die_error(404, "Object does not exist");
5426                 $type = <$fd>;
5427                 chomp $type;
5428                 close $fd
5429                         or die_error(404, "Object does not exist");
5430
5431         # - hash_base and file_name
5432         } elsif ($hash_base && defined $file_name) {
5433                 $file_name =~ s,/+$,,;
5434
5435                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5436                         or die_error(404, "Base object does not exist");
5437
5438                 # here errors should not hapen
5439                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5440                         or die_error(500, "Open git-ls-tree failed");
5441                 my $line = <$fd>;
5442                 close $fd;
5443
5444                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5445                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5446                         die_error(404, "File or directory for given base does not exist");
5447                 }
5448                 $type = $2;
5449                 $hash = $3;
5450         } else {
5451                 die_error(400, "Not enough information to find object");
5452         }
5453
5454         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5455                                           hash=>$hash, hash_base=>$hash_base,
5456                                           file_name=>$file_name),
5457                              -status => '302 Found');
5458 }
5459
5460 sub git_blobdiff {
5461         my $format = shift || 'html';
5462
5463         my $fd;
5464         my @difftree;
5465         my %diffinfo;
5466         my $expires;
5467
5468         # preparing $fd and %diffinfo for git_patchset_body
5469         # new style URI
5470         if (defined $hash_base && defined $hash_parent_base) {
5471                 if (defined $file_name) {
5472                         # read raw output
5473                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5474                                 $hash_parent_base, $hash_base,
5475                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
5476                                 or die_error(500, "Open git-diff-tree failed");
5477                         @difftree = map { chomp; $_ } <$fd>;
5478                         close $fd
5479                                 or die_error(404, "Reading git-diff-tree failed");
5480                         @difftree
5481                                 or die_error(404, "Blob diff not found");
5482
5483                 } elsif (defined $hash &&
5484                          $hash =~ /[0-9a-fA-F]{40}/) {
5485                         # try to find filename from $hash
5486
5487                         # read filtered raw output
5488                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5489                                 $hash_parent_base, $hash_base, "--"
5490                                 or die_error(500, "Open git-diff-tree failed");
5491                         @difftree =
5492                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5493                                 # $hash == to_id
5494                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5495                                 map { chomp; $_ } <$fd>;
5496                         close $fd
5497                                 or die_error(404, "Reading git-diff-tree failed");
5498                         @difftree
5499                                 or die_error(404, "Blob diff not found");
5500
5501                 } else {
5502                         die_error(400, "Missing one of the blob diff parameters");
5503                 }
5504
5505                 if (@difftree > 1) {
5506                         die_error(400, "Ambiguous blob diff specification");
5507                 }
5508
5509                 %diffinfo = parse_difftree_raw_line($difftree[0]);
5510                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5511                 $file_name   ||= $diffinfo{'to_file'};
5512
5513                 $hash_parent ||= $diffinfo{'from_id'};
5514                 $hash        ||= $diffinfo{'to_id'};
5515
5516                 # non-textual hash id's can be cached
5517                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5518                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5519                         $expires = '+1d';
5520                 }
5521
5522                 # open patch output
5523                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5524                         '-p', ($format eq 'html' ? "--full-index" : ()),
5525                         $hash_parent_base, $hash_base,
5526                         "--", (defined $file_parent ? $file_parent : ()), $file_name
5527                         or die_error(500, "Open git-diff-tree failed");
5528         }
5529
5530         # old/legacy style URI -- not generated anymore since 1.4.3.
5531         if (!%diffinfo) {
5532                 die_error('404 Not Found', "Missing one of the blob diff parameters")
5533         }
5534
5535         # header
5536         if ($format eq 'html') {
5537                 my $formats_nav =
5538                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5539                                 "raw");
5540                 git_header_html(undef, $expires);
5541                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5542                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5543                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5544                 } else {
5545                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5546                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5547                 }
5548                 if (defined $file_name) {
5549                         git_print_page_path($file_name, "blob", $hash_base);
5550                 } else {
5551                         print "<div class=\"page_path\"></div>\n";
5552                 }
5553
5554         } elsif ($format eq 'plain') {
5555                 print $cgi->header(
5556                         -type => 'text/plain',
5557                         -charset => 'utf-8',
5558                         -expires => $expires,
5559                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5560
5561                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5562
5563         } else {
5564                 die_error(400, "Unknown blobdiff format");
5565         }
5566
5567         # patch
5568         if ($format eq 'html') {
5569                 print "<div class=\"page_body\">\n";
5570
5571                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5572                 close $fd;
5573
5574                 print "</div>\n"; # class="page_body"
5575                 git_footer_html();
5576
5577         } else {
5578                 while (my $line = <$fd>) {
5579                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5580                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5581
5582                         print $line;
5583
5584                         last if $line =~ m!^\+\+\+!;
5585                 }
5586                 local $/ = undef;
5587                 print <$fd>;
5588                 close $fd;
5589         }
5590 }
5591
5592 sub git_blobdiff_plain {
5593         git_blobdiff('plain');
5594 }
5595
5596 sub git_commitdiff {
5597         my %params = @_;
5598         my $format = $params{-format} || 'html';
5599
5600         my ($patch_max) = gitweb_get_feature('patches');
5601         if ($format eq 'patch') {
5602                 die_error(403, "Patch view not allowed") unless $patch_max;
5603         }
5604
5605         $hash ||= $hash_base || "HEAD";
5606         my %co = parse_commit($hash)
5607             or die_error(404, "Unknown commit object");
5608
5609         # choose format for commitdiff for merge
5610         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5611                 $hash_parent = '--cc';
5612         }
5613         # we need to prepare $formats_nav before almost any parameter munging
5614         my $formats_nav;
5615         if ($format eq 'html') {
5616                 $formats_nav =
5617                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5618                                 "raw");
5619                 if ($patch_max && @{$co{'parents'}} <= 1) {
5620                         $formats_nav .= " | " .
5621                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5622                                         "patch");
5623                 }
5624
5625                 if (defined $hash_parent &&
5626                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
5627                         # commitdiff with two commits given
5628                         my $hash_parent_short = $hash_parent;
5629                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5630                                 $hash_parent_short = substr($hash_parent, 0, 7);
5631                         }
5632                         $formats_nav .=
5633                                 ' (from';
5634                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5635                                 if ($co{'parents'}[$i] eq $hash_parent) {
5636                                         $formats_nav .= ' parent ' . ($i+1);
5637                                         last;
5638                                 }
5639                         }
5640                         $formats_nav .= ': ' .
5641                                 $cgi->a({-href => href(action=>"commitdiff",
5642                                                        hash=>$hash_parent)},
5643                                         esc_html($hash_parent_short)) .
5644                                 ')';
5645                 } elsif (!$co{'parent'}) {
5646                         # --root commitdiff
5647                         $formats_nav .= ' (initial)';
5648                 } elsif (scalar @{$co{'parents'}} == 1) {
5649                         # single parent commit
5650                         $formats_nav .=
5651                                 ' (parent: ' .
5652                                 $cgi->a({-href => href(action=>"commitdiff",
5653                                                        hash=>$co{'parent'})},
5654                                         esc_html(substr($co{'parent'}, 0, 7))) .
5655                                 ')';
5656                 } else {
5657                         # merge commit
5658                         if ($hash_parent eq '--cc') {
5659                                 $formats_nav .= ' | ' .
5660                                         $cgi->a({-href => href(action=>"commitdiff",
5661                                                                hash=>$hash, hash_parent=>'-c')},
5662                                                 'combined');
5663                         } else { # $hash_parent eq '-c'
5664                                 $formats_nav .= ' | ' .
5665                                         $cgi->a({-href => href(action=>"commitdiff",
5666                                                                hash=>$hash, hash_parent=>'--cc')},
5667                                                 'compact');
5668                         }
5669                         $formats_nav .=
5670                                 ' (merge: ' .
5671                                 join(' ', map {
5672                                         $cgi->a({-href => href(action=>"commitdiff",
5673                                                                hash=>$_)},
5674                                                 esc_html(substr($_, 0, 7)));
5675                                 } @{$co{'parents'}} ) .
5676                                 ')';
5677                 }
5678         }
5679
5680         my $hash_parent_param = $hash_parent;
5681         if (!defined $hash_parent_param) {
5682                 # --cc for multiple parents, --root for parentless
5683                 $hash_parent_param =
5684                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5685         }
5686
5687         # read commitdiff
5688         my $fd;
5689         my @difftree;
5690         if ($format eq 'html') {
5691                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5692                         "--no-commit-id", "--patch-with-raw", "--full-index",
5693                         $hash_parent_param, $hash, "--"
5694                         or die_error(500, "Open git-diff-tree failed");
5695
5696                 while (my $line = <$fd>) {
5697                         chomp $line;
5698                         # empty line ends raw part of diff-tree output
5699                         last unless $line;
5700                         push @difftree, scalar parse_difftree_raw_line($line);
5701                 }
5702
5703         } elsif ($format eq 'plain') {
5704                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5705                         '-p', $hash_parent_param, $hash, "--"
5706                         or die_error(500, "Open git-diff-tree failed");
5707         } elsif ($format eq 'patch') {
5708                 # For commit ranges, we limit the output to the number of
5709                 # patches specified in the 'patches' feature.
5710                 # For single commits, we limit the output to a single patch,
5711                 # diverging from the git-format-patch default.
5712                 my @commit_spec = ();
5713                 if ($hash_parent) {
5714                         if ($patch_max > 0) {
5715                                 push @commit_spec, "-$patch_max";
5716                         }
5717                         push @commit_spec, '-n', "$hash_parent..$hash";
5718                 } else {
5719                         if ($params{-single}) {
5720                                 push @commit_spec, '-1';
5721                         } else {
5722                                 if ($patch_max > 0) {
5723                                         push @commit_spec, "-$patch_max";
5724                                 }
5725                                 push @commit_spec, "-n";
5726                         }
5727                         push @commit_spec, '--root', $hash;
5728                 }
5729                 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5730                         '--stdout', @commit_spec
5731                         or die_error(500, "Open git-format-patch failed");
5732         } else {
5733                 die_error(400, "Unknown commitdiff format");
5734         }
5735
5736         # non-textual hash id's can be cached
5737         my $expires;
5738         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5739                 $expires = "+1d";
5740         }
5741
5742         # write commit message
5743         if ($format eq 'html') {
5744                 my $refs = git_get_references();
5745                 my $ref = format_ref_marker($refs, $co{'id'});
5746
5747                 git_header_html(undef, $expires);
5748                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5749                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5750                 print "<div class=\"title_text\">\n" .
5751                       "<table class=\"object_header\">\n";
5752                 git_print_authorship_rows(\%co);
5753                 print "</table>".
5754                       "</div>\n";
5755                 print "<div class=\"page_body\">\n";
5756                 if (@{$co{'comment'}} > 1) {
5757                         print "<div class=\"log\">\n";
5758                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5759                         print "</div>\n"; # class="log"
5760                 }
5761
5762         } elsif ($format eq 'plain') {
5763                 my $refs = git_get_references("tags");
5764                 my $tagname = git_get_rev_name_tags($hash);
5765                 my $filename = basename($project) . "-$hash.patch";
5766
5767                 print $cgi->header(
5768                         -type => 'text/plain',
5769                         -charset => 'utf-8',
5770                         -expires => $expires,
5771                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5772                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5773                 print "From: " . to_utf8($co{'author'}) . "\n";
5774                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5775                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5776
5777                 print "X-Git-Tag: $tagname\n" if $tagname;
5778                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5779
5780                 foreach my $line (@{$co{'comment'}}) {
5781                         print to_utf8($line) . "\n";
5782                 }
5783                 print "---\n\n";
5784         } elsif ($format eq 'patch') {
5785                 my $filename = basename($project) . "-$hash.patch";
5786
5787                 print $cgi->header(
5788                         -type => 'text/plain',
5789                         -charset => 'utf-8',
5790                         -expires => $expires,
5791                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5792         }
5793
5794         # write patch
5795         if ($format eq 'html') {
5796                 my $use_parents = !defined $hash_parent ||
5797                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5798                 git_difftree_body(\@difftree, $hash,
5799                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5800                 print "<br/>\n";
5801
5802                 git_patchset_body($fd, \@difftree, $hash,
5803                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5804                 close $fd;
5805                 print "</div>\n"; # class="page_body"
5806                 git_footer_html();
5807
5808         } elsif ($format eq 'plain') {
5809                 local $/ = undef;
5810                 print <$fd>;
5811                 close $fd
5812                         or print "Reading git-diff-tree failed\n";
5813         } elsif ($format eq 'patch') {
5814                 local $/ = undef;
5815                 print <$fd>;
5816                 close $fd
5817                         or print "Reading git-format-patch failed\n";
5818         }
5819 }
5820
5821 sub git_commitdiff_plain {
5822         git_commitdiff(-format => 'plain');
5823 }
5824
5825 # format-patch-style patches
5826 sub git_patch {
5827         git_commitdiff(-format => 'patch', -single => 1);
5828 }
5829
5830 sub git_patches {
5831         git_commitdiff(-format => 'patch');
5832 }
5833
5834 sub git_history {
5835         if (!defined $hash_base) {
5836                 $hash_base = git_get_head_hash($project);
5837         }
5838         if (!defined $page) {
5839                 $page = 0;
5840         }
5841         my $ftype;
5842         my %co = parse_commit($hash_base)
5843             or die_error(404, "Unknown commit object");
5844
5845         my $refs = git_get_references();
5846         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5847
5848         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5849                                        $file_name, "--full-history")
5850             or die_error(404, "No such file or directory on given branch");
5851
5852         if (!defined $hash && defined $file_name) {
5853                 # some commits could have deleted file in question,
5854                 # and not have it in tree, but one of them has to have it
5855                 for (my $i = 0; $i <= @commitlist; $i++) {
5856                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5857                         last if defined $hash;
5858                 }
5859         }
5860         if (defined $hash) {
5861                 $ftype = git_get_type($hash);
5862         }
5863         if (!defined $ftype) {
5864                 die_error(500, "Unknown type of object");
5865         }
5866
5867         my $paging_nav = '';
5868         if ($page > 0) {
5869                 $paging_nav .=
5870                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5871                                                file_name=>$file_name)},
5872                                 "first");
5873                 $paging_nav .= " &sdot; " .
5874                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5875                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5876         } else {
5877                 $paging_nav .= "first";
5878                 $paging_nav .= " &sdot; prev";
5879         }
5880         my $next_link = '';
5881         if ($#commitlist >= 100) {
5882                 $next_link =
5883                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5884                                  -accesskey => "n", -title => "Alt-n"}, "next");
5885                 $paging_nav .= " &sdot; $next_link";
5886         } else {
5887                 $paging_nav .= " &sdot; next";
5888         }
5889
5890         git_header_html();
5891         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5892         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5893         git_print_page_path($file_name, $ftype, $hash_base);
5894
5895         git_history_body(\@commitlist, 0, 99,
5896                          $refs, $hash_base, $ftype, $next_link);
5897
5898         git_footer_html();
5899 }
5900
5901 sub git_search {
5902         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5903         if (!defined $searchtext) {
5904                 die_error(400, "Text field is empty");
5905         }
5906         if (!defined $hash) {
5907                 $hash = git_get_head_hash($project);
5908         }
5909         my %co = parse_commit($hash);
5910         if (!%co) {
5911                 die_error(404, "Unknown commit object");
5912         }
5913         if (!defined $page) {
5914                 $page = 0;
5915         }
5916
5917         $searchtype ||= 'commit';
5918         if ($searchtype eq 'pickaxe') {
5919                 # pickaxe may take all resources of your box and run for several minutes
5920                 # with every query - so decide by yourself how public you make this feature
5921                 gitweb_check_feature('pickaxe')
5922                     or die_error(403, "Pickaxe is disabled");
5923         }
5924         if ($searchtype eq 'grep') {
5925                 gitweb_check_feature('grep')
5926                     or die_error(403, "Grep is disabled");
5927         }
5928
5929         git_header_html();
5930
5931         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5932                 my $greptype;
5933                 if ($searchtype eq 'commit') {
5934                         $greptype = "--grep=";
5935                 } elsif ($searchtype eq 'author') {
5936                         $greptype = "--author=";
5937                 } elsif ($searchtype eq 'committer') {
5938                         $greptype = "--committer=";
5939                 }
5940                 $greptype .= $searchtext;
5941                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5942                                                $greptype, '--regexp-ignore-case',
5943                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5944
5945                 my $paging_nav = '';
5946                 if ($page > 0) {
5947                         $paging_nav .=
5948                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5949                                                        searchtext=>$searchtext,
5950                                                        searchtype=>$searchtype)},
5951                                         "first");
5952                         $paging_nav .= " &sdot; " .
5953                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5954                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5955                 } else {
5956                         $paging_nav .= "first";
5957                         $paging_nav .= " &sdot; prev";
5958                 }
5959                 my $next_link = '';
5960                 if ($#commitlist >= 100) {
5961                         $next_link =
5962                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5963                                          -accesskey => "n", -title => "Alt-n"}, "next");
5964                         $paging_nav .= " &sdot; $next_link";
5965                 } else {
5966                         $paging_nav .= " &sdot; next";
5967                 }
5968
5969                 if ($#commitlist >= 100) {
5970                 }
5971
5972                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5973                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5974                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5975         }
5976
5977         if ($searchtype eq 'pickaxe') {
5978                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5979                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5980
5981                 print "<table class=\"pickaxe search\">\n";
5982                 my $alternate = 1;
5983                 local $/ = "\n";
5984                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5985                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5986                         ($search_use_regexp ? '--pickaxe-regex' : ());
5987                 undef %co;
5988                 my @files;
5989                 while (my $line = <$fd>) {
5990                         chomp $line;
5991                         next unless $line;
5992
5993                         my %set = parse_difftree_raw_line($line);
5994                         if (defined $set{'commit'}) {
5995                                 # finish previous commit
5996                                 if (%co) {
5997                                         print "</td>\n" .
5998                                               "<td class=\"link\">" .
5999                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6000                                               " | " .
6001                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6002                                         print "</td>\n" .
6003                                               "</tr>\n";
6004                                 }
6005
6006                                 if ($alternate) {
6007                                         print "<tr class=\"dark\">\n";
6008                                 } else {
6009                                         print "<tr class=\"light\">\n";
6010                                 }
6011                                 $alternate ^= 1;
6012                                 %co = parse_commit($set{'commit'});
6013                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6014                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6015                                       "<td><i>$author</i></td>\n" .
6016                                       "<td>" .
6017                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6018                                               -class => "list subject"},
6019                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
6020                         } elsif (defined $set{'to_id'}) {
6021                                 next if ($set{'to_id'} =~ m/^0{40}$/);
6022
6023                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6024                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6025                                               -class => "list"},
6026                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6027                                       "<br/>\n";
6028                         }
6029                 }
6030                 close $fd;
6031
6032                 # finish last commit (warning: repetition!)
6033                 if (%co) {
6034                         print "</td>\n" .
6035                               "<td class=\"link\">" .
6036                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6037                               " | " .
6038                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6039                         print "</td>\n" .
6040                               "</tr>\n";
6041                 }
6042
6043                 print "</table>\n";
6044         }
6045
6046         if ($searchtype eq 'grep') {
6047                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6048                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6049
6050                 print "<table class=\"grep_search\">\n";
6051                 my $alternate = 1;
6052                 my $matches = 0;
6053                 local $/ = "\n";
6054                 open my $fd, "-|", git_cmd(), 'grep', '-n',
6055                         $search_use_regexp ? ('-E', '-i') : '-F',
6056                         $searchtext, $co{'tree'};
6057                 my $lastfile = '';
6058                 while (my $line = <$fd>) {
6059                         chomp $line;
6060                         my ($file, $lno, $ltext, $binary);
6061                         last if ($matches++ > 1000);
6062                         if ($line =~ /^Binary file (.+) matches$/) {
6063                                 $file = $1;
6064                                 $binary = 1;
6065                         } else {
6066                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6067                         }
6068                         if ($file ne $lastfile) {
6069                                 $lastfile and print "</td></tr>\n";
6070                                 if ($alternate++) {
6071                                         print "<tr class=\"dark\">\n";
6072                                 } else {
6073                                         print "<tr class=\"light\">\n";
6074                                 }
6075                                 print "<td class=\"list\">".
6076                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6077                                                                file_name=>"$file"),
6078                                                 -class => "list"}, esc_path($file));
6079                                 print "</td><td>\n";
6080                                 $lastfile = $file;
6081                         }
6082                         if ($binary) {
6083                                 print "<div class=\"binary\">Binary file</div>\n";
6084                         } else {
6085                                 $ltext = untabify($ltext);
6086                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6087                                         $ltext = esc_html($1, -nbsp=>1);
6088                                         $ltext .= '<span class="match">';
6089                                         $ltext .= esc_html($2, -nbsp=>1);
6090                                         $ltext .= '</span>';
6091                                         $ltext .= esc_html($3, -nbsp=>1);
6092                                 } else {
6093                                         $ltext = esc_html($ltext, -nbsp=>1);
6094                                 }
6095                                 print "<div class=\"pre\">" .
6096                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6097                                                                file_name=>"$file").'#l'.$lno,
6098                                                 -class => "linenr"}, sprintf('%4i', $lno))
6099                                         . ' ' .  $ltext . "</div>\n";
6100                         }
6101                 }
6102                 if ($lastfile) {
6103                         print "</td></tr>\n";
6104                         if ($matches > 1000) {
6105                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6106                         }
6107                 } else {
6108                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
6109                 }
6110                 close $fd;
6111
6112                 print "</table>\n";
6113         }
6114         git_footer_html();
6115 }
6116
6117 sub git_search_help {
6118         git_header_html();
6119         git_print_page_nav('','', $hash,$hash,$hash);
6120         print <<EOT;
6121 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6122 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6123 the pattern entered is recognized as the POSIX extended
6124 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6125 insensitive).</p>
6126 <dl>
6127 <dt><b>commit</b></dt>
6128 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6129 EOT
6130         my $have_grep = gitweb_check_feature('grep');
6131         if ($have_grep) {
6132                 print <<EOT;
6133 <dt><b>grep</b></dt>
6134 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6135     a different one) are searched for the given pattern. On large trees, this search can take
6136 a while and put some strain on the server, so please use it with some consideration. Note that
6137 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6138 case-sensitive.</dd>
6139 EOT
6140         }
6141         print <<EOT;
6142 <dt><b>author</b></dt>
6143 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6144 <dt><b>committer</b></dt>
6145 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6146 EOT
6147         my $have_pickaxe = gitweb_check_feature('pickaxe');
6148         if ($have_pickaxe) {
6149                 print <<EOT;
6150 <dt><b>pickaxe</b></dt>
6151 <dd>All commits that caused the string to appear or disappear from any file (changes that
6152 added, removed or "modified" the string) will be listed. This search can take a while and
6153 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6154 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6155 EOT
6156         }
6157         print "</dl>\n";
6158         git_footer_html();
6159 }
6160
6161 sub git_shortlog {
6162         my $head = git_get_head_hash($project);
6163         if (!defined $hash) {
6164                 $hash = $head;
6165         }
6166         if (!defined $page) {
6167                 $page = 0;
6168         }
6169         my $refs = git_get_references();
6170
6171         my $commit_hash = $hash;
6172         if (defined $hash_parent) {
6173                 $commit_hash = "$hash_parent..$hash";
6174         }
6175         my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6176
6177         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6178         my $next_link = '';
6179         if ($#commitlist >= 100) {
6180                 $next_link =
6181                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
6182                                  -accesskey => "n", -title => "Alt-n"}, "next");
6183         }
6184         my $patch_max = gitweb_check_feature('patches');
6185         if ($patch_max) {
6186                 if ($patch_max < 0 || @commitlist <= $patch_max) {
6187                         $paging_nav .= " &sdot; " .
6188                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6189                                         "patches");
6190                 }
6191         }
6192
6193         git_header_html();
6194         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6195         git_print_header_div('summary', $project);
6196
6197         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6198
6199         git_footer_html();
6200 }
6201
6202 ## ......................................................................
6203 ## feeds (RSS, Atom; OPML)
6204
6205 sub git_feed {
6206         my $format = shift || 'atom';
6207         my $have_blame = gitweb_check_feature('blame');
6208
6209         # Atom: http://www.atomenabled.org/developers/syndication/
6210         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6211         if ($format ne 'rss' && $format ne 'atom') {
6212                 die_error(400, "Unknown web feed format");
6213         }
6214
6215         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6216         my $head = $hash || 'HEAD';
6217         my @commitlist = parse_commits($head, 150, 0, $file_name);
6218
6219         my %latest_commit;
6220         my %latest_date;
6221         my $content_type = "application/$format+xml";
6222         if (defined $cgi->http('HTTP_ACCEPT') &&
6223                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6224                 # browser (feed reader) prefers text/xml
6225                 $content_type = 'text/xml';
6226         }
6227         if (defined($commitlist[0])) {
6228                 %latest_commit = %{$commitlist[0]};
6229                 my $latest_epoch = $latest_commit{'committer_epoch'};
6230                 %latest_date   = parse_date($latest_epoch);
6231                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6232                 if (defined $if_modified) {
6233                         my $since;
6234                         if (eval { require HTTP::Date; 1; }) {
6235                                 $since = HTTP::Date::str2time($if_modified);
6236                         } elsif (eval { require Time::ParseDate; 1; }) {
6237                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6238                         }
6239                         if (defined $since && $latest_epoch <= $since) {
6240                                 print $cgi->header(
6241                                         -type => $content_type,
6242                                         -charset => 'utf-8',
6243                                         -last_modified => $latest_date{'rfc2822'},
6244                                         -status => '304 Not Modified');
6245                                 return;
6246                         }
6247                 }
6248                 print $cgi->header(
6249                         -type => $content_type,
6250                         -charset => 'utf-8',
6251                         -last_modified => $latest_date{'rfc2822'});
6252         } else {
6253                 print $cgi->header(
6254                         -type => $content_type,
6255                         -charset => 'utf-8');
6256         }
6257
6258         # Optimization: skip generating the body if client asks only
6259         # for Last-Modified date.
6260         return if ($cgi->request_method() eq 'HEAD');
6261
6262         # header variables
6263         my $title = "$site_name - $project/$action";
6264         my $feed_type = 'log';
6265         if (defined $hash) {
6266                 $title .= " - '$hash'";
6267                 $feed_type = 'branch log';
6268                 if (defined $file_name) {
6269                         $title .= " :: $file_name";
6270                         $feed_type = 'history';
6271                 }
6272         } elsif (defined $file_name) {
6273                 $title .= " - $file_name";
6274                 $feed_type = 'history';
6275         }
6276         $title .= " $feed_type";
6277         my $descr = git_get_project_description($project);
6278         if (defined $descr) {
6279                 $descr = esc_html($descr);
6280         } else {
6281                 $descr = "$project " .
6282                          ($format eq 'rss' ? 'RSS' : 'Atom') .
6283                          " feed";
6284         }
6285         my $owner = git_get_project_owner($project);
6286         $owner = esc_html($owner);
6287
6288         #header
6289         my $alt_url;
6290         if (defined $file_name) {
6291                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6292         } elsif (defined $hash) {
6293                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6294         } else {
6295                 $alt_url = href(-full=>1, action=>"summary");
6296         }
6297         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6298         if ($format eq 'rss') {
6299                 print <<XML;
6300 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6301 <channel>
6302 XML
6303                 print "<title>$title</title>\n" .
6304                       "<link>$alt_url</link>\n" .
6305                       "<description>$descr</description>\n" .
6306                       "<language>en</language>\n" .
6307                       # project owner is responsible for 'editorial' content
6308                       "<managingEditor>$owner</managingEditor>\n";
6309                 if (defined $logo || defined $favicon) {
6310                         # prefer the logo to the favicon, since RSS
6311                         # doesn't allow both
6312                         my $img = esc_url($logo || $favicon);
6313                         print "<image>\n" .
6314                               "<url>$img</url>\n" .
6315                               "<title>$title</title>\n" .
6316                               "<link>$alt_url</link>\n" .
6317                               "</image>\n";
6318                 }
6319                 if (%latest_date) {
6320                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6321                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6322                 }
6323                 print "<generator>gitweb v.$version/$git_version</generator>\n";
6324         } elsif ($format eq 'atom') {
6325                 print <<XML;
6326 <feed xmlns="http://www.w3.org/2005/Atom">
6327 XML
6328                 print "<title>$title</title>\n" .
6329                       "<subtitle>$descr</subtitle>\n" .
6330                       '<link rel="alternate" type="text/html" href="' .
6331                       $alt_url . '" />' . "\n" .
6332                       '<link rel="self" type="' . $content_type . '" href="' .
6333                       $cgi->self_url() . '" />' . "\n" .
6334                       "<id>" . href(-full=>1) . "</id>\n" .
6335                       # use project owner for feed author
6336                       "<author><name>$owner</name></author>\n";
6337                 if (defined $favicon) {
6338                         print "<icon>" . esc_url($favicon) . "</icon>\n";
6339                 }
6340                 if (defined $logo_url) {
6341                         # not twice as wide as tall: 72 x 27 pixels
6342                         print "<logo>" . esc_url($logo) . "</logo>\n";
6343                 }
6344                 if (! %latest_date) {
6345                         # dummy date to keep the feed valid until commits trickle in:
6346                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
6347                 } else {
6348                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
6349                 }
6350                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6351         }
6352
6353         # contents
6354         for (my $i = 0; $i <= $#commitlist; $i++) {
6355                 my %co = %{$commitlist[$i]};
6356                 my $commit = $co{'id'};
6357                 # we read 150, we always show 30 and the ones more recent than 48 hours
6358                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6359                         last;
6360                 }
6361                 my %cd = parse_date($co{'author_epoch'});
6362
6363                 # get list of changed files
6364                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6365                         $co{'parent'} || "--root",
6366                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
6367                         or next;
6368                 my @difftree = map { chomp; $_ } <$fd>;
6369                 close $fd
6370                         or next;
6371
6372                 # print element (entry, item)
6373                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6374                 if ($format eq 'rss') {
6375                         print "<item>\n" .
6376                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
6377                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
6378                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6379                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6380                               "<link>$co_url</link>\n" .
6381                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
6382                               "<content:encoded>" .
6383                               "<![CDATA[\n";
6384                 } elsif ($format eq 'atom') {
6385                         print "<entry>\n" .
6386                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6387                               "<updated>$cd{'iso-8601'}</updated>\n" .
6388                               "<author>\n" .
6389                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6390                         if ($co{'author_email'}) {
6391                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6392                         }
6393                         print "</author>\n" .
6394                               # use committer for contributor
6395                               "<contributor>\n" .
6396                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6397                         if ($co{'committer_email'}) {
6398                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6399                         }
6400                         print "</contributor>\n" .
6401                               "<published>$cd{'iso-8601'}</published>\n" .
6402                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6403                               "<id>$co_url</id>\n" .
6404                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6405                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6406                 }
6407                 my $comment = $co{'comment'};
6408                 print "<pre>\n";
6409                 foreach my $line (@$comment) {
6410                         $line = esc_html($line);
6411                         print "$line\n";
6412                 }
6413                 print "</pre><ul>\n";
6414                 foreach my $difftree_line (@difftree) {
6415                         my %difftree = parse_difftree_raw_line($difftree_line);
6416                         next if !$difftree{'from_id'};
6417
6418                         my $file = $difftree{'file'} || $difftree{'to_file'};
6419
6420                         print "<li>" .
6421                               "[" .
6422                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6423                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6424                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6425                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
6426                                       -title => "diff"}, 'D');
6427                         if ($have_blame) {
6428                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
6429                                                              file_name=>$file, hash_base=>$commit),
6430                                               -title => "blame"}, 'B');
6431                         }
6432                         # if this is not a feed of a file history
6433                         if (!defined $file_name || $file_name ne $file) {
6434                                 print $cgi->a({-href => href(-full=>1, action=>"history",
6435                                                              file_name=>$file, hash=>$commit),
6436                                               -title => "history"}, 'H');
6437                         }
6438                         $file = esc_path($file);
6439                         print "] ".
6440                               "$file</li>\n";
6441                 }
6442                 if ($format eq 'rss') {
6443                         print "</ul>]]>\n" .
6444                               "</content:encoded>\n" .
6445                               "</item>\n";
6446                 } elsif ($format eq 'atom') {
6447                         print "</ul>\n</div>\n" .
6448                               "</content>\n" .
6449                               "</entry>\n";
6450                 }
6451         }
6452
6453         # end of feed
6454         if ($format eq 'rss') {
6455                 print "</channel>\n</rss>\n";
6456         } elsif ($format eq 'atom') {
6457                 print "</feed>\n";
6458         }
6459 }
6460
6461 sub git_rss {
6462         git_feed('rss');
6463 }
6464
6465 sub git_atom {
6466         git_feed('atom');
6467 }
6468
6469 sub git_opml {
6470         my @list = git_get_projects_list();
6471
6472         print $cgi->header(
6473                 -type => 'text/xml',
6474                 -charset => 'utf-8',
6475                 -content_disposition => 'inline; filename="opml.xml"');
6476
6477         print <<XML;
6478 <?xml version="1.0" encoding="utf-8"?>
6479 <opml version="1.0">
6480 <head>
6481   <title>$site_name OPML Export</title>
6482 </head>
6483 <body>
6484 <outline text="git RSS feeds">
6485 XML
6486
6487         foreach my $pr (@list) {
6488                 my %proj = %$pr;
6489                 my $head = git_get_head_hash($proj{'path'});
6490                 if (!defined $head) {
6491                         next;
6492                 }
6493                 $git_dir = "$projectroot/$proj{'path'}";
6494                 my %co = parse_commit($head);
6495                 if (!%co) {
6496                         next;
6497                 }
6498
6499                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6500                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6501                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6502                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6503         }
6504         print <<XML;
6505 </outline>
6506 </body>
6507 </opml>
6508 XML
6509 }