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