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