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