Tart up the Makefile.PL a little
[catagits/Gitalist.git] / lib / gitweb.pm
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 package gitweb;
10
11 use strict;
12 use warnings;
13 use CGI qw(:standard :escapeHTML -nosticky);
14 use CGI::Util qw(unescape);
15 use CGI::Carp qw(fatalsToBrowser);
16 use Encode;
17 use Fcntl ':mode';
18 use File::Find qw();
19 use File::Basename qw(basename);
20 use FindBin;
21 binmode STDOUT, ':utf8';
22
23 BEGIN {
24         CGI->compile();
25 }
26
27 use vars qw(
28         $cgi $version $my_url $my_uri $base_url $path_info $GIT $projectroot
29         $project_maxdepth $home_link $home_link_str $site_header
30         $home_text $site_footer @stylesheets
31         $logo_url $logo_label $logo_url $logo_label $projects_list
32         $projects_list_description_width $default_projects_order
33         $export_ok $export_auth_hook $strict_export @git_base_url_list
34         $default_blob_plain_mimetype $default_text_plain_charset
35         $mimetypes_file $fallback_encoding @diff_opts $prevent_xss
36         %known_snapshot_formats %known_snapshot_format_aliases %feature
37         $GITWEB_CONFIG $GITWEB_CONFIG $GITWEB_CONFIG_SYSTEM $git_version
38         %input_params @cgi_param_mapping %cgi_param_mapping %actions
39         %allowed_options $action $project $file_name $file_parent $hash
40         $hash_parent $hash_base @extra_options $hash_parent_base $page
41         $searchtype $search_use_regexp $searchtext $search_regexp $git_dir
42         @snapshot_fmts
43
44         $c
45 );
46
47 sub main {
48         our $c   = shift;
49
50         our $cgi = new CGI;
51         our $version = "1.6.3.3";
52         our $my_url = $cgi->url();
53         our $my_uri = $cgi->url(-absolute => 1);
54
55         # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
56         # needed and used only for URLs with nonempty PATH_INFO
57         our $base_url = $my_url;
58
59         # When the script is used as DirectoryIndex, the URL does not contain the name
60         # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
61         # have to do it ourselves. We make $path_info global because it's also used
62         # later on.
63         #
64         # Another issue with the script being the DirectoryIndex is that the resulting
65         # $my_url data is not the full script URL: this is good, because we want
66         # generated links to keep implying the script name if it wasn't explicitly
67         # indicated in the URL we're handling, but it means that $my_url cannot be used
68         # as base URL.
69         # Therefore, if we needed to strip PATH_INFO, then we know that we have
70         # to build the base URL ourselves:
71         our $path_info = $ENV{"PATH_INFO"};
72         if ($path_info) {
73                 if ($my_url =~ s,\Q$path_info\E$,, &&
74                     $my_uri =~ s,\Q$path_info\E$,, &&
75                     defined $ENV{'SCRIPT_NAME'}) {
76                         $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
77                 }
78         }
79
80         # core git executable to use
81         # this can just be "git" if your webserver has a sensible PATH
82         our $GIT = `which git`;
83         chomp($GIT);
84
85         # absolute fs-path which will be prepended to the project path
86         #our $projectroot = "/pub/scm";
87
88         # target of the home link on top of all pages
89         our $home_link = $my_uri || "/";
90
91         # string of the home link on top of all pages
92         our $home_link_str = "Project Gitalist";
93
94         # filename of html text to include at top of each page
95         our $site_header = "";
96         # html text to include at home page
97         our $home_text = "indextext.html";
98         # filename of html text to include at bottom of each page
99         our $site_footer = "";
100
101         # URI of stylesheets
102         our @stylesheets = ("gitweb.css");
103
104         # URI and label (title) of GIT logo link
105         our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
106         our $logo_label = "git documentation";
107
108         # source of projects list
109         our $projectroot = our $projects_list = $c->config->{projectroot};
110
111         # the width (in characters) of the projects list "Description" column
112         our $projects_list_description_width = 25;
113
114         # default order of projects list
115         # valid values are none, project, descr, owner, and age
116         our $default_projects_order = "project";
117
118         # show repository only if this file exists
119         # (only effective if this variable evaluates to true)
120         our $export_ok = "";
121
122         # show repository only if this subroutine returns true
123         # when given the path to the project, for example:
124         #    sub { return -e "$_[0]/git-daemon-export-ok"; }
125         our $export_auth_hook = undef;
126
127         # only allow viewing of repositories also shown on the overview page
128         our $strict_export = "";
129
130         # list of git base URLs used for URL to where fetch project from,
131         # i.e. full URL is "$git_base_url/$project"
132         our @git_base_url_list = grep { $_ ne '' } ("");
133
134         # default blob_plain mimetype and default charset for text/plain blob
135         our $default_blob_plain_mimetype = 'text/plain';
136         our $default_text_plain_charset  = undef;
137
138         # file to use for guessing MIME types before trying /etc/mime.types
139         # (relative to the current git repository)
140         our $mimetypes_file = undef;
141
142         # assume this charset if line contains non-UTF-8 characters;
143         # it should be valid encoding (see Encoding::Supported(3pm) for list),
144         # for which encoding all byte sequences are valid, for example
145         # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
146         # could be even 'utf-8' for the old behavior)
147         our $fallback_encoding = 'latin1';
148
149         # rename detection options for git-diff and git-diff-tree
150         # - default is '-M', with the cost proportional to
151         #   (number of removed files) * (number of new files).
152         # - more costly is '-C' (which implies '-M'), with the cost proportional to
153         #   (number of changed files + number of removed files) * (number of new files)
154         # - even more costly is '-C', '--find-copies-harder' with cost
155         #   (number of files in the original tree) * (number of new files)
156         # - one might want to include '-B' option, e.g. '-B', '-M'
157         our @diff_opts = ('-M'); # taken from git_commit
158
159         # Disables features that would allow repository owners to inject script into
160         # the gitweb domain.
161         our $prevent_xss = 0;
162
163         # information about snapshot formats that gitweb is capable of serving
164         our %known_snapshot_formats = (
165                 # name => {
166                 #       'display' => display name,
167                 #       'type' => mime type,
168                 #       'suffix' => filename suffix,
169                 #       'format' => --format for git-archive,
170                 #       'compressor' => [compressor command and arguments]
171                 #                       (array reference, optional)}
172                 #
173                 'tgz' => {
174                         'display' => 'tar.gz',
175                         'type' => 'application/x-gzip',
176                         'suffix' => '.tar.gz',
177                         'format' => 'tar',
178                         'compressor' => ['gzip']},
179
180                 'tbz2' => {
181                         'display' => 'tar.bz2',
182                         'type' => 'application/x-bzip2',
183                         'suffix' => '.tar.bz2',
184                         'format' => 'tar',
185                         'compressor' => ['bzip2']},
186
187                 'zip' => {
188                         'display' => 'zip',
189                         'type' => 'application/x-zip',
190                         'suffix' => '.zip',
191                         'format' => 'zip'},
192         );
193
194         # Aliases so we understand old gitweb.snapshot values in repository
195         # configuration.
196         our %known_snapshot_format_aliases = (
197                 'gzip'  => 'tgz',
198                 'bzip2' => 'tbz2',
199
200                 # backward compatibility: legacy gitweb config support
201                 'x-gzip' => undef, 'gz' => undef,
202                 'x-bzip2' => undef, 'bz2' => undef,
203                 'x-zip' => undef, '' => undef,
204         );
205
206         my $feature_bool = sub {
207                 my $key = shift;
208                 my ($val) = git_get_project_config($key, '--bool');
209
210                 if (!defined $val) {
211                         return ($_[0]);
212                 } elsif ($val eq 'true') {
213                         return (1);
214                 } elsif ($val eq 'false') {
215                         return (0);
216                 }
217         };
218
219         my $feature_snapshot = sub {
220                 my (@fmts) = @_;
221
222                 my ($val) = git_get_project_config('snapshot');
223
224                 if ($val) {
225                         @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
226                 }
227
228                 return @fmts;
229         };
230
231         my $feature_patches = sub {
232                 my @val = (git_get_project_config('patches', '--int'));
233
234                 if (@val) {
235                         return @val;
236                 }
237
238                 return ($_[0]);
239         };
240
241
242         # You define site-wide feature defaults here; override them with
243         # $GITWEB_CONFIG as necessary.
244         our %feature = (
245                 # feature => {
246                 #       'sub' => feature-sub (subroutine),
247                 #       'override' => allow-override (boolean),
248                 #       'default' => [ default options...] (array reference)}
249                 #
250                 # if feature is overridable (it means that allow-override has true value),
251                 # then feature-sub will be called with default options as parameters;
252                 # return value of feature-sub indicates if to enable specified feature
253                 #
254                 # if there is no 'sub' key (no feature-sub), then feature cannot be
255                 # overriden
256                 #
257                 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
258                 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
259                 # is enabled
260
261                 # Enable the 'blame' blob view, showing the last commit that modified
262                 # each line in the file. This can be very CPU-intensive.
263
264                 # To enable system wide have in $GITWEB_CONFIG
265                 # $feature{'blame'}{'default'} = [1];
266                 # To have project specific config enable override in $GITWEB_CONFIG
267                 # $feature{'blame'}{'override'} = 1;
268                 # and in project config gitweb.blame = 0|1;
269                 'blame' => {
270                         'sub' => sub { &$feature_bool('blame', @_) },
271                         'override' => 0,
272                         'default' => [0]},
273
274                 # Enable the 'snapshot' link, providing a compressed archive of any
275                 # tree. This can potentially generate high traffic if you have large
276                 # project.
277
278                 # Value is a list of formats defined in %known_snapshot_formats that
279                 # you wish to offer.
280                 # To disable system wide have in $GITWEB_CONFIG
281                 # $feature{'snapshot'}{'default'} = [];
282                 # To have project specific config enable override in $GITWEB_CONFIG
283                 # $feature{'snapshot'}{'override'} = 1;
284                 # and in project config, a comma-separated list of formats or "none"
285                 # to disable.  Example: gitweb.snapshot = tbz2,zip;
286                 'snapshot' => {
287                         'sub' => $feature_snapshot,
288                         'override' => 0,
289                         'default' => ['tgz']},
290
291                 # Enable text search, which will list the commits which match author,
292                 # committer or commit text to a given string.  Enabled by default.
293                 # Project specific override is not supported.
294                 'search' => {
295                         'override' => 0,
296                         'default' => [1]},
297
298                 # Enable grep search, which will list the files in currently selected
299                 # tree containing the given string. Enabled by default. This can be
300                 # potentially CPU-intensive, of course.
301
302                 # To enable system wide have in $GITWEB_CONFIG
303                 # $feature{'grep'}{'default'} = [1];
304                 # To have project specific config enable override in $GITWEB_CONFIG
305                 # $feature{'grep'}{'override'} = 1;
306                 # and in project config gitweb.grep = 0|1;
307                 'grep' => {
308                         'sub' => sub { &$feature_bool('grep', @_) },
309                         'override' => 0,
310                         'default' => [1]},
311
312                 # Enable the pickaxe search, which will list the commits that modified
313                 # a given string in a file. This can be practical and quite faster
314                 # alternative to 'blame', but still potentially CPU-intensive.
315
316                 # To enable system wide have in $GITWEB_CONFIG
317                 # $feature{'pickaxe'}{'default'} = [1];
318                 # To have project specific config enable override in $GITWEB_CONFIG
319                 # $feature{'pickaxe'}{'override'} = 1;
320                 # and in project config gitweb.pickaxe = 0|1;
321                 'pickaxe' => {
322                         'sub' => sub { &$feature_bool('pickaxe', @_) },
323                         'override' => 0,
324                         'default' => [1]},
325
326                 # Make gitweb use an alternative format of the URLs which can be
327                 # more readable and natural-looking: project name is embedded
328                 # directly in the path and the query string contains other
329                 # auxiliary information. All gitweb installations recognize
330                 # URL in either format; this configures in which formats gitweb
331                 # generates links.
332
333                 # To enable system wide have in $GITWEB_CONFIG
334                 # $feature{'pathinfo'}{'default'} = [1];
335                 # Project specific override is not supported.
336
337                 # Note that you will need to change the default location of CSS,
338                 # favicon, logo and possibly other files to an absolute URL. Also,
339                 # if gitweb.cgi serves as your indexfile, you will need to force
340                 # $my_uri to contain the script name in your $GITWEB_CONFIG.
341                 'pathinfo' => {
342                         'override' => 0,
343                         'default' => [0]},
344
345                 # Make gitweb consider projects in project root subdirectories
346                 # to be forks of existing projects. Given project $projname.git,
347                 # projects matching $projname/*.git will not be shown in the main
348                 # projects list, instead a '+' mark will be added to $projname
349                 # there and a 'forks' view will be enabled for the project, listing
350                 # all the forks. If project list is taken from a file, forks have
351                 # to be listed after the main project.
352
353                 # To enable system wide have in $GITWEB_CONFIG
354                 # $feature{'forks'}{'default'} = [1];
355                 # Project specific override is not supported.
356                 'forks' => {
357                         'override' => 0,
358                         'default' => [0]},
359
360                 # Insert custom links to the action bar of all project pages.
361                 # This enables you mainly to link to third-party scripts integrating
362                 # into gitweb; e.g. git-browser for graphical history representation
363                 # or custom web-based repository administration interface.
364
365                 # The 'default' value consists of a list of triplets in the form
366                 # (label, link, position) where position is the label after which
367                 # to insert the link and link is a format string where %n expands
368                 # to the project name, %f to the project path within the filesystem,
369                 # %h to the current hash (h gitweb parameter) and %b to the current
370                 # hash base (hb gitweb parameter); %% expands to %.
371
372                 # To enable system wide have in $GITWEB_CONFIG e.g.
373                 # $feature{'actions'}{'default'} = [('graphiclog',
374                 #       '/git-browser/by-commit.html?r=%n', 'summary')];
375                 # Project specific override is not supported.
376                 'actions' => {
377                         'override' => 0,
378                         'default' => []},
379
380                 # Allow gitweb scan project content tags described in ctags/
381                 # of project repository, and display the popular Web 2.0-ish
382                 # "tag cloud" near the project list. Note that this is something
383                 # COMPLETELY different from the normal Git tags.
384
385                 # gitweb by itself can show existing tags, but it does not handle
386                 # tagging itself; you need an external application for that.
387                 # For an example script, check Girocco's cgi/tagproj.cgi.
388                 # You may want to install the HTML::TagCloud Perl module to get
389                 # a pretty tag cloud instead of just a list of tags.
390
391                 # To enable system wide have in $GITWEB_CONFIG
392                 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
393                 # Project specific override is not supported.
394                 'ctags' => {
395                         'override' => 0,
396                         'default' => [0]},
397
398                 # The maximum number of patches in a patchset generated in patch
399                 # view. Set this to 0 or undef to disable patch view, or to a
400                 # negative number to remove any limit.
401
402                 # To disable system wide have in $GITWEB_CONFIG
403                 # $feature{'patches'}{'default'} = [0];
404                 # To have project specific config enable override in $GITWEB_CONFIG
405                 # $feature{'patches'}{'override'} = 1;
406                 # and in project config gitweb.patches = 0|n;
407                 # where n is the maximum number of patches allowed in a patchset.
408                 'patches' => {
409                         'sub' => $feature_patches,
410                         'override' => 0,
411                         'default' => [16]},
412         );
413
414         our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl";
415         if (-e $GITWEB_CONFIG) {
416                 do $GITWEB_CONFIG;
417         } else {
418                 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "$FindBin::Bin/../gitweb.conf";
419                 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
420         }
421
422         # version of the core git binary
423         our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
424
425         # ======================================================================
426         # input validation and dispatch
427
428         # input parameters can be collected from a variety of sources (presently, CGI
429         # and PATH_INFO), so we define an %input_params hash that collects them all
430         # together during validation: this allows subsequent uses (e.g. href()) to be
431         # agnostic of the parameter origin
432
433         our %input_params = ();
434
435         # input parameters are stored with the long parameter name as key. This will
436         # also be used in the href subroutine to convert parameters to their CGI
437         # equivalent, and since the href() usage is the most frequent one, we store
438         # the name -> CGI key mapping here, instead of the reverse.
439         #
440         # XXX: Warning: If you touch this, check the search form for updating,
441         # too.
442
443         our @cgi_param_mapping = (
444                 project => "p",
445                 action => "a",
446                 file_name => "f",
447                 file_parent => "fp",
448                 hash => "h",
449                 hash_parent => "hp",
450                 hash_base => "hb",
451                 hash_parent_base => "hpb",
452                 page => "pg",
453                 order => "o",
454                 searchtext => "s",
455                 searchtype => "st",
456                 snapshot_format => "sf",
457                 extra_options => "opt",
458                 search_use_regexp => "sr",
459         );
460         our %cgi_param_mapping = @cgi_param_mapping;
461
462         # we will also need to know the possible actions, for validation
463         our %actions = (
464                 "blame" => \&git_blame,
465                 "blobdiff" => \&git_blobdiff,
466                 "blobdiff_plain" => \&git_blobdiff_plain,
467                 "blob" => \&git_blob,
468                 "blob_plain" => \&git_blob_plain,
469                 "commitdiff" => \&git_commitdiff,
470                 "commitdiff_plain" => \&git_commitdiff_plain,
471                 "commit" => \&git_commit,
472                 "forks" => \&git_forks,
473                 "heads" => \&git_heads,
474                 "history" => \&git_history,
475                 "log" => \&git_log,
476                 "patch" => \&git_patch,
477                 "patches" => \&git_patches,
478                 "rss" => \&git_rss,
479                 "atom" => \&git_atom,
480                 "search" => \&git_search,
481                 "search_help" => \&git_search_help,
482                 "shortlog" => \&git_shortlog,
483                 "summary" => \&git_summary,
484                 "tag" => \&git_tag,
485                 "tags" => \&git_tags,
486                 "tree" => \&git_tree,
487                 "snapshot" => \&git_snapshot,
488                 "object" => \&git_object,
489                 # those below don't need $project
490                 "opml" => \&git_opml,
491                 "project_list" => \&git_project_list,
492                 "project_index" => \&git_project_index,
493         );
494
495         # finally, we have the hash of allowed extra_options for the commands that
496         # allow them
497         our %allowed_options = (
498                 "--no-merges" => [ qw(rss atom log shortlog history) ],
499         );
500
501         # fill %input_params with the CGI parameters. All values except for 'opt'
502         # should be single values, but opt can be an array. We should probably
503         # build an array of parameters that can be multi-valued, but since for the time
504         # being it's only this one, we just single it out
505         while (my ($name, $symbol) = each %cgi_param_mapping) {
506                 if ($symbol eq 'opt') {
507                         $input_params{$name} = [ $c->req->param($symbol) ];
508                 } else {
509                         $input_params{$name} = $c->req->param($symbol);
510                 }
511         }
512
513         # now read PATH_INFO and update the parameter list for missing parameters
514         my $evaluate_path_info = sub {
515                 return if defined $input_params{'project'};
516                 return if !$path_info;
517                 $path_info =~ s,^/+,,;
518                 return if !$path_info;
519
520                 # find which part of PATH_INFO is project
521                 my $project = $path_info;
522                 $project =~ s,/+$,,;
523                 while ($project && !check_head_link("$projectroot/$project")) {
524                         $project =~ s,/*[^/]*$,,;
525                 }
526                 return unless $project;
527                 $input_params{'project'} = $project;
528
529                 # do not change any parameters if an action is given using the query string
530                 return if $input_params{'action'};
531                 $path_info =~ s,^\Q$project\E/*,,;
532
533                 # next, check if we have an action
534                 my $action = $path_info;
535                 $action =~ s,/.*$,,;
536                 if (exists $actions{$action}) {
537                         $path_info =~ s,^$action/*,,;
538                         $input_params{'action'} = $action;
539                 }
540
541                 # list of actions that want hash_base instead of hash, but can have no
542                 # pathname (f) parameter
543                 my @wants_base = (
544                         'tree',
545                         'history',
546                 );
547
548                 # we want to catch
549                 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
550                 my ($parentrefname, $parentpathname, $refname, $pathname) =
551                         ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
552
553                 # first, analyze the 'current' part
554                 if (defined $pathname) {
555                         # we got "branch:filename" or "branch:dir/"
556                         # we could use git_get_type(branch:pathname), but:
557                         # - it needs $git_dir
558                         # - it does a git() call
559                         # - the convention of terminating directories with a slash
560                         #   makes it superfluous
561                         # - embedding the action in the PATH_INFO would make it even
562                         #   more superfluous
563                         $pathname =~ s,^/+,,;
564                         if (!$pathname || substr($pathname, -1) eq "/") {
565                                 $input_params{'action'} ||= "tree";
566                                 $pathname =~ s,/$,,;
567                         } else {
568                                 # the default action depends on whether we had parent info
569                                 # or not
570                                 if ($parentrefname) {
571                                         $input_params{'action'} ||= "blobdiff_plain";
572                                 } else {
573                                         $input_params{'action'} ||= "blob_plain";
574                                 }
575                         }
576                         $input_params{'hash_base'} ||= $refname;
577                         $input_params{'file_name'} ||= $pathname;
578                 } elsif (defined $refname) {
579                         # we got "branch". In this case we have to choose if we have to
580                         # set hash or hash_base.
581                         #
582                         # Most of the actions without a pathname only want hash to be
583                         # set, except for the ones specified in @wants_base that want
584                         # hash_base instead. It should also be noted that hand-crafted
585                         # links having 'history' as an action and no pathname or hash
586                         # set will fail, but that happens regardless of PATH_INFO.
587                         $input_params{'action'} ||= "shortlog";
588                         if (grep { $_ eq $input_params{'action'} } @wants_base) {
589                                 $input_params{'hash_base'} ||= $refname;
590                         } else {
591                                 $input_params{'hash'} ||= $refname;
592                         }
593                 }
594
595                 # next, handle the 'parent' part, if present
596                 if (defined $parentrefname) {
597                         # a missing pathspec defaults to the 'current' filename, allowing e.g.
598                         # someproject/blobdiff/oldrev..newrev:/filename
599                         if ($parentpathname) {
600                                 $parentpathname =~ s,^/+,,;
601                                 $parentpathname =~ s,/$,,;
602                                 $input_params{'file_parent'} ||= $parentpathname;
603                         } else {
604                                 $input_params{'file_parent'} ||= $input_params{'file_name'};
605                         }
606                         # we assume that hash_parent_base is wanted if a path was specified,
607                         # or if the action wants hash_base instead of hash
608                         if (defined $input_params{'file_parent'} ||
609                                 grep { $_ eq $input_params{'action'} } @wants_base) {
610                                 $input_params{'hash_parent_base'} ||= $parentrefname;
611                         } else {
612                                 $input_params{'hash_parent'} ||= $parentrefname;
613                         }
614                 }
615
616                 # for the snapshot action, we allow URLs in the form
617                 # $project/snapshot/$hash.ext
618                 # where .ext determines the snapshot and gets removed from the
619                 # passed $refname to provide the $hash.
620                 #
621                 # To be able to tell that $refname includes the format extension, we
622                 # require the following two conditions to be satisfied:
623                 # - the hash input parameter MUST have been set from the $refname part
624                 #   of the URL (i.e. they must be equal)
625                 # - the snapshot format MUST NOT have been defined already (e.g. from
626                 #   CGI parameter sf)
627                 # It's also useless to try any matching unless $refname has a dot,
628                 # so we check for that too
629                 if (defined $input_params{'action'} &&
630                         $input_params{'action'} eq 'snapshot' &&
631                         defined $refname && index($refname, '.') != -1 &&
632                         $refname eq $input_params{'hash'} &&
633                         !defined $input_params{'snapshot_format'}) {
634                         # We loop over the known snapshot formats, checking for
635                         # extensions. Allowed extensions are both the defined suffix
636                         # (which includes the initial dot already) and the snapshot
637                         # format key itself, with a prepended dot
638                         while (my ($fmt, $opt) = each %known_snapshot_formats) {
639                                 my $hash = $refname;
640                                 my $sfx;
641                                 $hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//;
642                                 next unless $sfx = $1;
643                                 # a valid suffix was found, so set the snapshot format
644                                 # and reset the hash parameter
645                                 $input_params{'snapshot_format'} = $fmt;
646                                 $input_params{'hash'} = $hash;
647                                 # we also set the format suffix to the one requested
648                                 # in the URL: this way a request for e.g. .tgz returns
649                                 # a .tgz instead of a .tar.gz
650                                 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
651                                 last;
652                         }
653                 }
654         };
655
656         &$evaluate_path_info();
657
658         gitweb_validate_setup();
659
660         return $actions{$action};
661 }
662
663 sub gitweb_validate_setup {
664         our $action = $input_params{'action'};
665         if (defined $action) {
666                 if (!validate_action($action)) {
667                         die_error(400, "Invalid action parameter");
668                 }
669         }
670
671         # parameters which are pathnames
672         our $project = $input_params{'project'};
673         if (defined $project) {
674                 if (!validate_project($project)) {
675                         undef $project;
676                         die_error(404, "No such project");
677                 }
678         }
679
680         our $file_name = $input_params{'file_name'};
681         if (defined $file_name) {
682                 if (!validate_pathname($file_name)) {
683                         die_error(400, "Invalid file parameter");
684                 }
685         }
686
687         our $file_parent = $input_params{'file_parent'};
688         if (defined $file_parent) {
689                 if (!validate_pathname($file_parent)) {
690                         die_error(400, "Invalid file parent parameter");
691                 }
692         }
693
694         # parameters which are refnames
695         our $hash = $input_params{'hash'};
696         if (defined $hash) {
697                 if (!validate_refname($hash)) {
698                         die_error(400, "Invalid hash parameter");
699                 }
700         }
701
702         our $hash_parent = $input_params{'hash_parent'};
703         if (defined $hash_parent) {
704                 if (!validate_refname($hash_parent)) {
705                         die_error(400, "Invalid hash parent parameter");
706                 }
707         }
708
709         our $hash_base = $input_params{'hash_base'};
710         if (defined $hash_base) {
711                 if (!validate_refname($hash_base)) {
712                         die_error(400, "Invalid hash base parameter");
713                 }
714         }
715
716         our @extra_options = @{$input_params{'extra_options'}};
717         # @extra_options is always defined, since it can only be (currently) set from
718         # CGI, and $c->req->param() returns the empty array in array context if the param
719         # is not set
720         foreach my $opt (@extra_options) {
721                 if (not exists $allowed_options{$opt}) {
722                         die_error(400, "Invalid option parameter");
723                 }
724                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
725                         die_error(400, "Invalid option parameter for this action");
726                 }
727         }
728
729         our $hash_parent_base = $input_params{'hash_parent_base'};
730         if (defined $hash_parent_base) {
731                 if (!validate_refname($hash_parent_base)) {
732                         die_error(400, "Invalid hash parent base parameter");
733                 }
734         }
735
736         # other parameters
737         our $page = $input_params{'page'};
738         if (defined $page) {
739                 if ($page =~ m/[^0-9]/) {
740                         die_error(400, "Invalid page parameter");
741                 }
742         }
743
744         our $searchtype = $input_params{'searchtype'};
745         if (defined $searchtype) {
746                 if ($searchtype =~ m/[^a-z]/) {
747                         die_error(400, "Invalid searchtype parameter");
748                 }
749         }
750
751         our $search_use_regexp = $input_params{'search_use_regexp'};
752
753         our $searchtext = $input_params{'searchtext'};
754         our $search_regexp;
755         if (defined $searchtext) {
756                 if (length($searchtext) < 2) {
757                         die_error(403, "At least two characters are required for search parameter");
758                 }
759                 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
760         }
761
762         # path to the current git repository
763         our $git_dir;
764         $git_dir = "$projectroot/$project" if $project;
765
766         # process alternate names for backward compatibility
767         # filter out unsupported (unknown) snapshot formats
768         my $filter_snapshot_fmts = sub {
769                 my @fmts = @_;
770
771                 @fmts = map {
772                         exists $known_snapshot_format_aliases{$_} ?
773                                $known_snapshot_format_aliases{$_} : $_} @fmts;
774                 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
775
776         };
777         # list of supported snapshot formats
778         our @snapshot_fmts = gitweb_get_feature('snapshot');
779         @snapshot_fmts = &$filter_snapshot_fmts(@snapshot_fmts);
780
781         # dispatch
782         if (!defined $action) {
783                 if (defined $hash) {
784                         $action = git_get_type($hash);
785                 } elsif (defined $hash_base && defined $file_name) {
786                         $action = git_get_type("$hash_base:$file_name");
787                 } elsif (defined $project) {
788                         $action = 'summary';
789                 } else {
790                         $action = 'project_list';
791                 }
792         }
793         if (!defined($actions{$action})) {
794                 die_error(400, "Unknown action");
795         }
796         if ($action !~ m/^(opml|project_list|project_index)$/ &&
797             !$project) {
798                 die_error(400, "Project needed");
799         }
800 }
801
802 sub gitweb_get_feature {
803         my ($name) = @_;
804         return unless exists $feature{$name};
805         my ($sub, $override, @defaults) = (
806                 $feature{$name}{'sub'},
807                 $feature{$name}{'override'},
808                 @{$feature{$name}{'default'}});
809         if (!$override) { return @defaults; }
810         if (!defined $sub) {
811                 warn "feature $name is not overrideable";
812                 return @defaults;
813         }
814         return $sub->(@defaults);
815 }
816
817 # A wrapper to check if a given feature is enabled.
818 # With this, you can say
819 #
820 #   my $bool_feat = gitweb_check_feature('bool_feat');
821 #   gitweb_check_feature('bool_feat') or somecode;
822 #
823 # instead of
824 #
825 #   my ($bool_feat) = gitweb_get_feature('bool_feat');
826 #   (gitweb_get_feature('bool_feat'))[0] or somecode;
827 #
828 sub gitweb_check_feature {
829         return (gitweb_get_feature(@_))[0];
830 }
831
832 # checking HEAD file with -e is fragile if the repository was
833 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
834 # and then pruned.
835 sub check_head_link {
836         my ($dir) = @_;
837         my $headfile = "$dir/HEAD";
838         return ((-e $headfile) ||
839                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
840 }
841
842 sub check_export_ok {
843         my ($dir) = @_;
844         return (check_head_link($dir) &&
845                 (!$export_ok || -e "$dir/$export_ok") &&
846                 (!$export_auth_hook || $export_auth_hook->($dir)));
847 }
848
849
850 ## ======================================================================
851 ## action links
852
853 sub href (%) {
854         my %params = @_;
855         # default is to use -absolute url() i.e. $my_uri
856         my $href = $params{-full} ? $my_url : $my_uri;
857
858         $params{'project'} = $project unless exists $params{'project'};
859
860         if ($params{-replay}) {
861                 while (my ($name, $symbol) = each %cgi_param_mapping) {
862                         if (!exists $params{$name}) {
863                                 $params{$name} = $input_params{$name};
864                         }
865                 }
866         }
867
868         my $use_pathinfo = gitweb_check_feature('pathinfo');
869         if ($use_pathinfo and defined $params{'project'}) {
870                 # try to put as many parameters as possible in PATH_INFO:
871                 #   - project name
872                 #   - action
873                 #   - hash_parent or hash_parent_base:/file_parent
874                 #   - hash or hash_base:/filename
875                 #   - the snapshot_format as an appropriate suffix
876
877                 # When the script is the root DirectoryIndex for the domain,
878                 # $href here would be something like http://gitweb.example.com/
879                 # Thus, we strip any trailing / from $href, to spare us double
880                 # slashes in the final URL
881                 $href =~ s,/$,,;
882
883                 # Then add the project name, if present
884                 $href .= "/".esc_url($params{'project'});
885                 delete $params{'project'};
886
887                 # since we destructively absorb parameters, we keep this
888                 # boolean that remembers if we're handling a snapshot
889                 my $is_snapshot = $params{'action'} eq 'snapshot';
890
891                 # Summary just uses the project path URL, any other action is
892                 # added to the URL
893                 if (defined $params{'action'}) {
894                         $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
895                         delete $params{'action'};
896                 }
897
898                 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
899                 # stripping nonexistent or useless pieces
900                 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
901                         || $params{'hash_parent'} || $params{'hash'});
902                 if (defined $params{'hash_base'}) {
903                         if (defined $params{'hash_parent_base'}) {
904                                 $href .= esc_url($params{'hash_parent_base'});
905                                 # skip the file_parent if it's the same as the file_name
906                                 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
907                                 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
908                                         $href .= ":/".esc_url($params{'file_parent'});
909                                         delete $params{'file_parent'};
910                                 }
911                                 $href .= "..";
912                                 delete $params{'hash_parent'};
913                                 delete $params{'hash_parent_base'};
914                         } elsif (defined $params{'hash_parent'}) {
915                                 $href .= esc_url($params{'hash_parent'}). "..";
916                                 delete $params{'hash_parent'};
917                         }
918
919                         $href .= esc_url($params{'hash_base'});
920                         if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
921                                 $href .= ":/".esc_url($params{'file_name'});
922                                 delete $params{'file_name'};
923                         }
924                         delete $params{'hash'};
925                         delete $params{'hash_base'};
926                 } elsif (defined $params{'hash'}) {
927                         $href .= esc_url($params{'hash'});
928                         delete $params{'hash'};
929                 }
930
931                 # If the action was a snapshot, we can absorb the
932                 # snapshot_format parameter too
933                 if ($is_snapshot) {
934                         my $fmt = $params{'snapshot_format'};
935                         # snapshot_format should always be defined when href()
936                         # is called, but just in case some code forgets, we
937                         # fall back to the default
938                         $fmt ||= $snapshot_fmts[0];
939                         $href .= $known_snapshot_formats{$fmt}{'suffix'};
940                         delete $params{'snapshot_format'};
941                 }
942         }
943
944         # now encode the parameters explicitly
945         my @result = ();
946         for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
947                 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
948                 if (defined $params{$name}) {
949                         if (ref($params{$name}) eq "ARRAY") {
950                                 foreach my $par (@{$params{$name}}) {
951                                         push @result, $symbol . "=" . esc_param($par);
952                                 }
953                         } else {
954                                 push @result, $symbol . "=" . esc_param($params{$name});
955                         }
956                 }
957         }
958         $href .= "?" . join(';', @result) if scalar @result;
959
960         return $href;
961 }
962
963
964 ## ======================================================================
965 ## validation, quoting/unquoting and escaping
966
967 sub validate_action {
968         my $input = shift || return undef;
969         return undef unless exists $actions{$input};
970         return $input;
971 }
972
973 sub validate_project {
974         my $input = shift || return undef;
975         if (!validate_pathname($input) ||
976                 !(-d "$projectroot/$input") ||
977                 !check_export_ok("$projectroot/$input") ||
978                 ($strict_export && !project_in_list($input))) {
979                 return undef;
980         } else {
981                 return $input;
982         }
983 }
984
985 sub validate_pathname {
986         my $input = shift || return undef;
987
988         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
989         # at the beginning, at the end, and between slashes.
990         # also this catches doubled slashes
991         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
992                 return undef;
993         }
994         # no null characters
995         if ($input =~ m!\0!) {
996                 return undef;
997         }
998         return $input;
999 }
1000
1001 sub validate_refname {
1002         my $input = shift || return undef;
1003
1004         # textual hashes are O.K.
1005         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1006                 return $input;
1007         }
1008         # it must be correct pathname
1009         $input = validate_pathname($input)
1010                 or return undef;
1011         # restrictions on ref name according to git-check-ref-format
1012         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1013                 return undef;
1014         }
1015         return $input;
1016 }
1017
1018 # quote unsafe chars, but keep the slash, even when it's not
1019 # correct, but quoted slashes look too horrible in bookmarks
1020 sub esc_param {
1021         my $str = shift;
1022         $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
1023         $str =~ s/\+/%2B/g;
1024         $str =~ s/ /\+/g;
1025         return $str;
1026 }
1027
1028 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1029 sub esc_url {
1030         my $str = shift;
1031         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1032         $str =~ s/\+/%2B/g;
1033         $str =~ s/ /\+/g;
1034         return $str;
1035 }
1036
1037 # replace invalid utf8 character with SUBSTITUTION sequence
1038 sub esc_html ($;%) {
1039         my $str = shift;
1040         my %opts = @_;
1041
1042         $str = to_utf8($str);
1043         $str = $cgi->escapeHTML($str);
1044         if ($opts{'-nbsp'}) {
1045                 $str =~ s/ /&nbsp;/g;
1046         }
1047         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1048         return $str;
1049 }
1050
1051 # quote control characters and escape filename to HTML
1052 sub esc_path {
1053         my $str = shift;
1054         my %opts = @_;
1055
1056         $str = to_utf8($str);
1057         $str = $cgi->escapeHTML($str);
1058         if ($opts{'-nbsp'}) {
1059                 $str =~ s/ /&nbsp;/g;
1060         }
1061         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1062         return $str;
1063 }
1064
1065 # Make control characters "printable", using character escape codes (CEC)
1066 sub quot_cec {
1067         my $cntrl = shift;
1068         my %opts = @_;
1069         my %es = ( # character escape codes, aka escape sequences
1070                 "\t" => '\t',   # tab            (HT)
1071                 "\n" => '\n',   # line feed      (LF)
1072                 "\r" => '\r',   # carrige return (CR)
1073                 "\f" => '\f',   # form feed      (FF)
1074                 "\b" => '\b',   # backspace      (BS)
1075                 "\a" => '\a',   # alarm (bell)   (BEL)
1076                 "\e" => '\e',   # escape         (ESC)
1077                 "\013" => '\v', # vertical tab   (VT)
1078                 "\000" => '\0', # nul character  (NUL)
1079         );
1080         my $chr = ( (exists $es{$cntrl})
1081                     ? $es{$cntrl}
1082                     : sprintf('\%2x', ord($cntrl)) );
1083         if ($opts{-nohtml}) {
1084                 return $chr;
1085         } else {
1086                 return "<span class=\"cntrl\">$chr</span>";
1087         }
1088 }
1089
1090 # Alternatively use unicode control pictures codepoints,
1091 # Unicode "printable representation" (PR)
1092 sub quot_upr {
1093         my $cntrl = shift;
1094         my %opts = @_;
1095
1096         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1097         if ($opts{-nohtml}) {
1098                 return $chr;
1099         } else {
1100                 return "<span class=\"cntrl\">$chr</span>";
1101         }
1102 }
1103
1104 # git may return quoted and escaped filenames
1105 sub unquote {
1106         my $str = shift;
1107
1108         sub unq {
1109                 my $seq = shift;
1110                 my %es = ( # character escape codes, aka escape sequences
1111                         't' => "\t",   # tab            (HT, TAB)
1112                         'n' => "\n",   # newline        (NL)
1113                         'r' => "\r",   # return         (CR)
1114                         'f' => "\f",   # form feed      (FF)
1115                         'b' => "\b",   # backspace      (BS)
1116                         'a' => "\a",   # alarm (bell)   (BEL)
1117                         'e' => "\e",   # escape         (ESC)
1118                         'v' => "\013", # vertical tab   (VT)
1119                 );
1120
1121                 if ($seq =~ m/^[0-7]{1,3}$/) {
1122                         # octal char sequence
1123                         return chr(oct($seq));
1124                 } elsif (exists $es{$seq}) {
1125                         # C escape sequence, aka character escape code
1126                         return $es{$seq};
1127                 }
1128                 # quoted ordinary character
1129                 return $seq;
1130         }
1131
1132         if ($str =~ m/^"(.*)"$/) {
1133                 # needs unquoting
1134                 $str = $1;
1135                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1136         }
1137         return $str;
1138 }
1139
1140 # escape tabs (convert tabs to spaces)
1141 sub untabify {
1142         my $line = shift;
1143
1144         while ((my $pos = index($line, "\t")) != -1) {
1145                 if (my $count = (8 - ($pos % 8))) {
1146                         my $spaces = ' ' x $count;
1147                         $line =~ s/\t/$spaces/;
1148                 }
1149         }
1150
1151         return $line;
1152 }
1153
1154 sub project_in_list {
1155         my $project = shift;
1156         my @list = git_get_projects_list();
1157         return @list && scalar(grep { $_->{'path'} eq $project } @list);
1158 }
1159
1160 # decode sequences of octets in utf8 into Perl's internal form,
1161 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
1162 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1163 sub to_utf8 {
1164         my $str = shift;
1165         if (utf8::valid($str)) {
1166                 utf8::decode($str);
1167                 return $str;
1168         } else {
1169                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1170         }
1171 }
1172
1173 ## ----------------------------------------------------------------------
1174 ## HTML aware string manipulation
1175
1176 # Try to chop given string on a word boundary between position
1177 # $len and $len+$add_len. If there is no word boundary there,
1178 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1179 # (marking chopped part) would be longer than given string.
1180 sub chop_str {
1181         my $str = shift;
1182         my $len = shift;
1183         my $add_len = shift || 10;
1184         my $where = shift || 'right'; # 'left' | 'center' | 'right'
1185
1186         # Make sure perl knows it is utf8 encoded so we don't
1187         # cut in the middle of a utf8 multibyte char.
1188         $str = to_utf8($str);
1189
1190         # allow only $len chars, but don't cut a word if it would fit in $add_len
1191         # if it doesn't fit, cut it if it's still longer than the dots we would add
1192         # remove chopped character entities entirely
1193
1194         # when chopping in the middle, distribute $len into left and right part
1195         # return early if chopping wouldn't make string shorter
1196         if ($where eq 'center') {
1197                 return $str if ($len + 5 >= length($str)); # filler is length 5
1198                 $len = int($len/2);
1199         } else {
1200                 return $str if ($len + 4 >= length($str)); # filler is length 4
1201         }
1202
1203         # regexps: ending and beginning with word part up to $add_len
1204         my $endre = qr/.{$len}\w{0,$add_len}/;
1205         my $begre = qr/\w{0,$add_len}.{$len}/;
1206
1207         if ($where eq 'left') {
1208                 $str =~ m/^(.*?)($begre)$/;
1209                 my ($lead, $body) = ($1, $2);
1210                 if (length($lead) > 4) {
1211                         $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1212                         $lead = " ...";
1213                 }
1214                 return "$lead$body";
1215
1216         } elsif ($where eq 'center') {
1217                 $str =~ m/^($endre)(.*)$/;
1218                 my ($left, $str)  = ($1, $2);
1219                 $str =~ m/^(.*?)($begre)$/;
1220                 my ($mid, $right) = ($1, $2);
1221                 if (length($mid) > 5) {
1222                         $left  =~ s/&[^;]*$//;
1223                         $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1224                         $mid = " ... ";
1225                 }
1226                 return "$left$mid$right";
1227
1228         } else {
1229                 $str =~ m/^($endre)(.*)$/;
1230                 my $body = $1;
1231                 my $tail = $2;
1232                 if (length($tail) > 4) {
1233                         $body =~ s/&[^;]*$//;
1234                         $tail = "... ";
1235                 }
1236                 return "$body$tail";
1237         }
1238 }
1239
1240 # takes the same arguments as chop_str, but also wraps a <span> around the
1241 # result with a title attribute if it does get chopped. Additionally, the
1242 # string is HTML-escaped.
1243 sub chop_and_escape_str {
1244         my ($str) = @_;
1245
1246         my $chopped = chop_str(@_);
1247         if ($chopped eq $str) {
1248                 return esc_html($chopped);
1249         } else {
1250                 $str =~ s/([[:cntrl:]])/?/g;
1251                 return $cgi->span({-title=>$str}, esc_html($chopped));
1252         }
1253 }
1254
1255 ## ----------------------------------------------------------------------
1256 ## functions returning short strings
1257
1258 # CSS class for given age value (in seconds)
1259 sub age_class {
1260         my $age = shift;
1261
1262         if (!defined $age) {
1263                 return "noage";
1264         } elsif ($age < 60*60*2) {
1265                 return "age0";
1266         } elsif ($age < 60*60*24*2) {
1267                 return "age1";
1268         } else {
1269                 return "age2";
1270         }
1271 }
1272
1273 # convert age in seconds to "nn units ago" string
1274 sub age_string {
1275         my $age = shift;
1276         my $age_str;
1277
1278         if ($age > 60*60*24*365*2) {
1279                 $age_str = (int $age/60/60/24/365);
1280                 $age_str .= " years ago";
1281         } elsif ($age > 60*60*24*(365/12)*2) {
1282                 $age_str = int $age/60/60/24/(365/12);
1283                 $age_str .= " months ago";
1284         } elsif ($age > 60*60*24*7*2) {
1285                 $age_str = int $age/60/60/24/7;
1286                 $age_str .= " weeks ago";
1287         } elsif ($age > 60*60*24*2) {
1288                 $age_str = int $age/60/60/24;
1289                 $age_str .= " days ago";
1290         } elsif ($age > 60*60*2) {
1291                 $age_str = int $age/60/60;
1292                 $age_str .= " hours ago";
1293         } elsif ($age > 60*2) {
1294                 $age_str = int $age/60;
1295                 $age_str .= " min ago";
1296         } elsif ($age > 2) {
1297                 $age_str = int $age;
1298                 $age_str .= " sec ago";
1299         } else {
1300                 $age_str .= " right now";
1301         }
1302         return $age_str;
1303 }
1304
1305 use constant {
1306         S_IFINVALID => 0030000,
1307         S_IFGITLINK => 0160000,
1308 };
1309
1310 # submodule/subproject, a commit object reference
1311 sub S_ISGITLINK($) {
1312         my $mode = shift;
1313
1314         return (($mode & S_IFMT) == S_IFGITLINK)
1315 }
1316
1317 # convert file mode in octal to symbolic file mode string
1318 sub mode_str {
1319         my $mode = oct shift;
1320
1321         if (S_ISGITLINK($mode)) {
1322                 return 'm---------';
1323         } elsif (S_ISDIR($mode & S_IFMT)) {
1324                 return 'drwxr-xr-x';
1325         } elsif (S_ISLNK($mode)) {
1326                 return 'lrwxrwxrwx';
1327         } elsif (S_ISREG($mode)) {
1328                 # git cares only about the executable bit
1329                 if ($mode & S_IXUSR) {
1330                         return '-rwxr-xr-x';
1331                 } else {
1332                         return '-rw-r--r--';
1333                 };
1334         } else {
1335                 return '----------';
1336         }
1337 }
1338
1339 # convert file mode in octal to file type string
1340 sub file_type {
1341         my $mode = shift;
1342
1343         if ($mode !~ m/^[0-7]+$/) {
1344                 return $mode;
1345         } else {
1346                 $mode = oct $mode;
1347         }
1348
1349         if (S_ISGITLINK($mode)) {
1350                 return "submodule";
1351         } elsif (S_ISDIR($mode & S_IFMT)) {
1352                 return "directory";
1353         } elsif (S_ISLNK($mode)) {
1354                 return "symlink";
1355         } elsif (S_ISREG($mode)) {
1356                 return "file";
1357         } else {
1358                 return "unknown";
1359         }
1360 }
1361
1362 # convert file mode in octal to file type description string
1363 sub file_type_long {
1364         my $mode = shift;
1365
1366         if ($mode !~ m/^[0-7]+$/) {
1367                 return $mode;
1368         } else {
1369                 $mode = oct $mode;
1370         }
1371
1372         if (S_ISGITLINK($mode)) {
1373                 return "submodule";
1374         } elsif (S_ISDIR($mode & S_IFMT)) {
1375                 return "directory";
1376         } elsif (S_ISLNK($mode)) {
1377                 return "symlink";
1378         } elsif (S_ISREG($mode)) {
1379                 if ($mode & S_IXUSR) {
1380                         return "executable";
1381                 } else {
1382                         return "file";
1383                 };
1384         } else {
1385                 return "unknown";
1386         }
1387 }
1388
1389
1390 ## ----------------------------------------------------------------------
1391 ## functions returning short HTML fragments, or transforming HTML fragments
1392 ## which don't belong to other sections
1393
1394 # format line of commit message.
1395 sub format_log_line_html {
1396         my $line = shift;
1397
1398         $line = esc_html($line, -nbsp=>1);
1399         $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1400                 $cgi->a({-href => href(action=>"object", hash=>$1),
1401                                         -class => "text"}, $1);
1402         }eg;
1403
1404         return $line;
1405 }
1406
1407 # format marker of refs pointing to given object
1408
1409 # the destination action is chosen based on object type and current context:
1410 # - for annotated tags, we choose the tag view unless it's the current view
1411 #   already, in which case we go to shortlog view
1412 # - for other refs, we keep the current view if we're in history, shortlog or
1413 #   log view, and select shortlog otherwise
1414 sub format_ref_marker {
1415         my ($refs, $id) = @_;
1416         my $markers = '';
1417
1418         if (defined $refs->{$id}) {
1419                 foreach my $ref (@{$refs->{$id}}) {
1420                         # this code exploits the fact that non-lightweight tags are the
1421                         # only indirect objects, and that they are the only objects for which
1422                         # we want to use tag instead of shortlog as action
1423                         my ($type, $name) = qw();
1424                         my $indirect = ($ref =~ s/\^\{\}$//);
1425                         # e.g. tags/v2.6.11 or heads/next
1426                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1427                                 $type = $1;
1428                                 $name = $2;
1429                         } else {
1430                                 $type = "ref";
1431                                 $name = $ref;
1432                         }
1433
1434                         my $class = $type;
1435                         $class .= " indirect" if $indirect;
1436
1437                         my $dest_action = "shortlog";
1438
1439                         if ($indirect) {
1440                                 $dest_action = "tag" unless $action eq "tag";
1441                         } elsif ($action =~ /^(history|(short)?log)$/) {
1442                                 $dest_action = $action;
1443                         }
1444
1445                         my $dest = "";
1446                         $dest .= "refs/" unless $ref =~ m!^refs/!;
1447                         $dest .= $ref;
1448
1449                         my $link = $cgi->a({
1450                                 -href => href(
1451                                         action=>$dest_action,
1452                                         hash=>$dest
1453                                 )}, $name);
1454
1455                         $markers .= " <span class=\"$class\" title=\"$ref\">" .
1456                                 $link . "</span>";
1457                 }
1458         }
1459
1460         if ($markers) {
1461                 return ' <span class="refs">'. $markers . '</span>';
1462         } else {
1463                 return "";
1464         }
1465 }
1466
1467 # format, perhaps shortened and with markers, title line
1468 sub format_subject_html {
1469         my ($long, $short, $href, $extra) = @_;
1470         $extra = '' unless defined($extra);
1471
1472         if (length($short) < length($long)) {
1473                 return $cgi->a({-href => $href, -class => "list subject",
1474                                 -title => to_utf8($long)},
1475                        esc_html($short) . $extra);
1476         } else {
1477                 return $cgi->a({-href => $href, -class => "list subject"},
1478                        esc_html($long)  . $extra);
1479         }
1480 }
1481
1482 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1483 sub format_git_diff_header_line {
1484         my $line = shift;
1485         my $diffinfo = shift;
1486         my ($from, $to) = @_;
1487
1488         if ($diffinfo->{'nparents'}) {
1489                 # combined diff
1490                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1491                 if ($to->{'href'}) {
1492                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1493                                          esc_path($to->{'file'}));
1494                 } else { # file was deleted (no href)
1495                         $line .= esc_path($to->{'file'});
1496                 }
1497         } else {
1498                 # "ordinary" diff
1499                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1500                 if ($from->{'href'}) {
1501                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1502                                          'a/' . esc_path($from->{'file'}));
1503                 } else { # file was added (no href)
1504                         $line .= 'a/' . esc_path($from->{'file'});
1505                 }
1506                 $line .= ' ';
1507                 if ($to->{'href'}) {
1508                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1509                                          'b/' . esc_path($to->{'file'}));
1510                 } else { # file was deleted
1511                         $line .= 'b/' . esc_path($to->{'file'});
1512                 }
1513         }
1514
1515         return "<div class=\"diff header\">$line</div>\n";
1516 }
1517
1518 # format extended diff header line, before patch itself
1519 sub format_extended_diff_header_line {
1520         my $line = shift;
1521         my $diffinfo = shift;
1522         my ($from, $to) = @_;
1523
1524         # match <path>
1525         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1526                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1527                                        esc_path($from->{'file'}));
1528         }
1529         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1530                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1531                                  esc_path($to->{'file'}));
1532         }
1533         # match single <mode>
1534         if ($line =~ m/\s(\d{6})$/) {
1535                 $line .= '<span class="info"> (' .
1536                          file_type_long($1) .
1537                          ')</span>';
1538         }
1539         # match <hash>
1540         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1541                 # can match only for combined diff
1542                 $line = 'index ';
1543                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1544                         if ($from->{'href'}[$i]) {
1545                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1546                                                   -class=>"hash"},
1547                                                  substr($diffinfo->{'from_id'}[$i],0,7));
1548                         } else {
1549                                 $line .= '0' x 7;
1550                         }
1551                         # separator
1552                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1553                 }
1554                 $line .= '..';
1555                 if ($to->{'href'}) {
1556                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1557                                          substr($diffinfo->{'to_id'},0,7));
1558                 } else {
1559                         $line .= '0' x 7;
1560                 }
1561
1562         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1563                 # can match only for ordinary diff
1564                 my ($from_link, $to_link);
1565                 if ($from->{'href'}) {
1566                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1567                                              substr($diffinfo->{'from_id'},0,7));
1568                 } else {
1569                         $from_link = '0' x 7;
1570                 }
1571                 if ($to->{'href'}) {
1572                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1573                                            substr($diffinfo->{'to_id'},0,7));
1574                 } else {
1575                         $to_link = '0' x 7;
1576                 }
1577                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1578                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1579         }
1580
1581         return $line . "<br/>\n";
1582 }
1583
1584 # format from-file/to-file diff header
1585 sub format_diff_from_to_header {
1586         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1587         my $line;
1588         my $result = '';
1589
1590         $line = $from_line;
1591         #assert($line =~ m/^---/) if DEBUG;
1592         # no extra formatting for "^--- /dev/null"
1593         if (! $diffinfo->{'nparents'}) {
1594                 # ordinary (single parent) diff
1595                 if ($line =~ m!^--- "?a/!) {
1596                         if ($from->{'href'}) {
1597                                 $line = '--- a/' .
1598                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1599                                                 esc_path($from->{'file'}));
1600                         } else {
1601                                 $line = '--- a/' .
1602                                         esc_path($from->{'file'});
1603                         }
1604                 }
1605                 $result .= qq!<div class="diff from_file">$line</div>\n!;
1606
1607         } else {
1608                 # combined diff (merge commit)
1609                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1610                         if ($from->{'href'}[$i]) {
1611                                 $line = '--- ' .
1612                                         $cgi->a({-href=>href(action=>"blobdiff",
1613                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
1614                                                              hash_parent_base=>$parents[$i],
1615                                                              file_parent=>$from->{'file'}[$i],
1616                                                              hash=>$diffinfo->{'to_id'},
1617                                                              hash_base=>$hash,
1618                                                              file_name=>$to->{'file'}),
1619                                                  -class=>"path",
1620                                                  -title=>"diff" . ($i+1)},
1621                                                 $i+1) .
1622                                         '/' .
1623                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1624                                                 esc_path($from->{'file'}[$i]));
1625                         } else {
1626                                 $line = '--- /dev/null';
1627                         }
1628                         $result .= qq!<div class="diff from_file">$line</div>\n!;
1629                 }
1630         }
1631
1632         $line = $to_line;
1633         #assert($line =~ m/^\+\+\+/) if DEBUG;
1634         # no extra formatting for "^+++ /dev/null"
1635         if ($line =~ m!^\+\+\+ "?b/!) {
1636                 if ($to->{'href'}) {
1637                         $line = '+++ b/' .
1638                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1639                                         esc_path($to->{'file'}));
1640                 } else {
1641                         $line = '+++ b/' .
1642                                 esc_path($to->{'file'});
1643                 }
1644         }
1645         $result .= qq!<div class="diff to_file">$line</div>\n!;
1646
1647         return $result;
1648 }
1649
1650 # create note for patch simplified by combined diff
1651 sub format_diff_cc_simplified {
1652         my ($diffinfo, @parents) = @_;
1653         my $result = '';
1654
1655         $result .= "<div class=\"diff header\">" .
1656                    "diff --cc ";
1657         if (!is_deleted($diffinfo)) {
1658                 $result .= $cgi->a({-href => href(action=>"blob",
1659                                                   hash_base=>$hash,
1660                                                   hash=>$diffinfo->{'to_id'},
1661                                                   file_name=>$diffinfo->{'to_file'}),
1662                                     -class => "path"},
1663                                    esc_path($diffinfo->{'to_file'}));
1664         } else {
1665                 $result .= esc_path($diffinfo->{'to_file'});
1666         }
1667         $result .= "</div>\n" . # class="diff header"
1668                    "<div class=\"diff nodifferences\">" .
1669                    "Simple merge" .
1670                    "</div>\n"; # class="diff nodifferences"
1671
1672         return $result;
1673 }
1674
1675 # format patch (diff) line (not to be used for diff headers)
1676 sub format_diff_line {
1677         my $line = shift;
1678         my ($from, $to) = @_;
1679         my $diff_class = "";
1680
1681         chomp $line;
1682
1683         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1684                 # combined diff
1685                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1686                 if ($line =~ m/^\@{3}/) {
1687                         $diff_class = " chunk_header";
1688                 } elsif ($line =~ m/^\\/) {
1689                         $diff_class = " incomplete";
1690                 } elsif ($prefix =~ tr/+/+/) {
1691                         $diff_class = " add";
1692                 } elsif ($prefix =~ tr/-/-/) {
1693                         $diff_class = " rem";
1694                 }
1695         } else {
1696                 # assume ordinary diff
1697                 my $char = substr($line, 0, 1);
1698                 if ($char eq '+') {
1699                         $diff_class = " add";
1700                 } elsif ($char eq '-') {
1701                         $diff_class = " rem";
1702                 } elsif ($char eq '@') {
1703                         $diff_class = " chunk_header";
1704                 } elsif ($char eq "\\") {
1705                         $diff_class = " incomplete";
1706                 }
1707         }
1708         $line = untabify($line);
1709         if ($from && $to && $line =~ m/^\@{2} /) {
1710                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1711                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1712
1713                 $from_lines = 0 unless defined $from_lines;
1714                 $to_lines   = 0 unless defined $to_lines;
1715
1716                 if ($from->{'href'}) {
1717                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1718                                              -class=>"list"}, $from_text);
1719                 }
1720                 if ($to->{'href'}) {
1721                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1722                                              -class=>"list"}, $to_text);
1723                 }
1724                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1725                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1726                 return "<div class=\"diff$diff_class\">$line</div>\n";
1727         } elsif ($from && $to && $line =~ m/^\@{3}/) {
1728                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1729                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1730
1731                 @from_text = split(' ', $ranges);
1732                 for (my $i = 0; $i < @from_text; ++$i) {
1733                         ($from_start[$i], $from_nlines[$i]) =
1734                                 (split(',', substr($from_text[$i], 1)), 0);
1735                 }
1736
1737                 $to_text   = pop @from_text;
1738                 $to_start  = pop @from_start;
1739                 $to_nlines = pop @from_nlines;
1740
1741                 $line = "<span class=\"chunk_info\">$prefix ";
1742                 for (my $i = 0; $i < @from_text; ++$i) {
1743                         if ($from->{'href'}[$i]) {
1744                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1745                                                   -class=>"list"}, $from_text[$i]);
1746                         } else {
1747                                 $line .= $from_text[$i];
1748                         }
1749                         $line .= " ";
1750                 }
1751                 if ($to->{'href'}) {
1752                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1753                                           -class=>"list"}, $to_text);
1754                 } else {
1755                         $line .= $to_text;
1756                 }
1757                 $line .= " $prefix</span>" .
1758                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1759                 return "<div class=\"diff$diff_class\">$line</div>\n";
1760         }
1761         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1762 }
1763
1764 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1765 # linked.  Pass the hash of the tree/commit to snapshot.
1766 sub format_snapshot_links {
1767         my ($hash) = @_;
1768         my $num_fmts = @snapshot_fmts;
1769         if ($num_fmts > 1) {
1770                 # A parenthesized list of links bearing format names.
1771                 # e.g. "snapshot (_tar.gz_ _zip_)"
1772                 return "snapshot (" . join(' ', map
1773                         $cgi->a({
1774                                 -href => href(
1775                                         action=>"snapshot",
1776                                         hash=>$hash,
1777                                         snapshot_format=>$_
1778                                 )
1779                         }, $known_snapshot_formats{$_}{'display'})
1780                 , @snapshot_fmts) . ")";
1781         } elsif ($num_fmts == 1) {
1782                 # A single "snapshot" link whose tooltip bears the format name.
1783                 # i.e. "_snapshot_"
1784                 my ($fmt) = @snapshot_fmts;
1785                 return
1786                         $cgi->a({
1787                                 -href => href(
1788                                         action=>"snapshot",
1789                                         hash=>$hash,
1790                                         snapshot_format=>$fmt
1791                                 ),
1792                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1793                         }, "snapshot");
1794         } else { # $num_fmts == 0
1795                 return undef;
1796         }
1797 }
1798
1799 ## ......................................................................
1800 ## functions returning values to be passed, perhaps after some
1801 ## transformation, to other functions; e.g. returning arguments to href()
1802
1803 # returns hash to be passed to href to generate gitweb URL
1804 # in -title key it returns description of link
1805 sub get_feed_info {
1806         my $format = shift || 'Atom';
1807         my %res = (action => lc($format));
1808
1809         # feed links are possible only for project views
1810         return unless (defined $project);
1811         # some views should link to OPML, or to generic project feed,
1812         # or don't have specific feed yet (so they should use generic)
1813         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1814
1815         my $branch;
1816         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1817         # from tag links; this also makes possible to detect branch links
1818         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1819             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
1820                 $branch = $1;
1821         }
1822         # find log type for feed description (title)
1823         my $type = 'log';
1824         if (defined $file_name) {
1825                 $type  = "history of $file_name";
1826                 $type .= "/" if ($action eq 'tree');
1827                 $type .= " on '$branch'" if (defined $branch);
1828         } else {
1829                 $type = "log of $branch" if (defined $branch);
1830         }
1831
1832         $res{-title} = $type;
1833         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1834         $res{'file_name'} = $file_name;
1835
1836         return %res;
1837 }
1838
1839 ## ----------------------------------------------------------------------
1840 ## git utility subroutines, invoking git commands
1841
1842 # returns path to the core git executable and the --git-dir parameter as list
1843 sub git_cmd {
1844         return $GIT, '--git-dir='.$git_dir;
1845 }
1846
1847 # quote the given arguments for passing them to the shell
1848 # quote_command("command", "arg 1", "arg with ' and ! characters")
1849 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1850 # Try to avoid using this function wherever possible.
1851 sub quote_command {
1852         return join(' ',
1853                     map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1854 }
1855
1856 # get HEAD ref of given project as hash
1857 sub git_get_head_hash {
1858         my $project = shift;
1859         my $o_git_dir = $git_dir;
1860         my $retval = undef;
1861         $git_dir = "$projectroot/$project";
1862         if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1863                 my $head = <$fd>;
1864                 close $fd;
1865                 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1866                         $retval = $1;
1867                 }
1868         }
1869         if (defined $o_git_dir) {
1870                 $git_dir = $o_git_dir;
1871         }
1872         return $retval;
1873 }
1874
1875 # get type of given object
1876 sub git_get_type {
1877         my $hash = shift;
1878
1879         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1880         my $type = <$fd>;
1881         close $fd or return;
1882         chomp $type;
1883         return $type;
1884 }
1885
1886 # repository configuration
1887 our $config_file = '';
1888 our %config;
1889
1890 # store multiple values for single key as anonymous array reference
1891 # single values stored directly in the hash, not as [ <value> ]
1892 sub hash_set_multi {
1893         my ($hash, $key, $value) = @_;
1894
1895         if (!exists $hash->{$key}) {
1896                 $hash->{$key} = $value;
1897         } elsif (!ref $hash->{$key}) {
1898                 $hash->{$key} = [ $hash->{$key}, $value ];
1899         } else {
1900                 push @{$hash->{$key}}, $value;
1901         }
1902 }
1903
1904 # return hash of git project configuration
1905 # optionally limited to some section, e.g. 'gitweb'
1906 sub git_parse_project_config {
1907         my $section_regexp = shift;
1908         my %config;
1909
1910         local $/ = "\0";
1911
1912         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1913                 or return;
1914
1915         while (my $keyval = <$fh>) {
1916                 chomp $keyval;
1917                 my ($key, $value) = split(/\n/, $keyval, 2);
1918
1919                 hash_set_multi(\%config, $key, $value)
1920                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1921         }
1922         close $fh;
1923
1924         return %config;
1925 }
1926
1927 # convert config value to boolean: 'true' or 'false'
1928 # no value, number > 0, 'true' and 'yes' values are true
1929 # rest of values are treated as false (never as error)
1930 sub config_to_bool {
1931         my $val = shift;
1932
1933         return 1 if !defined $val;             # section.key
1934
1935         # strip leading and trailing whitespace
1936         $val =~ s/^\s+//;
1937         $val =~ s/\s+$//;
1938
1939         return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
1940                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
1941 }
1942
1943 # convert config value to simple decimal number
1944 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1945 # to be multiplied by 1024, 1048576, or 1073741824
1946 sub config_to_int {
1947         my $val = shift;
1948
1949         # strip leading and trailing whitespace
1950         $val =~ s/^\s+//;
1951         $val =~ s/\s+$//;
1952
1953         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1954                 $unit = lc($unit);
1955                 # unknown unit is treated as 1
1956                 return $num * ($unit eq 'g' ? 1073741824 :
1957                                $unit eq 'm' ?    1048576 :
1958                                $unit eq 'k' ?       1024 : 1);
1959         }
1960         return $val;
1961 }
1962
1963 # convert config value to array reference, if needed
1964 sub config_to_multi {
1965         my $val = shift;
1966
1967         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1968 }
1969
1970 sub git_get_project_config {
1971         my ($key, $type) = @_;
1972
1973         # key sanity check
1974         return unless ($key);
1975         $key =~ s/^gitweb\.//;
1976         return if ($key =~ m/\W/);
1977
1978         # type sanity check
1979         if (defined $type) {
1980                 $type =~ s/^--//;
1981                 $type = undef
1982                         unless ($type eq 'bool' || $type eq 'int');
1983         }
1984
1985         # get config
1986         if (!defined $config_file ||
1987             $config_file ne "$git_dir/config") {
1988                 %config = git_parse_project_config('gitweb');
1989                 $config_file = "$git_dir/config";
1990         }
1991
1992         # check if config variable (key) exists
1993         return unless exists $config{"gitweb.$key"};
1994
1995         # ensure given type
1996         if (!defined $type) {
1997                 return $config{"gitweb.$key"};
1998         } elsif ($type eq 'bool') {
1999                 # backward compatibility: 'git config --bool' returns true/false
2000                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2001         } elsif ($type eq 'int') {
2002                 return config_to_int($config{"gitweb.$key"});
2003         }
2004         return $config{"gitweb.$key"};
2005 }
2006
2007 # get hash of given path at given ref
2008 sub git_get_hash_by_path {
2009         my $base = shift;
2010         my $path = shift || return undef;
2011         my $type = shift;
2012
2013         $path =~ s,/+$,,;
2014
2015         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2016                 or die_error(500, "Open git-ls-tree failed");
2017         my $line = <$fd>;
2018         close $fd or return undef;
2019
2020         if (!defined $line) {
2021                 # there is no tree or hash given by $path at $base
2022                 return undef;
2023         }
2024
2025         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2026         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2027         if (defined $type && $type ne $2) {
2028                 # type doesn't match
2029                 return undef;
2030         }
2031         return $3;
2032 }
2033
2034 # get path of entry with given hash at given tree-ish (ref)
2035 # used to get 'from' filename for combined diff (merge commit) for renames
2036 sub git_get_path_by_hash {
2037         my $base = shift || return;
2038         my $hash = shift || return;
2039
2040         local $/ = "\0";
2041
2042         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2043                 or return undef;
2044         while (my $line = <$fd>) {
2045                 chomp $line;
2046
2047                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2048                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2049                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2050                         close $fd;
2051                         return $1;
2052                 }
2053         }
2054         close $fd;
2055         return undef;
2056 }
2057
2058 ## ......................................................................
2059 ## git utility functions, directly accessing git repository
2060
2061 sub git_get_project_description {
2062         my $path = shift;
2063
2064         $git_dir = "$projectroot/$path";
2065         open my $fd, "$git_dir/description"
2066                 or return git_get_project_config('description');
2067         my $descr = <$fd>;
2068         close $fd;
2069         if (defined $descr) {
2070                 chomp $descr;
2071         }
2072         return $descr;
2073 }
2074
2075 sub git_get_project_ctags {
2076         my $path = shift;
2077         my $ctags = {};
2078
2079         $git_dir = "$projectroot/$path";
2080         unless (opendir D, "$git_dir/ctags") {
2081                 return $ctags;
2082         }
2083         foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
2084                 open CT, $_ or next;
2085                 my $val = <CT>;
2086                 chomp $val;
2087                 close CT;
2088                 my $ctag = $_; $ctag =~ s#.*/##;
2089                 $ctags->{$ctag} = $val;
2090         }
2091         closedir D;
2092         $ctags;
2093 }
2094
2095 sub git_populate_project_tagcloud {
2096         my $ctags = shift;
2097
2098         # First, merge different-cased tags; tags vote on casing
2099         my %ctags_lc;
2100         foreach (keys %$ctags) {
2101                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2102                 if (not $ctags_lc{lc $_}->{topcount}
2103                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2104                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2105                         $ctags_lc{lc $_}->{topname} = $_;
2106                 }
2107         }
2108
2109         my $cloud;
2110         if (eval { require HTML::TagCloud; 1; }) {
2111                 $cloud = HTML::TagCloud->new;
2112                 foreach (sort keys %ctags_lc) {
2113                         # Pad the title with spaces so that the cloud looks
2114                         # less crammed.
2115                         my $title = $ctags_lc{$_}->{topname};
2116                         $title =~ s/ /&nbsp;/g;
2117                         $title =~ s/^/&nbsp;/g;
2118                         $title =~ s/$/&nbsp;/g;
2119                         $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2120                 }
2121         } else {
2122                 $cloud = \%ctags_lc;
2123         }
2124         $cloud;
2125 }
2126
2127 sub git_show_project_tagcloud {
2128         my ($cloud, $count) = @_;
2129         #print STDERR ref($cloud)."..\n";
2130         if (ref $cloud eq 'HTML::TagCloud') {
2131                 return $cloud->html_and_css($count);
2132         } else {
2133                 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2134                 return '<p align="center">' . join (', ', map {
2135                         "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2136                 } splice(@tags, 0, $count)) . '</p>';
2137         }
2138 }
2139
2140 sub git_get_project_url_list {
2141         my $path = shift;
2142
2143         $git_dir = "$projectroot/$path";
2144         open my $fd, "$git_dir/cloneurl"
2145                 or return wantarray ?
2146                 @{ config_to_multi(git_get_project_config('url')) } :
2147                    config_to_multi(git_get_project_config('url'));
2148         my @git_project_url_list = map { chomp; $_ } <$fd>;
2149         close $fd;
2150
2151         return wantarray ? @git_project_url_list : \@git_project_url_list;
2152 }
2153
2154 our $gitweb_project_owner = undef;
2155 sub git_get_project_list_from_file {
2156
2157         return if (defined $gitweb_project_owner);
2158
2159         $gitweb_project_owner = {};
2160         # read from file (url-encoded):
2161         # 'git%2Fgit.git Linus+Torvalds'
2162         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2163         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2164         if (-f $projects_list) {
2165                 open (my $fd , $projects_list);
2166                 while (my $line = <$fd>) {
2167                         chomp $line;
2168                         my ($pr, $ow) = split ' ', $line;
2169                         $pr = unescape($pr);
2170                         $ow = unescape($ow);
2171                         $gitweb_project_owner->{$pr} = to_utf8($ow);
2172                 }
2173                 close $fd;
2174         }
2175 }
2176
2177 sub git_get_project_owner {
2178         my $project = shift;
2179         my $owner;
2180
2181         return undef unless $project;
2182         $git_dir = "$projectroot/$project";
2183
2184         if (!defined $gitweb_project_owner) {
2185                 git_get_project_list_from_file();
2186         }
2187
2188         if (exists $gitweb_project_owner->{$project}) {
2189                 $owner = $gitweb_project_owner->{$project};
2190         }
2191         if (!defined $owner){
2192                 $owner = git_get_project_config('owner');
2193         }
2194         if (!defined $owner) {
2195                 $owner = get_file_owner("$git_dir");
2196         }
2197
2198         return $owner;
2199 }
2200
2201 sub git_get_last_activity {
2202         my ($path) = @_;
2203         my $fd;
2204
2205         $git_dir = "$projectroot/$path";
2206         open($fd, "-|", git_cmd(), 'for-each-ref',
2207              '--format=%(committer)',
2208              '--sort=-committerdate',
2209              '--count=1',
2210              'refs/heads') or return;
2211         my $most_recent = <$fd>;
2212         close $fd or return;
2213         if (defined $most_recent &&
2214             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2215                 my $timestamp = $1;
2216                 my $age = time - $timestamp;
2217                 return ($age, age_string($age));
2218         }
2219         return (undef, undef);
2220 }
2221
2222 sub git_get_references {
2223         my $type = shift || "";
2224         my %refs;
2225         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2226         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2227         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2228                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2229                 or return;
2230
2231         while (my $line = <$fd>) {
2232                 chomp $line;
2233                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2234                         if (defined $refs{$1}) {
2235                                 push @{$refs{$1}}, $2;
2236                         } else {
2237                                 $refs{$1} = [ $2 ];
2238                         }
2239                 }
2240         }
2241         close $fd or return;
2242         return \%refs;
2243 }
2244
2245 sub git_get_rev_name_tags {
2246         my $hash = shift || return undef;
2247
2248         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2249                 or return;
2250         my $name_rev = <$fd>;
2251         close $fd;
2252
2253         if ($name_rev =~ m|^$hash tags/(.*)$|) {
2254                 return $1;
2255         } else {
2256                 # catches also '$hash undefined' output
2257                 return undef;
2258         }
2259 }
2260
2261 ## ----------------------------------------------------------------------
2262 ## parse to hash functions
2263
2264 sub parse_date {
2265         my $epoch = shift;
2266         my $tz = shift || "-0000";
2267
2268         my %date;
2269         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2270         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2271         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2272         $date{'hour'} = $hour;
2273         $date{'minute'} = $min;
2274         $date{'mday'} = $mday;
2275         $date{'day'} = $days[$wday];
2276         $date{'month'} = $months[$mon];
2277         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2278                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2279         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2280                              $mday, $months[$mon], $hour ,$min;
2281         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2282                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2283
2284         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2285         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2286         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2287         $date{'hour_local'} = $hour;
2288         $date{'minute_local'} = $min;
2289         $date{'tz_local'} = $tz;
2290         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2291                                   1900+$year, $mon+1, $mday,
2292                                   $hour, $min, $sec, $tz);
2293         return %date;
2294 }
2295
2296 sub parse_tag {
2297         my $tag_id = shift;
2298         my %tag;
2299         my @comment;
2300
2301         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2302         $tag{'id'} = $tag_id;
2303         while (my $line = <$fd>) {
2304                 chomp $line;
2305                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2306                         $tag{'object'} = $1;
2307                 } elsif ($line =~ m/^type (.+)$/) {
2308                         $tag{'type'} = $1;
2309                 } elsif ($line =~ m/^tag (.+)$/) {
2310                         $tag{'name'} = $1;
2311                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2312                         $tag{'author'} = $1;
2313                         $tag{'epoch'} = $2;
2314                         $tag{'tz'} = $3;
2315                 } elsif ($line =~ m/--BEGIN/) {
2316                         push @comment, $line;
2317                         last;
2318                 } elsif ($line eq "") {
2319                         last;
2320                 }
2321         }
2322         push @comment, <$fd>;
2323         $tag{'comment'} = \@comment;
2324         close $fd or return;
2325         if (!defined $tag{'name'}) {
2326                 return
2327         };
2328         return %tag
2329 }
2330
2331 sub parse_commit_text {
2332         my ($commit_text, $withparents) = @_;
2333         my @commit_lines = split '\n', $commit_text;
2334         my %co;
2335
2336         pop @commit_lines; # Remove '\0'
2337
2338         if (! @commit_lines) {
2339                 return;
2340         }
2341
2342         my $header = shift @commit_lines;
2343         if ($header !~ m/^[0-9a-fA-F]{40}/) {
2344                 return;
2345         }
2346         ($co{'id'}, my @parents) = split ' ', $header;
2347         while (my $line = shift @commit_lines) {
2348                 last if $line eq "\n";
2349                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2350                         $co{'tree'} = $1;
2351                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2352                         push @parents, $1;
2353                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2354                         $co{'author'} = $1;
2355                         $co{'author_epoch'} = $2;
2356                         $co{'author_tz'} = $3;
2357                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2358                                 $co{'author_name'}  = $1;
2359                                 $co{'author_email'} = $2;
2360                         } else {
2361                                 $co{'author_name'} = $co{'author'};
2362                         }
2363                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2364                         $co{'committer'} = $1;
2365                         $co{'committer_epoch'} = $2;
2366                         $co{'committer_tz'} = $3;
2367                         $co{'committer_name'} = $co{'committer'};
2368                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2369                                 $co{'committer_name'}  = $1;
2370                                 $co{'committer_email'} = $2;
2371                         } else {
2372                                 $co{'committer_name'} = $co{'committer'};
2373                         }
2374                 }
2375         }
2376         if (!defined $co{'tree'}) {
2377                 return;
2378         };
2379         $co{'parents'} = \@parents;
2380         $co{'parent'} = $parents[0];
2381
2382         foreach my $title (@commit_lines) {
2383                 $title =~ s/^    //;
2384                 if ($title ne "") {
2385                         $co{'title'} = chop_str($title, 80, 5);
2386                         # remove leading stuff of merges to make the interesting part visible
2387                         if (length($title) > 50) {
2388                                 $title =~ s/^Automatic //;
2389                                 $title =~ s/^merge (of|with) /Merge ... /i;
2390                                 if (length($title) > 50) {
2391                                         $title =~ s/(http|rsync):\/\///;
2392                                 }
2393                                 if (length($title) > 50) {
2394                                         $title =~ s/(master|www|rsync)\.//;
2395                                 }
2396                                 if (length($title) > 50) {
2397                                         $title =~ s/kernel.org:?//;
2398                                 }
2399                                 if (length($title) > 50) {
2400                                         $title =~ s/\/pub\/scm//;
2401                                 }
2402                         }
2403                         $co{'title_short'} = chop_str($title, 50, 5);
2404                         last;
2405                 }
2406         }
2407         if (! defined $co{'title'} || $co{'title'} eq "") {
2408                 $co{'title'} = $co{'title_short'} = '(no commit message)';
2409         }
2410         # remove added spaces
2411         foreach my $line (@commit_lines) {
2412                 $line =~ s/^    //;
2413         }
2414         $co{'comment'} = \@commit_lines;
2415
2416         my $age = time - $co{'committer_epoch'};
2417         $co{'age'} = $age;
2418         $co{'age_string'} = age_string($age);
2419         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2420         if ($age > 60*60*24*7*2) {
2421                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2422                 $co{'age_string_age'} = $co{'age_string'};
2423         } else {
2424                 $co{'age_string_date'} = $co{'age_string'};
2425                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2426         }
2427         return %co;
2428 }
2429
2430 sub parse_commit {
2431         my ($commit_id) = @_;
2432         my %co;
2433
2434         local $/ = "\0";
2435
2436         open my $fd, "-|", git_cmd(), "rev-list",
2437                 "--parents",
2438                 "--header",
2439                 "--max-count=1",
2440                 $commit_id,
2441                 "--",
2442                 or die_error(500, "Open git-rev-list failed");
2443         %co = parse_commit_text(<$fd>, 1);
2444         close $fd;
2445
2446         return %co;
2447 }
2448
2449 sub parse_commits {
2450         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2451         my @cos;
2452
2453         $maxcount ||= 1;
2454         $skip ||= 0;
2455
2456         local $/ = "\0";
2457
2458         open my $fd, "-|", git_cmd(), "rev-list",
2459                 "--header",
2460                 @args,
2461                 ("--max-count=" . $maxcount),
2462                 ("--skip=" . $skip),
2463                 @extra_options,
2464                 $commit_id,
2465                 "--",
2466                 ($filename ? ($filename) : ())
2467                 or die_error(500, "Open git-rev-list failed");
2468         while (my $line = <$fd>) {
2469                 my %co = parse_commit_text($line);
2470                 push @cos, \%co;
2471         }
2472         close $fd;
2473
2474         return wantarray ? @cos : \@cos;
2475 }
2476
2477 # parse line of git-diff-tree "raw" output
2478 sub parse_difftree_raw_line {
2479         my $line = shift;
2480         my %res;
2481
2482         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2483         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2484         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2485                 $res{'from_mode'} = $1;
2486                 $res{'to_mode'} = $2;
2487                 $res{'from_id'} = $3;
2488                 $res{'to_id'} = $4;
2489                 $res{'status'} = $5;
2490                 $res{'similarity'} = $6;
2491                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2492                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2493                 } else {
2494                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2495                 }
2496         }
2497         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2498         # combined diff (for merge commit)
2499         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2500                 $res{'nparents'}  = length($1);
2501                 $res{'from_mode'} = [ split(' ', $2) ];
2502                 $res{'to_mode'} = pop @{$res{'from_mode'}};
2503                 $res{'from_id'} = [ split(' ', $3) ];
2504                 $res{'to_id'} = pop @{$res{'from_id'}};
2505                 $res{'status'} = [ split('', $4) ];
2506                 $res{'to_file'} = unquote($5);
2507         }
2508         # 'c512b523472485aef4fff9e57b229d9d243c967f'
2509         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2510                 $res{'commit'} = $1;
2511         }
2512
2513         return wantarray ? %res : \%res;
2514 }
2515
2516 # wrapper: return parsed line of git-diff-tree "raw" output
2517 # (the argument might be raw line, or parsed info)
2518 sub parsed_difftree_line {
2519         my $line_or_ref = shift;
2520
2521         if (ref($line_or_ref) eq "HASH") {
2522                 # pre-parsed (or generated by hand)
2523                 return $line_or_ref;
2524         } else {
2525                 return parse_difftree_raw_line($line_or_ref);
2526         }
2527 }
2528
2529 # parse line of git-ls-tree output
2530 sub parse_ls_tree_line ($;%) {
2531         my $line = shift;
2532         my %opts = @_;
2533         my %res;
2534
2535         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2536         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2537
2538         $res{'mode'} = $1;
2539         $res{'type'} = $2;
2540         $res{'hash'} = $3;
2541         if ($opts{'-z'}) {
2542                 $res{'name'} = $4;
2543         } else {
2544                 $res{'name'} = unquote($4);
2545         }
2546
2547         return wantarray ? %res : \%res;
2548 }
2549
2550 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2551 sub parse_from_to_diffinfo {
2552         my ($diffinfo, $from, $to, @parents) = @_;
2553
2554         if ($diffinfo->{'nparents'}) {
2555                 # combined diff
2556                 $from->{'file'} = [];
2557                 $from->{'href'} = [];
2558                 fill_from_file_info($diffinfo, @parents)
2559                         unless exists $diffinfo->{'from_file'};
2560                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2561                         $from->{'file'}[$i] =
2562                                 defined $diffinfo->{'from_file'}[$i] ?
2563                                         $diffinfo->{'from_file'}[$i] :
2564                                         $diffinfo->{'to_file'};
2565                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2566                                 $from->{'href'}[$i] = href(action=>"blob",
2567                                                            hash_base=>$parents[$i],
2568                                                            hash=>$diffinfo->{'from_id'}[$i],
2569                                                            file_name=>$from->{'file'}[$i]);
2570                         } else {
2571                                 $from->{'href'}[$i] = undef;
2572                         }
2573                 }
2574         } else {
2575                 # ordinary (not combined) diff
2576                 $from->{'file'} = $diffinfo->{'from_file'};
2577                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2578                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2579                                                hash=>$diffinfo->{'from_id'},
2580                                                file_name=>$from->{'file'});
2581                 } else {
2582                         delete $from->{'href'};
2583                 }
2584         }
2585
2586         $to->{'file'} = $diffinfo->{'to_file'};
2587         if (!is_deleted($diffinfo)) { # file exists in result
2588                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2589                                      hash=>$diffinfo->{'to_id'},
2590                                      file_name=>$to->{'file'});
2591         } else {
2592                 delete $to->{'href'};
2593         }
2594 }
2595
2596 ## ......................................................................
2597 ## parse to array of hashes functions
2598
2599 sub git_get_heads_list {
2600         my $limit = shift;
2601         my @headslist;
2602
2603         open my $fd, '-|', git_cmd(), 'for-each-ref',
2604                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2605                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2606                 'refs/heads'
2607                 or return;
2608         while (my $line = <$fd>) {
2609                 my %ref_item;
2610
2611                 chomp $line;
2612                 my ($refinfo, $committerinfo) = split(/\0/, $line);
2613                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2614                 my ($committer, $epoch, $tz) =
2615                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2616                 $ref_item{'fullname'}  = $name;
2617                 $name =~ s!^refs/heads/!!;
2618
2619                 $ref_item{'name'}  = $name;
2620                 $ref_item{'id'}    = $hash;
2621                 $ref_item{'title'} = $title || '(no commit message)';
2622                 $ref_item{'epoch'} = $epoch;
2623                 if ($epoch) {
2624                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2625                 } else {
2626                         $ref_item{'age'} = "unknown";
2627                 }
2628
2629                 push @headslist, \%ref_item;
2630         }
2631         close $fd;
2632
2633         return wantarray ? @headslist : \@headslist;
2634 }
2635
2636 sub git_get_tags_list {
2637         my $limit = shift;
2638         my @tagslist;
2639
2640         open my $fd, '-|', git_cmd(), 'for-each-ref',
2641                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2642                 '--format=%(objectname) %(objecttype) %(refname) '.
2643                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2644                 'refs/tags'
2645                 or return;
2646         while (my $line = <$fd>) {
2647                 my %ref_item;
2648
2649                 chomp $line;
2650                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2651                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2652                 my ($creator, $epoch, $tz) =
2653                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2654                 $ref_item{'fullname'} = $name;
2655                 $name =~ s!^refs/tags/!!;
2656
2657                 $ref_item{'type'} = $type;
2658                 $ref_item{'id'} = $id;
2659                 $ref_item{'name'} = $name;
2660                 if ($type eq "tag") {
2661                         $ref_item{'subject'} = $title;
2662                         $ref_item{'reftype'} = $reftype;
2663                         $ref_item{'refid'}   = $refid;
2664                 } else {
2665                         $ref_item{'reftype'} = $type;
2666                         $ref_item{'refid'}   = $id;
2667                 }
2668
2669                 if ($type eq "tag" || $type eq "commit") {
2670                         $ref_item{'epoch'} = $epoch;
2671                         if ($epoch) {
2672                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2673                         } else {
2674                                 $ref_item{'age'} = "unknown";
2675                         }
2676                 }
2677
2678                 push @tagslist, \%ref_item;
2679         }
2680         close $fd;
2681
2682         return wantarray ? @tagslist : \@tagslist;
2683 }
2684
2685 ## ----------------------------------------------------------------------
2686 ## filesystem-related functions
2687
2688 sub get_file_owner {
2689         my $path = shift;
2690
2691         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2692         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2693         if (!defined $gcos) {
2694                 return undef;
2695         }
2696         my $owner = $gcos;
2697         $owner =~ s/[,;].*$//;
2698         return to_utf8($owner);
2699 }
2700
2701 # assume that file exists
2702 sub insert_file {
2703         my $filename = shift;
2704
2705         open my $fd, '<', $filename;
2706         return join '', map to_utf8($_), <$fd>;
2707 }
2708
2709 ## ......................................................................
2710 ## mimetype related functions
2711
2712 sub mimetype_guess_file {
2713         my $filename = shift;
2714         my $mimemap = shift;
2715         -r $mimemap or return undef;
2716
2717         my %mimemap;
2718         open(MIME, $mimemap) or return undef;
2719         while (<MIME>) {
2720                 next if m/^#/; # skip comments
2721                 my ($mime, $exts) = split(/\t+/);
2722                 if (defined $exts) {
2723                         my @exts = split(/\s+/, $exts);
2724                         foreach my $ext (@exts) {
2725                                 $mimemap{$ext} = $mime;
2726                         }
2727                 }
2728         }
2729         close(MIME);
2730
2731         $filename =~ /\.([^.]*)$/;
2732         return $mimemap{$1};
2733 }
2734
2735 sub mimetype_guess {
2736         my $filename = shift;
2737         my $mime;
2738         $filename =~ /\./ or return undef;
2739
2740         if ($mimetypes_file) {
2741                 my $file = $mimetypes_file;
2742                 if ($file !~ m!^/!) { # if it is relative path
2743                         # it is relative to project
2744                         $file = "$projectroot/$project/$file";
2745                 }
2746                 $mime = mimetype_guess_file($filename, $file);
2747         }
2748         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2749         return $mime;
2750 }
2751
2752 sub blob_mimetype {
2753         my $fd = shift;
2754         my $filename = shift;
2755
2756         if ($filename) {
2757                 my $mime = mimetype_guess($filename);
2758                 $mime and return $mime;
2759         }
2760
2761         # just in case
2762         return $default_blob_plain_mimetype unless $fd;
2763
2764         if (-T $fd) {
2765                 return 'text/plain';
2766         } elsif (! $filename) {
2767                 return 'application/octet-stream';
2768         } elsif ($filename =~ m/\.png$/i) {
2769                 return 'image/png';
2770         } elsif ($filename =~ m/\.gif$/i) {
2771                 return 'image/gif';
2772         } elsif ($filename =~ m/\.jpe?g$/i) {
2773                 return 'image/jpeg';
2774         } else {
2775                 return 'application/octet-stream';
2776         }
2777 }
2778
2779 sub blob_contenttype {
2780         my ($fd, $file_name, $type) = @_;
2781
2782         $type ||= blob_mimetype($fd, $file_name);
2783         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2784                 $type .= "; charset=$default_text_plain_charset";
2785         }
2786
2787         return $type;
2788 }
2789
2790 # die_error(<http_status_code>, <error_message>)
2791 # Example: die_error(404, 'Hash not found')
2792 # By convention, use the following status codes (as defined in RFC 2616):
2793 # 400: Invalid or missing CGI parameters, or
2794 #      requested object exists but has wrong type.
2795 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2796 #      this server or project.
2797 # 404: Requested object/revision/project doesn't exist.
2798 # 500: The server isn't configured properly, or
2799 #      an internal error occurred (e.g. failed assertions caused by bugs), or
2800 #      an unknown error occurred (e.g. the git binary died unexpectedly).
2801 sub die_error {
2802         my $status = shift || 500;
2803         my $error = shift || "Internal server error";
2804
2805         my %http_responses = (400 => '400 Bad Request',
2806                               403 => '403 Forbidden',
2807                               404 => '404 Not Found',
2808                               500 => '500 Internal Server Error');
2809         $c->response->status($http_responses{$status});
2810
2811         $c->stash->{content} = <<EOF;
2812         <div class="page_body">
2813         <br /><br />
2814         $status - $error
2815         <br />
2816         </div>
2817 EOF
2818         die bless { $status => $http_responses{$status}, err => $error };
2819 }
2820
2821 ## ----------------------------------------------------------------------
2822 ## functions printing or outputting HTML: navigation
2823
2824 sub git_print_page_nav {
2825         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2826         $extra = '' if !defined $extra; # pager or formats
2827
2828         my @navs = qw(summary shortlog log commit commitdiff tree);
2829         if ($suppress) {
2830                 @navs = grep { $_ ne $suppress } @navs;
2831         }
2832
2833         my %arg = map { $_ => {action=>$_} } @navs;
2834         if (defined $head) {
2835                 for (qw(commit commitdiff)) {
2836                         $arg{$_}{'hash'} = $head;
2837                 }
2838                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2839                         for (qw(shortlog log)) {
2840                                 $arg{$_}{'hash'} = $head;
2841                         }
2842                 }
2843         }
2844
2845         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2846         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2847
2848         my @actions = gitweb_get_feature('actions');
2849         my %repl = (
2850                 '%' => '%',
2851                 'n' => $project,         # project name
2852                 'f' => $git_dir,         # project path within filesystem
2853                 'h' => $treehead || '',  # current hash ('h' parameter)
2854                 'b' => $treebase || '',  # hash base ('hb' parameter)
2855         );
2856         while (@actions) {
2857                 my ($label, $link, $pos) = splice(@actions,0,3);
2858                 # insert
2859                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2860                 # munch munch
2861                 $link =~ s/%([%nfhb])/$repl{$1}/g;
2862                 $arg{$label}{'_href'} = $link;
2863         }
2864
2865         $c->stash->{page_nav} = 1;
2866         $c->stash->{nav_links} =
2867                 (join " | ",
2868                  map { $_ eq $current ?
2869                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
2870                  } @navs);
2871         $c->stash->{extra} = $extra;
2872 }
2873
2874 sub format_paging_nav {
2875         my ($action, $hash, $head, $page, $has_next_link) = @_;
2876         my $paging_nav;
2877
2878
2879         if ($hash ne $head || $page) {
2880                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2881         } else {
2882                 $paging_nav .= "HEAD";
2883         }
2884
2885         if ($page > 0) {
2886                 $paging_nav .= " &sdot; " .
2887                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
2888                                  -accesskey => "p", -title => "Alt-p"}, "prev");
2889         } else {
2890                 $paging_nav .= " &sdot; prev";
2891         }
2892
2893         if ($has_next_link) {
2894                 $paging_nav .= " &sdot; " .
2895                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
2896                                  -accesskey => "n", -title => "Alt-n"}, "next");
2897         } else {
2898                 $paging_nav .= " &sdot; next";
2899         }
2900
2901         return $paging_nav;
2902 }
2903
2904 ## ......................................................................
2905 ## functions printing or outputting HTML: div
2906
2907 sub git_print_header_div {
2908         my ($action, $title, $hash, $hash_base) = @_;
2909         my %args = ();
2910
2911         $args{'action'} = $action;
2912         $args{'hash'} = $hash if $hash;
2913         $args{'hash_base'} = $hash_base if $hash_base;
2914
2915     print q[<div class="header">],
2916               $cgi->a({-href => href(%args), -class => "title"},
2917               $title ? $title : $action),
2918                   q[</div>];
2919 }
2920
2921 sub git_print_authorship {
2922         my $co = shift;
2923
2924         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2925         print "<div class=\"author_date\">" .
2926               esc_html($co->{'author_name'}) .
2927               " [$ad{'rfc2822'}";
2928         if ($ad{'hour_local'} < 6) {
2929                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2930                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2931         } else {
2932                 printf(" (%02d:%02d %s)",
2933                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2934         }
2935         print "]</div>\n";
2936 }
2937
2938 sub git_print_page_path {
2939         my $name = shift;
2940         my $type = shift;
2941         my $hb = shift;
2942
2943
2944         print "<div class=\"page_path\">";
2945         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2946                       -title => 'tree root'}, to_utf8("[$project]"));
2947         print " / ";
2948         if (defined $name) {
2949                 my @dirname = split '/', $name;
2950                 my $basename = pop @dirname;
2951                 my $fullname = '';
2952
2953                 foreach my $dir (@dirname) {
2954                         $fullname .= ($fullname ? '/' : '') . $dir;
2955                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2956                                                      hash_base=>$hb),
2957                                       -title => $fullname}, esc_path($dir));
2958                         print " / ";
2959                 }
2960                 if (defined $type && $type eq 'blob') {
2961                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2962                                                      hash_base=>$hb),
2963                                       -title => $name}, esc_path($basename));
2964                 } elsif (defined $type && $type eq 'tree') {
2965                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2966                                                      hash_base=>$hb),
2967                                       -title => $name}, esc_path($basename));
2968                         print " / ";
2969                 } else {
2970                         print esc_path($basename);
2971                 }
2972         }
2973         print "<br/></div>\n";
2974 }
2975
2976 # sub git_print_log (\@;%) {
2977 sub git_print_log ($;%) {
2978         my $log = shift;
2979         my %opts = @_;
2980
2981         if ($opts{'-remove_title'}) {
2982                 # remove title, i.e. first line of log
2983                 shift @$log;
2984         }
2985         # remove leading empty lines
2986         while (defined $log->[0] && $log->[0] eq "") {
2987                 shift @$log;
2988         }
2989
2990         # print log
2991         my $signoff = 0;
2992         my $empty = 0;
2993         foreach my $line (@$log) {
2994                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2995                         $signoff = 1;
2996                         $empty = 0;
2997                         if (! $opts{'-remove_signoff'}) {
2998                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2999                                 next;
3000                         } else {
3001                                 # remove signoff lines
3002                                 next;
3003                         }
3004                 } else {
3005                         $signoff = 0;
3006                 }
3007
3008                 # print only one empty line
3009                 # do not print empty line after signoff
3010                 if ($line eq "") {
3011                         next if ($empty || $signoff);
3012                         $empty = 1;
3013                 } else {
3014                         $empty = 0;
3015                 }
3016
3017                 print format_log_line_html($line) . "<br/>\n";
3018         }
3019
3020         if ($opts{'-final_empty_line'}) {
3021                 # end with single empty line
3022                 print "<br/>\n" unless $empty;
3023         }
3024 }
3025
3026 # return link target (what link points to)
3027 sub git_get_link_target {
3028         my $hash = shift;
3029         my $link_target;
3030
3031         # read link
3032         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3033                 or return;
3034         {
3035                 local $/;
3036                 $link_target = <$fd>;
3037         }
3038         close $fd
3039                 or return;
3040
3041         return $link_target;
3042 }
3043
3044 # given link target, and the directory (basedir) the link is in,
3045 # return target of link relative to top directory (top tree);
3046 # return undef if it is not possible (including absolute links).
3047 sub normalize_link_target {
3048         my ($link_target, $basedir, $hash_base) = @_;
3049
3050         # we can normalize symlink target only if $hash_base is provided
3051         return unless $hash_base;
3052
3053         # absolute symlinks (beginning with '/') cannot be normalized
3054         return if (substr($link_target, 0, 1) eq '/');
3055
3056         # normalize link target to path from top (root) tree (dir)
3057         my $path;
3058         if ($basedir) {
3059                 $path = $basedir . '/' . $link_target;
3060         } else {
3061                 # we are in top (root) tree (dir)
3062                 $path = $link_target;
3063         }
3064
3065         # remove //, /./, and /../
3066         my @path_parts;
3067         foreach my $part (split('/', $path)) {
3068                 # discard '.' and ''
3069                 next if (!$part || $part eq '.');
3070                 # handle '..'
3071                 if ($part eq '..') {
3072                         if (@path_parts) {
3073                                 pop @path_parts;
3074                         } else {
3075                                 # link leads outside repository (outside top dir)
3076                                 return;
3077                         }
3078                 } else {
3079                         push @path_parts, $part;
3080                 }
3081         }
3082         $path = join('/', @path_parts);
3083
3084         return $path;
3085 }
3086
3087 # print tree entry (row of git_tree), but without encompassing <tr> element
3088 sub git_print_tree_entry {
3089         my ($t, $basedir, $hash_base, $have_blame) = @_;
3090
3091         my %base_key = ();
3092         $base_key{'hash_base'} = $hash_base if defined $hash_base;
3093
3094         # The format of a table row is: mode list link.  Where mode is
3095         # the mode of the entry, list is the name of the entry, an href,
3096         # and link is the action links of the entry.
3097
3098         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3099         if ($t->{'type'} eq "blob") {
3100                 print "<td class=\"list\">" .
3101                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3102                                                file_name=>"$basedir$t->{'name'}", %base_key),
3103                                 -class => "list"}, esc_path($t->{'name'}));
3104                 if (S_ISLNK(oct $t->{'mode'})) {
3105                         my $link_target = git_get_link_target($t->{'hash'});
3106                         if ($link_target) {
3107                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3108                                 if (defined $norm_target) {
3109                                         print " -> " .
3110                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3111                                                                      file_name=>$norm_target),
3112                                                        -title => $norm_target}, esc_path($link_target));
3113                                 } else {
3114                                         print " -> " . esc_path($link_target);
3115                                 }
3116                         }
3117                 }
3118                 print "</td>\n";
3119                 print "<td class=\"link\">";
3120                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3121                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3122                               "blob");
3123                 if ($have_blame) {
3124                         print " | " .
3125                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3126                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3127                                       "blame");
3128                 }
3129                 if (defined $hash_base) {
3130                         print " | " .
3131                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3132                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3133                                       "history");
3134                 }
3135                 print " | " .
3136                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3137                                                file_name=>"$basedir$t->{'name'}")},
3138                                 "raw");
3139                 print "</td>\n";
3140
3141         } elsif ($t->{'type'} eq "tree") {
3142                 print "<td class=\"list\">";
3143                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3144                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3145                               esc_path($t->{'name'}));
3146                 print "</td>\n";
3147                 print "<td class=\"link\">";
3148                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3149                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3150                               "tree");
3151                 if (defined $hash_base) {
3152                         print " | " .
3153                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3154                                                      file_name=>"$basedir$t->{'name'}")},
3155                                       "history");
3156                 }
3157                 print "</td>\n";
3158         } else {
3159                 # unknown object: we can only present history for it
3160                 # (this includes 'commit' object, i.e. submodule support)
3161                 print "<td class=\"list\">" .
3162                       esc_path($t->{'name'}) .
3163                       "</td>\n";
3164                 print "<td class=\"link\">";
3165                 if (defined $hash_base) {
3166                         print $cgi->a({-href => href(action=>"history",
3167                                                      hash_base=>$hash_base,
3168                                                      file_name=>"$basedir$t->{'name'}")},
3169                                       "history");
3170                 }
3171                 print "</td>\n";
3172         }
3173 }
3174
3175 ## ......................................................................
3176 ## functions printing large fragments of HTML
3177
3178 # get pre-image filenames for merge (combined) diff
3179 sub fill_from_file_info {
3180         my ($diff, @parents) = @_;
3181
3182         $diff->{'from_file'} = [ ];
3183         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3184         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3185                 if ($diff->{'status'}[$i] eq 'R' ||
3186                     $diff->{'status'}[$i] eq 'C') {
3187                         $diff->{'from_file'}[$i] =
3188                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3189                 }
3190         }
3191
3192         return $diff;
3193 }
3194
3195 # is current raw difftree line of file deletion
3196 sub is_deleted {
3197         my $diffinfo = shift;
3198
3199         return $diffinfo->{'to_id'} eq ('0' x 40);
3200 }
3201
3202 # does patch correspond to [previous] difftree raw line
3203 # $diffinfo  - hashref of parsed raw diff format
3204 # $patchinfo - hashref of parsed patch diff format
3205 #              (the same keys as in $diffinfo)
3206 sub is_patch_split {
3207         my ($diffinfo, $patchinfo) = @_;
3208
3209         return defined $diffinfo && defined $patchinfo
3210                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3211 }
3212
3213
3214 sub git_difftree_body {
3215         my ($difftree, $hash, @parents) = @_;
3216         my ($parent) = $parents[0];
3217         my $have_blame = gitweb_check_feature('blame');
3218         print "<div class=\"list_head\">\n";
3219         if ($#{$difftree} > 10) {
3220                 print(($#{$difftree} + 1) . " files changed:\n");
3221         }
3222         print "</div>\n";
3223
3224         print "<table class=\"" .
3225               (@parents > 1 ? "combined " : "") .
3226               "diff_tree\">\n";
3227
3228         # header only for combined diff in 'commitdiff' view
3229         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3230         if ($has_header) {
3231                 # table header
3232                 print "<thead><tr>\n" .
3233                        "<th></th><th></th>\n"; # filename, patchN link
3234                 for (my $i = 0; $i < @parents; $i++) {
3235                         my $par = $parents[$i];
3236                         print "<th>" .
3237                               $cgi->a({-href => href(action=>"commitdiff",
3238                                                      hash=>$hash, hash_parent=>$par),
3239                                        -title => 'commitdiff to parent number ' .
3240                                                   ($i+1) . ': ' . substr($par,0,7)},
3241                                       $i+1) .
3242                               "&nbsp;</th>\n";
3243                 }
3244                 print "</tr></thead>\n<tbody>\n";
3245         }
3246
3247         my $alternate = 1;
3248         my $patchno = 0;
3249         foreach my $line (@{$difftree}) {
3250                 my $diff = parsed_difftree_line($line);
3251
3252                 if ($alternate) {
3253                         print "<tr class=\"dark\">\n";
3254                 } else {
3255                         print "<tr class=\"light\">\n";
3256                 }
3257                 $alternate ^= 1;
3258
3259                 if (exists $diff->{'nparents'}) { # combined diff
3260
3261                         fill_from_file_info($diff, @parents)
3262                                 unless exists $diff->{'from_file'};
3263
3264                         if (!is_deleted($diff)) {
3265                                 # file exists in the result (child) commit
3266                                 print "<td>" .
3267                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3268                                                              file_name=>$diff->{'to_file'},
3269                                                              hash_base=>$hash),
3270                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3271                                       "</td>\n";
3272                         } else {
3273                                 print "<td>" .
3274                                       esc_path($diff->{'to_file'}) .
3275                                       "</td>\n";
3276                         }
3277
3278                         if ($action eq 'commitdiff') {
3279                                 # link to patch
3280                                 $patchno++;
3281                                 print "<td class=\"link\">" .
3282                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3283                                       " | " .
3284                                       "</td>\n";
3285                         }
3286
3287                         my $has_history = 0;
3288                         my $not_deleted = 0;
3289                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3290                                 my $hash_parent = $parents[$i];
3291                                 my $from_hash = $diff->{'from_id'}[$i];
3292                                 my $from_path = $diff->{'from_file'}[$i];
3293                                 my $status = $diff->{'status'}[$i];
3294
3295                                 $has_history ||= ($status ne 'A');
3296                                 $not_deleted ||= ($status ne 'D');
3297
3298                                 if ($status eq 'A') {
3299                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3300                                 } elsif ($status eq 'D') {
3301                                         print "<td class=\"link\">" .
3302                                               $cgi->a({-href => href(action=>"blob",
3303                                                                      hash_base=>$hash,
3304                                                                      hash=>$from_hash,
3305                                                                      file_name=>$from_path)},
3306                                                       "blob" . ($i+1)) .
3307                                               " | </td>\n";
3308                                 } else {
3309                                         if ($diff->{'to_id'} eq $from_hash) {
3310                                                 print "<td class=\"link nochange\">";
3311                                         } else {
3312                                                 print "<td class=\"link\">";
3313                                         }
3314                                         print $cgi->a({-href => href(action=>"blobdiff",
3315                                                                      hash=>$diff->{'to_id'},
3316                                                                      hash_parent=>$from_hash,
3317                                                                      hash_base=>$hash,
3318                                                                      hash_parent_base=>$hash_parent,
3319                                                                      file_name=>$diff->{'to_file'},
3320                                                                      file_parent=>$from_path)},
3321                                                       "diff" . ($i+1)) .
3322                                               " | </td>\n";
3323                                 }
3324                         }
3325
3326                         print "<td class=\"link\">";
3327                         if ($not_deleted) {
3328                                 print $cgi->a({-href => href(action=>"blob",
3329                                                              hash=>$diff->{'to_id'},
3330                                                              file_name=>$diff->{'to_file'},
3331                                                              hash_base=>$hash)},
3332                                               "blob");
3333                                 print " | " if ($has_history);
3334                         }
3335                         if ($has_history) {
3336                                 print $cgi->a({-href => href(action=>"history",
3337                                                              file_name=>$diff->{'to_file'},
3338                                                              hash_base=>$hash)},
3339                                               "history");
3340                         }
3341                         print "</td>\n";
3342
3343                         print "</tr>\n";
3344                         next; # instead of 'else' clause, to avoid extra indent
3345                 }
3346                 # else ordinary diff
3347
3348                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3349                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3350                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3351                         $to_mode_oct = oct $diff->{'to_mode'};
3352                         if (S_ISREG($to_mode_oct)) { # only for regular file
3353                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3354                         }
3355                         $to_file_type = file_type($diff->{'to_mode'});
3356                 }
3357                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3358                         $from_mode_oct = oct $diff->{'from_mode'};
3359                         if (S_ISREG($to_mode_oct)) { # only for regular file
3360                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3361                         }
3362                         $from_file_type = file_type($diff->{'from_mode'});
3363                 }
3364
3365                 if ($diff->{'status'} eq "A") { # created
3366                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3367                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3368                         $mode_chng   .= "]</span>";
3369                         print "<td>";
3370                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3371                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3372                                       -class => "list"}, esc_path($diff->{'file'}));
3373                         print "</td>\n";
3374                         print "<td>$mode_chng</td>\n";
3375                         print "<td class=\"link\">";
3376                         if ($action eq 'commitdiff') {
3377                                 # link to patch
3378                                 $patchno++;
3379                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3380                                 print " | ";
3381                         }
3382                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3383                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3384                                       "blob");
3385                         print "</td>\n";
3386
3387                 } elsif ($diff->{'status'} eq "D") { # deleted
3388                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3389                         print "<td>";
3390                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3391                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3392                                        -class => "list"}, esc_path($diff->{'file'}));
3393                         print "</td>\n";
3394                         print "<td>$mode_chng</td>\n";
3395                         print "<td class=\"link\">";
3396                         if ($action eq 'commitdiff') {
3397                                 # link to patch
3398                                 $patchno++;
3399                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3400                                 print " | ";
3401                         }
3402                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3403                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3404                                       "blob") . " | ";
3405                         if ($have_blame) {
3406                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3407                                                              file_name=>$diff->{'file'})},
3408                                               "blame") . " | ";
3409                         }
3410                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3411                                                      file_name=>$diff->{'file'})},
3412                                       "history");
3413                         print "</td>\n";
3414
3415                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3416                         my $mode_chnge = "";
3417                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3418                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3419                                 if ($from_file_type ne $to_file_type) {
3420                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3421                                 }
3422                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3423                                         if ($from_mode_str && $to_mode_str) {
3424                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3425                                         } elsif ($to_mode_str) {
3426                                                 $mode_chnge .= " mode: $to_mode_str";
3427                                         }
3428                                 }
3429                                 $mode_chnge .= "]</span>\n";
3430                         }
3431                         print "<td>";
3432                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3433                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3434                                       -class => "list"}, esc_path($diff->{'file'}));
3435                         print "</td>\n";
3436                         print "<td>$mode_chnge</td>\n";
3437                         print "<td class=\"link\">";
3438                         if ($action eq 'commitdiff') {
3439                                 # link to patch
3440                                 $patchno++;
3441                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3442                                       " | ";
3443                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3444                                 # "commit" view and modified file (not onlu mode changed)
3445                                 print $cgi->a({-href => href(action=>"blobdiff",
3446                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3447                                                              hash_base=>$hash, hash_parent_base=>$parent,
3448                                                              file_name=>$diff->{'file'})},
3449                                               "diff") .
3450                                       " | ";
3451                         }
3452                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3453                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3454                                        "blob") . " | ";
3455                         if ($have_blame) {
3456                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3457                                                              file_name=>$diff->{'file'})},
3458                                               "blame") . " | ";
3459                         }
3460                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3461                                                      file_name=>$diff->{'file'})},
3462                                       "history");
3463                         print "</td>\n";
3464
3465                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3466                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3467                         my $nstatus = $status_name{$diff->{'status'}};
3468                         my $mode_chng = "";
3469                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3470                                 # mode also for directories, so we cannot use $to_mode_str
3471                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3472                         }
3473                         print "<td>" .
3474                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3475                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3476                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3477                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3478                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3479                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3480                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3481                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3482                               "<td class=\"link\">";
3483                         if ($action eq 'commitdiff') {
3484                                 # link to patch
3485                                 $patchno++;
3486                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3487                                       " | ";
3488                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3489                                 # "commit" view and modified file (not only pure rename or copy)
3490                                 print $cgi->a({-href => href(action=>"blobdiff",
3491                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3492                                                              hash_base=>$hash, hash_parent_base=>$parent,
3493                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3494                                               "diff") .
3495                                       " | ";
3496                         }
3497                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3498                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3499                                       "blob") . " | ";
3500                         if ($have_blame) {
3501                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3502                                                              file_name=>$diff->{'to_file'})},
3503                                               "blame") . " | ";
3504                         }
3505                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3506                                                     file_name=>$diff->{'to_file'})},
3507                                       "history");
3508                         print "</td>\n";
3509
3510                 } # we should not encounter Unmerged (U) or Unknown (X) status
3511                 print "</tr>\n";
3512         }
3513         print "</tbody>" if $has_header;
3514         print "</table>\n";
3515 }
3516
3517 sub git_patchset_body {
3518         my ($fd, $difftree, $hash, @hash_parents) = @_;
3519         my ($hash_parent) = $hash_parents[0];
3520
3521         my $is_combined = (@hash_parents > 1);
3522         my $patch_idx = 0;
3523         my $patch_number = 0;
3524         my $patch_line;
3525         my $diffinfo;
3526         my $to_name;
3527         my (%from, %to);
3528
3529         print "<div class=\"patchset\">\n";
3530
3531         # skip to first patch
3532         while ($patch_line = <$fd>) {
3533                 chomp $patch_line;
3534
3535                 last if ($patch_line =~ m/^diff /);
3536         }
3537
3538  PATCH:
3539         while ($patch_line) {
3540
3541                 # parse "git diff" header line
3542                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3543                         # $1 is from_name, which we do not use
3544                         $to_name = unquote($2);
3545                         $to_name =~ s!^b/!!;
3546                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3547                         # $1 is 'cc' or 'combined', which we do not use
3548                         $to_name = unquote($2);
3549                 } else {
3550                         $to_name = undef;
3551                 }
3552
3553                 # check if current patch belong to current raw line
3554                 # and parse raw git-diff line if needed
3555                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3556                         # this is continuation of a split patch
3557                         print "<div class=\"patch cont\">\n";
3558                 } else {
3559                         # advance raw git-diff output if needed
3560                         $patch_idx++ if defined $diffinfo;
3561
3562                         # read and prepare patch information
3563                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3564
3565                         # compact combined diff output can have some patches skipped
3566                         # find which patch (using pathname of result) we are at now;
3567                         if ($is_combined) {
3568                                 while ($to_name ne $diffinfo->{'to_file'}) {
3569                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3570                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3571                                               "</div>\n";  # class="patch"
3572
3573                                         $patch_idx++;
3574                                         $patch_number++;
3575
3576                                         last if $patch_idx > $#$difftree;
3577                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3578                                 }
3579                         }
3580
3581                         # modifies %from, %to hashes
3582                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3583
3584                         # this is first patch for raw difftree line with $patch_idx index
3585                         # we index @$difftree array from 0, but number patches from 1
3586                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3587                 }
3588
3589                 # git diff header
3590                 #assert($patch_line =~ m/^diff /) if DEBUG;
3591                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3592                 $patch_number++;
3593                 # print "git diff" header
3594                 print format_git_diff_header_line($patch_line, $diffinfo,
3595                                                   \%from, \%to);
3596
3597                 # print extended diff header
3598                 print "<div class=\"diff extended_header\">\n";
3599         EXTENDED_HEADER:
3600                 while ($patch_line = <$fd>) {
3601                         chomp $patch_line;
3602
3603                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3604
3605                         print format_extended_diff_header_line($patch_line, $diffinfo,
3606                                                                \%from, \%to);
3607                 }
3608                 print "</div>\n"; # class="diff extended_header"
3609
3610                 # from-file/to-file diff header
3611                 if (! $patch_line) {
3612                         print "</div>\n"; # class="patch"
3613                         last PATCH;
3614                 }
3615                 next PATCH if ($patch_line =~ m/^diff /);
3616                 #assert($patch_line =~ m/^---/) if DEBUG;
3617
3618                 my $last_patch_line = $patch_line;
3619                 $patch_line = <$fd>;
3620                 chomp $patch_line;
3621                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3622
3623                 print format_diff_from_to_header($last_patch_line, $patch_line,
3624                                                  $diffinfo, \%from, \%to,
3625                                                  @hash_parents);
3626
3627                 # the patch itself
3628         LINE:
3629                 while ($patch_line = <$fd>) {
3630                         chomp $patch_line;
3631
3632                         next PATCH if ($patch_line =~ m/^diff /);
3633
3634                         print format_diff_line($patch_line, \%from, \%to);
3635                 }
3636
3637         } continue {
3638                 print "</div>\n"; # class="patch"
3639         }
3640
3641         # for compact combined (--cc) format, with chunk and patch simpliciaction
3642         # patchset might be empty, but there might be unprocessed raw lines
3643         for (++$patch_idx if $patch_number > 0;
3644              $patch_idx < @$difftree;
3645              ++$patch_idx) {
3646                 # read and prepare patch information
3647                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3648
3649                 # generate anchor for "patch" links in difftree / whatchanged part
3650                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3651                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3652                       "</div>\n";  # class="patch"
3653
3654                 $patch_number++;
3655         }
3656
3657         if ($patch_number == 0) {
3658                 if (@hash_parents > 1) {
3659                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3660                 } else {
3661                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3662                 }
3663         }
3664
3665         print "</div>\n"; # class="patchset"
3666 }
3667
3668 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3669
3670 # fills project list info (age, description, owner, forks) for each
3671 # project in the list, removing invalid projects from returned list
3672 # NOTE: modifies $projlist, but does not remove entries from it
3673 sub fill_project_list_info {
3674         my ($projlist, $check_forks) = @_;
3675         my @projects;
3676
3677         my $show_ctags = gitweb_check_feature('ctags');
3678  PROJECT:
3679         foreach my $pr (@$projlist) {
3680                 my (@activity) = git_get_last_activity($pr->{'path'});
3681                 unless (@activity) {
3682                         next PROJECT;
3683                 }
3684                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3685                 if (!defined $pr->{'descr'}) {
3686                         my $descr = git_get_project_description($pr->{'path'}) || "";
3687                         $descr = to_utf8($descr);
3688                         $pr->{'descr_long'} = $descr;
3689                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3690                 }
3691                 if (!defined $pr->{'owner'}) {
3692                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3693                 }
3694                 if ($check_forks) {
3695                         my $pname = $pr->{'path'};
3696                         if (($pname =~ s/\.git$//) &&
3697                             ($pname !~ /\/$/) &&
3698                             (-d "$projectroot/$pname")) {
3699                                 $pr->{'forks'} = "-d $projectroot/$pname";
3700                         }       else {
3701                                 $pr->{'forks'} = 0;
3702                         }
3703                 }
3704                 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3705                 push @projects, $pr;
3706         }
3707
3708         return @projects;
3709 }
3710
3711 # print 'sort by' <th> element, generating 'sort by $name' replay link
3712 # if that order is not selected
3713 sub print_sort_th {
3714         my ($name, $order, $header) = @_;
3715         $header ||= ucfirst($name);
3716
3717         if ($order eq $name) {
3718                 print "<th>$header</th>\n";
3719         } else {
3720                 print "<th>" .
3721                       $cgi->a({-href => href(-replay=>1, order=>$name),
3722                                -class => "header"}, $header) .
3723                       "</th>\n";
3724         }
3725 }
3726
3727 sub git_project_list_body {
3728         # actually uses global variable $project
3729         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3730
3731         my $check_forks = gitweb_check_feature('forks');
3732         my @projects = fill_project_list_info($projlist, $check_forks);
3733
3734         $order ||= $default_projects_order;
3735         $from = 0 unless defined $from;
3736         $to = $#projects if (!defined $to || $#projects < $to);
3737
3738         my %order_info = (
3739                 project => { key => 'path', type => 'str' },
3740                 descr => { key => 'descr_long', type => 'str' },
3741                 owner => { key => 'owner', type => 'str' },
3742                 age => { key => 'age', type => 'num' }
3743         );
3744         my $oi = $order_info{$order};
3745         if ($oi->{'type'} eq 'str') {
3746                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3747         } else {
3748                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3749         }
3750
3751         my $show_ctags = gitweb_check_feature('ctags');
3752         if ($show_ctags) {
3753                 my %ctags;
3754                 foreach my $p (@projects) {
3755                         foreach my $ct (keys %{$p->{'ctags'}}) {
3756                                 $ctags{$ct} += $p->{'ctags'}->{$ct};
3757                         }
3758                 }
3759                 my $cloud = git_populate_project_tagcloud(\%ctags);
3760                 print git_show_project_tagcloud($cloud, 64);
3761         }
3762
3763         print "<table class=\"project_list\">\n";
3764         unless ($no_header) {
3765                 print "<tr>\n";
3766                 if ($check_forks) {
3767                         print "<th></th>\n";
3768                 }
3769                 print_sort_th('project', $order, 'Project');
3770                 print_sort_th('descr', $order, 'Description');
3771                 print_sort_th('owner', $order, 'Owner');
3772                 print_sort_th('age', $order, 'Last Change');
3773                 print "<th></th>\n" . # for links
3774                       "</tr>\n";
3775         }
3776         my $alternate = 1;
3777         my $tagfilter = $c->req->param('by_tag');
3778         for (my $i = $from; $i <= $to; $i++) {
3779                 my $pr = $projects[$i];
3780
3781                 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3782                 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3783                         and not $pr->{'descr_long'} =~ /$searchtext/;
3784                 # Weed out forks or non-matching entries of search
3785                 if ($check_forks) {
3786                         my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3787                         $forkbase="^$forkbase" if $forkbase;
3788                         next if not $searchtext and not $tagfilter and $show_ctags
3789                                 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3790                 }
3791
3792                 if ($alternate) {
3793                         print "<tr class=\"dark\">\n";
3794                 } else {
3795                         print "<tr class=\"light\">\n";
3796                 }
3797                 $alternate ^= 1;
3798                 if ($check_forks) {
3799                         print "<td>";
3800                         if ($pr->{'forks'}) {
3801                                 print "<!-- $pr->{'forks'} -->\n";
3802                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3803                         }
3804                         print "</td>\n";
3805                 }
3806                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3807                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3808                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3809                                         -class => "list", -title => $pr->{'descr_long'}},
3810                                         esc_html($pr->{'descr'})) . "</td>\n" .
3811                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3812                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3813                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3814                       "<td class=\"link\">" .
3815                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3816                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3817                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3818                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3819                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3820                       "</td>\n" .
3821                       "</tr>\n";
3822         }
3823         if (defined $extra) {
3824                 print "<tr>\n";
3825                 if ($check_forks) {
3826                         print "<td></td>\n";
3827                 }
3828                 print "<td colspan=\"5\">$extra</td>\n" .
3829                       "</tr>\n";
3830         }
3831         print "</table>\n";
3832 }
3833
3834 sub git_shortlog_body {
3835         # uses global variable $project
3836         my ($commitlist, $from, $to, $refs, $extra) = @_;
3837
3838         $from = 0 unless defined $from;
3839         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3840
3841         print "<table class=\"shortlog\">\n";
3842         my $alternate = 1;
3843         for (my $i = $from; $i <= $to; $i++) {
3844                 my %co = %{$commitlist->[$i]};
3845                 my $commit = $co{'id'};
3846                 my $ref = format_ref_marker($refs, $commit);
3847                 if ($alternate) {
3848                         print "<tr class=\"dark\">\n";
3849                 } else {
3850                         print "<tr class=\"light\">\n";
3851                 }
3852                 $alternate ^= 1;
3853                 my $author = chop_and_escape_str($co{'author_name'}, 10);
3854                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3855                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3856                       "<td><i>" . $author . "</i></td>\n" .
3857                       "<td>";
3858                 print format_subject_html($co{'title'}, $co{'title_short'},
3859                                           href(action=>"commit", hash=>$commit), $ref);
3860                 print "</td>\n" .
3861                       "<td class=\"link\">" .
3862                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3863                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3864                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3865                 my $snapshot_links = format_snapshot_links($commit);
3866                 if (defined $snapshot_links) {
3867                         print " | " . $snapshot_links;
3868                 }
3869                 print "</td>\n" .
3870                       "</tr>\n";
3871         }
3872         if (defined $extra) {
3873                 print "<tr>\n" .
3874                       "<td colspan=\"4\">$extra</td>\n" .
3875                       "</tr>\n";
3876         }
3877         print "</table>\n";
3878 }
3879
3880 sub git_history_body {
3881         # Warning: assumes constant type (blob or tree) during history
3882         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3883
3884         $from = 0 unless defined $from;
3885         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3886
3887         print "<table class=\"history\">\n";
3888         my $alternate = 1;
3889         for (my $i = $from; $i <= $to; $i++) {
3890                 my %co = %{$commitlist->[$i]};
3891                 if (!%co) {
3892                         next;
3893                 }
3894                 my $commit = $co{'id'};
3895
3896                 my $ref = format_ref_marker($refs, $commit);
3897
3898                 if ($alternate) {
3899                         print "<tr class=\"dark\">\n";
3900                 } else {
3901                         print "<tr class=\"light\">\n";
3902                 }
3903                 $alternate ^= 1;
3904         # shortlog uses      chop_str($co{'author_name'}, 10)
3905                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3906                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3907                       "<td><i>" . $author . "</i></td>\n" .
3908                       "<td>";
3909                 # originally git_history used chop_str($co{'title'}, 50)
3910                 print format_subject_html($co{'title'}, $co{'title_short'},
3911                                           href(action=>"commit", hash=>$commit), $ref);
3912                 print "</td>\n" .
3913                       "<td class=\"link\">" .
3914                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3915                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3916
3917                 if ($ftype eq 'blob') {
3918                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3919                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3920                         if (defined $blob_current && defined $blob_parent &&
3921                                         $blob_current ne $blob_parent) {
3922                                 print " | " .
3923                                         $cgi->a({-href => href(action=>"blobdiff",
3924                                                                hash=>$blob_current, hash_parent=>$blob_parent,
3925                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
3926                                                                file_name=>$file_name)},
3927                                                 "diff to current");
3928                         }
3929                 }
3930                 print "</td>\n" .
3931                       "</tr>\n";
3932         }
3933         if (defined $extra) {
3934                 print "<tr>\n" .
3935                       "<td colspan=\"4\">$extra</td>\n" .
3936                       "</tr>\n";
3937         }
3938         print "</table>\n";
3939 }
3940
3941 sub git_tags_body {
3942         # uses global variable $project
3943         my ($taglist, $from, $to, $extra) = @_;
3944         $from = 0 unless defined $from;
3945         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3946
3947         print "<table class=\"tags\">\n";
3948         my $alternate = 1;
3949         for (my $i = $from; $i <= $to; $i++) {
3950                 my $entry = $taglist->[$i];
3951                 my %tag = %$entry;
3952                 my $comment = $tag{'subject'};
3953                 my $comment_short;
3954                 if (defined $comment) {
3955                         $comment_short = chop_str($comment, 30, 5);
3956                 }
3957                 if ($alternate) {
3958                         print "<tr class=\"dark\">\n";
3959                 } else {
3960                         print "<tr class=\"light\">\n";
3961                 }
3962                 $alternate ^= 1;
3963                 if (defined $tag{'age'}) {
3964                         print "<td><i>$tag{'age'}</i></td>\n";
3965                 } else {
3966                         print "<td></td>\n";
3967                 }
3968                 print "<td>" .
3969                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3970                                -class => "list name"}, esc_html($tag{'name'})) .
3971                       "</td>\n" .
3972                       "<td>";
3973                 if (defined $comment) {
3974                         print format_subject_html($comment, $comment_short,
3975                                                   href(action=>"tag", hash=>$tag{'id'}));
3976                 }
3977                 print "</td>\n" .
3978                       "<td class=\"selflink\">";
3979                 if ($tag{'type'} eq "tag") {
3980                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3981                 } else {
3982                         print "&nbsp;";
3983                 }
3984                 print "</td>\n" .
3985                       "<td class=\"link\">" . " | " .
3986                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3987                 if ($tag{'reftype'} eq "commit") {
3988                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3989                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3990                 } elsif ($tag{'reftype'} eq "blob") {
3991                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3992                 }
3993                 print "</td>\n" .
3994                       "</tr>";
3995         }
3996         if (defined $extra) {
3997                 print "<tr>\n" .
3998                       "<td colspan=\"5\">$extra</td>\n" .
3999                       "</tr>\n";
4000         }
4001         print "</table>\n";
4002 }
4003
4004 sub git_heads_body {
4005         # uses global variable $project
4006         my ($headlist, $head, $from, $to, $extra) = @_;
4007         $from = 0 unless defined $from;
4008         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4009
4010         print "<table class=\"heads\">\n";
4011         my $alternate = 1;
4012         for (my $i = $from; $i <= $to; $i++) {
4013                 my $entry = $headlist->[$i];
4014                 my %ref = %$entry;
4015                 my $curr = $ref{'id'} eq $head;
4016                 if ($alternate) {
4017                         print "<tr class=\"dark\">\n";
4018                 } else {
4019                         print "<tr class=\"light\">\n";
4020                 }
4021                 $alternate ^= 1;
4022                 print "<td><i>$ref{'age'}</i></td>\n" .
4023                       ($curr ? "<td class=\"current_head\">" : "<td>") .
4024                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4025                                -class => "list name"},esc_html($ref{'name'})) .
4026                       "</td>\n" .
4027                       "<td class=\"link\">" .
4028                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4029                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4030                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4031                       "</td>\n" .
4032                       "</tr>";
4033         }
4034         if (defined $extra) {
4035                 print "<tr>\n" .
4036                       "<td colspan=\"3\">$extra</td>\n" .
4037                       "</tr>\n";
4038         }
4039         print "</table>\n";
4040 }
4041
4042 sub git_search_grep_body {
4043         my ($commitlist, $from, $to, $extra) = @_;
4044         $from = 0 unless defined $from;
4045         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4046
4047         print "<table class=\"commit_search\">\n";
4048         my $alternate = 1;
4049         for (my $i = $from; $i <= $to; $i++) {
4050                 my %co = %{$commitlist->[$i]};
4051                 if (!%co) {
4052                         next;
4053                 }
4054                 my $commit = $co{'id'};
4055                 if ($alternate) {
4056                         print "<tr class=\"dark\">\n";
4057                 } else {
4058                         print "<tr class=\"light\">\n";
4059                 }
4060                 $alternate ^= 1;
4061                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4062                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4063                       "<td><i>" . $author . "</i></td>\n" .
4064                       "<td>" .
4065                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4066                                -class => "list subject"},
4067                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
4068                 my $comment = $co{'comment'};
4069                 foreach my $line (@$comment) {
4070                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4071                                 my ($lead, $match, $trail) = ($1, $2, $3);
4072                                 $match = chop_str($match, 70, 5, 'center');
4073                                 my $contextlen = int((80 - length($match))/2);
4074                                 $contextlen = 30 if ($contextlen > 30);
4075                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
4076                                 $trail = chop_str($trail, $contextlen, 10, 'right');
4077
4078                                 $lead  = esc_html($lead);
4079                                 $match = esc_html($match);
4080                                 $trail = esc_html($trail);
4081
4082                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
4083                         }
4084                 }
4085                 print "</td>\n" .
4086                       "<td class=\"link\">" .
4087                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4088                       " | " .
4089                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4090                       " | " .
4091                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4092                 print "</td>\n" .
4093                       "</tr>\n";
4094         }
4095         if (defined $extra) {
4096                 print "<tr>\n" .
4097                       "<td colspan=\"3\">$extra</td>\n" .
4098                       "</tr>\n";
4099         }
4100         print "</table>\n";
4101 }
4102
4103 ## ======================================================================
4104 ## ======================================================================
4105 ## actions
4106
4107 sub git_project_list {
4108         my $order = $input_params{'order'};
4109         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4110                 die_error(400, "Unknown order parameter");
4111         }
4112
4113         my @list = git_get_projects_list();
4114         if (!@list) {
4115                 die_error(404, "No projects found");
4116         }
4117
4118         if (-f $home_text) {
4119                 print "<div class=\"index_include\">\n";
4120                 print insert_file($home_text);
4121                 print "</div>\n";
4122         }
4123         print $cgi->startform(-method => "get") .
4124               "<p class=\"projsearch\">Search:\n" .
4125               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4126               "</p>" .
4127               $cgi->end_form() . "\n";
4128         git_project_list_body(\@list, $order);
4129 }
4130
4131 sub git_forks {
4132         my $order = $input_params{'order'};
4133         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4134                 die_error(400, "Unknown order parameter");
4135         }
4136
4137         my @list = git_get_projects_list($project);
4138         if (!@list) {
4139                 die_error(404, "No forks found");
4140         }
4141
4142         git_print_page_nav('','');
4143         git_print_header_div('summary', "$project forks");
4144         git_project_list_body(\@list, $order);
4145 }
4146
4147 sub git_project_index {
4148         my @projects = git_get_projects_list($project);
4149
4150         print $cgi->header(
4151                 -type => 'text/plain',
4152                 -charset => 'utf-8',
4153                 -content_disposition => 'inline; filename="index.aux"');
4154
4155         foreach my $pr (@projects) {
4156                 if (!exists $pr->{'owner'}) {
4157                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4158                 }
4159
4160                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4161                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4162                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4163                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4164                 $path  =~ s/ /\+/g;
4165                 $owner =~ s/ /\+/g;
4166
4167                 print "$path $owner\n";
4168         }
4169 }
4170
4171 sub git_summary {
4172         my $descr = git_get_project_description($project) || "none";
4173         my %co = parse_commit("HEAD");
4174         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4175         my $head = $co{'id'};
4176
4177         my $owner = git_get_project_owner($project);
4178
4179         my $refs = git_get_references();
4180         # These get_*_list functions return one more to allow us to see if
4181         # there are more ...
4182         my @taglist  = git_get_tags_list(16);
4183         my @headlist = git_get_heads_list(16);
4184         my @forklist;
4185         my $check_forks = gitweb_check_feature('forks');
4186
4187         if ($check_forks) {
4188                 @forklist = git_get_projects_list($project);
4189         }
4190
4191         git_print_page_nav('summary','', $head);
4192
4193         print "<div class=\"title\">&nbsp;</div>\n";
4194         print "<table class=\"projects_list\">\n" .
4195               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4196               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4197         if (defined $cd{'rfc2822'}) {
4198                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4199         }
4200
4201         # use per project git URL list in $projectroot/$project/cloneurl
4202         # or make project git URL from git base URL and project name
4203         my $url_tag = "URL";
4204         my @url_list = git_get_project_url_list($project);
4205         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4206         foreach my $git_url (@url_list) {
4207                 next unless $git_url;
4208                 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4209                 $url_tag = "";
4210         }
4211
4212         # Tag cloud
4213         my $show_ctags = gitweb_check_feature('ctags');
4214         if ($show_ctags) {
4215                 my $ctags = git_get_project_ctags($project);
4216                 my $cloud = git_populate_project_tagcloud($ctags);
4217                 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4218                 print "</td>\n<td>" unless %$ctags;
4219                 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4220                 print "</td>\n<td>" if %$ctags;
4221                 print git_show_project_tagcloud($cloud, 48);
4222                 print "</td></tr>";
4223         }
4224
4225         print "</table>\n";
4226
4227         # If XSS prevention is on, we don't include README.html.
4228         # TODO: Allow a readme in some safe format.
4229         if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4230                 print "<div class=\"title\">readme</div>\n" .
4231                       "<div class=\"readme\">\n";
4232                 print insert_file("$projectroot/$project/README.html");
4233                 print "\n</div>\n"; # class="readme"
4234         }
4235
4236         # we need to request one more than 16 (0..15) to check if
4237         # those 16 are all
4238         my @commitlist = $head ? parse_commits($head, 17) : ();
4239         if (@commitlist) {
4240                 git_print_header_div('shortlog');
4241                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4242                                   $#commitlist <=  15 ? undef :
4243                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4244         }
4245
4246         if (@taglist) {
4247                 git_print_header_div('tags');
4248                 git_tags_body(\@taglist, 0, 15,
4249                               $#taglist <=  15 ? undef :
4250                               $cgi->a({-href => href(action=>"tags")}, "..."));
4251         }
4252
4253         if (@headlist) {
4254                 git_print_header_div('heads');
4255                 git_heads_body(\@headlist, $head, 0, 15,
4256                                $#headlist <= 15 ? undef :
4257                                $cgi->a({-href => href(action=>"heads")}, "..."));
4258         }
4259
4260         if (@forklist) {
4261                 git_print_header_div('forks');
4262                 git_project_list_body(\@forklist, 'age', 0, 15,
4263                                       $#forklist <= 15 ? undef :
4264                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4265                                       'no_header');
4266         }
4267
4268 }
4269
4270 sub git_tag {
4271         my $head = git_get_head_hash($project);
4272         git_print_page_nav('','', $head,undef,$head);
4273         my %tag = parse_tag($hash);
4274
4275         if (! %tag) {
4276                 die_error(404, "Unknown tag object");
4277         }
4278
4279         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4280         print "<div class=\"title_text\">\n" .
4281               "<table class=\"object_header\">\n" .
4282               "<tr>\n" .
4283               "<td>object</td>\n" .
4284               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4285                                $tag{'object'}) . "</td>\n" .
4286               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4287                                               $tag{'type'}) . "</td>\n" .
4288               "</tr>\n";
4289         if (defined($tag{'author'})) {
4290                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4291                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4292                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4293                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4294                         "</td></tr>\n";
4295         }
4296         print "</table>\n\n" .
4297               "</div>\n";
4298         print "<div class=\"page_body\">";
4299         my $comment = $tag{'comment'};
4300         foreach my $line (@$comment) {
4301                 chomp $line;
4302                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4303         }
4304         print "</div>\n";
4305 }
4306
4307 sub git_blame {
4308         # permissions
4309         gitweb_check_feature('blame')
4310                 or die_error(403, "Blame view not allowed");
4311
4312         # error checking
4313         die_error(400, "No file name given") unless $file_name;
4314         $hash_base ||= git_get_head_hash($project);
4315         die_error(404, "Couldn't find base commit") unless $hash_base;
4316         my %co = parse_commit($hash_base)
4317                 or die_error(404, "Commit not found");
4318         my $ftype = "blob";
4319         if (!defined $hash) {
4320                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4321                         or die_error(404, "Error looking up file");
4322         } else {
4323                 $ftype = git_get_type($hash);
4324                 if ($ftype !~ "blob") {
4325                         die_error(400, "Object is not a blob - $hash");
4326                 }
4327         }
4328
4329         # run git-blame --porcelain
4330         open my $fd, "-|", git_cmd(), "blame", '-p',
4331                 $hash_base, '--', $file_name
4332                 or die_error(500, "Open git-blame failed");
4333
4334         # page header
4335         my $formats_nav =
4336                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4337                         "blob") .
4338                 " | " .
4339                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4340                         "history") .
4341                 " | " .
4342                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4343                         "HEAD");
4344         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4345         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4346         git_print_page_path($file_name, $ftype, $hash_base);
4347
4348         # page body
4349         my @rev_color = qw(light2 dark2);
4350         my $num_colors = scalar(@rev_color);
4351         my $current_color = 0;
4352         my %metainfo = ();
4353
4354         print <<HTML;
4355 <div class="page_body">
4356 <table class="blame">
4357 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4358 HTML
4359  LINE:
4360         while (my $line = <$fd>) {
4361                 chomp $line;
4362                 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4363                 # no <lines in group> for subsequent lines in group of lines
4364                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4365                    ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4366                 if (!exists $metainfo{$full_rev}) {
4367                         $metainfo{$full_rev} = {};
4368                 }
4369                 my $meta = $metainfo{$full_rev};
4370                 my $data;
4371                 while ($data = <$fd>) {
4372                         chomp $data;
4373                         last if ($data =~ s/^\t//); # contents of line
4374                         if ($data =~ /^(\S+) (.*)$/) {
4375                                 $meta->{$1} = $2;
4376                         }
4377                 }
4378                 my $short_rev = substr($full_rev, 0, 8);
4379                 my $author = $meta->{'author'};
4380                 my %date =
4381                         parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4382                 my $date = $date{'iso-tz'};
4383                 if ($group_size) {
4384                         $current_color = ($current_color + 1) % $num_colors;
4385                 }
4386                 print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
4387                 if ($group_size) {
4388                         print "<td class=\"sha1\"";
4389                         print " title=\"". esc_html($author) . ", $date\"";
4390                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4391                         print ">";
4392                         print $cgi->a({-href => href(action=>"commit",
4393                                                      hash=>$full_rev,
4394                                                      file_name=>$file_name)},
4395                                       esc_html($short_rev));
4396                         print "</td>\n";
4397                 }
4398                 my $parent_commit;
4399                 if (!exists $meta->{'parent'}) {
4400                         open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4401                                 or die_error(500, "Open git-rev-parse failed");
4402                         $parent_commit = <$dd>;
4403                         close $dd;
4404                         chomp($parent_commit);
4405                         $meta->{'parent'} = $parent_commit;
4406                 } else {
4407                         $parent_commit = $meta->{'parent'};
4408                 }
4409                 my $blamed = href(action => 'blame',
4410                                   file_name => $meta->{'filename'},
4411                                   hash_base => $parent_commit);
4412                 print "<td class=\"linenr\">";
4413                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4414                                 -class => "linenr" },
4415                               esc_html($lineno));
4416                 print "</td>";
4417                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4418                 print "</tr>\n";
4419         }
4420         print "</table>\n";
4421         print "</div>";
4422         close $fd
4423                 or print "Reading blob failed\n";
4424
4425         # page footer
4426 }
4427
4428 sub git_tags {
4429         my $head = git_get_head_hash($project);
4430         git_print_page_nav('','', $head,undef,$head);
4431         git_print_header_div('summary', $project);
4432
4433         my @tagslist = git_get_tags_list();
4434         if (@tagslist) {
4435                 git_tags_body(\@tagslist);
4436         }
4437 }
4438
4439 sub git_heads {
4440         my $head = git_get_head_hash($project);
4441         git_print_page_nav('','', $head,undef,$head);
4442         git_print_header_div('summary', $project);
4443
4444         my @headslist = git_get_heads_list();
4445         if (@headslist) {
4446                 git_heads_body(\@headslist, $head);
4447         }
4448 }
4449
4450 sub git_blob_plain {
4451         my $type = shift;
4452         my $expires;
4453
4454         if (!defined $hash) {
4455                 if (defined $file_name) {
4456                         my $base = $hash_base || git_get_head_hash($project);
4457                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4458                                 or die_error(404, "Cannot find file");
4459                 } else {
4460                         die_error(400, "No file name defined");
4461                 }
4462         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4463                 # blobs defined by non-textual hash id's can be cached
4464                 $expires = "+1d";
4465         }
4466
4467         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4468                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4469
4470         # content-type (can include charset)
4471         $type = blob_contenttype($fd, $file_name, $type);
4472
4473         # "save as" filename, even when no $file_name is given
4474         my $save_as = "$hash";
4475         if (defined $file_name) {
4476                 $save_as = $file_name;
4477         } elsif ($type =~ m/^text\//) {
4478                 $save_as .= '.txt';
4479         }
4480
4481         # With XSS prevention on, blobs of all types except a few known safe
4482         # ones are served with "Content-Disposition: attachment" to make sure
4483         # they don't run in our security domain.  For certain image types,
4484         # blob view writes an <img> tag referring to blob_plain view, and we
4485         # want to be sure not to break that by serving the image as an
4486         # attachment (though Firefox 3 doesn't seem to care).
4487         my $sandbox = $prevent_xss &&
4488                 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4489
4490         print $cgi->header(
4491                 -type => $type,
4492                 -expires => $expires,
4493                 -content_disposition =>
4494                         ($sandbox ? 'attachment' : 'inline')
4495                         . '; filename="' . $save_as . '"');
4496         undef $/;
4497         binmode STDOUT, ':raw';
4498         print <$fd>;
4499         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4500         $/ = "\n";
4501         close $fd;
4502 }
4503
4504 sub git_blob {
4505         my $expires;
4506
4507         if (!defined $hash) {
4508                 if (defined $file_name) {
4509                         my $base = $hash_base || git_get_head_hash($project);
4510                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4511                                 or die_error(404, "Cannot find file");
4512                 } else {
4513                         die_error(400, "No file name defined");
4514                 }
4515         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4516                 # blobs defined by non-textual hash id's can be cached
4517                 $expires = "+1d";
4518         }
4519
4520         my $have_blame = gitweb_check_feature('blame');
4521         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4522                 or die_error(500, "Couldn't cat $file_name, $hash");
4523         my $mimetype = blob_mimetype($fd, $file_name);
4524         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4525                 close $fd;
4526                 return git_blob_plain($mimetype);
4527         }
4528         # we can have blame only for text/* mimetype
4529         $have_blame &&= ($mimetype =~ m!^text/!);
4530
4531         my $formats_nav = '';
4532         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4533                 if (defined $file_name) {
4534                         if ($have_blame) {
4535                                 $formats_nav .=
4536                                         $cgi->a({-href => href(action=>"blame", hash => $hash, -replay=>1)},
4537                                                 "blame") .
4538                                         " | ";
4539                         }
4540                         $formats_nav .=
4541                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4542                                         "history") .
4543                                 " | " .
4544                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4545                                         "raw") .
4546                                 " | " .
4547                                 $cgi->a({-href => href(action=>"blob",
4548                                                        hash_base=>"HEAD", file_name=>$file_name)},
4549                                         "HEAD");
4550                 } else {
4551                         $formats_nav .=
4552                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4553                                         "raw");
4554                 }
4555                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4556                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4557         } else {
4558                 print "<div class=\"page_nav\">\n" .
4559                       "<br/><br/></div>\n" .
4560                       "<div class=\"title\">$hash</div>\n";
4561         }
4562         git_print_page_path($file_name, "blob", $hash_base);
4563         print "<div class=\"page_body\">\n";
4564         if ($mimetype =~ m!^image/!) {
4565                 print qq!<img type="$mimetype"!;
4566                 if ($file_name) {
4567                         print qq! alt="$file_name" title="$file_name"!;
4568                 }
4569                 print qq! src="! .
4570                       href(action=>"blob_plain", hash=>$hash,
4571                            hash_base=>$hash_base, file_name=>$file_name) .
4572                       qq!" />\n!;
4573         } else {
4574                 my $nr;
4575                 while (my $line = <$fd>) {
4576                         chomp $line;
4577                         $nr++;
4578                         $line = untabify($line);
4579                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4580                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4581                 }
4582         }
4583         close $fd
4584                 or print "Reading blob failed.\n";
4585         print "</div>";
4586 }
4587
4588 sub git_tree {
4589         if (!defined $hash_base) {
4590                 $hash_base = "HEAD";
4591         }
4592         if (!defined $hash) {
4593                 if (defined $file_name) {
4594                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4595                 } else {
4596                         $hash = $hash_base;
4597                 }
4598         }
4599         die_error(404, "No such tree") unless defined($hash);
4600         $/ = "\0";
4601         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4602                 or die_error(500, "Open git-ls-tree failed");
4603         my @entries = map { chomp; $_ } <$fd>;
4604         close $fd or die_error(404, "Reading tree failed");
4605         $/ = "\n";
4606
4607         my $refs = git_get_references();
4608         my $ref = format_ref_marker($refs, $hash_base);
4609         my $basedir = '';
4610         my $have_blame = gitweb_check_feature('blame');
4611         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4612                 my @views_nav = ();
4613                 if (defined $file_name) {
4614                         push @views_nav,
4615                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4616                                         "history"),
4617                                 $cgi->a({-href => href(action=>"tree",
4618                                                        hash_base=>"HEAD", file_name=>$file_name)},
4619                                         "HEAD"),
4620                 }
4621                 my $snapshot_links = format_snapshot_links($hash);
4622                 if (defined $snapshot_links) {
4623                         # FIXME: Should be available when we have no hash base as well.
4624                         push @views_nav, $snapshot_links;
4625                 }
4626                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4627                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4628         } else {
4629                 undef $hash_base;
4630                 print "<div class=\"page_nav\">\n";
4631                 print "<br/><br/></div>\n";
4632                 print "<div class=\"title\">$hash</div>\n";
4633         }
4634         if (defined $file_name) {
4635                 $basedir = $file_name;
4636                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4637                         $basedir .= '/';
4638                 }
4639                 git_print_page_path($file_name, 'tree', $hash_base);
4640         }
4641         print "<div class=\"page_body\">\n";
4642         print "<table class=\"tree\">\n";
4643         my $alternate = 1;
4644         # '..' (top directory) link if possible
4645         if (defined $hash_base &&
4646             defined $file_name && $file_name =~ m![^/]+$!) {
4647                 if ($alternate) {
4648                         print "<tr class=\"dark\">\n";
4649                 } else {
4650                         print "<tr class=\"light\">\n";
4651                 }
4652                 $alternate ^= 1;
4653
4654                 my $up = $file_name;
4655                 $up =~ s!/?[^/]+$!!;
4656                 undef $up unless $up;
4657                 # based on git_print_tree_entry
4658                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4659                 print '<td class="list">';
4660                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4661                                              file_name=>$up)},
4662                               "..");
4663                 print "</td>\n";
4664                 print "<td class=\"link\"></td>\n";
4665
4666                 print "</tr>\n";
4667         }
4668         foreach my $line (@entries) {
4669                 my %t = parse_ls_tree_line($line, -z => 1);
4670
4671                 if ($alternate) {
4672                         print "<tr class=\"dark\">\n";
4673                 } else {
4674                         print "<tr class=\"light\">\n";
4675                 }
4676                 $alternate ^= 1;
4677
4678                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4679
4680                 print "</tr>\n";
4681         }
4682         print "</table>\n" .
4683               "</div>";
4684 }
4685
4686 sub git_snapshot {
4687         my $format = $input_params{'snapshot_format'};
4688         if (!@snapshot_fmts) {
4689                 die_error(403, "Snapshots not allowed");
4690         }
4691         # default to first supported snapshot format
4692         $format ||= $snapshot_fmts[0];
4693         if ($format !~ m/^[a-z0-9]+$/) {
4694                 die_error(400, "Invalid snapshot format parameter");
4695         } elsif (!exists($known_snapshot_formats{$format})) {
4696                 die_error(400, "Unknown snapshot format");
4697         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
4698                 die_error(403, "Unsupported snapshot format");
4699         }
4700
4701         if (!defined $hash) {
4702                 $hash = git_get_head_hash($project);
4703         }
4704
4705         my $name = $project;
4706         $name =~ s,([^/])/*\.git$,$1,;
4707         $name = basename($name);
4708         my $filename = to_utf8($name);
4709         $name =~ s/\047/\047\\\047\047/g;
4710         my $cmd;
4711         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4712         $cmd = quote_command(
4713                 git_cmd(), 'archive',
4714                 "--format=$known_snapshot_formats{$format}{'format'}",
4715                 "--prefix=$name/", $hash);
4716         if (exists $known_snapshot_formats{$format}{'compressor'}) {
4717                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4718         }
4719
4720         print $cgi->header(
4721                 -type => $known_snapshot_formats{$format}{'type'},
4722                 -content_disposition => 'inline; filename="' . "$filename" . '"',
4723                 -status => '200 OK');
4724
4725         open my $fd, "-|", $cmd
4726                 or die_error(500, "Execute git-archive failed");
4727         binmode STDOUT, ':raw';
4728         print <$fd>;
4729         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4730         close $fd;
4731 }
4732
4733 sub git_log {
4734         my $head = git_get_head_hash($project);
4735         if (!defined $hash) {
4736                 $hash = $head;
4737         }
4738         if (!defined $page) {
4739                 $page = 0;
4740         }
4741         my $refs = git_get_references();
4742
4743         my @commitlist = parse_commits($hash, 101, (100 * $page));
4744
4745         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4746
4747         my ($patch_max) = gitweb_get_feature('patches');
4748         if ($patch_max) {
4749                 if ($patch_max < 0 || @commitlist <= $patch_max) {
4750                         $paging_nav .= " &sdot; " .
4751                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
4752                                         "patches");
4753                 }
4754         }
4755
4756         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4757
4758         if (!@commitlist) {
4759                 my %co = parse_commit($hash);
4760
4761                 git_print_header_div('summary', $project);
4762                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4763         }
4764         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4765         for (my $i = 0; $i <= $to; $i++) {
4766                 my %co = %{$commitlist[$i]};
4767                 next if !%co;
4768                 my $commit = $co{'id'};
4769                 my $ref = format_ref_marker($refs, $commit);
4770                 my %ad = parse_date($co{'author_epoch'});
4771                 git_print_header_div('commit',
4772                                "<span class=\"age\">$co{'age_string'}</span>" .
4773                                esc_html($co{'title'}) . $ref,
4774                                $commit);
4775                 print "<div class=\"title_text\">\n" .
4776                       "<div class=\"log_link\">\n" .
4777                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4778                       " | " .
4779                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4780                       " | " .
4781                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4782                       "<br/>\n" .
4783                       "</div>\n" .
4784                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4785                       "</div>\n";
4786
4787                 print "<div class=\"log_body\">\n";
4788                 git_print_log($co{'comment'}, -final_empty_line=> 1);
4789                 print "</div>\n";
4790         }
4791         if ($#commitlist >= 100) {
4792                 print "<div class=\"page_nav\">\n";
4793                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4794                                -accesskey => "n", -title => "Alt-n"}, "next");
4795                 print "</div>\n";
4796         }
4797 }
4798
4799 sub git_commit {
4800         $hash ||= $hash_base || "HEAD";
4801         my %co = parse_commit($hash)
4802             or die_error(404, "Unknown commit object");
4803         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4804         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4805
4806         my $parent  = $co{'parent'};
4807         my $parents = $co{'parents'}; # listref
4808
4809         # we need to prepare $formats_nav before any parameter munging
4810         my $formats_nav;
4811         if (!defined $parent) {
4812                 # --root commitdiff
4813                 $formats_nav .= '(initial)';
4814         } elsif (@$parents == 1) {
4815                 # single parent commit
4816                 $formats_nav .=
4817                         '(parent: ' .
4818                         $cgi->a({-href => href(action=>"commit",
4819                                                hash=>$parent)},
4820                                 esc_html(substr($parent, 0, 7))) .
4821                         ')';
4822         } else {
4823                 # merge commit
4824                 $formats_nav .=
4825                         '(merge: ' .
4826                         join(' ', map {
4827                                 $cgi->a({-href => href(action=>"commit",
4828                                                        hash=>$_)},
4829                                         esc_html(substr($_, 0, 7)));
4830                         } @$parents ) .
4831                         ')';
4832         }
4833         if (gitweb_check_feature('patches')) {
4834                 $formats_nav .= " | " .
4835                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
4836                                 "patch");
4837         }
4838
4839         if (!defined $parent) {
4840                 $parent = "--root";
4841         }
4842         my @difftree;
4843         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4844                 @diff_opts,
4845                 (@$parents <= 1 ? $parent : '-c'),
4846                 $hash, "--"
4847                 or die_error(500, "Open git-diff-tree failed");
4848         @difftree = map { chomp; $_ } <$fd>;
4849         close $fd or die_error(404, "Reading git-diff-tree failed");
4850
4851         # non-textual hash id's can be cached
4852         my $expires;
4853         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4854                 $expires = "+1d";
4855         }
4856         my $refs = git_get_references();
4857         my $ref = format_ref_marker($refs, $co{'id'});
4858
4859         git_print_page_nav('commit', '',
4860                            $hash, $co{'tree'}, $hash,
4861                            $formats_nav);
4862
4863         if (defined $co{'parent'}) {
4864                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4865         } else {
4866                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4867         }
4868         print "<div class=\"title_text\">\n" .
4869               "<table class=\"object_header\">\n";
4870         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4871               "<tr>" .
4872               "<td></td><td> $ad{'rfc2822'}";
4873         if ($ad{'hour_local'} < 6) {
4874                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4875                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4876         } else {
4877                 printf(" (%02d:%02d %s)",
4878                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4879         }
4880         print "</td>" .
4881               "</tr>\n";
4882         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4883         print "<tr><td></td><td> $cd{'rfc2822'}" .
4884               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4885               "</td></tr>\n";
4886         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4887         print "<tr>" .
4888               "<td>tree</td>" .
4889               "<td class=\"sha1\">" .
4890               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4891                        class => "list"}, $co{'tree'}) .
4892               "</td>" .
4893               "<td class=\"link\">" .
4894               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4895                       "tree");
4896         my $snapshot_links = format_snapshot_links($hash);
4897         if (defined $snapshot_links) {
4898                 print " | " . $snapshot_links;
4899         }
4900         print "</td>" .
4901               "</tr>\n";
4902
4903         foreach my $par (@$parents) {
4904                 print "<tr>" .
4905                       "<td>parent</td>" .
4906                       "<td class=\"sha1\">" .
4907                       $cgi->a({-href => href(action=>"commit", hash=>$par),
4908                                class => "list"}, $par) .
4909                       "</td>" .
4910                       "<td class=\"link\">" .
4911                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4912                       " | " .
4913                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4914                       "</td>" .
4915                       "</tr>\n";
4916         }
4917         print "</table>".
4918               "</div>\n";
4919
4920         print "<div class=\"page_body\">\n";
4921         git_print_log($co{'comment'});
4922         print "</div>\n";
4923
4924         git_difftree_body(\@difftree, $hash, @$parents);
4925
4926 }
4927
4928 sub git_object {
4929         # object is defined by:
4930         # - hash or hash_base alone
4931         # - hash_base and file_name
4932         my $type;
4933
4934         # - hash or hash_base alone
4935         if ($hash || ($hash_base && !defined $file_name)) {
4936                 my $object_id = $hash || $hash_base;
4937
4938                 open my $fd, "-|", quote_command(
4939                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4940                         or die_error(404, "Object does not exist");
4941                 $type = <$fd>;
4942                 chomp $type;
4943                 close $fd
4944                         or die_error(404, "Object does not exist");
4945
4946         # - hash_base and file_name
4947         } elsif ($hash_base && defined $file_name) {
4948                 $file_name =~ s,/+$,,;
4949
4950                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4951                         or die_error(404, "Base object does not exist");
4952
4953                 # here errors should not hapen
4954                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4955                         or die_error(500, "Open git-ls-tree failed");
4956                 my $line = <$fd>;
4957                 close $fd;
4958
4959                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4960                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4961                         die_error(404, "File or directory for given base does not exist");
4962                 }
4963                 $type = $2;
4964                 $hash = $3;
4965         } else {
4966                 die_error(400, "Not enough information to find object");
4967         }
4968
4969         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4970                                           hash=>$hash, hash_base=>$hash_base,
4971                                           file_name=>$file_name),
4972                              -status => '302 Found');
4973 }
4974
4975 sub git_blobdiff {
4976         my $format = shift || 'html';
4977
4978         my $fd;
4979         my @difftree;
4980         my %diffinfo;
4981         my $expires;
4982
4983         # preparing $fd and %diffinfo for git_patchset_body
4984         # new style URI
4985         if (defined $hash_base && defined $hash_parent_base) {
4986                 if (defined $file_name) {
4987                         # read raw output
4988                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4989                                 $hash_parent_base, $hash_base,
4990                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
4991                                 or die_error(500, "Open git-diff-tree failed");
4992                         @difftree = map { chomp; $_ } <$fd>;
4993                         close $fd
4994                                 or die_error(404, "Reading git-diff-tree failed");
4995                         @difftree
4996                                 or die_error(404, "Blob diff not found");
4997
4998                 } elsif (defined $hash &&
4999                          $hash =~ /[0-9a-fA-F]{40}/) {
5000                         # try to find filename from $hash
5001
5002                         # read filtered raw output
5003                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5004                                 $hash_parent_base, $hash_base, "--"
5005                                 or die_error(500, "Open git-diff-tree failed");
5006                         @difftree =
5007                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5008                                 # $hash == to_id
5009                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5010                                 map { chomp; $_ } <$fd>;
5011                         close $fd
5012                                 or die_error(404, "Reading git-diff-tree failed");
5013                         @difftree
5014                                 or die_error(404, "Blob diff not found");
5015
5016                 } else {
5017                         die_error(400, "Missing one of the blob diff parameters");
5018                 }
5019
5020                 if (@difftree > 1) {
5021                         die_error(400, "Ambiguous blob diff specification");
5022                 }
5023
5024                 %diffinfo = parse_difftree_raw_line($difftree[0]);
5025                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5026                 $file_name   ||= $diffinfo{'to_file'};
5027
5028                 $hash_parent ||= $diffinfo{'from_id'};
5029                 $hash        ||= $diffinfo{'to_id'};
5030
5031                 # non-textual hash id's can be cached
5032                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5033                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5034                         $expires = '+1d';
5035                 }
5036
5037                 # open patch output
5038                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5039                         '-p', ($format eq 'html' ? "--full-index" : ()),
5040                         $hash_parent_base, $hash_base,
5041                         "--", (defined $file_parent ? $file_parent : ()), $file_name
5042                         or die_error(500, "Open git-diff-tree failed");
5043         }
5044
5045         # old/legacy style URI -- not generated anymore since 1.4.3.
5046         if (!%diffinfo) {
5047                 die_error('404 Not Found', "Missing one of the blob diff parameters")
5048         }
5049
5050         # header
5051         if ($format eq 'html') {
5052                 my $formats_nav =
5053                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5054                                 "raw");
5055                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5056                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5057                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5058                 } else {
5059                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5060                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5061                 }
5062                 if (defined $file_name) {
5063                         git_print_page_path($file_name, "blob", $hash_base);
5064                 } else {
5065                         print "<div class=\"page_path\"></div>\n";
5066                 }
5067
5068         } elsif ($format eq 'plain') {
5069                 print $cgi->header(
5070                         -type => 'text/plain',
5071                         -charset => 'utf-8',
5072                         -expires => $expires,
5073                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5074
5075                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5076
5077         } else {
5078                 die_error(400, "Unknown blobdiff format");
5079         }
5080
5081         # patch
5082         if ($format eq 'html') {
5083                 print "<div class=\"page_body\">\n";
5084
5085                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5086                 close $fd;
5087
5088                 print "</div>\n"; # class="page_body"
5089
5090         } else {
5091                 while (my $line = <$fd>) {
5092                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5093                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5094
5095                         print $line;
5096
5097                         last if $line =~ m!^\+\+\+!;
5098                 }
5099                 local $/ = undef;
5100                 print <$fd>;
5101                 close $fd;
5102         }
5103 }
5104
5105 sub git_blobdiff_plain {
5106         git_blobdiff('plain');
5107 }
5108
5109 sub git_commitdiff {
5110         my %params = @_;
5111         my $format = $params{-format} || 'html';
5112
5113         my ($patch_max) = gitweb_get_feature('patches');
5114         if ($format eq 'patch') {
5115                 die_error(403, "Patch view not allowed") unless $patch_max;
5116         }
5117
5118         $hash ||= $hash_base || "HEAD";
5119         my %co = parse_commit($hash)
5120             or die_error(404, "Unknown commit object");
5121
5122         # choose format for commitdiff for merge
5123         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5124                 $hash_parent = '--cc';
5125         }
5126         # we need to prepare $formats_nav before almost any parameter munging
5127         my $formats_nav;
5128         if ($format eq 'html') {
5129                 $formats_nav =
5130                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5131                                 "raw");
5132                 if ($patch_max) {
5133                         $formats_nav .= " | " .
5134                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5135                                         "patch");
5136                 }
5137
5138                 if (defined $hash_parent &&
5139                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
5140                         # commitdiff with two commits given
5141                         my $hash_parent_short = $hash_parent;
5142                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5143                                 $hash_parent_short = substr($hash_parent, 0, 7);
5144                         }
5145                         $formats_nav .=
5146                                 ' (from';
5147                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5148                                 if ($co{'parents'}[$i] eq $hash_parent) {
5149                                         $formats_nav .= ' parent ' . ($i+1);
5150                                         last;
5151                                 }
5152                         }
5153                         $formats_nav .= ': ' .
5154                                 $cgi->a({-href => href(action=>"commitdiff",
5155                                                        hash=>$hash_parent)},
5156                                         esc_html($hash_parent_short)) .
5157                                 ')';
5158                 } elsif (!$co{'parent'}) {
5159                         # --root commitdiff
5160                         $formats_nav .= ' (initial)';
5161                 } elsif (scalar @{$co{'parents'}} == 1) {
5162                         # single parent commit
5163                         $formats_nav .=
5164                                 ' (parent: ' .
5165                                 $cgi->a({-href => href(action=>"commitdiff",
5166                                                        hash=>$co{'parent'})},
5167                                         esc_html(substr($co{'parent'}, 0, 7))) .
5168                                 ')';
5169                 } else {
5170                         # merge commit
5171                         if ($hash_parent eq '--cc') {
5172                                 $formats_nav .= ' | ' .
5173                                         $cgi->a({-href => href(action=>"commitdiff",
5174                                                                hash=>$hash, hash_parent=>'-c')},
5175                                                 'combined');
5176                         } else { # $hash_parent eq '-c'
5177                                 $formats_nav .= ' | ' .
5178                                         $cgi->a({-href => href(action=>"commitdiff",
5179                                                                hash=>$hash, hash_parent=>'--cc')},
5180                                                 'compact');
5181                         }
5182                         $formats_nav .=
5183                                 ' (merge: ' .
5184                                 join(' ', map {
5185                                         $cgi->a({-href => href(action=>"commitdiff",
5186                                                                hash=>$_)},
5187                                                 esc_html(substr($_, 0, 7)));
5188                                 } @{$co{'parents'}} ) .
5189                                 ')';
5190                 }
5191         }
5192
5193         my $hash_parent_param = $hash_parent;
5194         if (!defined $hash_parent_param) {
5195                 # --cc for multiple parents, --root for parentless
5196                 $hash_parent_param =
5197                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5198         }
5199
5200         # read commitdiff
5201         my $fd;
5202         my @difftree;
5203         if ($format eq 'html') {
5204                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5205                         "--no-commit-id", "--patch-with-raw", "--full-index",
5206                         $hash_parent_param, $hash, "--"
5207                         or die_error(500, "Open git-diff-tree failed");
5208
5209                 while (my $line = <$fd>) {
5210                         chomp $line;
5211                         # empty line ends raw part of diff-tree output
5212                         last unless $line;
5213                         push @difftree, scalar parse_difftree_raw_line($line);
5214                 }
5215
5216         } elsif ($format eq 'plain') {
5217                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5218                         '-p', $hash_parent_param, $hash, "--"
5219                         or die_error(500, "Open git-diff-tree failed");
5220         } elsif ($format eq 'patch') {
5221                 # For commit ranges, we limit the output to the number of
5222                 # patches specified in the 'patches' feature.
5223                 # For single commits, we limit the output to a single patch,
5224                 # diverging from the git-format-patch default.
5225                 my @commit_spec = ();
5226                 if ($hash_parent) {
5227                         if ($patch_max > 0) {
5228                                 push @commit_spec, "-$patch_max";
5229                         }
5230                         push @commit_spec, '-n', "$hash_parent..$hash";
5231                 } else {
5232                         if ($params{-single}) {
5233                                 push @commit_spec, '-1';
5234                         } else {
5235                                 if ($patch_max > 0) {
5236                                         push @commit_spec, "-$patch_max";
5237                                 }
5238                                 push @commit_spec, "-n";
5239                         }
5240                         push @commit_spec, '--root', $hash;
5241                 }
5242                 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5243                         '--stdout', @commit_spec
5244                         or die_error(500, "Open git-format-patch failed");
5245         } else {
5246                 die_error(400, "Unknown commitdiff format");
5247         }
5248
5249         # non-textual hash id's can be cached
5250         my $expires;
5251         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5252                 $expires = "+1d";
5253         }
5254
5255         # write commit message
5256         if ($format eq 'html') {
5257                 my $refs = git_get_references();
5258                 my $ref = format_ref_marker($refs, $co{'id'});
5259
5260                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5261                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5262                 git_print_authorship(\%co);
5263                 print "<div class=\"page_body\">\n";
5264                 if (@{$co{'comment'}} > 1) {
5265                         print "<div class=\"log\">\n";
5266                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5267                         print "</div>\n"; # class="log"
5268                 }
5269
5270         } elsif ($format eq 'plain') {
5271                 my $refs = git_get_references("tags");
5272                 my $tagname = git_get_rev_name_tags($hash);
5273                 my $filename = basename($project) . "-$hash.patch";
5274
5275                 print $cgi->header(
5276                         -type => 'text/plain',
5277                         -charset => 'utf-8',
5278                         -expires => $expires,
5279                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5280                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5281                 print "From: " . to_utf8($co{'author'}) . "\n";
5282                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5283                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5284
5285                 print "X-Git-Tag: $tagname\n" if $tagname;
5286                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5287
5288                 foreach my $line (@{$co{'comment'}}) {
5289                         print to_utf8($line) . "\n";
5290                 }
5291                 print "---\n\n";
5292         } elsif ($format eq 'patch') {
5293                 my $filename = basename($project) . "-$hash.patch";
5294
5295                 print $cgi->header(
5296                         -type => 'text/plain',
5297                         -charset => 'utf-8',
5298                         -expires => $expires,
5299                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5300         }
5301
5302         # write patch
5303         if ($format eq 'html') {
5304                 my $use_parents = !defined $hash_parent ||
5305                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5306                 git_difftree_body(\@difftree, $hash,
5307                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5308                 print "<br/>\n";
5309
5310                 git_patchset_body($fd, \@difftree, $hash,
5311                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5312                 close $fd;
5313                 print "</div>\n"; # class="page_body"
5314
5315         } elsif ($format eq 'plain') {
5316                 local $/ = undef;
5317                 print <$fd>;
5318                 close $fd
5319                         or print "Reading git-diff-tree failed\n";
5320         } elsif ($format eq 'patch') {
5321                 local $/ = undef;
5322                 print <$fd>;
5323                 close $fd
5324                         or print "Reading git-format-patch failed\n";
5325         }
5326 }
5327
5328 sub git_commitdiff_plain {
5329         git_commitdiff(-format => 'plain');
5330 }
5331
5332 # format-patch-style patches
5333 sub git_patch {
5334         git_commitdiff(-format => 'patch', -single=> 1);
5335 }
5336
5337 sub git_patches {
5338         git_commitdiff(-format => 'patch');
5339 }
5340
5341 sub git_history {
5342         if (!defined $hash_base) {
5343                 $hash_base = git_get_head_hash($project);
5344         }
5345         if (!defined $page) {
5346                 $page = 0;
5347         }
5348         my $ftype;
5349         my %co = parse_commit($hash_base)
5350             or die_error(404, "Unknown commit object");
5351
5352         my $refs = git_get_references();
5353         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5354
5355         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5356                                        $file_name, "--full-history")
5357             or die_error(404, "No such file or directory on given branch");
5358
5359         if (!defined $hash && defined $file_name) {
5360                 # some commits could have deleted file in question,
5361                 # and not have it in tree, but one of them has to have it
5362                 for (my $i = 0; $i <= @commitlist; $i++) {
5363                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5364                         last if defined $hash;
5365                 }
5366         }
5367         if (defined $hash) {
5368                 $ftype = git_get_type($hash);
5369         }
5370         if (!defined $ftype) {
5371                 die_error(500, "Unknown type of object");
5372         }
5373
5374         my $paging_nav = '';
5375         if ($page > 0) {
5376                 $paging_nav .=
5377                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5378                                                file_name=>$file_name)},
5379                                 "first");
5380                 $paging_nav .= " &sdot; " .
5381                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5382                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5383         } else {
5384                 $paging_nav .= "first";
5385                 $paging_nav .= " &sdot; prev";
5386         }
5387         my $next_link = '';
5388         if ($#commitlist >= 100) {
5389                 $next_link =
5390                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5391                                  -accesskey => "n", -title => "Alt-n"}, "next");
5392                 $paging_nav .= " &sdot; $next_link";
5393         } else {
5394                 $paging_nav .= " &sdot; next";
5395         }
5396
5397         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5398         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5399         git_print_page_path($file_name, $ftype, $hash_base);
5400
5401         git_history_body(\@commitlist, 0, 99,
5402                          $refs, $hash_base, $ftype, $next_link);
5403
5404 }
5405
5406 sub git_search {
5407         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5408         if (!defined $searchtext) {
5409                 die_error(400, "Text field is empty");
5410         }
5411         if (!defined $hash) {
5412                 $hash = git_get_head_hash($project);
5413         }
5414         my %co = parse_commit($hash);
5415         if (!%co) {
5416                 die_error(404, "Unknown commit object");
5417         }
5418         if (!defined $page) {
5419                 $page = 0;
5420         }
5421
5422         $searchtype ||= 'commit';
5423         if ($searchtype eq 'pickaxe') {
5424                 # pickaxe may take all resources of your box and run for several minutes
5425                 # with every query - so decide by yourself how public you make this feature
5426                 gitweb_check_feature('pickaxe')
5427                     or die_error(403, "Pickaxe is disabled");
5428         }
5429         if ($searchtype eq 'grep') {
5430                 gitweb_check_feature('grep')
5431                     or die_error(403, "Grep is disabled");
5432         }
5433
5434
5435         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5436                 my $greptype;
5437                 if ($searchtype eq 'commit') {
5438                         $greptype = "--grep=";
5439                 } elsif ($searchtype eq 'author') {
5440                         $greptype = "--author=";
5441                 } elsif ($searchtype eq 'committer') {
5442                         $greptype = "--committer=";
5443                 }
5444                 $greptype .= $searchtext;
5445                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5446                                                $greptype, '--regexp-ignore-case',
5447                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5448
5449                 my $paging_nav = '';
5450                 if ($page > 0) {
5451                         $paging_nav .=
5452                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5453                                                        searchtext=>$searchtext,
5454                                                        searchtype=>$searchtype)},
5455                                         "first");
5456                         $paging_nav .= " &sdot; " .
5457                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5458                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5459                 } else {
5460                         $paging_nav .= "first";
5461                         $paging_nav .= " &sdot; prev";
5462                 }
5463                 my $next_link = '';
5464                 if ($#commitlist >= 100) {
5465                         $next_link =
5466                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5467                                          -accesskey => "n", -title => "Alt-n"}, "next");
5468                         $paging_nav .= " &sdot; $next_link";
5469                 } else {
5470                         $paging_nav .= " &sdot; next";
5471                 }
5472
5473                 if ($#commitlist >= 100) {
5474                 }
5475
5476                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5477                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5478                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5479         }
5480
5481         if ($searchtype eq 'pickaxe') {
5482                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5483                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5484
5485                 print "<table class=\"pickaxe search\">\n";
5486                 my $alternate = 1;
5487                 $/ = "\n";
5488                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5489                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5490                         ($search_use_regexp ? '--pickaxe-regex' : ());
5491                 undef %co;
5492                 my @files;
5493                 while (my $line = <$fd>) {
5494                         chomp $line;
5495                         next unless $line;
5496
5497                         my %set = parse_difftree_raw_line($line);
5498                         if (defined $set{'commit'}) {
5499                                 # finish previous commit
5500                                 if (%co) {
5501                                         print "</td>\n" .
5502                                               "<td class=\"link\">" .
5503                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5504                                               " | " .
5505                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5506                                         print "</td>\n" .
5507                                               "</tr>\n";
5508                                 }
5509
5510                                 if ($alternate) {
5511                                         print "<tr class=\"dark\">\n";
5512                                 } else {
5513                                         print "<tr class=\"light\">\n";
5514                                 }
5515                                 $alternate ^= 1;
5516                                 %co = parse_commit($set{'commit'});
5517                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5518                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5519                                       "<td><i>$author</i></td>\n" .
5520                                       "<td>" .
5521                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5522                                               -class => "list subject"},
5523                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5524                         } elsif (defined $set{'to_id'}) {
5525                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5526
5527                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5528                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5529                                               -class => "list"},
5530                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5531                                       "<br/>\n";
5532                         }
5533                 }
5534                 close $fd;
5535
5536                 # finish last commit (warning: repetition!)
5537                 if (%co) {
5538                         print "</td>\n" .
5539                               "<td class=\"link\">" .
5540                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5541                               " | " .
5542                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5543                         print "</td>\n" .
5544                               "</tr>\n";
5545                 }
5546
5547                 print "</table>\n";
5548         }
5549
5550         if ($searchtype eq 'grep') {
5551                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5552                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5553
5554                 print "<table class=\"grep_search\">\n";
5555                 my $alternate = 1;
5556                 my $matches = 0;
5557                 $/ = "\n";
5558                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5559                         $search_use_regexp ? ('-E', '-i') : '-F',
5560                         $searchtext, $co{'tree'};
5561                 my $lastfile = '';
5562                 while (my $line = <$fd>) {
5563                         chomp $line;
5564                         my ($file, $lno, $ltext, $binary);
5565                         last if ($matches++ > 1000);
5566                         if ($line =~ /^Binary file (.+) matches$/) {
5567                                 $file = $1;
5568                                 $binary = 1;
5569                         } else {
5570                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5571                         }
5572                         if ($file ne $lastfile) {
5573                                 $lastfile and print "</td></tr>\n";
5574                                 if ($alternate++) {
5575                                         print "<tr class=\"dark\">\n";
5576                                 } else {
5577                                         print "<tr class=\"light\">\n";
5578                                 }
5579                                 print "<td class=\"list\">".
5580                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5581                                                                file_name=>"$file"),
5582                                                 -class => "list"}, esc_path($file));
5583                                 print "</td><td>\n";
5584                                 $lastfile = $file;
5585                         }
5586                         if ($binary) {
5587                                 print "<div class=\"binary\">Binary file</div>\n";
5588                         } else {
5589                                 $ltext = untabify($ltext);
5590                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5591                                         $ltext = esc_html($1, -nbsp=>1);
5592                                         $ltext .= '<span class="match">';
5593                                         $ltext .= esc_html($2, -nbsp=>1);
5594                                         $ltext .= '</span>';
5595                                         $ltext .= esc_html($3, -nbsp=>1);
5596                                 } else {
5597                                         $ltext = esc_html($ltext, -nbsp=>1);
5598                                 }
5599                                 print "<div class=\"pre\">" .
5600                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5601                                                                file_name=>"$file").'#l'.$lno,
5602                                                 -class => "linenr"}, sprintf('%4i', $lno))
5603                                         . ' ' .  $ltext . "</div>\n";
5604                         }
5605                 }
5606                 if ($lastfile) {
5607                         print "</td></tr>\n";
5608                         if ($matches > 1000) {
5609                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5610                         }
5611                 } else {
5612                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5613                 }
5614                 close $fd;
5615
5616                 print "</table>\n";
5617         }
5618 }
5619
5620 sub git_search_help {
5621         git_print_page_nav('','', $hash,$hash,$hash);
5622         print <<EOT;
5623 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5624 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5625 the pattern entered is recognized as the POSIX extended
5626 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5627 insensitive).</p>
5628 <dl>
5629 <dt><b>commit</b></dt>
5630 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5631 EOT
5632         my $have_grep = gitweb_check_feature('grep');
5633         if ($have_grep) {
5634                 print <<EOT;
5635 <dt><b>grep</b></dt>
5636 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5637     a different one) are searched for the given pattern. On large trees, this search can take
5638 a while and put some strain on the server, so please use it with some consideration. Note that
5639 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5640 case-sensitive.</dd>
5641 EOT
5642         }
5643         print <<EOT;
5644 <dt><b>author</b></dt>
5645 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5646 <dt><b>committer</b></dt>
5647 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5648 EOT
5649         my $have_pickaxe = gitweb_check_feature('pickaxe');
5650         if ($have_pickaxe) {
5651                 print <<EOT;
5652 <dt><b>pickaxe</b></dt>
5653 <dd>All commits that caused the string to appear or disappear from any file (changes that
5654 added, removed or "modified" the string) will be listed. This search can take a while and
5655 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5656 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5657 EOT
5658         }
5659         print "</dl>\n";
5660 }
5661
5662 sub git_shortlog {
5663         my $head = git_get_head_hash($project);
5664         if (!defined $hash) {
5665                 $hash = $head;
5666         }
5667         if (!defined $page) {
5668                 $page = 0;
5669         }
5670         my $refs = git_get_references();
5671
5672         my $commit_hash = $hash;
5673         if (defined $hash_parent) {
5674                 $commit_hash = "$hash_parent..$hash";
5675         }
5676         my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5677
5678         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5679         my $next_link = '';
5680         if ($#commitlist >= 100) {
5681                 $next_link =
5682                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5683                                  -accesskey => "n", -title => "Alt-n"}, "next");
5684         }
5685         my $patch_max = gitweb_check_feature('patches');
5686         if ($patch_max) {
5687                 if ($patch_max < 0 || @commitlist <= $patch_max) {
5688                         $paging_nav .= " &sdot; " .
5689                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5690                                         "patches");
5691                 }
5692         }
5693
5694         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5695         git_print_header_div('summary', $project);
5696
5697         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5698
5699 }
5700
5701 ## ......................................................................
5702 ## feeds (RSS, Atom; OPML)
5703
5704 # XXX This does header stuff which may not play nice with Catalyst, so likely
5705 # broken in some/many ways.
5706 sub git_feed {
5707         my $format = shift || 'atom';
5708         my $have_blame = gitweb_check_feature('blame');
5709
5710         # Atom: http://www.atomenabled.org/developers/syndication/
5711         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5712         if ($format ne 'rss' && $format ne 'atom') {
5713                 die_error(400, "Unknown web feed format");
5714         }
5715
5716         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5717         my $head = $hash || 'HEAD';
5718         my @commitlist = parse_commits($head, 150, 0, $file_name);
5719
5720         my %latest_commit;
5721         my %latest_date;
5722         my $content_type = "application/$format+xml";
5723         if (defined $cgi->http('HTTP_ACCEPT') &&
5724                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5725                 # browser (feed reader) prefers text/xml
5726                 $content_type = 'text/xml';
5727         }
5728         if (defined($commitlist[0])) {
5729                 %latest_commit = %{$commitlist[0]};
5730                 my $latest_epoch = $latest_commit{'committer_epoch'};
5731                 %latest_date   = parse_date($latest_epoch);
5732                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
5733                 if (defined $if_modified) {
5734                         my $since;
5735                         if (eval { require HTTP::Date; 1; }) {
5736                                 $since = HTTP::Date::str2time($if_modified);
5737                         } elsif (eval { require Time::ParseDate; 1; }) {
5738                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
5739                         }
5740                         if (defined $since && $latest_epoch <= $since) {
5741                                 print $cgi->header(
5742                                         -type => $content_type,
5743                                         -charset => 'utf-8',
5744                                         -last_modified => $latest_date{'rfc2822'},
5745                                         -status => '304 Not Modified');
5746                                 return;
5747                         }
5748                 }
5749                 print $cgi->header(
5750                         -type => $content_type,
5751                         -charset => 'utf-8',
5752                         -last_modified => $latest_date{'rfc2822'});
5753         } else {
5754                 print $cgi->header(
5755                         -type => $content_type,
5756                         -charset => 'utf-8');
5757         }
5758
5759         # Optimization: skip generating the body if client asks only
5760         # for Last-Modified date.
5761         return if ($cgi->request_method() eq 'HEAD');
5762
5763         # header variables
5764         my $title = $c->config->{sitename} . " - $project/$action";
5765         my $feed_type = 'log';
5766         if (defined $hash) {
5767                 $title .= " - '$hash'";
5768                 $feed_type = 'branch log';
5769                 if (defined $file_name) {
5770                         $title .= " :: $file_name";
5771                         $feed_type = 'history';
5772                 }
5773         } elsif (defined $file_name) {
5774                 $title .= " - $file_name";
5775                 $feed_type = 'history';
5776         }
5777         $title .= " $feed_type";
5778         my $descr = git_get_project_description($project);
5779         if (defined $descr) {
5780                 $descr = esc_html($descr);
5781         } else {
5782                 $descr = "$project " .
5783                          ($format eq 'rss' ? 'RSS' : 'Atom') .
5784                          " feed";
5785         }
5786         my $owner = git_get_project_owner($project);
5787         $owner = esc_html($owner);
5788
5789         #header
5790         my $alt_url;
5791         if (defined $file_name) {
5792                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5793         } elsif (defined $hash) {
5794                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5795         } else {
5796                 $alt_url = href(-full=>1, action=>"summary");
5797         }
5798         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5799         if ($format eq 'rss') {
5800                 print <<XML;
5801 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5802 <channel>
5803 XML
5804                 print "<title>$title</title>\n" .
5805                       "<link>$alt_url</link>\n" .
5806                       "<description>$descr</description>\n" .
5807                       "<language>en</language>\n" .
5808                       # project owner is responsible for 'editorial' content
5809                       "<managingEditor>$owner</managingEditor>\n";
5810                 if ($c->config->{logo} || $c->config->{favicon}) {
5811                         # prefer the logo to the favicon, since RSS
5812                         # doesn't allow both
5813                         my $img = esc_url($c->config->{logo} || $c->config->{favicon});
5814                         print "<image>\n" .
5815                               "<url>$img</url>\n" .
5816                               "<title>$title</title>\n" .
5817                               "<link>$alt_url</link>\n" .
5818                               "</image>\n";
5819                 }
5820                 if (%latest_date) {
5821                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
5822                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
5823                 }
5824                 print "<generator>gitweb v.$version/$git_version</generator>\n";
5825         } elsif ($format eq 'atom') {
5826                 print <<XML;
5827 <feed xmlns="http://www.w3.org/2005/Atom">
5828 XML
5829                 print "<title>$title</title>\n" .
5830                       "<subtitle>$descr</subtitle>\n" .
5831                       '<link rel="alternate" type="text/html" href="' .
5832                       $alt_url . '" />' . "\n" .
5833                       '<link rel="self" type="' . $content_type . '" href="' .
5834                       $cgi->self_url() . '" />' . "\n" .
5835                       "<id>" . href(-full=>1) . "</id>\n" .
5836                       # use project owner for feed author
5837                       "<author><name>$owner</name></author>\n";
5838                 if ($c->config->{favicon}) {
5839                         print "<icon>" . esc_url($c->config->{favicon}) . "</icon>\n";
5840                 }
5841                 if (defined $logo_url) {
5842                         # not twice as wide as tall: 72 x 27 pixels
5843                         print "<logo>" . esc_url($c->config->{logo}) . "</logo>\n";
5844                 }
5845                 if (! %latest_date) {
5846                         # dummy date to keep the feed valid until commits trickle in:
5847                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
5848                 } else {
5849                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
5850                 }
5851                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
5852         }
5853
5854         # contents
5855         for (my $i = 0; $i <= $#commitlist; $i++) {
5856                 my %co = %{$commitlist[$i]};
5857                 my $commit = $co{'id'};
5858                 # we read 150, we always show 30 and the ones more recent than 48 hours
5859                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5860                         last;
5861                 }
5862                 my %cd = parse_date($co{'author_epoch'});
5863
5864                 # get list of changed files
5865                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5866                         $co{'parent'} || "--root",
5867                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
5868                         or next;
5869                 my @difftree = map { chomp; $_ } <$fd>;
5870                 close $fd
5871                         or next;
5872
5873                 # print element (entry, item)
5874                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5875                 if ($format eq 'rss') {
5876                         print "<item>\n" .
5877                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
5878                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
5879                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5880                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5881                               "<link>$co_url</link>\n" .
5882                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
5883                               "<content:encoded>" .
5884                               "<![CDATA[\n";
5885                 } elsif ($format eq 'atom') {
5886                         print "<entry>\n" .
5887                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5888                               "<updated>$cd{'iso-8601'}</updated>\n" .
5889                               "<author>\n" .
5890                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5891                         if ($co{'author_email'}) {
5892                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5893                         }
5894                         print "</author>\n" .
5895                               # use committer for contributor
5896                               "<contributor>\n" .
5897                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5898                         if ($co{'committer_email'}) {
5899                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5900                         }
5901                         print "</contributor>\n" .
5902                               "<published>$cd{'iso-8601'}</published>\n" .
5903                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5904                               "<id>$co_url</id>\n" .
5905                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5906                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5907                 }
5908                 my $comment = $co{'comment'};
5909                 print "<pre>\n";
5910                 foreach my $line (@$comment) {
5911                         $line = esc_html($line);
5912                         print "$line\n";
5913                 }
5914                 print "</pre><ul>\n";
5915                 foreach my $difftree_line (@difftree) {
5916                         my %difftree = parse_difftree_raw_line($difftree_line);
5917                         next if !$difftree{'from_id'};
5918
5919                         my $file = $difftree{'file'} || $difftree{'to_file'};
5920
5921                         print "<li>" .
5922                               "[" .
5923                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5924                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5925                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5926                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
5927                                       -title => "diff"}, 'D');
5928                         if ($have_blame) {
5929                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
5930                                                              file_name=>$file, hash_base=>$commit),
5931                                               -title => "blame"}, 'B');
5932                         }
5933                         # if this is not a feed of a file history
5934                         if (!defined $file_name || $file_name ne $file) {
5935                                 print $cgi->a({-href => href(-full=>1, action=>"history",
5936                                                              file_name=>$file, hash=>$commit),
5937                                               -title => "history"}, 'H');
5938                         }
5939                         $file = esc_path($file);
5940                         print "] ".
5941                               "$file</li>\n";
5942                 }
5943                 if ($format eq 'rss') {
5944                         print "</ul>]]>\n" .
5945                               "</content:encoded>\n" .
5946                               "</item>\n";
5947                 } elsif ($format eq 'atom') {
5948                         print "</ul>\n</div>\n" .
5949                               "</content>\n" .
5950                               "</entry>\n";
5951                 }
5952         }
5953
5954         # end of feed
5955         if ($format eq 'rss') {
5956                 print "</channel>\n</rss>\n";
5957         }       elsif ($format eq 'atom') {
5958                 print "</feed>\n";
5959         }
5960 }
5961
5962 sub git_rss {
5963         git_feed('rss');
5964 }
5965
5966 sub git_atom {
5967         git_feed('atom');
5968 }
5969
5970 sub git_opml {
5971         my @list = git_get_projects_list();
5972
5973         print $cgi->header(
5974                 -type => 'text/xml',
5975                 -charset => 'utf-8',
5976                 -content_disposition => 'inline; filename="opml.xml"');
5977
5978         my $sitename = $c->config->{sitename};
5979         print <<XML;
5980 <?xml version="1.0" encoding="utf-8"?>
5981 <opml version="1.0">
5982 <head>
5983   <title>$sitename OPML Export</title>
5984 </head>
5985 <body>
5986 <outline text="git RSS feeds">
5987 XML
5988
5989         foreach my $pr (@list) {
5990                 my %proj = %$pr;
5991                 my $head = git_get_head_hash($proj{'path'});
5992                 if (!defined $head) {
5993                         next;
5994                 }
5995                 $git_dir = "$projectroot/$proj{'path'}";
5996                 my %co = parse_commit($head);
5997                 if (!%co) {
5998                         next;
5999                 }
6000
6001                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6002                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6003                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6004                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6005         }
6006         print <<XML;
6007 </outline>
6008 </body>
6009 </opml>
6010 XML
6011 }
6012
6013 1;