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