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