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