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