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