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