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