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