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