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