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