Added gitweb.cgi copyright info. Thanks to chrisa for pointing that out!
[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 = "/usr/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 = "Project Gitalist";
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
121         # source of projects list
122         our $projects_list = "";
123
124         # the width (in characters) of the projects list "Description" column
125         our $projects_list_description_width = 25;
126
127         # default order of projects list
128         # valid values are none, project, descr, owner, and age
129         our $default_projects_order = "project";
130
131         # show repository only if this file exists
132         # (only effective if this variable evaluates to true)
133         our $export_ok = "";
134
135         # show repository only if this subroutine returns true
136         # when given the path to the project, for example:
137         #    sub { return -e "$_[0]/git-daemon-export-ok"; }
138         our $export_auth_hook = undef;
139
140         # only allow viewing of repositories also shown on the overview page
141         our $strict_export = "";
142
143         # list of git base URLs used for URL to where fetch project from,
144         # i.e. full URL is "$git_base_url/$project"
145         our @git_base_url_list = grep { $_ ne '' } ("");
146
147         # default blob_plain mimetype and default charset for text/plain blob
148         our $default_blob_plain_mimetype = 'text/plain';
149         our $default_text_plain_charset  = undef;
150
151         # file to use for guessing MIME types before trying /etc/mime.types
152         # (relative to the current git repository)
153         our $mimetypes_file = undef;
154
155         # assume this charset if line contains non-UTF-8 characters;
156         # it should be valid encoding (see Encoding::Supported(3pm) for list),
157         # for which encoding all byte sequences are valid, for example
158         # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
159         # could be even 'utf-8' for the old behavior)
160         our $fallback_encoding = 'latin1';
161
162         # rename detection options for git-diff and git-diff-tree
163         # - default is '-M', with the cost proportional to
164         #   (number of removed files) * (number of new files).
165         # - more costly is '-C' (which implies '-M'), with the cost proportional to
166         #   (number of changed files + number of removed files) * (number of new files)
167         # - even more costly is '-C', '--find-copies-harder' with cost
168         #   (number of files in the original tree) * (number of new files)
169         # - one might want to include '-B' option, e.g. '-B', '-M'
170         our @diff_opts = ('-M'); # taken from git_commit
171
172         # Disables features that would allow repository owners to inject script into
173         # the gitweb domain.
174         our $prevent_xss = 0;
175
176         # information about snapshot formats that gitweb is capable of serving
177         our %known_snapshot_formats = (
178                 # name => {
179                 #       'display' => display name,
180                 #       'type' => mime type,
181                 #       'suffix' => filename suffix,
182                 #       'format' => --format for git-archive,
183                 #       'compressor' => [compressor command and arguments]
184                 #                       (array reference, optional)}
185                 #
186                 'tgz' => {
187                         'display' => 'tar.gz',
188                         'type' => 'application/x-gzip',
189                         'suffix' => '.tar.gz',
190                         'format' => 'tar',
191                         'compressor' => ['gzip']},
192
193                 'tbz2' => {
194                         'display' => 'tar.bz2',
195                         'type' => 'application/x-bzip2',
196                         'suffix' => '.tar.bz2',
197                         'format' => 'tar',
198                         'compressor' => ['bzip2']},
199
200                 'zip' => {
201                         'display' => 'zip',
202                         'type' => 'application/x-zip',
203                         'suffix' => '.zip',
204                         'format' => 'zip'},
205         );
206
207         # Aliases so we understand old gitweb.snapshot values in repository
208         # configuration.
209         our %known_snapshot_format_aliases = (
210                 'gzip'  => 'tgz',
211                 'bzip2' => 'tbz2',
212
213                 # backward compatibility: legacy gitweb config support
214                 'x-gzip' => undef, 'gz' => undef,
215                 'x-bzip2' => undef, 'bz2' => undef,
216                 'x-zip' => undef, '' => undef,
217         );
218
219         my $feature_bool = sub {
220                 my $key = shift;
221                 my ($val) = git_get_project_config($key, '--bool');
222
223                 if (!defined $val) {
224                         return ($_[0]);
225                 } elsif ($val eq 'true') {
226                         return (1);
227                 } elsif ($val eq 'false') {
228                         return (0);
229                 }
230         };
231
232         my $feature_snapshot = sub {
233                 my (@fmts) = @_;
234
235                 my ($val) = git_get_project_config('snapshot');
236
237                 if ($val) {
238                         @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
239                 }
240
241                 return @fmts;
242         };
243
244         my $feature_patches = sub {
245                 my @val = (git_get_project_config('patches', '--int'));
246
247                 if (@val) {
248                         return @val;
249                 }
250
251                 return ($_[0]);
252         };
253
254
255         # You define site-wide feature defaults here; override them with
256         # $GITWEB_CONFIG as necessary.
257         our %feature = (
258                 # feature => {
259                 #       'sub' => feature-sub (subroutine),
260                 #       'override' => allow-override (boolean),
261                 #       'default' => [ default options...] (array reference)}
262                 #
263                 # if feature is overridable (it means that allow-override has true value),
264                 # then feature-sub will be called with default options as parameters;
265                 # return value of feature-sub indicates if to enable specified feature
266                 #
267                 # if there is no 'sub' key (no feature-sub), then feature cannot be
268                 # overriden
269                 #
270                 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
271                 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
272                 # is enabled
273
274                 # Enable the 'blame' blob view, showing the last commit that modified
275                 # each line in the file. This can be very CPU-intensive.
276
277                 # To enable system wide have in $GITWEB_CONFIG
278                 # $feature{'blame'}{'default'} = [1];
279                 # To have project specific config enable override in $GITWEB_CONFIG
280                 # $feature{'blame'}{'override'} = 1;
281                 # and in project config gitweb.blame = 0|1;
282                 'blame' => {
283                         'sub' => sub { &$feature_bool('blame', @_) },
284                         'override' => 0,
285                         'default' => [0]},
286
287                 # Enable the 'snapshot' link, providing a compressed archive of any
288                 # tree. This can potentially generate high traffic if you have large
289                 # project.
290
291                 # Value is a list of formats defined in %known_snapshot_formats that
292                 # you wish to offer.
293                 # To disable system wide have in $GITWEB_CONFIG
294                 # $feature{'snapshot'}{'default'} = [];
295                 # To have project specific config enable override in $GITWEB_CONFIG
296                 # $feature{'snapshot'}{'override'} = 1;
297                 # and in project config, a comma-separated list of formats or "none"
298                 # to disable.  Example: gitweb.snapshot = tbz2,zip;
299                 'snapshot' => {
300                         'sub' => $feature_snapshot,
301                         'override' => 0,
302                         'default' => ['tgz']},
303
304                 # Enable text search, which will list the commits which match author,
305                 # committer or commit text to a given string.  Enabled by default.
306                 # Project specific override is not supported.
307                 'search' => {
308                         'override' => 0,
309                         'default' => [1]},
310
311                 # Enable grep search, which will list the files in currently selected
312                 # tree containing the given string. Enabled by default. This can be
313                 # potentially CPU-intensive, of course.
314
315                 # To enable system wide have in $GITWEB_CONFIG
316                 # $feature{'grep'}{'default'} = [1];
317                 # To have project specific config enable override in $GITWEB_CONFIG
318                 # $feature{'grep'}{'override'} = 1;
319                 # and in project config gitweb.grep = 0|1;
320                 'grep' => {
321                         'sub' => sub { &$feature_bool('grep', @_) },
322                         'override' => 0,
323                         'default' => [1]},
324
325                 # Enable the pickaxe search, which will list the commits that modified
326                 # a given string in a file. This can be practical and quite faster
327                 # alternative to 'blame', but still potentially CPU-intensive.
328
329                 # To enable system wide have in $GITWEB_CONFIG
330                 # $feature{'pickaxe'}{'default'} = [1];
331                 # To have project specific config enable override in $GITWEB_CONFIG
332                 # $feature{'pickaxe'}{'override'} = 1;
333                 # and in project config gitweb.pickaxe = 0|1;
334                 'pickaxe' => {
335                         'sub' => sub { &$feature_bool('pickaxe', @_) },
336                         'override' => 0,
337                         'default' => [1]},
338
339                 # Make gitweb use an alternative format of the URLs which can be
340                 # more readable and natural-looking: project name is embedded
341                 # directly in the path and the query string contains other
342                 # auxiliary information. All gitweb installations recognize
343                 # URL in either format; this configures in which formats gitweb
344                 # generates links.
345
346                 # To enable system wide have in $GITWEB_CONFIG
347                 # $feature{'pathinfo'}{'default'} = [1];
348                 # Project specific override is not supported.
349
350                 # Note that you will need to change the default location of CSS,
351                 # favicon, logo and possibly other files to an absolute URL. Also,
352                 # if gitweb.cgi serves as your indexfile, you will need to force
353                 # $my_uri to contain the script name in your $GITWEB_CONFIG.
354                 'pathinfo' => {
355                         'override' => 0,
356                         'default' => [0]},
357
358                 # Make gitweb consider projects in project root subdirectories
359                 # to be forks of existing projects. Given project $projname.git,
360                 # projects matching $projname/*.git will not be shown in the main
361                 # projects list, instead a '+' mark will be added to $projname
362                 # there and a 'forks' view will be enabled for the project, listing
363                 # all the forks. If project list is taken from a file, forks have
364                 # to be listed after the main project.
365
366                 # To enable system wide have in $GITWEB_CONFIG
367                 # $feature{'forks'}{'default'} = [1];
368                 # Project specific override is not supported.
369                 'forks' => {
370                         'override' => 0,
371                         'default' => [0]},
372
373                 # Insert custom links to the action bar of all project pages.
374                 # This enables you mainly to link to third-party scripts integrating
375                 # into gitweb; e.g. git-browser for graphical history representation
376                 # or custom web-based repository administration interface.
377
378                 # The 'default' value consists of a list of triplets in the form
379                 # (label, link, position) where position is the label after which
380                 # to insert the link and link is a format string where %n expands
381                 # to the project name, %f to the project path within the filesystem,
382                 # %h to the current hash (h gitweb parameter) and %b to the current
383                 # hash base (hb gitweb parameter); %% expands to %.
384
385                 # To enable system wide have in $GITWEB_CONFIG e.g.
386                 # $feature{'actions'}{'default'} = [('graphiclog',
387                 #       '/git-browser/by-commit.html?r=%n', 'summary')];
388                 # Project specific override is not supported.
389                 'actions' => {
390                         'override' => 0,
391                         'default' => []},
392
393                 # Allow gitweb scan project content tags described in ctags/
394                 # of project repository, and display the popular Web 2.0-ish
395                 # "tag cloud" near the project list. Note that this is something
396                 # COMPLETELY different from the normal Git tags.
397
398                 # gitweb by itself can show existing tags, but it does not handle
399                 # tagging itself; you need an external application for that.
400                 # For an example script, check Girocco's cgi/tagproj.cgi.
401                 # You may want to install the HTML::TagCloud Perl module to get
402                 # a pretty tag cloud instead of just a list of tags.
403
404                 # To enable system wide have in $GITWEB_CONFIG
405                 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
406                 # Project specific override is not supported.
407                 'ctags' => {
408                         'override' => 0,
409                         'default' => [0]},
410
411                 # The maximum number of patches in a patchset generated in patch
412                 # view. Set this to 0 or undef to disable patch view, or to a
413                 # negative number to remove any limit.
414
415                 # To disable system wide have in $GITWEB_CONFIG
416                 # $feature{'patches'}{'default'} = [0];
417                 # To have project specific config enable override in $GITWEB_CONFIG
418                 # $feature{'patches'}{'override'} = 1;
419                 # and in project config gitweb.patches = 0|n;
420                 # where n is the maximum number of patches allowed in a patchset.
421                 'patches' => {
422                         'sub' => $feature_patches,
423                         'override' => 0,
424                         'default' => [16]},
425         );
426
427         our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl";
428         if (-e $GITWEB_CONFIG) {
429                 do $GITWEB_CONFIG;
430         } else {
431                 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || '../../gitweb.conf';
432                 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
433         }
434
435         # version of the core git binary
436         our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
437
438         $projects_list ||= $projectroot;
439
440         # ======================================================================
441         # input validation and dispatch
442
443         # input parameters can be collected from a variety of sources (presently, CGI
444         # and PATH_INFO), so we define an %input_params hash that collects them all
445         # together during validation: this allows subsequent uses (e.g. href()) to be
446         # agnostic of the parameter origin
447
448         our %input_params = ();
449
450         # input parameters are stored with the long parameter name as key. This will
451         # also be used in the href subroutine to convert parameters to their CGI
452         # equivalent, and since the href() usage is the most frequent one, we store
453         # the name -> CGI key mapping here, instead of the reverse.
454         #
455         # XXX: Warning: If you touch this, check the search form for updating,
456         # too.
457
458         our @cgi_param_mapping = (
459                 project => "p",
460                 action => "a",
461                 file_name => "f",
462                 file_parent => "fp",
463                 hash => "h",
464                 hash_parent => "hp",
465                 hash_base => "hb",
466                 hash_parent_base => "hpb",
467                 page => "pg",
468                 order => "o",
469                 searchtext => "s",
470                 searchtype => "st",
471                 snapshot_format => "sf",
472                 extra_options => "opt",
473                 search_use_regexp => "sr",
474         );
475         our %cgi_param_mapping = @cgi_param_mapping;
476
477         # we will also need to know the possible actions, for validation
478         our %actions = (
479                 "blame" => \&git_blame,
480                 "blobdiff" => \&git_blobdiff,
481                 "blobdiff_plain" => \&git_blobdiff_plain,
482                 "blob" => \&git_blob,
483                 "blob_plain" => \&git_blob_plain,
484                 "commitdiff" => \&git_commitdiff,
485                 "commitdiff_plain" => \&git_commitdiff_plain,
486                 "commit" => \&git_commit,
487                 "forks" => \&git_forks,
488                 "heads" => \&git_heads,
489                 "history" => \&git_history,
490                 "log" => \&git_log,
491                 "patch" => \&git_patch,
492                 "patches" => \&git_patches,
493                 "rss" => \&git_rss,
494                 "atom" => \&git_atom,
495                 "search" => \&git_search,
496                 "search_help" => \&git_search_help,
497                 "shortlog" => \&git_shortlog,
498                 "summary" => \&git_summary,
499                 "tag" => \&git_tag,
500                 "tags" => \&git_tags,
501                 "tree" => \&git_tree,
502                 "snapshot" => \&git_snapshot,
503                 "object" => \&git_object,
504                 # those below don't need $project
505                 "opml" => \&git_opml,
506                 "project_list" => \&git_project_list,
507                 "project_index" => \&git_project_index,
508         );
509
510         # finally, we have the hash of allowed extra_options for the commands that
511         # allow them
512         our %allowed_options = (
513                 "--no-merges" => [ qw(rss atom log shortlog history) ],
514         );
515
516         # fill %input_params with the CGI parameters. All values except for 'opt'
517         # should be single values, but opt can be an array. We should probably
518         # build an array of parameters that can be multi-valued, but since for the time
519         # being it's only this one, we just single it out
520         while (my ($name, $symbol) = each %cgi_param_mapping) {
521                 if ($symbol eq 'opt') {
522                         $input_params{$name} = [ $cgi->param($symbol) ];
523                 } else {
524                         $input_params{$name} = $cgi->param($symbol);
525                 }
526         }
527
528         # now read PATH_INFO and update the parameter list for missing parameters
529         my $evaluate_path_info = sub {
530                 return if defined $input_params{'project'};
531                 return if !$path_info;
532                 $path_info =~ s,^/+,,;
533                 return if !$path_info;
534
535                 # find which part of PATH_INFO is project
536                 my $project = $path_info;
537                 $project =~ s,/+$,,;
538                 while ($project && !check_head_link("$projectroot/$project")) {
539                         $project =~ s,/*[^/]*$,,;
540                 }
541                 return unless $project;
542                 $input_params{'project'} = $project;
543
544                 # do not change any parameters if an action is given using the query string
545                 return if $input_params{'action'};
546                 $path_info =~ s,^\Q$project\E/*,,;
547
548                 # next, check if we have an action
549                 my $action = $path_info;
550                 $action =~ s,/.*$,,;
551                 if (exists $actions{$action}) {
552                         $path_info =~ s,^$action/*,,;
553                         $input_params{'action'} = $action;
554                 }
555
556                 # list of actions that want hash_base instead of hash, but can have no
557                 # pathname (f) parameter
558                 my @wants_base = (
559                         'tree',
560                         'history',
561                 );
562
563                 # we want to catch
564                 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
565                 my ($parentrefname, $parentpathname, $refname, $pathname) =
566                         ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
567
568                 # first, analyze the 'current' part
569                 if (defined $pathname) {
570                         # we got "branch:filename" or "branch:dir/"
571                         # we could use git_get_type(branch:pathname), but:
572                         # - it needs $git_dir
573                         # - it does a git() call
574                         # - the convention of terminating directories with a slash
575                         #   makes it superfluous
576                         # - embedding the action in the PATH_INFO would make it even
577                         #   more superfluous
578                         $pathname =~ s,^/+,,;
579                         if (!$pathname || substr($pathname, -1) eq "/") {
580                                 $input_params{'action'} ||= "tree";
581                                 $pathname =~ s,/$,,;
582                         } else {
583                                 # the default action depends on whether we had parent info
584                                 # or not
585                                 if ($parentrefname) {
586                                         $input_params{'action'} ||= "blobdiff_plain";
587                                 } else {
588                                         $input_params{'action'} ||= "blob_plain";
589                                 }
590                         }
591                         $input_params{'hash_base'} ||= $refname;
592                         $input_params{'file_name'} ||= $pathname;
593                 } elsif (defined $refname) {
594                         # we got "branch". In this case we have to choose if we have to
595                         # set hash or hash_base.
596                         #
597                         # Most of the actions without a pathname only want hash to be
598                         # set, except for the ones specified in @wants_base that want
599                         # hash_base instead. It should also be noted that hand-crafted
600                         # links having 'history' as an action and no pathname or hash
601                         # set will fail, but that happens regardless of PATH_INFO.
602                         $input_params{'action'} ||= "shortlog";
603                         if (grep { $_ eq $input_params{'action'} } @wants_base) {
604                                 $input_params{'hash_base'} ||= $refname;
605                         } else {
606                                 $input_params{'hash'} ||= $refname;
607                         }
608                 }
609
610                 # next, handle the 'parent' part, if present
611                 if (defined $parentrefname) {
612                         # a missing pathspec defaults to the 'current' filename, allowing e.g.
613                         # someproject/blobdiff/oldrev..newrev:/filename
614                         if ($parentpathname) {
615                                 $parentpathname =~ s,^/+,,;
616                                 $parentpathname =~ s,/$,,;
617                                 $input_params{'file_parent'} ||= $parentpathname;
618                         } else {
619                                 $input_params{'file_parent'} ||= $input_params{'file_name'};
620                         }
621                         # we assume that hash_parent_base is wanted if a path was specified,
622                         # or if the action wants hash_base instead of hash
623                         if (defined $input_params{'file_parent'} ||
624                                 grep { $_ eq $input_params{'action'} } @wants_base) {
625                                 $input_params{'hash_parent_base'} ||= $parentrefname;
626                         } else {
627                                 $input_params{'hash_parent'} ||= $parentrefname;
628                         }
629                 }
630
631                 # for the snapshot action, we allow URLs in the form
632                 # $project/snapshot/$hash.ext
633                 # where .ext determines the snapshot and gets removed from the
634                 # passed $refname to provide the $hash.
635                 #
636                 # To be able to tell that $refname includes the format extension, we
637                 # require the following two conditions to be satisfied:
638                 # - the hash input parameter MUST have been set from the $refname part
639                 #   of the URL (i.e. they must be equal)
640                 # - the snapshot format MUST NOT have been defined already (e.g. from
641                 #   CGI parameter sf)
642                 # It's also useless to try any matching unless $refname has a dot,
643                 # so we check for that too
644                 if (defined $input_params{'action'} &&
645                         $input_params{'action'} eq 'snapshot' &&
646                         defined $refname && index($refname, '.') != -1 &&
647                         $refname eq $input_params{'hash'} &&
648                         !defined $input_params{'snapshot_format'}) {
649                         # We loop over the known snapshot formats, checking for
650                         # extensions. Allowed extensions are both the defined suffix
651                         # (which includes the initial dot already) and the snapshot
652                         # format key itself, with a prepended dot
653                         while (my ($fmt, $opt) = each %known_snapshot_formats) {
654                                 my $hash = $refname;
655                                 my $sfx;
656                                 $hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//;
657                                 next unless $sfx = $1;
658                                 # a valid suffix was found, so set the snapshot format
659                                 # and reset the hash parameter
660                                 $input_params{'snapshot_format'} = $fmt;
661                                 $input_params{'hash'} = $hash;
662                                 # we also set the format suffix to the one requested
663                                 # in the URL: this way a request for e.g. .tgz returns
664                                 # a .tgz instead of a .tar.gz
665                                 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
666                                 last;
667                         }
668                 }
669         };
670         &$evaluate_path_info();
671
672         gitweb_validate_setup();
673
674         return $actions{$action};
675 }
676
677 sub gitweb_validate_setup {
678         our $action = $input_params{'action'};
679         if (defined $action) {
680                 if (!validate_action($action)) {
681                         die_error(400, "Invalid action parameter");
682                 }
683         }
684
685         # parameters which are pathnames
686         our $project = $input_params{'project'};
687         if (defined $project) {
688                 if (!validate_project($project)) {
689                         undef $project;
690                         die_error(404, "No such project");
691                 }
692         }
693
694         our $file_name = $input_params{'file_name'};
695         if (defined $file_name) {
696                 if (!validate_pathname($file_name)) {
697                         die_error(400, "Invalid file parameter");
698                 }
699         }
700
701         our $file_parent = $input_params{'file_parent'};
702         if (defined $file_parent) {
703                 if (!validate_pathname($file_parent)) {
704                         die_error(400, "Invalid file parent parameter");
705                 }
706         }
707
708         # parameters which are refnames
709         our $hash = $input_params{'hash'};
710         if (defined $hash) {
711                 if (!validate_refname($hash)) {
712                         die_error(400, "Invalid hash parameter");
713                 }
714         }
715
716         our $hash_parent = $input_params{'hash_parent'};
717         if (defined $hash_parent) {
718                 if (!validate_refname($hash_parent)) {
719                         die_error(400, "Invalid hash parent parameter");
720                 }
721         }
722
723         our $hash_base = $input_params{'hash_base'};
724         if (defined $hash_base) {
725                 if (!validate_refname($hash_base)) {
726                         die_error(400, "Invalid hash base parameter");
727                 }
728         }
729
730         our @extra_options = @{$input_params{'extra_options'}};
731         # @extra_options is always defined, since it can only be (currently) set from
732         # CGI, and $cgi->param() returns the empty array in array context if the param
733         # is not set
734         foreach my $opt (@extra_options) {
735                 if (not exists $allowed_options{$opt}) {
736                         die_error(400, "Invalid option parameter");
737                 }
738                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
739                         die_error(400, "Invalid option parameter for this action");
740                 }
741         }
742
743         our $hash_parent_base = $input_params{'hash_parent_base'};
744         if (defined $hash_parent_base) {
745                 if (!validate_refname($hash_parent_base)) {
746                         die_error(400, "Invalid hash parent base parameter");
747                 }
748         }
749
750         # other parameters
751         our $page = $input_params{'page'};
752         if (defined $page) {
753                 if ($page =~ m/[^0-9]/) {
754                         die_error(400, "Invalid page parameter");
755                 }
756         }
757
758         our $searchtype = $input_params{'searchtype'};
759         if (defined $searchtype) {
760                 if ($searchtype =~ m/[^a-z]/) {
761                         die_error(400, "Invalid searchtype parameter");
762                 }
763         }
764
765         our $search_use_regexp = $input_params{'search_use_regexp'};
766
767         our $searchtext = $input_params{'searchtext'};
768         our $search_regexp;
769         if (defined $searchtext) {
770                 if (length($searchtext) < 2) {
771                         die_error(403, "At least two characters are required for search parameter");
772                 }
773                 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
774         }
775
776         # path to the current git repository
777         our $git_dir;
778         $git_dir = "$projectroot/$project" if $project;
779
780         # process alternate names for backward compatibility
781         # filter out unsupported (unknown) snapshot formats
782         my $filter_snapshot_fmts = sub {
783                 my @fmts = @_;
784
785                 @fmts = map {
786                         exists $known_snapshot_format_aliases{$_} ?
787                                $known_snapshot_format_aliases{$_} : $_} @fmts;
788                 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
789
790         };
791         # list of supported snapshot formats
792         our @snapshot_fmts = gitweb_get_feature('snapshot');
793         @snapshot_fmts = &$filter_snapshot_fmts(@snapshot_fmts);
794
795         # dispatch
796         if (!defined $action) {
797                 if (defined $hash) {
798                         $action = git_get_type($hash);
799                 } elsif (defined $hash_base && defined $file_name) {
800                         $action = git_get_type("$hash_base:$file_name");
801                 } elsif (defined $project) {
802                         $action = 'summary';
803                 } else {
804                         $action = 'project_list';
805                 }
806         }
807         if (!defined($actions{$action})) {
808                 die_error(400, "Unknown action");
809         }
810         if ($action !~ m/^(opml|project_list|project_index)$/ &&
811             !$project) {
812                 die_error(400, "Project needed");
813         }
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         return join '', map to_utf8($_), <$fd>;
2821 }
2822
2823 ## ......................................................................
2824 ## mimetype related functions
2825
2826 sub mimetype_guess_file {
2827         my $filename = shift;
2828         my $mimemap = shift;
2829         -r $mimemap or return undef;
2830
2831         my %mimemap;
2832         open(MIME, $mimemap) or return undef;
2833         while (<MIME>) {
2834                 next if m/^#/; # skip comments
2835                 my ($mime, $exts) = split(/\t+/);
2836                 if (defined $exts) {
2837                         my @exts = split(/\s+/, $exts);
2838                         foreach my $ext (@exts) {
2839                                 $mimemap{$ext} = $mime;
2840                         }
2841                 }
2842         }
2843         close(MIME);
2844
2845         $filename =~ /\.([^.]*)$/;
2846         return $mimemap{$1};
2847 }
2848
2849 sub mimetype_guess {
2850         my $filename = shift;
2851         my $mime;
2852         $filename =~ /\./ or return undef;
2853
2854         if ($mimetypes_file) {
2855                 my $file = $mimetypes_file;
2856                 if ($file !~ m!^/!) { # if it is relative path
2857                         # it is relative to project
2858                         $file = "$projectroot/$project/$file";
2859                 }
2860                 $mime = mimetype_guess_file($filename, $file);
2861         }
2862         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2863         return $mime;
2864 }
2865
2866 sub blob_mimetype {
2867         my $fd = shift;
2868         my $filename = shift;
2869
2870         if ($filename) {
2871                 my $mime = mimetype_guess($filename);
2872                 $mime and return $mime;
2873         }
2874
2875         # just in case
2876         return $default_blob_plain_mimetype unless $fd;
2877
2878         if (-T $fd) {
2879                 return 'text/plain';
2880         } elsif (! $filename) {
2881                 return 'application/octet-stream';
2882         } elsif ($filename =~ m/\.png$/i) {
2883                 return 'image/png';
2884         } elsif ($filename =~ m/\.gif$/i) {
2885                 return 'image/gif';
2886         } elsif ($filename =~ m/\.jpe?g$/i) {
2887                 return 'image/jpeg';
2888         } else {
2889                 return 'application/octet-stream';
2890         }
2891 }
2892
2893 sub blob_contenttype {
2894         my ($fd, $file_name, $type) = @_;
2895
2896         $type ||= blob_mimetype($fd, $file_name);
2897         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2898                 $type .= "; charset=$default_text_plain_charset";
2899         }
2900
2901         return $type;
2902 }
2903
2904 ## ======================================================================
2905 ## functions printing HTML: header, footer, error page
2906
2907 sub git_header_html {
2908         # XXX These aren't used, how odd.
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         $c->response->content_type($content_type);
2938
2939         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2940
2941         $c->stash->{version} = $version;
2942         $c->stash->{git_version} = $git_version;
2943         $c->stash->{content_type} = $content_type;
2944         $c->stash->{mod_perl_version} = $mod_perl_version;
2945         $c->stash->{title} = $title;
2946
2947         # the stylesheet, favicon etc urls won't work correctly with path_info
2948         # unless we set the appropriate base URL
2949         $c->stash->{baseurl} = $ENV{PATH_INFO}
2950                                                  ? q[<base href="].esc_url($base_url).q[" />]
2951                                                  : '';
2952
2953         # print out each stylesheet that exist, providing backwards capability
2954         # for those people who defined $stylesheet in a config file
2955         my $ssfmt = q[<link rel="stylesheet" type="text/css" href="%s"/>];
2956         $c->stash->{stylesheets} = [defined $stylesheet
2957                 ? sprintf($ssfmt, $stylesheet)
2958                 : map(sprintf($ssfmt, $_), grep $_, @stylesheets)
2959         ];
2960
2961         $c->stash->{project} = defined $project;
2962         if (defined $project) {
2963                 my %href_params = get_feed_info();
2964                 if (!exists $href_params{'-title'}) {
2965                         $href_params{'-title'} = 'log';
2966                 }
2967
2968                 foreach my $format qw(RSS Atom) {
2969                         my $type = lc($format);
2970                         my %link_attr = (
2971                                 '-rel' => 'alternate',
2972                                 '-title' => "$project - $href_params{'-title'} - $format feed",
2973                                 '-type' => "application/$type+xml"
2974                         );
2975
2976                         $href_params{'action'} = $type;
2977                         $link_attr{'-href'} = href(%href_params);
2978                         $c->stash->{lc $format.'_link'} = "<link ".
2979                               "rel=\"$link_attr{'-rel'}\" ".
2980                               "title=\"$link_attr{'-title'}\" ".
2981                               "href=\"$link_attr{'-href'}\" ".
2982                               "type=\"$link_attr{'-type'}\" ".
2983                               "/>\n";
2984
2985                         $href_params{'extra_options'} = '--no-merges';
2986                         $link_attr{'-href'} = href(%href_params);
2987                         $link_attr{'-title'} .= ' (no merges)';
2988                         $c->stash->{lc $format.'_link_no_merges'} = "<link ".
2989                               "rel=\"$link_attr{'-rel'}\" ".
2990                               "title=\"$link_attr{'-title'}\" ".
2991                               "href=\"$link_attr{'-href'}\" ".
2992                               "type=\"$link_attr{'-type'}\" ".
2993                               "/>\n";
2994                 }
2995
2996         } else {
2997                 $c->stash->{projects_list} = sprintf('<link rel="alternate" title="%s projects list" '.
2998                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
2999                        $site_name, href(project=>undef, action=>"project_index"));
3000                 $c->stash->{projects_feed} = sprintf('<link rel="alternate" title="%s projects feeds" '.
3001                        'href="%s" type="text/x-opml" />'."\n",
3002                        $site_name, href(project=>undef, action=>"opml"));
3003         }
3004
3005         $c->stash->{favicon} = defined $favicon
3006                 ? qq(<link rel="shortcut icon" href="$favicon" type="image/png" />)
3007                 : '';
3008
3009         # </head><body>
3010
3011         $c->stash->{site_header} = -f $site_header
3012                 ? insert_file($site_header)
3013                 : '';
3014
3015         $c->stash->{logo}
3016                 = $cgi->a({-href => esc_url($logo_url),
3017                                    -title => $logo_label},
3018                                    qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3019         $c->stash->{home_link} =  $cgi->a({-href => esc_url($home_link)}, $home_link_str);
3020
3021         if(defined $project) {
3022                 $c->stash->{summary} = $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3023                 $c->stash->{action}  = $action;
3024         }
3025
3026         my $have_search = $c->stash->{have_search} = gitweb_check_feature('search');
3027         if (defined $project && $have_search) {
3028                 if (!defined $searchtext) {
3029                         $searchtext = "";
3030                 }
3031                 my $search_hash;
3032                 if (defined $hash_base) {
3033                         $search_hash = $hash_base;
3034                 } elsif (defined $hash) {
3035                         $search_hash = $hash;
3036                 } else {
3037                         $search_hash = "HEAD";
3038                 }
3039                 my $action = $my_uri;
3040                 my $use_pathinfo = gitweb_check_feature('pathinfo');
3041                 if ($use_pathinfo) {
3042                         $action .= "/".esc_url($project);
3043                 }
3044                 # This could be done better, but meh.
3045                 $c->stash->{search_form} = $cgi->startform(-method => "get", -action => $action) .
3046                       (!$use_pathinfo &&
3047                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3048                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3049                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3050                       $cgi->popup_menu(-name => 'st', -default => 'commit',
3051                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3052                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3053                       " search:\n".
3054                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3055                       "<span title=\"Extended regular expression\">" .
3056                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3057                                      -checked => $search_use_regexp) .
3058                       "</span>" .
3059                       $cgi->end_form() . "\n";
3060         }
3061 }
3062
3063 sub git_footer_html {
3064         my $feed_class = 'rss_logo';
3065
3066         if (defined $project) {
3067                 my $descr = git_get_project_description($project);
3068                 $c->stash->{project_description} = defined $descr
3069                         ? esc_html($descr)
3070                         : '';
3071
3072                 my %href_params = get_feed_info();
3073                 if (!%href_params) {
3074                         $feed_class .= ' generic';
3075                 }
3076                 $href_params{'-title'} ||= 'log';
3077
3078                 foreach my $format qw(RSS Atom) {
3079                         $href_params{'action'} = lc($format);
3080                         $c->stash->{lc $format.'_feed'} = $cgi->a({-href => href(%href_params),
3081                                       -title => "$href_params{'-title'} $format feed",
3082                                       -class => $feed_class}, $format);
3083                 }
3084
3085         } else {
3086                 $c->stash->{opml_feed} = $cgi->a({-href => href(project=>undef, action=>"opml"),
3087                               -class => $feed_class}, "OPML");
3088                 $c->stash->{index_feed} = $cgi->a({-href => href(project=>undef, action=>"project_index"),
3089                               -class => $feed_class}, "TXT");
3090         }
3091
3092         $c->stash->{site_footer} = -f $site_footer
3093                 ? insert_file($site_footer)
3094                 : '';
3095 }
3096
3097 # die_error(<http_status_code>, <error_message>)
3098 # Example: die_error(404, 'Hash not found')
3099 # By convention, use the following status codes (as defined in RFC 2616):
3100 # 400: Invalid or missing CGI parameters, or
3101 #      requested object exists but has wrong type.
3102 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3103 #      this server or project.
3104 # 404: Requested object/revision/project doesn't exist.
3105 # 500: The server isn't configured properly, or
3106 #      an internal error occurred (e.g. failed assertions caused by bugs), or
3107 #      an unknown error occurred (e.g. the git binary died unexpectedly).
3108 sub die_error {
3109         my $status = shift || 500;
3110         my $error = shift || "Internal server error";
3111
3112         my %http_responses = (400 => '400 Bad Request',
3113                               403 => '403 Forbidden',
3114                               404 => '404 Not Found',
3115                               500 => '500 Internal Server Error');
3116         $c->response->status($http_responses{$status});
3117
3118         $c->stash->{content} = <<EOF;
3119         <div class="page_body">
3120         <br /><br />
3121         $status - $error
3122         <br />
3123         </div>
3124 EOF
3125         die bless {};
3126 }
3127
3128 ## ----------------------------------------------------------------------
3129 ## functions printing or outputting HTML: navigation
3130
3131 sub git_print_page_nav {
3132         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3133         $extra = '' if !defined $extra; # pager or formats
3134
3135         my @navs = qw(summary shortlog log commit commitdiff tree);
3136         if ($suppress) {
3137                 @navs = grep { $_ ne $suppress } @navs;
3138         }
3139
3140         my %arg = map { $_ => {action=>$_} } @navs;
3141         if (defined $head) {
3142                 for (qw(commit commitdiff)) {
3143                         $arg{$_}{'hash'} = $head;
3144                 }
3145                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3146                         for (qw(shortlog log)) {
3147                                 $arg{$_}{'hash'} = $head;
3148                         }
3149                 }
3150         }
3151
3152         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3153         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3154
3155         my @actions = gitweb_get_feature('actions');
3156         my %repl = (
3157                 '%' => '%',
3158                 'n' => $project,         # project name
3159                 'f' => $git_dir,         # project path within filesystem
3160                 'h' => $treehead || '',  # current hash ('h' parameter)
3161                 'b' => $treebase || '',  # hash base ('hb' parameter)
3162         );
3163         while (@actions) {
3164                 my ($label, $link, $pos) = splice(@actions,0,3);
3165                 # insert
3166                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3167                 # munch munch
3168                 $link =~ s/%([%nfhb])/$repl{$1}/g;
3169                 $arg{$label}{'_href'} = $link;
3170         }
3171
3172         $c->stash->{page_nav} = 1;
3173         $c->stash->{nav_links} =
3174                 (join " | ",
3175                  map { $_ eq $current ?
3176                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3177                  } @navs);
3178         $c->stash->{extra} = $extra;
3179 }
3180
3181 sub format_paging_nav {
3182         my ($action, $hash, $head, $page, $has_next_link) = @_;
3183         my $paging_nav;
3184
3185
3186         if ($hash ne $head || $page) {
3187                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3188         } else {
3189                 $paging_nav .= "HEAD";
3190         }
3191
3192         if ($page > 0) {
3193                 $paging_nav .= " &sdot; " .
3194                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
3195                                  -accesskey => "p", -title => "Alt-p"}, "prev");
3196         } else {
3197                 $paging_nav .= " &sdot; prev";
3198         }
3199
3200         if ($has_next_link) {
3201                 $paging_nav .= " &sdot; " .
3202                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
3203                                  -accesskey => "n", -title => "Alt-n"}, "next");
3204         } else {
3205                 $paging_nav .= " &sdot; next";
3206         }
3207
3208         return $paging_nav;
3209 }
3210
3211 ## ......................................................................
3212 ## functions printing or outputting HTML: div
3213
3214 sub git_print_header_div {
3215         my ($action, $title, $hash, $hash_base) = @_;
3216         my %args = ();
3217
3218         $args{'action'} = $action;
3219         $args{'hash'} = $hash if $hash;
3220         $args{'hash_base'} = $hash_base if $hash_base;
3221
3222     print q[<div class="header">],
3223               $cgi->a({-href => href(%args), -class => "title"},
3224               $title ? $title : $action),
3225                   q[</div>];
3226 }
3227
3228 sub git_print_authorship {
3229         my $co = shift;
3230
3231         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3232         print "<div class=\"author_date\">" .
3233               esc_html($co->{'author_name'}) .
3234               " [$ad{'rfc2822'}";
3235         if ($ad{'hour_local'} < 6) {
3236                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3237                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3238         } else {
3239                 printf(" (%02d:%02d %s)",
3240                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3241         }
3242         print "]</div>\n";
3243 }
3244
3245 sub git_print_page_path {
3246         my $name = shift;
3247         my $type = shift;
3248         my $hb = shift;
3249
3250
3251         print "<div class=\"page_path\">";
3252         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3253                       -title => 'tree root'}, to_utf8("[$project]"));
3254         print " / ";
3255         if (defined $name) {
3256                 my @dirname = split '/', $name;
3257                 my $basename = pop @dirname;
3258                 my $fullname = '';
3259
3260                 foreach my $dir (@dirname) {
3261                         $fullname .= ($fullname ? '/' : '') . $dir;
3262                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3263                                                      hash_base=>$hb),
3264                                       -title => $fullname}, esc_path($dir));
3265                         print " / ";
3266                 }
3267                 if (defined $type && $type eq 'blob') {
3268                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3269                                                      hash_base=>$hb),
3270                                       -title => $name}, esc_path($basename));
3271                 } elsif (defined $type && $type eq 'tree') {
3272                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3273                                                      hash_base=>$hb),
3274                                       -title => $name}, esc_path($basename));
3275                         print " / ";
3276                 } else {
3277                         print esc_path($basename);
3278                 }
3279         }
3280         print "<br/></div>\n";
3281 }
3282
3283 # sub git_print_log (\@;%) {
3284 sub git_print_log ($;%) {
3285         my $log = shift;
3286         my %opts = @_;
3287
3288         if ($opts{'-remove_title'}) {
3289                 # remove title, i.e. first line of log
3290                 shift @$log;
3291         }
3292         # remove leading empty lines
3293         while (defined $log->[0] && $log->[0] eq "") {
3294                 shift @$log;
3295         }
3296
3297         # print log
3298         my $signoff = 0;
3299         my $empty = 0;
3300         foreach my $line (@$log) {
3301                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3302                         $signoff = 1;
3303                         $empty = 0;
3304                         if (! $opts{'-remove_signoff'}) {
3305                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3306                                 next;
3307                         } else {
3308                                 # remove signoff lines
3309                                 next;
3310                         }
3311                 } else {
3312                         $signoff = 0;
3313                 }
3314
3315                 # print only one empty line
3316                 # do not print empty line after signoff
3317                 if ($line eq "") {
3318                         next if ($empty || $signoff);
3319                         $empty = 1;
3320                 } else {
3321                         $empty = 0;
3322                 }
3323
3324                 print format_log_line_html($line) . "<br/>\n";
3325         }
3326
3327         if ($opts{'-final_empty_line'}) {
3328                 # end with single empty line
3329                 print "<br/>\n" unless $empty;
3330         }
3331 }
3332
3333 # return link target (what link points to)
3334 sub git_get_link_target {
3335         my $hash = shift;
3336         my $link_target;
3337
3338         # read link
3339         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3340                 or return;
3341         {
3342                 local $/;
3343                 $link_target = <$fd>;
3344         }
3345         close $fd
3346                 or return;
3347
3348         return $link_target;
3349 }
3350
3351 # given link target, and the directory (basedir) the link is in,
3352 # return target of link relative to top directory (top tree);
3353 # return undef if it is not possible (including absolute links).
3354 sub normalize_link_target {
3355         my ($link_target, $basedir, $hash_base) = @_;
3356
3357         # we can normalize symlink target only if $hash_base is provided
3358         return unless $hash_base;
3359
3360         # absolute symlinks (beginning with '/') cannot be normalized
3361         return if (substr($link_target, 0, 1) eq '/');
3362
3363         # normalize link target to path from top (root) tree (dir)
3364         my $path;
3365         if ($basedir) {
3366                 $path = $basedir . '/' . $link_target;
3367         } else {
3368                 # we are in top (root) tree (dir)
3369                 $path = $link_target;
3370         }
3371
3372         # remove //, /./, and /../
3373         my @path_parts;
3374         foreach my $part (split('/', $path)) {
3375                 # discard '.' and ''
3376                 next if (!$part || $part eq '.');
3377                 # handle '..'
3378                 if ($part eq '..') {
3379                         if (@path_parts) {
3380                                 pop @path_parts;
3381                         } else {
3382                                 # link leads outside repository (outside top dir)
3383                                 return;
3384                         }
3385                 } else {
3386                         push @path_parts, $part;
3387                 }
3388         }
3389         $path = join('/', @path_parts);
3390
3391         return $path;
3392 }
3393
3394 # print tree entry (row of git_tree), but without encompassing <tr> element
3395 sub git_print_tree_entry {
3396         my ($t, $basedir, $hash_base, $have_blame) = @_;
3397
3398         my %base_key = ();
3399         $base_key{'hash_base'} = $hash_base if defined $hash_base;
3400
3401         # The format of a table row is: mode list link.  Where mode is
3402         # the mode of the entry, list is the name of the entry, an href,
3403         # and link is the action links of the entry.
3404
3405         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3406         if ($t->{'type'} eq "blob") {
3407                 print "<td class=\"list\">" .
3408                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3409                                                file_name=>"$basedir$t->{'name'}", %base_key),
3410                                 -class => "list"}, esc_path($t->{'name'}));
3411                 if (S_ISLNK(oct $t->{'mode'})) {
3412                         my $link_target = git_get_link_target($t->{'hash'});
3413                         if ($link_target) {
3414                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3415                                 if (defined $norm_target) {
3416                                         print " -> " .
3417                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3418                                                                      file_name=>$norm_target),
3419                                                        -title => $norm_target}, esc_path($link_target));
3420                                 } else {
3421                                         print " -> " . esc_path($link_target);
3422                                 }
3423                         }
3424                 }
3425                 print "</td>\n";
3426                 print "<td class=\"link\">";
3427                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3428                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3429                               "blob");
3430                 if ($have_blame) {
3431                         print " | " .
3432                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3433                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3434                                       "blame");
3435                 }
3436                 if (defined $hash_base) {
3437                         print " | " .
3438                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3439                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3440                                       "history");
3441                 }
3442                 print " | " .
3443                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3444                                                file_name=>"$basedir$t->{'name'}")},
3445                                 "raw");
3446                 print "</td>\n";
3447
3448         } elsif ($t->{'type'} eq "tree") {
3449                 print "<td class=\"list\">";
3450                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3451                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3452                               esc_path($t->{'name'}));
3453                 print "</td>\n";
3454                 print "<td class=\"link\">";
3455                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3456                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3457                               "tree");
3458                 if (defined $hash_base) {
3459                         print " | " .
3460                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3461                                                      file_name=>"$basedir$t->{'name'}")},
3462                                       "history");
3463                 }
3464                 print "</td>\n";
3465         } else {
3466                 # unknown object: we can only present history for it
3467                 # (this includes 'commit' object, i.e. submodule support)
3468                 print "<td class=\"list\">" .
3469                       esc_path($t->{'name'}) .
3470                       "</td>\n";
3471                 print "<td class=\"link\">";
3472                 if (defined $hash_base) {
3473                         print $cgi->a({-href => href(action=>"history",
3474                                                      hash_base=>$hash_base,
3475                                                      file_name=>"$basedir$t->{'name'}")},
3476                                       "history");
3477                 }
3478                 print "</td>\n";
3479         }
3480 }
3481
3482 ## ......................................................................
3483 ## functions printing large fragments of HTML
3484
3485 # get pre-image filenames for merge (combined) diff
3486 sub fill_from_file_info {
3487         my ($diff, @parents) = @_;
3488
3489         $diff->{'from_file'} = [ ];
3490         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3491         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3492                 if ($diff->{'status'}[$i] eq 'R' ||
3493                     $diff->{'status'}[$i] eq 'C') {
3494                         $diff->{'from_file'}[$i] =
3495                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3496                 }
3497         }
3498
3499         return $diff;
3500 }
3501
3502 # is current raw difftree line of file deletion
3503 sub is_deleted {
3504         my $diffinfo = shift;
3505
3506         return $diffinfo->{'to_id'} eq ('0' x 40);
3507 }
3508
3509 # does patch correspond to [previous] difftree raw line
3510 # $diffinfo  - hashref of parsed raw diff format
3511 # $patchinfo - hashref of parsed patch diff format
3512 #              (the same keys as in $diffinfo)
3513 sub is_patch_split {
3514         my ($diffinfo, $patchinfo) = @_;
3515
3516         return defined $diffinfo && defined $patchinfo
3517                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3518 }
3519
3520
3521 sub git_difftree_body {
3522         my ($difftree, $hash, @parents) = @_;
3523         my ($parent) = $parents[0];
3524         my $have_blame = gitweb_check_feature('blame');
3525         print "<div class=\"list_head\">\n";
3526         if ($#{$difftree} > 10) {
3527                 print(($#{$difftree} + 1) . " files changed:\n");
3528         }
3529         print "</div>\n";
3530
3531         print "<table class=\"" .
3532               (@parents > 1 ? "combined " : "") .
3533               "diff_tree\">\n";
3534
3535         # header only for combined diff in 'commitdiff' view
3536         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3537         if ($has_header) {
3538                 # table header
3539                 print "<thead><tr>\n" .
3540                        "<th></th><th></th>\n"; # filename, patchN link
3541                 for (my $i = 0; $i < @parents; $i++) {
3542                         my $par = $parents[$i];
3543                         print "<th>" .
3544                               $cgi->a({-href => href(action=>"commitdiff",
3545                                                      hash=>$hash, hash_parent=>$par),
3546                                        -title => 'commitdiff to parent number ' .
3547                                                   ($i+1) . ': ' . substr($par,0,7)},
3548                                       $i+1) .
3549                               "&nbsp;</th>\n";
3550                 }
3551                 print "</tr></thead>\n<tbody>\n";
3552         }
3553
3554         my $alternate = 1;
3555         my $patchno = 0;
3556         foreach my $line (@{$difftree}) {
3557                 my $diff = parsed_difftree_line($line);
3558
3559                 if ($alternate) {
3560                         print "<tr class=\"dark\">\n";
3561                 } else {
3562                         print "<tr class=\"light\">\n";
3563                 }
3564                 $alternate ^= 1;
3565
3566                 if (exists $diff->{'nparents'}) { # combined diff
3567
3568                         fill_from_file_info($diff, @parents)
3569                                 unless exists $diff->{'from_file'};
3570
3571                         if (!is_deleted($diff)) {
3572                                 # file exists in the result (child) commit
3573                                 print "<td>" .
3574                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3575                                                              file_name=>$diff->{'to_file'},
3576                                                              hash_base=>$hash),
3577                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3578                                       "</td>\n";
3579                         } else {
3580                                 print "<td>" .
3581                                       esc_path($diff->{'to_file'}) .
3582                                       "</td>\n";
3583                         }
3584
3585                         if ($action eq 'commitdiff') {
3586                                 # link to patch
3587                                 $patchno++;
3588                                 print "<td class=\"link\">" .
3589                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3590                                       " | " .
3591                                       "</td>\n";
3592                         }
3593
3594                         my $has_history = 0;
3595                         my $not_deleted = 0;
3596                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3597                                 my $hash_parent = $parents[$i];
3598                                 my $from_hash = $diff->{'from_id'}[$i];
3599                                 my $from_path = $diff->{'from_file'}[$i];
3600                                 my $status = $diff->{'status'}[$i];
3601
3602                                 $has_history ||= ($status ne 'A');
3603                                 $not_deleted ||= ($status ne 'D');
3604
3605                                 if ($status eq 'A') {
3606                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3607                                 } elsif ($status eq 'D') {
3608                                         print "<td class=\"link\">" .
3609                                               $cgi->a({-href => href(action=>"blob",
3610                                                                      hash_base=>$hash,
3611                                                                      hash=>$from_hash,
3612                                                                      file_name=>$from_path)},
3613                                                       "blob" . ($i+1)) .
3614                                               " | </td>\n";
3615                                 } else {
3616                                         if ($diff->{'to_id'} eq $from_hash) {
3617                                                 print "<td class=\"link nochange\">";
3618                                         } else {
3619                                                 print "<td class=\"link\">";
3620                                         }
3621                                         print $cgi->a({-href => href(action=>"blobdiff",
3622                                                                      hash=>$diff->{'to_id'},
3623                                                                      hash_parent=>$from_hash,
3624                                                                      hash_base=>$hash,
3625                                                                      hash_parent_base=>$hash_parent,
3626                                                                      file_name=>$diff->{'to_file'},
3627                                                                      file_parent=>$from_path)},
3628                                                       "diff" . ($i+1)) .
3629                                               " | </td>\n";
3630                                 }
3631                         }
3632
3633                         print "<td class=\"link\">";
3634                         if ($not_deleted) {
3635                                 print $cgi->a({-href => href(action=>"blob",
3636                                                              hash=>$diff->{'to_id'},
3637                                                              file_name=>$diff->{'to_file'},
3638                                                              hash_base=>$hash)},
3639                                               "blob");
3640                                 print " | " if ($has_history);
3641                         }
3642                         if ($has_history) {
3643                                 print $cgi->a({-href => href(action=>"history",
3644                                                              file_name=>$diff->{'to_file'},
3645                                                              hash_base=>$hash)},
3646                                               "history");
3647                         }
3648                         print "</td>\n";
3649
3650                         print "</tr>\n";
3651                         next; # instead of 'else' clause, to avoid extra indent
3652                 }
3653                 # else ordinary diff
3654
3655                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3656                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3657                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3658                         $to_mode_oct = oct $diff->{'to_mode'};
3659                         if (S_ISREG($to_mode_oct)) { # only for regular file
3660                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3661                         }
3662                         $to_file_type = file_type($diff->{'to_mode'});
3663                 }
3664                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3665                         $from_mode_oct = oct $diff->{'from_mode'};
3666                         if (S_ISREG($to_mode_oct)) { # only for regular file
3667                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3668                         }
3669                         $from_file_type = file_type($diff->{'from_mode'});
3670                 }
3671
3672                 if ($diff->{'status'} eq "A") { # created
3673                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3674                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3675                         $mode_chng   .= "]</span>";
3676                         print "<td>";
3677                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3678                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3679                                       -class => "list"}, esc_path($diff->{'file'}));
3680                         print "</td>\n";
3681                         print "<td>$mode_chng</td>\n";
3682                         print "<td class=\"link\">";
3683                         if ($action eq 'commitdiff') {
3684                                 # link to patch
3685                                 $patchno++;
3686                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3687                                 print " | ";
3688                         }
3689                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3690                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3691                                       "blob");
3692                         print "</td>\n";
3693
3694                 } elsif ($diff->{'status'} eq "D") { # deleted
3695                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3696                         print "<td>";
3697                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3698                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3699                                        -class => "list"}, esc_path($diff->{'file'}));
3700                         print "</td>\n";
3701                         print "<td>$mode_chng</td>\n";
3702                         print "<td class=\"link\">";
3703                         if ($action eq 'commitdiff') {
3704                                 # link to patch
3705                                 $patchno++;
3706                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3707                                 print " | ";
3708                         }
3709                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3710                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3711                                       "blob") . " | ";
3712                         if ($have_blame) {
3713                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3714                                                              file_name=>$diff->{'file'})},
3715                                               "blame") . " | ";
3716                         }
3717                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3718                                                      file_name=>$diff->{'file'})},
3719                                       "history");
3720                         print "</td>\n";
3721
3722                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3723                         my $mode_chnge = "";
3724                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3725                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3726                                 if ($from_file_type ne $to_file_type) {
3727                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3728                                 }
3729                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3730                                         if ($from_mode_str && $to_mode_str) {
3731                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3732                                         } elsif ($to_mode_str) {
3733                                                 $mode_chnge .= " mode: $to_mode_str";
3734                                         }
3735                                 }
3736                                 $mode_chnge .= "]</span>\n";
3737                         }
3738                         print "<td>";
3739                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3740                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3741                                       -class => "list"}, esc_path($diff->{'file'}));
3742                         print "</td>\n";
3743                         print "<td>$mode_chnge</td>\n";
3744                         print "<td class=\"link\">";
3745                         if ($action eq 'commitdiff') {
3746                                 # link to patch
3747                                 $patchno++;
3748                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3749                                       " | ";
3750                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3751                                 # "commit" view and modified file (not onlu mode changed)
3752                                 print $cgi->a({-href => href(action=>"blobdiff",
3753                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3754                                                              hash_base=>$hash, hash_parent_base=>$parent,
3755                                                              file_name=>$diff->{'file'})},
3756                                               "diff") .
3757                                       " | ";
3758                         }
3759                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3760                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3761                                        "blob") . " | ";
3762                         if ($have_blame) {
3763                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3764                                                              file_name=>$diff->{'file'})},
3765                                               "blame") . " | ";
3766                         }
3767                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3768                                                      file_name=>$diff->{'file'})},
3769                                       "history");
3770                         print "</td>\n";
3771
3772                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3773                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3774                         my $nstatus = $status_name{$diff->{'status'}};
3775                         my $mode_chng = "";
3776                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3777                                 # mode also for directories, so we cannot use $to_mode_str
3778                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3779                         }
3780                         print "<td>" .
3781                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3782                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3783                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3784                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3785                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3786                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3787                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3788                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3789                               "<td class=\"link\">";
3790                         if ($action eq 'commitdiff') {
3791                                 # link to patch
3792                                 $patchno++;
3793                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3794                                       " | ";
3795                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3796                                 # "commit" view and modified file (not only pure rename or copy)
3797                                 print $cgi->a({-href => href(action=>"blobdiff",
3798                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3799                                                              hash_base=>$hash, hash_parent_base=>$parent,
3800                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3801                                               "diff") .
3802                                       " | ";
3803                         }
3804                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3805                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3806                                       "blob") . " | ";
3807                         if ($have_blame) {
3808                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3809                                                              file_name=>$diff->{'to_file'})},
3810                                               "blame") . " | ";
3811                         }
3812                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3813                                                     file_name=>$diff->{'to_file'})},
3814                                       "history");
3815                         print "</td>\n";
3816
3817                 } # we should not encounter Unmerged (U) or Unknown (X) status
3818                 print "</tr>\n";
3819         }
3820         print "</tbody>" if $has_header;
3821         print "</table>\n";
3822 }
3823
3824 sub git_patchset_body {
3825         my ($fd, $difftree, $hash, @hash_parents) = @_;
3826         my ($hash_parent) = $hash_parents[0];
3827
3828         my $is_combined = (@hash_parents > 1);
3829         my $patch_idx = 0;
3830         my $patch_number = 0;
3831         my $patch_line;
3832         my $diffinfo;
3833         my $to_name;
3834         my (%from, %to);
3835
3836         print "<div class=\"patchset\">\n";
3837
3838         # skip to first patch
3839         while ($patch_line = <$fd>) {
3840                 chomp $patch_line;
3841
3842                 last if ($patch_line =~ m/^diff /);
3843         }
3844
3845  PATCH:
3846         while ($patch_line) {
3847
3848                 # parse "git diff" header line
3849                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3850                         # $1 is from_name, which we do not use
3851                         $to_name = unquote($2);
3852                         $to_name =~ s!^b/!!;
3853                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3854                         # $1 is 'cc' or 'combined', which we do not use
3855                         $to_name = unquote($2);
3856                 } else {
3857                         $to_name = undef;
3858                 }
3859
3860                 # check if current patch belong to current raw line
3861                 # and parse raw git-diff line if needed
3862                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3863                         # this is continuation of a split patch
3864                         print "<div class=\"patch cont\">\n";
3865                 } else {
3866                         # advance raw git-diff output if needed
3867                         $patch_idx++ if defined $diffinfo;
3868
3869                         # read and prepare patch information
3870                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3871
3872                         # compact combined diff output can have some patches skipped
3873                         # find which patch (using pathname of result) we are at now;
3874                         if ($is_combined) {
3875                                 while ($to_name ne $diffinfo->{'to_file'}) {
3876                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3877                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3878                                               "</div>\n";  # class="patch"
3879
3880                                         $patch_idx++;
3881                                         $patch_number++;
3882
3883                                         last if $patch_idx > $#$difftree;
3884                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3885                                 }
3886                         }
3887
3888                         # modifies %from, %to hashes
3889                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3890
3891                         # this is first patch for raw difftree line with $patch_idx index
3892                         # we index @$difftree array from 0, but number patches from 1
3893                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3894                 }
3895
3896                 # git diff header
3897                 #assert($patch_line =~ m/^diff /) if DEBUG;
3898                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3899                 $patch_number++;
3900                 # print "git diff" header
3901                 print format_git_diff_header_line($patch_line, $diffinfo,
3902                                                   \%from, \%to);
3903
3904                 # print extended diff header
3905                 print "<div class=\"diff extended_header\">\n";
3906         EXTENDED_HEADER:
3907                 while ($patch_line = <$fd>) {
3908                         chomp $patch_line;
3909
3910                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3911
3912                         print format_extended_diff_header_line($patch_line, $diffinfo,
3913                                                                \%from, \%to);
3914                 }
3915                 print "</div>\n"; # class="diff extended_header"
3916
3917                 # from-file/to-file diff header
3918                 if (! $patch_line) {
3919                         print "</div>\n"; # class="patch"
3920                         last PATCH;
3921                 }
3922                 next PATCH if ($patch_line =~ m/^diff /);
3923                 #assert($patch_line =~ m/^---/) if DEBUG;
3924
3925                 my $last_patch_line = $patch_line;
3926                 $patch_line = <$fd>;
3927                 chomp $patch_line;
3928                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3929
3930                 print format_diff_from_to_header($last_patch_line, $patch_line,
3931                                                  $diffinfo, \%from, \%to,
3932                                                  @hash_parents);
3933
3934                 # the patch itself
3935         LINE:
3936                 while ($patch_line = <$fd>) {
3937                         chomp $patch_line;
3938
3939                         next PATCH if ($patch_line =~ m/^diff /);
3940
3941                         print format_diff_line($patch_line, \%from, \%to);
3942                 }
3943
3944         } continue {
3945                 print "</div>\n"; # class="patch"
3946         }
3947
3948         # for compact combined (--cc) format, with chunk and patch simpliciaction
3949         # patchset might be empty, but there might be unprocessed raw lines
3950         for (++$patch_idx if $patch_number > 0;
3951              $patch_idx < @$difftree;
3952              ++$patch_idx) {
3953                 # read and prepare patch information
3954                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3955
3956                 # generate anchor for "patch" links in difftree / whatchanged part
3957                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3958                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3959                       "</div>\n";  # class="patch"
3960
3961                 $patch_number++;
3962         }
3963
3964         if ($patch_number == 0) {
3965                 if (@hash_parents > 1) {
3966                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3967                 } else {
3968                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3969                 }
3970         }
3971
3972         print "</div>\n"; # class="patchset"
3973 }
3974
3975 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3976
3977 # fills project list info (age, description, owner, forks) for each
3978 # project in the list, removing invalid projects from returned list
3979 # NOTE: modifies $projlist, but does not remove entries from it
3980 sub fill_project_list_info {
3981         my ($projlist, $check_forks) = @_;
3982         my @projects;
3983
3984         my $show_ctags = gitweb_check_feature('ctags');
3985  PROJECT:
3986         foreach my $pr (@$projlist) {
3987                 my (@activity) = git_get_last_activity($pr->{'path'});
3988                 unless (@activity) {
3989                         next PROJECT;
3990                 }
3991                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3992                 if (!defined $pr->{'descr'}) {
3993                         my $descr = git_get_project_description($pr->{'path'}) || "";
3994                         $descr = to_utf8($descr);
3995                         $pr->{'descr_long'} = $descr;
3996                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3997                 }
3998                 if (!defined $pr->{'owner'}) {
3999                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4000                 }
4001                 if ($check_forks) {
4002                         my $pname = $pr->{'path'};
4003                         if (($pname =~ s/\.git$//) &&
4004                             ($pname !~ /\/$/) &&
4005                             (-d "$projectroot/$pname")) {
4006                                 $pr->{'forks'} = "-d $projectroot/$pname";
4007                         }       else {
4008                                 $pr->{'forks'} = 0;
4009                         }
4010                 }
4011                 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4012                 push @projects, $pr;
4013         }
4014
4015         return @projects;
4016 }
4017
4018 # print 'sort by' <th> element, generating 'sort by $name' replay link
4019 # if that order is not selected
4020 sub print_sort_th {
4021         my ($name, $order, $header) = @_;
4022         $header ||= ucfirst($name);
4023
4024         if ($order eq $name) {
4025                 print "<th>$header</th>\n";
4026         } else {
4027                 print "<th>" .
4028                       $cgi->a({-href => href(-replay=>1, order=>$name),
4029                                -class => "header"}, $header) .
4030                       "</th>\n";
4031         }
4032 }
4033
4034 sub git_project_list_body {
4035         # actually uses global variable $project
4036         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4037
4038         my $check_forks = gitweb_check_feature('forks');
4039         my @projects = fill_project_list_info($projlist, $check_forks);
4040
4041         $order ||= $default_projects_order;
4042         $from = 0 unless defined $from;
4043         $to = $#projects if (!defined $to || $#projects < $to);
4044
4045         my %order_info = (
4046                 project => { key => 'path', type => 'str' },
4047                 descr => { key => 'descr_long', type => 'str' },
4048                 owner => { key => 'owner', type => 'str' },
4049                 age => { key => 'age', type => 'num' }
4050         );
4051         my $oi = $order_info{$order};
4052         if ($oi->{'type'} eq 'str') {
4053                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4054         } else {
4055                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4056         }
4057
4058         my $show_ctags = gitweb_check_feature('ctags');
4059         if ($show_ctags) {
4060                 my %ctags;
4061                 foreach my $p (@projects) {
4062                         foreach my $ct (keys %{$p->{'ctags'}}) {
4063                                 $ctags{$ct} += $p->{'ctags'}->{$ct};
4064                         }
4065                 }
4066                 my $cloud = git_populate_project_tagcloud(\%ctags);
4067                 print git_show_project_tagcloud($cloud, 64);
4068         }
4069
4070         print "<table class=\"project_list\">\n";
4071         unless ($no_header) {
4072                 print "<tr>\n";
4073                 if ($check_forks) {
4074                         print "<th></th>\n";
4075                 }
4076                 print_sort_th('project', $order, 'Project');
4077                 print_sort_th('descr', $order, 'Description');
4078                 print_sort_th('owner', $order, 'Owner');
4079                 print_sort_th('age', $order, 'Last Change');
4080                 print "<th></th>\n" . # for links
4081                       "</tr>\n";
4082         }
4083         my $alternate = 1;
4084         my $tagfilter = $cgi->param('by_tag');
4085         for (my $i = $from; $i <= $to; $i++) {
4086                 my $pr = $projects[$i];
4087
4088                 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4089                 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4090                         and not $pr->{'descr_long'} =~ /$searchtext/;
4091                 # Weed out forks or non-matching entries of search
4092                 if ($check_forks) {
4093                         my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4094                         $forkbase="^$forkbase" if $forkbase;
4095                         next if not $searchtext and not $tagfilter and $show_ctags
4096                                 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4097                 }
4098
4099                 if ($alternate) {
4100                         print "<tr class=\"dark\">\n";
4101                 } else {
4102                         print "<tr class=\"light\">\n";
4103                 }
4104                 $alternate ^= 1;
4105                 if ($check_forks) {
4106                         print "<td>";
4107                         if ($pr->{'forks'}) {
4108                                 print "<!-- $pr->{'forks'} -->\n";
4109                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4110                         }
4111                         print "</td>\n";
4112                 }
4113                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4114                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4115                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4116                                         -class => "list", -title => $pr->{'descr_long'}},
4117                                         esc_html($pr->{'descr'})) . "</td>\n" .
4118                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4119                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4120                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4121                       "<td class=\"link\">" .
4122                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4123                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4124                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4125                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4126                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4127                       "</td>\n" .
4128                       "</tr>\n";
4129         }
4130         if (defined $extra) {
4131                 print "<tr>\n";
4132                 if ($check_forks) {
4133                         print "<td></td>\n";
4134                 }
4135                 print "<td colspan=\"5\">$extra</td>\n" .
4136                       "</tr>\n";
4137         }
4138         print "</table>\n";
4139 }
4140
4141 sub git_shortlog_body {
4142         # uses global variable $project
4143         my ($commitlist, $from, $to, $refs, $extra) = @_;
4144
4145         $from = 0 unless defined $from;
4146         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4147
4148         print "<table class=\"shortlog\">\n";
4149         my $alternate = 1;
4150         for (my $i = $from; $i <= $to; $i++) {
4151                 my %co = %{$commitlist->[$i]};
4152                 my $commit = $co{'id'};
4153                 my $ref = format_ref_marker($refs, $commit);
4154                 if ($alternate) {
4155                         print "<tr class=\"dark\">\n";
4156                 } else {
4157                         print "<tr class=\"light\">\n";
4158                 }
4159                 $alternate ^= 1;
4160                 my $author = chop_and_escape_str($co{'author_name'}, 10);
4161                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4162                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4163                       "<td><i>" . $author . "</i></td>\n" .
4164                       "<td>";
4165                 print format_subject_html($co{'title'}, $co{'title_short'},
4166                                           href(action=>"commit", hash=>$commit), $ref);
4167                 print "</td>\n" .
4168                       "<td class=\"link\">" .
4169                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4170                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4171                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4172                 my $snapshot_links = format_snapshot_links($commit);
4173                 if (defined $snapshot_links) {
4174                         print " | " . $snapshot_links;
4175                 }
4176                 print "</td>\n" .
4177                       "</tr>\n";
4178         }
4179         if (defined $extra) {
4180                 print "<tr>\n" .
4181                       "<td colspan=\"4\">$extra</td>\n" .
4182                       "</tr>\n";
4183         }
4184         print "</table>\n";
4185 }
4186
4187 sub git_history_body {
4188         # Warning: assumes constant type (blob or tree) during history
4189         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4190
4191         $from = 0 unless defined $from;
4192         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4193
4194         print "<table class=\"history\">\n";
4195         my $alternate = 1;
4196         for (my $i = $from; $i <= $to; $i++) {
4197                 my %co = %{$commitlist->[$i]};
4198                 if (!%co) {
4199                         next;
4200                 }
4201                 my $commit = $co{'id'};
4202
4203                 my $ref = format_ref_marker($refs, $commit);
4204
4205                 if ($alternate) {
4206                         print "<tr class=\"dark\">\n";
4207                 } else {
4208                         print "<tr class=\"light\">\n";
4209                 }
4210                 $alternate ^= 1;
4211         # shortlog uses      chop_str($co{'author_name'}, 10)
4212                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4213                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4214                       "<td><i>" . $author . "</i></td>\n" .
4215                       "<td>";
4216                 # originally git_history used chop_str($co{'title'}, 50)
4217                 print format_subject_html($co{'title'}, $co{'title_short'},
4218                                           href(action=>"commit", hash=>$commit), $ref);
4219                 print "</td>\n" .
4220                       "<td class=\"link\">" .
4221                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4222                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4223
4224                 if ($ftype eq 'blob') {
4225                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4226                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4227                         if (defined $blob_current && defined $blob_parent &&
4228                                         $blob_current ne $blob_parent) {
4229                                 print " | " .
4230                                         $cgi->a({-href => href(action=>"blobdiff",
4231                                                                hash=>$blob_current, hash_parent=>$blob_parent,
4232                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
4233                                                                file_name=>$file_name)},
4234                                                 "diff to current");
4235                         }
4236                 }
4237                 print "</td>\n" .
4238                       "</tr>\n";
4239         }
4240         if (defined $extra) {
4241                 print "<tr>\n" .
4242                       "<td colspan=\"4\">$extra</td>\n" .
4243                       "</tr>\n";
4244         }
4245         print "</table>\n";
4246 }
4247
4248 sub git_tags_body {
4249         # uses global variable $project
4250         my ($taglist, $from, $to, $extra) = @_;
4251         $from = 0 unless defined $from;
4252         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4253
4254         print "<table class=\"tags\">\n";
4255         my $alternate = 1;
4256         for (my $i = $from; $i <= $to; $i++) {
4257                 my $entry = $taglist->[$i];
4258                 my %tag = %$entry;
4259                 my $comment = $tag{'subject'};
4260                 my $comment_short;
4261                 if (defined $comment) {
4262                         $comment_short = chop_str($comment, 30, 5);
4263                 }
4264                 if ($alternate) {
4265                         print "<tr class=\"dark\">\n";
4266                 } else {
4267                         print "<tr class=\"light\">\n";
4268                 }
4269                 $alternate ^= 1;
4270                 if (defined $tag{'age'}) {
4271                         print "<td><i>$tag{'age'}</i></td>\n";
4272                 } else {
4273                         print "<td></td>\n";
4274                 }
4275                 print "<td>" .
4276                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4277                                -class => "list name"}, esc_html($tag{'name'})) .
4278                       "</td>\n" .
4279                       "<td>";
4280                 if (defined $comment) {
4281                         print format_subject_html($comment, $comment_short,
4282                                                   href(action=>"tag", hash=>$tag{'id'}));
4283                 }
4284                 print "</td>\n" .
4285                       "<td class=\"selflink\">";
4286                 if ($tag{'type'} eq "tag") {
4287                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4288                 } else {
4289                         print "&nbsp;";
4290                 }
4291                 print "</td>\n" .
4292                       "<td class=\"link\">" . " | " .
4293                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4294                 if ($tag{'reftype'} eq "commit") {
4295                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4296                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4297                 } elsif ($tag{'reftype'} eq "blob") {
4298                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4299                 }
4300                 print "</td>\n" .
4301                       "</tr>";
4302         }
4303         if (defined $extra) {
4304                 print "<tr>\n" .
4305                       "<td colspan=\"5\">$extra</td>\n" .
4306                       "</tr>\n";
4307         }
4308         print "</table>\n";
4309 }
4310
4311 sub git_heads_body {
4312         # uses global variable $project
4313         my ($headlist, $head, $from, $to, $extra) = @_;
4314         $from = 0 unless defined $from;
4315         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4316
4317         print "<table class=\"heads\">\n";
4318         my $alternate = 1;
4319         for (my $i = $from; $i <= $to; $i++) {
4320                 my $entry = $headlist->[$i];
4321                 my %ref = %$entry;
4322                 my $curr = $ref{'id'} eq $head;
4323                 if ($alternate) {
4324                         print "<tr class=\"dark\">\n";
4325                 } else {
4326                         print "<tr class=\"light\">\n";
4327                 }
4328                 $alternate ^= 1;
4329                 print "<td><i>$ref{'age'}</i></td>\n" .
4330                       ($curr ? "<td class=\"current_head\">" : "<td>") .
4331                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4332                                -class => "list name"},esc_html($ref{'name'})) .
4333                       "</td>\n" .
4334                       "<td class=\"link\">" .
4335                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4336                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4337                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4338                       "</td>\n" .
4339                       "</tr>";
4340         }
4341         if (defined $extra) {
4342                 print "<tr>\n" .
4343                       "<td colspan=\"3\">$extra</td>\n" .
4344                       "</tr>\n";
4345         }
4346         print "</table>\n";
4347 }
4348
4349 sub git_search_grep_body {
4350         my ($commitlist, $from, $to, $extra) = @_;
4351         $from = 0 unless defined $from;
4352         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4353
4354         print "<table class=\"commit_search\">\n";
4355         my $alternate = 1;
4356         for (my $i = $from; $i <= $to; $i++) {
4357                 my %co = %{$commitlist->[$i]};
4358                 if (!%co) {
4359                         next;
4360                 }
4361                 my $commit = $co{'id'};
4362                 if ($alternate) {
4363                         print "<tr class=\"dark\">\n";
4364                 } else {
4365                         print "<tr class=\"light\">\n";
4366                 }
4367                 $alternate ^= 1;
4368                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4369                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4370                       "<td><i>" . $author . "</i></td>\n" .
4371                       "<td>" .
4372                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4373                                -class => "list subject"},
4374                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
4375                 my $comment = $co{'comment'};
4376                 foreach my $line (@$comment) {
4377                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4378                                 my ($lead, $match, $trail) = ($1, $2, $3);
4379                                 $match = chop_str($match, 70, 5, 'center');
4380                                 my $contextlen = int((80 - length($match))/2);
4381                                 $contextlen = 30 if ($contextlen > 30);
4382                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
4383                                 $trail = chop_str($trail, $contextlen, 10, 'right');
4384
4385                                 $lead  = esc_html($lead);
4386                                 $match = esc_html($match);
4387                                 $trail = esc_html($trail);
4388
4389                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
4390                         }
4391                 }
4392                 print "</td>\n" .
4393                       "<td class=\"link\">" .
4394                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4395                       " | " .
4396                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4397                       " | " .
4398                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4399                 print "</td>\n" .
4400                       "</tr>\n";
4401         }
4402         if (defined $extra) {
4403                 print "<tr>\n" .
4404                       "<td colspan=\"3\">$extra</td>\n" .
4405                       "</tr>\n";
4406         }
4407         print "</table>\n";
4408 }
4409
4410 ## ======================================================================
4411 ## ======================================================================
4412 ## actions
4413
4414 sub git_project_list {
4415         my $order = $input_params{'order'};
4416         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4417                 die_error(400, "Unknown order parameter");
4418         }
4419
4420         my @list = git_get_projects_list();
4421         if (!@list) {
4422                 die_error(404, "No projects found");
4423         }
4424
4425         if (-f $home_text) {
4426                 print "<div class=\"index_include\">\n";
4427                 print insert_file($home_text);
4428                 print "</div>\n";
4429         }
4430         print $cgi->startform(-method => "get") .
4431               "<p class=\"projsearch\">Search:\n" .
4432               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4433               "</p>" .
4434               $cgi->end_form() . "\n";
4435         git_project_list_body(\@list, $order);
4436 }
4437
4438 sub git_forks {
4439         my $order = $input_params{'order'};
4440         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4441                 die_error(400, "Unknown order parameter");
4442         }
4443
4444         my @list = git_get_projects_list($project);
4445         if (!@list) {
4446                 die_error(404, "No forks found");
4447         }
4448
4449         git_print_page_nav('','');
4450         git_print_header_div('summary', "$project forks");
4451         git_project_list_body(\@list, $order);
4452 }
4453
4454 sub git_project_index {
4455         my @projects = git_get_projects_list($project);
4456
4457         print $cgi->header(
4458                 -type => 'text/plain',
4459                 -charset => 'utf-8',
4460                 -content_disposition => 'inline; filename="index.aux"');
4461
4462         foreach my $pr (@projects) {
4463                 if (!exists $pr->{'owner'}) {
4464                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4465                 }
4466
4467                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4468                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4469                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4470                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4471                 $path  =~ s/ /\+/g;
4472                 $owner =~ s/ /\+/g;
4473
4474                 print "$path $owner\n";
4475         }
4476 }
4477
4478 sub git_summary {
4479         my $descr = git_get_project_description($project) || "none";
4480         my %co = parse_commit("HEAD");
4481         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4482         my $head = $co{'id'};
4483
4484         my $owner = git_get_project_owner($project);
4485
4486         my $refs = git_get_references();
4487         # These get_*_list functions return one more to allow us to see if
4488         # there are more ...
4489         my @taglist  = git_get_tags_list(16);
4490         my @headlist = git_get_heads_list(16);
4491         my @forklist;
4492         my $check_forks = gitweb_check_feature('forks');
4493
4494         if ($check_forks) {
4495                 @forklist = git_get_projects_list($project);
4496         }
4497
4498         git_print_page_nav('summary','', $head);
4499
4500         print "<div class=\"title\">&nbsp;</div>\n";
4501         print "<table class=\"projects_list\">\n" .
4502               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4503               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4504         if (defined $cd{'rfc2822'}) {
4505                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4506         }
4507
4508         # use per project git URL list in $projectroot/$project/cloneurl
4509         # or make project git URL from git base URL and project name
4510         my $url_tag = "URL";
4511         my @url_list = git_get_project_url_list($project);
4512         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4513         foreach my $git_url (@url_list) {
4514                 next unless $git_url;
4515                 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4516                 $url_tag = "";
4517         }
4518
4519         # Tag cloud
4520         my $show_ctags = gitweb_check_feature('ctags');
4521         if ($show_ctags) {
4522                 my $ctags = git_get_project_ctags($project);
4523                 my $cloud = git_populate_project_tagcloud($ctags);
4524                 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4525                 print "</td>\n<td>" unless %$ctags;
4526                 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4527                 print "</td>\n<td>" if %$ctags;
4528                 print git_show_project_tagcloud($cloud, 48);
4529                 print "</td></tr>";
4530         }
4531
4532         print "</table>\n";
4533
4534         # If XSS prevention is on, we don't include README.html.
4535         # TODO: Allow a readme in some safe format.
4536         if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4537                 print "<div class=\"title\">readme</div>\n" .
4538                       "<div class=\"readme\">\n";
4539                 print insert_file("$projectroot/$project/README.html");
4540                 print "\n</div>\n"; # class="readme"
4541         }
4542
4543         # we need to request one more than 16 (0..15) to check if
4544         # those 16 are all
4545         my @commitlist = $head ? parse_commits($head, 17) : ();
4546         if (@commitlist) {
4547                 git_print_header_div('shortlog');
4548                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4549                                   $#commitlist <=  15 ? undef :
4550                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4551         }
4552
4553         if (@taglist) {
4554                 git_print_header_div('tags');
4555                 git_tags_body(\@taglist, 0, 15,
4556                               $#taglist <=  15 ? undef :
4557                               $cgi->a({-href => href(action=>"tags")}, "..."));
4558         }
4559
4560         if (@headlist) {
4561                 git_print_header_div('heads');
4562                 git_heads_body(\@headlist, $head, 0, 15,
4563                                $#headlist <= 15 ? undef :
4564                                $cgi->a({-href => href(action=>"heads")}, "..."));
4565         }
4566
4567         if (@forklist) {
4568                 git_print_header_div('forks');
4569                 git_project_list_body(\@forklist, 'age', 0, 15,
4570                                       $#forklist <= 15 ? undef :
4571                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4572                                       'no_header');
4573         }
4574
4575 }
4576
4577 sub git_tag {
4578         my $head = git_get_head_hash($project);
4579         git_print_page_nav('','', $head,undef,$head);
4580         my %tag = parse_tag($hash);
4581
4582         if (! %tag) {
4583                 die_error(404, "Unknown tag object");
4584         }
4585
4586         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4587         print "<div class=\"title_text\">\n" .
4588               "<table class=\"object_header\">\n" .
4589               "<tr>\n" .
4590               "<td>object</td>\n" .
4591               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4592                                $tag{'object'}) . "</td>\n" .
4593               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4594                                               $tag{'type'}) . "</td>\n" .
4595               "</tr>\n";
4596         if (defined($tag{'author'})) {
4597                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4598                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4599                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4600                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4601                         "</td></tr>\n";
4602         }
4603         print "</table>\n\n" .
4604               "</div>\n";
4605         print "<div class=\"page_body\">";
4606         my $comment = $tag{'comment'};
4607         foreach my $line (@$comment) {
4608                 chomp $line;
4609                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4610         }
4611         print "</div>\n";
4612 }
4613
4614 sub git_blame {
4615         # permissions
4616         gitweb_check_feature('blame')
4617                 or die_error(403, "Blame view not allowed");
4618
4619         # error checking
4620         die_error(400, "No file name given") unless $file_name;
4621         $hash_base ||= git_get_head_hash($project);
4622         die_error(404, "Couldn't find base commit") unless $hash_base;
4623         my %co = parse_commit($hash_base)
4624                 or die_error(404, "Commit not found");
4625         my $ftype = "blob";
4626         if (!defined $hash) {
4627                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4628                         or die_error(404, "Error looking up file");
4629         } else {
4630                 $ftype = git_get_type($hash);
4631                 if ($ftype !~ "blob") {
4632                         die_error(400, "Object is not a blob - $hash");
4633                 }
4634         }
4635
4636         # run git-blame --porcelain
4637         open my $fd, "-|", git_cmd(), "blame", '-p',
4638                 $hash_base, '--', $file_name
4639                 or die_error(500, "Open git-blame failed");
4640
4641         # page header
4642         my $formats_nav =
4643                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4644                         "blob") .
4645                 " | " .
4646                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4647                         "history") .
4648                 " | " .
4649                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4650                         "HEAD");
4651         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4652         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4653         git_print_page_path($file_name, $ftype, $hash_base);
4654
4655         # page body
4656         my @rev_color = qw(light2 dark2);
4657         my $num_colors = scalar(@rev_color);
4658         my $current_color = 0;
4659         my %metainfo = ();
4660
4661         print <<HTML;
4662 <div class="page_body">
4663 <table class="blame">
4664 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4665 HTML
4666  LINE:
4667         while (my $line = <$fd>) {
4668                 chomp $line;
4669                 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4670                 # no <lines in group> for subsequent lines in group of lines
4671                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4672                    ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4673                 if (!exists $metainfo{$full_rev}) {
4674                         $metainfo{$full_rev} = {};
4675                 }
4676                 my $meta = $metainfo{$full_rev};
4677                 my $data;
4678                 while ($data = <$fd>) {
4679                         chomp $data;
4680                         last if ($data =~ s/^\t//); # contents of line
4681                         if ($data =~ /^(\S+) (.*)$/) {
4682                                 $meta->{$1} = $2;
4683                         }
4684                 }
4685                 my $short_rev = substr($full_rev, 0, 8);
4686                 my $author = $meta->{'author'};
4687                 my %date =
4688                         parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4689                 my $date = $date{'iso-tz'};
4690                 if ($group_size) {
4691                         $current_color = ($current_color + 1) % $num_colors;
4692                 }
4693                 print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
4694                 if ($group_size) {
4695                         print "<td class=\"sha1\"";
4696                         print " title=\"". esc_html($author) . ", $date\"";
4697                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4698                         print ">";
4699                         print $cgi->a({-href => href(action=>"commit",
4700                                                      hash=>$full_rev,
4701                                                      file_name=>$file_name)},
4702                                       esc_html($short_rev));
4703                         print "</td>\n";
4704                 }
4705                 my $parent_commit;
4706                 if (!exists $meta->{'parent'}) {
4707                         open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4708                                 or die_error(500, "Open git-rev-parse failed");
4709                         $parent_commit = <$dd>;
4710                         close $dd;
4711                         chomp($parent_commit);
4712                         $meta->{'parent'} = $parent_commit;
4713                 } else {
4714                         $parent_commit = $meta->{'parent'};
4715                 }
4716                 my $blamed = href(action => 'blame',
4717                                   file_name => $meta->{'filename'},
4718                                   hash_base => $parent_commit);
4719                 print "<td class=\"linenr\">";
4720                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4721                                 -class => "linenr" },
4722                               esc_html($lineno));
4723                 print "</td>";
4724                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4725                 print "</tr>\n";
4726         }
4727         print "</table>\n";
4728         print "</div>";
4729         close $fd
4730                 or print "Reading blob failed\n";
4731
4732         # page footer
4733 }
4734
4735 sub git_tags {
4736         my $head = git_get_head_hash($project);
4737         git_print_page_nav('','', $head,undef,$head);
4738         git_print_header_div('summary', $project);
4739
4740         my @tagslist = git_get_tags_list();
4741         if (@tagslist) {
4742                 git_tags_body(\@tagslist);
4743         }
4744 }
4745
4746 sub git_heads {
4747         my $head = git_get_head_hash($project);
4748         git_print_page_nav('','', $head,undef,$head);
4749         git_print_header_div('summary', $project);
4750
4751         my @headslist = git_get_heads_list();
4752         if (@headslist) {
4753                 git_heads_body(\@headslist, $head);
4754         }
4755 }
4756
4757 sub git_blob_plain {
4758         my $type = shift;
4759         my $expires;
4760
4761         if (!defined $hash) {
4762                 if (defined $file_name) {
4763                         my $base = $hash_base || git_get_head_hash($project);
4764                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4765                                 or die_error(404, "Cannot find file");
4766                 } else {
4767                         die_error(400, "No file name defined");
4768                 }
4769         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4770                 # blobs defined by non-textual hash id's can be cached
4771                 $expires = "+1d";
4772         }
4773
4774         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4775                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4776
4777         # content-type (can include charset)
4778         $type = blob_contenttype($fd, $file_name, $type);
4779
4780         # "save as" filename, even when no $file_name is given
4781         my $save_as = "$hash";
4782         if (defined $file_name) {
4783                 $save_as = $file_name;
4784         } elsif ($type =~ m/^text\//) {
4785                 $save_as .= '.txt';
4786         }
4787
4788         # With XSS prevention on, blobs of all types except a few known safe
4789         # ones are served with "Content-Disposition: attachment" to make sure
4790         # they don't run in our security domain.  For certain image types,
4791         # blob view writes an <img> tag referring to blob_plain view, and we
4792         # want to be sure not to break that by serving the image as an
4793         # attachment (though Firefox 3 doesn't seem to care).
4794         my $sandbox = $prevent_xss &&
4795                 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4796
4797         print $cgi->header(
4798                 -type => $type,
4799                 -expires => $expires,
4800                 -content_disposition =>
4801                         ($sandbox ? 'attachment' : 'inline')
4802                         . '; filename="' . $save_as . '"');
4803         undef $/;
4804         binmode STDOUT, ':raw';
4805         print <$fd>;
4806         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4807         $/ = "\n";
4808         close $fd;
4809 }
4810
4811 sub git_blob {
4812         my $expires;
4813
4814         if (!defined $hash) {
4815                 if (defined $file_name) {
4816                         my $base = $hash_base || git_get_head_hash($project);
4817                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4818                                 or die_error(404, "Cannot find file");
4819                 } else {
4820                         die_error(400, "No file name defined");
4821                 }
4822         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4823                 # blobs defined by non-textual hash id's can be cached
4824                 $expires = "+1d";
4825         }
4826
4827         my $have_blame = gitweb_check_feature('blame');
4828         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4829                 or die_error(500, "Couldn't cat $file_name, $hash");
4830         my $mimetype = blob_mimetype($fd, $file_name);
4831         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4832                 close $fd;
4833                 return git_blob_plain($mimetype);
4834         }
4835         # we can have blame only for text/* mimetype
4836         $have_blame &&= ($mimetype =~ m!^text/!);
4837
4838         my $formats_nav = '';
4839         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4840                 if (defined $file_name) {
4841                         if ($have_blame) {
4842                                 $formats_nav .=
4843                                         $cgi->a({-href => href(action=>"blame", hash => $hash, -replay=>1)},
4844                                                 "blame") .
4845                                         " | ";
4846                         }
4847                         $formats_nav .=
4848                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4849                                         "history") .
4850                                 " | " .
4851                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4852                                         "raw") .
4853                                 " | " .
4854                                 $cgi->a({-href => href(action=>"blob",
4855                                                        hash_base=>"HEAD", file_name=>$file_name)},
4856                                         "HEAD");
4857                 } else {
4858                         $formats_nav .=
4859                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4860                                         "raw");
4861                 }
4862                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4863                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4864         } else {
4865                 print "<div class=\"page_nav\">\n" .
4866                       "<br/><br/></div>\n" .
4867                       "<div class=\"title\">$hash</div>\n";
4868         }
4869         git_print_page_path($file_name, "blob", $hash_base);
4870         print "<div class=\"page_body\">\n";
4871         if ($mimetype =~ m!^image/!) {
4872                 print qq!<img type="$mimetype"!;
4873                 if ($file_name) {
4874                         print qq! alt="$file_name" title="$file_name"!;
4875                 }
4876                 print qq! src="! .
4877                       href(action=>"blob_plain", hash=>$hash,
4878                            hash_base=>$hash_base, file_name=>$file_name) .
4879                       qq!" />\n!;
4880         } else {
4881                 my $nr;
4882                 while (my $line = <$fd>) {
4883                         chomp $line;
4884                         $nr++;
4885                         $line = untabify($line);
4886                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4887                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4888                 }
4889         }
4890         close $fd
4891                 or print "Reading blob failed.\n";
4892         print "</div>";
4893 }
4894
4895 sub git_tree {
4896         if (!defined $hash_base) {
4897                 $hash_base = "HEAD";
4898         }
4899         if (!defined $hash) {
4900                 if (defined $file_name) {
4901                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4902                 } else {
4903                         $hash = $hash_base;
4904                 }
4905         }
4906         die_error(404, "No such tree") unless defined($hash);
4907         $/ = "\0";
4908         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4909                 or die_error(500, "Open git-ls-tree failed");
4910         my @entries = map { chomp; $_ } <$fd>;
4911         close $fd or die_error(404, "Reading tree failed");
4912         $/ = "\n";
4913
4914         my $refs = git_get_references();
4915         my $ref = format_ref_marker($refs, $hash_base);
4916         my $basedir = '';
4917         my $have_blame = gitweb_check_feature('blame');
4918         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4919                 my @views_nav = ();
4920                 if (defined $file_name) {
4921                         push @views_nav,
4922                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4923                                         "history"),
4924                                 $cgi->a({-href => href(action=>"tree",
4925                                                        hash_base=>"HEAD", file_name=>$file_name)},
4926                                         "HEAD"),
4927                 }
4928                 my $snapshot_links = format_snapshot_links($hash);
4929                 if (defined $snapshot_links) {
4930                         # FIXME: Should be available when we have no hash base as well.
4931                         push @views_nav, $snapshot_links;
4932                 }
4933                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4934                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4935         } else {
4936                 undef $hash_base;
4937                 print "<div class=\"page_nav\">\n";
4938                 print "<br/><br/></div>\n";
4939                 print "<div class=\"title\">$hash</div>\n";
4940         }
4941         if (defined $file_name) {
4942                 $basedir = $file_name;
4943                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4944                         $basedir .= '/';
4945                 }
4946                 git_print_page_path($file_name, 'tree', $hash_base);
4947         }
4948         print "<div class=\"page_body\">\n";
4949         print "<table class=\"tree\">\n";
4950         my $alternate = 1;
4951         # '..' (top directory) link if possible
4952         if (defined $hash_base &&
4953             defined $file_name && $file_name =~ m![^/]+$!) {
4954                 if ($alternate) {
4955                         print "<tr class=\"dark\">\n";
4956                 } else {
4957                         print "<tr class=\"light\">\n";
4958                 }
4959                 $alternate ^= 1;
4960
4961                 my $up = $file_name;
4962                 $up =~ s!/?[^/]+$!!;
4963                 undef $up unless $up;
4964                 # based on git_print_tree_entry
4965                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4966                 print '<td class="list">';
4967                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4968                                              file_name=>$up)},
4969                               "..");
4970                 print "</td>\n";
4971                 print "<td class=\"link\"></td>\n";
4972
4973                 print "</tr>\n";
4974         }
4975         foreach my $line (@entries) {
4976                 my %t = parse_ls_tree_line($line, -z => 1);
4977
4978                 if ($alternate) {
4979                         print "<tr class=\"dark\">\n";
4980                 } else {
4981                         print "<tr class=\"light\">\n";
4982                 }
4983                 $alternate ^= 1;
4984
4985                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4986
4987                 print "</tr>\n";
4988         }
4989         print "</table>\n" .
4990               "</div>";
4991 }
4992
4993 sub git_snapshot {
4994         my $format = $input_params{'snapshot_format'};
4995         if (!@snapshot_fmts) {
4996                 die_error(403, "Snapshots not allowed");
4997         }
4998         # default to first supported snapshot format
4999         $format ||= $snapshot_fmts[0];
5000         if ($format !~ m/^[a-z0-9]+$/) {
5001                 die_error(400, "Invalid snapshot format parameter");
5002         } elsif (!exists($known_snapshot_formats{$format})) {
5003                 die_error(400, "Unknown snapshot format");
5004         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5005                 die_error(403, "Unsupported snapshot format");
5006         }
5007
5008         if (!defined $hash) {
5009                 $hash = git_get_head_hash($project);
5010         }
5011
5012         my $name = $project;
5013         $name =~ s,([^/])/*\.git$,$1,;
5014         $name = basename($name);
5015         my $filename = to_utf8($name);
5016         $name =~ s/\047/\047\\\047\047/g;
5017         my $cmd;
5018         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5019         $cmd = quote_command(
5020                 git_cmd(), 'archive',
5021                 "--format=$known_snapshot_formats{$format}{'format'}",
5022                 "--prefix=$name/", $hash);
5023         if (exists $known_snapshot_formats{$format}{'compressor'}) {
5024                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5025         }
5026
5027         print $cgi->header(
5028                 -type => $known_snapshot_formats{$format}{'type'},
5029                 -content_disposition => 'inline; filename="' . "$filename" . '"',
5030                 -status => '200 OK');
5031
5032         open my $fd, "-|", $cmd
5033                 or die_error(500, "Execute git-archive failed");
5034         binmode STDOUT, ':raw';
5035         print <$fd>;
5036         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5037         close $fd;
5038 }
5039
5040 sub git_log {
5041         my $head = git_get_head_hash($project);
5042         if (!defined $hash) {
5043                 $hash = $head;
5044         }
5045         if (!defined $page) {
5046                 $page = 0;
5047         }
5048         my $refs = git_get_references();
5049
5050         my @commitlist = parse_commits($hash, 101, (100 * $page));
5051
5052         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5053
5054         my ($patch_max) = gitweb_get_feature('patches');
5055         if ($patch_max) {
5056                 if ($patch_max < 0 || @commitlist <= $patch_max) {
5057                         $paging_nav .= " &sdot; " .
5058                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5059                                         "patches");
5060                 }
5061         }
5062
5063         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5064
5065         if (!@commitlist) {
5066                 my %co = parse_commit($hash);
5067
5068                 git_print_header_div('summary', $project);
5069                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5070         }
5071         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5072         for (my $i = 0; $i <= $to; $i++) {
5073                 my %co = %{$commitlist[$i]};
5074                 next if !%co;
5075                 my $commit = $co{'id'};
5076                 my $ref = format_ref_marker($refs, $commit);
5077                 my %ad = parse_date($co{'author_epoch'});
5078                 git_print_header_div('commit',
5079                                "<span class=\"age\">$co{'age_string'}</span>" .
5080                                esc_html($co{'title'}) . $ref,
5081                                $commit);
5082                 print "<div class=\"title_text\">\n" .
5083                       "<div class=\"log_link\">\n" .
5084                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5085                       " | " .
5086                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5087                       " | " .
5088                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5089                       "<br/>\n" .
5090                       "</div>\n" .
5091                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
5092                       "</div>\n";
5093
5094                 print "<div class=\"log_body\">\n";
5095                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5096                 print "</div>\n";
5097         }
5098         if ($#commitlist >= 100) {
5099                 print "<div class=\"page_nav\">\n";
5100                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5101                                -accesskey => "n", -title => "Alt-n"}, "next");
5102                 print "</div>\n";
5103         }
5104 }
5105
5106 sub git_commit {
5107         $hash ||= $hash_base || "HEAD";
5108         my %co = parse_commit($hash)
5109             or die_error(404, "Unknown commit object");
5110         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5111         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
5112
5113         my $parent  = $co{'parent'};
5114         my $parents = $co{'parents'}; # listref
5115
5116         # we need to prepare $formats_nav before any parameter munging
5117         my $formats_nav;
5118         if (!defined $parent) {
5119                 # --root commitdiff
5120                 $formats_nav .= '(initial)';
5121         } elsif (@$parents == 1) {
5122                 # single parent commit
5123                 $formats_nav .=
5124                         '(parent: ' .
5125                         $cgi->a({-href => href(action=>"commit",
5126                                                hash=>$parent)},
5127                                 esc_html(substr($parent, 0, 7))) .
5128                         ')';
5129         } else {
5130                 # merge commit
5131                 $formats_nav .=
5132                         '(merge: ' .
5133                         join(' ', map {
5134                                 $cgi->a({-href => href(action=>"commit",
5135                                                        hash=>$_)},
5136                                         esc_html(substr($_, 0, 7)));
5137                         } @$parents ) .
5138                         ')';
5139         }
5140         if (gitweb_check_feature('patches')) {
5141                 $formats_nav .= " | " .
5142                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
5143                                 "patch");
5144         }
5145
5146         if (!defined $parent) {
5147                 $parent = "--root";
5148         }
5149         my @difftree;
5150         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5151                 @diff_opts,
5152                 (@$parents <= 1 ? $parent : '-c'),
5153                 $hash, "--"
5154                 or die_error(500, "Open git-diff-tree failed");
5155         @difftree = map { chomp; $_ } <$fd>;
5156         close $fd or die_error(404, "Reading git-diff-tree failed");
5157
5158         # non-textual hash id's can be cached
5159         my $expires;
5160         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5161                 $expires = "+1d";
5162         }
5163         my $refs = git_get_references();
5164         my $ref = format_ref_marker($refs, $co{'id'});
5165
5166         git_print_page_nav('commit', '',
5167                            $hash, $co{'tree'}, $hash,
5168                            $formats_nav);
5169
5170         if (defined $co{'parent'}) {
5171                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5172         } else {
5173                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5174         }
5175         print "<div class=\"title_text\">\n" .
5176               "<table class=\"object_header\">\n";
5177         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5178               "<tr>" .
5179               "<td></td><td> $ad{'rfc2822'}";
5180         if ($ad{'hour_local'} < 6) {
5181                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5182                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5183         } else {
5184                 printf(" (%02d:%02d %s)",
5185                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5186         }
5187         print "</td>" .
5188               "</tr>\n";
5189         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5190         print "<tr><td></td><td> $cd{'rfc2822'}" .
5191               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5192               "</td></tr>\n";
5193         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5194         print "<tr>" .
5195               "<td>tree</td>" .
5196               "<td class=\"sha1\">" .
5197               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5198                        class => "list"}, $co{'tree'}) .
5199               "</td>" .
5200               "<td class=\"link\">" .
5201               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5202                       "tree");
5203         my $snapshot_links = format_snapshot_links($hash);
5204         if (defined $snapshot_links) {
5205                 print " | " . $snapshot_links;
5206         }
5207         print "</td>" .
5208               "</tr>\n";
5209
5210         foreach my $par (@$parents) {
5211                 print "<tr>" .
5212                       "<td>parent</td>" .
5213                       "<td class=\"sha1\">" .
5214                       $cgi->a({-href => href(action=>"commit", hash=>$par),
5215                                class => "list"}, $par) .
5216                       "</td>" .
5217                       "<td class=\"link\">" .
5218                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5219                       " | " .
5220                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5221                       "</td>" .
5222                       "</tr>\n";
5223         }
5224         print "</table>".
5225               "</div>\n";
5226
5227         print "<div class=\"page_body\">\n";
5228         git_print_log($co{'comment'});
5229         print "</div>\n";
5230
5231         git_difftree_body(\@difftree, $hash, @$parents);
5232
5233 }
5234
5235 sub git_object {
5236         # object is defined by:
5237         # - hash or hash_base alone
5238         # - hash_base and file_name
5239         my $type;
5240
5241         # - hash or hash_base alone
5242         if ($hash || ($hash_base && !defined $file_name)) {
5243                 my $object_id = $hash || $hash_base;
5244
5245                 open my $fd, "-|", quote_command(
5246                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5247                         or die_error(404, "Object does not exist");
5248                 $type = <$fd>;
5249                 chomp $type;
5250                 close $fd
5251                         or die_error(404, "Object does not exist");
5252
5253         # - hash_base and file_name
5254         } elsif ($hash_base && defined $file_name) {
5255                 $file_name =~ s,/+$,,;
5256
5257                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5258                         or die_error(404, "Base object does not exist");
5259
5260                 # here errors should not hapen
5261                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5262                         or die_error(500, "Open git-ls-tree failed");
5263                 my $line = <$fd>;
5264                 close $fd;
5265
5266                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5267                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5268                         die_error(404, "File or directory for given base does not exist");
5269                 }
5270                 $type = $2;
5271                 $hash = $3;
5272         } else {
5273                 die_error(400, "Not enough information to find object");
5274         }
5275
5276         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5277                                           hash=>$hash, hash_base=>$hash_base,
5278                                           file_name=>$file_name),
5279                              -status => '302 Found');
5280 }
5281
5282 sub git_blobdiff {
5283         my $format = shift || 'html';
5284
5285         my $fd;
5286         my @difftree;
5287         my %diffinfo;
5288         my $expires;
5289
5290         # preparing $fd and %diffinfo for git_patchset_body
5291         # new style URI
5292         if (defined $hash_base && defined $hash_parent_base) {
5293                 if (defined $file_name) {
5294                         # read raw output
5295                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5296                                 $hash_parent_base, $hash_base,
5297                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
5298                                 or die_error(500, "Open git-diff-tree failed");
5299                         @difftree = map { chomp; $_ } <$fd>;
5300                         close $fd
5301                                 or die_error(404, "Reading git-diff-tree failed");
5302                         @difftree
5303                                 or die_error(404, "Blob diff not found");
5304
5305                 } elsif (defined $hash &&
5306                          $hash =~ /[0-9a-fA-F]{40}/) {
5307                         # try to find filename from $hash
5308
5309                         # read filtered raw output
5310                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5311                                 $hash_parent_base, $hash_base, "--"
5312                                 or die_error(500, "Open git-diff-tree failed");
5313                         @difftree =
5314                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5315                                 # $hash == to_id
5316                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5317                                 map { chomp; $_ } <$fd>;
5318                         close $fd
5319                                 or die_error(404, "Reading git-diff-tree failed");
5320                         @difftree
5321                                 or die_error(404, "Blob diff not found");
5322
5323                 } else {
5324                         die_error(400, "Missing one of the blob diff parameters");
5325                 }
5326
5327                 if (@difftree > 1) {
5328                         die_error(400, "Ambiguous blob diff specification");
5329                 }
5330
5331                 %diffinfo = parse_difftree_raw_line($difftree[0]);
5332                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5333                 $file_name   ||= $diffinfo{'to_file'};
5334
5335                 $hash_parent ||= $diffinfo{'from_id'};
5336                 $hash        ||= $diffinfo{'to_id'};
5337
5338                 # non-textual hash id's can be cached
5339                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5340                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5341                         $expires = '+1d';
5342                 }
5343
5344                 # open patch output
5345                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5346                         '-p', ($format eq 'html' ? "--full-index" : ()),
5347                         $hash_parent_base, $hash_base,
5348                         "--", (defined $file_parent ? $file_parent : ()), $file_name
5349                         or die_error(500, "Open git-diff-tree failed");
5350         }
5351
5352         # old/legacy style URI -- not generated anymore since 1.4.3.
5353         if (!%diffinfo) {
5354                 die_error('404 Not Found', "Missing one of the blob diff parameters")
5355         }
5356
5357         # header
5358         if ($format eq 'html') {
5359                 my $formats_nav =
5360                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5361                                 "raw");
5362                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5363                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5364                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5365                 } else {
5366                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5367                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5368                 }
5369                 if (defined $file_name) {
5370                         git_print_page_path($file_name, "blob", $hash_base);
5371                 } else {
5372                         print "<div class=\"page_path\"></div>\n";
5373                 }
5374
5375         } elsif ($format eq 'plain') {
5376                 print $cgi->header(
5377                         -type => 'text/plain',
5378                         -charset => 'utf-8',
5379                         -expires => $expires,
5380                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5381
5382                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5383
5384         } else {
5385                 die_error(400, "Unknown blobdiff format");
5386         }
5387
5388         # patch
5389         if ($format eq 'html') {
5390                 print "<div class=\"page_body\">\n";
5391
5392                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5393                 close $fd;
5394
5395                 print "</div>\n"; # class="page_body"
5396
5397         } else {
5398                 while (my $line = <$fd>) {
5399                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5400                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5401
5402                         print $line;
5403
5404                         last if $line =~ m!^\+\+\+!;
5405                 }
5406                 local $/ = undef;
5407                 print <$fd>;
5408                 close $fd;
5409         }
5410 }
5411
5412 sub git_blobdiff_plain {
5413         git_blobdiff('plain');
5414 }
5415
5416 sub git_commitdiff {
5417         my %params = @_;
5418         my $format = $params{-format} || 'html';
5419
5420         my ($patch_max) = gitweb_get_feature('patches');
5421         if ($format eq 'patch') {
5422                 die_error(403, "Patch view not allowed") unless $patch_max;
5423         }
5424
5425         $hash ||= $hash_base || "HEAD";
5426         my %co = parse_commit($hash)
5427             or die_error(404, "Unknown commit object");
5428
5429         # choose format for commitdiff for merge
5430         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5431                 $hash_parent = '--cc';
5432         }
5433         # we need to prepare $formats_nav before almost any parameter munging
5434         my $formats_nav;
5435         if ($format eq 'html') {
5436                 $formats_nav =
5437                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5438                                 "raw");
5439                 if ($patch_max) {
5440                         $formats_nav .= " | " .
5441                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5442                                         "patch");
5443                 }
5444
5445                 if (defined $hash_parent &&
5446                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
5447                         # commitdiff with two commits given
5448                         my $hash_parent_short = $hash_parent;
5449                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5450                                 $hash_parent_short = substr($hash_parent, 0, 7);
5451                         }
5452                         $formats_nav .=
5453                                 ' (from';
5454                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5455                                 if ($co{'parents'}[$i] eq $hash_parent) {
5456                                         $formats_nav .= ' parent ' . ($i+1);
5457                                         last;
5458                                 }
5459                         }
5460                         $formats_nav .= ': ' .
5461                                 $cgi->a({-href => href(action=>"commitdiff",
5462                                                        hash=>$hash_parent)},
5463                                         esc_html($hash_parent_short)) .
5464                                 ')';
5465                 } elsif (!$co{'parent'}) {
5466                         # --root commitdiff
5467                         $formats_nav .= ' (initial)';
5468                 } elsif (scalar @{$co{'parents'}} == 1) {
5469                         # single parent commit
5470                         $formats_nav .=
5471                                 ' (parent: ' .
5472                                 $cgi->a({-href => href(action=>"commitdiff",
5473                                                        hash=>$co{'parent'})},
5474                                         esc_html(substr($co{'parent'}, 0, 7))) .
5475                                 ')';
5476                 } else {
5477                         # merge commit
5478                         if ($hash_parent eq '--cc') {
5479                                 $formats_nav .= ' | ' .
5480                                         $cgi->a({-href => href(action=>"commitdiff",
5481                                                                hash=>$hash, hash_parent=>'-c')},
5482                                                 'combined');
5483                         } else { # $hash_parent eq '-c'
5484                                 $formats_nav .= ' | ' .
5485                                         $cgi->a({-href => href(action=>"commitdiff",
5486                                                                hash=>$hash, hash_parent=>'--cc')},
5487                                                 'compact');
5488                         }
5489                         $formats_nav .=
5490                                 ' (merge: ' .
5491                                 join(' ', map {
5492                                         $cgi->a({-href => href(action=>"commitdiff",
5493                                                                hash=>$_)},
5494                                                 esc_html(substr($_, 0, 7)));
5495                                 } @{$co{'parents'}} ) .
5496                                 ')';
5497                 }
5498         }
5499
5500         my $hash_parent_param = $hash_parent;
5501         if (!defined $hash_parent_param) {
5502                 # --cc for multiple parents, --root for parentless
5503                 $hash_parent_param =
5504                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5505         }
5506
5507         # read commitdiff
5508         my $fd;
5509         my @difftree;
5510         if ($format eq 'html') {
5511                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5512                         "--no-commit-id", "--patch-with-raw", "--full-index",
5513                         $hash_parent_param, $hash, "--"
5514                         or die_error(500, "Open git-diff-tree failed");
5515
5516                 while (my $line = <$fd>) {
5517                         chomp $line;
5518                         # empty line ends raw part of diff-tree output
5519                         last unless $line;
5520                         push @difftree, scalar parse_difftree_raw_line($line);
5521                 }
5522
5523         } elsif ($format eq 'plain') {
5524                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5525                         '-p', $hash_parent_param, $hash, "--"
5526                         or die_error(500, "Open git-diff-tree failed");
5527         } elsif ($format eq 'patch') {
5528                 # For commit ranges, we limit the output to the number of
5529                 # patches specified in the 'patches' feature.
5530                 # For single commits, we limit the output to a single patch,
5531                 # diverging from the git-format-patch default.
5532                 my @commit_spec = ();
5533                 if ($hash_parent) {
5534                         if ($patch_max > 0) {
5535                                 push @commit_spec, "-$patch_max";
5536                         }
5537                         push @commit_spec, '-n', "$hash_parent..$hash";
5538                 } else {
5539                         if ($params{-single}) {
5540                                 push @commit_spec, '-1';
5541                         } else {
5542                                 if ($patch_max > 0) {
5543                                         push @commit_spec, "-$patch_max";
5544                                 }
5545                                 push @commit_spec, "-n";
5546                         }
5547                         push @commit_spec, '--root', $hash;
5548                 }
5549                 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5550                         '--stdout', @commit_spec
5551                         or die_error(500, "Open git-format-patch failed");
5552         } else {
5553                 die_error(400, "Unknown commitdiff format");
5554         }
5555
5556         # non-textual hash id's can be cached
5557         my $expires;
5558         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5559                 $expires = "+1d";
5560         }
5561
5562         # write commit message
5563         if ($format eq 'html') {
5564                 my $refs = git_get_references();
5565                 my $ref = format_ref_marker($refs, $co{'id'});
5566
5567                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5568                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5569                 git_print_authorship(\%co);
5570                 print "<div class=\"page_body\">\n";
5571                 if (@{$co{'comment'}} > 1) {
5572                         print "<div class=\"log\">\n";
5573                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5574                         print "</div>\n"; # class="log"
5575                 }
5576
5577         } elsif ($format eq 'plain') {
5578                 my $refs = git_get_references("tags");
5579                 my $tagname = git_get_rev_name_tags($hash);
5580                 my $filename = basename($project) . "-$hash.patch";
5581
5582                 print $cgi->header(
5583                         -type => 'text/plain',
5584                         -charset => 'utf-8',
5585                         -expires => $expires,
5586                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5587                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5588                 print "From: " . to_utf8($co{'author'}) . "\n";
5589                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5590                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5591
5592                 print "X-Git-Tag: $tagname\n" if $tagname;
5593                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5594
5595                 foreach my $line (@{$co{'comment'}}) {
5596                         print to_utf8($line) . "\n";
5597                 }
5598                 print "---\n\n";
5599         } elsif ($format eq 'patch') {
5600                 my $filename = basename($project) . "-$hash.patch";
5601
5602                 print $cgi->header(
5603                         -type => 'text/plain',
5604                         -charset => 'utf-8',
5605                         -expires => $expires,
5606                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5607         }
5608
5609         # write patch
5610         if ($format eq 'html') {
5611                 my $use_parents = !defined $hash_parent ||
5612                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5613                 git_difftree_body(\@difftree, $hash,
5614                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5615                 print "<br/>\n";
5616
5617                 git_patchset_body($fd, \@difftree, $hash,
5618                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5619                 close $fd;
5620                 print "</div>\n"; # class="page_body"
5621
5622         } elsif ($format eq 'plain') {
5623                 local $/ = undef;
5624                 print <$fd>;
5625                 close $fd
5626                         or print "Reading git-diff-tree failed\n";
5627         } elsif ($format eq 'patch') {
5628                 local $/ = undef;
5629                 print <$fd>;
5630                 close $fd
5631                         or print "Reading git-format-patch failed\n";
5632         }
5633 }
5634
5635 sub git_commitdiff_plain {
5636         git_commitdiff(-format => 'plain');
5637 }
5638
5639 # format-patch-style patches
5640 sub git_patch {
5641         git_commitdiff(-format => 'patch', -single=> 1);
5642 }
5643
5644 sub git_patches {
5645         git_commitdiff(-format => 'patch');
5646 }
5647
5648 sub git_history {
5649         if (!defined $hash_base) {
5650                 $hash_base = git_get_head_hash($project);
5651         }
5652         if (!defined $page) {
5653                 $page = 0;
5654         }
5655         my $ftype;
5656         my %co = parse_commit($hash_base)
5657             or die_error(404, "Unknown commit object");
5658
5659         my $refs = git_get_references();
5660         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5661
5662         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5663                                        $file_name, "--full-history")
5664             or die_error(404, "No such file or directory on given branch");
5665
5666         if (!defined $hash && defined $file_name) {
5667                 # some commits could have deleted file in question,
5668                 # and not have it in tree, but one of them has to have it
5669                 for (my $i = 0; $i <= @commitlist; $i++) {
5670                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5671                         last if defined $hash;
5672                 }
5673         }
5674         if (defined $hash) {
5675                 $ftype = git_get_type($hash);
5676         }
5677         if (!defined $ftype) {
5678                 die_error(500, "Unknown type of object");
5679         }
5680
5681         my $paging_nav = '';
5682         if ($page > 0) {
5683                 $paging_nav .=
5684                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5685                                                file_name=>$file_name)},
5686                                 "first");
5687                 $paging_nav .= " &sdot; " .
5688                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5689                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5690         } else {
5691                 $paging_nav .= "first";
5692                 $paging_nav .= " &sdot; prev";
5693         }
5694         my $next_link = '';
5695         if ($#commitlist >= 100) {
5696                 $next_link =
5697                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5698                                  -accesskey => "n", -title => "Alt-n"}, "next");
5699                 $paging_nav .= " &sdot; $next_link";
5700         } else {
5701                 $paging_nav .= " &sdot; next";
5702         }
5703
5704         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5705         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5706         git_print_page_path($file_name, $ftype, $hash_base);
5707
5708         git_history_body(\@commitlist, 0, 99,
5709                          $refs, $hash_base, $ftype, $next_link);
5710
5711 }
5712
5713 sub git_search {
5714         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5715         if (!defined $searchtext) {
5716                 die_error(400, "Text field is empty");
5717         }
5718         if (!defined $hash) {
5719                 $hash = git_get_head_hash($project);
5720         }
5721         my %co = parse_commit($hash);
5722         if (!%co) {
5723                 die_error(404, "Unknown commit object");
5724         }
5725         if (!defined $page) {
5726                 $page = 0;
5727         }
5728
5729         $searchtype ||= 'commit';
5730         if ($searchtype eq 'pickaxe') {
5731                 # pickaxe may take all resources of your box and run for several minutes
5732                 # with every query - so decide by yourself how public you make this feature
5733                 gitweb_check_feature('pickaxe')
5734                     or die_error(403, "Pickaxe is disabled");
5735         }
5736         if ($searchtype eq 'grep') {
5737                 gitweb_check_feature('grep')
5738                     or die_error(403, "Grep is disabled");
5739         }
5740
5741
5742         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5743                 my $greptype;
5744                 if ($searchtype eq 'commit') {
5745                         $greptype = "--grep=";
5746                 } elsif ($searchtype eq 'author') {
5747                         $greptype = "--author=";
5748                 } elsif ($searchtype eq 'committer') {
5749                         $greptype = "--committer=";
5750                 }
5751                 $greptype .= $searchtext;
5752                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5753                                                $greptype, '--regexp-ignore-case',
5754                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5755
5756                 my $paging_nav = '';
5757                 if ($page > 0) {
5758                         $paging_nav .=
5759                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5760                                                        searchtext=>$searchtext,
5761                                                        searchtype=>$searchtype)},
5762                                         "first");
5763                         $paging_nav .= " &sdot; " .
5764                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5765                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5766                 } else {
5767                         $paging_nav .= "first";
5768                         $paging_nav .= " &sdot; prev";
5769                 }
5770                 my $next_link = '';
5771                 if ($#commitlist >= 100) {
5772                         $next_link =
5773                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5774                                          -accesskey => "n", -title => "Alt-n"}, "next");
5775                         $paging_nav .= " &sdot; $next_link";
5776                 } else {
5777                         $paging_nav .= " &sdot; next";
5778                 }
5779
5780                 if ($#commitlist >= 100) {
5781                 }
5782
5783                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5784                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5785                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5786         }
5787
5788         if ($searchtype eq 'pickaxe') {
5789                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5790                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5791
5792                 print "<table class=\"pickaxe search\">\n";
5793                 my $alternate = 1;
5794                 $/ = "\n";
5795                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5796                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5797                         ($search_use_regexp ? '--pickaxe-regex' : ());
5798                 undef %co;
5799                 my @files;
5800                 while (my $line = <$fd>) {
5801                         chomp $line;
5802                         next unless $line;
5803
5804                         my %set = parse_difftree_raw_line($line);
5805                         if (defined $set{'commit'}) {
5806                                 # finish previous commit
5807                                 if (%co) {
5808                                         print "</td>\n" .
5809                                               "<td class=\"link\">" .
5810                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5811                                               " | " .
5812                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5813                                         print "</td>\n" .
5814                                               "</tr>\n";
5815                                 }
5816
5817                                 if ($alternate) {
5818                                         print "<tr class=\"dark\">\n";
5819                                 } else {
5820                                         print "<tr class=\"light\">\n";
5821                                 }
5822                                 $alternate ^= 1;
5823                                 %co = parse_commit($set{'commit'});
5824                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5825                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5826                                       "<td><i>$author</i></td>\n" .
5827                                       "<td>" .
5828                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5829                                               -class => "list subject"},
5830                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5831                         } elsif (defined $set{'to_id'}) {
5832                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5833
5834                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5835                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5836                                               -class => "list"},
5837                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5838                                       "<br/>\n";
5839                         }
5840                 }
5841                 close $fd;
5842
5843                 # finish last commit (warning: repetition!)
5844                 if (%co) {
5845                         print "</td>\n" .
5846                               "<td class=\"link\">" .
5847                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5848                               " | " .
5849                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5850                         print "</td>\n" .
5851                               "</tr>\n";
5852                 }
5853
5854                 print "</table>\n";
5855         }
5856
5857         if ($searchtype eq 'grep') {
5858                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5859                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5860
5861                 print "<table class=\"grep_search\">\n";
5862                 my $alternate = 1;
5863                 my $matches = 0;
5864                 $/ = "\n";
5865                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5866                         $search_use_regexp ? ('-E', '-i') : '-F',
5867                         $searchtext, $co{'tree'};
5868                 my $lastfile = '';
5869                 while (my $line = <$fd>) {
5870                         chomp $line;
5871                         my ($file, $lno, $ltext, $binary);
5872                         last if ($matches++ > 1000);
5873                         if ($line =~ /^Binary file (.+) matches$/) {
5874                                 $file = $1;
5875                                 $binary = 1;
5876                         } else {
5877                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5878                         }
5879                         if ($file ne $lastfile) {
5880                                 $lastfile and print "</td></tr>\n";
5881                                 if ($alternate++) {
5882                                         print "<tr class=\"dark\">\n";
5883                                 } else {
5884                                         print "<tr class=\"light\">\n";
5885                                 }
5886                                 print "<td class=\"list\">".
5887                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5888                                                                file_name=>"$file"),
5889                                                 -class => "list"}, esc_path($file));
5890                                 print "</td><td>\n";
5891                                 $lastfile = $file;
5892                         }
5893                         if ($binary) {
5894                                 print "<div class=\"binary\">Binary file</div>\n";
5895                         } else {
5896                                 $ltext = untabify($ltext);
5897                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5898                                         $ltext = esc_html($1, -nbsp=>1);
5899                                         $ltext .= '<span class="match">';
5900                                         $ltext .= esc_html($2, -nbsp=>1);
5901                                         $ltext .= '</span>';
5902                                         $ltext .= esc_html($3, -nbsp=>1);
5903                                 } else {
5904                                         $ltext = esc_html($ltext, -nbsp=>1);
5905                                 }
5906                                 print "<div class=\"pre\">" .
5907                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5908                                                                file_name=>"$file").'#l'.$lno,
5909                                                 -class => "linenr"}, sprintf('%4i', $lno))
5910                                         . ' ' .  $ltext . "</div>\n";
5911                         }
5912                 }
5913                 if ($lastfile) {
5914                         print "</td></tr>\n";
5915                         if ($matches > 1000) {
5916                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5917                         }
5918                 } else {
5919                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5920                 }
5921                 close $fd;
5922
5923                 print "</table>\n";
5924         }
5925 }
5926
5927 sub git_search_help {
5928         git_print_page_nav('','', $hash,$hash,$hash);
5929         print <<EOT;
5930 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5931 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5932 the pattern entered is recognized as the POSIX extended
5933 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5934 insensitive).</p>
5935 <dl>
5936 <dt><b>commit</b></dt>
5937 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5938 EOT
5939         my $have_grep = gitweb_check_feature('grep');
5940         if ($have_grep) {
5941                 print <<EOT;
5942 <dt><b>grep</b></dt>
5943 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5944     a different one) are searched for the given pattern. On large trees, this search can take
5945 a while and put some strain on the server, so please use it with some consideration. Note that
5946 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5947 case-sensitive.</dd>
5948 EOT
5949         }
5950         print <<EOT;
5951 <dt><b>author</b></dt>
5952 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5953 <dt><b>committer</b></dt>
5954 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5955 EOT
5956         my $have_pickaxe = gitweb_check_feature('pickaxe');
5957         if ($have_pickaxe) {
5958                 print <<EOT;
5959 <dt><b>pickaxe</b></dt>
5960 <dd>All commits that caused the string to appear or disappear from any file (changes that
5961 added, removed or "modified" the string) will be listed. This search can take a while and
5962 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5963 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5964 EOT
5965         }
5966         print "</dl>\n";
5967 }
5968
5969 sub git_shortlog {
5970         my $head = git_get_head_hash($project);
5971         if (!defined $hash) {
5972                 $hash = $head;
5973         }
5974         if (!defined $page) {
5975                 $page = 0;
5976         }
5977         my $refs = git_get_references();
5978
5979         my $commit_hash = $hash;
5980         if (defined $hash_parent) {
5981                 $commit_hash = "$hash_parent..$hash";
5982         }
5983         my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5984
5985         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5986         my $next_link = '';
5987         if ($#commitlist >= 100) {
5988                 $next_link =
5989                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5990                                  -accesskey => "n", -title => "Alt-n"}, "next");
5991         }
5992         my $patch_max = gitweb_check_feature('patches');
5993         if ($patch_max) {
5994                 if ($patch_max < 0 || @commitlist <= $patch_max) {
5995                         $paging_nav .= " &sdot; " .
5996                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5997                                         "patches");
5998                 }
5999         }
6000
6001         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6002         git_print_header_div('summary', $project);
6003
6004         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6005
6006 }
6007
6008 ## ......................................................................
6009 ## feeds (RSS, Atom; OPML)
6010
6011 sub git_feed {
6012         my $format = shift || 'atom';
6013         my $have_blame = gitweb_check_feature('blame');
6014
6015         # Atom: http://www.atomenabled.org/developers/syndication/
6016         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6017         if ($format ne 'rss' && $format ne 'atom') {
6018                 die_error(400, "Unknown web feed format");
6019         }
6020
6021         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6022         my $head = $hash || 'HEAD';
6023         my @commitlist = parse_commits($head, 150, 0, $file_name);
6024
6025         my %latest_commit;
6026         my %latest_date;
6027         my $content_type = "application/$format+xml";
6028         if (defined $cgi->http('HTTP_ACCEPT') &&
6029                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6030                 # browser (feed reader) prefers text/xml
6031                 $content_type = 'text/xml';
6032         }
6033         if (defined($commitlist[0])) {
6034                 %latest_commit = %{$commitlist[0]};
6035                 my $latest_epoch = $latest_commit{'committer_epoch'};
6036                 %latest_date   = parse_date($latest_epoch);
6037                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6038                 if (defined $if_modified) {
6039                         my $since;
6040                         if (eval { require HTTP::Date; 1; }) {
6041                                 $since = HTTP::Date::str2time($if_modified);
6042                         } elsif (eval { require Time::ParseDate; 1; }) {
6043                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6044                         }
6045                         if (defined $since && $latest_epoch <= $since) {
6046                                 print $cgi->header(
6047                                         -type => $content_type,
6048                                         -charset => 'utf-8',
6049                                         -last_modified => $latest_date{'rfc2822'},
6050                                         -status => '304 Not Modified');
6051                                 return;
6052                         }
6053                 }
6054                 print $cgi->header(
6055                         -type => $content_type,
6056                         -charset => 'utf-8',
6057                         -last_modified => $latest_date{'rfc2822'});
6058         } else {
6059                 print $cgi->header(
6060                         -type => $content_type,
6061                         -charset => 'utf-8');
6062         }
6063
6064         # Optimization: skip generating the body if client asks only
6065         # for Last-Modified date.
6066         return if ($cgi->request_method() eq 'HEAD');
6067
6068         # header variables
6069         my $title = "$site_name - $project/$action";
6070         my $feed_type = 'log';
6071         if (defined $hash) {
6072                 $title .= " - '$hash'";
6073                 $feed_type = 'branch log';
6074                 if (defined $file_name) {
6075                         $title .= " :: $file_name";
6076                         $feed_type = 'history';
6077                 }
6078         } elsif (defined $file_name) {
6079                 $title .= " - $file_name";
6080                 $feed_type = 'history';
6081         }
6082         $title .= " $feed_type";
6083         my $descr = git_get_project_description($project);
6084         if (defined $descr) {
6085                 $descr = esc_html($descr);
6086         } else {
6087                 $descr = "$project " .
6088                          ($format eq 'rss' ? 'RSS' : 'Atom') .
6089                          " feed";
6090         }
6091         my $owner = git_get_project_owner($project);
6092         $owner = esc_html($owner);
6093
6094         #header
6095         my $alt_url;
6096         if (defined $file_name) {
6097                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6098         } elsif (defined $hash) {
6099                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6100         } else {
6101                 $alt_url = href(-full=>1, action=>"summary");
6102         }
6103         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6104         if ($format eq 'rss') {
6105                 print <<XML;
6106 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6107 <channel>
6108 XML
6109                 print "<title>$title</title>\n" .
6110                       "<link>$alt_url</link>\n" .
6111                       "<description>$descr</description>\n" .
6112                       "<language>en</language>\n" .
6113                       # project owner is responsible for 'editorial' content
6114                       "<managingEditor>$owner</managingEditor>\n";
6115                 if (defined $logo || defined $favicon) {
6116                         # prefer the logo to the favicon, since RSS
6117                         # doesn't allow both
6118                         my $img = esc_url($logo || $favicon);
6119                         print "<image>\n" .
6120                               "<url>$img</url>\n" .
6121                               "<title>$title</title>\n" .
6122                               "<link>$alt_url</link>\n" .
6123                               "</image>\n";
6124                 }
6125                 if (%latest_date) {
6126                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6127                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6128                 }
6129                 print "<generator>gitweb v.$version/$git_version</generator>\n";
6130         } elsif ($format eq 'atom') {
6131                 print <<XML;
6132 <feed xmlns="http://www.w3.org/2005/Atom">
6133 XML
6134                 print "<title>$title</title>\n" .
6135                       "<subtitle>$descr</subtitle>\n" .
6136                       '<link rel="alternate" type="text/html" href="' .
6137                       $alt_url . '" />' . "\n" .
6138                       '<link rel="self" type="' . $content_type . '" href="' .
6139                       $cgi->self_url() . '" />' . "\n" .
6140                       "<id>" . href(-full=>1) . "</id>\n" .
6141                       # use project owner for feed author
6142                       "<author><name>$owner</name></author>\n";
6143                 if (defined $favicon) {
6144                         print "<icon>" . esc_url($favicon) . "</icon>\n";
6145                 }
6146                 if (defined $logo_url) {
6147                         # not twice as wide as tall: 72 x 27 pixels
6148                         print "<logo>" . esc_url($logo) . "</logo>\n";
6149                 }
6150                 if (! %latest_date) {
6151                         # dummy date to keep the feed valid until commits trickle in:
6152                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
6153                 } else {
6154                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
6155                 }
6156                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6157         }
6158
6159         # contents
6160         for (my $i = 0; $i <= $#commitlist; $i++) {
6161                 my %co = %{$commitlist[$i]};
6162                 my $commit = $co{'id'};
6163                 # we read 150, we always show 30 and the ones more recent than 48 hours
6164                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6165                         last;
6166                 }
6167                 my %cd = parse_date($co{'author_epoch'});
6168
6169                 # get list of changed files
6170                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6171                         $co{'parent'} || "--root",
6172                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
6173                         or next;
6174                 my @difftree = map { chomp; $_ } <$fd>;
6175                 close $fd
6176                         or next;
6177
6178                 # print element (entry, item)
6179                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6180                 if ($format eq 'rss') {
6181                         print "<item>\n" .
6182                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
6183                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
6184                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6185                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6186                               "<link>$co_url</link>\n" .
6187                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
6188                               "<content:encoded>" .
6189                               "<![CDATA[\n";
6190                 } elsif ($format eq 'atom') {
6191                         print "<entry>\n" .
6192                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6193                               "<updated>$cd{'iso-8601'}</updated>\n" .
6194                               "<author>\n" .
6195                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6196                         if ($co{'author_email'}) {
6197                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6198                         }
6199                         print "</author>\n" .
6200                               # use committer for contributor
6201                               "<contributor>\n" .
6202                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6203                         if ($co{'committer_email'}) {
6204                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6205                         }
6206                         print "</contributor>\n" .
6207                               "<published>$cd{'iso-8601'}</published>\n" .
6208                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6209                               "<id>$co_url</id>\n" .
6210                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6211                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6212                 }
6213                 my $comment = $co{'comment'};
6214                 print "<pre>\n";
6215                 foreach my $line (@$comment) {
6216                         $line = esc_html($line);
6217                         print "$line\n";
6218                 }
6219                 print "</pre><ul>\n";
6220                 foreach my $difftree_line (@difftree) {
6221                         my %difftree = parse_difftree_raw_line($difftree_line);
6222                         next if !$difftree{'from_id'};
6223
6224                         my $file = $difftree{'file'} || $difftree{'to_file'};
6225
6226                         print "<li>" .
6227                               "[" .
6228                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6229                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6230                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6231                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
6232                                       -title => "diff"}, 'D');
6233                         if ($have_blame) {
6234                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
6235                                                              file_name=>$file, hash_base=>$commit),
6236                                               -title => "blame"}, 'B');
6237                         }
6238                         # if this is not a feed of a file history
6239                         if (!defined $file_name || $file_name ne $file) {
6240                                 print $cgi->a({-href => href(-full=>1, action=>"history",
6241                                                              file_name=>$file, hash=>$commit),
6242                                               -title => "history"}, 'H');
6243                         }
6244                         $file = esc_path($file);
6245                         print "] ".
6246                               "$file</li>\n";
6247                 }
6248                 if ($format eq 'rss') {
6249                         print "</ul>]]>\n" .
6250                               "</content:encoded>\n" .
6251                               "</item>\n";
6252                 } elsif ($format eq 'atom') {
6253                         print "</ul>\n</div>\n" .
6254                               "</content>\n" .
6255                               "</entry>\n";
6256                 }
6257         }
6258
6259         # end of feed
6260         if ($format eq 'rss') {
6261                 print "</channel>\n</rss>\n";
6262         }       elsif ($format eq 'atom') {
6263                 print "</feed>\n";
6264         }
6265 }
6266
6267 sub git_rss {
6268         git_feed('rss');
6269 }
6270
6271 sub git_atom {
6272         git_feed('atom');
6273 }
6274
6275 sub git_opml {
6276         my @list = git_get_projects_list();
6277
6278         print $cgi->header(
6279                 -type => 'text/xml',
6280                 -charset => 'utf-8',
6281                 -content_disposition => 'inline; filename="opml.xml"');
6282
6283         print <<XML;
6284 <?xml version="1.0" encoding="utf-8"?>
6285 <opml version="1.0">
6286 <head>
6287   <title>$site_name OPML Export</title>
6288 </head>
6289 <body>
6290 <outline text="git RSS feeds">
6291 XML
6292
6293         foreach my $pr (@list) {
6294                 my %proj = %$pr;
6295                 my $head = git_get_head_hash($proj{'path'});
6296                 if (!defined $head) {
6297                         next;
6298                 }
6299                 $git_dir = "$projectroot/$proj{'path'}";
6300                 my %co = parse_commit($head);
6301                 if (!%co) {
6302                         next;
6303                 }
6304
6305                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6306                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6307                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6308                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6309         }
6310         print <<XML;
6311 </outline>
6312 </body>
6313 </opml>
6314 XML
6315 }
6316
6317 1;