Code

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