gitweb: Don't set owner if got empty value from projects.list
[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                 $out .= esc_html(substr($str, $pos, $s->[0] - $pos))
1742                         if ($s->[0] - $pos > 0);
1743                 $out .= $cgi->span({-class => $css_class},
1744                                    esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
1745
1746                 $pos = $s->[1];
1747         }
1748         $out .= esc_html(substr($str, $pos))
1749                 if ($pos < length($str));
1750
1751         return $out;
1752 }
1753
1754 # return positions of beginning and end of each match
1755 sub matchpos_list {
1756         my ($str, $regexp) = @_;
1757         return unless (defined $str && defined $regexp);
1758
1759         my @matches;
1760         while ($str =~ /$regexp/g) {
1761                 push @matches, [$-[0], $+[0]];
1762         }
1763         return @matches;
1764 }
1765
1766 # highlight match (if any), and escape HTML
1767 sub esc_html_match_hl {
1768         my ($str, $regexp) = @_;
1769         return esc_html($str) unless defined $regexp;
1770
1771         my @matches = matchpos_list($str, $regexp);
1772         return esc_html($str) unless @matches;
1773
1774         return esc_html_hl_regions($str, 'match', @matches);
1775 }
1776
1777
1778 # highlight match (if any) of shortened string, and escape HTML
1779 sub esc_html_match_hl_chopped {
1780         my ($str, $chopped, $regexp) = @_;
1781         return esc_html_match_hl($str, $regexp) unless defined $chopped;
1782
1783         my @matches = matchpos_list($str, $regexp);
1784         return esc_html($chopped) unless @matches;
1785
1786         # filter matches so that we mark chopped string
1787         my $tail = "... "; # see chop_str
1788         unless ($chopped =~ s/\Q$tail\E$//) {
1789                 $tail = '';
1790         }
1791         my $chop_len = length($chopped);
1792         my $tail_len = length($tail);
1793         my @filtered;
1794
1795         for my $m (@matches) {
1796                 if ($m->[0] > $chop_len) {
1797                         push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
1798                         last;
1799                 } elsif ($m->[1] > $chop_len) {
1800                         push @filtered, [ $m->[0], $chop_len + $tail_len ];
1801                         last;
1802                 }
1803                 push @filtered, $m;
1804         }
1805
1806         return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
1807 }
1808
1809 ## ----------------------------------------------------------------------
1810 ## functions returning short strings
1811
1812 # CSS class for given age value (in seconds)
1813 sub age_class {
1814         my $age = shift;
1815
1816         if (!defined $age) {
1817                 return "noage";
1818         } elsif ($age < 60*60*2) {
1819                 return "age0";
1820         } elsif ($age < 60*60*24*2) {
1821                 return "age1";
1822         } else {
1823                 return "age2";
1824         }
1825 }
1826
1827 # convert age in seconds to "nn units ago" string
1828 sub age_string {
1829         my $age = shift;
1830         my $age_str;
1831
1832         if ($age > 60*60*24*365*2) {
1833                 $age_str = (int $age/60/60/24/365);
1834                 $age_str .= " years ago";
1835         } elsif ($age > 60*60*24*(365/12)*2) {
1836                 $age_str = int $age/60/60/24/(365/12);
1837                 $age_str .= " months ago";
1838         } elsif ($age > 60*60*24*7*2) {
1839                 $age_str = int $age/60/60/24/7;
1840                 $age_str .= " weeks ago";
1841         } elsif ($age > 60*60*24*2) {
1842                 $age_str = int $age/60/60/24;
1843                 $age_str .= " days ago";
1844         } elsif ($age > 60*60*2) {
1845                 $age_str = int $age/60/60;
1846                 $age_str .= " hours ago";
1847         } elsif ($age > 60*2) {
1848                 $age_str = int $age/60;
1849                 $age_str .= " min ago";
1850         } elsif ($age > 2) {
1851                 $age_str = int $age;
1852                 $age_str .= " sec ago";
1853         } else {
1854                 $age_str .= " right now";
1855         }
1856         return $age_str;
1857 }
1858
1859 use constant {
1860         S_IFINVALID => 0030000,
1861         S_IFGITLINK => 0160000,
1862 };
1863
1864 # submodule/subproject, a commit object reference
1865 sub S_ISGITLINK {
1866         my $mode = shift;
1867
1868         return (($mode & S_IFMT) == S_IFGITLINK)
1869 }
1870
1871 # convert file mode in octal to symbolic file mode string
1872 sub mode_str {
1873         my $mode = oct shift;
1874
1875         if (S_ISGITLINK($mode)) {
1876                 return 'm---------';
1877         } elsif (S_ISDIR($mode & S_IFMT)) {
1878                 return 'drwxr-xr-x';
1879         } elsif (S_ISLNK($mode)) {
1880                 return 'lrwxrwxrwx';
1881         } elsif (S_ISREG($mode)) {
1882                 # git cares only about the executable bit
1883                 if ($mode & S_IXUSR) {
1884                         return '-rwxr-xr-x';
1885                 } else {
1886                         return '-rw-r--r--';
1887                 };
1888         } else {
1889                 return '----------';
1890         }
1891 }
1892
1893 # convert file mode in octal to file type string
1894 sub file_type {
1895         my $mode = shift;
1896
1897         if ($mode !~ m/^[0-7]+$/) {
1898                 return $mode;
1899         } else {
1900                 $mode = oct $mode;
1901         }
1902
1903         if (S_ISGITLINK($mode)) {
1904                 return "submodule";
1905         } elsif (S_ISDIR($mode & S_IFMT)) {
1906                 return "directory";
1907         } elsif (S_ISLNK($mode)) {
1908                 return "symlink";
1909         } elsif (S_ISREG($mode)) {
1910                 return "file";
1911         } else {
1912                 return "unknown";
1913         }
1914 }
1915
1916 # convert file mode in octal to file type description string
1917 sub file_type_long {
1918         my $mode = shift;
1919
1920         if ($mode !~ m/^[0-7]+$/) {
1921                 return $mode;
1922         } else {
1923                 $mode = oct $mode;
1924         }
1925
1926         if (S_ISGITLINK($mode)) {
1927                 return "submodule";
1928         } elsif (S_ISDIR($mode & S_IFMT)) {
1929                 return "directory";
1930         } elsif (S_ISLNK($mode)) {
1931                 return "symlink";
1932         } elsif (S_ISREG($mode)) {
1933                 if ($mode & S_IXUSR) {
1934                         return "executable";
1935                 } else {
1936                         return "file";
1937                 };
1938         } else {
1939                 return "unknown";
1940         }
1941 }
1942
1943
1944 ## ----------------------------------------------------------------------
1945 ## functions returning short HTML fragments, or transforming HTML fragments
1946 ## which don't belong to other sections
1947
1948 # format line of commit message.
1949 sub format_log_line_html {
1950         my $line = shift;
1951
1952         $line = esc_html($line, -nbsp=>1);
1953         $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1954                 $cgi->a({-href => href(action=>"object", hash=>$1),
1955                                         -class => "text"}, $1);
1956         }eg;
1957
1958         return $line;
1959 }
1960
1961 # format marker of refs pointing to given object
1962
1963 # the destination action is chosen based on object type and current context:
1964 # - for annotated tags, we choose the tag view unless it's the current view
1965 #   already, in which case we go to shortlog view
1966 # - for other refs, we keep the current view if we're in history, shortlog or
1967 #   log view, and select shortlog otherwise
1968 sub format_ref_marker {
1969         my ($refs, $id) = @_;
1970         my $markers = '';
1971
1972         if (defined $refs->{$id}) {
1973                 foreach my $ref (@{$refs->{$id}}) {
1974                         # this code exploits the fact that non-lightweight tags are the
1975                         # only indirect objects, and that they are the only objects for which
1976                         # we want to use tag instead of shortlog as action
1977                         my ($type, $name) = qw();
1978                         my $indirect = ($ref =~ s/\^\{\}$//);
1979                         # e.g. tags/v2.6.11 or heads/next
1980                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1981                                 $type = $1;
1982                                 $name = $2;
1983                         } else {
1984                                 $type = "ref";
1985                                 $name = $ref;
1986                         }
1987
1988                         my $class = $type;
1989                         $class .= " indirect" if $indirect;
1990
1991                         my $dest_action = "shortlog";
1992
1993                         if ($indirect) {
1994                                 $dest_action = "tag" unless $action eq "tag";
1995                         } elsif ($action =~ /^(history|(short)?log)$/) {
1996                                 $dest_action = $action;
1997                         }
1998
1999                         my $dest = "";
2000                         $dest .= "refs/" unless $ref =~ m!^refs/!;
2001                         $dest .= $ref;
2002
2003                         my $link = $cgi->a({
2004                                 -href => href(
2005                                         action=>$dest_action,
2006                                         hash=>$dest
2007                                 )}, $name);
2008
2009                         $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2010                                 $link . "</span>";
2011                 }
2012         }
2013
2014         if ($markers) {
2015                 return ' <span class="refs">'. $markers . '</span>';
2016         } else {
2017                 return "";
2018         }
2019 }
2020
2021 # format, perhaps shortened and with markers, title line
2022 sub format_subject_html {
2023         my ($long, $short, $href, $extra) = @_;
2024         $extra = '' unless defined($extra);
2025
2026         if (length($short) < length($long)) {
2027                 $long =~ s/[[:cntrl:]]/?/g;
2028                 return $cgi->a({-href => $href, -class => "list subject",
2029                                 -title => to_utf8($long)},
2030                        esc_html($short)) . $extra;
2031         } else {
2032                 return $cgi->a({-href => $href, -class => "list subject"},
2033                        esc_html($long)) . $extra;
2034         }
2035 }
2036
2037 # Rather than recomputing the url for an email multiple times, we cache it
2038 # after the first hit. This gives a visible benefit in views where the avatar
2039 # for the same email is used repeatedly (e.g. shortlog).
2040 # The cache is shared by all avatar engines (currently gravatar only), which
2041 # are free to use it as preferred. Since only one avatar engine is used for any
2042 # given page, there's no risk for cache conflicts.
2043 our %avatar_cache = ();
2044
2045 # Compute the picon url for a given email, by using the picon search service over at
2046 # http://www.cs.indiana.edu/picons/search.html
2047 sub picon_url {
2048         my $email = lc shift;
2049         if (!$avatar_cache{$email}) {
2050                 my ($user, $domain) = split('@', $email);
2051                 $avatar_cache{$email} =
2052                         "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2053                         "$domain/$user/" .
2054                         "users+domains+unknown/up/single";
2055         }
2056         return $avatar_cache{$email};
2057 }
2058
2059 # Compute the gravatar url for a given email, if it's not in the cache already.
2060 # Gravatar stores only the part of the URL before the size, since that's the
2061 # one computationally more expensive. This also allows reuse of the cache for
2062 # different sizes (for this particular engine).
2063 sub gravatar_url {
2064         my $email = lc shift;
2065         my $size = shift;
2066         $avatar_cache{$email} ||=
2067                 "http://www.gravatar.com/avatar/" .
2068                         Digest::MD5::md5_hex($email) . "?s=";
2069         return $avatar_cache{$email} . $size;
2070 }
2071
2072 # Insert an avatar for the given $email at the given $size if the feature
2073 # is enabled.
2074 sub git_get_avatar {
2075         my ($email, %opts) = @_;
2076         my $pre_white  = ($opts{-pad_before} ? "&nbsp;" : "");
2077         my $post_white = ($opts{-pad_after}  ? "&nbsp;" : "");
2078         $opts{-size} ||= 'default';
2079         my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2080         my $url = "";
2081         if ($git_avatar eq 'gravatar') {
2082                 $url = gravatar_url($email, $size);
2083         } elsif ($git_avatar eq 'picon') {
2084                 $url = picon_url($email);
2085         }
2086         # Other providers can be added by extending the if chain, defining $url
2087         # as needed. If no variant puts something in $url, we assume avatars
2088         # are completely disabled/unavailable.
2089         if ($url) {
2090                 return $pre_white .
2091                        "<img width=\"$size\" " .
2092                             "class=\"avatar\" " .
2093                             "src=\"".esc_url($url)."\" " .
2094                             "alt=\"\" " .
2095                        "/>" . $post_white;
2096         } else {
2097                 return "";
2098         }
2099 }
2100
2101 sub format_search_author {
2102         my ($author, $searchtype, $displaytext) = @_;
2103         my $have_search = gitweb_check_feature('search');
2104
2105         if ($have_search) {
2106                 my $performed = "";
2107                 if ($searchtype eq 'author') {
2108                         $performed = "authored";
2109                 } elsif ($searchtype eq 'committer') {
2110                         $performed = "committed";
2111                 }
2112
2113                 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2114                                 searchtext=>$author,
2115                                 searchtype=>$searchtype), class=>"list",
2116                                 title=>"Search for commits $performed by $author"},
2117                                 $displaytext);
2118
2119         } else {
2120                 return $displaytext;
2121         }
2122 }
2123
2124 # format the author name of the given commit with the given tag
2125 # the author name is chopped and escaped according to the other
2126 # optional parameters (see chop_str).
2127 sub format_author_html {
2128         my $tag = shift;
2129         my $co = shift;
2130         my $author = chop_and_escape_str($co->{'author_name'}, @_);
2131         return "<$tag class=\"author\">" .
2132                format_search_author($co->{'author_name'}, "author",
2133                        git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2134                        $author) .
2135                "</$tag>";
2136 }
2137
2138 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2139 sub format_git_diff_header_line {
2140         my $line = shift;
2141         my $diffinfo = shift;
2142         my ($from, $to) = @_;
2143
2144         if ($diffinfo->{'nparents'}) {
2145                 # combined diff
2146                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2147                 if ($to->{'href'}) {
2148                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2149                                          esc_path($to->{'file'}));
2150                 } else { # file was deleted (no href)
2151                         $line .= esc_path($to->{'file'});
2152                 }
2153         } else {
2154                 # "ordinary" diff
2155                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2156                 if ($from->{'href'}) {
2157                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2158                                          'a/' . esc_path($from->{'file'}));
2159                 } else { # file was added (no href)
2160                         $line .= 'a/' . esc_path($from->{'file'});
2161                 }
2162                 $line .= ' ';
2163                 if ($to->{'href'}) {
2164                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2165                                          'b/' . esc_path($to->{'file'}));
2166                 } else { # file was deleted
2167                         $line .= 'b/' . esc_path($to->{'file'});
2168                 }
2169         }
2170
2171         return "<div class=\"diff header\">$line</div>\n";
2172 }
2173
2174 # format extended diff header line, before patch itself
2175 sub format_extended_diff_header_line {
2176         my $line = shift;
2177         my $diffinfo = shift;
2178         my ($from, $to) = @_;
2179
2180         # match <path>
2181         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2182                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2183                                        esc_path($from->{'file'}));
2184         }
2185         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2186                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2187                                  esc_path($to->{'file'}));
2188         }
2189         # match single <mode>
2190         if ($line =~ m/\s(\d{6})$/) {
2191                 $line .= '<span class="info"> (' .
2192                          file_type_long($1) .
2193                          ')</span>';
2194         }
2195         # match <hash>
2196         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2197                 # can match only for combined diff
2198                 $line = 'index ';
2199                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2200                         if ($from->{'href'}[$i]) {
2201                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2202                                                   -class=>"hash"},
2203                                                  substr($diffinfo->{'from_id'}[$i],0,7));
2204                         } else {
2205                                 $line .= '0' x 7;
2206                         }
2207                         # separator
2208                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2209                 }
2210                 $line .= '..';
2211                 if ($to->{'href'}) {
2212                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2213                                          substr($diffinfo->{'to_id'},0,7));
2214                 } else {
2215                         $line .= '0' x 7;
2216                 }
2217
2218         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2219                 # can match only for ordinary diff
2220                 my ($from_link, $to_link);
2221                 if ($from->{'href'}) {
2222                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2223                                              substr($diffinfo->{'from_id'},0,7));
2224                 } else {
2225                         $from_link = '0' x 7;
2226                 }
2227                 if ($to->{'href'}) {
2228                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2229                                            substr($diffinfo->{'to_id'},0,7));
2230                 } else {
2231                         $to_link = '0' x 7;
2232                 }
2233                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2234                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2235         }
2236
2237         return $line . "<br/>\n";
2238 }
2239
2240 # format from-file/to-file diff header
2241 sub format_diff_from_to_header {
2242         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2243         my $line;
2244         my $result = '';
2245
2246         $line = $from_line;
2247         #assert($line =~ m/^---/) if DEBUG;
2248         # no extra formatting for "^--- /dev/null"
2249         if (! $diffinfo->{'nparents'}) {
2250                 # ordinary (single parent) diff
2251                 if ($line =~ m!^--- "?a/!) {
2252                         if ($from->{'href'}) {
2253                                 $line = '--- a/' .
2254                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2255                                                 esc_path($from->{'file'}));
2256                         } else {
2257                                 $line = '--- a/' .
2258                                         esc_path($from->{'file'});
2259                         }
2260                 }
2261                 $result .= qq!<div class="diff from_file">$line</div>\n!;
2262
2263         } else {
2264                 # combined diff (merge commit)
2265                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2266                         if ($from->{'href'}[$i]) {
2267                                 $line = '--- ' .
2268                                         $cgi->a({-href=>href(action=>"blobdiff",
2269                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
2270                                                              hash_parent_base=>$parents[$i],
2271                                                              file_parent=>$from->{'file'}[$i],
2272                                                              hash=>$diffinfo->{'to_id'},
2273                                                              hash_base=>$hash,
2274                                                              file_name=>$to->{'file'}),
2275                                                  -class=>"path",
2276                                                  -title=>"diff" . ($i+1)},
2277                                                 $i+1) .
2278                                         '/' .
2279                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2280                                                 esc_path($from->{'file'}[$i]));
2281                         } else {
2282                                 $line = '--- /dev/null';
2283                         }
2284                         $result .= qq!<div class="diff from_file">$line</div>\n!;
2285                 }
2286         }
2287
2288         $line = $to_line;
2289         #assert($line =~ m/^\+\+\+/) if DEBUG;
2290         # no extra formatting for "^+++ /dev/null"
2291         if ($line =~ m!^\+\+\+ "?b/!) {
2292                 if ($to->{'href'}) {
2293                         $line = '+++ b/' .
2294                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2295                                         esc_path($to->{'file'}));
2296                 } else {
2297                         $line = '+++ b/' .
2298                                 esc_path($to->{'file'});
2299                 }
2300         }
2301         $result .= qq!<div class="diff to_file">$line</div>\n!;
2302
2303         return $result;
2304 }
2305
2306 # create note for patch simplified by combined diff
2307 sub format_diff_cc_simplified {
2308         my ($diffinfo, @parents) = @_;
2309         my $result = '';
2310
2311         $result .= "<div class=\"diff header\">" .
2312                    "diff --cc ";
2313         if (!is_deleted($diffinfo)) {
2314                 $result .= $cgi->a({-href => href(action=>"blob",
2315                                                   hash_base=>$hash,
2316                                                   hash=>$diffinfo->{'to_id'},
2317                                                   file_name=>$diffinfo->{'to_file'}),
2318                                     -class => "path"},
2319                                    esc_path($diffinfo->{'to_file'}));
2320         } else {
2321                 $result .= esc_path($diffinfo->{'to_file'});
2322         }
2323         $result .= "</div>\n" . # class="diff header"
2324                    "<div class=\"diff nodifferences\">" .
2325                    "Simple merge" .
2326                    "</div>\n"; # class="diff nodifferences"
2327
2328         return $result;
2329 }
2330
2331 sub diff_line_class {
2332         my ($line, $from, $to) = @_;
2333
2334         # ordinary diff
2335         my $num_sign = 1;
2336         # combined diff
2337         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2338                 $num_sign = scalar @{$from->{'href'}};
2339         }
2340
2341         my @diff_line_classifier = (
2342                 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
2343                 { regexp => qr/^\\/,               class => "incomplete"  },
2344                 { regexp => qr/^ {$num_sign}/,     class => "ctx" },
2345                 # classifier for context must come before classifier add/rem,
2346                 # or we would have to use more complicated regexp, for example
2347                 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2348                 { regexp => qr/^[+ ]{$num_sign}/,   class => "add" },
2349                 { regexp => qr/^[- ]{$num_sign}/,   class => "rem" },
2350         );
2351         for my $clsfy (@diff_line_classifier) {
2352                 return $clsfy->{'class'}
2353                         if ($line =~ $clsfy->{'regexp'});
2354         }
2355
2356         # fallback
2357         return "";
2358 }
2359
2360 # assumes that $from and $to are defined and correctly filled,
2361 # and that $line holds a line of chunk header for unified diff
2362 sub format_unidiff_chunk_header {
2363         my ($line, $from, $to) = @_;
2364
2365         my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2366                 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2367
2368         $from_lines = 0 unless defined $from_lines;
2369         $to_lines   = 0 unless defined $to_lines;
2370
2371         if ($from->{'href'}) {
2372                 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2373                                      -class=>"list"}, $from_text);
2374         }
2375         if ($to->{'href'}) {
2376                 $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2377                                      -class=>"list"}, $to_text);
2378         }
2379         $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2380                 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2381         return $line;
2382 }
2383
2384 # assumes that $from and $to are defined and correctly filled,
2385 # and that $line holds a line of chunk header for combined diff
2386 sub format_cc_diff_chunk_header {
2387         my ($line, $from, $to) = @_;
2388
2389         my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2390         my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2391
2392         @from_text = split(' ', $ranges);
2393         for (my $i = 0; $i < @from_text; ++$i) {
2394                 ($from_start[$i], $from_nlines[$i]) =
2395                         (split(',', substr($from_text[$i], 1)), 0);
2396         }
2397
2398         $to_text   = pop @from_text;
2399         $to_start  = pop @from_start;
2400         $to_nlines = pop @from_nlines;
2401
2402         $line = "<span class=\"chunk_info\">$prefix ";
2403         for (my $i = 0; $i < @from_text; ++$i) {
2404                 if ($from->{'href'}[$i]) {
2405                         $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2406                                           -class=>"list"}, $from_text[$i]);
2407                 } else {
2408                         $line .= $from_text[$i];
2409                 }
2410                 $line .= " ";
2411         }
2412         if ($to->{'href'}) {
2413                 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2414                                   -class=>"list"}, $to_text);
2415         } else {
2416                 $line .= $to_text;
2417         }
2418         $line .= " $prefix</span>" .
2419                  "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2420         return $line;
2421 }
2422
2423 # process patch (diff) line (not to be used for diff headers),
2424 # returning class and HTML-formatted (but not wrapped) line
2425 sub process_diff_line {
2426         my $line = shift;
2427         my ($from, $to) = @_;
2428
2429         my $diff_class = diff_line_class($line, $from, $to);
2430
2431         chomp $line;
2432         $line = untabify($line);
2433
2434         if ($from && $to && $line =~ m/^\@{2} /) {
2435                 $line = format_unidiff_chunk_header($line, $from, $to);
2436                 return $diff_class, $line;
2437
2438         } elsif ($from && $to && $line =~ m/^\@{3}/) {
2439                 $line = format_cc_diff_chunk_header($line, $from, $to);
2440                 return $diff_class, $line;
2441
2442         }
2443         return $diff_class, esc_html($line, -nbsp=>1);
2444 }
2445
2446 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2447 # linked.  Pass the hash of the tree/commit to snapshot.
2448 sub format_snapshot_links {
2449         my ($hash) = @_;
2450         my $num_fmts = @snapshot_fmts;
2451         if ($num_fmts > 1) {
2452                 # A parenthesized list of links bearing format names.
2453                 # e.g. "snapshot (_tar.gz_ _zip_)"
2454                 return "snapshot (" . join(' ', map
2455                         $cgi->a({
2456                                 -href => href(
2457                                         action=>"snapshot",
2458                                         hash=>$hash,
2459                                         snapshot_format=>$_
2460                                 )
2461                         }, $known_snapshot_formats{$_}{'display'})
2462                 , @snapshot_fmts) . ")";
2463         } elsif ($num_fmts == 1) {
2464                 # A single "snapshot" link whose tooltip bears the format name.
2465                 # i.e. "_snapshot_"
2466                 my ($fmt) = @snapshot_fmts;
2467                 return
2468                         $cgi->a({
2469                                 -href => href(
2470                                         action=>"snapshot",
2471                                         hash=>$hash,
2472                                         snapshot_format=>$fmt
2473                                 ),
2474                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2475                         }, "snapshot");
2476         } else { # $num_fmts == 0
2477                 return undef;
2478         }
2479 }
2480
2481 ## ......................................................................
2482 ## functions returning values to be passed, perhaps after some
2483 ## transformation, to other functions; e.g. returning arguments to href()
2484
2485 # returns hash to be passed to href to generate gitweb URL
2486 # in -title key it returns description of link
2487 sub get_feed_info {
2488         my $format = shift || 'Atom';
2489         my %res = (action => lc($format));
2490
2491         # feed links are possible only for project views
2492         return unless (defined $project);
2493         # some views should link to OPML, or to generic project feed,
2494         # or don't have specific feed yet (so they should use generic)
2495         return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2496
2497         my $branch;
2498         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2499         # from tag links; this also makes possible to detect branch links
2500         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2501             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
2502                 $branch = $1;
2503         }
2504         # find log type for feed description (title)
2505         my $type = 'log';
2506         if (defined $file_name) {
2507                 $type  = "history of $file_name";
2508                 $type .= "/" if ($action eq 'tree');
2509                 $type .= " on '$branch'" if (defined $branch);
2510         } else {
2511                 $type = "log of $branch" if (defined $branch);
2512         }
2513
2514         $res{-title} = $type;
2515         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2516         $res{'file_name'} = $file_name;
2517
2518         return %res;
2519 }
2520
2521 ## ----------------------------------------------------------------------
2522 ## git utility subroutines, invoking git commands
2523
2524 # returns path to the core git executable and the --git-dir parameter as list
2525 sub git_cmd {
2526         $number_of_git_cmds++;
2527         return $GIT, '--git-dir='.$git_dir;
2528 }
2529
2530 # quote the given arguments for passing them to the shell
2531 # quote_command("command", "arg 1", "arg with ' and ! characters")
2532 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2533 # Try to avoid using this function wherever possible.
2534 sub quote_command {
2535         return join(' ',
2536                 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2537 }
2538
2539 # get HEAD ref of given project as hash
2540 sub git_get_head_hash {
2541         return git_get_full_hash(shift, 'HEAD');
2542 }
2543
2544 sub git_get_full_hash {
2545         return git_get_hash(@_);
2546 }
2547
2548 sub git_get_short_hash {
2549         return git_get_hash(@_, '--short=7');
2550 }
2551
2552 sub git_get_hash {
2553         my ($project, $hash, @options) = @_;
2554         my $o_git_dir = $git_dir;
2555         my $retval = undef;
2556         $git_dir = "$projectroot/$project";
2557         if (open my $fd, '-|', git_cmd(), 'rev-parse',
2558             '--verify', '-q', @options, $hash) {
2559                 $retval = <$fd>;
2560                 chomp $retval if defined $retval;
2561                 close $fd;
2562         }
2563         if (defined $o_git_dir) {
2564                 $git_dir = $o_git_dir;
2565         }
2566         return $retval;
2567 }
2568
2569 # get type of given object
2570 sub git_get_type {
2571         my $hash = shift;
2572
2573         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2574         my $type = <$fd>;
2575         close $fd or return;
2576         chomp $type;
2577         return $type;
2578 }
2579
2580 # repository configuration
2581 our $config_file = '';
2582 our %config;
2583
2584 # store multiple values for single key as anonymous array reference
2585 # single values stored directly in the hash, not as [ <value> ]
2586 sub hash_set_multi {
2587         my ($hash, $key, $value) = @_;
2588
2589         if (!exists $hash->{$key}) {
2590                 $hash->{$key} = $value;
2591         } elsif (!ref $hash->{$key}) {
2592                 $hash->{$key} = [ $hash->{$key}, $value ];
2593         } else {
2594                 push @{$hash->{$key}}, $value;
2595         }
2596 }
2597
2598 # return hash of git project configuration
2599 # optionally limited to some section, e.g. 'gitweb'
2600 sub git_parse_project_config {
2601         my $section_regexp = shift;
2602         my %config;
2603
2604         local $/ = "\0";
2605
2606         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2607                 or return;
2608
2609         while (my $keyval = <$fh>) {
2610                 chomp $keyval;
2611                 my ($key, $value) = split(/\n/, $keyval, 2);
2612
2613                 hash_set_multi(\%config, $key, $value)
2614                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2615         }
2616         close $fh;
2617
2618         return %config;
2619 }
2620
2621 # convert config value to boolean: 'true' or 'false'
2622 # no value, number > 0, 'true' and 'yes' values are true
2623 # rest of values are treated as false (never as error)
2624 sub config_to_bool {
2625         my $val = shift;
2626
2627         return 1 if !defined $val;             # section.key
2628
2629         # strip leading and trailing whitespace
2630         $val =~ s/^\s+//;
2631         $val =~ s/\s+$//;
2632
2633         return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
2634                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
2635 }
2636
2637 # convert config value to simple decimal number
2638 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2639 # to be multiplied by 1024, 1048576, or 1073741824
2640 sub config_to_int {
2641         my $val = shift;
2642
2643         # strip leading and trailing whitespace
2644         $val =~ s/^\s+//;
2645         $val =~ s/\s+$//;
2646
2647         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2648                 $unit = lc($unit);
2649                 # unknown unit is treated as 1
2650                 return $num * ($unit eq 'g' ? 1073741824 :
2651                                $unit eq 'm' ?    1048576 :
2652                                $unit eq 'k' ?       1024 : 1);
2653         }
2654         return $val;
2655 }
2656
2657 # convert config value to array reference, if needed
2658 sub config_to_multi {
2659         my $val = shift;
2660
2661         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2662 }
2663
2664 sub git_get_project_config {
2665         my ($key, $type) = @_;
2666
2667         return unless defined $git_dir;
2668
2669         # key sanity check
2670         return unless ($key);
2671         # only subsection, if exists, is case sensitive,
2672         # and not lowercased by 'git config -z -l'
2673         if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2674                 $key = join(".", lc($hi), $mi, lc($lo));
2675         } else {
2676                 $key = lc($key);
2677         }
2678         $key =~ s/^gitweb\.//;
2679         return if ($key =~ m/\W/);
2680
2681         # type sanity check
2682         if (defined $type) {
2683                 $type =~ s/^--//;
2684                 $type = undef
2685                         unless ($type eq 'bool' || $type eq 'int');
2686         }
2687
2688         # get config
2689         if (!defined $config_file ||
2690             $config_file ne "$git_dir/config") {
2691                 %config = git_parse_project_config('gitweb');
2692                 $config_file = "$git_dir/config";
2693         }
2694
2695         # check if config variable (key) exists
2696         return unless exists $config{"gitweb.$key"};
2697
2698         # ensure given type
2699         if (!defined $type) {
2700                 return $config{"gitweb.$key"};
2701         } elsif ($type eq 'bool') {
2702                 # backward compatibility: 'git config --bool' returns true/false
2703                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2704         } elsif ($type eq 'int') {
2705                 return config_to_int($config{"gitweb.$key"});
2706         }
2707         return $config{"gitweb.$key"};
2708 }
2709
2710 # get hash of given path at given ref
2711 sub git_get_hash_by_path {
2712         my $base = shift;
2713         my $path = shift || return undef;
2714         my $type = shift;
2715
2716         $path =~ s,/+$,,;
2717
2718         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2719                 or die_error(500, "Open git-ls-tree failed");
2720         my $line = <$fd>;
2721         close $fd or return undef;
2722
2723         if (!defined $line) {
2724                 # there is no tree or hash given by $path at $base
2725                 return undef;
2726         }
2727
2728         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2729         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2730         if (defined $type && $type ne $2) {
2731                 # type doesn't match
2732                 return undef;
2733         }
2734         return $3;
2735 }
2736
2737 # get path of entry with given hash at given tree-ish (ref)
2738 # used to get 'from' filename for combined diff (merge commit) for renames
2739 sub git_get_path_by_hash {
2740         my $base = shift || return;
2741         my $hash = shift || return;
2742
2743         local $/ = "\0";
2744
2745         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2746                 or return undef;
2747         while (my $line = <$fd>) {
2748                 chomp $line;
2749
2750                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2751                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2752                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2753                         close $fd;
2754                         return $1;
2755                 }
2756         }
2757         close $fd;
2758         return undef;
2759 }
2760
2761 ## ......................................................................
2762 ## git utility functions, directly accessing git repository
2763
2764 # get the value of config variable either from file named as the variable
2765 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
2766 # configuration variable in the repository config file.
2767 sub git_get_file_or_project_config {
2768         my ($path, $name) = @_;
2769
2770         $git_dir = "$projectroot/$path";
2771         open my $fd, '<', "$git_dir/$name"
2772                 or return git_get_project_config($name);
2773         my $conf = <$fd>;
2774         close $fd;
2775         if (defined $conf) {
2776                 chomp $conf;
2777         }
2778         return $conf;
2779 }
2780
2781 sub git_get_project_description {
2782         my $path = shift;
2783         return git_get_file_or_project_config($path, 'description');
2784 }
2785
2786 sub git_get_project_category {
2787         my $path = shift;
2788         return git_get_file_or_project_config($path, 'category');
2789 }
2790
2791
2792 # supported formats:
2793 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2794 #   - if its contents is a number, use it as tag weight,
2795 #   - otherwise add a tag with weight 1
2796 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2797 #   the same value multiple times increases tag weight
2798 # * `gitweb.ctag' multi-valued repo config variable
2799 sub git_get_project_ctags {
2800         my $project = shift;
2801         my $ctags = {};
2802
2803         $git_dir = "$projectroot/$project";
2804         if (opendir my $dh, "$git_dir/ctags") {
2805                 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2806                 foreach my $tagfile (@files) {
2807                         open my $ct, '<', $tagfile
2808                                 or next;
2809                         my $val = <$ct>;
2810                         chomp $val if $val;
2811                         close $ct;
2812
2813                         (my $ctag = $tagfile) =~ s#.*/##;
2814                         if ($val =~ /^\d+$/) {
2815                                 $ctags->{$ctag} = $val;
2816                         } else {
2817                                 $ctags->{$ctag} = 1;
2818                         }
2819                 }
2820                 closedir $dh;
2821
2822         } elsif (open my $fh, '<', "$git_dir/ctags") {
2823                 while (my $line = <$fh>) {
2824                         chomp $line;
2825                         $ctags->{$line}++ if $line;
2826                 }
2827                 close $fh;
2828
2829         } else {
2830                 my $taglist = config_to_multi(git_get_project_config('ctag'));
2831                 foreach my $tag (@$taglist) {
2832                         $ctags->{$tag}++;
2833                 }
2834         }
2835
2836         return $ctags;
2837 }
2838
2839 # return hash, where keys are content tags ('ctags'),
2840 # and values are sum of weights of given tag in every project
2841 sub git_gather_all_ctags {
2842         my $projects = shift;
2843         my $ctags = {};
2844
2845         foreach my $p (@$projects) {
2846                 foreach my $ct (keys %{$p->{'ctags'}}) {
2847                         $ctags->{$ct} += $p->{'ctags'}->{$ct};
2848                 }
2849         }
2850
2851         return $ctags;
2852 }
2853
2854 sub git_populate_project_tagcloud {
2855         my $ctags = shift;
2856
2857         # First, merge different-cased tags; tags vote on casing
2858         my %ctags_lc;
2859         foreach (keys %$ctags) {
2860                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2861                 if (not $ctags_lc{lc $_}->{topcount}
2862                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2863                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2864                         $ctags_lc{lc $_}->{topname} = $_;
2865                 }
2866         }
2867
2868         my $cloud;
2869         my $matched = $input_params{'ctag'};
2870         if (eval { require HTML::TagCloud; 1; }) {
2871                 $cloud = HTML::TagCloud->new;
2872                 foreach my $ctag (sort keys %ctags_lc) {
2873                         # Pad the title with spaces so that the cloud looks
2874                         # less crammed.
2875                         my $title = esc_html($ctags_lc{$ctag}->{topname});
2876                         $title =~ s/ /&nbsp;/g;
2877                         $title =~ s/^/&nbsp;/g;
2878                         $title =~ s/$/&nbsp;/g;
2879                         if (defined $matched && $matched eq $ctag) {
2880                                 $title = qq(<span class="match">$title</span>);
2881                         }
2882                         $cloud->add($title, href(project=>undef, ctag=>$ctag),
2883                                     $ctags_lc{$ctag}->{count});
2884                 }
2885         } else {
2886                 $cloud = {};
2887                 foreach my $ctag (keys %ctags_lc) {
2888                         my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
2889                         if (defined $matched && $matched eq $ctag) {
2890                                 $title = qq(<span class="match">$title</span>);
2891                         }
2892                         $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2893                         $cloud->{$ctag}{ctag} =
2894                                 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
2895                 }
2896         }
2897         return $cloud;
2898 }
2899
2900 sub git_show_project_tagcloud {
2901         my ($cloud, $count) = @_;
2902         if (ref $cloud eq 'HTML::TagCloud') {
2903                 return $cloud->html_and_css($count);
2904         } else {
2905                 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2906                 return
2907                         '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2908                         join (', ', map {
2909                                 $cloud->{$_}->{'ctag'}
2910                         } splice(@tags, 0, $count)) .
2911                         '</div>';
2912         }
2913 }
2914
2915 sub git_get_project_url_list {
2916         my $path = shift;
2917
2918         $git_dir = "$projectroot/$path";
2919         open my $fd, '<', "$git_dir/cloneurl"
2920                 or return wantarray ?
2921                 @{ config_to_multi(git_get_project_config('url')) } :
2922                    config_to_multi(git_get_project_config('url'));
2923         my @git_project_url_list = map { chomp; $_ } <$fd>;
2924         close $fd;
2925
2926         return wantarray ? @git_project_url_list : \@git_project_url_list;
2927 }
2928
2929 sub git_get_projects_list {
2930         my $filter = shift || '';
2931         my $paranoid = shift;
2932         my @list;
2933
2934         if (-d $projects_list) {
2935                 # search in directory
2936                 my $dir = $projects_list;
2937                 # remove the trailing "/"
2938                 $dir =~ s!/+$!!;
2939                 my $pfxlen = length("$dir");
2940                 my $pfxdepth = ($dir =~ tr!/!!);
2941                 # when filtering, search only given subdirectory
2942                 if ($filter && !$paranoid) {
2943                         $dir .= "/$filter";
2944                         $dir =~ s!/+$!!;
2945                 }
2946
2947                 File::Find::find({
2948                         follow_fast => 1, # follow symbolic links
2949                         follow_skip => 2, # ignore duplicates
2950                         dangling_symlinks => 0, # ignore dangling symlinks, silently
2951                         wanted => sub {
2952                                 # global variables
2953                                 our $project_maxdepth;
2954                                 our $projectroot;
2955                                 # skip project-list toplevel, if we get it.
2956                                 return if (m!^[/.]$!);
2957                                 # only directories can be git repositories
2958                                 return unless (-d $_);
2959                                 # don't traverse too deep (Find is super slow on os x)
2960                                 # $project_maxdepth excludes depth of $projectroot
2961                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2962                                         $File::Find::prune = 1;
2963                                         return;
2964                                 }
2965
2966                                 my $path = substr($File::Find::name, $pfxlen + 1);
2967                                 # paranoidly only filter here
2968                                 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
2969                                         next;
2970                                 }
2971                                 # we check related file in $projectroot
2972                                 if (check_export_ok("$projectroot/$path")) {
2973                                         push @list, { path => $path };
2974                                         $File::Find::prune = 1;
2975                                 }
2976                         },
2977                 }, "$dir");
2978
2979         } elsif (-f $projects_list) {
2980                 # read from file(url-encoded):
2981                 # 'git%2Fgit.git Linus+Torvalds'
2982                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2983                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2984                 open my $fd, '<', $projects_list or return;
2985         PROJECT:
2986                 while (my $line = <$fd>) {
2987                         chomp $line;
2988                         my ($path, $owner) = split ' ', $line;
2989                         $path = unescape($path);
2990                         $owner = unescape($owner);
2991                         if (!defined $path) {
2992                                 next;
2993                         }
2994                         # if $filter is rpovided, check if $path begins with $filter
2995                         if ($filter && $path !~ m!^\Q$filter\E/!) {
2996                                 next;
2997                         }
2998                         if (check_export_ok("$projectroot/$path")) {
2999                                 my $pr = {
3000                                         path => $path
3001                                 };
3002                                 if ($owner) {
3003                                         $pr->{'owner'} = to_utf8($owner);
3004                                 }
3005                                 push @list, $pr;
3006                         }
3007                 }
3008                 close $fd;
3009         }
3010         return @list;
3011 }
3012
3013 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3014 # as side effects it sets 'forks' field to list of forks for forked projects
3015 sub filter_forks_from_projects_list {
3016         my $projects = shift;
3017
3018         my %trie; # prefix tree of directories (path components)
3019         # generate trie out of those directories that might contain forks
3020         foreach my $pr (@$projects) {
3021                 my $path = $pr->{'path'};
3022                 $path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
3023                 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3024                 next unless ($path);      # skip '.git' repository: tests, git-instaweb
3025                 next unless (-d "$projectroot/$path"); # containing directory exists
3026                 $pr->{'forks'} = [];      # there can be 0 or more forks of project
3027
3028                 # add to trie
3029                 my @dirs = split('/', $path);
3030                 # walk the trie, until either runs out of components or out of trie
3031                 my $ref = \%trie;
3032                 while (scalar @dirs &&
3033                        exists($ref->{$dirs[0]})) {
3034                         $ref = $ref->{shift @dirs};
3035                 }
3036                 # create rest of trie structure from rest of components
3037                 foreach my $dir (@dirs) {
3038                         $ref = $ref->{$dir} = {};
3039                 }
3040                 # create end marker, store $pr as a data
3041                 $ref->{''} = $pr if (!exists $ref->{''});
3042         }
3043
3044         # filter out forks, by finding shortest prefix match for paths
3045         my @filtered;
3046  PROJECT:
3047         foreach my $pr (@$projects) {
3048                 # trie lookup
3049                 my $ref = \%trie;
3050         DIR:
3051                 foreach my $dir (split('/', $pr->{'path'})) {
3052                         if (exists $ref->{''}) {
3053                                 # found [shortest] prefix, is a fork - skip it
3054                                 push @{$ref->{''}{'forks'}}, $pr;
3055                                 next PROJECT;
3056                         }
3057                         if (!exists $ref->{$dir}) {
3058                                 # not in trie, cannot have prefix, not a fork
3059                                 push @filtered, $pr;
3060                                 next PROJECT;
3061                         }
3062                         # If the dir is there, we just walk one step down the trie.
3063                         $ref = $ref->{$dir};
3064                 }
3065                 # we ran out of trie
3066                 # (shouldn't happen: it's either no match, or end marker)
3067                 push @filtered, $pr;
3068         }
3069
3070         return @filtered;
3071 }
3072
3073 # note: fill_project_list_info must be run first,
3074 # for 'descr_long' and 'ctags' to be filled
3075 sub search_projects_list {
3076         my ($projlist, %opts) = @_;
3077         my $tagfilter  = $opts{'tagfilter'};
3078         my $search_re = $opts{'search_regexp'};
3079
3080         return @$projlist
3081                 unless ($tagfilter || $search_re);
3082
3083         # searching projects require filling to be run before it;
3084         fill_project_list_info($projlist,
3085                                $tagfilter  ? 'ctags' : (),
3086                                $search_re ? ('path', 'descr') : ());
3087         my @projects;
3088  PROJECT:
3089         foreach my $pr (@$projlist) {
3090
3091                 if ($tagfilter) {
3092                         next unless ref($pr->{'ctags'}) eq 'HASH';
3093                         next unless
3094                                 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3095                 }
3096
3097                 if ($search_re) {
3098                         next unless
3099                                 $pr->{'path'} =~ /$search_re/ ||
3100                                 $pr->{'descr_long'} =~ /$search_re/;
3101                 }
3102
3103                 push @projects, $pr;
3104         }
3105
3106         return @projects;
3107 }
3108
3109 our $gitweb_project_owner = undef;
3110 sub git_get_project_list_from_file {
3111
3112         return if (defined $gitweb_project_owner);
3113
3114         $gitweb_project_owner = {};
3115         # read from file (url-encoded):
3116         # 'git%2Fgit.git Linus+Torvalds'
3117         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3118         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3119         if (-f $projects_list) {
3120                 open(my $fd, '<', $projects_list);
3121                 while (my $line = <$fd>) {
3122                         chomp $line;
3123                         my ($pr, $ow) = split ' ', $line;
3124                         $pr = unescape($pr);
3125                         $ow = unescape($ow);
3126                         $gitweb_project_owner->{$pr} = to_utf8($ow);
3127                 }
3128                 close $fd;
3129         }
3130 }
3131
3132 sub git_get_project_owner {
3133         my $project = shift;
3134         my $owner;
3135
3136         return undef unless $project;
3137         $git_dir = "$projectroot/$project";
3138
3139         if (!defined $gitweb_project_owner) {
3140                 git_get_project_list_from_file();
3141         }
3142
3143         if (exists $gitweb_project_owner->{$project}) {
3144                 $owner = $gitweb_project_owner->{$project};
3145         }
3146         if (!defined $owner){
3147                 $owner = git_get_project_config('owner');
3148         }
3149         if (!defined $owner) {
3150                 $owner = get_file_owner("$git_dir");
3151         }
3152
3153         return $owner;
3154 }
3155
3156 sub git_get_last_activity {
3157         my ($path) = @_;
3158         my $fd;
3159
3160         $git_dir = "$projectroot/$path";
3161         open($fd, "-|", git_cmd(), 'for-each-ref',
3162              '--format=%(committer)',
3163              '--sort=-committerdate',
3164              '--count=1',
3165              'refs/heads') or return;
3166         my $most_recent = <$fd>;
3167         close $fd or return;
3168         if (defined $most_recent &&
3169             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
3170                 my $timestamp = $1;
3171                 my $age = time - $timestamp;
3172                 return ($age, age_string($age));
3173         }
3174         return (undef, undef);
3175 }
3176
3177 # Implementation note: when a single remote is wanted, we cannot use 'git
3178 # remote show -n' because that command always work (assuming it's a remote URL
3179 # if it's not defined), and we cannot use 'git remote show' because that would
3180 # try to make a network roundtrip. So the only way to find if that particular
3181 # remote is defined is to walk the list provided by 'git remote -v' and stop if
3182 # and when we find what we want.
3183 sub git_get_remotes_list {
3184         my $wanted = shift;
3185         my %remotes = ();
3186
3187         open my $fd, '-|' , git_cmd(), 'remote', '-v';
3188         return unless $fd;
3189         while (my $remote = <$fd>) {
3190                 chomp $remote;
3191                 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3192                 next if $wanted and not $remote eq $wanted;
3193                 my ($url, $key) = ($1, $2);
3194
3195                 $remotes{$remote} ||= { 'heads' => () };
3196                 $remotes{$remote}{$key} = $url;
3197         }
3198         close $fd or return;
3199         return wantarray ? %remotes : \%remotes;
3200 }
3201
3202 # Takes a hash of remotes as first parameter and fills it by adding the
3203 # available remote heads for each of the indicated remotes.
3204 sub fill_remote_heads {
3205         my $remotes = shift;
3206         my @heads = map { "remotes/$_" } keys %$remotes;
3207         my @remoteheads = git_get_heads_list(undef, @heads);
3208         foreach my $remote (keys %$remotes) {
3209                 $remotes->{$remote}{'heads'} = [ grep {
3210                         $_->{'name'} =~ s!^$remote/!!
3211                         } @remoteheads ];
3212         }
3213 }
3214
3215 sub git_get_references {
3216         my $type = shift || "";
3217         my %refs;
3218         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3219         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3220         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3221                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
3222                 or return;
3223
3224         while (my $line = <$fd>) {
3225                 chomp $line;
3226                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3227                         if (defined $refs{$1}) {
3228                                 push @{$refs{$1}}, $2;
3229                         } else {
3230                                 $refs{$1} = [ $2 ];
3231                         }
3232                 }
3233         }
3234         close $fd or return;
3235         return \%refs;
3236 }
3237
3238 sub git_get_rev_name_tags {
3239         my $hash = shift || return undef;
3240
3241         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3242                 or return;
3243         my $name_rev = <$fd>;
3244         close $fd;
3245
3246         if ($name_rev =~ m|^$hash tags/(.*)$|) {
3247                 return $1;
3248         } else {
3249                 # catches also '$hash undefined' output
3250                 return undef;
3251         }
3252 }
3253
3254 ## ----------------------------------------------------------------------
3255 ## parse to hash functions
3256
3257 sub parse_date {
3258         my $epoch = shift;
3259         my $tz = shift || "-0000";
3260
3261         my %date;
3262         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3263         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3264         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3265         $date{'hour'} = $hour;
3266         $date{'minute'} = $min;
3267         $date{'mday'} = $mday;
3268         $date{'day'} = $days[$wday];
3269         $date{'month'} = $months[$mon];
3270         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3271                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3272         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3273                              $mday, $months[$mon], $hour ,$min;
3274         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3275                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3276
3277         my ($tz_sign, $tz_hour, $tz_min) =
3278                 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3279         $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3280         my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3281         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3282         $date{'hour_local'} = $hour;
3283         $date{'minute_local'} = $min;
3284         $date{'tz_local'} = $tz;
3285         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3286                                   1900+$year, $mon+1, $mday,
3287                                   $hour, $min, $sec, $tz);
3288         return %date;
3289 }
3290
3291 sub parse_tag {
3292         my $tag_id = shift;
3293         my %tag;
3294         my @comment;
3295
3296         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3297         $tag{'id'} = $tag_id;
3298         while (my $line = <$fd>) {
3299                 chomp $line;
3300                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3301                         $tag{'object'} = $1;
3302                 } elsif ($line =~ m/^type (.+)$/) {
3303                         $tag{'type'} = $1;
3304                 } elsif ($line =~ m/^tag (.+)$/) {
3305                         $tag{'name'} = $1;
3306                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3307                         $tag{'author'} = $1;
3308                         $tag{'author_epoch'} = $2;
3309                         $tag{'author_tz'} = $3;
3310                         if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3311                                 $tag{'author_name'}  = $1;
3312                                 $tag{'author_email'} = $2;
3313                         } else {
3314                                 $tag{'author_name'} = $tag{'author'};
3315                         }
3316                 } elsif ($line =~ m/--BEGIN/) {
3317                         push @comment, $line;
3318                         last;
3319                 } elsif ($line eq "") {
3320                         last;
3321                 }
3322         }
3323         push @comment, <$fd>;
3324         $tag{'comment'} = \@comment;
3325         close $fd or return;
3326         if (!defined $tag{'name'}) {
3327                 return
3328         };
3329         return %tag
3330 }
3331
3332 sub parse_commit_text {
3333         my ($commit_text, $withparents) = @_;
3334         my @commit_lines = split '\n', $commit_text;
3335         my %co;
3336
3337         pop @commit_lines; # Remove '\0'
3338
3339         if (! @commit_lines) {
3340                 return;
3341         }
3342
3343         my $header = shift @commit_lines;
3344         if ($header !~ m/^[0-9a-fA-F]{40}/) {
3345                 return;
3346         }
3347         ($co{'id'}, my @parents) = split ' ', $header;
3348         while (my $line = shift @commit_lines) {
3349                 last if $line eq "\n";
3350                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3351                         $co{'tree'} = $1;
3352                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3353                         push @parents, $1;
3354                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3355                         $co{'author'} = to_utf8($1);
3356                         $co{'author_epoch'} = $2;
3357                         $co{'author_tz'} = $3;
3358                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3359                                 $co{'author_name'}  = $1;
3360                                 $co{'author_email'} = $2;
3361                         } else {
3362                                 $co{'author_name'} = $co{'author'};
3363                         }
3364                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3365                         $co{'committer'} = to_utf8($1);
3366                         $co{'committer_epoch'} = $2;
3367                         $co{'committer_tz'} = $3;
3368                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3369                                 $co{'committer_name'}  = $1;
3370                                 $co{'committer_email'} = $2;
3371                         } else {
3372                                 $co{'committer_name'} = $co{'committer'};
3373                         }
3374                 }
3375         }
3376         if (!defined $co{'tree'}) {
3377                 return;
3378         };
3379         $co{'parents'} = \@parents;
3380         $co{'parent'} = $parents[0];
3381
3382         foreach my $title (@commit_lines) {
3383                 $title =~ s/^    //;
3384                 if ($title ne "") {
3385                         $co{'title'} = chop_str($title, 80, 5);
3386                         # remove leading stuff of merges to make the interesting part visible
3387                         if (length($title) > 50) {
3388                                 $title =~ s/^Automatic //;
3389                                 $title =~ s/^merge (of|with) /Merge ... /i;
3390                                 if (length($title) > 50) {
3391                                         $title =~ s/(http|rsync):\/\///;
3392                                 }
3393                                 if (length($title) > 50) {
3394                                         $title =~ s/(master|www|rsync)\.//;
3395                                 }
3396                                 if (length($title) > 50) {
3397                                         $title =~ s/kernel.org:?//;
3398                                 }
3399                                 if (length($title) > 50) {
3400                                         $title =~ s/\/pub\/scm//;
3401                                 }
3402                         }
3403                         $co{'title_short'} = chop_str($title, 50, 5);
3404                         last;
3405                 }
3406         }
3407         if (! defined $co{'title'} || $co{'title'} eq "") {
3408                 $co{'title'} = $co{'title_short'} = '(no commit message)';
3409         }
3410         # remove added spaces
3411         foreach my $line (@commit_lines) {
3412                 $line =~ s/^    //;
3413         }
3414         $co{'comment'} = \@commit_lines;
3415
3416         my $age = time - $co{'committer_epoch'};
3417         $co{'age'} = $age;
3418         $co{'age_string'} = age_string($age);
3419         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3420         if ($age > 60*60*24*7*2) {
3421                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3422                 $co{'age_string_age'} = $co{'age_string'};
3423         } else {
3424                 $co{'age_string_date'} = $co{'age_string'};
3425                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3426         }
3427         return %co;
3428 }
3429
3430 sub parse_commit {
3431         my ($commit_id) = @_;
3432         my %co;
3433
3434         local $/ = "\0";
3435
3436         open my $fd, "-|", git_cmd(), "rev-list",
3437                 "--parents",
3438                 "--header",
3439                 "--max-count=1",
3440                 $commit_id,
3441                 "--",
3442                 or die_error(500, "Open git-rev-list failed");
3443         %co = parse_commit_text(<$fd>, 1);
3444         close $fd;
3445
3446         return %co;
3447 }
3448
3449 sub parse_commits {
3450         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3451         my @cos;
3452
3453         $maxcount ||= 1;
3454         $skip ||= 0;
3455
3456         local $/ = "\0";
3457
3458         open my $fd, "-|", git_cmd(), "rev-list",
3459                 "--header",
3460                 @args,
3461                 ("--max-count=" . $maxcount),
3462                 ("--skip=" . $skip),
3463                 @extra_options,
3464                 $commit_id,
3465                 "--",
3466                 ($filename ? ($filename) : ())
3467                 or die_error(500, "Open git-rev-list failed");
3468         while (my $line = <$fd>) {
3469                 my %co = parse_commit_text($line);
3470                 push @cos, \%co;
3471         }
3472         close $fd;
3473
3474         return wantarray ? @cos : \@cos;
3475 }
3476
3477 # parse line of git-diff-tree "raw" output
3478 sub parse_difftree_raw_line {
3479         my $line = shift;
3480         my %res;
3481
3482         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
3483         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
3484         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3485                 $res{'from_mode'} = $1;
3486                 $res{'to_mode'} = $2;
3487                 $res{'from_id'} = $3;
3488                 $res{'to_id'} = $4;
3489                 $res{'status'} = $5;
3490                 $res{'similarity'} = $6;
3491                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3492                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3493                 } else {
3494                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3495                 }
3496         }
3497         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3498         # combined diff (for merge commit)
3499         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3500                 $res{'nparents'}  = length($1);
3501                 $res{'from_mode'} = [ split(' ', $2) ];
3502                 $res{'to_mode'} = pop @{$res{'from_mode'}};
3503                 $res{'from_id'} = [ split(' ', $3) ];
3504                 $res{'to_id'} = pop @{$res{'from_id'}};
3505                 $res{'status'} = [ split('', $4) ];
3506                 $res{'to_file'} = unquote($5);
3507         }
3508         # 'c512b523472485aef4fff9e57b229d9d243c967f'
3509         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3510                 $res{'commit'} = $1;
3511         }
3512
3513         return wantarray ? %res : \%res;
3514 }
3515
3516 # wrapper: return parsed line of git-diff-tree "raw" output
3517 # (the argument might be raw line, or parsed info)
3518 sub parsed_difftree_line {
3519         my $line_or_ref = shift;
3520
3521         if (ref($line_or_ref) eq "HASH") {
3522                 # pre-parsed (or generated by hand)
3523                 return $line_or_ref;
3524         } else {
3525                 return parse_difftree_raw_line($line_or_ref);
3526         }
3527 }
3528
3529 # parse line of git-ls-tree output
3530 sub parse_ls_tree_line {
3531         my $line = shift;
3532         my %opts = @_;
3533         my %res;
3534
3535         if ($opts{'-l'}) {
3536                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717  panic.c'
3537                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3538
3539                 $res{'mode'} = $1;
3540                 $res{'type'} = $2;
3541                 $res{'hash'} = $3;
3542                 $res{'size'} = $4;
3543                 if ($opts{'-z'}) {
3544                         $res{'name'} = $5;
3545                 } else {
3546                         $res{'name'} = unquote($5);
3547                 }
3548         } else {
3549                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
3550                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3551
3552                 $res{'mode'} = $1;
3553                 $res{'type'} = $2;
3554                 $res{'hash'} = $3;
3555                 if ($opts{'-z'}) {
3556                         $res{'name'} = $4;
3557                 } else {
3558                         $res{'name'} = unquote($4);
3559                 }
3560         }
3561
3562         return wantarray ? %res : \%res;
3563 }
3564
3565 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3566 sub parse_from_to_diffinfo {
3567         my ($diffinfo, $from, $to, @parents) = @_;
3568
3569         if ($diffinfo->{'nparents'}) {
3570                 # combined diff
3571                 $from->{'file'} = [];
3572                 $from->{'href'} = [];
3573                 fill_from_file_info($diffinfo, @parents)
3574                         unless exists $diffinfo->{'from_file'};
3575                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3576                         $from->{'file'}[$i] =
3577                                 defined $diffinfo->{'from_file'}[$i] ?
3578                                         $diffinfo->{'from_file'}[$i] :
3579                                         $diffinfo->{'to_file'};
3580                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3581                                 $from->{'href'}[$i] = href(action=>"blob",
3582                                                            hash_base=>$parents[$i],
3583                                                            hash=>$diffinfo->{'from_id'}[$i],
3584                                                            file_name=>$from->{'file'}[$i]);
3585                         } else {
3586                                 $from->{'href'}[$i] = undef;
3587                         }
3588                 }
3589         } else {
3590                 # ordinary (not combined) diff
3591                 $from->{'file'} = $diffinfo->{'from_file'};
3592                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3593                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3594                                                hash=>$diffinfo->{'from_id'},
3595                                                file_name=>$from->{'file'});
3596                 } else {
3597                         delete $from->{'href'};
3598                 }
3599         }
3600
3601         $to->{'file'} = $diffinfo->{'to_file'};
3602         if (!is_deleted($diffinfo)) { # file exists in result
3603                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3604                                      hash=>$diffinfo->{'to_id'},
3605                                      file_name=>$to->{'file'});
3606         } else {
3607                 delete $to->{'href'};
3608         }
3609 }
3610
3611 ## ......................................................................
3612 ## parse to array of hashes functions
3613
3614 sub git_get_heads_list {
3615         my ($limit, @classes) = @_;
3616         @classes = ('heads') unless @classes;
3617         my @patterns = map { "refs/$_" } @classes;
3618         my @headslist;
3619
3620         open my $fd, '-|', git_cmd(), 'for-each-ref',
3621                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3622                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3623                 @patterns
3624                 or return;
3625         while (my $line = <$fd>) {
3626                 my %ref_item;
3627
3628                 chomp $line;
3629                 my ($refinfo, $committerinfo) = split(/\0/, $line);
3630                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3631                 my ($committer, $epoch, $tz) =
3632                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3633                 $ref_item{'fullname'}  = $name;
3634                 $name =~ s!^refs/(?:head|remote)s/!!;
3635
3636                 $ref_item{'name'}  = $name;
3637                 $ref_item{'id'}    = $hash;
3638                 $ref_item{'title'} = $title || '(no commit message)';
3639                 $ref_item{'epoch'} = $epoch;
3640                 if ($epoch) {
3641                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3642                 } else {
3643                         $ref_item{'age'} = "unknown";
3644                 }
3645
3646                 push @headslist, \%ref_item;
3647         }
3648         close $fd;
3649
3650         return wantarray ? @headslist : \@headslist;
3651 }
3652
3653 sub git_get_tags_list {
3654         my $limit = shift;
3655         my @tagslist;
3656
3657         open my $fd, '-|', git_cmd(), 'for-each-ref',
3658                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3659                 '--format=%(objectname) %(objecttype) %(refname) '.
3660                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3661                 'refs/tags'
3662                 or return;
3663         while (my $line = <$fd>) {
3664                 my %ref_item;
3665
3666                 chomp $line;
3667                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3668                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3669                 my ($creator, $epoch, $tz) =
3670                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3671                 $ref_item{'fullname'} = $name;
3672                 $name =~ s!^refs/tags/!!;
3673
3674                 $ref_item{'type'} = $type;
3675                 $ref_item{'id'} = $id;
3676                 $ref_item{'name'} = $name;
3677                 if ($type eq "tag") {
3678                         $ref_item{'subject'} = $title;
3679                         $ref_item{'reftype'} = $reftype;
3680                         $ref_item{'refid'}   = $refid;
3681                 } else {
3682                         $ref_item{'reftype'} = $type;
3683                         $ref_item{'refid'}   = $id;
3684                 }
3685
3686                 if ($type eq "tag" || $type eq "commit") {
3687                         $ref_item{'epoch'} = $epoch;
3688                         if ($epoch) {
3689                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3690                         } else {
3691                                 $ref_item{'age'} = "unknown";
3692                         }
3693                 }
3694
3695                 push @tagslist, \%ref_item;
3696         }
3697         close $fd;
3698
3699         return wantarray ? @tagslist : \@tagslist;
3700 }
3701
3702 ## ----------------------------------------------------------------------
3703 ## filesystem-related functions
3704
3705 sub get_file_owner {
3706         my $path = shift;
3707
3708         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3709         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3710         if (!defined $gcos) {
3711                 return undef;
3712         }
3713         my $owner = $gcos;
3714         $owner =~ s/[,;].*$//;
3715         return to_utf8($owner);
3716 }
3717
3718 # assume that file exists
3719 sub insert_file {
3720         my $filename = shift;
3721
3722         open my $fd, '<', $filename;
3723         print map { to_utf8($_) } <$fd>;
3724         close $fd;
3725 }
3726
3727 ## ......................................................................
3728 ## mimetype related functions
3729
3730 sub mimetype_guess_file {
3731         my $filename = shift;
3732         my $mimemap = shift;
3733         -r $mimemap or return undef;
3734
3735         my %mimemap;
3736         open(my $mh, '<', $mimemap) or return undef;
3737         while (<$mh>) {
3738                 next if m/^#/; # skip comments
3739                 my ($mimetype, @exts) = split(/\s+/);
3740                 foreach my $ext (@exts) {
3741                         $mimemap{$ext} = $mimetype;
3742                 }
3743         }
3744         close($mh);
3745
3746         $filename =~ /\.([^.]*)$/;
3747         return $mimemap{$1};
3748 }
3749
3750 sub mimetype_guess {
3751         my $filename = shift;
3752         my $mime;
3753         $filename =~ /\./ or return undef;
3754
3755         if ($mimetypes_file) {
3756                 my $file = $mimetypes_file;
3757                 if ($file !~ m!^/!) { # if it is relative path
3758                         # it is relative to project
3759                         $file = "$projectroot/$project/$file";
3760                 }
3761                 $mime = mimetype_guess_file($filename, $file);
3762         }
3763         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3764         return $mime;
3765 }
3766
3767 sub blob_mimetype {
3768         my $fd = shift;
3769         my $filename = shift;
3770
3771         if ($filename) {
3772                 my $mime = mimetype_guess($filename);
3773                 $mime and return $mime;
3774         }
3775
3776         # just in case
3777         return $default_blob_plain_mimetype unless $fd;
3778
3779         if (-T $fd) {
3780                 return 'text/plain';
3781         } elsif (! $filename) {
3782                 return 'application/octet-stream';
3783         } elsif ($filename =~ m/\.png$/i) {
3784                 return 'image/png';
3785         } elsif ($filename =~ m/\.gif$/i) {
3786                 return 'image/gif';
3787         } elsif ($filename =~ m/\.jpe?g$/i) {
3788                 return 'image/jpeg';
3789         } else {
3790                 return 'application/octet-stream';
3791         }
3792 }
3793
3794 sub blob_contenttype {
3795         my ($fd, $file_name, $type) = @_;
3796
3797         $type ||= blob_mimetype($fd, $file_name);
3798         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3799                 $type .= "; charset=$default_text_plain_charset";
3800         }
3801
3802         return $type;
3803 }
3804
3805 # guess file syntax for syntax highlighting; return undef if no highlighting
3806 # the name of syntax can (in the future) depend on syntax highlighter used
3807 sub guess_file_syntax {
3808         my ($highlight, $mimetype, $file_name) = @_;
3809         return undef unless ($highlight && defined $file_name);
3810         my $basename = basename($file_name, '.in');
3811         return $highlight_basename{$basename}
3812                 if exists $highlight_basename{$basename};
3813
3814         $basename =~ /\.([^.]*)$/;
3815         my $ext = $1 or return undef;
3816         return $highlight_ext{$ext}
3817                 if exists $highlight_ext{$ext};
3818
3819         return undef;
3820 }
3821
3822 # run highlighter and return FD of its output,
3823 # or return original FD if no highlighting
3824 sub run_highlighter {
3825         my ($fd, $highlight, $syntax) = @_;
3826         return $fd unless ($highlight && defined $syntax);
3827
3828         close $fd;
3829         open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3830                   quote_command($highlight_bin).
3831                   " --replace-tabs=8 --fragment --syntax $syntax |"
3832                 or die_error(500, "Couldn't open file or run syntax highlighter");
3833         return $fd;
3834 }
3835
3836 ## ======================================================================
3837 ## functions printing HTML: header, footer, error page
3838
3839 sub get_page_title {
3840         my $title = to_utf8($site_name);
3841
3842         unless (defined $project) {
3843                 if (defined $project_filter) {
3844                         $title .= " - projects in '" . esc_path($project_filter) . "'";
3845                 }
3846                 return $title;
3847         }
3848         $title .= " - " . to_utf8($project);
3849
3850         return $title unless (defined $action);
3851         $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3852
3853         return $title unless (defined $file_name);
3854         $title .= " - " . esc_path($file_name);
3855         if ($action eq "tree" && $file_name !~ m|/$|) {
3856                 $title .= "/";
3857         }
3858
3859         return $title;
3860 }
3861
3862 sub get_content_type_html {
3863         # require explicit support from the UA if we are to send the page as
3864         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3865         # we have to do this because MSIE sometimes globs '*/*', pretending to
3866         # support xhtml+xml but choking when it gets what it asked for.
3867         if (defined $cgi->http('HTTP_ACCEPT') &&
3868             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3869             $cgi->Accept('application/xhtml+xml') != 0) {
3870                 return 'application/xhtml+xml';
3871         } else {
3872                 return 'text/html';
3873         }
3874 }
3875
3876 sub print_feed_meta {
3877         if (defined $project) {
3878                 my %href_params = get_feed_info();
3879                 if (!exists $href_params{'-title'}) {
3880                         $href_params{'-title'} = 'log';
3881                 }
3882
3883                 foreach my $format (qw(RSS Atom)) {
3884                         my $type = lc($format);
3885                         my %link_attr = (
3886                                 '-rel' => 'alternate',
3887                                 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3888                                 '-type' => "application/$type+xml"
3889                         );
3890
3891                         $href_params{'action'} = $type;
3892                         $link_attr{'-href'} = href(%href_params);
3893                         print "<link ".
3894                               "rel=\"$link_attr{'-rel'}\" ".
3895                               "title=\"$link_attr{'-title'}\" ".
3896                               "href=\"$link_attr{'-href'}\" ".
3897                               "type=\"$link_attr{'-type'}\" ".
3898                               "/>\n";
3899
3900                         $href_params{'extra_options'} = '--no-merges';
3901                         $link_attr{'-href'} = href(%href_params);
3902                         $link_attr{'-title'} .= ' (no merges)';
3903                         print "<link ".
3904                               "rel=\"$link_attr{'-rel'}\" ".
3905                               "title=\"$link_attr{'-title'}\" ".
3906                               "href=\"$link_attr{'-href'}\" ".
3907                               "type=\"$link_attr{'-type'}\" ".
3908                               "/>\n";
3909                 }
3910
3911         } else {
3912                 printf('<link rel="alternate" title="%s projects list" '.
3913                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
3914                        esc_attr($site_name), href(project=>undef, action=>"project_index"));
3915                 printf('<link rel="alternate" title="%s projects feeds" '.
3916                        'href="%s" type="text/x-opml" />'."\n",
3917                        esc_attr($site_name), href(project=>undef, action=>"opml"));
3918         }
3919 }
3920
3921 sub print_header_links {
3922         my $status = shift;
3923
3924         # print out each stylesheet that exist, providing backwards capability
3925         # for those people who defined $stylesheet in a config file
3926         if (defined $stylesheet) {
3927                 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3928         } else {
3929                 foreach my $stylesheet (@stylesheets) {
3930                         next unless $stylesheet;
3931                         print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3932                 }
3933         }
3934         print_feed_meta()
3935                 if ($status eq '200 OK');
3936         if (defined $favicon) {
3937                 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3938         }
3939 }
3940
3941 sub print_nav_breadcrumbs_path {
3942         my $dirprefix = undef;
3943         while (my $part = shift) {
3944                 $dirprefix .= "/" if defined $dirprefix;
3945                 $dirprefix .= $part;
3946                 print $cgi->a({-href => href(project => undef,
3947                                              project_filter => $dirprefix,
3948                                              action => "project_list")},
3949                               esc_html($part)) . " / ";
3950         }
3951 }
3952
3953 sub print_nav_breadcrumbs {
3954         my %opts = @_;
3955
3956         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3957         if (defined $project) {
3958                 my @dirname = split '/', $project;
3959                 my $projectbasename = pop @dirname;
3960                 print_nav_breadcrumbs_path(@dirname);
3961                 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
3962                 if (defined $action) {
3963                         my $action_print = $action ;
3964                         if (defined $opts{-action_extra}) {
3965                                 $action_print = $cgi->a({-href => href(action=>$action)},
3966                                         $action);
3967                         }
3968                         print " / $action_print";
3969                 }
3970                 if (defined $opts{-action_extra}) {
3971                         print " / $opts{-action_extra}";
3972                 }
3973                 print "\n";
3974         } elsif (defined $project_filter) {
3975                 print_nav_breadcrumbs_path(split '/', $project_filter);
3976         }
3977 }
3978
3979 sub print_search_form {
3980         if (!defined $searchtext) {
3981                 $searchtext = "";
3982         }
3983         my $search_hash;
3984         if (defined $hash_base) {
3985                 $search_hash = $hash_base;
3986         } elsif (defined $hash) {
3987                 $search_hash = $hash;
3988         } else {
3989                 $search_hash = "HEAD";
3990         }
3991         my $action = $my_uri;
3992         my $use_pathinfo = gitweb_check_feature('pathinfo');
3993         if ($use_pathinfo) {
3994                 $action .= "/".esc_url($project);
3995         }
3996         print $cgi->startform(-method => "get", -action => $action) .
3997               "<div class=\"search\">\n" .
3998               (!$use_pathinfo &&
3999               $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
4000               $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
4001               $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
4002               $cgi->popup_menu(-name => 'st', -default => 'commit',
4003                                -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
4004               $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
4005               " search:\n",
4006               $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
4007               "<span title=\"Extended regular expression\">" .
4008               $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
4009                              -checked => $search_use_regexp) .
4010               "</span>" .
4011               "</div>" .
4012               $cgi->end_form() . "\n";
4013 }
4014
4015 sub git_header_html {
4016         my $status = shift || "200 OK";
4017         my $expires = shift;
4018         my %opts = @_;
4019
4020         my $title = get_page_title();
4021         my $content_type = get_content_type_html();
4022         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
4023                            -status=> $status, -expires => $expires)
4024                 unless ($opts{'-no_http_header'});
4025         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
4026         print <<EOF;
4027 <?xml version="1.0" encoding="utf-8"?>
4028 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4029 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
4030 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
4031 <!-- git core binaries version $git_version -->
4032 <head>
4033 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
4034 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
4035 <meta name="robots" content="index, nofollow"/>
4036 <title>$title</title>
4037 EOF
4038         # the stylesheet, favicon etc urls won't work correctly with path_info
4039         # unless we set the appropriate base URL
4040         if ($ENV{'PATH_INFO'}) {
4041                 print "<base href=\"".esc_url($base_url)."\" />\n";
4042         }
4043         print_header_links($status);
4044
4045         if (defined $site_html_head_string) {
4046                 print to_utf8($site_html_head_string);
4047         }
4048
4049         print "</head>\n" .
4050               "<body>\n";
4051
4052         if (defined $site_header && -f $site_header) {
4053                 insert_file($site_header);
4054         }
4055
4056         print "<div class=\"page_header\">\n";
4057         if (defined $logo) {
4058                 print $cgi->a({-href => esc_url($logo_url),
4059                                -title => $logo_label},
4060                               $cgi->img({-src => esc_url($logo),
4061                                          -width => 72, -height => 27,
4062                                          -alt => "git",
4063                                          -class => "logo"}));
4064         }
4065         print_nav_breadcrumbs(%opts);
4066         print "</div>\n";
4067
4068         my $have_search = gitweb_check_feature('search');
4069         if (defined $project && $have_search) {
4070                 print_search_form();
4071         }
4072 }
4073
4074 sub git_footer_html {
4075         my $feed_class = 'rss_logo';
4076
4077         print "<div class=\"page_footer\">\n";
4078         if (defined $project) {
4079                 my $descr = git_get_project_description($project);
4080                 if (defined $descr) {
4081                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
4082                 }
4083
4084                 my %href_params = get_feed_info();
4085                 if (!%href_params) {
4086                         $feed_class .= ' generic';
4087                 }
4088                 $href_params{'-title'} ||= 'log';
4089
4090                 foreach my $format (qw(RSS Atom)) {
4091                         $href_params{'action'} = lc($format);
4092                         print $cgi->a({-href => href(%href_params),
4093                                       -title => "$href_params{'-title'} $format feed",
4094                                       -class => $feed_class}, $format)."\n";
4095                 }
4096
4097         } else {
4098                 print $cgi->a({-href => href(project=>undef, action=>"opml",
4099                                              project_filter => $project_filter),
4100                               -class => $feed_class}, "OPML") . " ";
4101                 print $cgi->a({-href => href(project=>undef, action=>"project_index",
4102                                              project_filter => $project_filter),
4103                               -class => $feed_class}, "TXT") . "\n";
4104         }
4105         print "</div>\n"; # class="page_footer"
4106
4107         if (defined $t0 && gitweb_check_feature('timed')) {
4108                 print "<div id=\"generating_info\">\n";
4109                 print 'This page took '.
4110                       '<span id="generating_time" class="time_span">'.
4111                       tv_interval($t0, [ gettimeofday() ]).
4112                       ' seconds </span>'.
4113                       ' and '.
4114                       '<span id="generating_cmd">'.
4115                       $number_of_git_cmds.
4116                       '</span> git commands '.
4117                       " to generate.\n";
4118                 print "</div>\n"; # class="page_footer"
4119         }
4120
4121         if (defined $site_footer && -f $site_footer) {
4122                 insert_file($site_footer);
4123         }
4124
4125         print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
4126         if (defined $action &&
4127             $action eq 'blame_incremental') {
4128                 print qq!<script type="text/javascript">\n!.
4129                       qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
4130                       qq!           "!. href() .qq!");\n!.
4131                       qq!</script>\n!;
4132         } else {
4133                 my ($jstimezone, $tz_cookie, $datetime_class) =
4134                         gitweb_get_feature('javascript-timezone');
4135
4136                 print qq!<script type="text/javascript">\n!.
4137                       qq!window.onload = function () {\n!;
4138                 if (gitweb_check_feature('javascript-actions')) {
4139                         print qq!       fixLinks();\n!;
4140                 }
4141                 if ($jstimezone && $tz_cookie && $datetime_class) {
4142                         print qq!       var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
4143                               qq!       onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
4144                 }
4145                 print qq!};\n!.
4146                       qq!</script>\n!;
4147         }
4148
4149         print "</body>\n" .
4150               "</html>";
4151 }
4152
4153 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
4154 # Example: die_error(404, 'Hash not found')
4155 # By convention, use the following status codes (as defined in RFC 2616):
4156 # 400: Invalid or missing CGI parameters, or
4157 #      requested object exists but has wrong type.
4158 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4159 #      this server or project.
4160 # 404: Requested object/revision/project doesn't exist.
4161 # 500: The server isn't configured properly, or
4162 #      an internal error occurred (e.g. failed assertions caused by bugs), or
4163 #      an unknown error occurred (e.g. the git binary died unexpectedly).
4164 # 503: The server is currently unavailable (because it is overloaded,
4165 #      or down for maintenance).  Generally, this is a temporary state.
4166 sub die_error {
4167         my $status = shift || 500;
4168         my $error = esc_html(shift) || "Internal Server Error";
4169         my $extra = shift;
4170         my %opts = @_;
4171
4172         my %http_responses = (
4173                 400 => '400 Bad Request',
4174                 403 => '403 Forbidden',
4175                 404 => '404 Not Found',
4176                 500 => '500 Internal Server Error',
4177                 503 => '503 Service Unavailable',
4178         );
4179         git_header_html($http_responses{$status}, undef, %opts);
4180         print <<EOF;
4181 <div class="page_body">
4182 <br /><br />
4183 $status - $error
4184 <br />
4185 EOF
4186         if (defined $extra) {
4187                 print "<hr />\n" .
4188                       "$extra\n";
4189         }
4190         print "</div>\n";
4191
4192         git_footer_html();
4193         goto DONE_GITWEB
4194                 unless ($opts{'-error_handler'});
4195 }
4196
4197 ## ----------------------------------------------------------------------
4198 ## functions printing or outputting HTML: navigation
4199
4200 sub git_print_page_nav {
4201         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4202         $extra = '' if !defined $extra; # pager or formats
4203
4204         my @navs = qw(summary shortlog log commit commitdiff tree);
4205         if ($suppress) {
4206                 @navs = grep { $_ ne $suppress } @navs;
4207         }
4208
4209         my %arg = map { $_ => {action=>$_} } @navs;
4210         if (defined $head) {
4211                 for (qw(commit commitdiff)) {
4212                         $arg{$_}{'hash'} = $head;
4213                 }
4214                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4215                         for (qw(shortlog log)) {
4216                                 $arg{$_}{'hash'} = $head;
4217                         }
4218                 }
4219         }
4220
4221         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
4222         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
4223
4224         my @actions = gitweb_get_feature('actions');
4225         my %repl = (
4226                 '%' => '%',
4227                 'n' => $project,         # project name
4228                 'f' => $git_dir,         # project path within filesystem
4229                 'h' => $treehead || '',  # current hash ('h' parameter)
4230                 'b' => $treebase || '',  # hash base ('hb' parameter)
4231         );
4232         while (@actions) {
4233                 my ($label, $link, $pos) = splice(@actions,0,3);
4234                 # insert
4235                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
4236                 # munch munch
4237                 $link =~ s/%([%nfhb])/$repl{$1}/g;
4238                 $arg{$label}{'_href'} = $link;
4239         }
4240
4241         print "<div class=\"page_nav\">\n" .
4242                 (join " | ",
4243                  map { $_ eq $current ?
4244                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
4245                  } @navs);
4246         print "<br/>\n$extra<br/>\n" .
4247               "</div>\n";
4248 }
4249
4250 # returns a submenu for the nagivation of the refs views (tags, heads,
4251 # remotes) with the current view disabled and the remotes view only
4252 # available if the feature is enabled
4253 sub format_ref_views {
4254         my ($current) = @_;
4255         my @ref_views = qw{tags heads};
4256         push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
4257         return join " | ", map {
4258                 $_ eq $current ? $_ :
4259                 $cgi->a({-href => href(action=>$_)}, $_)
4260         } @ref_views
4261 }
4262
4263 sub format_paging_nav {
4264         my ($action, $page, $has_next_link) = @_;
4265         my $paging_nav;
4266
4267
4268         if ($page > 0) {
4269                 $paging_nav .=
4270                         $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4271                         " &sdot; " .
4272                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
4273                                  -accesskey => "p", -title => "Alt-p"}, "prev");
4274         } else {
4275                 $paging_nav .= "first &sdot; prev";
4276         }
4277
4278         if ($has_next_link) {
4279                 $paging_nav .= " &sdot; " .
4280                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
4281                                  -accesskey => "n", -title => "Alt-n"}, "next");
4282         } else {
4283                 $paging_nav .= " &sdot; next";
4284         }
4285
4286         return $paging_nav;
4287 }
4288
4289 ## ......................................................................
4290 ## functions printing or outputting HTML: div
4291
4292 sub git_print_header_div {
4293         my ($action, $title, $hash, $hash_base) = @_;
4294         my %args = ();
4295
4296         $args{'action'} = $action;
4297         $args{'hash'} = $hash if $hash;
4298         $args{'hash_base'} = $hash_base if $hash_base;
4299
4300         print "<div class=\"header\">\n" .
4301               $cgi->a({-href => href(%args), -class => "title"},
4302               $title ? $title : $action) .
4303               "\n</div>\n";
4304 }
4305
4306 sub format_repo_url {
4307         my ($name, $url) = @_;
4308         return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4309 }
4310
4311 # Group output by placing it in a DIV element and adding a header.
4312 # Options for start_div() can be provided by passing a hash reference as the
4313 # first parameter to the function.
4314 # Options to git_print_header_div() can be provided by passing an array
4315 # reference. This must follow the options to start_div if they are present.
4316 # The content can be a scalar, which is output as-is, a scalar reference, which
4317 # is output after html escaping, an IO handle passed either as *handle or
4318 # *handle{IO}, or a function reference. In the latter case all following
4319 # parameters will be taken as argument to the content function call.
4320 sub git_print_section {
4321         my ($div_args, $header_args, $content);
4322         my $arg = shift;
4323         if (ref($arg) eq 'HASH') {
4324                 $div_args = $arg;
4325                 $arg = shift;
4326         }
4327         if (ref($arg) eq 'ARRAY') {
4328                 $header_args = $arg;
4329                 $arg = shift;
4330         }
4331         $content = $arg;
4332
4333         print $cgi->start_div($div_args);
4334         git_print_header_div(@$header_args);
4335
4336         if (ref($content) eq 'CODE') {
4337                 $content->(@_);
4338         } elsif (ref($content) eq 'SCALAR') {
4339                 print esc_html($$content);
4340         } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4341                 print <$content>;
4342         } elsif (!ref($content) && defined($content)) {
4343                 print $content;
4344         }
4345
4346         print $cgi->end_div;
4347 }
4348
4349 sub format_timestamp_html {
4350         my $date = shift;
4351         my $strtime = $date->{'rfc2822'};
4352
4353         my (undef, undef, $datetime_class) =
4354                 gitweb_get_feature('javascript-timezone');
4355         if ($datetime_class) {
4356                 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
4357         }
4358
4359         my $localtime_format = '(%02d:%02d %s)';
4360         if ($date->{'hour_local'} < 6) {
4361                 $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
4362         }
4363         $strtime .= ' ' .
4364                     sprintf($localtime_format,
4365                             $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
4366
4367         return $strtime;
4368 }
4369
4370 # Outputs the author name and date in long form
4371 sub git_print_authorship {
4372         my $co = shift;
4373         my %opts = @_;
4374         my $tag = $opts{-tag} || 'div';
4375         my $author = $co->{'author_name'};
4376
4377         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4378         print "<$tag class=\"author_date\">" .
4379               format_search_author($author, "author", esc_html($author)) .
4380               " [".format_timestamp_html(\%ad)."]".
4381               git_get_avatar($co->{'author_email'}, -pad_before => 1) .
4382               "</$tag>\n";
4383 }
4384
4385 # Outputs table rows containing the full author or committer information,
4386 # in the format expected for 'commit' view (& similar).
4387 # Parameters are a commit hash reference, followed by the list of people
4388 # to output information for. If the list is empty it defaults to both
4389 # author and committer.
4390 sub git_print_authorship_rows {
4391         my $co = shift;
4392         # too bad we can't use @people = @_ || ('author', 'committer')
4393         my @people = @_;
4394         @people = ('author', 'committer') unless @people;
4395         foreach my $who (@people) {
4396                 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4397                 print "<tr><td>$who</td><td>" .
4398                       format_search_author($co->{"${who}_name"}, $who,
4399                                            esc_html($co->{"${who}_name"})) . " " .
4400                       format_search_author($co->{"${who}_email"}, $who,
4401                                            esc_html("<" . $co->{"${who}_email"} . ">")) .
4402                       "</td><td rowspan=\"2\">" .
4403                       git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4404                       "</td></tr>\n" .
4405                       "<tr>" .
4406                       "<td></td><td>" .
4407                       format_timestamp_html(\%wd) .
4408                       "</td>" .
4409                       "</tr>\n";
4410         }
4411 }
4412
4413 sub git_print_page_path {
4414         my $name = shift;
4415         my $type = shift;
4416         my $hb = shift;
4417
4418
4419         print "<div class=\"page_path\">";
4420         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4421                       -title => 'tree root'}, to_utf8("[$project]"));
4422         print " / ";
4423         if (defined $name) {
4424                 my @dirname = split '/', $name;
4425                 my $basename = pop @dirname;
4426                 my $fullname = '';
4427
4428                 foreach my $dir (@dirname) {
4429                         $fullname .= ($fullname ? '/' : '') . $dir;
4430                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4431                                                      hash_base=>$hb),
4432                                       -title => $fullname}, esc_path($dir));
4433                         print " / ";
4434                 }
4435                 if (defined $type && $type eq 'blob') {
4436                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4437                                                      hash_base=>$hb),
4438                                       -title => $name}, esc_path($basename));
4439                 } elsif (defined $type && $type eq 'tree') {
4440                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4441                                                      hash_base=>$hb),
4442                                       -title => $name}, esc_path($basename));
4443                         print " / ";
4444                 } else {
4445                         print esc_path($basename);
4446                 }
4447         }
4448         print "<br/></div>\n";
4449 }
4450
4451 sub git_print_log {
4452         my $log = shift;
4453         my %opts = @_;
4454
4455         if ($opts{'-remove_title'}) {
4456                 # remove title, i.e. first line of log
4457                 shift @$log;
4458         }
4459         # remove leading empty lines
4460         while (defined $log->[0] && $log->[0] eq "") {
4461                 shift @$log;
4462         }
4463
4464         # print log
4465         my $signoff = 0;
4466         my $empty = 0;
4467         foreach my $line (@$log) {
4468                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4469                         $signoff = 1;
4470                         $empty = 0;
4471                         if (! $opts{'-remove_signoff'}) {
4472                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4473                                 next;
4474                         } else {
4475                                 # remove signoff lines
4476                                 next;
4477                         }
4478                 } else {
4479                         $signoff = 0;
4480                 }
4481
4482                 # print only one empty line
4483                 # do not print empty line after signoff
4484                 if ($line eq "") {
4485                         next if ($empty || $signoff);
4486                         $empty = 1;
4487                 } else {
4488                         $empty = 0;
4489                 }
4490
4491                 print format_log_line_html($line) . "<br/>\n";
4492         }
4493
4494         if ($opts{'-final_empty_line'}) {
4495                 # end with single empty line
4496                 print "<br/>\n" unless $empty;
4497         }
4498 }
4499
4500 # return link target (what link points to)
4501 sub git_get_link_target {
4502         my $hash = shift;
4503         my $link_target;
4504
4505         # read link
4506         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4507                 or return;
4508         {
4509                 local $/ = undef;
4510                 $link_target = <$fd>;
4511         }
4512         close $fd
4513                 or return;
4514
4515         return $link_target;
4516 }
4517
4518 # given link target, and the directory (basedir) the link is in,
4519 # return target of link relative to top directory (top tree);
4520 # return undef if it is not possible (including absolute links).
4521 sub normalize_link_target {
4522         my ($link_target, $basedir) = @_;
4523
4524         # absolute symlinks (beginning with '/') cannot be normalized
4525         return if (substr($link_target, 0, 1) eq '/');
4526
4527         # normalize link target to path from top (root) tree (dir)
4528         my $path;
4529         if ($basedir) {
4530                 $path = $basedir . '/' . $link_target;
4531         } else {
4532                 # we are in top (root) tree (dir)
4533                 $path = $link_target;
4534         }
4535
4536         # remove //, /./, and /../
4537         my @path_parts;
4538         foreach my $part (split('/', $path)) {
4539                 # discard '.' and ''
4540                 next if (!$part || $part eq '.');
4541                 # handle '..'
4542                 if ($part eq '..') {
4543                         if (@path_parts) {
4544                                 pop @path_parts;
4545                         } else {
4546                                 # link leads outside repository (outside top dir)
4547                                 return;
4548                         }
4549                 } else {
4550                         push @path_parts, $part;
4551                 }
4552         }
4553         $path = join('/', @path_parts);
4554
4555         return $path;
4556 }
4557
4558 # print tree entry (row of git_tree), but without encompassing <tr> element
4559 sub git_print_tree_entry {
4560         my ($t, $basedir, $hash_base, $have_blame) = @_;
4561
4562         my %base_key = ();
4563         $base_key{'hash_base'} = $hash_base if defined $hash_base;
4564
4565         # The format of a table row is: mode list link.  Where mode is
4566         # the mode of the entry, list is the name of the entry, an href,
4567         # and link is the action links of the entry.
4568
4569         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4570         if (exists $t->{'size'}) {
4571                 print "<td class=\"size\">$t->{'size'}</td>\n";
4572         }
4573         if ($t->{'type'} eq "blob") {
4574                 print "<td class=\"list\">" .
4575                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4576                                                file_name=>"$basedir$t->{'name'}", %base_key),
4577                                 -class => "list"}, esc_path($t->{'name'}));
4578                 if (S_ISLNK(oct $t->{'mode'})) {
4579                         my $link_target = git_get_link_target($t->{'hash'});
4580                         if ($link_target) {
4581                                 my $norm_target = normalize_link_target($link_target, $basedir);
4582                                 if (defined $norm_target) {
4583                                         print " -> " .
4584                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4585                                                                      file_name=>$norm_target),
4586                                                        -title => $norm_target}, esc_path($link_target));
4587                                 } else {
4588                                         print " -> " . esc_path($link_target);
4589                                 }
4590                         }
4591                 }
4592                 print "</td>\n";
4593                 print "<td class=\"link\">";
4594                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4595                                              file_name=>"$basedir$t->{'name'}", %base_key)},
4596                               "blob");
4597                 if ($have_blame) {
4598                         print " | " .
4599                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4600                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
4601                                       "blame");
4602                 }
4603                 if (defined $hash_base) {
4604                         print " | " .
4605                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4606                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4607                                       "history");
4608                 }
4609                 print " | " .
4610                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4611                                                file_name=>"$basedir$t->{'name'}")},
4612                                 "raw");
4613                 print "</td>\n";
4614
4615         } elsif ($t->{'type'} eq "tree") {
4616                 print "<td class=\"list\">";
4617                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4618                                              file_name=>"$basedir$t->{'name'}",
4619                                              %base_key)},
4620                               esc_path($t->{'name'}));
4621                 print "</td>\n";
4622                 print "<td class=\"link\">";
4623                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4624                                              file_name=>"$basedir$t->{'name'}",
4625                                              %base_key)},
4626                               "tree");
4627                 if (defined $hash_base) {
4628                         print " | " .
4629                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4630                                                      file_name=>"$basedir$t->{'name'}")},
4631                                       "history");
4632                 }
4633                 print "</td>\n";
4634         } else {
4635                 # unknown object: we can only present history for it
4636                 # (this includes 'commit' object, i.e. submodule support)
4637                 print "<td class=\"list\">" .
4638                       esc_path($t->{'name'}) .
4639                       "</td>\n";
4640                 print "<td class=\"link\">";
4641                 if (defined $hash_base) {
4642                         print $cgi->a({-href => href(action=>"history",
4643                                                      hash_base=>$hash_base,
4644                                                      file_name=>"$basedir$t->{'name'}")},
4645                                       "history");
4646                 }
4647                 print "</td>\n";
4648         }
4649 }
4650
4651 ## ......................................................................
4652 ## functions printing large fragments of HTML
4653
4654 # get pre-image filenames for merge (combined) diff
4655 sub fill_from_file_info {
4656         my ($diff, @parents) = @_;
4657
4658         $diff->{'from_file'} = [ ];
4659         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4660         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4661                 if ($diff->{'status'}[$i] eq 'R' ||
4662                     $diff->{'status'}[$i] eq 'C') {
4663                         $diff->{'from_file'}[$i] =
4664                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4665                 }
4666         }
4667
4668         return $diff;
4669 }
4670
4671 # is current raw difftree line of file deletion
4672 sub is_deleted {
4673         my $diffinfo = shift;
4674
4675         return $diffinfo->{'to_id'} eq ('0' x 40);
4676 }
4677
4678 # does patch correspond to [previous] difftree raw line
4679 # $diffinfo  - hashref of parsed raw diff format
4680 # $patchinfo - hashref of parsed patch diff format
4681 #              (the same keys as in $diffinfo)
4682 sub is_patch_split {
4683         my ($diffinfo, $patchinfo) = @_;
4684
4685         return defined $diffinfo && defined $patchinfo
4686                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4687 }
4688
4689
4690 sub git_difftree_body {
4691         my ($difftree, $hash, @parents) = @_;
4692         my ($parent) = $parents[0];
4693         my $have_blame = gitweb_check_feature('blame');
4694         print "<div class=\"list_head\">\n";
4695         if ($#{$difftree} > 10) {
4696                 print(($#{$difftree} + 1) . " files changed:\n");
4697         }
4698         print "</div>\n";
4699
4700         print "<table class=\"" .
4701               (@parents > 1 ? "combined " : "") .
4702               "diff_tree\">\n";
4703
4704         # header only for combined diff in 'commitdiff' view
4705         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4706         if ($has_header) {
4707                 # table header
4708                 print "<thead><tr>\n" .
4709                        "<th></th><th></th>\n"; # filename, patchN link
4710                 for (my $i = 0; $i < @parents; $i++) {
4711                         my $par = $parents[$i];
4712                         print "<th>" .
4713                               $cgi->a({-href => href(action=>"commitdiff",
4714                                                      hash=>$hash, hash_parent=>$par),
4715                                        -title => 'commitdiff to parent number ' .
4716                                                   ($i+1) . ': ' . substr($par,0,7)},
4717                                       $i+1) .
4718                               "&nbsp;</th>\n";
4719                 }
4720                 print "</tr></thead>\n<tbody>\n";
4721         }
4722
4723         my $alternate = 1;
4724         my $patchno = 0;
4725         foreach my $line (@{$difftree}) {
4726                 my $diff = parsed_difftree_line($line);
4727
4728                 if ($alternate) {
4729                         print "<tr class=\"dark\">\n";
4730                 } else {
4731                         print "<tr class=\"light\">\n";
4732                 }
4733                 $alternate ^= 1;
4734
4735                 if (exists $diff->{'nparents'}) { # combined diff
4736
4737                         fill_from_file_info($diff, @parents)
4738                                 unless exists $diff->{'from_file'};
4739
4740                         if (!is_deleted($diff)) {
4741                                 # file exists in the result (child) commit
4742                                 print "<td>" .
4743                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4744                                                              file_name=>$diff->{'to_file'},
4745                                                              hash_base=>$hash),
4746                                               -class => "list"}, esc_path($diff->{'to_file'})) .
4747                                       "</td>\n";
4748                         } else {
4749                                 print "<td>" .
4750                                       esc_path($diff->{'to_file'}) .
4751                                       "</td>\n";
4752                         }
4753
4754                         if ($action eq 'commitdiff') {
4755                                 # link to patch
4756                                 $patchno++;
4757                                 print "<td class=\"link\">" .
4758                                       $cgi->a({-href => href(-anchor=>"patch$patchno")},
4759                                               "patch") .
4760                                       " | " .
4761                                       "</td>\n";
4762                         }
4763
4764                         my $has_history = 0;
4765                         my $not_deleted = 0;
4766                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4767                                 my $hash_parent = $parents[$i];
4768                                 my $from_hash = $diff->{'from_id'}[$i];
4769                                 my $from_path = $diff->{'from_file'}[$i];
4770                                 my $status = $diff->{'status'}[$i];
4771
4772                                 $has_history ||= ($status ne 'A');
4773                                 $not_deleted ||= ($status ne 'D');
4774
4775                                 if ($status eq 'A') {
4776                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
4777                                 } elsif ($status eq 'D') {
4778                                         print "<td class=\"link\">" .
4779                                               $cgi->a({-href => href(action=>"blob",
4780                                                                      hash_base=>$hash,
4781                                                                      hash=>$from_hash,
4782                                                                      file_name=>$from_path)},
4783                                                       "blob" . ($i+1)) .
4784                                               " | </td>\n";
4785                                 } else {
4786                                         if ($diff->{'to_id'} eq $from_hash) {
4787                                                 print "<td class=\"link nochange\">";
4788                                         } else {
4789                                                 print "<td class=\"link\">";
4790                                         }
4791                                         print $cgi->a({-href => href(action=>"blobdiff",
4792                                                                      hash=>$diff->{'to_id'},
4793                                                                      hash_parent=>$from_hash,
4794                                                                      hash_base=>$hash,
4795                                                                      hash_parent_base=>$hash_parent,
4796                                                                      file_name=>$diff->{'to_file'},
4797                                                                      file_parent=>$from_path)},
4798                                                       "diff" . ($i+1)) .
4799                                               " | </td>\n";
4800                                 }
4801                         }
4802
4803                         print "<td class=\"link\">";
4804                         if ($not_deleted) {
4805                                 print $cgi->a({-href => href(action=>"blob",
4806                                                              hash=>$diff->{'to_id'},
4807                                                              file_name=>$diff->{'to_file'},
4808                                                              hash_base=>$hash)},
4809                                               "blob");
4810                                 print " | " if ($has_history);
4811                         }
4812                         if ($has_history) {
4813                                 print $cgi->a({-href => href(action=>"history",
4814                                                              file_name=>$diff->{'to_file'},
4815                                                              hash_base=>$hash)},
4816                                               "history");
4817                         }
4818                         print "</td>\n";
4819
4820                         print "</tr>\n";
4821                         next; # instead of 'else' clause, to avoid extra indent
4822                 }
4823                 # else ordinary diff
4824
4825                 my ($to_mode_oct, $to_mode_str, $to_file_type);
4826                 my ($from_mode_oct, $from_mode_str, $from_file_type);
4827                 if ($diff->{'to_mode'} ne ('0' x 6)) {
4828                         $to_mode_oct = oct $diff->{'to_mode'};
4829                         if (S_ISREG($to_mode_oct)) { # only for regular file
4830                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4831                         }
4832                         $to_file_type = file_type($diff->{'to_mode'});
4833                 }
4834                 if ($diff->{'from_mode'} ne ('0' x 6)) {
4835                         $from_mode_oct = oct $diff->{'from_mode'};
4836                         if (S_ISREG($from_mode_oct)) { # only for regular file
4837                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4838                         }
4839                         $from_file_type = file_type($diff->{'from_mode'});
4840                 }
4841
4842                 if ($diff->{'status'} eq "A") { # created
4843                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4844                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
4845                         $mode_chng   .= "]</span>";
4846                         print "<td>";
4847                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4848                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4849                                       -class => "list"}, esc_path($diff->{'file'}));
4850                         print "</td>\n";
4851                         print "<td>$mode_chng</td>\n";
4852                         print "<td class=\"link\">";
4853                         if ($action eq 'commitdiff') {
4854                                 # link to patch
4855                                 $patchno++;
4856                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4857                                               "patch") .
4858                                       " | ";
4859                         }
4860                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4861                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4862                                       "blob");
4863                         print "</td>\n";
4864
4865                 } elsif ($diff->{'status'} eq "D") { # deleted
4866                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4867                         print "<td>";
4868                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4869                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
4870                                        -class => "list"}, esc_path($diff->{'file'}));
4871                         print "</td>\n";
4872                         print "<td>$mode_chng</td>\n";
4873                         print "<td class=\"link\">";
4874                         if ($action eq 'commitdiff') {
4875                                 # link to patch
4876                                 $patchno++;
4877                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4878                                               "patch") .
4879                                       " | ";
4880                         }
4881                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4882                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
4883                                       "blob") . " | ";
4884                         if ($have_blame) {
4885                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4886                                                              file_name=>$diff->{'file'})},
4887                                               "blame") . " | ";
4888                         }
4889                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4890                                                      file_name=>$diff->{'file'})},
4891                                       "history");
4892                         print "</td>\n";
4893
4894                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4895                         my $mode_chnge = "";
4896                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4897                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4898                                 if ($from_file_type ne $to_file_type) {
4899                                         $mode_chnge .= " from $from_file_type to $to_file_type";
4900                                 }
4901                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4902                                         if ($from_mode_str && $to_mode_str) {
4903                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4904                                         } elsif ($to_mode_str) {
4905                                                 $mode_chnge .= " mode: $to_mode_str";
4906                                         }
4907                                 }
4908                                 $mode_chnge .= "]</span>\n";
4909                         }
4910                         print "<td>";
4911                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4912                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4913                                       -class => "list"}, esc_path($diff->{'file'}));
4914                         print "</td>\n";
4915                         print "<td>$mode_chnge</td>\n";
4916                         print "<td class=\"link\">";
4917                         if ($action eq 'commitdiff') {
4918                                 # link to patch
4919                                 $patchno++;
4920                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4921                                               "patch") .
4922                                       " | ";
4923                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4924                                 # "commit" view and modified file (not onlu mode changed)
4925                                 print $cgi->a({-href => href(action=>"blobdiff",
4926                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4927                                                              hash_base=>$hash, hash_parent_base=>$parent,
4928                                                              file_name=>$diff->{'file'})},
4929                                               "diff") .
4930                                       " | ";
4931                         }
4932                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4933                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4934                                        "blob") . " | ";
4935                         if ($have_blame) {
4936                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4937                                                              file_name=>$diff->{'file'})},
4938                                               "blame") . " | ";
4939                         }
4940                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4941                                                      file_name=>$diff->{'file'})},
4942                                       "history");
4943                         print "</td>\n";
4944
4945                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4946                         my %status_name = ('R' => 'moved', 'C' => 'copied');
4947                         my $nstatus = $status_name{$diff->{'status'}};
4948                         my $mode_chng = "";
4949                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4950                                 # mode also for directories, so we cannot use $to_mode_str
4951                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4952                         }
4953                         print "<td>" .
4954                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4955                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4956                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4957                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4958                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4959                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4960                                       -class => "list"}, esc_path($diff->{'from_file'})) .
4961                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4962                               "<td class=\"link\">";
4963                         if ($action eq 'commitdiff') {
4964                                 # link to patch
4965                                 $patchno++;
4966                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4967                                               "patch") .
4968                                       " | ";
4969                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4970                                 # "commit" view and modified file (not only pure rename or copy)
4971                                 print $cgi->a({-href => href(action=>"blobdiff",
4972                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4973                                                              hash_base=>$hash, hash_parent_base=>$parent,
4974                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4975                                               "diff") .
4976                                       " | ";
4977                         }
4978                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4979                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
4980                                       "blob") . " | ";
4981                         if ($have_blame) {
4982                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4983                                                              file_name=>$diff->{'to_file'})},
4984                                               "blame") . " | ";
4985                         }
4986                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4987                                                     file_name=>$diff->{'to_file'})},
4988                                       "history");
4989                         print "</td>\n";
4990
4991                 } # we should not encounter Unmerged (U) or Unknown (X) status
4992                 print "</tr>\n";
4993         }
4994         print "</tbody>" if $has_header;
4995         print "</table>\n";
4996 }
4997
4998 sub print_sidebyside_diff_chunk {
4999         my @chunk = @_;
5000         my (@ctx, @rem, @add);
5001
5002         return unless @chunk;
5003
5004         # incomplete last line might be among removed or added lines,
5005         # or both, or among context lines: find which
5006         for (my $i = 1; $i < @chunk; $i++) {
5007                 if ($chunk[$i][0] eq 'incomplete') {
5008                         $chunk[$i][0] = $chunk[$i-1][0];
5009                 }
5010         }
5011
5012         # guardian
5013         push @chunk, ["", ""];
5014
5015         foreach my $line_info (@chunk) {
5016                 my ($class, $line) = @$line_info;
5017
5018                 # print chunk headers
5019                 if ($class && $class eq 'chunk_header') {
5020                         print $line;
5021                         next;
5022                 }
5023
5024                 ## print from accumulator when type of class of lines change
5025                 # empty contents block on start rem/add block, or end of chunk
5026                 if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
5027                         print join '',
5028                                 '<div class="chunk_block ctx">',
5029                                         '<div class="old">',
5030                                         @ctx,
5031                                         '</div>',
5032                                         '<div class="new">',
5033                                         @ctx,
5034                                         '</div>',
5035                                 '</div>';
5036                         @ctx = ();
5037                 }
5038                 # empty add/rem block on start context block, or end of chunk
5039                 if ((@rem || @add) && (!$class || $class eq 'ctx')) {
5040                         if (!@add) {
5041                                 # pure removal
5042                                 print join '',
5043                                         '<div class="chunk_block rem">',
5044                                                 '<div class="old">',
5045                                                 @rem,
5046                                                 '</div>',
5047                                         '</div>';
5048                         } elsif (!@rem) {
5049                                 # pure addition
5050                                 print join '',
5051                                         '<div class="chunk_block add">',
5052                                                 '<div class="new">',
5053                                                 @add,
5054                                                 '</div>',
5055                                         '</div>';
5056                         } else {
5057                                 # assume that it is change
5058                                 print join '',
5059                                         '<div class="chunk_block chg">',
5060                                                 '<div class="old">',
5061                                                 @rem,
5062                                                 '</div>',
5063                                                 '<div class="new">',
5064                                                 @add,
5065                                                 '</div>',
5066                                         '</div>';
5067                         }
5068                         @rem = @add = ();
5069                 }
5070
5071                 ## adding lines to accumulator
5072                 # guardian value
5073                 last unless $line;
5074                 # rem, add or change
5075                 if ($class eq 'rem') {
5076                         push @rem, $line;
5077                 } elsif ($class eq 'add') {
5078                         push @add, $line;
5079                 }
5080                 # context line
5081                 if ($class eq 'ctx') {
5082                         push @ctx, $line;
5083                 }
5084         }
5085 }
5086
5087 sub git_patchset_body {
5088         my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
5089         my ($hash_parent) = $hash_parents[0];
5090
5091         my $is_combined = (@hash_parents > 1);
5092         my $patch_idx = 0;
5093         my $patch_number = 0;
5094         my $patch_line;
5095         my $diffinfo;
5096         my $to_name;
5097         my (%from, %to);
5098         my @chunk; # for side-by-side diff
5099
5100         print "<div class=\"patchset\">\n";
5101
5102         # skip to first patch
5103         while ($patch_line = <$fd>) {
5104                 chomp $patch_line;
5105
5106                 last if ($patch_line =~ m/^diff /);
5107         }
5108
5109  PATCH:
5110         while ($patch_line) {
5111
5112                 # parse "git diff" header line
5113                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
5114                         # $1 is from_name, which we do not use
5115                         $to_name = unquote($2);
5116                         $to_name =~ s!^b/!!;
5117                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
5118                         # $1 is 'cc' or 'combined', which we do not use
5119                         $to_name = unquote($2);
5120                 } else {
5121                         $to_name = undef;
5122                 }
5123
5124                 # check if current patch belong to current raw line
5125                 # and parse raw git-diff line if needed
5126                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
5127                         # this is continuation of a split patch
5128                         print "<div class=\"patch cont\">\n";
5129                 } else {
5130                         # advance raw git-diff output if needed
5131                         $patch_idx++ if defined $diffinfo;
5132
5133                         # read and prepare patch information
5134                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5135
5136                         # compact combined diff output can have some patches skipped
5137                         # find which patch (using pathname of result) we are at now;
5138                         if ($is_combined) {
5139                                 while ($to_name ne $diffinfo->{'to_file'}) {
5140                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5141                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
5142                                               "</div>\n";  # class="patch"
5143
5144                                         $patch_idx++;
5145                                         $patch_number++;
5146
5147                                         last if $patch_idx > $#$difftree;
5148                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5149                                 }
5150                         }
5151
5152                         # modifies %from, %to hashes
5153                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5154
5155                         # this is first patch for raw difftree line with $patch_idx index
5156                         # we index @$difftree array from 0, but number patches from 1
5157                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
5158                 }
5159
5160                 # git diff header
5161                 #assert($patch_line =~ m/^diff /) if DEBUG;
5162                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5163                 $patch_number++;
5164                 # print "git diff" header
5165                 print format_git_diff_header_line($patch_line, $diffinfo,
5166                                                   \%from, \%to);
5167
5168                 # print extended diff header
5169                 print "<div class=\"diff extended_header\">\n";
5170         EXTENDED_HEADER:
5171                 while ($patch_line = <$fd>) {
5172                         chomp $patch_line;
5173
5174                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
5175
5176                         print format_extended_diff_header_line($patch_line, $diffinfo,
5177                                                                \%from, \%to);
5178                 }
5179                 print "</div>\n"; # class="diff extended_header"
5180
5181                 # from-file/to-file diff header
5182                 if (! $patch_line) {
5183                         print "</div>\n"; # class="patch"
5184                         last PATCH;
5185                 }
5186                 next PATCH if ($patch_line =~ m/^diff /);
5187                 #assert($patch_line =~ m/^---/) if DEBUG;
5188
5189                 my $last_patch_line = $patch_line;
5190                 $patch_line = <$fd>;
5191                 chomp $patch_line;
5192                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
5193
5194                 print format_diff_from_to_header($last_patch_line, $patch_line,
5195                                                  $diffinfo, \%from, \%to,
5196                                                  @hash_parents);
5197
5198                 # the patch itself
5199         LINE:
5200                 while ($patch_line = <$fd>) {
5201                         chomp $patch_line;
5202
5203                         next PATCH if ($patch_line =~ m/^diff /);
5204
5205                         my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
5206                         my $diff_classes = "diff";
5207                         $diff_classes .= " $class" if ($class);
5208                         $line = "<div class=\"$diff_classes\">$line</div>\n";
5209
5210                         if ($diff_style eq 'sidebyside' && !$is_combined) {
5211                                 if ($class eq 'chunk_header') {
5212                                         print_sidebyside_diff_chunk(@chunk);
5213                                         @chunk = ( [ $class, $line ] );
5214                                 } else {
5215                                         push @chunk, [ $class, $line ];
5216                                 }
5217                         } else {
5218                                 # default 'inline' style and unknown styles
5219                                 print $line;
5220                         }
5221                 }
5222
5223         } continue {
5224                 if (@chunk) {
5225                         print_sidebyside_diff_chunk(@chunk);
5226                         @chunk = ();
5227                 }
5228                 print "</div>\n"; # class="patch"
5229         }
5230
5231         # for compact combined (--cc) format, with chunk and patch simplification
5232         # the patchset might be empty, but there might be unprocessed raw lines
5233         for (++$patch_idx if $patch_number > 0;
5234              $patch_idx < @$difftree;
5235              ++$patch_idx) {
5236                 # read and prepare patch information
5237                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5238
5239                 # generate anchor for "patch" links in difftree / whatchanged part
5240                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5241                       format_diff_cc_simplified($diffinfo, @hash_parents) .
5242                       "</div>\n";  # class="patch"
5243
5244                 $patch_number++;
5245         }
5246
5247         if ($patch_number == 0) {
5248                 if (@hash_parents > 1) {
5249                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
5250                 } else {
5251                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
5252                 }
5253         }
5254
5255         print "</div>\n"; # class="patchset"
5256 }
5257
5258 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5259
5260 sub git_project_search_form {
5261         my ($searchtext, $search_use_regexp) = @_;
5262
5263         my $limit = '';
5264         if ($project_filter) {
5265                 $limit = " in '$project_filter/'";
5266         }
5267
5268         print "<div class=\"projsearch\">\n";
5269         print $cgi->startform(-method => 'get', -action => $my_uri) .
5270               $cgi->hidden(-name => 'a', -value => 'project_list')  . "\n";
5271         print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
5272                 if (defined $project_filter);
5273         print $cgi->textfield(-name => 's', -value => $searchtext,
5274                               -title => "Search project by name and description$limit",
5275                               -size => 60) . "\n" .
5276               "<span title=\"Extended regular expression\">" .
5277               $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5278                              -checked => $search_use_regexp) .
5279               "</span>\n" .
5280               $cgi->submit(-name => 'btnS', -value => 'Search') .
5281               $cgi->end_form() . "\n" .
5282               $cgi->a({-href => href(project => undef, searchtext => undef,
5283                                      project_filter => $project_filter)},
5284                       esc_html("List all projects$limit")) . "<br />\n";
5285         print "</div>\n";
5286 }
5287
5288 # entry for given @keys needs filling if at least one of keys in list
5289 # is not present in %$project_info
5290 sub project_info_needs_filling {
5291         my ($project_info, @keys) = @_;
5292
5293         # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
5294         foreach my $key (@keys) {
5295                 if (!exists $project_info->{$key}) {
5296                         return 1;
5297                 }
5298         }
5299         return;
5300 }
5301
5302 # fills project list info (age, description, owner, category, forks, etc.)
5303 # for each project in the list, removing invalid projects from
5304 # returned list, or fill only specified info.
5305 #
5306 # Invalid projects are removed from the returned list if and only if you
5307 # ask 'age' or 'age_string' to be filled, because they are the only fields
5308 # that run unconditionally git command that requires repository, and
5309 # therefore do always check if project repository is invalid.
5310 #
5311 # USAGE:
5312 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
5313 #   ensures that 'descr_long' and 'ctags' fields are filled
5314 # * @project_list = fill_project_list_info(\@project_list)
5315 #   ensures that all fields are filled (and invalid projects removed)
5316 #
5317 # NOTE: modifies $projlist, but does not remove entries from it
5318 sub fill_project_list_info {
5319         my ($projlist, @wanted_keys) = @_;
5320         my @projects;
5321         my $filter_set = sub { return @_; };
5322         if (@wanted_keys) {
5323                 my %wanted_keys = map { $_ => 1 } @wanted_keys;
5324                 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
5325         }
5326
5327         my $show_ctags = gitweb_check_feature('ctags');
5328  PROJECT:
5329         foreach my $pr (@$projlist) {
5330                 if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
5331                         my (@activity) = git_get_last_activity($pr->{'path'});
5332                         unless (@activity) {
5333                                 next PROJECT;
5334                         }
5335                         ($pr->{'age'}, $pr->{'age_string'}) = @activity;
5336                 }
5337                 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
5338                         my $descr = git_get_project_description($pr->{'path'}) || "";
5339                         $descr = to_utf8($descr);
5340                         $pr->{'descr_long'} = $descr;
5341                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
5342                 }
5343                 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
5344                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
5345                 }
5346                 if ($show_ctags &&
5347                     project_info_needs_filling($pr, $filter_set->('ctags'))) {
5348                         $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
5349                 }
5350                 if ($projects_list_group_categories &&
5351                     project_info_needs_filling($pr, $filter_set->('category'))) {
5352                         my $cat = git_get_project_category($pr->{'path'}) ||
5353                                                            $project_list_default_category;
5354                         $pr->{'category'} = to_utf8($cat);
5355                 }
5356
5357                 push @projects, $pr;
5358         }
5359
5360         return @projects;
5361 }
5362
5363 sub sort_projects_list {
5364         my ($projlist, $order) = @_;
5365         my @projects;
5366
5367         my %order_info = (
5368                 project => { key => 'path', type => 'str' },
5369                 descr => { key => 'descr_long', type => 'str' },
5370                 owner => { key => 'owner', type => 'str' },
5371                 age => { key => 'age', type => 'num' }
5372         );
5373         my $oi = $order_info{$order};
5374         return @$projlist unless defined $oi;
5375         if ($oi->{'type'} eq 'str') {
5376                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
5377         } else {
5378                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
5379         }
5380
5381         return @projects;
5382 }
5383
5384 # returns a hash of categories, containing the list of project
5385 # belonging to each category
5386 sub build_projlist_by_category {
5387         my ($projlist, $from, $to) = @_;
5388         my %categories;
5389
5390         $from = 0 unless defined $from;
5391         $to = $#$projlist if (!defined $to || $#$projlist < $to);
5392
5393         for (my $i = $from; $i <= $to; $i++) {
5394                 my $pr = $projlist->[$i];
5395                 push @{$categories{ $pr->{'category'} }}, $pr;
5396         }
5397
5398         return wantarray ? %categories : \%categories;
5399 }
5400
5401 # print 'sort by' <th> element, generating 'sort by $name' replay link
5402 # if that order is not selected
5403 sub print_sort_th {
5404         print format_sort_th(@_);
5405 }
5406
5407 sub format_sort_th {
5408         my ($name, $order, $header) = @_;
5409         my $sort_th = "";
5410         $header ||= ucfirst($name);
5411
5412         if ($order eq $name) {
5413                 $sort_th .= "<th>$header</th>\n";
5414         } else {
5415                 $sort_th .= "<th>" .
5416                             $cgi->a({-href => href(-replay=>1, order=>$name),
5417                                      -class => "header"}, $header) .
5418                             "</th>\n";
5419         }
5420
5421         return $sort_th;
5422 }
5423
5424 sub git_project_list_rows {
5425         my ($projlist, $from, $to, $check_forks) = @_;
5426
5427         $from = 0 unless defined $from;
5428         $to = $#$projlist if (!defined $to || $#$projlist < $to);
5429
5430         my $alternate = 1;
5431         for (my $i = $from; $i <= $to; $i++) {
5432                 my $pr = $projlist->[$i];
5433
5434                 if ($alternate) {
5435                         print "<tr class=\"dark\">\n";
5436                 } else {
5437                         print "<tr class=\"light\">\n";
5438                 }
5439                 $alternate ^= 1;
5440
5441                 if ($check_forks) {
5442                         print "<td>";
5443                         if ($pr->{'forks'}) {
5444                                 my $nforks = scalar @{$pr->{'forks'}};
5445                                 if ($nforks > 0) {
5446                                         print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5447                                                        -title => "$nforks forks"}, "+");
5448                                 } else {
5449                                         print $cgi->span({-title => "$nforks forks"}, "+");
5450                                 }
5451                         }
5452                         print "</td>\n";
5453                 }
5454                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5455                                         -class => "list"},
5456                                        esc_html_match_hl($pr->{'path'}, $search_regexp)) .
5457                       "</td>\n" .
5458                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5459                                         -class => "list",
5460                                         -title => $pr->{'descr_long'}},
5461                                         $search_regexp
5462                                         ? esc_html_match_hl_chopped($pr->{'descr_long'},
5463                                                                     $pr->{'descr'}, $search_regexp)
5464                                         : esc_html($pr->{'descr'})) .
5465                       "</td>\n" .
5466                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5467                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5468                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
5469                       "<td class=\"link\">" .
5470                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
5471                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5472                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5473                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5474                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5475                       "</td>\n" .
5476                       "</tr>\n";
5477         }
5478 }
5479
5480 sub git_project_list_body {
5481         # actually uses global variable $project
5482         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5483         my @projects = @$projlist;
5484
5485         my $check_forks = gitweb_check_feature('forks');
5486         my $show_ctags  = gitweb_check_feature('ctags');
5487         my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
5488         $check_forks = undef
5489                 if ($tagfilter || $search_regexp);
5490
5491         # filtering out forks before filling info allows to do less work
5492         @projects = filter_forks_from_projects_list(\@projects)
5493                 if ($check_forks);
5494         # search_projects_list pre-fills required info
5495         @projects = search_projects_list(\@projects,
5496                                          'search_regexp' => $search_regexp,
5497                                          'tagfilter'  => $tagfilter)
5498                 if ($tagfilter || $search_regexp);
5499         # fill the rest
5500         @projects = fill_project_list_info(\@projects);
5501
5502         $order ||= $default_projects_order;
5503         $from = 0 unless defined $from;
5504         $to = $#projects if (!defined $to || $#projects < $to);
5505
5506         # short circuit
5507         if ($from > $to) {
5508                 print "<center>\n".
5509                       "<b>No such projects found</b><br />\n".
5510                       "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5511                       "</center>\n<br />\n";
5512                 return;
5513         }
5514
5515         @projects = sort_projects_list(\@projects, $order);
5516
5517         if ($show_ctags) {
5518                 my $ctags = git_gather_all_ctags(\@projects);
5519                 my $cloud = git_populate_project_tagcloud($ctags);
5520                 print git_show_project_tagcloud($cloud, 64);
5521         }
5522
5523         print "<table class=\"project_list\">\n";
5524         unless ($no_header) {
5525                 print "<tr>\n";
5526                 if ($check_forks) {
5527                         print "<th></th>\n";
5528                 }
5529                 print_sort_th('project', $order, 'Project');
5530                 print_sort_th('descr', $order, 'Description');
5531                 print_sort_th('owner', $order, 'Owner');
5532                 print_sort_th('age', $order, 'Last Change');
5533                 print "<th></th>\n" . # for links
5534                       "</tr>\n";
5535         }
5536
5537         if ($projects_list_group_categories) {
5538                 # only display categories with projects in the $from-$to window
5539                 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
5540                 my %categories = build_projlist_by_category(\@projects, $from, $to);
5541                 foreach my $cat (sort keys %categories) {
5542                         unless ($cat eq "") {
5543                                 print "<tr>\n";
5544                                 if ($check_forks) {
5545                                         print "<td></td>\n";
5546                                 }
5547                                 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
5548                                 print "</tr>\n";
5549                         }
5550
5551                         git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
5552                 }
5553         } else {
5554                 git_project_list_rows(\@projects, $from, $to, $check_forks);
5555         }
5556
5557         if (defined $extra) {
5558                 print "<tr>\n";
5559                 if ($check_forks) {
5560                         print "<td></td>\n";
5561                 }
5562                 print "<td colspan=\"5\">$extra</td>\n" .
5563                       "</tr>\n";
5564         }
5565         print "</table>\n";
5566 }
5567
5568 sub git_log_body {
5569         # uses global variable $project
5570         my ($commitlist, $from, $to, $refs, $extra) = @_;
5571
5572         $from = 0 unless defined $from;
5573         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5574
5575         for (my $i = 0; $i <= $to; $i++) {
5576                 my %co = %{$commitlist->[$i]};
5577                 next if !%co;
5578                 my $commit = $co{'id'};
5579                 my $ref = format_ref_marker($refs, $commit);
5580                 git_print_header_div('commit',
5581                                "<span class=\"age\">$co{'age_string'}</span>" .
5582                                esc_html($co{'title'}) . $ref,
5583                                $commit);
5584                 print "<div class=\"title_text\">\n" .
5585                       "<div class=\"log_link\">\n" .
5586                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5587                       " | " .
5588                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5589                       " | " .
5590                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5591                       "<br/>\n" .
5592                       "</div>\n";
5593                       git_print_authorship(\%co, -tag => 'span');
5594                       print "<br/>\n</div>\n";
5595
5596                 print "<div class=\"log_body\">\n";
5597                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5598                 print "</div>\n";
5599         }
5600         if ($extra) {
5601                 print "<div class=\"page_nav\">\n";
5602                 print "$extra\n";
5603                 print "</div>\n";
5604         }
5605 }
5606
5607 sub git_shortlog_body {
5608         # uses global variable $project
5609         my ($commitlist, $from, $to, $refs, $extra) = @_;
5610
5611         $from = 0 unless defined $from;
5612         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5613
5614         print "<table class=\"shortlog\">\n";
5615         my $alternate = 1;
5616         for (my $i = $from; $i <= $to; $i++) {
5617                 my %co = %{$commitlist->[$i]};
5618                 my $commit = $co{'id'};
5619                 my $ref = format_ref_marker($refs, $commit);
5620                 if ($alternate) {
5621                         print "<tr class=\"dark\">\n";
5622                 } else {
5623                         print "<tr class=\"light\">\n";
5624                 }
5625                 $alternate ^= 1;
5626                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5627                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5628                       format_author_html('td', \%co, 10) . "<td>";
5629                 print format_subject_html($co{'title'}, $co{'title_short'},
5630                                           href(action=>"commit", hash=>$commit), $ref);
5631                 print "</td>\n" .
5632                       "<td class=\"link\">" .
5633                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5634                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5635                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5636                 my $snapshot_links = format_snapshot_links($commit);
5637                 if (defined $snapshot_links) {
5638                         print " | " . $snapshot_links;
5639                 }
5640                 print "</td>\n" .
5641                       "</tr>\n";
5642         }
5643         if (defined $extra) {
5644                 print "<tr>\n" .
5645                       "<td colspan=\"4\">$extra</td>\n" .
5646                       "</tr>\n";
5647         }
5648         print "</table>\n";
5649 }
5650
5651 sub git_history_body {
5652         # Warning: assumes constant type (blob or tree) during history
5653         my ($commitlist, $from, $to, $refs, $extra,
5654             $file_name, $file_hash, $ftype) = @_;
5655
5656         $from = 0 unless defined $from;
5657         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5658
5659         print "<table class=\"history\">\n";
5660         my $alternate = 1;
5661         for (my $i = $from; $i <= $to; $i++) {
5662                 my %co = %{$commitlist->[$i]};
5663                 if (!%co) {
5664                         next;
5665                 }
5666                 my $commit = $co{'id'};
5667
5668                 my $ref = format_ref_marker($refs, $commit);
5669
5670                 if ($alternate) {
5671                         print "<tr class=\"dark\">\n";
5672                 } else {
5673                         print "<tr class=\"light\">\n";
5674                 }
5675                 $alternate ^= 1;
5676                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5677         # shortlog:   format_author_html('td', \%co, 10)
5678                       format_author_html('td', \%co, 15, 3) . "<td>";
5679                 # originally git_history used chop_str($co{'title'}, 50)
5680                 print format_subject_html($co{'title'}, $co{'title_short'},
5681                                           href(action=>"commit", hash=>$commit), $ref);
5682                 print "</td>\n" .
5683                       "<td class=\"link\">" .
5684                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5685                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5686
5687                 if ($ftype eq 'blob') {
5688                         my $blob_current = $file_hash;
5689                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
5690                         if (defined $blob_current && defined $blob_parent &&
5691                                         $blob_current ne $blob_parent) {
5692                                 print " | " .
5693                                         $cgi->a({-href => href(action=>"blobdiff",
5694                                                                hash=>$blob_current, hash_parent=>$blob_parent,
5695                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
5696                                                                file_name=>$file_name)},
5697                                                 "diff to current");
5698                         }
5699                 }
5700                 print "</td>\n" .
5701                       "</tr>\n";
5702         }
5703         if (defined $extra) {
5704                 print "<tr>\n" .
5705                       "<td colspan=\"4\">$extra</td>\n" .
5706                       "</tr>\n";
5707         }
5708         print "</table>\n";
5709 }
5710
5711 sub git_tags_body {
5712         # uses global variable $project
5713         my ($taglist, $from, $to, $extra) = @_;
5714         $from = 0 unless defined $from;
5715         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5716
5717         print "<table class=\"tags\">\n";
5718         my $alternate = 1;
5719         for (my $i = $from; $i <= $to; $i++) {
5720                 my $entry = $taglist->[$i];
5721                 my %tag = %$entry;
5722                 my $comment = $tag{'subject'};
5723                 my $comment_short;
5724                 if (defined $comment) {
5725                         $comment_short = chop_str($comment, 30, 5);
5726                 }
5727                 if ($alternate) {
5728                         print "<tr class=\"dark\">\n";
5729                 } else {
5730                         print "<tr class=\"light\">\n";
5731                 }
5732                 $alternate ^= 1;
5733                 if (defined $tag{'age'}) {
5734                         print "<td><i>$tag{'age'}</i></td>\n";
5735                 } else {
5736                         print "<td></td>\n";
5737                 }
5738                 print "<td>" .
5739                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5740                                -class => "list name"}, esc_html($tag{'name'})) .
5741                       "</td>\n" .
5742                       "<td>";
5743                 if (defined $comment) {
5744                         print format_subject_html($comment, $comment_short,
5745                                                   href(action=>"tag", hash=>$tag{'id'}));
5746                 }
5747                 print "</td>\n" .
5748                       "<td class=\"selflink\">";
5749                 if ($tag{'type'} eq "tag") {
5750                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5751                 } else {
5752                         print "&nbsp;";
5753                 }
5754                 print "</td>\n" .
5755                       "<td class=\"link\">" . " | " .
5756                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5757                 if ($tag{'reftype'} eq "commit") {
5758                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5759                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5760                 } elsif ($tag{'reftype'} eq "blob") {
5761                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5762                 }
5763                 print "</td>\n" .
5764                       "</tr>";
5765         }
5766         if (defined $extra) {
5767                 print "<tr>\n" .
5768                       "<td colspan=\"5\">$extra</td>\n" .
5769                       "</tr>\n";
5770         }
5771         print "</table>\n";
5772 }
5773
5774 sub git_heads_body {
5775         # uses global variable $project
5776         my ($headlist, $head_at, $from, $to, $extra) = @_;
5777         $from = 0 unless defined $from;
5778         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5779
5780         print "<table class=\"heads\">\n";
5781         my $alternate = 1;
5782         for (my $i = $from; $i <= $to; $i++) {
5783                 my $entry = $headlist->[$i];
5784                 my %ref = %$entry;
5785                 my $curr = defined $head_at && $ref{'id'} eq $head_at;
5786                 if ($alternate) {
5787                         print "<tr class=\"dark\">\n";
5788                 } else {
5789                         print "<tr class=\"light\">\n";
5790                 }
5791                 $alternate ^= 1;
5792                 print "<td><i>$ref{'age'}</i></td>\n" .
5793                       ($curr ? "<td class=\"current_head\">" : "<td>") .
5794                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5795                                -class => "list name"},esc_html($ref{'name'})) .
5796                       "</td>\n" .
5797                       "<td class=\"link\">" .
5798                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5799                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5800                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5801                       "</td>\n" .
5802                       "</tr>";
5803         }
5804         if (defined $extra) {
5805                 print "<tr>\n" .
5806                       "<td colspan=\"3\">$extra</td>\n" .
5807                       "</tr>\n";
5808         }
5809         print "</table>\n";
5810 }
5811
5812 # Display a single remote block
5813 sub git_remote_block {
5814         my ($remote, $rdata, $limit, $head) = @_;
5815
5816         my $heads = $rdata->{'heads'};
5817         my $fetch = $rdata->{'fetch'};
5818         my $push = $rdata->{'push'};
5819
5820         my $urls_table = "<table class=\"projects_list\">\n" ;
5821
5822         if (defined $fetch) {
5823                 if ($fetch eq $push) {
5824                         $urls_table .= format_repo_url("URL", $fetch);
5825                 } else {
5826                         $urls_table .= format_repo_url("Fetch URL", $fetch);
5827                         $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5828                 }
5829         } elsif (defined $push) {
5830                 $urls_table .= format_repo_url("Push URL", $push);
5831         } else {
5832                 $urls_table .= format_repo_url("", "No remote URL");
5833         }
5834
5835         $urls_table .= "</table>\n";
5836
5837         my $dots;
5838         if (defined $limit && $limit < @$heads) {
5839                 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5840         }
5841
5842         print $urls_table;
5843         git_heads_body($heads, $head, 0, $limit, $dots);
5844 }
5845
5846 # Display a list of remote names with the respective fetch and push URLs
5847 sub git_remotes_list {
5848         my ($remotedata, $limit) = @_;
5849         print "<table class=\"heads\">\n";
5850         my $alternate = 1;
5851         my @remotes = sort keys %$remotedata;
5852
5853         my $limited = $limit && $limit < @remotes;
5854
5855         $#remotes = $limit - 1 if $limited;
5856
5857         while (my $remote = shift @remotes) {
5858                 my $rdata = $remotedata->{$remote};
5859                 my $fetch = $rdata->{'fetch'};
5860                 my $push = $rdata->{'push'};
5861                 if ($alternate) {
5862                         print "<tr class=\"dark\">\n";
5863                 } else {
5864                         print "<tr class=\"light\">\n";
5865                 }
5866                 $alternate ^= 1;
5867                 print "<td>" .
5868                       $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5869                                -class=> "list name"},esc_html($remote)) .
5870                       "</td>";
5871                 print "<td class=\"link\">" .
5872                       (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5873                       " | " .
5874                       (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5875                       "</td>";
5876
5877                 print "</tr>\n";
5878         }
5879
5880         if ($limited) {
5881                 print "<tr>\n" .
5882                       "<td colspan=\"3\">" .
5883                       $cgi->a({-href => href(action=>"remotes")}, "...") .
5884                       "</td>\n" . "</tr>\n";
5885         }
5886
5887         print "</table>";
5888 }
5889
5890 # Display remote heads grouped by remote, unless there are too many
5891 # remotes, in which case we only display the remote names
5892 sub git_remotes_body {
5893         my ($remotedata, $limit, $head) = @_;
5894         if ($limit and $limit < keys %$remotedata) {
5895                 git_remotes_list($remotedata, $limit);
5896         } else {
5897                 fill_remote_heads($remotedata);
5898                 while (my ($remote, $rdata) = each %$remotedata) {
5899                         git_print_section({-class=>"remote", -id=>$remote},
5900                                 ["remotes", $remote, $remote], sub {
5901                                         git_remote_block($remote, $rdata, $limit, $head);
5902                                 });
5903                 }
5904         }
5905 }
5906
5907 sub git_search_message {
5908         my %co = @_;
5909
5910         my $greptype;
5911         if ($searchtype eq 'commit') {
5912                 $greptype = "--grep=";
5913         } elsif ($searchtype eq 'author') {
5914                 $greptype = "--author=";
5915         } elsif ($searchtype eq 'committer') {
5916                 $greptype = "--committer=";
5917         }
5918         $greptype .= $searchtext;
5919         my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5920                                        $greptype, '--regexp-ignore-case',
5921                                        $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5922
5923         my $paging_nav = '';
5924         if ($page > 0) {
5925                 $paging_nav .=
5926                         $cgi->a({-href => href(-replay=>1, page=>undef)},
5927                                 "first") .
5928                         " &sdot; " .
5929                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5930                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5931         } else {
5932                 $paging_nav .= "first &sdot; prev";
5933         }
5934         my $next_link = '';
5935         if ($#commitlist >= 100) {
5936                 $next_link =
5937                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5938                                  -accesskey => "n", -title => "Alt-n"}, "next");
5939                 $paging_nav .= " &sdot; $next_link";
5940         } else {
5941                 $paging_nav .= " &sdot; next";
5942         }
5943
5944         git_header_html();
5945
5946         git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5947         git_print_header_div('commit', esc_html($co{'title'}), $hash);
5948         if ($page == 0 && !@commitlist) {
5949                 print "<p>No match.</p>\n";
5950         } else {
5951                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5952         }
5953
5954         git_footer_html();
5955 }
5956
5957 sub git_search_changes {
5958         my %co = @_;
5959
5960         local $/ = "\n";
5961         open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5962                 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5963                 ($search_use_regexp ? '--pickaxe-regex' : ())
5964                         or die_error(500, "Open git-log failed");
5965
5966         git_header_html();
5967
5968         git_print_page_nav('','', $hash,$co{'tree'},$hash);
5969         git_print_header_div('commit', esc_html($co{'title'}), $hash);
5970
5971         print "<table class=\"pickaxe search\">\n";
5972         my $alternate = 1;
5973         undef %co;
5974         my @files;
5975         while (my $line = <$fd>) {
5976                 chomp $line;
5977                 next unless $line;
5978
5979                 my %set = parse_difftree_raw_line($line);
5980                 if (defined $set{'commit'}) {
5981                         # finish previous commit
5982                         if (%co) {
5983                                 print "</td>\n" .
5984                                       "<td class=\"link\">" .
5985                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
5986                                               "commit") .
5987                                       " | " .
5988                                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
5989                                                              hash_base=>$co{'id'})},
5990                                               "tree") .
5991                                       "</td>\n" .
5992                                       "</tr>\n";
5993                         }
5994
5995                         if ($alternate) {
5996                                 print "<tr class=\"dark\">\n";
5997                         } else {
5998                                 print "<tr class=\"light\">\n";
5999                         }
6000                         $alternate ^= 1;
6001                         %co = parse_commit($set{'commit'});
6002                         my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6003                         print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6004                               "<td><i>$author</i></td>\n" .
6005                               "<td>" .
6006                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6007                                       -class => "list subject"},
6008                                       chop_and_escape_str($co{'title'}, 50) . "<br/>");
6009                 } elsif (defined $set{'to_id'}) {
6010                         next if ($set{'to_id'} =~ m/^0{40}$/);
6011
6012                         print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6013                                                      hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6014                                       -class => "list"},
6015                                       "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6016                               "<br/>\n";
6017                 }
6018         }
6019         close $fd;
6020
6021         # finish last commit (warning: repetition!)
6022         if (%co) {
6023                 print "</td>\n" .
6024                       "<td class=\"link\">" .
6025                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6026                               "commit") .
6027                       " | " .
6028                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6029                                              hash_base=>$co{'id'})},
6030                               "tree") .
6031                       "</td>\n" .
6032                       "</tr>\n";
6033         }
6034
6035         print "</table>\n";
6036
6037         git_footer_html();
6038 }
6039
6040 sub git_search_files {
6041         my %co = @_;
6042
6043         local $/ = "\n";
6044         open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
6045                 $search_use_regexp ? ('-E', '-i') : '-F',
6046                 $searchtext, $co{'tree'}
6047                         or die_error(500, "Open git-grep failed");
6048
6049         git_header_html();
6050
6051         git_print_page_nav('','', $hash,$co{'tree'},$hash);
6052         git_print_header_div('commit', esc_html($co{'title'}), $hash);
6053
6054         print "<table class=\"grep_search\">\n";
6055         my $alternate = 1;
6056         my $matches = 0;
6057         my $lastfile = '';
6058         my $file_href;
6059         while (my $line = <$fd>) {
6060                 chomp $line;
6061                 my ($file, $lno, $ltext, $binary);
6062                 last if ($matches++ > 1000);
6063                 if ($line =~ /^Binary file (.+) matches$/) {
6064                         $file = $1;
6065                         $binary = 1;
6066                 } else {
6067                         ($file, $lno, $ltext) = split(/\0/, $line, 3);
6068                         $file =~ s/^$co{'tree'}://;
6069                 }
6070                 if ($file ne $lastfile) {
6071                         $lastfile and print "</td></tr>\n";
6072                         if ($alternate++) {
6073                                 print "<tr class=\"dark\">\n";
6074                         } else {
6075                                 print "<tr class=\"light\">\n";
6076                         }
6077                         $file_href = href(action=>"blob", hash_base=>$co{'id'},
6078                                           file_name=>$file);
6079                         print "<td class=\"list\">".
6080                                 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
6081                         print "</td><td>\n";
6082                         $lastfile = $file;
6083                 }
6084                 if ($binary) {
6085                         print "<div class=\"binary\">Binary file</div>\n";
6086                 } else {
6087                         $ltext = untabify($ltext);
6088                         if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6089                                 $ltext = esc_html($1, -nbsp=>1);
6090                                 $ltext .= '<span class="match">';
6091                                 $ltext .= esc_html($2, -nbsp=>1);
6092                                 $ltext .= '</span>';
6093                                 $ltext .= esc_html($3, -nbsp=>1);
6094                         } else {
6095                                 $ltext = esc_html($ltext, -nbsp=>1);
6096                         }
6097                         print "<div class=\"pre\">" .
6098                                 $cgi->a({-href => $file_href.'#l'.$lno,
6099                                         -class => "linenr"}, sprintf('%4i', $lno)) .
6100                                 ' ' .  $ltext . "</div>\n";
6101                 }
6102         }
6103         if ($lastfile) {
6104                 print "</td></tr>\n";
6105                 if ($matches > 1000) {
6106                         print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6107                 }
6108         } else {
6109                 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6110         }
6111         close $fd;
6112
6113         print "</table>\n";
6114
6115         git_footer_html();
6116 }
6117
6118 sub git_search_grep_body {
6119         my ($commitlist, $from, $to, $extra) = @_;
6120         $from = 0 unless defined $from;
6121         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6122
6123         print "<table class=\"commit_search\">\n";
6124         my $alternate = 1;
6125         for (my $i = $from; $i <= $to; $i++) {
6126                 my %co = %{$commitlist->[$i]};
6127                 if (!%co) {
6128                         next;
6129                 }
6130                 my $commit = $co{'id'};
6131                 if ($alternate) {
6132                         print "<tr class=\"dark\">\n";
6133                 } else {
6134                         print "<tr class=\"light\">\n";
6135                 }
6136                 $alternate ^= 1;
6137                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6138                       format_author_html('td', \%co, 15, 5) .
6139                       "<td>" .
6140                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6141                                -class => "list subject"},
6142                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
6143                 my $comment = $co{'comment'};
6144                 foreach my $line (@$comment) {
6145                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
6146                                 my ($lead, $match, $trail) = ($1, $2, $3);
6147                                 $match = chop_str($match, 70, 5, 'center');
6148                                 my $contextlen = int((80 - length($match))/2);
6149                                 $contextlen = 30 if ($contextlen > 30);
6150                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
6151                                 $trail = chop_str($trail, $contextlen, 10, 'right');
6152
6153                                 $lead  = esc_html($lead);
6154                                 $match = esc_html($match);
6155                                 $trail = esc_html($trail);
6156
6157                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
6158                         }
6159                 }
6160                 print "</td>\n" .
6161                       "<td class=\"link\">" .
6162                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6163                       " | " .
6164                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
6165                       " | " .
6166                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6167                 print "</td>\n" .
6168                       "</tr>\n";
6169         }
6170         if (defined $extra) {
6171                 print "<tr>\n" .
6172                       "<td colspan=\"3\">$extra</td>\n" .
6173                       "</tr>\n";
6174         }
6175         print "</table>\n";
6176 }
6177
6178 ## ======================================================================
6179 ## ======================================================================
6180 ## actions
6181
6182 sub git_project_list {
6183         my $order = $input_params{'order'};
6184         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6185                 die_error(400, "Unknown order parameter");
6186         }
6187
6188         my @list = git_get_projects_list($project_filter, $strict_export);
6189         if (!@list) {
6190                 die_error(404, "No projects found");
6191         }
6192
6193         git_header_html();
6194         if (defined $home_text && -f $home_text) {
6195                 print "<div class=\"index_include\">\n";
6196                 insert_file($home_text);
6197                 print "</div>\n";
6198         }
6199
6200         git_project_search_form($searchtext, $search_use_regexp);
6201         git_project_list_body(\@list, $order);
6202         git_footer_html();
6203 }
6204
6205 sub git_forks {
6206         my $order = $input_params{'order'};
6207         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6208                 die_error(400, "Unknown order parameter");
6209         }
6210
6211         my $filter = $project;
6212         $filter =~ s/\.git$//;
6213         my @list = git_get_projects_list($filter);
6214         if (!@list) {
6215                 die_error(404, "No forks found");
6216         }
6217
6218         git_header_html();
6219         git_print_page_nav('','');
6220         git_print_header_div('summary', "$project forks");
6221         git_project_list_body(\@list, $order);
6222         git_footer_html();
6223 }
6224
6225 sub git_project_index {
6226         my @projects = git_get_projects_list($project_filter, $strict_export);
6227         if (!@projects) {
6228                 die_error(404, "No projects found");
6229         }
6230
6231         print $cgi->header(
6232                 -type => 'text/plain',
6233                 -charset => 'utf-8',
6234                 -content_disposition => 'inline; filename="index.aux"');
6235
6236         foreach my $pr (@projects) {
6237                 if (!exists $pr->{'owner'}) {
6238                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
6239                 }
6240
6241                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6242                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6243                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6244                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6245                 $path  =~ s/ /\+/g;
6246                 $owner =~ s/ /\+/g;
6247
6248                 print "$path $owner\n";
6249         }
6250 }
6251
6252 sub git_summary {
6253         my $descr = git_get_project_description($project) || "none";
6254         my %co = parse_commit("HEAD");
6255         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
6256         my $head = $co{'id'};
6257         my $remote_heads = gitweb_check_feature('remote_heads');
6258
6259         my $owner = git_get_project_owner($project);
6260
6261         my $refs = git_get_references();
6262         # These get_*_list functions return one more to allow us to see if
6263         # there are more ...
6264         my @taglist  = git_get_tags_list(16);
6265         my @headlist = git_get_heads_list(16);
6266         my %remotedata = $remote_heads ? git_get_remotes_list() : ();
6267         my @forklist;
6268         my $check_forks = gitweb_check_feature('forks');
6269
6270         if ($check_forks) {
6271                 # find forks of a project
6272                 my $filter = $project;
6273                 $filter =~ s/\.git$//;
6274                 @forklist = git_get_projects_list($filter);
6275                 # filter out forks of forks
6276                 @forklist = filter_forks_from_projects_list(\@forklist)
6277                         if (@forklist);
6278         }
6279
6280         git_header_html();
6281         git_print_page_nav('summary','', $head);
6282
6283         print "<div class=\"title\">&nbsp;</div>\n";
6284         print "<table class=\"projects_list\">\n" .
6285               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
6286               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
6287         if (defined $cd{'rfc2822'}) {
6288                 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6289                       "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
6290         }
6291
6292         # use per project git URL list in $projectroot/$project/cloneurl
6293         # or make project git URL from git base URL and project name
6294         my $url_tag = "URL";
6295         my @url_list = git_get_project_url_list($project);
6296         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6297         foreach my $git_url (@url_list) {
6298                 next unless $git_url;
6299                 print format_repo_url($url_tag, $git_url);
6300                 $url_tag = "";
6301         }
6302
6303         # Tag cloud
6304         my $show_ctags = gitweb_check_feature('ctags');
6305         if ($show_ctags) {
6306                 my $ctags = git_get_project_ctags($project);
6307                 if (%$ctags) {
6308                         # without ability to add tags, don't show if there are none
6309                         my $cloud = git_populate_project_tagcloud($ctags);
6310                         print "<tr id=\"metadata_ctags\">" .
6311                               "<td>content tags</td>" .
6312                               "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6313                               "</tr>\n";
6314                 }
6315         }
6316
6317         print "</table>\n";
6318
6319         # If XSS prevention is on, we don't include README.html.
6320         # TODO: Allow a readme in some safe format.
6321         if (!$prevent_xss && -s "$projectroot/$project/README.html") {
6322                 print "<div class=\"title\">readme</div>\n" .
6323                       "<div class=\"readme\">\n";
6324                 insert_file("$projectroot/$project/README.html");
6325                 print "\n</div>\n"; # class="readme"
6326         }
6327
6328         # we need to request one more than 16 (0..15) to check if
6329         # those 16 are all
6330         my @commitlist = $head ? parse_commits($head, 17) : ();
6331         if (@commitlist) {
6332                 git_print_header_div('shortlog');
6333                 git_shortlog_body(\@commitlist, 0, 15, $refs,
6334                                   $#commitlist <=  15 ? undef :
6335                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
6336         }
6337
6338         if (@taglist) {
6339                 git_print_header_div('tags');
6340                 git_tags_body(\@taglist, 0, 15,
6341                               $#taglist <=  15 ? undef :
6342                               $cgi->a({-href => href(action=>"tags")}, "..."));
6343         }
6344
6345         if (@headlist) {
6346                 git_print_header_div('heads');
6347                 git_heads_body(\@headlist, $head, 0, 15,
6348                                $#headlist <= 15 ? undef :
6349                                $cgi->a({-href => href(action=>"heads")}, "..."));
6350         }
6351
6352         if (%remotedata) {
6353                 git_print_header_div('remotes');
6354                 git_remotes_body(\%remotedata, 15, $head);
6355         }
6356
6357         if (@forklist) {
6358                 git_print_header_div('forks');
6359                 git_project_list_body(\@forklist, 'age', 0, 15,
6360                                       $#forklist <= 15 ? undef :
6361                                       $cgi->a({-href => href(action=>"forks")}, "..."),
6362                                       'no_header');
6363         }
6364
6365         git_footer_html();
6366 }
6367
6368 sub git_tag {
6369         my %tag = parse_tag($hash);
6370
6371         if (! %tag) {
6372                 die_error(404, "Unknown tag object");
6373         }
6374
6375         my $head = git_get_head_hash($project);
6376         git_header_html();
6377         git_print_page_nav('','', $head,undef,$head);
6378         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
6379         print "<div class=\"title_text\">\n" .
6380               "<table class=\"object_header\">\n" .
6381               "<tr>\n" .
6382               "<td>object</td>\n" .
6383               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6384                                $tag{'object'}) . "</td>\n" .
6385               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6386                                               $tag{'type'}) . "</td>\n" .
6387               "</tr>\n";
6388         if (defined($tag{'author'})) {
6389                 git_print_authorship_rows(\%tag, 'author');
6390         }
6391         print "</table>\n\n" .
6392               "</div>\n";
6393         print "<div class=\"page_body\">";
6394         my $comment = $tag{'comment'};
6395         foreach my $line (@$comment) {
6396                 chomp $line;
6397                 print esc_html($line, -nbsp=>1) . "<br/>\n";
6398         }
6399         print "</div>\n";
6400         git_footer_html();
6401 }
6402
6403 sub git_blame_common {
6404         my $format = shift || 'porcelain';
6405         if ($format eq 'porcelain' && $input_params{'javascript'}) {
6406                 $format = 'incremental';
6407                 $action = 'blame_incremental'; # for page title etc
6408         }
6409
6410         # permissions
6411         gitweb_check_feature('blame')
6412                 or die_error(403, "Blame view not allowed");
6413
6414         # error checking
6415         die_error(400, "No file name given") unless $file_name;
6416         $hash_base ||= git_get_head_hash($project);
6417         die_error(404, "Couldn't find base commit") unless $hash_base;
6418         my %co = parse_commit($hash_base)
6419                 or die_error(404, "Commit not found");
6420         my $ftype = "blob";
6421         if (!defined $hash) {
6422                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
6423                         or die_error(404, "Error looking up file");
6424         } else {
6425                 $ftype = git_get_type($hash);
6426                 if ($ftype !~ "blob") {
6427                         die_error(400, "Object is not a blob");
6428                 }
6429         }
6430
6431         my $fd;
6432         if ($format eq 'incremental') {
6433                 # get file contents (as base)
6434                 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
6435                         or die_error(500, "Open git-cat-file failed");
6436         } elsif ($format eq 'data') {
6437                 # run git-blame --incremental
6438                 open $fd, "-|", git_cmd(), "blame", "--incremental",
6439                         $hash_base, "--", $file_name
6440                         or die_error(500, "Open git-blame --incremental failed");
6441         } else {
6442                 # run git-blame --porcelain
6443                 open $fd, "-|", git_cmd(), "blame", '-p',
6444                         $hash_base, '--', $file_name
6445                         or die_error(500, "Open git-blame --porcelain failed");
6446         }
6447
6448         # incremental blame data returns early
6449         if ($format eq 'data') {
6450                 print $cgi->header(
6451                         -type=>"text/plain", -charset => "utf-8",
6452                         -status=> "200 OK");
6453                 local $| = 1; # output autoflush
6454                 while (my $line = <$fd>) {
6455                         print to_utf8($line);
6456                 }
6457                 close $fd
6458                         or print "ERROR $!\n";
6459
6460                 print 'END';
6461                 if (defined $t0 && gitweb_check_feature('timed')) {
6462                         print ' '.
6463                               tv_interval($t0, [ gettimeofday() ]).
6464                               ' '.$number_of_git_cmds;
6465                 }
6466                 print "\n";
6467
6468                 return;
6469         }
6470
6471         # page header
6472         git_header_html();
6473         my $formats_nav =
6474                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
6475                         "blob") .
6476                 " | ";
6477         if ($format eq 'incremental') {
6478                 $formats_nav .=
6479                         $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6480                                 "blame") . " (non-incremental)";
6481         } else {
6482                 $formats_nav .=
6483                         $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6484                                 "blame") . " (incremental)";
6485         }
6486         $formats_nav .=
6487                 " | " .
6488                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6489                         "history") .
6490                 " | " .
6491                 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
6492                         "HEAD");
6493         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6494         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6495         git_print_page_path($file_name, $ftype, $hash_base);
6496
6497         # page body
6498         if ($format eq 'incremental') {
6499                 print "<noscript>\n<div class=\"error\"><center><b>\n".
6500                       "This page requires JavaScript to run.\n Use ".
6501                       $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
6502                               'this page').
6503                       " instead.\n".
6504                       "</b></center></div>\n</noscript>\n";
6505
6506                 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6507         }
6508
6509         print qq!<div class="page_body">\n!;
6510         print qq!<div id="progress_info">... / ...</div>\n!
6511                 if ($format eq 'incremental');
6512         print qq!<table id="blame_table" class="blame" width="100%">\n!.
6513               #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6514               qq!<thead>\n!.
6515               qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6516               qq!</thead>\n!.
6517               qq!<tbody>\n!;
6518
6519         my @rev_color = qw(light dark);
6520         my $num_colors = scalar(@rev_color);
6521         my $current_color = 0;
6522
6523         if ($format eq 'incremental') {
6524                 my $color_class = $rev_color[$current_color];
6525
6526                 #contents of a file
6527                 my $linenr = 0;
6528         LINE:
6529                 while (my $line = <$fd>) {
6530                         chomp $line;
6531                         $linenr++;
6532
6533                         print qq!<tr id="l$linenr" class="$color_class">!.
6534                               qq!<td class="sha1"><a href=""> </a></td>!.
6535                               qq!<td class="linenr">!.
6536                               qq!<a class="linenr" href="">$linenr</a></td>!;
6537                         print qq!<td class="pre">! . esc_html($line) . "</td>\n";
6538                         print qq!</tr>\n!;
6539                 }
6540
6541         } else { # porcelain, i.e. ordinary blame
6542                 my %metainfo = (); # saves information about commits
6543
6544                 # blame data
6545         LINE:
6546                 while (my $line = <$fd>) {
6547                         chomp $line;
6548                         # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
6549                         # no <lines in group> for subsequent lines in group of lines
6550                         my ($full_rev, $orig_lineno, $lineno, $group_size) =
6551                            ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
6552                         if (!exists $metainfo{$full_rev}) {
6553                                 $metainfo{$full_rev} = { 'nprevious' => 0 };
6554                         }
6555                         my $meta = $metainfo{$full_rev};
6556                         my $data;
6557                         while ($data = <$fd>) {
6558                                 chomp $data;
6559                                 last if ($data =~ s/^\t//); # contents of line
6560                                 if ($data =~ /^(\S+)(?: (.*))?$/) {
6561                                         $meta->{$1} = $2 unless exists $meta->{$1};
6562                                 }
6563                                 if ($data =~ /^previous /) {
6564                                         $meta->{'nprevious'}++;
6565                                 }
6566                         }
6567                         my $short_rev = substr($full_rev, 0, 8);
6568                         my $author = $meta->{'author'};
6569                         my %date =
6570                                 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
6571                         my $date = $date{'iso-tz'};
6572                         if ($group_size) {
6573                                 $current_color = ($current_color + 1) % $num_colors;
6574                         }
6575                         my $tr_class = $rev_color[$current_color];
6576                         $tr_class .= ' boundary' if (exists $meta->{'boundary'});
6577                         $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
6578                         $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
6579                         print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
6580                         if ($group_size) {
6581                                 print "<td class=\"sha1\"";
6582                                 print " title=\"". esc_html($author) . ", $date\"";
6583                                 print " rowspan=\"$group_size\"" if ($group_size > 1);
6584                                 print ">";
6585                                 print $cgi->a({-href => href(action=>"commit",
6586                                                              hash=>$full_rev,
6587                                                              file_name=>$file_name)},
6588                                               esc_html($short_rev));
6589                                 if ($group_size >= 2) {
6590                                         my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
6591                                         if (@author_initials) {
6592                                                 print "<br />" .
6593                                                       esc_html(join('', @author_initials));
6594                                                 #           or join('.', ...)
6595                                         }
6596                                 }
6597                                 print "</td>\n";
6598                         }
6599                         # 'previous' <sha1 of parent commit> <filename at commit>
6600                         if (exists $meta->{'previous'} &&
6601                             $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
6602                                 $meta->{'parent'} = $1;
6603                                 $meta->{'file_parent'} = unquote($2);
6604                         }
6605                         my $linenr_commit =
6606                                 exists($meta->{'parent'}) ?
6607                                 $meta->{'parent'} : $full_rev;
6608                         my $linenr_filename =
6609                                 exists($meta->{'file_parent'}) ?
6610                                 $meta->{'file_parent'} : unquote($meta->{'filename'});
6611                         my $blamed = href(action => 'blame',
6612                                           file_name => $linenr_filename,
6613                                           hash_base => $linenr_commit);
6614                         print "<td class=\"linenr\">";
6615                         print $cgi->a({ -href => "$blamed#l$orig_lineno",
6616                                         -class => "linenr" },
6617                                       esc_html($lineno));
6618                         print "</td>";
6619                         print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
6620                         print "</tr>\n";
6621                 } # end while
6622
6623         }
6624
6625         # footer
6626         print "</tbody>\n".
6627               "</table>\n"; # class="blame"
6628         print "</div>\n";   # class="blame_body"
6629         close $fd
6630                 or print "Reading blob failed\n";
6631
6632         git_footer_html();
6633 }
6634
6635 sub git_blame {
6636         git_blame_common();
6637 }
6638
6639 sub git_blame_incremental {
6640         git_blame_common('incremental');
6641 }
6642
6643 sub git_blame_data {
6644         git_blame_common('data');
6645 }
6646
6647 sub git_tags {
6648         my $head = git_get_head_hash($project);
6649         git_header_html();
6650         git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
6651         git_print_header_div('summary', $project);
6652
6653         my @tagslist = git_get_tags_list();
6654         if (@tagslist) {
6655                 git_tags_body(\@tagslist);
6656         }
6657         git_footer_html();
6658 }
6659
6660 sub git_heads {
6661         my $head = git_get_head_hash($project);
6662         git_header_html();
6663         git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
6664         git_print_header_div('summary', $project);
6665
6666         my @headslist = git_get_heads_list();
6667         if (@headslist) {
6668                 git_heads_body(\@headslist, $head);
6669         }
6670         git_footer_html();
6671 }
6672
6673 # used both for single remote view and for list of all the remotes
6674 sub git_remotes {
6675         gitweb_check_feature('remote_heads')
6676                 or die_error(403, "Remote heads view is disabled");
6677
6678         my $head = git_get_head_hash($project);
6679         my $remote = $input_params{'hash'};
6680
6681         my $remotedata = git_get_remotes_list($remote);
6682         die_error(500, "Unable to get remote information") unless defined $remotedata;
6683
6684         unless (%$remotedata) {
6685                 die_error(404, defined $remote ?
6686                         "Remote $remote not found" :
6687                         "No remotes found");
6688         }
6689
6690         git_header_html(undef, undef, -action_extra => $remote);
6691         git_print_page_nav('', '',  $head, undef, $head,
6692                 format_ref_views($remote ? '' : 'remotes'));
6693
6694         fill_remote_heads($remotedata);
6695         if (defined $remote) {
6696                 git_print_header_div('remotes', "$remote remote for $project");
6697                 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
6698         } else {
6699                 git_print_header_div('summary', "$project remotes");
6700                 git_remotes_body($remotedata, undef, $head);
6701         }
6702
6703         git_footer_html();
6704 }
6705
6706 sub git_blob_plain {
6707         my $type = shift;
6708         my $expires;
6709
6710         if (!defined $hash) {
6711                 if (defined $file_name) {
6712                         my $base = $hash_base || git_get_head_hash($project);
6713                         $hash = git_get_hash_by_path($base, $file_name, "blob")
6714                                 or die_error(404, "Cannot find file");
6715                 } else {
6716                         die_error(400, "No file name defined");
6717                 }
6718         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6719                 # blobs defined by non-textual hash id's can be cached
6720                 $expires = "+1d";
6721         }
6722
6723         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6724                 or die_error(500, "Open git-cat-file blob '$hash' failed");
6725
6726         # content-type (can include charset)
6727         $type = blob_contenttype($fd, $file_name, $type);
6728
6729         # "save as" filename, even when no $file_name is given
6730         my $save_as = "$hash";
6731         if (defined $file_name) {
6732                 $save_as = $file_name;
6733         } elsif ($type =~ m/^text\//) {
6734                 $save_as .= '.txt';
6735         }
6736
6737         # With XSS prevention on, blobs of all types except a few known safe
6738         # ones are served with "Content-Disposition: attachment" to make sure
6739         # they don't run in our security domain.  For certain image types,
6740         # blob view writes an <img> tag referring to blob_plain view, and we
6741         # want to be sure not to break that by serving the image as an
6742         # attachment (though Firefox 3 doesn't seem to care).
6743         my $sandbox = $prevent_xss &&
6744                 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
6745
6746         # serve text/* as text/plain
6747         if ($prevent_xss &&
6748             ($type =~ m!^text/[a-z]+\b(.*)$! ||
6749              ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
6750                 my $rest = $1;
6751                 $rest = defined $rest ? $rest : '';
6752                 $type = "text/plain$rest";
6753         }
6754
6755         print $cgi->header(
6756                 -type => $type,
6757                 -expires => $expires,
6758                 -content_disposition =>
6759                         ($sandbox ? 'attachment' : 'inline')
6760                         . '; filename="' . $save_as . '"');
6761         local $/ = undef;
6762         binmode STDOUT, ':raw';
6763         print <$fd>;
6764         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6765         close $fd;
6766 }
6767
6768 sub git_blob {
6769         my $expires;
6770
6771         if (!defined $hash) {
6772                 if (defined $file_name) {
6773                         my $base = $hash_base || git_get_head_hash($project);
6774                         $hash = git_get_hash_by_path($base, $file_name, "blob")
6775                                 or die_error(404, "Cannot find file");
6776                 } else {
6777                         die_error(400, "No file name defined");
6778                 }
6779         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6780                 # blobs defined by non-textual hash id's can be cached
6781                 $expires = "+1d";
6782         }
6783
6784         my $have_blame = gitweb_check_feature('blame');
6785         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6786                 or die_error(500, "Couldn't cat $file_name, $hash");
6787         my $mimetype = blob_mimetype($fd, $file_name);
6788         # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6789         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6790                 close $fd;
6791                 return git_blob_plain($mimetype);
6792         }
6793         # we can have blame only for text/* mimetype
6794         $have_blame &&= ($mimetype =~ m!^text/!);
6795
6796         my $highlight = gitweb_check_feature('highlight');
6797         my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6798         $fd = run_highlighter($fd, $highlight, $syntax)
6799                 if $syntax;
6800
6801         git_header_html(undef, $expires);
6802         my $formats_nav = '';
6803         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6804                 if (defined $file_name) {
6805                         if ($have_blame) {
6806                                 $formats_nav .=
6807                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
6808                                                 "blame") .
6809                                         " | ";
6810                         }
6811                         $formats_nav .=
6812                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6813                                         "history") .
6814                                 " | " .
6815                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6816                                         "raw") .
6817                                 " | " .
6818                                 $cgi->a({-href => href(action=>"blob",
6819                                                        hash_base=>"HEAD", file_name=>$file_name)},
6820                                         "HEAD");
6821                 } else {
6822                         $formats_nav .=
6823                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6824                                         "raw");
6825                 }
6826                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6827                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6828         } else {
6829                 print "<div class=\"page_nav\">\n" .
6830                       "<br/><br/></div>\n" .
6831                       "<div class=\"title\">".esc_html($hash)."</div>\n";
6832         }
6833         git_print_page_path($file_name, "blob", $hash_base);
6834         print "<div class=\"page_body\">\n";
6835         if ($mimetype =~ m!^image/!) {
6836                 print qq!<img type="!.esc_attr($mimetype).qq!"!;
6837                 if ($file_name) {
6838                         print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6839                 }
6840                 print qq! src="! .
6841                       href(action=>"blob_plain", hash=>$hash,
6842                            hash_base=>$hash_base, file_name=>$file_name) .
6843                       qq!" />\n!;
6844         } else {
6845                 my $nr;
6846                 while (my $line = <$fd>) {
6847                         chomp $line;
6848                         $nr++;
6849                         $line = untabify($line);
6850                         printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6851                                $nr, esc_attr(href(-replay => 1)), $nr, $nr,
6852                                $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);
6853                 }
6854         }
6855         close $fd
6856                 or print "Reading blob failed.\n";
6857         print "</div>";
6858         git_footer_html();
6859 }
6860
6861 sub git_tree {
6862         if (!defined $hash_base) {
6863                 $hash_base = "HEAD";
6864         }
6865         if (!defined $hash) {
6866                 if (defined $file_name) {
6867                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6868                 } else {
6869                         $hash = $hash_base;
6870                 }
6871         }
6872         die_error(404, "No such tree") unless defined($hash);
6873
6874         my $show_sizes = gitweb_check_feature('show-sizes');
6875         my $have_blame = gitweb_check_feature('blame');
6876
6877         my @entries = ();
6878         {
6879                 local $/ = "\0";
6880                 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6881                         ($show_sizes ? '-l' : ()), @extra_options, $hash
6882                         or die_error(500, "Open git-ls-tree failed");
6883                 @entries = map { chomp; $_ } <$fd>;
6884                 close $fd
6885                         or die_error(404, "Reading tree failed");
6886         }
6887
6888         my $refs = git_get_references();
6889         my $ref = format_ref_marker($refs, $hash_base);
6890         git_header_html();
6891         my $basedir = '';
6892         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6893                 my @views_nav = ();
6894                 if (defined $file_name) {
6895                         push @views_nav,
6896                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6897                                         "history"),
6898                                 $cgi->a({-href => href(action=>"tree",
6899                                                        hash_base=>"HEAD", file_name=>$file_name)},
6900                                         "HEAD"),
6901                 }
6902                 my $snapshot_links = format_snapshot_links($hash);
6903                 if (defined $snapshot_links) {
6904                         # FIXME: Should be available when we have no hash base as well.
6905                         push @views_nav, $snapshot_links;
6906                 }
6907                 git_print_page_nav('tree','', $hash_base, undef, undef,
6908                                    join(' | ', @views_nav));
6909                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
6910         } else {
6911                 undef $hash_base;
6912                 print "<div class=\"page_nav\">\n";
6913                 print "<br/><br/></div>\n";
6914                 print "<div class=\"title\">".esc_html($hash)."</div>\n";
6915         }
6916         if (defined $file_name) {
6917                 $basedir = $file_name;
6918                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
6919                         $basedir .= '/';
6920                 }
6921                 git_print_page_path($file_name, 'tree', $hash_base);
6922         }
6923         print "<div class=\"page_body\">\n";
6924         print "<table class=\"tree\">\n";
6925         my $alternate = 1;
6926         # '..' (top directory) link if possible
6927         if (defined $hash_base &&
6928             defined $file_name && $file_name =~ m![^/]+$!) {
6929                 if ($alternate) {
6930                         print "<tr class=\"dark\">\n";
6931                 } else {
6932                         print "<tr class=\"light\">\n";
6933                 }
6934                 $alternate ^= 1;
6935
6936                 my $up = $file_name;
6937                 $up =~ s!/?[^/]+$!!;
6938                 undef $up unless $up;
6939                 # based on git_print_tree_entry
6940                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
6941                 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
6942                 print '<td class="list">';
6943                 print $cgi->a({-href => href(action=>"tree",
6944                                              hash_base=>$hash_base,
6945                                              file_name=>$up)},
6946                               "..");
6947                 print "</td>\n";
6948                 print "<td class=\"link\"></td>\n";
6949
6950                 print "</tr>\n";
6951         }
6952         foreach my $line (@entries) {
6953                 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
6954
6955                 if ($alternate) {
6956                         print "<tr class=\"dark\">\n";
6957                 } else {
6958                         print "<tr class=\"light\">\n";
6959                 }
6960                 $alternate ^= 1;
6961
6962                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
6963
6964                 print "</tr>\n";
6965         }
6966         print "</table>\n" .
6967               "</div>";
6968         git_footer_html();
6969 }
6970
6971 sub snapshot_name {
6972         my ($project, $hash) = @_;
6973
6974         # path/to/project.git  -> project
6975         # path/to/project/.git -> project
6976         my $name = to_utf8($project);
6977         $name =~ s,([^/])/*\.git$,$1,;
6978         $name = basename($name);
6979         # sanitize name
6980         $name =~ s/[[:cntrl:]]/?/g;
6981
6982         my $ver = $hash;
6983         if ($hash =~ /^[0-9a-fA-F]+$/) {
6984                 # shorten SHA-1 hash
6985                 my $full_hash = git_get_full_hash($project, $hash);
6986                 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6987                         $ver = git_get_short_hash($project, $hash);
6988                 }
6989         } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6990                 # tags don't need shortened SHA-1 hash
6991                 $ver = $1;
6992         } else {
6993                 # branches and other need shortened SHA-1 hash
6994                 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
6995                         $ver = $1;
6996                 }
6997                 $ver .= '-' . git_get_short_hash($project, $hash);
6998         }
6999         # in case of hierarchical branch names
7000         $ver =~ s!/!.!g;
7001
7002         # name = project-version_string
7003         $name = "$name-$ver";
7004
7005         return wantarray ? ($name, $name) : $name;
7006 }
7007
7008 sub git_snapshot {
7009         my $format = $input_params{'snapshot_format'};
7010         if (!@snapshot_fmts) {
7011                 die_error(403, "Snapshots not allowed");
7012         }
7013         # default to first supported snapshot format
7014         $format ||= $snapshot_fmts[0];
7015         if ($format !~ m/^[a-z0-9]+$/) {
7016                 die_error(400, "Invalid snapshot format parameter");
7017         } elsif (!exists($known_snapshot_formats{$format})) {
7018                 die_error(400, "Unknown snapshot format");
7019         } elsif ($known_snapshot_formats{$format}{'disabled'}) {
7020                 die_error(403, "Snapshot format not allowed");
7021         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
7022                 die_error(403, "Unsupported snapshot format");
7023         }
7024
7025         my $type = git_get_type("$hash^{}");
7026         if (!$type) {
7027                 die_error(404, 'Object does not exist');
7028         }  elsif ($type eq 'blob') {
7029                 die_error(400, 'Object is not a tree-ish');
7030         }
7031
7032         my ($name, $prefix) = snapshot_name($project, $hash);
7033         my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
7034         my $cmd = quote_command(
7035                 git_cmd(), 'archive',
7036                 "--format=$known_snapshot_formats{$format}{'format'}",
7037                 "--prefix=$prefix/", $hash);
7038         if (exists $known_snapshot_formats{$format}{'compressor'}) {
7039                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
7040         }
7041
7042         $filename =~ s/(["\\])/\\$1/g;
7043         print $cgi->header(
7044                 -type => $known_snapshot_formats{$format}{'type'},
7045                 -content_disposition => 'inline; filename="' . $filename . '"',
7046                 -status => '200 OK');
7047
7048         open my $fd, "-|", $cmd
7049                 or die_error(500, "Execute git-archive failed");
7050         binmode STDOUT, ':raw';
7051         print <$fd>;
7052         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7053         close $fd;
7054 }
7055
7056 sub git_log_generic {
7057         my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
7058
7059         my $head = git_get_head_hash($project);
7060         if (!defined $base) {
7061                 $base = $head;
7062         }
7063         if (!defined $page) {
7064                 $page = 0;
7065         }
7066         my $refs = git_get_references();
7067
7068         my $commit_hash = $base;
7069         if (defined $parent) {
7070                 $commit_hash = "$parent..$base";
7071         }
7072         my @commitlist =
7073                 parse_commits($commit_hash, 101, (100 * $page),
7074                               defined $file_name ? ($file_name, "--full-history") : ());
7075
7076         my $ftype;
7077         if (!defined $file_hash && defined $file_name) {
7078                 # some commits could have deleted file in question,
7079                 # and not have it in tree, but one of them has to have it
7080                 for (my $i = 0; $i < @commitlist; $i++) {
7081                         $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
7082                         last if defined $file_hash;
7083                 }
7084         }
7085         if (defined $file_hash) {
7086                 $ftype = git_get_type($file_hash);
7087         }
7088         if (defined $file_name && !defined $ftype) {
7089                 die_error(500, "Unknown type of object");
7090         }
7091         my %co;
7092         if (defined $file_name) {
7093                 %co = parse_commit($base)
7094                         or die_error(404, "Unknown commit object");
7095         }
7096
7097
7098         my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
7099         my $next_link = '';
7100         if ($#commitlist >= 100) {
7101                 $next_link =
7102                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
7103                                  -accesskey => "n", -title => "Alt-n"}, "next");
7104         }
7105         my $patch_max = gitweb_get_feature('patches');
7106         if ($patch_max && !defined $file_name) {
7107                 if ($patch_max < 0 || @commitlist <= $patch_max) {
7108                         $paging_nav .= " &sdot; " .
7109                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
7110                                         "patches");
7111                 }
7112         }
7113
7114         git_header_html();
7115         git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
7116         if (defined $file_name) {
7117                 git_print_header_div('commit', esc_html($co{'title'}), $base);
7118         } else {
7119                 git_print_header_div('summary', $project)
7120         }
7121         git_print_page_path($file_name, $ftype, $hash_base)
7122                 if (defined $file_name);
7123
7124         $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
7125                      $file_name, $file_hash, $ftype);
7126
7127         git_footer_html();
7128 }
7129
7130 sub git_log {
7131         git_log_generic('log', \&git_log_body,
7132                         $hash, $hash_parent);
7133 }
7134
7135 sub git_commit {
7136         $hash ||= $hash_base || "HEAD";
7137         my %co = parse_commit($hash)
7138             or die_error(404, "Unknown commit object");
7139
7140         my $parent  = $co{'parent'};
7141         my $parents = $co{'parents'}; # listref
7142
7143         # we need to prepare $formats_nav before any parameter munging
7144         my $formats_nav;
7145         if (!defined $parent) {
7146                 # --root commitdiff
7147                 $formats_nav .= '(initial)';
7148         } elsif (@$parents == 1) {
7149                 # single parent commit
7150                 $formats_nav .=
7151                         '(parent: ' .
7152                         $cgi->a({-href => href(action=>"commit",
7153                                                hash=>$parent)},
7154                                 esc_html(substr($parent, 0, 7))) .
7155                         ')';
7156         } else {
7157                 # merge commit
7158                 $formats_nav .=
7159                         '(merge: ' .
7160                         join(' ', map {
7161                                 $cgi->a({-href => href(action=>"commit",
7162                                                        hash=>$_)},
7163                                         esc_html(substr($_, 0, 7)));
7164                         } @$parents ) .
7165                         ')';
7166         }
7167         if (gitweb_check_feature('patches') && @$parents <= 1) {
7168                 $formats_nav .= " | " .
7169                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
7170                                 "patch");
7171         }
7172
7173         if (!defined $parent) {
7174                 $parent = "--root";
7175         }
7176         my @difftree;
7177         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
7178                 @diff_opts,
7179                 (@$parents <= 1 ? $parent : '-c'),
7180                 $hash, "--"
7181                 or die_error(500, "Open git-diff-tree failed");
7182         @difftree = map { chomp; $_ } <$fd>;
7183         close $fd or die_error(404, "Reading git-diff-tree failed");
7184
7185         # non-textual hash id's can be cached
7186         my $expires;
7187         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7188                 $expires = "+1d";
7189         }
7190         my $refs = git_get_references();
7191         my $ref = format_ref_marker($refs, $co{'id'});
7192
7193         git_header_html(undef, $expires);
7194         git_print_page_nav('commit', '',
7195                            $hash, $co{'tree'}, $hash,
7196                            $formats_nav);
7197
7198         if (defined $co{'parent'}) {
7199                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
7200         } else {
7201                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
7202         }
7203         print "<div class=\"title_text\">\n" .
7204               "<table class=\"object_header\">\n";
7205         git_print_authorship_rows(\%co);
7206         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
7207         print "<tr>" .
7208               "<td>tree</td>" .
7209               "<td class=\"sha1\">" .
7210               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7211                        class => "list"}, $co{'tree'}) .
7212               "</td>" .
7213               "<td class=\"link\">" .
7214               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7215                       "tree");
7216         my $snapshot_links = format_snapshot_links($hash);
7217         if (defined $snapshot_links) {
7218                 print " | " . $snapshot_links;
7219         }
7220         print "</td>" .
7221               "</tr>\n";
7222
7223         foreach my $par (@$parents) {
7224                 print "<tr>" .
7225                       "<td>parent</td>" .
7226                       "<td class=\"sha1\">" .
7227                       $cgi->a({-href => href(action=>"commit", hash=>$par),
7228                                class => "list"}, $par) .
7229                       "</td>" .
7230                       "<td class=\"link\">" .
7231                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
7232                       " | " .
7233                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
7234                       "</td>" .
7235                       "</tr>\n";
7236         }
7237         print "</table>".
7238               "</div>\n";
7239
7240         print "<div class=\"page_body\">\n";
7241         git_print_log($co{'comment'});
7242         print "</div>\n";
7243
7244         git_difftree_body(\@difftree, $hash, @$parents);
7245
7246         git_footer_html();
7247 }
7248
7249 sub git_object {
7250         # object is defined by:
7251         # - hash or hash_base alone
7252         # - hash_base and file_name
7253         my $type;
7254
7255         # - hash or hash_base alone
7256         if ($hash || ($hash_base && !defined $file_name)) {
7257                 my $object_id = $hash || $hash_base;
7258
7259                 open my $fd, "-|", quote_command(
7260                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
7261                         or die_error(404, "Object does not exist");
7262                 $type = <$fd>;
7263                 chomp $type;
7264                 close $fd
7265                         or die_error(404, "Object does not exist");
7266
7267         # - hash_base and file_name
7268         } elsif ($hash_base && defined $file_name) {
7269                 $file_name =~ s,/+$,,;
7270
7271                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
7272                         or die_error(404, "Base object does not exist");
7273
7274                 # here errors should not hapen
7275                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
7276                         or die_error(500, "Open git-ls-tree failed");
7277                 my $line = <$fd>;
7278                 close $fd;
7279
7280                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
7281                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
7282                         die_error(404, "File or directory for given base does not exist");
7283                 }
7284                 $type = $2;
7285                 $hash = $3;
7286         } else {
7287                 die_error(400, "Not enough information to find object");
7288         }
7289
7290         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7291                                           hash=>$hash, hash_base=>$hash_base,
7292                                           file_name=>$file_name),
7293                              -status => '302 Found');
7294 }
7295
7296 sub git_blobdiff {
7297         my $format = shift || 'html';
7298         my $diff_style = $input_params{'diff_style'} || 'inline';
7299
7300         my $fd;
7301         my @difftree;
7302         my %diffinfo;
7303         my $expires;
7304
7305         # preparing $fd and %diffinfo for git_patchset_body
7306         # new style URI
7307         if (defined $hash_base && defined $hash_parent_base) {
7308                 if (defined $file_name) {
7309                         # read raw output
7310                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7311                                 $hash_parent_base, $hash_base,
7312                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
7313                                 or die_error(500, "Open git-diff-tree failed");
7314                         @difftree = map { chomp; $_ } <$fd>;
7315                         close $fd
7316                                 or die_error(404, "Reading git-diff-tree failed");
7317                         @difftree
7318                                 or die_error(404, "Blob diff not found");
7319
7320                 } elsif (defined $hash &&
7321                          $hash =~ /[0-9a-fA-F]{40}/) {
7322                         # try to find filename from $hash
7323
7324                         # read filtered raw output
7325                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7326                                 $hash_parent_base, $hash_base, "--"
7327                                 or die_error(500, "Open git-diff-tree failed");
7328                         @difftree =
7329                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
7330                                 # $hash == to_id
7331                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7332                                 map { chomp; $_ } <$fd>;
7333                         close $fd
7334                                 or die_error(404, "Reading git-diff-tree failed");
7335                         @difftree
7336                                 or die_error(404, "Blob diff not found");
7337
7338                 } else {
7339                         die_error(400, "Missing one of the blob diff parameters");
7340                 }
7341
7342                 if (@difftree > 1) {
7343                         die_error(400, "Ambiguous blob diff specification");
7344                 }
7345
7346                 %diffinfo = parse_difftree_raw_line($difftree[0]);
7347                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
7348                 $file_name   ||= $diffinfo{'to_file'};
7349
7350                 $hash_parent ||= $diffinfo{'from_id'};
7351                 $hash        ||= $diffinfo{'to_id'};
7352
7353                 # non-textual hash id's can be cached
7354                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
7355                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
7356                         $expires = '+1d';
7357                 }
7358
7359                 # open patch output
7360                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7361                         '-p', ($format eq 'html' ? "--full-index" : ()),
7362                         $hash_parent_base, $hash_base,
7363                         "--", (defined $file_parent ? $file_parent : ()), $file_name
7364                         or die_error(500, "Open git-diff-tree failed");
7365         }
7366
7367         # old/legacy style URI -- not generated anymore since 1.4.3.
7368         if (!%diffinfo) {
7369                 die_error('404 Not Found', "Missing one of the blob diff parameters")
7370         }
7371
7372         # header
7373         if ($format eq 'html') {
7374                 my $formats_nav =
7375                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
7376                                 "raw");
7377                 $formats_nav .= diff_style_nav($diff_style);
7378                 git_header_html(undef, $expires);
7379                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7380                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7381                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7382                 } else {
7383                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
7384                         print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
7385                 }
7386                 if (defined $file_name) {
7387                         git_print_page_path($file_name, "blob", $hash_base);
7388                 } else {
7389                         print "<div class=\"page_path\"></div>\n";
7390                 }
7391
7392         } elsif ($format eq 'plain') {
7393                 print $cgi->header(
7394                         -type => 'text/plain',
7395                         -charset => 'utf-8',
7396                         -expires => $expires,
7397                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
7398
7399                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7400
7401         } else {
7402                 die_error(400, "Unknown blobdiff format");
7403         }
7404
7405         # patch
7406         if ($format eq 'html') {
7407                 print "<div class=\"page_body\">\n";
7408
7409                 git_patchset_body($fd, $diff_style,
7410                                   [ \%diffinfo ], $hash_base, $hash_parent_base);
7411                 close $fd;
7412
7413                 print "</div>\n"; # class="page_body"
7414                 git_footer_html();
7415
7416         } else {
7417                 while (my $line = <$fd>) {
7418                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7419                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
7420
7421                         print $line;
7422
7423                         last if $line =~ m!^\+\+\+!;
7424                 }
7425                 local $/ = undef;
7426                 print <$fd>;
7427                 close $fd;
7428         }
7429 }
7430
7431 sub git_blobdiff_plain {
7432         git_blobdiff('plain');
7433 }
7434
7435 # assumes that it is added as later part of already existing navigation,
7436 # so it returns "| foo | bar" rather than just "foo | bar"
7437 sub diff_style_nav {
7438         my ($diff_style, $is_combined) = @_;
7439         $diff_style ||= 'inline';
7440
7441         return "" if ($is_combined);
7442
7443         my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7444         my %styles = @styles;
7445         @styles =
7446                 @styles[ map { $_ * 2 } 0..$#styles/2 ];
7447
7448         return join '',
7449                 map { " | ".$_ }
7450                 map {
7451                         $_ eq $diff_style ? $styles{$_} :
7452                         $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7453                 } @styles;
7454 }
7455
7456 sub git_commitdiff {
7457         my %params = @_;
7458         my $format = $params{-format} || 'html';
7459         my $diff_style = $input_params{'diff_style'} || 'inline';
7460
7461         my ($patch_max) = gitweb_get_feature('patches');
7462         if ($format eq 'patch') {
7463                 die_error(403, "Patch view not allowed") unless $patch_max;
7464         }
7465
7466         $hash ||= $hash_base || "HEAD";
7467         my %co = parse_commit($hash)
7468             or die_error(404, "Unknown commit object");
7469
7470         # choose format for commitdiff for merge
7471         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
7472                 $hash_parent = '--cc';
7473         }
7474         # we need to prepare $formats_nav before almost any parameter munging
7475         my $formats_nav;
7476         if ($format eq 'html') {
7477                 $formats_nav =
7478                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
7479                                 "raw");
7480                 if ($patch_max && @{$co{'parents'}} <= 1) {
7481                         $formats_nav .= " | " .
7482                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7483                                         "patch");
7484                 }
7485                 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
7486
7487                 if (defined $hash_parent &&
7488                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
7489                         # commitdiff with two commits given
7490                         my $hash_parent_short = $hash_parent;
7491                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
7492                                 $hash_parent_short = substr($hash_parent, 0, 7);
7493                         }
7494                         $formats_nav .=
7495                                 ' (from';
7496                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
7497                                 if ($co{'parents'}[$i] eq $hash_parent) {
7498                                         $formats_nav .= ' parent ' . ($i+1);
7499                                         last;
7500                                 }
7501                         }
7502                         $formats_nav .= ': ' .
7503                                 $cgi->a({-href => href(-replay=>1,
7504                                                        hash=>$hash_parent, hash_base=>undef)},
7505                                         esc_html($hash_parent_short)) .
7506                                 ')';
7507                 } elsif (!$co{'parent'}) {
7508                         # --root commitdiff
7509                         $formats_nav .= ' (initial)';
7510                 } elsif (scalar @{$co{'parents'}} == 1) {
7511                         # single parent commit
7512                         $formats_nav .=
7513                                 ' (parent: ' .
7514                                 $cgi->a({-href => href(-replay=>1,
7515                                                        hash=>$co{'parent'}, hash_base=>undef)},
7516                                         esc_html(substr($co{'parent'}, 0, 7))) .
7517                                 ')';
7518                 } else {
7519                         # merge commit
7520                         if ($hash_parent eq '--cc') {
7521                                 $formats_nav .= ' | ' .
7522                                         $cgi->a({-href => href(-replay=>1,
7523                                                                hash=>$hash, hash_parent=>'-c')},
7524                                                 'combined');
7525                         } else { # $hash_parent eq '-c'
7526                                 $formats_nav .= ' | ' .
7527                                         $cgi->a({-href => href(-replay=>1,
7528                                                                hash=>$hash, hash_parent=>'--cc')},
7529                                                 'compact');
7530                         }
7531                         $formats_nav .=
7532                                 ' (merge: ' .
7533                                 join(' ', map {
7534                                         $cgi->a({-href => href(-replay=>1,
7535                                                                hash=>$_, hash_base=>undef)},
7536                                                 esc_html(substr($_, 0, 7)));
7537                                 } @{$co{'parents'}} ) .
7538                                 ')';
7539                 }
7540         }
7541
7542         my $hash_parent_param = $hash_parent;
7543         if (!defined $hash_parent_param) {
7544                 # --cc for multiple parents, --root for parentless
7545                 $hash_parent_param =
7546                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
7547         }
7548
7549         # read commitdiff
7550         my $fd;
7551         my @difftree;
7552         if ($format eq 'html') {
7553                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7554                         "--no-commit-id", "--patch-with-raw", "--full-index",
7555                         $hash_parent_param, $hash, "--"
7556                         or die_error(500, "Open git-diff-tree failed");
7557
7558                 while (my $line = <$fd>) {
7559                         chomp $line;
7560                         # empty line ends raw part of diff-tree output
7561                         last unless $line;
7562                         push @difftree, scalar parse_difftree_raw_line($line);
7563                 }
7564
7565         } elsif ($format eq 'plain') {
7566                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7567                         '-p', $hash_parent_param, $hash, "--"
7568                         or die_error(500, "Open git-diff-tree failed");
7569         } elsif ($format eq 'patch') {
7570                 # For commit ranges, we limit the output to the number of
7571                 # patches specified in the 'patches' feature.
7572                 # For single commits, we limit the output to a single patch,
7573                 # diverging from the git-format-patch default.
7574                 my @commit_spec = ();
7575                 if ($hash_parent) {
7576                         if ($patch_max > 0) {
7577                                 push @commit_spec, "-$patch_max";
7578                         }
7579                         push @commit_spec, '-n', "$hash_parent..$hash";
7580                 } else {
7581                         if ($params{-single}) {
7582                                 push @commit_spec, '-1';
7583                         } else {
7584                                 if ($patch_max > 0) {
7585                                         push @commit_spec, "-$patch_max";
7586                                 }
7587                                 push @commit_spec, "-n";
7588                         }
7589                         push @commit_spec, '--root', $hash;
7590                 }
7591                 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
7592                         '--encoding=utf8', '--stdout', @commit_spec
7593                         or die_error(500, "Open git-format-patch failed");
7594         } else {
7595                 die_error(400, "Unknown commitdiff format");
7596         }
7597
7598         # non-textual hash id's can be cached
7599         my $expires;
7600         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7601                 $expires = "+1d";
7602         }
7603
7604         # write commit message
7605         if ($format eq 'html') {
7606                 my $refs = git_get_references();
7607                 my $ref = format_ref_marker($refs, $co{'id'});
7608
7609                 git_header_html(undef, $expires);
7610                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
7611                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
7612                 print "<div class=\"title_text\">\n" .
7613                       "<table class=\"object_header\">\n";
7614                 git_print_authorship_rows(\%co);
7615                 print "</table>".
7616                       "</div>\n";
7617                 print "<div class=\"page_body\">\n";
7618                 if (@{$co{'comment'}} > 1) {
7619                         print "<div class=\"log\">\n";
7620                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
7621                         print "</div>\n"; # class="log"
7622                 }
7623
7624         } elsif ($format eq 'plain') {
7625                 my $refs = git_get_references("tags");
7626                 my $tagname = git_get_rev_name_tags($hash);
7627                 my $filename = basename($project) . "-$hash.patch";
7628
7629                 print $cgi->header(
7630                         -type => 'text/plain',
7631                         -charset => 'utf-8',
7632                         -expires => $expires,
7633                         -content_disposition => 'inline; filename="' . "$filename" . '"');
7634                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7635                 print "From: " . to_utf8($co{'author'}) . "\n";
7636                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
7637                 print "Subject: " . to_utf8($co{'title'}) . "\n";
7638
7639                 print "X-Git-Tag: $tagname\n" if $tagname;
7640                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7641
7642                 foreach my $line (@{$co{'comment'}}) {
7643                         print to_utf8($line) . "\n";
7644                 }
7645                 print "---\n\n";
7646         } elsif ($format eq 'patch') {
7647                 my $filename = basename($project) . "-$hash.patch";
7648
7649                 print $cgi->header(
7650                         -type => 'text/plain',
7651                         -charset => 'utf-8',
7652                         -expires => $expires,
7653                         -content_disposition => 'inline; filename="' . "$filename" . '"');
7654         }
7655
7656         # write patch
7657         if ($format eq 'html') {
7658                 my $use_parents = !defined $hash_parent ||
7659                         $hash_parent eq '-c' || $hash_parent eq '--cc';
7660                 git_difftree_body(\@difftree, $hash,
7661                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
7662                 print "<br/>\n";
7663
7664                 git_patchset_body($fd, $diff_style,
7665                                   \@difftree, $hash,
7666                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
7667                 close $fd;
7668                 print "</div>\n"; # class="page_body"
7669                 git_footer_html();
7670
7671         } elsif ($format eq 'plain') {
7672                 local $/ = undef;
7673                 print <$fd>;
7674                 close $fd
7675                         or print "Reading git-diff-tree failed\n";
7676         } elsif ($format eq 'patch') {
7677                 local $/ = undef;
7678                 print <$fd>;
7679                 close $fd
7680                         or print "Reading git-format-patch failed\n";
7681         }
7682 }
7683
7684 sub git_commitdiff_plain {
7685         git_commitdiff(-format => 'plain');
7686 }
7687
7688 # format-patch-style patches
7689 sub git_patch {
7690         git_commitdiff(-format => 'patch', -single => 1);
7691 }
7692
7693 sub git_patches {
7694         git_commitdiff(-format => 'patch');
7695 }
7696
7697 sub git_history {
7698         git_log_generic('history', \&git_history_body,
7699                         $hash_base, $hash_parent_base,
7700                         $file_name, $hash);
7701 }
7702
7703 sub git_search {
7704         $searchtype ||= 'commit';
7705
7706         # check if appropriate features are enabled
7707         gitweb_check_feature('search')
7708                 or die_error(403, "Search is disabled");
7709         if ($searchtype eq 'pickaxe') {
7710                 # pickaxe may take all resources of your box and run for several minutes
7711                 # with every query - so decide by yourself how public you make this feature
7712                 gitweb_check_feature('pickaxe')
7713                         or die_error(403, "Pickaxe search is disabled");
7714         }
7715         if ($searchtype eq 'grep') {
7716                 # grep search might be potentially CPU-intensive, too
7717                 gitweb_check_feature('grep')
7718                         or die_error(403, "Grep search is disabled");
7719         }
7720
7721         if (!defined $searchtext) {
7722                 die_error(400, "Text field is empty");
7723         }
7724         if (!defined $hash) {
7725                 $hash = git_get_head_hash($project);
7726         }
7727         my %co = parse_commit($hash);
7728         if (!%co) {
7729                 die_error(404, "Unknown commit object");
7730         }
7731         if (!defined $page) {
7732                 $page = 0;
7733         }
7734
7735         if ($searchtype eq 'commit' ||
7736             $searchtype eq 'author' ||
7737             $searchtype eq 'committer') {
7738                 git_search_message(%co);
7739         } elsif ($searchtype eq 'pickaxe') {
7740                 git_search_changes(%co);
7741         } elsif ($searchtype eq 'grep') {
7742                 git_search_files(%co);
7743         } else {
7744                 die_error(400, "Unknown search type");
7745         }
7746 }
7747
7748 sub git_search_help {
7749         git_header_html();
7750         git_print_page_nav('','', $hash,$hash,$hash);
7751         print <<EOT;
7752 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7753 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7754 the pattern entered is recognized as the POSIX extended
7755 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7756 insensitive).</p>
7757 <dl>
7758 <dt><b>commit</b></dt>
7759 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7760 EOT
7761         my $have_grep = gitweb_check_feature('grep');
7762         if ($have_grep) {
7763                 print <<EOT;
7764 <dt><b>grep</b></dt>
7765 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7766     a different one) are searched for the given pattern. On large trees, this search can take
7767 a while and put some strain on the server, so please use it with some consideration. Note that
7768 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7769 case-sensitive.</dd>
7770 EOT
7771         }
7772         print <<EOT;
7773 <dt><b>author</b></dt>
7774 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7775 <dt><b>committer</b></dt>
7776 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7777 EOT
7778         my $have_pickaxe = gitweb_check_feature('pickaxe');
7779         if ($have_pickaxe) {
7780                 print <<EOT;
7781 <dt><b>pickaxe</b></dt>
7782 <dd>All commits that caused the string to appear or disappear from any file (changes that
7783 added, removed or "modified" the string) will be listed. This search can take a while and
7784 takes a lot of strain on the server, so please use it wisely. Note that since you may be
7785 interested even in changes just changing the case as well, this search is case sensitive.</dd>
7786 EOT
7787         }
7788         print "</dl>\n";
7789         git_footer_html();
7790 }
7791
7792 sub git_shortlog {
7793         git_log_generic('shortlog', \&git_shortlog_body,
7794                         $hash, $hash_parent);
7795 }
7796
7797 ## ......................................................................
7798 ## feeds (RSS, Atom; OPML)
7799
7800 sub git_feed {
7801         my $format = shift || 'atom';
7802         my $have_blame = gitweb_check_feature('blame');
7803
7804         # Atom: http://www.atomenabled.org/developers/syndication/
7805         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7806         if ($format ne 'rss' && $format ne 'atom') {
7807                 die_error(400, "Unknown web feed format");
7808         }
7809
7810         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7811         my $head = $hash || 'HEAD';
7812         my @commitlist = parse_commits($head, 150, 0, $file_name);
7813
7814         my %latest_commit;
7815         my %latest_date;
7816         my $content_type = "application/$format+xml";
7817         if (defined $cgi->http('HTTP_ACCEPT') &&
7818                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7819                 # browser (feed reader) prefers text/xml
7820                 $content_type = 'text/xml';
7821         }
7822         if (defined($commitlist[0])) {
7823                 %latest_commit = %{$commitlist[0]};
7824                 my $latest_epoch = $latest_commit{'committer_epoch'};
7825                 %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
7826                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7827                 if (defined $if_modified) {
7828                         my $since;
7829                         if (eval { require HTTP::Date; 1; }) {
7830                                 $since = HTTP::Date::str2time($if_modified);
7831                         } elsif (eval { require Time::ParseDate; 1; }) {
7832                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7833                         }
7834                         if (defined $since && $latest_epoch <= $since) {
7835                                 print $cgi->header(
7836                                         -type => $content_type,
7837                                         -charset => 'utf-8',
7838                                         -last_modified => $latest_date{'rfc2822'},
7839                                         -status => '304 Not Modified');
7840                                 return;
7841                         }
7842                 }
7843                 print $cgi->header(
7844                         -type => $content_type,
7845                         -charset => 'utf-8',
7846                         -last_modified => $latest_date{'rfc2822'});
7847         } else {
7848                 print $cgi->header(
7849                         -type => $content_type,
7850                         -charset => 'utf-8');
7851         }
7852
7853         # Optimization: skip generating the body if client asks only
7854         # for Last-Modified date.
7855         return if ($cgi->request_method() eq 'HEAD');
7856
7857         # header variables
7858         my $title = "$site_name - $project/$action";
7859         my $feed_type = 'log';
7860         if (defined $hash) {
7861                 $title .= " - '$hash'";
7862                 $feed_type = 'branch log';
7863                 if (defined $file_name) {
7864                         $title .= " :: $file_name";
7865                         $feed_type = 'history';
7866                 }
7867         } elsif (defined $file_name) {
7868                 $title .= " - $file_name";
7869                 $feed_type = 'history';
7870         }
7871         $title .= " $feed_type";
7872         my $descr = git_get_project_description($project);
7873         if (defined $descr) {
7874                 $descr = esc_html($descr);
7875         } else {
7876                 $descr = "$project " .
7877                          ($format eq 'rss' ? 'RSS' : 'Atom') .
7878                          " feed";
7879         }
7880         my $owner = git_get_project_owner($project);
7881         $owner = esc_html($owner);
7882
7883         #header
7884         my $alt_url;
7885         if (defined $file_name) {
7886                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7887         } elsif (defined $hash) {
7888                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7889         } else {
7890                 $alt_url = href(-full=>1, action=>"summary");
7891         }
7892         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7893         if ($format eq 'rss') {
7894                 print <<XML;
7895 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7896 <channel>
7897 XML
7898                 print "<title>$title</title>\n" .
7899                       "<link>$alt_url</link>\n" .
7900                       "<description>$descr</description>\n" .
7901                       "<language>en</language>\n" .
7902                       # project owner is responsible for 'editorial' content
7903                       "<managingEditor>$owner</managingEditor>\n";
7904                 if (defined $logo || defined $favicon) {
7905                         # prefer the logo to the favicon, since RSS
7906                         # doesn't allow both
7907                         my $img = esc_url($logo || $favicon);
7908                         print "<image>\n" .
7909                               "<url>$img</url>\n" .
7910                               "<title>$title</title>\n" .
7911                               "<link>$alt_url</link>\n" .
7912                               "</image>\n";
7913                 }
7914                 if (%latest_date) {
7915                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7916                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7917                 }
7918                 print "<generator>gitweb v.$version/$git_version</generator>\n";
7919         } elsif ($format eq 'atom') {
7920                 print <<XML;
7921 <feed xmlns="http://www.w3.org/2005/Atom">
7922 XML
7923                 print "<title>$title</title>\n" .
7924                       "<subtitle>$descr</subtitle>\n" .
7925                       '<link rel="alternate" type="text/html" href="' .
7926                       $alt_url . '" />' . "\n" .
7927                       '<link rel="self" type="' . $content_type . '" href="' .
7928                       $cgi->self_url() . '" />' . "\n" .
7929                       "<id>" . href(-full=>1) . "</id>\n" .
7930                       # use project owner for feed author
7931                       "<author><name>$owner</name></author>\n";
7932                 if (defined $favicon) {
7933                         print "<icon>" . esc_url($favicon) . "</icon>\n";
7934                 }
7935                 if (defined $logo) {
7936                         # not twice as wide as tall: 72 x 27 pixels
7937                         print "<logo>" . esc_url($logo) . "</logo>\n";
7938                 }
7939                 if (! %latest_date) {
7940                         # dummy date to keep the feed valid until commits trickle in:
7941                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
7942                 } else {
7943                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
7944                 }
7945                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
7946         }
7947
7948         # contents
7949         for (my $i = 0; $i <= $#commitlist; $i++) {
7950                 my %co = %{$commitlist[$i]};
7951                 my $commit = $co{'id'};
7952                 # we read 150, we always show 30 and the ones more recent than 48 hours
7953                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
7954                         last;
7955                 }
7956                 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
7957
7958                 # get list of changed files
7959                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7960                         $co{'parent'} || "--root",
7961                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
7962                         or next;
7963                 my @difftree = map { chomp; $_ } <$fd>;
7964                 close $fd
7965                         or next;
7966
7967                 # print element (entry, item)
7968                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7969                 if ($format eq 'rss') {
7970                         print "<item>\n" .
7971                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
7972                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
7973                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7974                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7975                               "<link>$co_url</link>\n" .
7976                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
7977                               "<content:encoded>" .
7978                               "<![CDATA[\n";
7979                 } elsif ($format eq 'atom') {
7980                         print "<entry>\n" .
7981                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7982                               "<updated>$cd{'iso-8601'}</updated>\n" .
7983                               "<author>\n" .
7984                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
7985                         if ($co{'author_email'}) {
7986                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
7987                         }
7988                         print "</author>\n" .
7989                               # use committer for contributor
7990                               "<contributor>\n" .
7991                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7992                         if ($co{'committer_email'}) {
7993                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7994                         }
7995                         print "</contributor>\n" .
7996                               "<published>$cd{'iso-8601'}</published>\n" .
7997                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
7998                               "<id>$co_url</id>\n" .
7999                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
8000                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
8001                 }
8002                 my $comment = $co{'comment'};
8003                 print "<pre>\n";
8004                 foreach my $line (@$comment) {
8005                         $line = esc_html($line);
8006                         print "$line\n";
8007                 }
8008                 print "</pre><ul>\n";
8009                 foreach my $difftree_line (@difftree) {
8010                         my %difftree = parse_difftree_raw_line($difftree_line);
8011                         next if !$difftree{'from_id'};
8012
8013                         my $file = $difftree{'file'} || $difftree{'to_file'};
8014
8015                         print "<li>" .
8016                               "[" .
8017                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
8018                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
8019                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
8020                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
8021                                       -title => "diff"}, 'D');
8022                         if ($have_blame) {
8023                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
8024                                                              file_name=>$file, hash_base=>$commit),
8025                                               -title => "blame"}, 'B');
8026                         }
8027                         # if this is not a feed of a file history
8028                         if (!defined $file_name || $file_name ne $file) {
8029                                 print $cgi->a({-href => href(-full=>1, action=>"history",
8030                                                              file_name=>$file, hash=>$commit),
8031                                               -title => "history"}, 'H');
8032                         }
8033                         $file = esc_path($file);
8034                         print "] ".
8035                               "$file</li>\n";
8036                 }
8037                 if ($format eq 'rss') {
8038                         print "</ul>]]>\n" .
8039                               "</content:encoded>\n" .
8040                               "</item>\n";
8041                 } elsif ($format eq 'atom') {
8042                         print "</ul>\n</div>\n" .
8043                               "</content>\n" .
8044                               "</entry>\n";
8045                 }
8046         }
8047
8048         # end of feed
8049         if ($format eq 'rss') {
8050                 print "</channel>\n</rss>\n";
8051         } elsif ($format eq 'atom') {
8052                 print "</feed>\n";
8053         }
8054 }
8055
8056 sub git_rss {
8057         git_feed('rss');
8058 }
8059
8060 sub git_atom {
8061         git_feed('atom');
8062 }
8063
8064 sub git_opml {
8065         my @list = git_get_projects_list($project_filter, $strict_export);
8066         if (!@list) {
8067                 die_error(404, "No projects found");
8068         }
8069
8070         print $cgi->header(
8071                 -type => 'text/xml',
8072                 -charset => 'utf-8',
8073                 -content_disposition => 'inline; filename="opml.xml"');
8074
8075         my $title = esc_html($site_name);
8076         my $filter = " within subdirectory ";
8077         if (defined $project_filter) {
8078                 $filter .= esc_html($project_filter);
8079         } else {
8080                 $filter = "";
8081         }
8082         print <<XML;
8083 <?xml version="1.0" encoding="utf-8"?>
8084 <opml version="1.0">
8085 <head>
8086   <title>$title OPML Export$filter</title>
8087 </head>
8088 <body>
8089 <outline text="git RSS feeds">
8090 XML
8091
8092         foreach my $pr (@list) {
8093                 my %proj = %$pr;
8094                 my $head = git_get_head_hash($proj{'path'});
8095                 if (!defined $head) {
8096                         next;
8097                 }
8098                 $git_dir = "$projectroot/$proj{'path'}";
8099                 my %co = parse_commit($head);
8100                 if (!%co) {
8101                         next;
8102                 }
8103
8104                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
8105                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
8106                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
8107                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
8108         }
8109         print <<XML;
8110 </outline>
8111 </body>
8112 </opml>
8113 XML
8114 }