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