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