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