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