gitweb: Modularized git_get_project_description to be more generic
[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 # get the value of config variable either from file named as the variable
2566 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
2567 # configuration variable in the repository config file.
2568 sub git_get_file_or_project_config {
2569         my ($path, $name) = @_;
2570
2571         $git_dir = "$projectroot/$path";
2572         open my $fd, '<', "$git_dir/$name"
2573                 or return git_get_project_config($name);
2574         my $conf = <$fd>;
2575         close $fd;
2576         if (defined $conf) {
2577                 chomp $conf;
2578         }
2579         return $conf;
2580 }
2581
2582 sub git_get_project_description {
2583         my $path = shift;
2584         return git_get_file_or_project_config($path, 'description');
2585 }
2586
2587 # supported formats:
2588 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2589 #   - if its contents is a number, use it as tag weight,
2590 #   - otherwise add a tag with weight 1
2591 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2592 #   the same value multiple times increases tag weight
2593 # * `gitweb.ctag' multi-valued repo config variable
2594 sub git_get_project_ctags {
2595         my $project = shift;
2596         my $ctags = {};
2597
2598         $git_dir = "$projectroot/$project";
2599         if (opendir my $dh, "$git_dir/ctags") {
2600                 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2601                 foreach my $tagfile (@files) {
2602                         open my $ct, '<', $tagfile
2603                                 or next;
2604                         my $val = <$ct>;
2605                         chomp $val if $val;
2606                         close $ct;
2607
2608                         (my $ctag = $tagfile) =~ s#.*/##;
2609                         if ($val =~ /\d+/) {
2610                                 $ctags->{$ctag} = $val;
2611                         } else {
2612                                 $ctags->{$ctag} = 1;
2613                         }
2614                 }
2615                 closedir $dh;
2616
2617         } elsif (open my $fh, '<', "$git_dir/ctags") {
2618                 while (my $line = <$fh>) {
2619                         chomp $line;
2620                         $ctags->{$line}++ if $line;
2621                 }
2622                 close $fh;
2623
2624         } else {
2625                 my $taglist = config_to_multi(git_get_project_config('ctag'));
2626                 foreach my $tag (@$taglist) {
2627                         $ctags->{$tag}++;
2628                 }
2629         }
2630
2631         return $ctags;
2632 }
2633
2634 # return hash, where keys are content tags ('ctags'),
2635 # and values are sum of weights of given tag in every project
2636 sub git_gather_all_ctags {
2637         my $projects = shift;
2638         my $ctags = {};
2639
2640         foreach my $p (@$projects) {
2641                 foreach my $ct (keys %{$p->{'ctags'}}) {
2642                         $ctags->{$ct} += $p->{'ctags'}->{$ct};
2643                 }
2644         }
2645
2646         return $ctags;
2647 }
2648
2649 sub git_populate_project_tagcloud {
2650         my $ctags = shift;
2651
2652         # First, merge different-cased tags; tags vote on casing
2653         my %ctags_lc;
2654         foreach (keys %$ctags) {
2655                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2656                 if (not $ctags_lc{lc $_}->{topcount}
2657                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2658                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2659                         $ctags_lc{lc $_}->{topname} = $_;
2660                 }
2661         }
2662
2663         my $cloud;
2664         my $matched = $cgi->param('by_tag');
2665         if (eval { require HTML::TagCloud; 1; }) {
2666                 $cloud = HTML::TagCloud->new;
2667                 foreach my $ctag (sort keys %ctags_lc) {
2668                         # Pad the title with spaces so that the cloud looks
2669                         # less crammed.
2670                         my $title = esc_html($ctags_lc{$ctag}->{topname});
2671                         $title =~ s/ /&nbsp;/g;
2672                         $title =~ s/^/&nbsp;/g;
2673                         $title =~ s/$/&nbsp;/g;
2674                         if (defined $matched && $matched eq $ctag) {
2675                                 $title = qq(<span class="match">$title</span>);
2676                         }
2677                         $cloud->add($title, href(project=>undef, ctag=>$ctag),
2678                                     $ctags_lc{$ctag}->{count});
2679                 }
2680         } else {
2681                 $cloud = {};
2682                 foreach my $ctag (keys %ctags_lc) {
2683                         my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
2684                         if (defined $matched && $matched eq $ctag) {
2685                                 $title = qq(<span class="match">$title</span>);
2686                         }
2687                         $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2688                         $cloud->{$ctag}{ctag} =
2689                                 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
2690                 }
2691         }
2692         return $cloud;
2693 }
2694
2695 sub git_show_project_tagcloud {
2696         my ($cloud, $count) = @_;
2697         if (ref $cloud eq 'HTML::TagCloud') {
2698                 return $cloud->html_and_css($count);
2699         } else {
2700                 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2701                 return
2702                         '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2703                         join (', ', map {
2704                                 $cloud->{$_}->{'ctag'}
2705                         } splice(@tags, 0, $count)) .
2706                         '</div>';
2707         }
2708 }
2709
2710 sub git_get_project_url_list {
2711         my $path = shift;
2712
2713         $git_dir = "$projectroot/$path";
2714         open my $fd, '<', "$git_dir/cloneurl"
2715                 or return wantarray ?
2716                 @{ config_to_multi(git_get_project_config('url')) } :
2717                    config_to_multi(git_get_project_config('url'));
2718         my @git_project_url_list = map { chomp; $_ } <$fd>;
2719         close $fd;
2720
2721         return wantarray ? @git_project_url_list : \@git_project_url_list;
2722 }
2723
2724 sub git_get_projects_list {
2725         my $filter = shift || '';
2726         my @list;
2727
2728         $filter =~ s/\.git$//;
2729
2730         if (-d $projects_list) {
2731                 # search in directory
2732                 my $dir = $projects_list;
2733                 # remove the trailing "/"
2734                 $dir =~ s!/+$!!;
2735                 my $pfxlen = length("$projects_list");
2736                 my $pfxdepth = ($projects_list =~ tr!/!!);
2737                 # when filtering, search only given subdirectory
2738                 if ($filter) {
2739                         $dir .= "/$filter";
2740                         $dir =~ s!/+$!!;
2741                 }
2742
2743                 File::Find::find({
2744                         follow_fast => 1, # follow symbolic links
2745                         follow_skip => 2, # ignore duplicates
2746                         dangling_symlinks => 0, # ignore dangling symlinks, silently
2747                         wanted => sub {
2748                                 # global variables
2749                                 our $project_maxdepth;
2750                                 our $projectroot;
2751                                 # skip project-list toplevel, if we get it.
2752                                 return if (m!^[/.]$!);
2753                                 # only directories can be git repositories
2754                                 return unless (-d $_);
2755                                 # don't traverse too deep (Find is super slow on os x)
2756                                 # $project_maxdepth excludes depth of $projectroot
2757                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2758                                         $File::Find::prune = 1;
2759                                         return;
2760                                 }
2761
2762                                 my $path = substr($File::Find::name, $pfxlen + 1);
2763                                 # we check related file in $projectroot
2764                                 if (check_export_ok("$projectroot/$path")) {
2765                                         push @list, { path => $path };
2766                                         $File::Find::prune = 1;
2767                                 }
2768                         },
2769                 }, "$dir");
2770
2771         } elsif (-f $projects_list) {
2772                 # read from file(url-encoded):
2773                 # 'git%2Fgit.git Linus+Torvalds'
2774                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2775                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2776                 open my $fd, '<', $projects_list or return;
2777         PROJECT:
2778                 while (my $line = <$fd>) {
2779                         chomp $line;
2780                         my ($path, $owner) = split ' ', $line;
2781                         $path = unescape($path);
2782                         $owner = unescape($owner);
2783                         if (!defined $path) {
2784                                 next;
2785                         }
2786                         # if $filter is rpovided, check if $path begins with $filter
2787                         if ($filter && $path !~ m!^\Q$filter\E/!) {
2788                                 next;
2789                         }
2790                         if (check_export_ok("$projectroot/$path")) {
2791                                 my $pr = {
2792                                         path => $path,
2793                                         owner => to_utf8($owner),
2794                                 };
2795                                 push @list, $pr;
2796                         }
2797                 }
2798                 close $fd;
2799         }
2800         return @list;
2801 }
2802
2803 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
2804 # as side effects it sets 'forks' field to list of forks for forked projects
2805 sub filter_forks_from_projects_list {
2806         my $projects = shift;
2807
2808         my %trie; # prefix tree of directories (path components)
2809         # generate trie out of those directories that might contain forks
2810         foreach my $pr (@$projects) {
2811                 my $path = $pr->{'path'};
2812                 $path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
2813                 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
2814                 next unless ($path);      # skip '.git' repository: tests, git-instaweb
2815                 next unless (-d $path);   # containing directory exists
2816                 $pr->{'forks'} = [];      # there can be 0 or more forks of project
2817
2818                 # add to trie
2819                 my @dirs = split('/', $path);
2820                 # walk the trie, until either runs out of components or out of trie
2821                 my $ref = \%trie;
2822                 while (scalar @dirs &&
2823                        exists($ref->{$dirs[0]})) {
2824                         $ref = $ref->{shift @dirs};
2825                 }
2826                 # create rest of trie structure from rest of components
2827                 foreach my $dir (@dirs) {
2828                         $ref = $ref->{$dir} = {};
2829                 }
2830                 # create end marker, store $pr as a data
2831                 $ref->{''} = $pr if (!exists $ref->{''});
2832         }
2833
2834         # filter out forks, by finding shortest prefix match for paths
2835         my @filtered;
2836  PROJECT:
2837         foreach my $pr (@$projects) {
2838                 # trie lookup
2839                 my $ref = \%trie;
2840         DIR:
2841                 foreach my $dir (split('/', $pr->{'path'})) {
2842                         if (exists $ref->{''}) {
2843                                 # found [shortest] prefix, is a fork - skip it
2844                                 push @{$ref->{''}{'forks'}}, $pr;
2845                                 next PROJECT;
2846                         }
2847                         if (!exists $ref->{$dir}) {
2848                                 # not in trie, cannot have prefix, not a fork
2849                                 push @filtered, $pr;
2850                                 next PROJECT;
2851                         }
2852                         # If the dir is there, we just walk one step down the trie.
2853                         $ref = $ref->{$dir};
2854                 }
2855                 # we ran out of trie
2856                 # (shouldn't happen: it's either no match, or end marker)
2857                 push @filtered, $pr;
2858         }
2859
2860         return @filtered;
2861 }
2862
2863 # note: fill_project_list_info must be run first,
2864 # for 'descr_long' and 'ctags' to be filled
2865 sub search_projects_list {
2866         my ($projlist, %opts) = @_;
2867         my $tagfilter  = $opts{'tagfilter'};
2868         my $searchtext = $opts{'searchtext'};
2869
2870         return @$projlist
2871                 unless ($tagfilter || $searchtext);
2872
2873         my @projects;
2874  PROJECT:
2875         foreach my $pr (@$projlist) {
2876
2877                 if ($tagfilter) {
2878                         next unless ref($pr->{'ctags'}) eq 'HASH';
2879                         next unless
2880                                 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
2881                 }
2882
2883                 if ($searchtext) {
2884                         next unless
2885                                 $pr->{'path'} =~ /$searchtext/ ||
2886                                 $pr->{'descr_long'} =~ /$searchtext/;
2887                 }
2888
2889                 push @projects, $pr;
2890         }
2891
2892         return @projects;
2893 }
2894
2895 our $gitweb_project_owner = undef;
2896 sub git_get_project_list_from_file {
2897
2898         return if (defined $gitweb_project_owner);
2899
2900         $gitweb_project_owner = {};
2901         # read from file (url-encoded):
2902         # 'git%2Fgit.git Linus+Torvalds'
2903         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2904         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2905         if (-f $projects_list) {
2906                 open(my $fd, '<', $projects_list);
2907                 while (my $line = <$fd>) {
2908                         chomp $line;
2909                         my ($pr, $ow) = split ' ', $line;
2910                         $pr = unescape($pr);
2911                         $ow = unescape($ow);
2912                         $gitweb_project_owner->{$pr} = to_utf8($ow);
2913                 }
2914                 close $fd;
2915         }
2916 }
2917
2918 sub git_get_project_owner {
2919         my $project = shift;
2920         my $owner;
2921
2922         return undef unless $project;
2923         $git_dir = "$projectroot/$project";
2924
2925         if (!defined $gitweb_project_owner) {
2926                 git_get_project_list_from_file();
2927         }
2928
2929         if (exists $gitweb_project_owner->{$project}) {
2930                 $owner = $gitweb_project_owner->{$project};
2931         }
2932         if (!defined $owner){
2933                 $owner = git_get_project_config('owner');
2934         }
2935         if (!defined $owner) {
2936                 $owner = get_file_owner("$git_dir");
2937         }
2938
2939         return $owner;
2940 }
2941
2942 sub git_get_last_activity {
2943         my ($path) = @_;
2944         my $fd;
2945
2946         $git_dir = "$projectroot/$path";
2947         open($fd, "-|", git_cmd(), 'for-each-ref',
2948              '--format=%(committer)',
2949              '--sort=-committerdate',
2950              '--count=1',
2951              'refs/heads') or return;
2952         my $most_recent = <$fd>;
2953         close $fd or return;
2954         if (defined $most_recent &&
2955             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2956                 my $timestamp = $1;
2957                 my $age = time - $timestamp;
2958                 return ($age, age_string($age));
2959         }
2960         return (undef, undef);
2961 }
2962
2963 # Implementation note: when a single remote is wanted, we cannot use 'git
2964 # remote show -n' because that command always work (assuming it's a remote URL
2965 # if it's not defined), and we cannot use 'git remote show' because that would
2966 # try to make a network roundtrip. So the only way to find if that particular
2967 # remote is defined is to walk the list provided by 'git remote -v' and stop if
2968 # and when we find what we want.
2969 sub git_get_remotes_list {
2970         my $wanted = shift;
2971         my %remotes = ();
2972
2973         open my $fd, '-|' , git_cmd(), 'remote', '-v';
2974         return unless $fd;
2975         while (my $remote = <$fd>) {
2976                 chomp $remote;
2977                 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
2978                 next if $wanted and not $remote eq $wanted;
2979                 my ($url, $key) = ($1, $2);
2980
2981                 $remotes{$remote} ||= { 'heads' => () };
2982                 $remotes{$remote}{$key} = $url;
2983         }
2984         close $fd or return;
2985         return wantarray ? %remotes : \%remotes;
2986 }
2987
2988 # Takes a hash of remotes as first parameter and fills it by adding the
2989 # available remote heads for each of the indicated remotes.
2990 sub fill_remote_heads {
2991         my $remotes = shift;
2992         my @heads = map { "remotes/$_" } keys %$remotes;
2993         my @remoteheads = git_get_heads_list(undef, @heads);
2994         foreach my $remote (keys %$remotes) {
2995                 $remotes->{$remote}{'heads'} = [ grep {
2996                         $_->{'name'} =~ s!^$remote/!!
2997                         } @remoteheads ];
2998         }
2999 }
3000
3001 sub git_get_references {
3002         my $type = shift || "";
3003         my %refs;
3004         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3005         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3006         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3007                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
3008                 or return;
3009
3010         while (my $line = <$fd>) {
3011                 chomp $line;
3012                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3013                         if (defined $refs{$1}) {
3014                                 push @{$refs{$1}}, $2;
3015                         } else {
3016                                 $refs{$1} = [ $2 ];
3017                         }
3018                 }
3019         }
3020         close $fd or return;
3021         return \%refs;
3022 }
3023
3024 sub git_get_rev_name_tags {
3025         my $hash = shift || return undef;
3026
3027         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3028                 or return;
3029         my $name_rev = <$fd>;
3030         close $fd;
3031
3032         if ($name_rev =~ m|^$hash tags/(.*)$|) {
3033                 return $1;
3034         } else {
3035                 # catches also '$hash undefined' output
3036                 return undef;
3037         }
3038 }
3039
3040 ## ----------------------------------------------------------------------
3041 ## parse to hash functions
3042
3043 sub parse_date {
3044         my $epoch = shift;
3045         my $tz = shift || "-0000";
3046
3047         my %date;
3048         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3049         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3050         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3051         $date{'hour'} = $hour;
3052         $date{'minute'} = $min;
3053         $date{'mday'} = $mday;
3054         $date{'day'} = $days[$wday];
3055         $date{'month'} = $months[$mon];
3056         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3057                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3058         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3059                              $mday, $months[$mon], $hour ,$min;
3060         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3061                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3062
3063         my ($tz_sign, $tz_hour, $tz_min) =
3064                 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3065         $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3066         my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3067         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3068         $date{'hour_local'} = $hour;
3069         $date{'minute_local'} = $min;
3070         $date{'tz_local'} = $tz;
3071         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3072                                   1900+$year, $mon+1, $mday,
3073                                   $hour, $min, $sec, $tz);
3074         return %date;
3075 }
3076
3077 sub parse_tag {
3078         my $tag_id = shift;
3079         my %tag;
3080         my @comment;
3081
3082         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3083         $tag{'id'} = $tag_id;
3084         while (my $line = <$fd>) {
3085                 chomp $line;
3086                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3087                         $tag{'object'} = $1;
3088                 } elsif ($line =~ m/^type (.+)$/) {
3089                         $tag{'type'} = $1;
3090                 } elsif ($line =~ m/^tag (.+)$/) {
3091                         $tag{'name'} = $1;
3092                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3093                         $tag{'author'} = $1;
3094                         $tag{'author_epoch'} = $2;
3095                         $tag{'author_tz'} = $3;
3096                         if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3097                                 $tag{'author_name'}  = $1;
3098                                 $tag{'author_email'} = $2;
3099                         } else {
3100                                 $tag{'author_name'} = $tag{'author'};
3101                         }
3102                 } elsif ($line =~ m/--BEGIN/) {
3103                         push @comment, $line;
3104                         last;
3105                 } elsif ($line eq "") {
3106                         last;
3107                 }
3108         }
3109         push @comment, <$fd>;
3110         $tag{'comment'} = \@comment;
3111         close $fd or return;
3112         if (!defined $tag{'name'}) {
3113                 return
3114         };
3115         return %tag
3116 }
3117
3118 sub parse_commit_text {
3119         my ($commit_text, $withparents) = @_;
3120         my @commit_lines = split '\n', $commit_text;
3121         my %co;
3122
3123         pop @commit_lines; # Remove '\0'
3124
3125         if (! @commit_lines) {
3126                 return;
3127         }
3128
3129         my $header = shift @commit_lines;
3130         if ($header !~ m/^[0-9a-fA-F]{40}/) {
3131                 return;
3132         }
3133         ($co{'id'}, my @parents) = split ' ', $header;
3134         while (my $line = shift @commit_lines) {
3135                 last if $line eq "\n";
3136                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3137                         $co{'tree'} = $1;
3138                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3139                         push @parents, $1;
3140                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3141                         $co{'author'} = to_utf8($1);
3142                         $co{'author_epoch'} = $2;
3143                         $co{'author_tz'} = $3;
3144                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3145                                 $co{'author_name'}  = $1;
3146                                 $co{'author_email'} = $2;
3147                         } else {
3148                                 $co{'author_name'} = $co{'author'};
3149                         }
3150                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3151                         $co{'committer'} = to_utf8($1);
3152                         $co{'committer_epoch'} = $2;
3153                         $co{'committer_tz'} = $3;
3154                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3155                                 $co{'committer_name'}  = $1;
3156                                 $co{'committer_email'} = $2;
3157                         } else {
3158                                 $co{'committer_name'} = $co{'committer'};
3159                         }
3160                 }
3161         }
3162         if (!defined $co{'tree'}) {
3163                 return;
3164         };
3165         $co{'parents'} = \@parents;
3166         $co{'parent'} = $parents[0];
3167
3168         foreach my $title (@commit_lines) {
3169                 $title =~ s/^    //;
3170                 if ($title ne "") {
3171                         $co{'title'} = chop_str($title, 80, 5);
3172                         # remove leading stuff of merges to make the interesting part visible
3173                         if (length($title) > 50) {
3174                                 $title =~ s/^Automatic //;
3175                                 $title =~ s/^merge (of|with) /Merge ... /i;
3176                                 if (length($title) > 50) {
3177                                         $title =~ s/(http|rsync):\/\///;
3178                                 }
3179                                 if (length($title) > 50) {
3180                                         $title =~ s/(master|www|rsync)\.//;
3181                                 }
3182                                 if (length($title) > 50) {
3183                                         $title =~ s/kernel.org:?//;
3184                                 }
3185                                 if (length($title) > 50) {
3186                                         $title =~ s/\/pub\/scm//;
3187                                 }
3188                         }
3189                         $co{'title_short'} = chop_str($title, 50, 5);
3190                         last;
3191                 }
3192         }
3193         if (! defined $co{'title'} || $co{'title'} eq "") {
3194                 $co{'title'} = $co{'title_short'} = '(no commit message)';
3195         }
3196         # remove added spaces
3197         foreach my $line (@commit_lines) {
3198                 $line =~ s/^    //;
3199         }
3200         $co{'comment'} = \@commit_lines;
3201
3202         my $age = time - $co{'committer_epoch'};
3203         $co{'age'} = $age;
3204         $co{'age_string'} = age_string($age);
3205         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3206         if ($age > 60*60*24*7*2) {
3207                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3208                 $co{'age_string_age'} = $co{'age_string'};
3209         } else {
3210                 $co{'age_string_date'} = $co{'age_string'};
3211                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3212         }
3213         return %co;
3214 }
3215
3216 sub parse_commit {
3217         my ($commit_id) = @_;
3218         my %co;
3219
3220         local $/ = "\0";
3221
3222         open my $fd, "-|", git_cmd(), "rev-list",
3223                 "--parents",
3224                 "--header",
3225                 "--max-count=1",
3226                 $commit_id,
3227                 "--",
3228                 or die_error(500, "Open git-rev-list failed");
3229         %co = parse_commit_text(<$fd>, 1);
3230         close $fd;
3231
3232         return %co;
3233 }
3234
3235 sub parse_commits {
3236         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3237         my @cos;
3238
3239         $maxcount ||= 1;
3240         $skip ||= 0;
3241
3242         local $/ = "\0";
3243
3244         open my $fd, "-|", git_cmd(), "rev-list",
3245                 "--header",
3246                 @args,
3247                 ("--max-count=" . $maxcount),
3248                 ("--skip=" . $skip),
3249                 @extra_options,
3250                 $commit_id,
3251                 "--",
3252                 ($filename ? ($filename) : ())
3253                 or die_error(500, "Open git-rev-list failed");
3254         while (my $line = <$fd>) {
3255                 my %co = parse_commit_text($line);
3256                 push @cos, \%co;
3257         }
3258         close $fd;
3259
3260         return wantarray ? @cos : \@cos;
3261 }
3262
3263 # parse line of git-diff-tree "raw" output
3264 sub parse_difftree_raw_line {
3265         my $line = shift;
3266         my %res;
3267
3268         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
3269         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
3270         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3271                 $res{'from_mode'} = $1;
3272                 $res{'to_mode'} = $2;
3273                 $res{'from_id'} = $3;
3274                 $res{'to_id'} = $4;
3275                 $res{'status'} = $5;
3276                 $res{'similarity'} = $6;
3277                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3278                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3279                 } else {
3280                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3281                 }
3282         }
3283         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3284         # combined diff (for merge commit)
3285         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3286                 $res{'nparents'}  = length($1);
3287                 $res{'from_mode'} = [ split(' ', $2) ];
3288                 $res{'to_mode'} = pop @{$res{'from_mode'}};
3289                 $res{'from_id'} = [ split(' ', $3) ];
3290                 $res{'to_id'} = pop @{$res{'from_id'}};
3291                 $res{'status'} = [ split('', $4) ];
3292                 $res{'to_file'} = unquote($5);
3293         }
3294         # 'c512b523472485aef4fff9e57b229d9d243c967f'
3295         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3296                 $res{'commit'} = $1;
3297         }
3298
3299         return wantarray ? %res : \%res;
3300 }
3301
3302 # wrapper: return parsed line of git-diff-tree "raw" output
3303 # (the argument might be raw line, or parsed info)
3304 sub parsed_difftree_line {
3305         my $line_or_ref = shift;
3306
3307         if (ref($line_or_ref) eq "HASH") {
3308                 # pre-parsed (or generated by hand)
3309                 return $line_or_ref;
3310         } else {
3311                 return parse_difftree_raw_line($line_or_ref);
3312         }
3313 }
3314
3315 # parse line of git-ls-tree output
3316 sub parse_ls_tree_line {
3317         my $line = shift;
3318         my %opts = @_;
3319         my %res;
3320
3321         if ($opts{'-l'}) {
3322                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717  panic.c'
3323                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3324
3325                 $res{'mode'} = $1;
3326                 $res{'type'} = $2;
3327                 $res{'hash'} = $3;
3328                 $res{'size'} = $4;
3329                 if ($opts{'-z'}) {
3330                         $res{'name'} = $5;
3331                 } else {
3332                         $res{'name'} = unquote($5);
3333                 }
3334         } else {
3335                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
3336                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3337
3338                 $res{'mode'} = $1;
3339                 $res{'type'} = $2;
3340                 $res{'hash'} = $3;
3341                 if ($opts{'-z'}) {
3342                         $res{'name'} = $4;
3343                 } else {
3344                         $res{'name'} = unquote($4);
3345                 }
3346         }
3347
3348         return wantarray ? %res : \%res;
3349 }
3350
3351 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3352 sub parse_from_to_diffinfo {
3353         my ($diffinfo, $from, $to, @parents) = @_;
3354
3355         if ($diffinfo->{'nparents'}) {
3356                 # combined diff
3357                 $from->{'file'} = [];
3358                 $from->{'href'} = [];
3359                 fill_from_file_info($diffinfo, @parents)
3360                         unless exists $diffinfo->{'from_file'};
3361                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3362                         $from->{'file'}[$i] =
3363                                 defined $diffinfo->{'from_file'}[$i] ?
3364                                         $diffinfo->{'from_file'}[$i] :
3365                                         $diffinfo->{'to_file'};
3366                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3367                                 $from->{'href'}[$i] = href(action=>"blob",
3368                                                            hash_base=>$parents[$i],
3369                                                            hash=>$diffinfo->{'from_id'}[$i],
3370                                                            file_name=>$from->{'file'}[$i]);
3371                         } else {
3372                                 $from->{'href'}[$i] = undef;
3373                         }
3374                 }
3375         } else {
3376                 # ordinary (not combined) diff
3377                 $from->{'file'} = $diffinfo->{'from_file'};
3378                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3379                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3380                                                hash=>$diffinfo->{'from_id'},
3381                                                file_name=>$from->{'file'});
3382                 } else {
3383                         delete $from->{'href'};
3384                 }
3385         }
3386
3387         $to->{'file'} = $diffinfo->{'to_file'};
3388         if (!is_deleted($diffinfo)) { # file exists in result
3389                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3390                                      hash=>$diffinfo->{'to_id'},
3391                                      file_name=>$to->{'file'});
3392         } else {
3393                 delete $to->{'href'};
3394         }
3395 }
3396
3397 ## ......................................................................
3398 ## parse to array of hashes functions
3399
3400 sub git_get_heads_list {
3401         my ($limit, @classes) = @_;
3402         @classes = ('heads') unless @classes;
3403         my @patterns = map { "refs/$_" } @classes;
3404         my @headslist;
3405
3406         open my $fd, '-|', git_cmd(), 'for-each-ref',
3407                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3408                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3409                 @patterns
3410                 or return;
3411         while (my $line = <$fd>) {
3412                 my %ref_item;
3413
3414                 chomp $line;
3415                 my ($refinfo, $committerinfo) = split(/\0/, $line);
3416                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3417                 my ($committer, $epoch, $tz) =
3418                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3419                 $ref_item{'fullname'}  = $name;
3420                 $name =~ s!^refs/(?:head|remote)s/!!;
3421
3422                 $ref_item{'name'}  = $name;
3423                 $ref_item{'id'}    = $hash;
3424                 $ref_item{'title'} = $title || '(no commit message)';
3425                 $ref_item{'epoch'} = $epoch;
3426                 if ($epoch) {
3427                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3428                 } else {
3429                         $ref_item{'age'} = "unknown";
3430                 }
3431
3432                 push @headslist, \%ref_item;
3433         }
3434         close $fd;
3435
3436         return wantarray ? @headslist : \@headslist;
3437 }
3438
3439 sub git_get_tags_list {
3440         my $limit = shift;
3441         my @tagslist;
3442
3443         open my $fd, '-|', git_cmd(), 'for-each-ref',
3444                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3445                 '--format=%(objectname) %(objecttype) %(refname) '.
3446                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3447                 'refs/tags'
3448                 or return;
3449         while (my $line = <$fd>) {
3450                 my %ref_item;
3451
3452                 chomp $line;
3453                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3454                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3455                 my ($creator, $epoch, $tz) =
3456                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3457                 $ref_item{'fullname'} = $name;
3458                 $name =~ s!^refs/tags/!!;
3459
3460                 $ref_item{'type'} = $type;
3461                 $ref_item{'id'} = $id;
3462                 $ref_item{'name'} = $name;
3463                 if ($type eq "tag") {
3464                         $ref_item{'subject'} = $title;
3465                         $ref_item{'reftype'} = $reftype;
3466                         $ref_item{'refid'}   = $refid;
3467                 } else {
3468                         $ref_item{'reftype'} = $type;
3469                         $ref_item{'refid'}   = $id;
3470                 }
3471
3472                 if ($type eq "tag" || $type eq "commit") {
3473                         $ref_item{'epoch'} = $epoch;
3474                         if ($epoch) {
3475                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3476                         } else {
3477                                 $ref_item{'age'} = "unknown";
3478                         }
3479                 }
3480
3481                 push @tagslist, \%ref_item;
3482         }
3483         close $fd;
3484
3485         return wantarray ? @tagslist : \@tagslist;
3486 }
3487
3488 ## ----------------------------------------------------------------------
3489 ## filesystem-related functions
3490
3491 sub get_file_owner {
3492         my $path = shift;
3493
3494         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3495         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3496         if (!defined $gcos) {
3497                 return undef;
3498         }
3499         my $owner = $gcos;
3500         $owner =~ s/[,;].*$//;
3501         return to_utf8($owner);
3502 }
3503
3504 # assume that file exists
3505 sub insert_file {
3506         my $filename = shift;
3507
3508         open my $fd, '<', $filename;
3509         print map { to_utf8($_) } <$fd>;
3510         close $fd;
3511 }
3512
3513 ## ......................................................................
3514 ## mimetype related functions
3515
3516 sub mimetype_guess_file {
3517         my $filename = shift;
3518         my $mimemap = shift;
3519         -r $mimemap or return undef;
3520
3521         my %mimemap;
3522         open(my $mh, '<', $mimemap) or return undef;
3523         while (<$mh>) {
3524                 next if m/^#/; # skip comments
3525                 my ($mimetype, $exts) = split(/\t+/);
3526                 if (defined $exts) {
3527                         my @exts = split(/\s+/, $exts);
3528                         foreach my $ext (@exts) {
3529                                 $mimemap{$ext} = $mimetype;
3530                         }
3531                 }
3532         }
3533         close($mh);
3534
3535         $filename =~ /\.([^.]*)$/;
3536         return $mimemap{$1};
3537 }
3538
3539 sub mimetype_guess {
3540         my $filename = shift;
3541         my $mime;
3542         $filename =~ /\./ or return undef;
3543
3544         if ($mimetypes_file) {
3545                 my $file = $mimetypes_file;
3546                 if ($file !~ m!^/!) { # if it is relative path
3547                         # it is relative to project
3548                         $file = "$projectroot/$project/$file";
3549                 }
3550                 $mime = mimetype_guess_file($filename, $file);
3551         }
3552         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3553         return $mime;
3554 }
3555
3556 sub blob_mimetype {
3557         my $fd = shift;
3558         my $filename = shift;
3559
3560         if ($filename) {
3561                 my $mime = mimetype_guess($filename);
3562                 $mime and return $mime;
3563         }
3564
3565         # just in case
3566         return $default_blob_plain_mimetype unless $fd;
3567
3568         if (-T $fd) {
3569                 return 'text/plain';
3570         } elsif (! $filename) {
3571                 return 'application/octet-stream';
3572         } elsif ($filename =~ m/\.png$/i) {
3573                 return 'image/png';
3574         } elsif ($filename =~ m/\.gif$/i) {
3575                 return 'image/gif';
3576         } elsif ($filename =~ m/\.jpe?g$/i) {
3577                 return 'image/jpeg';
3578         } else {
3579                 return 'application/octet-stream';
3580         }
3581 }
3582
3583 sub blob_contenttype {
3584         my ($fd, $file_name, $type) = @_;
3585
3586         $type ||= blob_mimetype($fd, $file_name);
3587         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3588                 $type .= "; charset=$default_text_plain_charset";
3589         }
3590
3591         return $type;
3592 }
3593
3594 # guess file syntax for syntax highlighting; return undef if no highlighting
3595 # the name of syntax can (in the future) depend on syntax highlighter used
3596 sub guess_file_syntax {
3597         my ($highlight, $mimetype, $file_name) = @_;
3598         return undef unless ($highlight && defined $file_name);
3599         my $basename = basename($file_name, '.in');
3600         return $highlight_basename{$basename}
3601                 if exists $highlight_basename{$basename};
3602
3603         $basename =~ /\.([^.]*)$/;
3604         my $ext = $1 or return undef;
3605         return $highlight_ext{$ext}
3606                 if exists $highlight_ext{$ext};
3607
3608         return undef;
3609 }
3610
3611 # run highlighter and return FD of its output,
3612 # or return original FD if no highlighting
3613 sub run_highlighter {
3614         my ($fd, $highlight, $syntax) = @_;
3615         return $fd unless ($highlight && defined $syntax);
3616
3617         close $fd;
3618         open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3619                   quote_command($highlight_bin).
3620                   " --replace-tabs=8 --fragment --syntax $syntax |"
3621                 or die_error(500, "Couldn't open file or run syntax highlighter");
3622         return $fd;
3623 }
3624
3625 ## ======================================================================
3626 ## functions printing HTML: header, footer, error page
3627
3628 sub get_page_title {
3629         my $title = to_utf8($site_name);
3630
3631         return $title unless (defined $project);
3632         $title .= " - " . to_utf8($project);
3633
3634         return $title unless (defined $action);
3635         $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3636
3637         return $title unless (defined $file_name);
3638         $title .= " - " . esc_path($file_name);
3639         if ($action eq "tree" && $file_name !~ m|/$|) {
3640                 $title .= "/";
3641         }
3642
3643         return $title;
3644 }
3645
3646 sub print_feed_meta {
3647         if (defined $project) {
3648                 my %href_params = get_feed_info();
3649                 if (!exists $href_params{'-title'}) {
3650                         $href_params{'-title'} = 'log';
3651                 }
3652
3653                 foreach my $format (qw(RSS Atom)) {
3654                         my $type = lc($format);
3655                         my %link_attr = (
3656                                 '-rel' => 'alternate',
3657                                 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3658                                 '-type' => "application/$type+xml"
3659                         );
3660
3661                         $href_params{'action'} = $type;
3662                         $link_attr{'-href'} = href(%href_params);
3663                         print "<link ".
3664                               "rel=\"$link_attr{'-rel'}\" ".
3665                               "title=\"$link_attr{'-title'}\" ".
3666                               "href=\"$link_attr{'-href'}\" ".
3667                               "type=\"$link_attr{'-type'}\" ".
3668                               "/>\n";
3669
3670                         $href_params{'extra_options'} = '--no-merges';
3671                         $link_attr{'-href'} = href(%href_params);
3672                         $link_attr{'-title'} .= ' (no merges)';
3673                         print "<link ".
3674                               "rel=\"$link_attr{'-rel'}\" ".
3675                               "title=\"$link_attr{'-title'}\" ".
3676                               "href=\"$link_attr{'-href'}\" ".
3677                               "type=\"$link_attr{'-type'}\" ".
3678                               "/>\n";
3679                 }
3680
3681         } else {
3682                 printf('<link rel="alternate" title="%s projects list" '.
3683                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
3684                        esc_attr($site_name), href(project=>undef, action=>"project_index"));
3685                 printf('<link rel="alternate" title="%s projects feeds" '.
3686                        'href="%s" type="text/x-opml" />'."\n",
3687                        esc_attr($site_name), href(project=>undef, action=>"opml"));
3688         }
3689 }
3690
3691 sub git_header_html {
3692         my $status = shift || "200 OK";
3693         my $expires = shift;
3694         my %opts = @_;
3695
3696         my $title = get_page_title();
3697         my $content_type;
3698         # require explicit support from the UA if we are to send the page as
3699         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3700         # we have to do this because MSIE sometimes globs '*/*', pretending to
3701         # support xhtml+xml but choking when it gets what it asked for.
3702         if (defined $cgi->http('HTTP_ACCEPT') &&
3703             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3704             $cgi->Accept('application/xhtml+xml') != 0) {
3705                 $content_type = 'application/xhtml+xml';
3706         } else {
3707                 $content_type = 'text/html';
3708         }
3709         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3710                            -status=> $status, -expires => $expires)
3711                 unless ($opts{'-no_http_header'});
3712         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3713         print <<EOF;
3714 <?xml version="1.0" encoding="utf-8"?>
3715 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3716 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3717 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3718 <!-- git core binaries version $git_version -->
3719 <head>
3720 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3721 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3722 <meta name="robots" content="index, nofollow"/>
3723 <title>$title</title>
3724 EOF
3725         # the stylesheet, favicon etc urls won't work correctly with path_info
3726         # unless we set the appropriate base URL
3727         if ($ENV{'PATH_INFO'}) {
3728                 print "<base href=\"".esc_url($base_url)."\" />\n";
3729         }
3730         # print out each stylesheet that exist, providing backwards capability
3731         # for those people who defined $stylesheet in a config file
3732         if (defined $stylesheet) {
3733                 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3734         } else {
3735                 foreach my $stylesheet (@stylesheets) {
3736                         next unless $stylesheet;
3737                         print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3738                 }
3739         }
3740         print_feed_meta()
3741                 if ($status eq '200 OK');
3742         if (defined $favicon) {
3743                 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3744         }
3745
3746         print "</head>\n" .
3747               "<body>\n";
3748
3749         if (defined $site_header && -f $site_header) {
3750                 insert_file($site_header);
3751         }
3752
3753         print "<div class=\"page_header\">\n";
3754         if (defined $logo) {
3755                 print $cgi->a({-href => esc_url($logo_url),
3756                                -title => $logo_label},
3757                               $cgi->img({-src => esc_url($logo),
3758                                          -width => 72, -height => 27,
3759                                          -alt => "git",
3760                                          -class => "logo"}));
3761         }
3762         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3763         if (defined $project) {
3764                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3765                 if (defined $action) {
3766                         my $action_print = $action ;
3767                         if (defined $opts{-action_extra}) {
3768                                 $action_print = $cgi->a({-href => href(action=>$action)},
3769                                         $action);
3770                         }
3771                         print " / $action_print";
3772                 }
3773                 if (defined $opts{-action_extra}) {
3774                         print " / $opts{-action_extra}";
3775                 }
3776                 print "\n";
3777         }
3778         print "</div>\n";
3779
3780         my $have_search = gitweb_check_feature('search');
3781         if (defined $project && $have_search) {
3782                 if (!defined $searchtext) {
3783                         $searchtext = "";
3784                 }
3785                 my $search_hash;
3786                 if (defined $hash_base) {
3787                         $search_hash = $hash_base;
3788                 } elsif (defined $hash) {
3789                         $search_hash = $hash;
3790                 } else {
3791                         $search_hash = "HEAD";
3792                 }
3793                 my $action = $my_uri;
3794                 my $use_pathinfo = gitweb_check_feature('pathinfo');
3795                 if ($use_pathinfo) {
3796                         $action .= "/".esc_url($project);
3797                 }
3798                 print $cgi->startform(-method => "get", -action => $action) .
3799                       "<div class=\"search\">\n" .
3800                       (!$use_pathinfo &&
3801                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3802                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3803                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3804                       $cgi->popup_menu(-name => 'st', -default => 'commit',
3805                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3806                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3807                       " search:\n",
3808                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3809                       "<span title=\"Extended regular expression\">" .
3810                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3811                                      -checked => $search_use_regexp) .
3812                       "</span>" .
3813                       "</div>" .
3814                       $cgi->end_form() . "\n";
3815         }
3816 }
3817
3818 sub git_footer_html {
3819         my $feed_class = 'rss_logo';
3820
3821         print "<div class=\"page_footer\">\n";
3822         if (defined $project) {
3823                 my $descr = git_get_project_description($project);
3824                 if (defined $descr) {
3825                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3826                 }
3827
3828                 my %href_params = get_feed_info();
3829                 if (!%href_params) {
3830                         $feed_class .= ' generic';
3831                 }
3832                 $href_params{'-title'} ||= 'log';
3833
3834                 foreach my $format (qw(RSS Atom)) {
3835                         $href_params{'action'} = lc($format);
3836                         print $cgi->a({-href => href(%href_params),
3837                                       -title => "$href_params{'-title'} $format feed",
3838                                       -class => $feed_class}, $format)."\n";
3839                 }
3840
3841         } else {
3842                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3843                               -class => $feed_class}, "OPML") . " ";
3844                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3845                               -class => $feed_class}, "TXT") . "\n";
3846         }
3847         print "</div>\n"; # class="page_footer"
3848
3849         if (defined $t0 && gitweb_check_feature('timed')) {
3850                 print "<div id=\"generating_info\">\n";
3851                 print 'This page took '.
3852                       '<span id="generating_time" class="time_span">'.
3853                       tv_interval($t0, [ gettimeofday() ]).
3854                       ' seconds </span>'.
3855                       ' and '.
3856                       '<span id="generating_cmd">'.
3857                       $number_of_git_cmds.
3858                       '</span> git commands '.
3859                       " to generate.\n";
3860                 print "</div>\n"; # class="page_footer"
3861         }
3862
3863         if (defined $site_footer && -f $site_footer) {
3864                 insert_file($site_footer);
3865         }
3866
3867         print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
3868         if (defined $action &&
3869             $action eq 'blame_incremental') {
3870                 print qq!<script type="text/javascript">\n!.
3871                       qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3872                       qq!           "!. href() .qq!");\n!.
3873                       qq!</script>\n!;
3874         } elsif (gitweb_check_feature('javascript-actions')) {
3875                 print qq!<script type="text/javascript">\n!.
3876                       qq!window.onload = fixLinks;\n!.
3877                       qq!</script>\n!;
3878         }
3879
3880         print "</body>\n" .
3881               "</html>";
3882 }
3883
3884 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3885 # Example: die_error(404, 'Hash not found')
3886 # By convention, use the following status codes (as defined in RFC 2616):
3887 # 400: Invalid or missing CGI parameters, or
3888 #      requested object exists but has wrong type.
3889 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3890 #      this server or project.
3891 # 404: Requested object/revision/project doesn't exist.
3892 # 500: The server isn't configured properly, or
3893 #      an internal error occurred (e.g. failed assertions caused by bugs), or
3894 #      an unknown error occurred (e.g. the git binary died unexpectedly).
3895 # 503: The server is currently unavailable (because it is overloaded,
3896 #      or down for maintenance).  Generally, this is a temporary state.
3897 sub die_error {
3898         my $status = shift || 500;
3899         my $error = esc_html(shift) || "Internal Server Error";
3900         my $extra = shift;
3901         my %opts = @_;
3902
3903         my %http_responses = (
3904                 400 => '400 Bad Request',
3905                 403 => '403 Forbidden',
3906                 404 => '404 Not Found',
3907                 500 => '500 Internal Server Error',
3908                 503 => '503 Service Unavailable',
3909         );
3910         git_header_html($http_responses{$status}, undef, %opts);
3911         print <<EOF;
3912 <div class="page_body">
3913 <br /><br />
3914 $status - $error
3915 <br />
3916 EOF
3917         if (defined $extra) {
3918                 print "<hr />\n" .
3919                       "$extra\n";
3920         }
3921         print "</div>\n";
3922
3923         git_footer_html();
3924         goto DONE_GITWEB
3925                 unless ($opts{'-error_handler'});
3926 }
3927
3928 ## ----------------------------------------------------------------------
3929 ## functions printing or outputting HTML: navigation
3930
3931 sub git_print_page_nav {
3932         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3933         $extra = '' if !defined $extra; # pager or formats
3934
3935         my @navs = qw(summary shortlog log commit commitdiff tree);
3936         if ($suppress) {
3937                 @navs = grep { $_ ne $suppress } @navs;
3938         }
3939
3940         my %arg = map { $_ => {action=>$_} } @navs;
3941         if (defined $head) {
3942                 for (qw(commit commitdiff)) {
3943                         $arg{$_}{'hash'} = $head;
3944                 }
3945                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3946                         for (qw(shortlog log)) {
3947                                 $arg{$_}{'hash'} = $head;
3948                         }
3949                 }
3950         }
3951
3952         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3953         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3954
3955         my @actions = gitweb_get_feature('actions');
3956         my %repl = (
3957                 '%' => '%',
3958                 'n' => $project,         # project name
3959                 'f' => $git_dir,         # project path within filesystem
3960                 'h' => $treehead || '',  # current hash ('h' parameter)
3961                 'b' => $treebase || '',  # hash base ('hb' parameter)
3962         );
3963         while (@actions) {
3964                 my ($label, $link, $pos) = splice(@actions,0,3);
3965                 # insert
3966                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3967                 # munch munch
3968                 $link =~ s/%([%nfhb])/$repl{$1}/g;
3969                 $arg{$label}{'_href'} = $link;
3970         }
3971
3972         print "<div class=\"page_nav\">\n" .
3973                 (join " | ",
3974                  map { $_ eq $current ?
3975                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3976                  } @navs);
3977         print "<br/>\n$extra<br/>\n" .
3978               "</div>\n";
3979 }
3980
3981 # returns a submenu for the nagivation of the refs views (tags, heads,
3982 # remotes) with the current view disabled and the remotes view only
3983 # available if the feature is enabled
3984 sub format_ref_views {
3985         my ($current) = @_;
3986         my @ref_views = qw{tags heads};
3987         push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
3988         return join " | ", map {
3989                 $_ eq $current ? $_ :
3990                 $cgi->a({-href => href(action=>$_)}, $_)
3991         } @ref_views
3992 }
3993
3994 sub format_paging_nav {
3995         my ($action, $page, $has_next_link) = @_;
3996         my $paging_nav;
3997
3998
3999         if ($page > 0) {
4000                 $paging_nav .=
4001                         $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4002                         " &sdot; " .
4003                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
4004                                  -accesskey => "p", -title => "Alt-p"}, "prev");
4005         } else {
4006                 $paging_nav .= "first &sdot; prev";
4007         }
4008
4009         if ($has_next_link) {
4010                 $paging_nav .= " &sdot; " .
4011                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
4012                                  -accesskey => "n", -title => "Alt-n"}, "next");
4013         } else {
4014                 $paging_nav .= " &sdot; next";
4015         }
4016
4017         return $paging_nav;
4018 }
4019
4020 ## ......................................................................
4021 ## functions printing or outputting HTML: div
4022
4023 sub git_print_header_div {
4024         my ($action, $title, $hash, $hash_base) = @_;
4025         my %args = ();
4026
4027         $args{'action'} = $action;
4028         $args{'hash'} = $hash if $hash;
4029         $args{'hash_base'} = $hash_base if $hash_base;
4030
4031         print "<div class=\"header\">\n" .
4032               $cgi->a({-href => href(%args), -class => "title"},
4033               $title ? $title : $action) .
4034               "\n</div>\n";
4035 }
4036
4037 sub format_repo_url {
4038         my ($name, $url) = @_;
4039         return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4040 }
4041
4042 # Group output by placing it in a DIV element and adding a header.
4043 # Options for start_div() can be provided by passing a hash reference as the
4044 # first parameter to the function.
4045 # Options to git_print_header_div() can be provided by passing an array
4046 # reference. This must follow the options to start_div if they are present.
4047 # The content can be a scalar, which is output as-is, a scalar reference, which
4048 # is output after html escaping, an IO handle passed either as *handle or
4049 # *handle{IO}, or a function reference. In the latter case all following
4050 # parameters will be taken as argument to the content function call.
4051 sub git_print_section {
4052         my ($div_args, $header_args, $content);
4053         my $arg = shift;
4054         if (ref($arg) eq 'HASH') {
4055                 $div_args = $arg;
4056                 $arg = shift;
4057         }
4058         if (ref($arg) eq 'ARRAY') {
4059                 $header_args = $arg;
4060                 $arg = shift;
4061         }
4062         $content = $arg;
4063
4064         print $cgi->start_div($div_args);
4065         git_print_header_div(@$header_args);
4066
4067         if (ref($content) eq 'CODE') {
4068                 $content->(@_);
4069         } elsif (ref($content) eq 'SCALAR') {
4070                 print esc_html($$content);
4071         } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4072                 print <$content>;
4073         } elsif (!ref($content) && defined($content)) {
4074                 print $content;
4075         }
4076
4077         print $cgi->end_div;
4078 }
4079
4080 sub print_local_time {
4081         print format_local_time(@_);
4082 }
4083
4084 sub format_local_time {
4085         my $localtime = '';
4086         my %date = @_;
4087         if ($date{'hour_local'} < 6) {
4088                 $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4089                         $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
4090         } else {
4091                 $localtime .= sprintf(" (%02d:%02d %s)",
4092                         $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
4093         }
4094
4095         return $localtime;
4096 }
4097
4098 # Outputs the author name and date in long form
4099 sub git_print_authorship {
4100         my $co = shift;
4101         my %opts = @_;
4102         my $tag = $opts{-tag} || 'div';
4103         my $author = $co->{'author_name'};
4104
4105         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4106         print "<$tag class=\"author_date\">" .
4107               format_search_author($author, "author", esc_html($author)) .
4108               " [$ad{'rfc2822'}";
4109         print_local_time(%ad) if ($opts{-localtime});
4110         print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
4111                   . "</$tag>\n";
4112 }
4113
4114 # Outputs table rows containing the full author or committer information,
4115 # in the format expected for 'commit' view (& similar).
4116 # Parameters are a commit hash reference, followed by the list of people
4117 # to output information for. If the list is empty it defaults to both
4118 # author and committer.
4119 sub git_print_authorship_rows {
4120         my $co = shift;
4121         # too bad we can't use @people = @_ || ('author', 'committer')
4122         my @people = @_;
4123         @people = ('author', 'committer') unless @people;
4124         foreach my $who (@people) {
4125                 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4126                 print "<tr><td>$who</td><td>" .
4127                       format_search_author($co->{"${who}_name"}, $who,
4128                                esc_html($co->{"${who}_name"})) . " " .
4129                       format_search_author($co->{"${who}_email"}, $who,
4130                                esc_html("<" . $co->{"${who}_email"} . ">")) .
4131                       "</td><td rowspan=\"2\">" .
4132                       git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4133                       "</td></tr>\n" .
4134                       "<tr>" .
4135                       "<td></td><td> $wd{'rfc2822'}";
4136                 print_local_time(%wd);
4137                 print "</td>" .
4138                       "</tr>\n";
4139         }
4140 }
4141
4142 sub git_print_page_path {
4143         my $name = shift;
4144         my $type = shift;
4145         my $hb = shift;
4146
4147
4148         print "<div class=\"page_path\">";
4149         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4150                       -title => 'tree root'}, to_utf8("[$project]"));
4151         print " / ";
4152         if (defined $name) {
4153                 my @dirname = split '/', $name;
4154                 my $basename = pop @dirname;
4155                 my $fullname = '';
4156
4157                 foreach my $dir (@dirname) {
4158                         $fullname .= ($fullname ? '/' : '') . $dir;
4159                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4160                                                      hash_base=>$hb),
4161                                       -title => $fullname}, esc_path($dir));
4162                         print " / ";
4163                 }
4164                 if (defined $type && $type eq 'blob') {
4165                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4166                                                      hash_base=>$hb),
4167                                       -title => $name}, esc_path($basename));
4168                 } elsif (defined $type && $type eq 'tree') {
4169                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4170                                                      hash_base=>$hb),
4171                                       -title => $name}, esc_path($basename));
4172                         print " / ";
4173                 } else {
4174                         print esc_path($basename);
4175                 }
4176         }
4177         print "<br/></div>\n";
4178 }
4179
4180 sub git_print_log {
4181         my $log = shift;
4182         my %opts = @_;
4183
4184         if ($opts{'-remove_title'}) {
4185                 # remove title, i.e. first line of log
4186                 shift @$log;
4187         }
4188         # remove leading empty lines
4189         while (defined $log->[0] && $log->[0] eq "") {
4190                 shift @$log;
4191         }
4192
4193         # print log
4194         my $signoff = 0;
4195         my $empty = 0;
4196         foreach my $line (@$log) {
4197                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4198                         $signoff = 1;
4199                         $empty = 0;
4200                         if (! $opts{'-remove_signoff'}) {
4201                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4202                                 next;
4203                         } else {
4204                                 # remove signoff lines
4205                                 next;
4206                         }
4207                 } else {
4208                         $signoff = 0;
4209                 }
4210
4211                 # print only one empty line
4212                 # do not print empty line after signoff
4213                 if ($line eq "") {
4214                         next if ($empty || $signoff);
4215                         $empty = 1;
4216                 } else {
4217                         $empty = 0;
4218                 }
4219
4220                 print format_log_line_html($line) . "<br/>\n";
4221         }
4222
4223         if ($opts{'-final_empty_line'}) {
4224                 # end with single empty line
4225                 print "<br/>\n" unless $empty;
4226         }
4227 }
4228
4229 # return link target (what link points to)
4230 sub git_get_link_target {
4231         my $hash = shift;
4232         my $link_target;
4233
4234         # read link
4235         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4236                 or return;
4237         {
4238                 local $/ = undef;
4239                 $link_target = <$fd>;
4240         }
4241         close $fd
4242                 or return;
4243
4244         return $link_target;
4245 }
4246
4247 # given link target, and the directory (basedir) the link is in,
4248 # return target of link relative to top directory (top tree);
4249 # return undef if it is not possible (including absolute links).
4250 sub normalize_link_target {
4251         my ($link_target, $basedir) = @_;
4252
4253         # absolute symlinks (beginning with '/') cannot be normalized
4254         return if (substr($link_target, 0, 1) eq '/');
4255
4256         # normalize link target to path from top (root) tree (dir)
4257         my $path;
4258         if ($basedir) {
4259                 $path = $basedir . '/' . $link_target;
4260         } else {
4261                 # we are in top (root) tree (dir)
4262                 $path = $link_target;
4263         }
4264
4265         # remove //, /./, and /../
4266         my @path_parts;
4267         foreach my $part (split('/', $path)) {
4268                 # discard '.' and ''
4269                 next if (!$part || $part eq '.');
4270                 # handle '..'
4271                 if ($part eq '..') {
4272                         if (@path_parts) {
4273                                 pop @path_parts;
4274                         } else {
4275                                 # link leads outside repository (outside top dir)
4276                                 return;
4277                         }
4278                 } else {
4279                         push @path_parts, $part;
4280                 }
4281         }
4282         $path = join('/', @path_parts);
4283
4284         return $path;
4285 }
4286
4287 # print tree entry (row of git_tree), but without encompassing <tr> element
4288 sub git_print_tree_entry {
4289         my ($t, $basedir, $hash_base, $have_blame) = @_;
4290
4291         my %base_key = ();
4292         $base_key{'hash_base'} = $hash_base if defined $hash_base;
4293
4294         # The format of a table row is: mode list link.  Where mode is
4295         # the mode of the entry, list is the name of the entry, an href,
4296         # and link is the action links of the entry.
4297
4298         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4299         if (exists $t->{'size'}) {
4300                 print "<td class=\"size\">$t->{'size'}</td>\n";
4301         }
4302         if ($t->{'type'} eq "blob") {
4303                 print "<td class=\"list\">" .
4304                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4305                                                file_name=>"$basedir$t->{'name'}", %base_key),
4306                                 -class => "list"}, esc_path($t->{'name'}));
4307                 if (S_ISLNK(oct $t->{'mode'})) {
4308                         my $link_target = git_get_link_target($t->{'hash'});
4309                         if ($link_target) {
4310                                 my $norm_target = normalize_link_target($link_target, $basedir);
4311                                 if (defined $norm_target) {
4312                                         print " -> " .
4313                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4314                                                                      file_name=>$norm_target),
4315                                                        -title => $norm_target}, esc_path($link_target));
4316                                 } else {
4317                                         print " -> " . esc_path($link_target);
4318                                 }
4319                         }
4320                 }
4321                 print "</td>\n";
4322                 print "<td class=\"link\">";
4323                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4324                                              file_name=>"$basedir$t->{'name'}", %base_key)},
4325                               "blob");
4326                 if ($have_blame) {
4327                         print " | " .
4328                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4329                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
4330                                       "blame");
4331                 }
4332                 if (defined $hash_base) {
4333                         print " | " .
4334                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4335                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4336                                       "history");
4337                 }
4338                 print " | " .
4339                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4340                                                file_name=>"$basedir$t->{'name'}")},
4341                                 "raw");
4342                 print "</td>\n";
4343
4344         } elsif ($t->{'type'} eq "tree") {
4345                 print "<td class=\"list\">";
4346                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4347                                              file_name=>"$basedir$t->{'name'}",
4348                                              %base_key)},
4349                               esc_path($t->{'name'}));
4350                 print "</td>\n";
4351                 print "<td class=\"link\">";
4352                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4353                                              file_name=>"$basedir$t->{'name'}",
4354                                              %base_key)},
4355                               "tree");
4356                 if (defined $hash_base) {
4357                         print " | " .
4358                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4359                                                      file_name=>"$basedir$t->{'name'}")},
4360                                       "history");
4361                 }
4362                 print "</td>\n";
4363         } else {
4364                 # unknown object: we can only present history for it
4365                 # (this includes 'commit' object, i.e. submodule support)
4366                 print "<td class=\"list\">" .
4367                       esc_path($t->{'name'}) .
4368                       "</td>\n";
4369                 print "<td class=\"link\">";
4370                 if (defined $hash_base) {
4371                         print $cgi->a({-href => href(action=>"history",
4372                                                      hash_base=>$hash_base,
4373                                                      file_name=>"$basedir$t->{'name'}")},
4374                                       "history");
4375                 }
4376                 print "</td>\n";
4377         }
4378 }
4379
4380 ## ......................................................................
4381 ## functions printing large fragments of HTML
4382
4383 # get pre-image filenames for merge (combined) diff
4384 sub fill_from_file_info {
4385         my ($diff, @parents) = @_;
4386
4387         $diff->{'from_file'} = [ ];
4388         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4389         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4390                 if ($diff->{'status'}[$i] eq 'R' ||
4391                     $diff->{'status'}[$i] eq 'C') {
4392                         $diff->{'from_file'}[$i] =
4393                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4394                 }
4395         }
4396
4397         return $diff;
4398 }
4399
4400 # is current raw difftree line of file deletion
4401 sub is_deleted {
4402         my $diffinfo = shift;
4403
4404         return $diffinfo->{'to_id'} eq ('0' x 40);
4405 }
4406
4407 # does patch correspond to [previous] difftree raw line
4408 # $diffinfo  - hashref of parsed raw diff format
4409 # $patchinfo - hashref of parsed patch diff format
4410 #              (the same keys as in $diffinfo)
4411 sub is_patch_split {
4412         my ($diffinfo, $patchinfo) = @_;
4413
4414         return defined $diffinfo && defined $patchinfo
4415                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4416 }
4417
4418
4419 sub git_difftree_body {
4420         my ($difftree, $hash, @parents) = @_;
4421         my ($parent) = $parents[0];
4422         my $have_blame = gitweb_check_feature('blame');
4423         print "<div class=\"list_head\">\n";
4424         if ($#{$difftree} > 10) {
4425                 print(($#{$difftree} + 1) . " files changed:\n");
4426         }
4427         print "</div>\n";
4428
4429         print "<table class=\"" .
4430               (@parents > 1 ? "combined " : "") .
4431               "diff_tree\">\n";
4432
4433         # header only for combined diff in 'commitdiff' view
4434         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4435         if ($has_header) {
4436                 # table header
4437                 print "<thead><tr>\n" .
4438                        "<th></th><th></th>\n"; # filename, patchN link
4439                 for (my $i = 0; $i < @parents; $i++) {
4440                         my $par = $parents[$i];
4441                         print "<th>" .
4442                               $cgi->a({-href => href(action=>"commitdiff",
4443                                                      hash=>$hash, hash_parent=>$par),
4444                                        -title => 'commitdiff to parent number ' .
4445                                                   ($i+1) . ': ' . substr($par,0,7)},
4446                                       $i+1) .
4447                               "&nbsp;</th>\n";
4448                 }
4449                 print "</tr></thead>\n<tbody>\n";
4450         }
4451
4452         my $alternate = 1;
4453         my $patchno = 0;
4454         foreach my $line (@{$difftree}) {
4455                 my $diff = parsed_difftree_line($line);
4456
4457                 if ($alternate) {
4458                         print "<tr class=\"dark\">\n";
4459                 } else {
4460                         print "<tr class=\"light\">\n";
4461                 }
4462                 $alternate ^= 1;
4463
4464                 if (exists $diff->{'nparents'}) { # combined diff
4465
4466                         fill_from_file_info($diff, @parents)
4467                                 unless exists $diff->{'from_file'};
4468
4469                         if (!is_deleted($diff)) {
4470                                 # file exists in the result (child) commit
4471                                 print "<td>" .
4472                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4473                                                              file_name=>$diff->{'to_file'},
4474                                                              hash_base=>$hash),
4475                                               -class => "list"}, esc_path($diff->{'to_file'})) .
4476                                       "</td>\n";
4477                         } else {
4478                                 print "<td>" .
4479                                       esc_path($diff->{'to_file'}) .
4480                                       "</td>\n";
4481                         }
4482
4483                         if ($action eq 'commitdiff') {
4484                                 # link to patch
4485                                 $patchno++;
4486                                 print "<td class=\"link\">" .
4487                                       $cgi->a({-href => href(-anchor=>"patch$patchno")},
4488                                               "patch") .
4489                                       " | " .
4490                                       "</td>\n";
4491                         }
4492
4493                         my $has_history = 0;
4494                         my $not_deleted = 0;
4495                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4496                                 my $hash_parent = $parents[$i];
4497                                 my $from_hash = $diff->{'from_id'}[$i];
4498                                 my $from_path = $diff->{'from_file'}[$i];
4499                                 my $status = $diff->{'status'}[$i];
4500
4501                                 $has_history ||= ($status ne 'A');
4502                                 $not_deleted ||= ($status ne 'D');
4503
4504                                 if ($status eq 'A') {
4505                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
4506                                 } elsif ($status eq 'D') {
4507                                         print "<td class=\"link\">" .
4508                                               $cgi->a({-href => href(action=>"blob",
4509                                                                      hash_base=>$hash,
4510                                                                      hash=>$from_hash,
4511                                                                      file_name=>$from_path)},
4512                                                       "blob" . ($i+1)) .
4513                                               " | </td>\n";
4514                                 } else {
4515                                         if ($diff->{'to_id'} eq $from_hash) {
4516                                                 print "<td class=\"link nochange\">";
4517                                         } else {
4518                                                 print "<td class=\"link\">";
4519                                         }
4520                                         print $cgi->a({-href => href(action=>"blobdiff",
4521                                                                      hash=>$diff->{'to_id'},
4522                                                                      hash_parent=>$from_hash,
4523                                                                      hash_base=>$hash,
4524                                                                      hash_parent_base=>$hash_parent,
4525                                                                      file_name=>$diff->{'to_file'},
4526                                                                      file_parent=>$from_path)},
4527                                                       "diff" . ($i+1)) .
4528                                               " | </td>\n";
4529                                 }
4530                         }
4531
4532                         print "<td class=\"link\">";
4533                         if ($not_deleted) {
4534                                 print $cgi->a({-href => href(action=>"blob",
4535                                                              hash=>$diff->{'to_id'},
4536                                                              file_name=>$diff->{'to_file'},
4537                                                              hash_base=>$hash)},
4538                                               "blob");
4539                                 print " | " if ($has_history);
4540                         }
4541                         if ($has_history) {
4542                                 print $cgi->a({-href => href(action=>"history",
4543                                                              file_name=>$diff->{'to_file'},
4544                                                              hash_base=>$hash)},
4545                                               "history");
4546                         }
4547                         print "</td>\n";
4548
4549                         print "</tr>\n";
4550                         next; # instead of 'else' clause, to avoid extra indent
4551                 }
4552                 # else ordinary diff
4553
4554                 my ($to_mode_oct, $to_mode_str, $to_file_type);
4555                 my ($from_mode_oct, $from_mode_str, $from_file_type);
4556                 if ($diff->{'to_mode'} ne ('0' x 6)) {
4557                         $to_mode_oct = oct $diff->{'to_mode'};
4558                         if (S_ISREG($to_mode_oct)) { # only for regular file
4559                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4560                         }
4561                         $to_file_type = file_type($diff->{'to_mode'});
4562                 }
4563                 if ($diff->{'from_mode'} ne ('0' x 6)) {
4564                         $from_mode_oct = oct $diff->{'from_mode'};
4565                         if (S_ISREG($from_mode_oct)) { # only for regular file
4566                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4567                         }
4568                         $from_file_type = file_type($diff->{'from_mode'});
4569                 }
4570
4571                 if ($diff->{'status'} eq "A") { # created
4572                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4573                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
4574                         $mode_chng   .= "]</span>";
4575                         print "<td>";
4576                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4577                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4578                                       -class => "list"}, esc_path($diff->{'file'}));
4579                         print "</td>\n";
4580                         print "<td>$mode_chng</td>\n";
4581                         print "<td class=\"link\">";
4582                         if ($action eq 'commitdiff') {
4583                                 # link to patch
4584                                 $patchno++;
4585                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4586                                               "patch") .
4587                                       " | ";
4588                         }
4589                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4590                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4591                                       "blob");
4592                         print "</td>\n";
4593
4594                 } elsif ($diff->{'status'} eq "D") { # deleted
4595                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4596                         print "<td>";
4597                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4598                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
4599                                        -class => "list"}, esc_path($diff->{'file'}));
4600                         print "</td>\n";
4601                         print "<td>$mode_chng</td>\n";
4602                         print "<td class=\"link\">";
4603                         if ($action eq 'commitdiff') {
4604                                 # link to patch
4605                                 $patchno++;
4606                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4607                                               "patch") .
4608                                       " | ";
4609                         }
4610                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4611                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
4612                                       "blob") . " | ";
4613                         if ($have_blame) {
4614                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4615                                                              file_name=>$diff->{'file'})},
4616                                               "blame") . " | ";
4617                         }
4618                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4619                                                      file_name=>$diff->{'file'})},
4620                                       "history");
4621                         print "</td>\n";
4622
4623                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4624                         my $mode_chnge = "";
4625                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4626                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4627                                 if ($from_file_type ne $to_file_type) {
4628                                         $mode_chnge .= " from $from_file_type to $to_file_type";
4629                                 }
4630                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4631                                         if ($from_mode_str && $to_mode_str) {
4632                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4633                                         } elsif ($to_mode_str) {
4634                                                 $mode_chnge .= " mode: $to_mode_str";
4635                                         }
4636                                 }
4637                                 $mode_chnge .= "]</span>\n";
4638                         }
4639                         print "<td>";
4640                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4641                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4642                                       -class => "list"}, esc_path($diff->{'file'}));
4643                         print "</td>\n";
4644                         print "<td>$mode_chnge</td>\n";
4645                         print "<td class=\"link\">";
4646                         if ($action eq 'commitdiff') {
4647                                 # link to patch
4648                                 $patchno++;
4649                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4650                                               "patch") .
4651                                       " | ";
4652                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4653                                 # "commit" view and modified file (not onlu mode changed)
4654                                 print $cgi->a({-href => href(action=>"blobdiff",
4655                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4656                                                              hash_base=>$hash, hash_parent_base=>$parent,
4657                                                              file_name=>$diff->{'file'})},
4658                                               "diff") .
4659                                       " | ";
4660                         }
4661                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4662                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4663                                        "blob") . " | ";
4664                         if ($have_blame) {
4665                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4666                                                              file_name=>$diff->{'file'})},
4667                                               "blame") . " | ";
4668                         }
4669                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4670                                                      file_name=>$diff->{'file'})},
4671                                       "history");
4672                         print "</td>\n";
4673
4674                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4675                         my %status_name = ('R' => 'moved', 'C' => 'copied');
4676                         my $nstatus = $status_name{$diff->{'status'}};
4677                         my $mode_chng = "";
4678                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4679                                 # mode also for directories, so we cannot use $to_mode_str
4680                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4681                         }
4682                         print "<td>" .
4683                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4684                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4685                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4686                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4687                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4688                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4689                                       -class => "list"}, esc_path($diff->{'from_file'})) .
4690                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4691                               "<td class=\"link\">";
4692                         if ($action eq 'commitdiff') {
4693                                 # link to patch
4694                                 $patchno++;
4695                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4696                                               "patch") .
4697                                       " | ";
4698                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4699                                 # "commit" view and modified file (not only pure rename or copy)
4700                                 print $cgi->a({-href => href(action=>"blobdiff",
4701                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4702                                                              hash_base=>$hash, hash_parent_base=>$parent,
4703                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4704                                               "diff") .
4705                                       " | ";
4706                         }
4707                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4708                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
4709                                       "blob") . " | ";
4710                         if ($have_blame) {
4711                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4712                                                              file_name=>$diff->{'to_file'})},
4713                                               "blame") . " | ";
4714                         }
4715                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4716                                                     file_name=>$diff->{'to_file'})},
4717                                       "history");
4718                         print "</td>\n";
4719
4720                 } # we should not encounter Unmerged (U) or Unknown (X) status
4721                 print "</tr>\n";
4722         }
4723         print "</tbody>" if $has_header;
4724         print "</table>\n";
4725 }
4726
4727 sub git_patchset_body {
4728         my ($fd, $difftree, $hash, @hash_parents) = @_;
4729         my ($hash_parent) = $hash_parents[0];
4730
4731         my $is_combined = (@hash_parents > 1);
4732         my $patch_idx = 0;
4733         my $patch_number = 0;
4734         my $patch_line;
4735         my $diffinfo;
4736         my $to_name;
4737         my (%from, %to);
4738
4739         print "<div class=\"patchset\">\n";
4740
4741         # skip to first patch
4742         while ($patch_line = <$fd>) {
4743                 chomp $patch_line;
4744
4745                 last if ($patch_line =~ m/^diff /);
4746         }
4747
4748  PATCH:
4749         while ($patch_line) {
4750
4751                 # parse "git diff" header line
4752                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4753                         # $1 is from_name, which we do not use
4754                         $to_name = unquote($2);
4755                         $to_name =~ s!^b/!!;
4756                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4757                         # $1 is 'cc' or 'combined', which we do not use
4758                         $to_name = unquote($2);
4759                 } else {
4760                         $to_name = undef;
4761                 }
4762
4763                 # check if current patch belong to current raw line
4764                 # and parse raw git-diff line if needed
4765                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4766                         # this is continuation of a split patch
4767                         print "<div class=\"patch cont\">\n";
4768                 } else {
4769                         # advance raw git-diff output if needed
4770                         $patch_idx++ if defined $diffinfo;
4771
4772                         # read and prepare patch information
4773                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4774
4775                         # compact combined diff output can have some patches skipped
4776                         # find which patch (using pathname of result) we are at now;
4777                         if ($is_combined) {
4778                                 while ($to_name ne $diffinfo->{'to_file'}) {
4779                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4780                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
4781                                               "</div>\n";  # class="patch"
4782
4783                                         $patch_idx++;
4784                                         $patch_number++;
4785
4786                                         last if $patch_idx > $#$difftree;
4787                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4788                                 }
4789                         }
4790
4791                         # modifies %from, %to hashes
4792                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4793
4794                         # this is first patch for raw difftree line with $patch_idx index
4795                         # we index @$difftree array from 0, but number patches from 1
4796                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4797                 }
4798
4799                 # git diff header
4800                 #assert($patch_line =~ m/^diff /) if DEBUG;
4801                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4802                 $patch_number++;
4803                 # print "git diff" header
4804                 print format_git_diff_header_line($patch_line, $diffinfo,
4805                                                   \%from, \%to);
4806
4807                 # print extended diff header
4808                 print "<div class=\"diff extended_header\">\n";
4809         EXTENDED_HEADER:
4810                 while ($patch_line = <$fd>) {
4811                         chomp $patch_line;
4812
4813                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4814
4815                         print format_extended_diff_header_line($patch_line, $diffinfo,
4816                                                                \%from, \%to);
4817                 }
4818                 print "</div>\n"; # class="diff extended_header"
4819
4820                 # from-file/to-file diff header
4821                 if (! $patch_line) {
4822                         print "</div>\n"; # class="patch"
4823                         last PATCH;
4824                 }
4825                 next PATCH if ($patch_line =~ m/^diff /);
4826                 #assert($patch_line =~ m/^---/) if DEBUG;
4827
4828                 my $last_patch_line = $patch_line;
4829                 $patch_line = <$fd>;
4830                 chomp $patch_line;
4831                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4832
4833                 print format_diff_from_to_header($last_patch_line, $patch_line,
4834                                                  $diffinfo, \%from, \%to,
4835                                                  @hash_parents);
4836
4837                 # the patch itself
4838         LINE:
4839                 while ($patch_line = <$fd>) {
4840                         chomp $patch_line;
4841
4842                         next PATCH if ($patch_line =~ m/^diff /);
4843
4844                         print format_diff_line($patch_line, \%from, \%to);
4845                 }
4846
4847         } continue {
4848                 print "</div>\n"; # class="patch"
4849         }
4850
4851         # for compact combined (--cc) format, with chunk and patch simplification
4852         # the patchset might be empty, but there might be unprocessed raw lines
4853         for (++$patch_idx if $patch_number > 0;
4854              $patch_idx < @$difftree;
4855              ++$patch_idx) {
4856                 # read and prepare patch information
4857                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4858
4859                 # generate anchor for "patch" links in difftree / whatchanged part
4860                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4861                       format_diff_cc_simplified($diffinfo, @hash_parents) .
4862                       "</div>\n";  # class="patch"
4863
4864                 $patch_number++;
4865         }
4866
4867         if ($patch_number == 0) {
4868                 if (@hash_parents > 1) {
4869                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4870                 } else {
4871                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
4872                 }
4873         }
4874
4875         print "</div>\n"; # class="patchset"
4876 }
4877
4878 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4879
4880 # fills project list info (age, description, owner, forks) for each
4881 # project in the list, removing invalid projects from returned list
4882 # NOTE: modifies $projlist, but does not remove entries from it
4883 sub fill_project_list_info {
4884         my $projlist = shift;
4885         my @projects;
4886
4887         my $show_ctags = gitweb_check_feature('ctags');
4888  PROJECT:
4889         foreach my $pr (@$projlist) {
4890                 my (@activity) = git_get_last_activity($pr->{'path'});
4891                 unless (@activity) {
4892                         next PROJECT;
4893                 }
4894                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4895                 if (!defined $pr->{'descr'}) {
4896                         my $descr = git_get_project_description($pr->{'path'}) || "";
4897                         $descr = to_utf8($descr);
4898                         $pr->{'descr_long'} = $descr;
4899                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4900                 }
4901                 if (!defined $pr->{'owner'}) {
4902                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4903                 }
4904                 if ($show_ctags) {
4905                         $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4906                 }
4907                 push @projects, $pr;
4908         }
4909
4910         return @projects;
4911 }
4912
4913 sub sort_projects_list {
4914         my ($projlist, $order) = @_;
4915         my @projects;
4916
4917         my %order_info = (
4918                 project => { key => 'path', type => 'str' },
4919                 descr => { key => 'descr_long', type => 'str' },
4920                 owner => { key => 'owner', type => 'str' },
4921                 age => { key => 'age', type => 'num' }
4922         );
4923         my $oi = $order_info{$order};
4924         return @$projlist unless defined $oi;
4925         if ($oi->{'type'} eq 'str') {
4926                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
4927         } else {
4928                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
4929         }
4930
4931         return @projects;
4932 }
4933
4934 # print 'sort by' <th> element, generating 'sort by $name' replay link
4935 # if that order is not selected
4936 sub print_sort_th {
4937         print format_sort_th(@_);
4938 }
4939
4940 sub format_sort_th {
4941         my ($name, $order, $header) = @_;
4942         my $sort_th = "";
4943         $header ||= ucfirst($name);
4944
4945         if ($order eq $name) {
4946                 $sort_th .= "<th>$header</th>\n";
4947         } else {
4948                 $sort_th .= "<th>" .
4949                             $cgi->a({-href => href(-replay=>1, order=>$name),
4950                                      -class => "header"}, $header) .
4951                             "</th>\n";
4952         }
4953
4954         return $sort_th;
4955 }
4956
4957 sub git_project_list_rows {
4958         my ($projlist, $from, $to, $check_forks) = @_;
4959
4960         $from = 0 unless defined $from;
4961         $to = $#$projlist if (!defined $to || $#$projlist < $to);
4962
4963         my $alternate = 1;
4964         for (my $i = $from; $i <= $to; $i++) {
4965                 my $pr = $projlist->[$i];
4966
4967                 if ($alternate) {
4968                         print "<tr class=\"dark\">\n";
4969                 } else {
4970                         print "<tr class=\"light\">\n";
4971                 }
4972                 $alternate ^= 1;
4973
4974                 if ($check_forks) {
4975                         print "<td>";
4976                         if ($pr->{'forks'}) {
4977                                 my $nforks = scalar @{$pr->{'forks'}};
4978                                 if ($nforks > 0) {
4979                                         print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
4980                                                        -title => "$nforks forks"}, "+");
4981                                 } else {
4982                                         print $cgi->span({-title => "$nforks forks"}, "+");
4983                                 }
4984                         }
4985                         print "</td>\n";
4986                 }
4987                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4988                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4989                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4990                                         -class => "list", -title => $pr->{'descr_long'}},
4991                                         esc_html($pr->{'descr'})) . "</td>\n" .
4992                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4993                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4994                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4995                       "<td class=\"link\">" .
4996                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4997                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4998                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4999                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5000                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5001                       "</td>\n" .
5002                       "</tr>\n";
5003         }
5004 }
5005
5006 sub git_project_list_body {
5007         # actually uses global variable $project
5008         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5009         my @projects = @$projlist;
5010
5011         my $check_forks = gitweb_check_feature('forks');
5012         my $show_ctags  = gitweb_check_feature('ctags');
5013         my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
5014         $check_forks = undef
5015                 if ($tagfilter || $searchtext);
5016
5017         # filtering out forks before filling info allows to do less work
5018         @projects = filter_forks_from_projects_list(\@projects)
5019                 if ($check_forks);
5020         @projects = fill_project_list_info(\@projects);
5021         # searching projects require filling to be run before it
5022         @projects = search_projects_list(\@projects,
5023                                          'searchtext' => $searchtext,
5024                                          'tagfilter'  => $tagfilter)
5025                 if ($tagfilter || $searchtext);
5026
5027         $order ||= $default_projects_order;
5028         $from = 0 unless defined $from;
5029         $to = $#projects if (!defined $to || $#projects < $to);
5030
5031         # short circuit
5032         if ($from > $to) {
5033                 print "<center>\n".
5034                       "<b>No such projects found</b><br />\n".
5035                       "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5036                       "</center>\n<br />\n";
5037                 return;
5038         }
5039
5040         @projects = sort_projects_list(\@projects, $order);
5041
5042         if ($show_ctags) {
5043                 my $ctags = git_gather_all_ctags(\@projects);
5044                 my $cloud = git_populate_project_tagcloud($ctags);
5045                 print git_show_project_tagcloud($cloud, 64);
5046         }
5047
5048         print "<table class=\"project_list\">\n";
5049         unless ($no_header) {
5050                 print "<tr>\n";
5051                 if ($check_forks) {
5052                         print "<th></th>\n";
5053                 }
5054                 print_sort_th('project', $order, 'Project');
5055                 print_sort_th('descr', $order, 'Description');
5056                 print_sort_th('owner', $order, 'Owner');
5057                 print_sort_th('age', $order, 'Last Change');
5058                 print "<th></th>\n" . # for links
5059                       "</tr>\n";
5060         }
5061
5062         git_project_list_rows(\@projects, $from, $to, $check_forks);
5063
5064         if (defined $extra) {
5065                 print "<tr>\n";
5066                 if ($check_forks) {
5067                         print "<td></td>\n";
5068                 }
5069                 print "<td colspan=\"5\">$extra</td>\n" .
5070                       "</tr>\n";
5071         }
5072         print "</table>\n";
5073 }
5074
5075 sub git_log_body {
5076         # uses global variable $project
5077         my ($commitlist, $from, $to, $refs, $extra) = @_;
5078
5079         $from = 0 unless defined $from;
5080         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5081
5082         for (my $i = 0; $i <= $to; $i++) {
5083                 my %co = %{$commitlist->[$i]};
5084                 next if !%co;
5085                 my $commit = $co{'id'};
5086                 my $ref = format_ref_marker($refs, $commit);
5087                 git_print_header_div('commit',
5088                                "<span class=\"age\">$co{'age_string'}</span>" .
5089                                esc_html($co{'title'}) . $ref,
5090                                $commit);
5091                 print "<div class=\"title_text\">\n" .
5092                       "<div class=\"log_link\">\n" .
5093                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5094                       " | " .
5095                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5096                       " | " .
5097                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5098                       "<br/>\n" .
5099                       "</div>\n";
5100                       git_print_authorship(\%co, -tag => 'span');
5101                       print "<br/>\n</div>\n";
5102
5103                 print "<div class=\"log_body\">\n";
5104                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5105                 print "</div>\n";
5106         }
5107         if ($extra) {
5108                 print "<div class=\"page_nav\">\n";
5109                 print "$extra\n";
5110                 print "</div>\n";
5111         }
5112 }
5113
5114 sub git_shortlog_body {
5115         # uses global variable $project
5116         my ($commitlist, $from, $to, $refs, $extra) = @_;
5117
5118         $from = 0 unless defined $from;
5119         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5120
5121         print "<table class=\"shortlog\">\n";
5122         my $alternate = 1;
5123         for (my $i = $from; $i <= $to; $i++) {
5124                 my %co = %{$commitlist->[$i]};
5125                 my $commit = $co{'id'};
5126                 my $ref = format_ref_marker($refs, $commit);
5127                 if ($alternate) {
5128                         print "<tr class=\"dark\">\n";
5129                 } else {
5130                         print "<tr class=\"light\">\n";
5131                 }
5132                 $alternate ^= 1;
5133                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5134                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5135                       format_author_html('td', \%co, 10) . "<td>";
5136                 print format_subject_html($co{'title'}, $co{'title_short'},
5137                                           href(action=>"commit", hash=>$commit), $ref);
5138                 print "</td>\n" .
5139                       "<td class=\"link\">" .
5140                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5141                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5142                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5143                 my $snapshot_links = format_snapshot_links($commit);
5144                 if (defined $snapshot_links) {
5145                         print " | " . $snapshot_links;
5146                 }
5147                 print "</td>\n" .
5148                       "</tr>\n";
5149         }
5150         if (defined $extra) {
5151                 print "<tr>\n" .
5152                       "<td colspan=\"4\">$extra</td>\n" .
5153                       "</tr>\n";
5154         }
5155         print "</table>\n";
5156 }
5157
5158 sub git_history_body {
5159         # Warning: assumes constant type (blob or tree) during history
5160         my ($commitlist, $from, $to, $refs, $extra,
5161             $file_name, $file_hash, $ftype) = @_;
5162
5163         $from = 0 unless defined $from;
5164         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5165
5166         print "<table class=\"history\">\n";
5167         my $alternate = 1;
5168         for (my $i = $from; $i <= $to; $i++) {
5169                 my %co = %{$commitlist->[$i]};
5170                 if (!%co) {
5171                         next;
5172                 }
5173                 my $commit = $co{'id'};
5174
5175                 my $ref = format_ref_marker($refs, $commit);
5176
5177                 if ($alternate) {
5178                         print "<tr class=\"dark\">\n";
5179                 } else {
5180                         print "<tr class=\"light\">\n";
5181                 }
5182                 $alternate ^= 1;
5183                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5184         # shortlog:   format_author_html('td', \%co, 10)
5185                       format_author_html('td', \%co, 15, 3) . "<td>";
5186                 # originally git_history used chop_str($co{'title'}, 50)
5187                 print format_subject_html($co{'title'}, $co{'title_short'},
5188                                           href(action=>"commit", hash=>$commit), $ref);
5189                 print "</td>\n" .
5190                       "<td class=\"link\">" .
5191                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5192                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5193
5194                 if ($ftype eq 'blob') {
5195                         my $blob_current = $file_hash;
5196                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
5197                         if (defined $blob_current && defined $blob_parent &&
5198                                         $blob_current ne $blob_parent) {
5199                                 print " | " .
5200                                         $cgi->a({-href => href(action=>"blobdiff",
5201                                                                hash=>$blob_current, hash_parent=>$blob_parent,
5202                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
5203                                                                file_name=>$file_name)},
5204                                                 "diff to current");
5205                         }
5206                 }
5207                 print "</td>\n" .
5208                       "</tr>\n";
5209         }
5210         if (defined $extra) {
5211                 print "<tr>\n" .
5212                       "<td colspan=\"4\">$extra</td>\n" .
5213                       "</tr>\n";
5214         }
5215         print "</table>\n";
5216 }
5217
5218 sub git_tags_body {
5219         # uses global variable $project
5220         my ($taglist, $from, $to, $extra) = @_;
5221         $from = 0 unless defined $from;
5222         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5223
5224         print "<table class=\"tags\">\n";
5225         my $alternate = 1;
5226         for (my $i = $from; $i <= $to; $i++) {
5227                 my $entry = $taglist->[$i];
5228                 my %tag = %$entry;
5229                 my $comment = $tag{'subject'};
5230                 my $comment_short;
5231                 if (defined $comment) {
5232                         $comment_short = chop_str($comment, 30, 5);
5233                 }
5234                 if ($alternate) {
5235                         print "<tr class=\"dark\">\n";
5236                 } else {
5237                         print "<tr class=\"light\">\n";
5238                 }
5239                 $alternate ^= 1;
5240                 if (defined $tag{'age'}) {
5241                         print "<td><i>$tag{'age'}</i></td>\n";
5242                 } else {
5243                         print "<td></td>\n";
5244                 }
5245                 print "<td>" .
5246                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5247                                -class => "list name"}, esc_html($tag{'name'})) .
5248                       "</td>\n" .
5249                       "<td>";
5250                 if (defined $comment) {
5251                         print format_subject_html($comment, $comment_short,
5252                                                   href(action=>"tag", hash=>$tag{'id'}));
5253                 }
5254                 print "</td>\n" .
5255                       "<td class=\"selflink\">";
5256                 if ($tag{'type'} eq "tag") {
5257                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5258                 } else {
5259                         print "&nbsp;";
5260                 }
5261                 print "</td>\n" .
5262                       "<td class=\"link\">" . " | " .
5263                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5264                 if ($tag{'reftype'} eq "commit") {
5265                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5266                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5267                 } elsif ($tag{'reftype'} eq "blob") {
5268                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5269                 }
5270                 print "</td>\n" .
5271                       "</tr>";
5272         }
5273         if (defined $extra) {
5274                 print "<tr>\n" .
5275                       "<td colspan=\"5\">$extra</td>\n" .
5276                       "</tr>\n";
5277         }
5278         print "</table>\n";
5279 }
5280
5281 sub git_heads_body {
5282         # uses global variable $project
5283         my ($headlist, $head, $from, $to, $extra) = @_;
5284         $from = 0 unless defined $from;
5285         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5286
5287         print "<table class=\"heads\">\n";
5288         my $alternate = 1;
5289         for (my $i = $from; $i <= $to; $i++) {
5290                 my $entry = $headlist->[$i];
5291                 my %ref = %$entry;
5292                 my $curr = $ref{'id'} eq $head;
5293                 if ($alternate) {
5294                         print "<tr class=\"dark\">\n";
5295                 } else {
5296                         print "<tr class=\"light\">\n";
5297                 }
5298                 $alternate ^= 1;
5299                 print "<td><i>$ref{'age'}</i></td>\n" .
5300                       ($curr ? "<td class=\"current_head\">" : "<td>") .
5301                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5302                                -class => "list name"},esc_html($ref{'name'})) .
5303                       "</td>\n" .
5304                       "<td class=\"link\">" .
5305                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5306                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5307                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5308                       "</td>\n" .
5309                       "</tr>";
5310         }
5311         if (defined $extra) {
5312                 print "<tr>\n" .
5313                       "<td colspan=\"3\">$extra</td>\n" .
5314                       "</tr>\n";
5315         }
5316         print "</table>\n";
5317 }
5318
5319 # Display a single remote block
5320 sub git_remote_block {
5321         my ($remote, $rdata, $limit, $head) = @_;
5322
5323         my $heads = $rdata->{'heads'};
5324         my $fetch = $rdata->{'fetch'};
5325         my $push = $rdata->{'push'};
5326
5327         my $urls_table = "<table class=\"projects_list\">\n" ;
5328
5329         if (defined $fetch) {
5330                 if ($fetch eq $push) {
5331                         $urls_table .= format_repo_url("URL", $fetch);
5332                 } else {
5333                         $urls_table .= format_repo_url("Fetch URL", $fetch);
5334                         $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5335                 }
5336         } elsif (defined $push) {
5337                 $urls_table .= format_repo_url("Push URL", $push);
5338         } else {
5339                 $urls_table .= format_repo_url("", "No remote URL");
5340         }
5341
5342         $urls_table .= "</table>\n";
5343
5344         my $dots;
5345         if (defined $limit && $limit < @$heads) {
5346                 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5347         }
5348
5349         print $urls_table;
5350         git_heads_body($heads, $head, 0, $limit, $dots);
5351 }
5352
5353 # Display a list of remote names with the respective fetch and push URLs
5354 sub git_remotes_list {
5355         my ($remotedata, $limit) = @_;
5356         print "<table class=\"heads\">\n";
5357         my $alternate = 1;
5358         my @remotes = sort keys %$remotedata;
5359
5360         my $limited = $limit && $limit < @remotes;
5361
5362         $#remotes = $limit - 1 if $limited;
5363
5364         while (my $remote = shift @remotes) {
5365                 my $rdata = $remotedata->{$remote};
5366                 my $fetch = $rdata->{'fetch'};
5367                 my $push = $rdata->{'push'};
5368                 if ($alternate) {
5369                         print "<tr class=\"dark\">\n";
5370                 } else {
5371                         print "<tr class=\"light\">\n";
5372                 }
5373                 $alternate ^= 1;
5374                 print "<td>" .
5375                       $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5376                                -class=> "list name"},esc_html($remote)) .
5377                       "</td>";
5378                 print "<td class=\"link\">" .
5379                       (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5380                       " | " .
5381                       (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5382                       "</td>";
5383
5384                 print "</tr>\n";
5385         }
5386
5387         if ($limited) {
5388                 print "<tr>\n" .
5389                       "<td colspan=\"3\">" .
5390                       $cgi->a({-href => href(action=>"remotes")}, "...") .
5391                       "</td>\n" . "</tr>\n";
5392         }
5393
5394         print "</table>";
5395 }
5396
5397 # Display remote heads grouped by remote, unless there are too many
5398 # remotes, in which case we only display the remote names
5399 sub git_remotes_body {
5400         my ($remotedata, $limit, $head) = @_;
5401         if ($limit and $limit < keys %$remotedata) {
5402                 git_remotes_list($remotedata, $limit);
5403         } else {
5404                 fill_remote_heads($remotedata);
5405                 while (my ($remote, $rdata) = each %$remotedata) {
5406                         git_print_section({-class=>"remote", -id=>$remote},
5407                                 ["remotes", $remote, $remote], sub {
5408                                         git_remote_block($remote, $rdata, $limit, $head);
5409                                 });
5410                 }
5411         }
5412 }
5413
5414 sub git_search_grep_body {
5415         my ($commitlist, $from, $to, $extra) = @_;
5416         $from = 0 unless defined $from;
5417         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5418
5419         print "<table class=\"commit_search\">\n";
5420         my $alternate = 1;
5421         for (my $i = $from; $i <= $to; $i++) {
5422                 my %co = %{$commitlist->[$i]};
5423                 if (!%co) {
5424                         next;
5425                 }
5426                 my $commit = $co{'id'};
5427                 if ($alternate) {
5428                         print "<tr class=\"dark\">\n";
5429                 } else {
5430                         print "<tr class=\"light\">\n";
5431                 }
5432                 $alternate ^= 1;
5433                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5434                       format_author_html('td', \%co, 15, 5) .
5435                       "<td>" .
5436                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5437                                -class => "list subject"},
5438                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5439                 my $comment = $co{'comment'};
5440                 foreach my $line (@$comment) {
5441                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
5442                                 my ($lead, $match, $trail) = ($1, $2, $3);
5443                                 $match = chop_str($match, 70, 5, 'center');
5444                                 my $contextlen = int((80 - length($match))/2);
5445                                 $contextlen = 30 if ($contextlen > 30);
5446                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
5447                                 $trail = chop_str($trail, $contextlen, 10, 'right');
5448
5449                                 $lead  = esc_html($lead);
5450                                 $match = esc_html($match);
5451                                 $trail = esc_html($trail);
5452
5453                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
5454                         }
5455                 }
5456                 print "</td>\n" .
5457                       "<td class=\"link\">" .
5458                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5459                       " | " .
5460                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
5461                       " | " .
5462                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5463                 print "</td>\n" .
5464                       "</tr>\n";
5465         }
5466         if (defined $extra) {
5467                 print "<tr>\n" .
5468                       "<td colspan=\"3\">$extra</td>\n" .
5469                       "</tr>\n";
5470         }
5471         print "</table>\n";
5472 }
5473
5474 ## ======================================================================
5475 ## ======================================================================
5476 ## actions
5477
5478 sub git_project_list {
5479         my $order = $input_params{'order'};
5480         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5481                 die_error(400, "Unknown order parameter");
5482         }
5483
5484         my @list = git_get_projects_list();
5485         if (!@list) {
5486                 die_error(404, "No projects found");
5487         }
5488
5489         git_header_html();
5490         if (defined $home_text && -f $home_text) {
5491                 print "<div class=\"index_include\">\n";
5492                 insert_file($home_text);
5493                 print "</div>\n";
5494         }
5495         print $cgi->startform(-method => "get") .
5496               "<p class=\"projsearch\">Search:\n" .
5497               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5498               "</p>" .
5499               $cgi->end_form() . "\n";
5500         git_project_list_body(\@list, $order);
5501         git_footer_html();
5502 }
5503
5504 sub git_forks {
5505         my $order = $input_params{'order'};
5506         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5507                 die_error(400, "Unknown order parameter");
5508         }
5509
5510         my @list = git_get_projects_list($project);
5511         if (!@list) {
5512                 die_error(404, "No forks found");
5513         }
5514
5515         git_header_html();
5516         git_print_page_nav('','');
5517         git_print_header_div('summary', "$project forks");
5518         git_project_list_body(\@list, $order);
5519         git_footer_html();
5520 }
5521
5522 sub git_project_index {
5523         my @projects = git_get_projects_list();
5524         if (!@projects) {
5525                 die_error(404, "No projects found");
5526         }
5527
5528         print $cgi->header(
5529                 -type => 'text/plain',
5530                 -charset => 'utf-8',
5531                 -content_disposition => 'inline; filename="index.aux"');
5532
5533         foreach my $pr (@projects) {
5534                 if (!exists $pr->{'owner'}) {
5535                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5536                 }
5537
5538                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5539                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5540                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5541                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5542                 $path  =~ s/ /\+/g;
5543                 $owner =~ s/ /\+/g;
5544
5545                 print "$path $owner\n";
5546         }
5547 }
5548
5549 sub git_summary {
5550         my $descr = git_get_project_description($project) || "none";
5551         my %co = parse_commit("HEAD");
5552         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5553         my $head = $co{'id'};
5554         my $remote_heads = gitweb_check_feature('remote_heads');
5555
5556         my $owner = git_get_project_owner($project);
5557
5558         my $refs = git_get_references();
5559         # These get_*_list functions return one more to allow us to see if
5560         # there are more ...
5561         my @taglist  = git_get_tags_list(16);
5562         my @headlist = git_get_heads_list(16);
5563         my %remotedata = $remote_heads ? git_get_remotes_list() : ();
5564         my @forklist;
5565         my $check_forks = gitweb_check_feature('forks');
5566
5567         if ($check_forks) {
5568                 # find forks of a project
5569                 @forklist = git_get_projects_list($project);
5570                 # filter out forks of forks
5571                 @forklist = filter_forks_from_projects_list(\@forklist)
5572                         if (@forklist);
5573         }
5574
5575         git_header_html();
5576         git_print_page_nav('summary','', $head);
5577
5578         print "<div class=\"title\">&nbsp;</div>\n";
5579         print "<table class=\"projects_list\">\n" .
5580               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5581               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5582         if (defined $cd{'rfc2822'}) {
5583                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5584         }
5585
5586         # use per project git URL list in $projectroot/$project/cloneurl
5587         # or make project git URL from git base URL and project name
5588         my $url_tag = "URL";
5589         my @url_list = git_get_project_url_list($project);
5590         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5591         foreach my $git_url (@url_list) {
5592                 next unless $git_url;
5593                 print format_repo_url($url_tag, $git_url);
5594                 $url_tag = "";
5595         }
5596
5597         # Tag cloud
5598         my $show_ctags = gitweb_check_feature('ctags');
5599         if ($show_ctags) {
5600                 my $ctags = git_get_project_ctags($project);
5601                 if (%$ctags) {
5602                         # without ability to add tags, don't show if there are none
5603                         my $cloud = git_populate_project_tagcloud($ctags);
5604                         print "<tr id=\"metadata_ctags\">" .
5605                               "<td>content tags</td>" .
5606                               "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
5607                               "</tr>\n";
5608                 }
5609         }
5610
5611         print "</table>\n";
5612
5613         # If XSS prevention is on, we don't include README.html.
5614         # TODO: Allow a readme in some safe format.
5615         if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5616                 print "<div class=\"title\">readme</div>\n" .
5617                       "<div class=\"readme\">\n";
5618                 insert_file("$projectroot/$project/README.html");
5619                 print "\n</div>\n"; # class="readme"
5620         }
5621
5622         # we need to request one more than 16 (0..15) to check if
5623         # those 16 are all
5624         my @commitlist = $head ? parse_commits($head, 17) : ();
5625         if (@commitlist) {
5626                 git_print_header_div('shortlog');
5627                 git_shortlog_body(\@commitlist, 0, 15, $refs,
5628                                   $#commitlist <=  15 ? undef :
5629                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
5630         }
5631
5632         if (@taglist) {
5633                 git_print_header_div('tags');
5634                 git_tags_body(\@taglist, 0, 15,
5635                               $#taglist <=  15 ? undef :
5636                               $cgi->a({-href => href(action=>"tags")}, "..."));
5637         }
5638
5639         if (@headlist) {
5640                 git_print_header_div('heads');
5641                 git_heads_body(\@headlist, $head, 0, 15,
5642                                $#headlist <= 15 ? undef :
5643                                $cgi->a({-href => href(action=>"heads")}, "..."));
5644         }
5645
5646         if (%remotedata) {
5647                 git_print_header_div('remotes');
5648                 git_remotes_body(\%remotedata, 15, $head);
5649         }
5650
5651         if (@forklist) {
5652                 git_print_header_div('forks');
5653                 git_project_list_body(\@forklist, 'age', 0, 15,
5654                                       $#forklist <= 15 ? undef :
5655                                       $cgi->a({-href => href(action=>"forks")}, "..."),
5656                                       'no_header');
5657         }
5658
5659         git_footer_html();
5660 }
5661
5662 sub git_tag {
5663         my %tag = parse_tag($hash);
5664
5665         if (! %tag) {
5666                 die_error(404, "Unknown tag object");
5667         }
5668
5669         my $head = git_get_head_hash($project);
5670         git_header_html();
5671         git_print_page_nav('','', $head,undef,$head);
5672         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5673         print "<div class=\"title_text\">\n" .
5674               "<table class=\"object_header\">\n" .
5675               "<tr>\n" .
5676               "<td>object</td>\n" .
5677               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5678                                $tag{'object'}) . "</td>\n" .
5679               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5680                                               $tag{'type'}) . "</td>\n" .
5681               "</tr>\n";
5682         if (defined($tag{'author'})) {
5683                 git_print_authorship_rows(\%tag, 'author');
5684         }
5685         print "</table>\n\n" .
5686               "</div>\n";
5687         print "<div class=\"page_body\">";
5688         my $comment = $tag{'comment'};
5689         foreach my $line (@$comment) {
5690                 chomp $line;
5691                 print esc_html($line, -nbsp=>1) . "<br/>\n";
5692         }
5693         print "</div>\n";
5694         git_footer_html();
5695 }
5696
5697 sub git_blame_common {
5698         my $format = shift || 'porcelain';
5699         if ($format eq 'porcelain' && $cgi->param('js')) {
5700                 $format = 'incremental';
5701                 $action = 'blame_incremental'; # for page title etc
5702         }
5703
5704         # permissions
5705         gitweb_check_feature('blame')
5706                 or die_error(403, "Blame view not allowed");
5707
5708         # error checking
5709         die_error(400, "No file name given") unless $file_name;
5710         $hash_base ||= git_get_head_hash($project);
5711         die_error(404, "Couldn't find base commit") unless $hash_base;
5712         my %co = parse_commit($hash_base)
5713                 or die_error(404, "Commit not found");
5714         my $ftype = "blob";
5715         if (!defined $hash) {
5716                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5717                         or die_error(404, "Error looking up file");
5718         } else {
5719                 $ftype = git_get_type($hash);
5720                 if ($ftype !~ "blob") {
5721                         die_error(400, "Object is not a blob");
5722                 }
5723         }
5724
5725         my $fd;
5726         if ($format eq 'incremental') {
5727                 # get file contents (as base)
5728                 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5729                         or die_error(500, "Open git-cat-file failed");
5730         } elsif ($format eq 'data') {
5731                 # run git-blame --incremental
5732                 open $fd, "-|", git_cmd(), "blame", "--incremental",
5733                         $hash_base, "--", $file_name
5734                         or die_error(500, "Open git-blame --incremental failed");
5735         } else {
5736                 # run git-blame --porcelain
5737                 open $fd, "-|", git_cmd(), "blame", '-p',
5738                         $hash_base, '--', $file_name
5739                         or die_error(500, "Open git-blame --porcelain failed");
5740         }
5741
5742         # incremental blame data returns early
5743         if ($format eq 'data') {
5744                 print $cgi->header(
5745                         -type=>"text/plain", -charset => "utf-8",
5746                         -status=> "200 OK");
5747                 local $| = 1; # output autoflush
5748                 print while <$fd>;
5749                 close $fd
5750                         or print "ERROR $!\n";
5751
5752                 print 'END';
5753                 if (defined $t0 && gitweb_check_feature('timed')) {
5754                         print ' '.
5755                               tv_interval($t0, [ gettimeofday() ]).
5756                               ' '.$number_of_git_cmds;
5757                 }
5758                 print "\n";
5759
5760                 return;
5761         }
5762
5763         # page header
5764         git_header_html();
5765         my $formats_nav =
5766                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5767                         "blob") .
5768                 " | ";
5769         if ($format eq 'incremental') {
5770                 $formats_nav .=
5771                         $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5772                                 "blame") . " (non-incremental)";
5773         } else {
5774                 $formats_nav .=
5775                         $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5776                                 "blame") . " (incremental)";
5777         }
5778         $formats_nav .=
5779                 " | " .
5780                 $cgi->a({-href => href(action=>"history", -replay=>1)},
5781                         "history") .
5782                 " | " .
5783                 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5784                         "HEAD");
5785         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5786         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5787         git_print_page_path($file_name, $ftype, $hash_base);
5788
5789         # page body
5790         if ($format eq 'incremental') {
5791                 print "<noscript>\n<div class=\"error\"><center><b>\n".
5792                       "This page requires JavaScript to run.\n Use ".
5793                       $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5794                               'this page').
5795                       " instead.\n".
5796                       "</b></center></div>\n</noscript>\n";
5797
5798                 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5799         }
5800
5801         print qq!<div class="page_body">\n!;
5802         print qq!<div id="progress_info">... / ...</div>\n!
5803                 if ($format eq 'incremental');
5804         print qq!<table id="blame_table" class="blame" width="100%">\n!.
5805               #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5806               qq!<thead>\n!.
5807               qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5808               qq!</thead>\n!.
5809               qq!<tbody>\n!;
5810
5811         my @rev_color = qw(light dark);
5812         my $num_colors = scalar(@rev_color);
5813         my $current_color = 0;
5814
5815         if ($format eq 'incremental') {
5816                 my $color_class = $rev_color[$current_color];
5817
5818                 #contents of a file
5819                 my $linenr = 0;
5820         LINE:
5821                 while (my $line = <$fd>) {
5822                         chomp $line;
5823                         $linenr++;
5824
5825                         print qq!<tr id="l$linenr" class="$color_class">!.
5826                               qq!<td class="sha1"><a href=""> </a></td>!.
5827                               qq!<td class="linenr">!.
5828                               qq!<a class="linenr" href="">$linenr</a></td>!;
5829                         print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5830                         print qq!</tr>\n!;
5831                 }
5832
5833         } else { # porcelain, i.e. ordinary blame
5834                 my %metainfo = (); # saves information about commits
5835
5836                 # blame data
5837         LINE:
5838                 while (my $line = <$fd>) {
5839                         chomp $line;
5840                         # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5841                         # no <lines in group> for subsequent lines in group of lines
5842                         my ($full_rev, $orig_lineno, $lineno, $group_size) =
5843                            ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5844                         if (!exists $metainfo{$full_rev}) {
5845                                 $metainfo{$full_rev} = { 'nprevious' => 0 };
5846                         }
5847                         my $meta = $metainfo{$full_rev};
5848                         my $data;
5849                         while ($data = <$fd>) {
5850                                 chomp $data;
5851                                 last if ($data =~ s/^\t//); # contents of line
5852                                 if ($data =~ /^(\S+)(?: (.*))?$/) {
5853                                         $meta->{$1} = $2 unless exists $meta->{$1};
5854                                 }
5855                                 if ($data =~ /^previous /) {
5856                                         $meta->{'nprevious'}++;
5857                                 }
5858                         }
5859                         my $short_rev = substr($full_rev, 0, 8);
5860                         my $author = $meta->{'author'};
5861                         my %date =
5862                                 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5863                         my $date = $date{'iso-tz'};
5864                         if ($group_size) {
5865                                 $current_color = ($current_color + 1) % $num_colors;
5866                         }
5867                         my $tr_class = $rev_color[$current_color];
5868                         $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5869                         $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5870                         $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5871                         print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5872                         if ($group_size) {
5873                                 print "<td class=\"sha1\"";
5874                                 print " title=\"". esc_html($author) . ", $date\"";
5875                                 print " rowspan=\"$group_size\"" if ($group_size > 1);
5876                                 print ">";
5877                                 print $cgi->a({-href => href(action=>"commit",
5878                                                              hash=>$full_rev,
5879                                                              file_name=>$file_name)},
5880                                               esc_html($short_rev));
5881                                 if ($group_size >= 2) {
5882                                         my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5883                                         if (@author_initials) {
5884                                                 print "<br />" .
5885                                                       esc_html(join('', @author_initials));
5886                                                 #           or join('.', ...)
5887                                         }
5888                                 }
5889                                 print "</td>\n";
5890                         }
5891                         # 'previous' <sha1 of parent commit> <filename at commit>
5892                         if (exists $meta->{'previous'} &&
5893                             $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5894                                 $meta->{'parent'} = $1;
5895                                 $meta->{'file_parent'} = unquote($2);
5896                         }
5897                         my $linenr_commit =
5898                                 exists($meta->{'parent'}) ?
5899                                 $meta->{'parent'} : $full_rev;
5900                         my $linenr_filename =
5901                                 exists($meta->{'file_parent'}) ?
5902                                 $meta->{'file_parent'} : unquote($meta->{'filename'});
5903                         my $blamed = href(action => 'blame',
5904                                           file_name => $linenr_filename,
5905                                           hash_base => $linenr_commit);
5906                         print "<td class=\"linenr\">";
5907                         print $cgi->a({ -href => "$blamed#l$orig_lineno",
5908                                         -class => "linenr" },
5909                                       esc_html($lineno));
5910                         print "</td>";
5911                         print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5912                         print "</tr>\n";
5913                 } # end while
5914
5915         }
5916
5917         # footer
5918         print "</tbody>\n".
5919               "</table>\n"; # class="blame"
5920         print "</div>\n";   # class="blame_body"
5921         close $fd
5922                 or print "Reading blob failed\n";
5923
5924         git_footer_html();
5925 }
5926
5927 sub git_blame {
5928         git_blame_common();
5929 }
5930
5931 sub git_blame_incremental {
5932         git_blame_common('incremental');
5933 }
5934
5935 sub git_blame_data {
5936         git_blame_common('data');
5937 }
5938
5939 sub git_tags {
5940         my $head = git_get_head_hash($project);
5941         git_header_html();
5942         git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
5943         git_print_header_div('summary', $project);
5944
5945         my @tagslist = git_get_tags_list();
5946         if (@tagslist) {
5947                 git_tags_body(\@tagslist);
5948         }
5949         git_footer_html();
5950 }
5951
5952 sub git_heads {
5953         my $head = git_get_head_hash($project);
5954         git_header_html();
5955         git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
5956         git_print_header_div('summary', $project);
5957
5958         my @headslist = git_get_heads_list();
5959         if (@headslist) {
5960                 git_heads_body(\@headslist, $head);
5961         }
5962         git_footer_html();
5963 }
5964
5965 # used both for single remote view and for list of all the remotes
5966 sub git_remotes {
5967         gitweb_check_feature('remote_heads')
5968                 or die_error(403, "Remote heads view is disabled");
5969
5970         my $head = git_get_head_hash($project);
5971         my $remote = $input_params{'hash'};
5972
5973         my $remotedata = git_get_remotes_list($remote);
5974         die_error(500, "Unable to get remote information") unless defined $remotedata;
5975
5976         unless (%$remotedata) {
5977                 die_error(404, defined $remote ?
5978                         "Remote $remote not found" :
5979                         "No remotes found");
5980         }
5981
5982         git_header_html(undef, undef, -action_extra => $remote);
5983         git_print_page_nav('', '',  $head, undef, $head,
5984                 format_ref_views($remote ? '' : 'remotes'));
5985
5986         fill_remote_heads($remotedata);
5987         if (defined $remote) {
5988                 git_print_header_div('remotes', "$remote remote for $project");
5989                 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
5990         } else {
5991                 git_print_header_div('summary', "$project remotes");
5992                 git_remotes_body($remotedata, undef, $head);
5993         }
5994
5995         git_footer_html();
5996 }
5997
5998 sub git_blob_plain {
5999         my $type = shift;
6000         my $expires;
6001
6002         if (!defined $hash) {
6003                 if (defined $file_name) {
6004                         my $base = $hash_base || git_get_head_hash($project);
6005                         $hash = git_get_hash_by_path($base, $file_name, "blob")
6006                                 or die_error(404, "Cannot find file");
6007                 } else {
6008                         die_error(400, "No file name defined");
6009                 }
6010         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6011                 # blobs defined by non-textual hash id's can be cached
6012                 $expires = "+1d";
6013         }
6014
6015         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6016                 or die_error(500, "Open git-cat-file blob '$hash' failed");
6017
6018         # content-type (can include charset)
6019         $type = blob_contenttype($fd, $file_name, $type);
6020
6021         # "save as" filename, even when no $file_name is given
6022         my $save_as = "$hash";
6023         if (defined $file_name) {
6024                 $save_as = $file_name;
6025         } elsif ($type =~ m/^text\//) {
6026                 $save_as .= '.txt';
6027         }
6028
6029         # With XSS prevention on, blobs of all types except a few known safe
6030         # ones are served with "Content-Disposition: attachment" to make sure
6031         # they don't run in our security domain.  For certain image types,
6032         # blob view writes an <img> tag referring to blob_plain view, and we
6033         # want to be sure not to break that by serving the image as an
6034         # attachment (though Firefox 3 doesn't seem to care).
6035         my $sandbox = $prevent_xss &&
6036                 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
6037
6038         print $cgi->header(
6039                 -type => $type,
6040                 -expires => $expires,
6041                 -content_disposition =>
6042                         ($sandbox ? 'attachment' : 'inline')
6043                         . '; filename="' . $save_as . '"');
6044         local $/ = undef;
6045         binmode STDOUT, ':raw';
6046         print <$fd>;
6047         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6048         close $fd;
6049 }
6050
6051 sub git_blob {
6052         my $expires;
6053
6054         if (!defined $hash) {
6055                 if (defined $file_name) {
6056                         my $base = $hash_base || git_get_head_hash($project);
6057                         $hash = git_get_hash_by_path($base, $file_name, "blob")
6058                                 or die_error(404, "Cannot find file");
6059                 } else {
6060                         die_error(400, "No file name defined");
6061                 }
6062         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6063                 # blobs defined by non-textual hash id's can be cached
6064                 $expires = "+1d";
6065         }
6066
6067         my $have_blame = gitweb_check_feature('blame');
6068         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6069                 or die_error(500, "Couldn't cat $file_name, $hash");
6070         my $mimetype = blob_mimetype($fd, $file_name);
6071         # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6072         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6073                 close $fd;
6074                 return git_blob_plain($mimetype);
6075         }
6076         # we can have blame only for text/* mimetype
6077         $have_blame &&= ($mimetype =~ m!^text/!);
6078
6079         my $highlight = gitweb_check_feature('highlight');
6080         my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6081         $fd = run_highlighter($fd, $highlight, $syntax)
6082                 if $syntax;
6083
6084         git_header_html(undef, $expires);
6085         my $formats_nav = '';
6086         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6087                 if (defined $file_name) {
6088                         if ($have_blame) {
6089                                 $formats_nav .=
6090                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
6091                                                 "blame") .
6092                                         " | ";
6093                         }
6094                         $formats_nav .=
6095                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6096                                         "history") .
6097                                 " | " .
6098                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6099                                         "raw") .
6100                                 " | " .
6101                                 $cgi->a({-href => href(action=>"blob",
6102                                                        hash_base=>"HEAD", file_name=>$file_name)},
6103                                         "HEAD");
6104                 } else {
6105                         $formats_nav .=
6106                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6107                                         "raw");
6108                 }
6109                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6110                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6111         } else {
6112                 print "<div class=\"page_nav\">\n" .
6113                       "<br/><br/></div>\n" .
6114                       "<div class=\"title\">".esc_html($hash)."</div>\n";
6115         }
6116         git_print_page_path($file_name, "blob", $hash_base);
6117         print "<div class=\"page_body\">\n";
6118         if ($mimetype =~ m!^image/!) {
6119                 print qq!<img type="!.esc_attr($mimetype).qq!"!;
6120                 if ($file_name) {
6121                         print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6122                 }
6123                 print qq! src="! .
6124                       href(action=>"blob_plain", hash=>$hash,
6125                            hash_base=>$hash_base, file_name=>$file_name) .
6126                       qq!" />\n!;
6127         } else {
6128                 my $nr;
6129                 while (my $line = <$fd>) {
6130                         chomp $line;
6131                         $nr++;
6132                         $line = untabify($line);
6133                         printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6134                                $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
6135                 }
6136         }
6137         close $fd
6138                 or print "Reading blob failed.\n";
6139         print "</div>";
6140         git_footer_html();
6141 }
6142
6143 sub git_tree {
6144         if (!defined $hash_base) {
6145                 $hash_base = "HEAD";
6146         }
6147         if (!defined $hash) {
6148                 if (defined $file_name) {
6149                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6150                 } else {
6151                         $hash = $hash_base;
6152                 }
6153         }
6154         die_error(404, "No such tree") unless defined($hash);
6155
6156         my $show_sizes = gitweb_check_feature('show-sizes');
6157         my $have_blame = gitweb_check_feature('blame');
6158
6159         my @entries = ();
6160         {
6161                 local $/ = "\0";
6162                 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6163                         ($show_sizes ? '-l' : ()), @extra_options, $hash
6164                         or die_error(500, "Open git-ls-tree failed");
6165                 @entries = map { chomp; $_ } <$fd>;
6166                 close $fd
6167                         or die_error(404, "Reading tree failed");
6168         }
6169
6170         my $refs = git_get_references();
6171         my $ref = format_ref_marker($refs, $hash_base);
6172         git_header_html();
6173         my $basedir = '';
6174         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6175                 my @views_nav = ();
6176                 if (defined $file_name) {
6177                         push @views_nav,
6178                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6179                                         "history"),
6180                                 $cgi->a({-href => href(action=>"tree",
6181                                                        hash_base=>"HEAD", file_name=>$file_name)},
6182                                         "HEAD"),
6183                 }
6184                 my $snapshot_links = format_snapshot_links($hash);
6185                 if (defined $snapshot_links) {
6186                         # FIXME: Should be available when we have no hash base as well.
6187                         push @views_nav, $snapshot_links;
6188                 }
6189                 git_print_page_nav('tree','', $hash_base, undef, undef,
6190                                    join(' | ', @views_nav));
6191                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
6192         } else {
6193                 undef $hash_base;
6194                 print "<div class=\"page_nav\">\n";
6195                 print "<br/><br/></div>\n";
6196                 print "<div class=\"title\">".esc_html($hash)."</div>\n";
6197         }
6198         if (defined $file_name) {
6199                 $basedir = $file_name;
6200                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
6201                         $basedir .= '/';
6202                 }
6203                 git_print_page_path($file_name, 'tree', $hash_base);
6204         }
6205         print "<div class=\"page_body\">\n";
6206         print "<table class=\"tree\">\n";
6207         my $alternate = 1;
6208         # '..' (top directory) link if possible
6209         if (defined $hash_base &&
6210             defined $file_name && $file_name =~ m![^/]+$!) {
6211                 if ($alternate) {
6212                         print "<tr class=\"dark\">\n";
6213                 } else {
6214                         print "<tr class=\"light\">\n";
6215                 }
6216                 $alternate ^= 1;
6217
6218                 my $up = $file_name;
6219                 $up =~ s!/?[^/]+$!!;
6220                 undef $up unless $up;
6221                 # based on git_print_tree_entry
6222                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
6223                 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
6224                 print '<td class="list">';
6225                 print $cgi->a({-href => href(action=>"tree",
6226                                              hash_base=>$hash_base,
6227                                              file_name=>$up)},
6228                               "..");
6229                 print "</td>\n";
6230                 print "<td class=\"link\"></td>\n";
6231
6232                 print "</tr>\n";
6233         }
6234         foreach my $line (@entries) {
6235                 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
6236
6237                 if ($alternate) {
6238                         print "<tr class=\"dark\">\n";
6239                 } else {
6240                         print "<tr class=\"light\">\n";
6241                 }
6242                 $alternate ^= 1;
6243
6244                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
6245
6246                 print "</tr>\n";
6247         }
6248         print "</table>\n" .
6249               "</div>";
6250         git_footer_html();
6251 }
6252
6253 sub snapshot_name {
6254         my ($project, $hash) = @_;
6255
6256         # path/to/project.git  -> project
6257         # path/to/project/.git -> project
6258         my $name = to_utf8($project);
6259         $name =~ s,([^/])/*\.git$,$1,;
6260         $name = basename($name);
6261         # sanitize name
6262         $name =~ s/[[:cntrl:]]/?/g;
6263
6264         my $ver = $hash;
6265         if ($hash =~ /^[0-9a-fA-F]+$/) {
6266                 # shorten SHA-1 hash
6267                 my $full_hash = git_get_full_hash($project, $hash);
6268                 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6269                         $ver = git_get_short_hash($project, $hash);
6270                 }
6271         } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6272                 # tags don't need shortened SHA-1 hash
6273                 $ver = $1;
6274         } else {
6275                 # branches and other need shortened SHA-1 hash
6276                 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
6277                         $ver = $1;
6278                 }
6279                 $ver .= '-' . git_get_short_hash($project, $hash);
6280         }
6281         # in case of hierarchical branch names
6282         $ver =~ s!/!.!g;
6283
6284         # name = project-version_string
6285         $name = "$name-$ver";
6286
6287         return wantarray ? ($name, $name) : $name;
6288 }
6289
6290 sub git_snapshot {
6291         my $format = $input_params{'snapshot_format'};
6292         if (!@snapshot_fmts) {
6293                 die_error(403, "Snapshots not allowed");
6294         }
6295         # default to first supported snapshot format
6296         $format ||= $snapshot_fmts[0];
6297         if ($format !~ m/^[a-z0-9]+$/) {
6298                 die_error(400, "Invalid snapshot format parameter");
6299         } elsif (!exists($known_snapshot_formats{$format})) {
6300                 die_error(400, "Unknown snapshot format");
6301         } elsif ($known_snapshot_formats{$format}{'disabled'}) {
6302                 die_error(403, "Snapshot format not allowed");
6303         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
6304                 die_error(403, "Unsupported snapshot format");
6305         }
6306
6307         my $type = git_get_type("$hash^{}");
6308         if (!$type) {
6309                 die_error(404, 'Object does not exist');
6310         }  elsif ($type eq 'blob') {
6311                 die_error(400, 'Object is not a tree-ish');
6312         }
6313
6314         my ($name, $prefix) = snapshot_name($project, $hash);
6315         my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
6316         my $cmd = quote_command(
6317                 git_cmd(), 'archive',
6318                 "--format=$known_snapshot_formats{$format}{'format'}",
6319                 "--prefix=$prefix/", $hash);
6320         if (exists $known_snapshot_formats{$format}{'compressor'}) {
6321                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
6322         }
6323
6324         $filename =~ s/(["\\])/\\$1/g;
6325         print $cgi->header(
6326                 -type => $known_snapshot_formats{$format}{'type'},
6327                 -content_disposition => 'inline; filename="' . $filename . '"',
6328                 -status => '200 OK');
6329
6330         open my $fd, "-|", $cmd
6331                 or die_error(500, "Execute git-archive failed");
6332         binmode STDOUT, ':raw';
6333         print <$fd>;
6334         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6335         close $fd;
6336 }
6337
6338 sub git_log_generic {
6339         my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
6340
6341         my $head = git_get_head_hash($project);
6342         if (!defined $base) {
6343                 $base = $head;
6344         }
6345         if (!defined $page) {
6346                 $page = 0;
6347         }
6348         my $refs = git_get_references();
6349
6350         my $commit_hash = $base;
6351         if (defined $parent) {
6352                 $commit_hash = "$parent..$base";
6353         }
6354         my @commitlist =
6355                 parse_commits($commit_hash, 101, (100 * $page),
6356                               defined $file_name ? ($file_name, "--full-history") : ());
6357
6358         my $ftype;
6359         if (!defined $file_hash && defined $file_name) {
6360                 # some commits could have deleted file in question,
6361                 # and not have it in tree, but one of them has to have it
6362                 for (my $i = 0; $i < @commitlist; $i++) {
6363                         $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
6364                         last if defined $file_hash;
6365                 }
6366         }
6367         if (defined $file_hash) {
6368                 $ftype = git_get_type($file_hash);
6369         }
6370         if (defined $file_name && !defined $ftype) {
6371                 die_error(500, "Unknown type of object");
6372         }
6373         my %co;
6374         if (defined $file_name) {
6375                 %co = parse_commit($base)
6376                         or die_error(404, "Unknown commit object");
6377         }
6378
6379
6380         my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
6381         my $next_link = '';
6382         if ($#commitlist >= 100) {
6383                 $next_link =
6384                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
6385                                  -accesskey => "n", -title => "Alt-n"}, "next");
6386         }
6387         my $patch_max = gitweb_get_feature('patches');
6388         if ($patch_max && !defined $file_name) {
6389                 if ($patch_max < 0 || @commitlist <= $patch_max) {
6390                         $paging_nav .= " &sdot; " .
6391                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6392                                         "patches");
6393                 }
6394         }
6395
6396         git_header_html();
6397         git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
6398         if (defined $file_name) {
6399                 git_print_header_div('commit', esc_html($co{'title'}), $base);
6400         } else {
6401                 git_print_header_div('summary', $project)
6402         }
6403         git_print_page_path($file_name, $ftype, $hash_base)
6404                 if (defined $file_name);
6405
6406         $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
6407                      $file_name, $file_hash, $ftype);
6408
6409         git_footer_html();
6410 }
6411
6412 sub git_log {
6413         git_log_generic('log', \&git_log_body,
6414                         $hash, $hash_parent);
6415 }
6416
6417 sub git_commit {
6418         $hash ||= $hash_base || "HEAD";
6419         my %co = parse_commit($hash)
6420             or die_error(404, "Unknown commit object");
6421
6422         my $parent  = $co{'parent'};
6423         my $parents = $co{'parents'}; # listref
6424
6425         # we need to prepare $formats_nav before any parameter munging
6426         my $formats_nav;
6427         if (!defined $parent) {
6428                 # --root commitdiff
6429                 $formats_nav .= '(initial)';
6430         } elsif (@$parents == 1) {
6431                 # single parent commit
6432                 $formats_nav .=
6433                         '(parent: ' .
6434                         $cgi->a({-href => href(action=>"commit",
6435                                                hash=>$parent)},
6436                                 esc_html(substr($parent, 0, 7))) .
6437                         ')';
6438         } else {
6439                 # merge commit
6440                 $formats_nav .=
6441                         '(merge: ' .
6442                         join(' ', map {
6443                                 $cgi->a({-href => href(action=>"commit",
6444                                                        hash=>$_)},
6445                                         esc_html(substr($_, 0, 7)));
6446                         } @$parents ) .
6447                         ')';
6448         }
6449         if (gitweb_check_feature('patches') && @$parents <= 1) {
6450                 $formats_nav .= " | " .
6451                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
6452                                 "patch");
6453         }
6454
6455         if (!defined $parent) {
6456                 $parent = "--root";
6457         }
6458         my @difftree;
6459         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
6460                 @diff_opts,
6461                 (@$parents <= 1 ? $parent : '-c'),
6462                 $hash, "--"
6463                 or die_error(500, "Open git-diff-tree failed");
6464         @difftree = map { chomp; $_ } <$fd>;
6465         close $fd or die_error(404, "Reading git-diff-tree failed");
6466
6467         # non-textual hash id's can be cached
6468         my $expires;
6469         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6470                 $expires = "+1d";
6471         }
6472         my $refs = git_get_references();
6473         my $ref = format_ref_marker($refs, $co{'id'});
6474
6475         git_header_html(undef, $expires);
6476         git_print_page_nav('commit', '',
6477                            $hash, $co{'tree'}, $hash,
6478                            $formats_nav);
6479
6480         if (defined $co{'parent'}) {
6481                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
6482         } else {
6483                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
6484         }
6485         print "<div class=\"title_text\">\n" .
6486               "<table class=\"object_header\">\n";
6487         git_print_authorship_rows(\%co);
6488         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
6489         print "<tr>" .
6490               "<td>tree</td>" .
6491               "<td class=\"sha1\">" .
6492               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
6493                        class => "list"}, $co{'tree'}) .
6494               "</td>" .
6495               "<td class=\"link\">" .
6496               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
6497                       "tree");
6498         my $snapshot_links = format_snapshot_links($hash);
6499         if (defined $snapshot_links) {
6500                 print " | " . $snapshot_links;
6501         }
6502         print "</td>" .
6503               "</tr>\n";
6504
6505         foreach my $par (@$parents) {
6506                 print "<tr>" .
6507                       "<td>parent</td>" .
6508                       "<td class=\"sha1\">" .
6509                       $cgi->a({-href => href(action=>"commit", hash=>$par),
6510                                class => "list"}, $par) .
6511                       "</td>" .
6512                       "<td class=\"link\">" .
6513                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
6514                       " | " .
6515                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
6516                       "</td>" .
6517                       "</tr>\n";
6518         }
6519         print "</table>".
6520               "</div>\n";
6521
6522         print "<div class=\"page_body\">\n";
6523         git_print_log($co{'comment'});
6524         print "</div>\n";
6525
6526         git_difftree_body(\@difftree, $hash, @$parents);
6527
6528         git_footer_html();
6529 }
6530
6531 sub git_object {
6532         # object is defined by:
6533         # - hash or hash_base alone
6534         # - hash_base and file_name
6535         my $type;
6536
6537         # - hash or hash_base alone
6538         if ($hash || ($hash_base && !defined $file_name)) {
6539                 my $object_id = $hash || $hash_base;
6540
6541                 open my $fd, "-|", quote_command(
6542                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
6543                         or die_error(404, "Object does not exist");
6544                 $type = <$fd>;
6545                 chomp $type;
6546                 close $fd
6547                         or die_error(404, "Object does not exist");
6548
6549         # - hash_base and file_name
6550         } elsif ($hash_base && defined $file_name) {
6551                 $file_name =~ s,/+$,,;
6552
6553                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
6554                         or die_error(404, "Base object does not exist");
6555
6556                 # here errors should not hapen
6557                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
6558                         or die_error(500, "Open git-ls-tree failed");
6559                 my $line = <$fd>;
6560                 close $fd;
6561
6562                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
6563                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
6564                         die_error(404, "File or directory for given base does not exist");
6565                 }
6566                 $type = $2;
6567                 $hash = $3;
6568         } else {
6569                 die_error(400, "Not enough information to find object");
6570         }
6571
6572         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
6573                                           hash=>$hash, hash_base=>$hash_base,
6574                                           file_name=>$file_name),
6575                              -status => '302 Found');
6576 }
6577
6578 sub git_blobdiff {
6579         my $format = shift || 'html';
6580
6581         my $fd;
6582         my @difftree;
6583         my %diffinfo;
6584         my $expires;
6585
6586         # preparing $fd and %diffinfo for git_patchset_body
6587         # new style URI
6588         if (defined $hash_base && defined $hash_parent_base) {
6589                 if (defined $file_name) {
6590                         # read raw output
6591                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6592                                 $hash_parent_base, $hash_base,
6593                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
6594                                 or die_error(500, "Open git-diff-tree failed");
6595                         @difftree = map { chomp; $_ } <$fd>;
6596                         close $fd
6597                                 or die_error(404, "Reading git-diff-tree failed");
6598                         @difftree
6599                                 or die_error(404, "Blob diff not found");
6600
6601                 } elsif (defined $hash &&
6602                          $hash =~ /[0-9a-fA-F]{40}/) {
6603                         # try to find filename from $hash
6604
6605                         # read filtered raw output
6606                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6607                                 $hash_parent_base, $hash_base, "--"
6608                                 or die_error(500, "Open git-diff-tree failed");
6609                         @difftree =
6610                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
6611                                 # $hash == to_id
6612                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
6613                                 map { chomp; $_ } <$fd>;
6614                         close $fd
6615                                 or die_error(404, "Reading git-diff-tree failed");
6616                         @difftree
6617                                 or die_error(404, "Blob diff not found");
6618
6619                 } else {
6620                         die_error(400, "Missing one of the blob diff parameters");
6621                 }
6622
6623                 if (@difftree > 1) {
6624                         die_error(400, "Ambiguous blob diff specification");
6625                 }
6626
6627                 %diffinfo = parse_difftree_raw_line($difftree[0]);
6628                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
6629                 $file_name   ||= $diffinfo{'to_file'};
6630
6631                 $hash_parent ||= $diffinfo{'from_id'};
6632                 $hash        ||= $diffinfo{'to_id'};
6633
6634                 # non-textual hash id's can be cached
6635                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6636                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6637                         $expires = '+1d';
6638                 }
6639
6640                 # open patch output
6641                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6642                         '-p', ($format eq 'html' ? "--full-index" : ()),
6643                         $hash_parent_base, $hash_base,
6644                         "--", (defined $file_parent ? $file_parent : ()), $file_name
6645                         or die_error(500, "Open git-diff-tree failed");
6646         }
6647
6648         # old/legacy style URI -- not generated anymore since 1.4.3.
6649         if (!%diffinfo) {
6650                 die_error('404 Not Found', "Missing one of the blob diff parameters")
6651         }
6652
6653         # header
6654         if ($format eq 'html') {
6655                 my $formats_nav =
6656                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6657                                 "raw");
6658                 git_header_html(undef, $expires);
6659                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6660                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6661                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6662                 } else {
6663                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6664                         print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
6665                 }
6666                 if (defined $file_name) {
6667                         git_print_page_path($file_name, "blob", $hash_base);
6668                 } else {
6669                         print "<div class=\"page_path\"></div>\n";
6670                 }
6671
6672         } elsif ($format eq 'plain') {
6673                 print $cgi->header(
6674                         -type => 'text/plain',
6675                         -charset => 'utf-8',
6676                         -expires => $expires,
6677                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6678
6679                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6680
6681         } else {
6682                 die_error(400, "Unknown blobdiff format");
6683         }
6684
6685         # patch
6686         if ($format eq 'html') {
6687                 print "<div class=\"page_body\">\n";
6688
6689                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6690                 close $fd;
6691
6692                 print "</div>\n"; # class="page_body"
6693                 git_footer_html();
6694
6695         } else {
6696                 while (my $line = <$fd>) {
6697                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6698                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6699
6700                         print $line;
6701
6702                         last if $line =~ m!^\+\+\+!;
6703                 }
6704                 local $/ = undef;
6705                 print <$fd>;
6706                 close $fd;
6707         }
6708 }
6709
6710 sub git_blobdiff_plain {
6711         git_blobdiff('plain');
6712 }
6713
6714 sub git_commitdiff {
6715         my %params = @_;
6716         my $format = $params{-format} || 'html';
6717
6718         my ($patch_max) = gitweb_get_feature('patches');
6719         if ($format eq 'patch') {
6720                 die_error(403, "Patch view not allowed") unless $patch_max;
6721         }
6722
6723         $hash ||= $hash_base || "HEAD";
6724         my %co = parse_commit($hash)
6725             or die_error(404, "Unknown commit object");
6726
6727         # choose format for commitdiff for merge
6728         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6729                 $hash_parent = '--cc';
6730         }
6731         # we need to prepare $formats_nav before almost any parameter munging
6732         my $formats_nav;
6733         if ($format eq 'html') {
6734                 $formats_nav =
6735                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6736                                 "raw");
6737                 if ($patch_max && @{$co{'parents'}} <= 1) {
6738                         $formats_nav .= " | " .
6739                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6740                                         "patch");
6741                 }
6742
6743                 if (defined $hash_parent &&
6744                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
6745                         # commitdiff with two commits given
6746                         my $hash_parent_short = $hash_parent;
6747                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6748                                 $hash_parent_short = substr($hash_parent, 0, 7);
6749                         }
6750                         $formats_nav .=
6751                                 ' (from';
6752                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6753                                 if ($co{'parents'}[$i] eq $hash_parent) {
6754                                         $formats_nav .= ' parent ' . ($i+1);
6755                                         last;
6756                                 }
6757                         }
6758                         $formats_nav .= ': ' .
6759                                 $cgi->a({-href => href(action=>"commitdiff",
6760                                                        hash=>$hash_parent)},
6761                                         esc_html($hash_parent_short)) .
6762                                 ')';
6763                 } elsif (!$co{'parent'}) {
6764                         # --root commitdiff
6765                         $formats_nav .= ' (initial)';
6766                 } elsif (scalar @{$co{'parents'}} == 1) {
6767                         # single parent commit
6768                         $formats_nav .=
6769                                 ' (parent: ' .
6770                                 $cgi->a({-href => href(action=>"commitdiff",
6771                                                        hash=>$co{'parent'})},
6772                                         esc_html(substr($co{'parent'}, 0, 7))) .
6773                                 ')';
6774                 } else {
6775                         # merge commit
6776                         if ($hash_parent eq '--cc') {
6777                                 $formats_nav .= ' | ' .
6778                                         $cgi->a({-href => href(action=>"commitdiff",
6779                                                                hash=>$hash, hash_parent=>'-c')},
6780                                                 'combined');
6781                         } else { # $hash_parent eq '-c'
6782                                 $formats_nav .= ' | ' .
6783                                         $cgi->a({-href => href(action=>"commitdiff",
6784                                                                hash=>$hash, hash_parent=>'--cc')},
6785                                                 'compact');
6786                         }
6787                         $formats_nav .=
6788                                 ' (merge: ' .
6789                                 join(' ', map {
6790                                         $cgi->a({-href => href(action=>"commitdiff",
6791                                                                hash=>$_)},
6792                                                 esc_html(substr($_, 0, 7)));
6793                                 } @{$co{'parents'}} ) .
6794                                 ')';
6795                 }
6796         }
6797
6798         my $hash_parent_param = $hash_parent;
6799         if (!defined $hash_parent_param) {
6800                 # --cc for multiple parents, --root for parentless
6801                 $hash_parent_param =
6802                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6803         }
6804
6805         # read commitdiff
6806         my $fd;
6807         my @difftree;
6808         if ($format eq 'html') {
6809                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6810                         "--no-commit-id", "--patch-with-raw", "--full-index",
6811                         $hash_parent_param, $hash, "--"
6812                         or die_error(500, "Open git-diff-tree failed");
6813
6814                 while (my $line = <$fd>) {
6815                         chomp $line;
6816                         # empty line ends raw part of diff-tree output
6817                         last unless $line;
6818                         push @difftree, scalar parse_difftree_raw_line($line);
6819                 }
6820
6821         } elsif ($format eq 'plain') {
6822                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6823                         '-p', $hash_parent_param, $hash, "--"
6824                         or die_error(500, "Open git-diff-tree failed");
6825         } elsif ($format eq 'patch') {
6826                 # For commit ranges, we limit the output to the number of
6827                 # patches specified in the 'patches' feature.
6828                 # For single commits, we limit the output to a single patch,
6829                 # diverging from the git-format-patch default.
6830                 my @commit_spec = ();
6831                 if ($hash_parent) {
6832                         if ($patch_max > 0) {
6833                                 push @commit_spec, "-$patch_max";
6834                         }
6835                         push @commit_spec, '-n', "$hash_parent..$hash";
6836                 } else {
6837                         if ($params{-single}) {
6838                                 push @commit_spec, '-1';
6839                         } else {
6840                                 if ($patch_max > 0) {
6841                                         push @commit_spec, "-$patch_max";
6842                                 }
6843                                 push @commit_spec, "-n";
6844                         }
6845                         push @commit_spec, '--root', $hash;
6846                 }
6847                 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
6848                         '--encoding=utf8', '--stdout', @commit_spec
6849                         or die_error(500, "Open git-format-patch failed");
6850         } else {
6851                 die_error(400, "Unknown commitdiff format");
6852         }
6853
6854         # non-textual hash id's can be cached
6855         my $expires;
6856         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6857                 $expires = "+1d";
6858         }
6859
6860         # write commit message
6861         if ($format eq 'html') {
6862                 my $refs = git_get_references();
6863                 my $ref = format_ref_marker($refs, $co{'id'});
6864
6865                 git_header_html(undef, $expires);
6866                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6867                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6868                 print "<div class=\"title_text\">\n" .
6869                       "<table class=\"object_header\">\n";
6870                 git_print_authorship_rows(\%co);
6871                 print "</table>".
6872                       "</div>\n";
6873                 print "<div class=\"page_body\">\n";
6874                 if (@{$co{'comment'}} > 1) {
6875                         print "<div class=\"log\">\n";
6876                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6877                         print "</div>\n"; # class="log"
6878                 }
6879
6880         } elsif ($format eq 'plain') {
6881                 my $refs = git_get_references("tags");
6882                 my $tagname = git_get_rev_name_tags($hash);
6883                 my $filename = basename($project) . "-$hash.patch";
6884
6885                 print $cgi->header(
6886                         -type => 'text/plain',
6887                         -charset => 'utf-8',
6888                         -expires => $expires,
6889                         -content_disposition => 'inline; filename="' . "$filename" . '"');
6890                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6891                 print "From: " . to_utf8($co{'author'}) . "\n";
6892                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6893                 print "Subject: " . to_utf8($co{'title'}) . "\n";
6894
6895                 print "X-Git-Tag: $tagname\n" if $tagname;
6896                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6897
6898                 foreach my $line (@{$co{'comment'}}) {
6899                         print to_utf8($line) . "\n";
6900                 }
6901                 print "---\n\n";
6902         } elsif ($format eq 'patch') {
6903                 my $filename = basename($project) . "-$hash.patch";
6904
6905                 print $cgi->header(
6906                         -type => 'text/plain',
6907                         -charset => 'utf-8',
6908                         -expires => $expires,
6909                         -content_disposition => 'inline; filename="' . "$filename" . '"');
6910         }
6911
6912         # write patch
6913         if ($format eq 'html') {
6914                 my $use_parents = !defined $hash_parent ||
6915                         $hash_parent eq '-c' || $hash_parent eq '--cc';
6916                 git_difftree_body(\@difftree, $hash,
6917                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
6918                 print "<br/>\n";
6919
6920                 git_patchset_body($fd, \@difftree, $hash,
6921                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
6922                 close $fd;
6923                 print "</div>\n"; # class="page_body"
6924                 git_footer_html();
6925
6926         } elsif ($format eq 'plain') {
6927                 local $/ = undef;
6928                 print <$fd>;
6929                 close $fd
6930                         or print "Reading git-diff-tree failed\n";
6931         } elsif ($format eq 'patch') {
6932                 local $/ = undef;
6933                 print <$fd>;
6934                 close $fd
6935                         or print "Reading git-format-patch failed\n";
6936         }
6937 }
6938
6939 sub git_commitdiff_plain {
6940         git_commitdiff(-format => 'plain');
6941 }
6942
6943 # format-patch-style patches
6944 sub git_patch {
6945         git_commitdiff(-format => 'patch', -single => 1);
6946 }
6947
6948 sub git_patches {
6949         git_commitdiff(-format => 'patch');
6950 }
6951
6952 sub git_history {
6953         git_log_generic('history', \&git_history_body,
6954                         $hash_base, $hash_parent_base,
6955                         $file_name, $hash);
6956 }
6957
6958 sub git_search {
6959         gitweb_check_feature('search') or die_error(403, "Search is disabled");
6960         if (!defined $searchtext) {
6961                 die_error(400, "Text field is empty");
6962         }
6963         if (!defined $hash) {
6964                 $hash = git_get_head_hash($project);
6965         }
6966         my %co = parse_commit($hash);
6967         if (!%co) {
6968                 die_error(404, "Unknown commit object");
6969         }
6970         if (!defined $page) {
6971                 $page = 0;
6972         }
6973
6974         $searchtype ||= 'commit';
6975         if ($searchtype eq 'pickaxe') {
6976                 # pickaxe may take all resources of your box and run for several minutes
6977                 # with every query - so decide by yourself how public you make this feature
6978                 gitweb_check_feature('pickaxe')
6979                     or die_error(403, "Pickaxe is disabled");
6980         }
6981         if ($searchtype eq 'grep') {
6982                 gitweb_check_feature('grep')
6983                     or die_error(403, "Grep is disabled");
6984         }
6985
6986         git_header_html();
6987
6988         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6989                 my $greptype;
6990                 if ($searchtype eq 'commit') {
6991                         $greptype = "--grep=";
6992                 } elsif ($searchtype eq 'author') {
6993                         $greptype = "--author=";
6994                 } elsif ($searchtype eq 'committer') {
6995                         $greptype = "--committer=";
6996                 }
6997                 $greptype .= $searchtext;
6998                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6999                                                $greptype, '--regexp-ignore-case',
7000                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
7001
7002                 my $paging_nav = '';
7003                 if ($page > 0) {
7004                         $paging_nav .=
7005                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
7006                                                        searchtext=>$searchtext,
7007                                                        searchtype=>$searchtype)},
7008                                         "first");
7009                         $paging_nav .= " &sdot; " .
7010                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
7011                                          -accesskey => "p", -title => "Alt-p"}, "prev");
7012                 } else {
7013                         $paging_nav .= "first";
7014                         $paging_nav .= " &sdot; prev";
7015                 }
7016                 my $next_link = '';
7017                 if ($#commitlist >= 100) {
7018                         $next_link =
7019                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7020                                          -accesskey => "n", -title => "Alt-n"}, "next");
7021                         $paging_nav .= " &sdot; $next_link";
7022                 } else {
7023                         $paging_nav .= " &sdot; next";
7024                 }
7025
7026                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
7027                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7028                 if ($page == 0 && !@commitlist) {
7029                         print "<p>No match.</p>\n";
7030                 } else {
7031                         git_search_grep_body(\@commitlist, 0, 99, $next_link);
7032                 }
7033         }
7034
7035         if ($searchtype eq 'pickaxe') {
7036                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7037                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7038
7039                 print "<table class=\"pickaxe search\">\n";
7040                 my $alternate = 1;
7041                 local $/ = "\n";
7042                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
7043                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7044                         ($search_use_regexp ? '--pickaxe-regex' : ());
7045                 undef %co;
7046                 my @files;
7047                 while (my $line = <$fd>) {
7048                         chomp $line;
7049                         next unless $line;
7050
7051                         my %set = parse_difftree_raw_line($line);
7052                         if (defined $set{'commit'}) {
7053                                 # finish previous commit
7054                                 if (%co) {
7055                                         print "</td>\n" .
7056                                               "<td class=\"link\">" .
7057                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7058                                               " | " .
7059                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7060                                         print "</td>\n" .
7061                                               "</tr>\n";
7062                                 }
7063
7064                                 if ($alternate) {
7065                                         print "<tr class=\"dark\">\n";
7066                                 } else {
7067                                         print "<tr class=\"light\">\n";
7068                                 }
7069                                 $alternate ^= 1;
7070                                 %co = parse_commit($set{'commit'});
7071                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
7072                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7073                                       "<td><i>$author</i></td>\n" .
7074                                       "<td>" .
7075                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7076                                               -class => "list subject"},
7077                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
7078                         } elsif (defined $set{'to_id'}) {
7079                                 next if ($set{'to_id'} =~ m/^0{40}$/);
7080
7081                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
7082                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
7083                                               -class => "list"},
7084                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
7085                                       "<br/>\n";
7086                         }
7087                 }
7088                 close $fd;
7089
7090                 # finish last commit (warning: repetition!)
7091                 if (%co) {
7092                         print "</td>\n" .
7093                               "<td class=\"link\">" .
7094                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7095                               " | " .
7096                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7097                         print "</td>\n" .
7098                               "</tr>\n";
7099                 }
7100
7101                 print "</table>\n";
7102         }
7103
7104         if ($searchtype eq 'grep') {
7105                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7106                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7107
7108                 print "<table class=\"grep_search\">\n";
7109                 my $alternate = 1;
7110                 my $matches = 0;
7111                 local $/ = "\n";
7112                 open my $fd, "-|", git_cmd(), 'grep', '-n',
7113                         $search_use_regexp ? ('-E', '-i') : '-F',
7114                         $searchtext, $co{'tree'};
7115                 my $lastfile = '';
7116                 while (my $line = <$fd>) {
7117                         chomp $line;
7118                         my ($file, $lno, $ltext, $binary);
7119                         last if ($matches++ > 1000);
7120                         if ($line =~ /^Binary file (.+) matches$/) {
7121                                 $file = $1;
7122                                 $binary = 1;
7123                         } else {
7124                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
7125                         }
7126                         if ($file ne $lastfile) {
7127                                 $lastfile and print "</td></tr>\n";
7128                                 if ($alternate++) {
7129                                         print "<tr class=\"dark\">\n";
7130                                 } else {
7131                                         print "<tr class=\"light\">\n";
7132                                 }
7133                                 print "<td class=\"list\">".
7134                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
7135                                                                file_name=>"$file"),
7136                                                 -class => "list"}, esc_path($file));
7137                                 print "</td><td>\n";
7138                                 $lastfile = $file;
7139                         }
7140                         if ($binary) {
7141                                 print "<div class=\"binary\">Binary file</div>\n";
7142                         } else {
7143                                 $ltext = untabify($ltext);
7144                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7145                                         $ltext = esc_html($1, -nbsp=>1);
7146                                         $ltext .= '<span class="match">';
7147                                         $ltext .= esc_html($2, -nbsp=>1);
7148                                         $ltext .= '</span>';
7149                                         $ltext .= esc_html($3, -nbsp=>1);
7150                                 } else {
7151                                         $ltext = esc_html($ltext, -nbsp=>1);
7152                                 }
7153                                 print "<div class=\"pre\">" .
7154                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
7155                                                                file_name=>"$file").'#l'.$lno,
7156                                                 -class => "linenr"}, sprintf('%4i', $lno))
7157                                         . ' ' .  $ltext . "</div>\n";
7158                         }
7159                 }
7160                 if ($lastfile) {
7161                         print "</td></tr>\n";
7162                         if ($matches > 1000) {
7163                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7164                         }
7165                 } else {
7166                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
7167                 }
7168                 close $fd;
7169
7170                 print "</table>\n";
7171         }
7172         git_footer_html();
7173 }
7174
7175 sub git_search_help {
7176         git_header_html();
7177         git_print_page_nav('','', $hash,$hash,$hash);
7178         print <<EOT;
7179 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7180 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7181 the pattern entered is recognized as the POSIX extended
7182 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7183 insensitive).</p>
7184 <dl>
7185 <dt><b>commit</b></dt>
7186 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7187 EOT
7188         my $have_grep = gitweb_check_feature('grep');
7189         if ($have_grep) {
7190                 print <<EOT;
7191 <dt><b>grep</b></dt>
7192 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7193     a different one) are searched for the given pattern. On large trees, this search can take
7194 a while and put some strain on the server, so please use it with some consideration. Note that
7195 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7196 case-sensitive.</dd>
7197 EOT
7198         }
7199         print <<EOT;
7200 <dt><b>author</b></dt>
7201 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7202 <dt><b>committer</b></dt>
7203 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7204 EOT
7205         my $have_pickaxe = gitweb_check_feature('pickaxe');
7206         if ($have_pickaxe) {
7207                 print <<EOT;
7208 <dt><b>pickaxe</b></dt>
7209 <dd>All commits that caused the string to appear or disappear from any file (changes that
7210 added, removed or "modified" the string) will be listed. This search can take a while and
7211 takes a lot of strain on the server, so please use it wisely. Note that since you may be
7212 interested even in changes just changing the case as well, this search is case sensitive.</dd>
7213 EOT
7214         }
7215         print "</dl>\n";
7216         git_footer_html();
7217 }
7218
7219 sub git_shortlog {
7220         git_log_generic('shortlog', \&git_shortlog_body,
7221                         $hash, $hash_parent);
7222 }
7223
7224 ## ......................................................................
7225 ## feeds (RSS, Atom; OPML)
7226
7227 sub git_feed {
7228         my $format = shift || 'atom';
7229         my $have_blame = gitweb_check_feature('blame');
7230
7231         # Atom: http://www.atomenabled.org/developers/syndication/
7232         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7233         if ($format ne 'rss' && $format ne 'atom') {
7234                 die_error(400, "Unknown web feed format");
7235         }
7236
7237         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7238         my $head = $hash || 'HEAD';
7239         my @commitlist = parse_commits($head, 150, 0, $file_name);
7240
7241         my %latest_commit;
7242         my %latest_date;
7243         my $content_type = "application/$format+xml";
7244         if (defined $cgi->http('HTTP_ACCEPT') &&
7245                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7246                 # browser (feed reader) prefers text/xml
7247                 $content_type = 'text/xml';
7248         }
7249         if (defined($commitlist[0])) {
7250                 %latest_commit = %{$commitlist[0]};
7251                 my $latest_epoch = $latest_commit{'committer_epoch'};
7252                 %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
7253                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7254                 if (defined $if_modified) {
7255                         my $since;
7256                         if (eval { require HTTP::Date; 1; }) {
7257                                 $since = HTTP::Date::str2time($if_modified);
7258                         } elsif (eval { require Time::ParseDate; 1; }) {
7259                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7260                         }
7261                         if (defined $since && $latest_epoch <= $since) {
7262                                 print $cgi->header(
7263                                         -type => $content_type,
7264                                         -charset => 'utf-8',
7265                                         -last_modified => $latest_date{'rfc2822'},
7266                                         -status => '304 Not Modified');
7267                                 return;
7268                         }
7269                 }
7270                 print $cgi->header(
7271                         -type => $content_type,
7272                         -charset => 'utf-8',
7273                         -last_modified => $latest_date{'rfc2822'});
7274         } else {
7275                 print $cgi->header(
7276                         -type => $content_type,
7277                         -charset => 'utf-8');
7278         }
7279
7280         # Optimization: skip generating the body if client asks only
7281         # for Last-Modified date.
7282         return if ($cgi->request_method() eq 'HEAD');
7283
7284         # header variables
7285         my $title = "$site_name - $project/$action";
7286         my $feed_type = 'log';
7287         if (defined $hash) {
7288                 $title .= " - '$hash'";
7289                 $feed_type = 'branch log';
7290                 if (defined $file_name) {
7291                         $title .= " :: $file_name";
7292                         $feed_type = 'history';
7293                 }
7294         } elsif (defined $file_name) {
7295                 $title .= " - $file_name";
7296                 $feed_type = 'history';
7297         }
7298         $title .= " $feed_type";
7299         my $descr = git_get_project_description($project);
7300         if (defined $descr) {
7301                 $descr = esc_html($descr);
7302         } else {
7303                 $descr = "$project " .
7304                          ($format eq 'rss' ? 'RSS' : 'Atom') .
7305                          " feed";
7306         }
7307         my $owner = git_get_project_owner($project);
7308         $owner = esc_html($owner);
7309
7310         #header
7311         my $alt_url;
7312         if (defined $file_name) {
7313                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7314         } elsif (defined $hash) {
7315                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7316         } else {
7317                 $alt_url = href(-full=>1, action=>"summary");
7318         }
7319         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7320         if ($format eq 'rss') {
7321                 print <<XML;
7322 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7323 <channel>
7324 XML
7325                 print "<title>$title</title>\n" .
7326                       "<link>$alt_url</link>\n" .
7327                       "<description>$descr</description>\n" .
7328                       "<language>en</language>\n" .
7329                       # project owner is responsible for 'editorial' content
7330                       "<managingEditor>$owner</managingEditor>\n";
7331                 if (defined $logo || defined $favicon) {
7332                         # prefer the logo to the favicon, since RSS
7333                         # doesn't allow both
7334                         my $img = esc_url($logo || $favicon);
7335                         print "<image>\n" .
7336                               "<url>$img</url>\n" .
7337                               "<title>$title</title>\n" .
7338                               "<link>$alt_url</link>\n" .
7339                               "</image>\n";
7340                 }
7341                 if (%latest_date) {
7342                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7343                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7344                 }
7345                 print "<generator>gitweb v.$version/$git_version</generator>\n";
7346         } elsif ($format eq 'atom') {
7347                 print <<XML;
7348 <feed xmlns="http://www.w3.org/2005/Atom">
7349 XML
7350                 print "<title>$title</title>\n" .
7351                       "<subtitle>$descr</subtitle>\n" .
7352                       '<link rel="alternate" type="text/html" href="' .
7353                       $alt_url . '" />' . "\n" .
7354                       '<link rel="self" type="' . $content_type . '" href="' .
7355                       $cgi->self_url() . '" />' . "\n" .
7356                       "<id>" . href(-full=>1) . "</id>\n" .
7357                       # use project owner for feed author
7358                       "<author><name>$owner</name></author>\n";
7359                 if (defined $favicon) {
7360                         print "<icon>" . esc_url($favicon) . "</icon>\n";
7361                 }
7362                 if (defined $logo) {
7363                         # not twice as wide as tall: 72 x 27 pixels
7364                         print "<logo>" . esc_url($logo) . "</logo>\n";
7365                 }
7366                 if (! %latest_date) {
7367                         # dummy date to keep the feed valid until commits trickle in:
7368                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
7369                 } else {
7370                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
7371                 }
7372                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
7373         }
7374
7375         # contents
7376         for (my $i = 0; $i <= $#commitlist; $i++) {
7377                 my %co = %{$commitlist[$i]};
7378                 my $commit = $co{'id'};
7379                 # we read 150, we always show 30 and the ones more recent than 48 hours
7380                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
7381                         last;
7382                 }
7383                 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
7384
7385                 # get list of changed files
7386                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7387                         $co{'parent'} || "--root",
7388                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
7389                         or next;
7390                 my @difftree = map { chomp; $_ } <$fd>;
7391                 close $fd
7392                         or next;
7393
7394                 # print element (entry, item)
7395                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7396                 if ($format eq 'rss') {
7397                         print "<item>\n" .
7398                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
7399                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
7400                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7401                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7402                               "<link>$co_url</link>\n" .
7403                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
7404                               "<content:encoded>" .
7405                               "<![CDATA[\n";
7406                 } elsif ($format eq 'atom') {
7407                         print "<entry>\n" .
7408                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7409                               "<updated>$cd{'iso-8601'}</updated>\n" .
7410                               "<author>\n" .
7411                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
7412                         if ($co{'author_email'}) {
7413                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
7414                         }
7415                         print "</author>\n" .
7416                               # use committer for contributor
7417                               "<contributor>\n" .
7418                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7419                         if ($co{'committer_email'}) {
7420                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7421                         }
7422                         print "</contributor>\n" .
7423                               "<published>$cd{'iso-8601'}</published>\n" .
7424                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
7425                               "<id>$co_url</id>\n" .
7426                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
7427                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
7428                 }
7429                 my $comment = $co{'comment'};
7430                 print "<pre>\n";
7431                 foreach my $line (@$comment) {
7432                         $line = esc_html($line);
7433                         print "$line\n";
7434                 }
7435                 print "</pre><ul>\n";
7436                 foreach my $difftree_line (@difftree) {
7437                         my %difftree = parse_difftree_raw_line($difftree_line);
7438                         next if !$difftree{'from_id'};
7439
7440                         my $file = $difftree{'file'} || $difftree{'to_file'};
7441
7442                         print "<li>" .
7443                               "[" .
7444                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
7445                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
7446                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
7447                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
7448                                       -title => "diff"}, 'D');
7449                         if ($have_blame) {
7450                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
7451                                                              file_name=>$file, hash_base=>$commit),
7452                                               -title => "blame"}, 'B');
7453                         }
7454                         # if this is not a feed of a file history
7455                         if (!defined $file_name || $file_name ne $file) {
7456                                 print $cgi->a({-href => href(-full=>1, action=>"history",
7457                                                              file_name=>$file, hash=>$commit),
7458                                               -title => "history"}, 'H');
7459                         }
7460                         $file = esc_path($file);
7461                         print "] ".
7462                               "$file</li>\n";
7463                 }
7464                 if ($format eq 'rss') {
7465                         print "</ul>]]>\n" .
7466                               "</content:encoded>\n" .
7467                               "</item>\n";
7468                 } elsif ($format eq 'atom') {
7469                         print "</ul>\n</div>\n" .
7470                               "</content>\n" .
7471                               "</entry>\n";
7472                 }
7473         }
7474
7475         # end of feed
7476         if ($format eq 'rss') {
7477                 print "</channel>\n</rss>\n";
7478         } elsif ($format eq 'atom') {
7479                 print "</feed>\n";
7480         }
7481 }
7482
7483 sub git_rss {
7484         git_feed('rss');
7485 }
7486
7487 sub git_atom {
7488         git_feed('atom');
7489 }
7490
7491 sub git_opml {
7492         my @list = git_get_projects_list();
7493         if (!@list) {
7494                 die_error(404, "No projects found");
7495         }
7496
7497         print $cgi->header(
7498                 -type => 'text/xml',
7499                 -charset => 'utf-8',
7500                 -content_disposition => 'inline; filename="opml.xml"');
7501
7502         print <<XML;
7503 <?xml version="1.0" encoding="utf-8"?>
7504 <opml version="1.0">
7505 <head>
7506   <title>$site_name OPML Export</title>
7507 </head>
7508 <body>
7509 <outline text="git RSS feeds">
7510 XML
7511
7512         foreach my $pr (@list) {
7513                 my %proj = %$pr;
7514                 my $head = git_get_head_hash($proj{'path'});
7515                 if (!defined $head) {
7516                         next;
7517                 }
7518                 $git_dir = "$projectroot/$proj{'path'}";
7519                 my %co = parse_commit($head);
7520                 if (!%co) {
7521                         next;
7522                 }
7523
7524                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
7525                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7526                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
7527                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7528         }
7529         print <<XML;
7530 </outline>
7531 </body>
7532 </opml>
7533 XML
7534 }