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