Code

Merge branch 'jc/bigfile'
[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 5.008;
11 use strict;
12 use warnings;
13 use CGI qw(:standard :escapeHTML -nosticky);
14 use CGI::Util qw(unescape);
15 use CGI::Carp qw(fatalsToBrowser set_message);
16 use Encode;
17 use Fcntl ':mode';
18 use File::Find qw();
19 use File::Basename qw(basename);
20 use Time::HiRes qw(gettimeofday tv_interval);
21 binmode STDOUT, ':utf8';
23 our $t0 = [ gettimeofday() ];
24 our $number_of_git_cmds = 0;
26 BEGIN {
27         CGI->compile() if $ENV{'MOD_PERL'};
28 }
30 our $version = "++GIT_VERSION++";
32 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
33 sub evaluate_uri {
34         our $cgi;
36         our $my_url = $cgi->url();
37         our $my_uri = $cgi->url(-absolute => 1);
39         # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
40         # needed and used only for URLs with nonempty PATH_INFO
41         our $base_url = $my_url;
43         # When the script is used as DirectoryIndex, the URL does not contain the name
44         # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
45         # have to do it ourselves. We make $path_info global because it's also used
46         # later on.
47         #
48         # Another issue with the script being the DirectoryIndex is that the resulting
49         # $my_url data is not the full script URL: this is good, because we want
50         # generated links to keep implying the script name if it wasn't explicitly
51         # indicated in the URL we're handling, but it means that $my_url cannot be used
52         # as base URL.
53         # Therefore, if we needed to strip PATH_INFO, then we know that we have
54         # to build the base URL ourselves:
55         our $path_info = $ENV{"PATH_INFO"};
56         if ($path_info) {
57                 if ($my_url =~ s,\Q$path_info\E$,, &&
58                     $my_uri =~ s,\Q$path_info\E$,, &&
59                     defined $ENV{'SCRIPT_NAME'}) {
60                         $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
61                 }
62         }
64         # target of the home link on top of all pages
65         our $home_link = $my_uri || "/";
66 }
68 # core git executable to use
69 # this can just be "git" if your webserver has a sensible PATH
70 our $GIT = "++GIT_BINDIR++/git";
72 # absolute fs-path which will be prepended to the project path
73 #our $projectroot = "/pub/scm";
74 our $projectroot = "++GITWEB_PROJECTROOT++";
76 # fs traversing limit for getting project list
77 # the number is relative to the projectroot
78 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
80 # string of the home link on top of all pages
81 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
83 # name of your site or organization to appear in page titles
84 # replace this with something more descriptive for clearer bookmarks
85 our $site_name = "++GITWEB_SITENAME++"
86                  || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
88 # filename of html text to include at top of each page
89 our $site_header = "++GITWEB_SITE_HEADER++";
90 # html text to include at home page
91 our $home_text = "++GITWEB_HOMETEXT++";
92 # filename of html text to include at bottom of each page
93 our $site_footer = "++GITWEB_SITE_FOOTER++";
95 # URI of stylesheets
96 our @stylesheets = ("++GITWEB_CSS++");
97 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
98 our $stylesheet = undef;
99 # URI of GIT logo (72x27 size)
100 our $logo = "++GITWEB_LOGO++";
101 # URI of GIT favicon, assumed to be image/png type
102 our $favicon = "++GITWEB_FAVICON++";
103 # URI of gitweb.js (JavaScript code for gitweb)
104 our $javascript = "++GITWEB_JS++";
106 # URI and label (title) of GIT logo link
107 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
108 #our $logo_label = "git documentation";
109 our $logo_url = "http://git-scm.com/";
110 our $logo_label = "git homepage";
112 # source of projects list
113 our $projects_list = "++GITWEB_LIST++";
115 # the width (in characters) of the projects list "Description" column
116 our $projects_list_description_width = 25;
118 # default order of projects list
119 # valid values are none, project, descr, owner, and age
120 our $default_projects_order = "project";
122 # show repository only if this file exists
123 # (only effective if this variable evaluates to true)
124 our $export_ok = "++GITWEB_EXPORT_OK++";
126 # show repository only if this subroutine returns true
127 # when given the path to the project, for example:
128 #    sub { return -e "$_[0]/git-daemon-export-ok"; }
129 our $export_auth_hook = undef;
131 # only allow viewing of repositories also shown on the overview page
132 our $strict_export = "++GITWEB_STRICT_EXPORT++";
134 # list of git base URLs used for URL to where fetch project from,
135 # i.e. full URL is "$git_base_url/$project"
136 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
138 # default blob_plain mimetype and default charset for text/plain blob
139 our $default_blob_plain_mimetype = 'text/plain';
140 our $default_text_plain_charset  = undef;
142 # file to use for guessing MIME types before trying /etc/mime.types
143 # (relative to the current git repository)
144 our $mimetypes_file = undef;
146 # assume this charset if line contains non-UTF-8 characters;
147 # it should be valid encoding (see Encoding::Supported(3pm) for list),
148 # for which encoding all byte sequences are valid, for example
149 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
150 # could be even 'utf-8' for the old behavior)
151 our $fallback_encoding = 'latin1';
153 # rename detection options for git-diff and git-diff-tree
154 # - default is '-M', with the cost proportional to
155 #   (number of removed files) * (number of new files).
156 # - more costly is '-C' (which implies '-M'), with the cost proportional to
157 #   (number of changed files + number of removed files) * (number of new files)
158 # - even more costly is '-C', '--find-copies-harder' with cost
159 #   (number of files in the original tree) * (number of new files)
160 # - one might want to include '-B' option, e.g. '-B', '-M'
161 our @diff_opts = ('-M'); # taken from git_commit
163 # Disables features that would allow repository owners to inject script into
164 # the gitweb domain.
165 our $prevent_xss = 0;
167 # Path to the highlight executable to use (must be the one from
168 # http://www.andre-simon.de due to assumptions about parameters and output).
169 # Useful if highlight is not installed on your webserver's PATH.
170 # [Default: highlight]
171 our $highlight_bin = "++HIGHLIGHT_BIN++";
173 # information about snapshot formats that gitweb is capable of serving
174 our %known_snapshot_formats = (
175         # name => {
176         #       'display' => display name,
177         #       'type' => mime type,
178         #       'suffix' => filename suffix,
179         #       'format' => --format for git-archive,
180         #       'compressor' => [compressor command and arguments]
181         #                       (array reference, optional)
182         #       'disabled' => boolean (optional)}
183         #
184         'tgz' => {
185                 'display' => 'tar.gz',
186                 'type' => 'application/x-gzip',
187                 'suffix' => '.tar.gz',
188                 'format' => 'tar',
189                 'compressor' => ['gzip', '-n']},
191         'tbz2' => {
192                 'display' => 'tar.bz2',
193                 'type' => 'application/x-bzip2',
194                 'suffix' => '.tar.bz2',
195                 'format' => 'tar',
196                 'compressor' => ['bzip2']},
198         'txz' => {
199                 'display' => 'tar.xz',
200                 'type' => 'application/x-xz',
201                 'suffix' => '.tar.xz',
202                 'format' => 'tar',
203                 'compressor' => ['xz'],
204                 'disabled' => 1},
206         'zip' => {
207                 'display' => 'zip',
208                 'type' => 'application/x-zip',
209                 'suffix' => '.zip',
210                 'format' => 'zip'},
211 );
213 # Aliases so we understand old gitweb.snapshot values in repository
214 # configuration.
215 our %known_snapshot_format_aliases = (
216         'gzip'  => 'tgz',
217         'bzip2' => 'tbz2',
218         'xz'    => 'txz',
220         # backward compatibility: legacy gitweb config support
221         'x-gzip' => undef, 'gz' => undef,
222         'x-bzip2' => undef, 'bz2' => undef,
223         'x-zip' => undef, '' => undef,
224 );
226 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
227 # are changed, it may be appropriate to change these values too via
228 # $GITWEB_CONFIG.
229 our %avatar_size = (
230         'default' => 16,
231         'double'  => 32
232 );
234 # Used to set the maximum load that we will still respond to gitweb queries.
235 # If server load exceed this value then return "503 server busy" error.
236 # If gitweb cannot determined server load, it is taken to be 0.
237 # Leave it undefined (or set to 'undef') to turn off load checking.
238 our $maxload = 300;
240 # configuration for 'highlight' (http://www.andre-simon.de/)
241 # match by basename
242 our %highlight_basename = (
243         #'Program' => 'py',
244         #'Library' => 'py',
245         'SConstruct' => 'py', # SCons equivalent of Makefile
246         'Makefile' => 'make',
247 );
248 # match by extension
249 our %highlight_ext = (
250         # main extensions, defining name of syntax;
251         # see files in /usr/share/highlight/langDefs/ directory
252         map { $_ => $_ }
253                 qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
254         # alternate extensions, see /etc/highlight/filetypes.conf
255         'h' => 'c',
256         map { $_ => 'sh'  } qw(bash zsh ksh),
257         map { $_ => 'cpp' } qw(cxx c++ cc),
258         map { $_ => 'php' } qw(php3 php4 php5 phps),
259         map { $_ => 'pl'  } qw(perl pm), # perhaps also 'cgi'
260         map { $_ => 'make'} qw(mak mk),
261         map { $_ => 'xml' } qw(xhtml html htm),
262 );
264 # You define site-wide feature defaults here; override them with
265 # $GITWEB_CONFIG as necessary.
266 our %feature = (
267         # feature => {
268         #       'sub' => feature-sub (subroutine),
269         #       'override' => allow-override (boolean),
270         #       'default' => [ default options...] (array reference)}
271         #
272         # if feature is overridable (it means that allow-override has true value),
273         # then feature-sub will be called with default options as parameters;
274         # return value of feature-sub indicates if to enable specified feature
275         #
276         # if there is no 'sub' key (no feature-sub), then feature cannot be
277         # overridden
278         #
279         # use gitweb_get_feature(<feature>) to retrieve the <feature> value
280         # (an array) or gitweb_check_feature(<feature>) to check if <feature>
281         # is enabled
283         # Enable the 'blame' blob view, showing the last commit that modified
284         # each line in the file. This can be very CPU-intensive.
286         # To enable system wide have in $GITWEB_CONFIG
287         # $feature{'blame'}{'default'} = [1];
288         # To have project specific config enable override in $GITWEB_CONFIG
289         # $feature{'blame'}{'override'} = 1;
290         # and in project config gitweb.blame = 0|1;
291         'blame' => {
292                 'sub' => sub { feature_bool('blame', @_) },
293                 'override' => 0,
294                 'default' => [0]},
296         # Enable the 'snapshot' link, providing a compressed archive of any
297         # tree. This can potentially generate high traffic if you have large
298         # project.
300         # Value is a list of formats defined in %known_snapshot_formats that
301         # you wish to offer.
302         # To disable system wide have in $GITWEB_CONFIG
303         # $feature{'snapshot'}{'default'} = [];
304         # To have project specific config enable override in $GITWEB_CONFIG
305         # $feature{'snapshot'}{'override'} = 1;
306         # and in project config, a comma-separated list of formats or "none"
307         # to disable.  Example: gitweb.snapshot = tbz2,zip;
308         'snapshot' => {
309                 'sub' => \&feature_snapshot,
310                 'override' => 0,
311                 'default' => ['tgz']},
313         # Enable text search, which will list the commits which match author,
314         # committer or commit text to a given string.  Enabled by default.
315         # Project specific override is not supported.
316         'search' => {
317                 'override' => 0,
318                 'default' => [1]},
320         # Enable grep search, which will list the files in currently selected
321         # tree containing the given string. Enabled by default. This can be
322         # potentially CPU-intensive, of course.
324         # To enable system wide have in $GITWEB_CONFIG
325         # $feature{'grep'}{'default'} = [1];
326         # To have project specific config enable override in $GITWEB_CONFIG
327         # $feature{'grep'}{'override'} = 1;
328         # and in project config gitweb.grep = 0|1;
329         'grep' => {
330                 'sub' => sub { feature_bool('grep', @_) },
331                 'override' => 0,
332                 'default' => [1]},
334         # Enable the pickaxe search, which will list the commits that modified
335         # a given string in a file. This can be practical and quite faster
336         # alternative to 'blame', but still potentially CPU-intensive.
338         # To enable system wide have in $GITWEB_CONFIG
339         # $feature{'pickaxe'}{'default'} = [1];
340         # To have project specific config enable override in $GITWEB_CONFIG
341         # $feature{'pickaxe'}{'override'} = 1;
342         # and in project config gitweb.pickaxe = 0|1;
343         'pickaxe' => {
344                 'sub' => sub { feature_bool('pickaxe', @_) },
345                 'override' => 0,
346                 'default' => [1]},
348         # Enable showing size of blobs in a 'tree' view, in a separate
349         # column, similar to what 'ls -l' does.  This cost a bit of IO.
351         # To disable system wide have in $GITWEB_CONFIG
352         # $feature{'show-sizes'}{'default'} = [0];
353         # To have project specific config enable override in $GITWEB_CONFIG
354         # $feature{'show-sizes'}{'override'} = 1;
355         # and in project config gitweb.showsizes = 0|1;
356         'show-sizes' => {
357                 'sub' => sub { feature_bool('showsizes', @_) },
358                 'override' => 0,
359                 'default' => [1]},
361         # Make gitweb use an alternative format of the URLs which can be
362         # more readable and natural-looking: project name is embedded
363         # directly in the path and the query string contains other
364         # auxiliary information. All gitweb installations recognize
365         # URL in either format; this configures in which formats gitweb
366         # generates links.
368         # To enable system wide have in $GITWEB_CONFIG
369         # $feature{'pathinfo'}{'default'} = [1];
370         # Project specific override is not supported.
372         # Note that you will need to change the default location of CSS,
373         # favicon, logo and possibly other files to an absolute URL. Also,
374         # if gitweb.cgi serves as your indexfile, you will need to force
375         # $my_uri to contain the script name in your $GITWEB_CONFIG.
376         'pathinfo' => {
377                 'override' => 0,
378                 'default' => [0]},
380         # Make gitweb consider projects in project root subdirectories
381         # to be forks of existing projects. Given project $projname.git,
382         # projects matching $projname/*.git will not be shown in the main
383         # projects list, instead a '+' mark will be added to $projname
384         # there and a 'forks' view will be enabled for the project, listing
385         # all the forks. If project list is taken from a file, forks have
386         # to be listed after the main project.
388         # To enable system wide have in $GITWEB_CONFIG
389         # $feature{'forks'}{'default'} = [1];
390         # Project specific override is not supported.
391         'forks' => {
392                 'override' => 0,
393                 'default' => [0]},
395         # Insert custom links to the action bar of all project pages.
396         # This enables you mainly to link to third-party scripts integrating
397         # into gitweb; e.g. git-browser for graphical history representation
398         # or custom web-based repository administration interface.
400         # The 'default' value consists of a list of triplets in the form
401         # (label, link, position) where position is the label after which
402         # to insert the link and link is a format string where %n expands
403         # to the project name, %f to the project path within the filesystem,
404         # %h to the current hash (h gitweb parameter) and %b to the current
405         # hash base (hb gitweb parameter); %% expands to %.
407         # To enable system wide have in $GITWEB_CONFIG e.g.
408         # $feature{'actions'}{'default'} = [('graphiclog',
409         #       '/git-browser/by-commit.html?r=%n', 'summary')];
410         # Project specific override is not supported.
411         'actions' => {
412                 'override' => 0,
413                 'default' => []},
415         # Allow gitweb scan project content tags of project repository,
416         # and display the popular Web 2.0-ish "tag cloud" near the projects
417         # list.  Note that this is something COMPLETELY different from the
418         # normal Git tags.
420         # gitweb by itself can show existing tags, but it does not handle
421         # tagging itself; you need to do it externally, outside gitweb.
422         # The format is described in git_get_project_ctags() subroutine.
423         # You may want to install the HTML::TagCloud Perl module to get
424         # a pretty tag cloud instead of just a list of tags.
426         # To enable system wide have in $GITWEB_CONFIG
427         # $feature{'ctags'}{'default'} = [1];
428         # Project specific override is not supported.
430         # In the future whether ctags editing is enabled might depend
431         # on the value, but using 1 should always mean no editing of ctags.
432         'ctags' => {
433                 'override' => 0,
434                 'default' => [0]},
436         # The maximum number of patches in a patchset generated in patch
437         # view. Set this to 0 or undef to disable patch view, or to a
438         # negative number to remove any limit.
440         # To disable system wide have in $GITWEB_CONFIG
441         # $feature{'patches'}{'default'} = [0];
442         # To have project specific config enable override in $GITWEB_CONFIG
443         # $feature{'patches'}{'override'} = 1;
444         # and in project config gitweb.patches = 0|n;
445         # where n is the maximum number of patches allowed in a patchset.
446         'patches' => {
447                 'sub' => \&feature_patches,
448                 'override' => 0,
449                 'default' => [16]},
451         # Avatar support. When this feature is enabled, views such as
452         # shortlog or commit will display an avatar associated with
453         # the email of the committer(s) and/or author(s).
455         # Currently available providers are gravatar and picon.
456         # If an unknown provider is specified, the feature is disabled.
458         # Gravatar depends on Digest::MD5.
459         # Picon currently relies on the indiana.edu database.
461         # To enable system wide have in $GITWEB_CONFIG
462         # $feature{'avatar'}{'default'} = ['<provider>'];
463         # where <provider> is either gravatar or picon.
464         # To have project specific config enable override in $GITWEB_CONFIG
465         # $feature{'avatar'}{'override'} = 1;
466         # and in project config gitweb.avatar = <provider>;
467         'avatar' => {
468                 'sub' => \&feature_avatar,
469                 'override' => 0,
470                 'default' => ['']},
472         # Enable displaying how much time and how many git commands
473         # it took to generate and display page.  Disabled by default.
474         # Project specific override is not supported.
475         'timed' => {
476                 'override' => 0,
477                 'default' => [0]},
479         # Enable turning some links into links to actions which require
480         # JavaScript to run (like 'blame_incremental').  Not enabled by
481         # default.  Project specific override is currently not supported.
482         'javascript-actions' => {
483                 'override' => 0,
484                 'default' => [0]},
486         # Syntax highlighting support. This is based on Daniel Svensson's
487         # and Sham Chukoury's work in gitweb-xmms2.git.
488         # It requires the 'highlight' program present in $PATH,
489         # and therefore is disabled by default.
491         # To enable system wide have in $GITWEB_CONFIG
492         # $feature{'highlight'}{'default'} = [1];
494         'highlight' => {
495                 'sub' => sub { feature_bool('highlight', @_) },
496                 'override' => 0,
497                 'default' => [0]},
499         # Enable displaying of remote heads in the heads list
501         # To enable system wide have in $GITWEB_CONFIG
502         # $feature{'remote_heads'}{'default'} = [1];
503         # To have project specific config enable override in $GITWEB_CONFIG
504         # $feature{'remote_heads'}{'override'} = 1;
505         # and in project config gitweb.remote_heads = 0|1;
506         'remote_heads' => {
507                 'sub' => sub { feature_bool('remote_heads', @_) },
508                 'override' => 0,
509                 'default' => [0]},
510 );
512 sub gitweb_get_feature {
513         my ($name) = @_;
514         return unless exists $feature{$name};
515         my ($sub, $override, @defaults) = (
516                 $feature{$name}{'sub'},
517                 $feature{$name}{'override'},
518                 @{$feature{$name}{'default'}});
519         # project specific override is possible only if we have project
520         our $git_dir; # global variable, declared later
521         if (!$override || !defined $git_dir) {
522                 return @defaults;
523         }
524         if (!defined $sub) {
525                 warn "feature $name is not overridable";
526                 return @defaults;
527         }
528         return $sub->(@defaults);
531 # A wrapper to check if a given feature is enabled.
532 # With this, you can say
534 #   my $bool_feat = gitweb_check_feature('bool_feat');
535 #   gitweb_check_feature('bool_feat') or somecode;
537 # instead of
539 #   my ($bool_feat) = gitweb_get_feature('bool_feat');
540 #   (gitweb_get_feature('bool_feat'))[0] or somecode;
542 sub gitweb_check_feature {
543         return (gitweb_get_feature(@_))[0];
547 sub feature_bool {
548         my $key = shift;
549         my ($val) = git_get_project_config($key, '--bool');
551         if (!defined $val) {
552                 return ($_[0]);
553         } elsif ($val eq 'true') {
554                 return (1);
555         } elsif ($val eq 'false') {
556                 return (0);
557         }
560 sub feature_snapshot {
561         my (@fmts) = @_;
563         my ($val) = git_get_project_config('snapshot');
565         if ($val) {
566                 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
567         }
569         return @fmts;
572 sub feature_patches {
573         my @val = (git_get_project_config('patches', '--int'));
575         if (@val) {
576                 return @val;
577         }
579         return ($_[0]);
582 sub feature_avatar {
583         my @val = (git_get_project_config('avatar'));
585         return @val ? @val : @_;
588 # checking HEAD file with -e is fragile if the repository was
589 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
590 # and then pruned.
591 sub check_head_link {
592         my ($dir) = @_;
593         my $headfile = "$dir/HEAD";
594         return ((-e $headfile) ||
595                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
598 sub check_export_ok {
599         my ($dir) = @_;
600         return (check_head_link($dir) &&
601                 (!$export_ok || -e "$dir/$export_ok") &&
602                 (!$export_auth_hook || $export_auth_hook->($dir)));
605 # process alternate names for backward compatibility
606 # filter out unsupported (unknown) snapshot formats
607 sub filter_snapshot_fmts {
608         my @fmts = @_;
610         @fmts = map {
611                 exists $known_snapshot_format_aliases{$_} ?
612                        $known_snapshot_format_aliases{$_} : $_} @fmts;
613         @fmts = grep {
614                 exists $known_snapshot_formats{$_} &&
615                 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
618 # If it is set to code reference, it is code that it is to be run once per
619 # request, allowing updating configurations that change with each request,
620 # while running other code in config file only once.
622 # Otherwise, if it is false then gitweb would process config file only once;
623 # if it is true then gitweb config would be run for each request.
624 our $per_request_config = 1;
626 # read and parse gitweb config file given by its parameter.
627 # returns true on success, false on recoverable error, allowing
628 # to chain this subroutine, using first file that exists.
629 # dies on errors during parsing config file, as it is unrecoverable.
630 sub read_config_file {
631         my $filename = shift;
632         return unless defined $filename;
633         # die if there are errors parsing config file
634         if (-e $filename) {
635                 do $filename;
636                 die $@ if $@;
637                 return 1;
638         }
639         return;
642 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
643 sub evaluate_gitweb_config {
644         our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
645         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
647         # use first config file that exists
648         read_config_file($GITWEB_CONFIG) or
649         read_config_file($GITWEB_CONFIG_SYSTEM);
652 # Get loadavg of system, to compare against $maxload.
653 # Currently it requires '/proc/loadavg' present to get loadavg;
654 # if it is not present it returns 0, which means no load checking.
655 sub get_loadavg {
656         if( -e '/proc/loadavg' ){
657                 open my $fd, '<', '/proc/loadavg'
658                         or return 0;
659                 my @load = split(/\s+/, scalar <$fd>);
660                 close $fd;
662                 # The first three columns measure CPU and IO utilization of the last one,
663                 # five, and 10 minute periods.  The fourth column shows the number of
664                 # currently running processes and the total number of processes in the m/n
665                 # format.  The last column displays the last process ID used.
666                 return $load[0] || 0;
667         }
668         # additional checks for load average should go here for things that don't export
669         # /proc/loadavg
671         return 0;
674 # version of the core git binary
675 our $git_version;
676 sub evaluate_git_version {
677         our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
678         $number_of_git_cmds++;
681 sub check_loadavg {
682         if (defined $maxload && get_loadavg() > $maxload) {
683                 die_error(503, "The load average on the server is too high");
684         }
687 # ======================================================================
688 # input validation and dispatch
690 # input parameters can be collected from a variety of sources (presently, CGI
691 # and PATH_INFO), so we define an %input_params hash that collects them all
692 # together during validation: this allows subsequent uses (e.g. href()) to be
693 # agnostic of the parameter origin
695 our %input_params = ();
697 # input parameters are stored with the long parameter name as key. This will
698 # also be used in the href subroutine to convert parameters to their CGI
699 # equivalent, and since the href() usage is the most frequent one, we store
700 # the name -> CGI key mapping here, instead of the reverse.
702 # XXX: Warning: If you touch this, check the search form for updating,
703 # too.
705 our @cgi_param_mapping = (
706         project => "p",
707         action => "a",
708         file_name => "f",
709         file_parent => "fp",
710         hash => "h",
711         hash_parent => "hp",
712         hash_base => "hb",
713         hash_parent_base => "hpb",
714         page => "pg",
715         order => "o",
716         searchtext => "s",
717         searchtype => "st",
718         snapshot_format => "sf",
719         extra_options => "opt",
720         search_use_regexp => "sr",
721         ctag => "by_tag",
722         # this must be last entry (for manipulation from JavaScript)
723         javascript => "js"
724 );
725 our %cgi_param_mapping = @cgi_param_mapping;
727 # we will also need to know the possible actions, for validation
728 our %actions = (
729         "blame" => \&git_blame,
730         "blame_incremental" => \&git_blame_incremental,
731         "blame_data" => \&git_blame_data,
732         "blobdiff" => \&git_blobdiff,
733         "blobdiff_plain" => \&git_blobdiff_plain,
734         "blob" => \&git_blob,
735         "blob_plain" => \&git_blob_plain,
736         "commitdiff" => \&git_commitdiff,
737         "commitdiff_plain" => \&git_commitdiff_plain,
738         "commit" => \&git_commit,
739         "forks" => \&git_forks,
740         "heads" => \&git_heads,
741         "history" => \&git_history,
742         "log" => \&git_log,
743         "patch" => \&git_patch,
744         "patches" => \&git_patches,
745         "remotes" => \&git_remotes,
746         "rss" => \&git_rss,
747         "atom" => \&git_atom,
748         "search" => \&git_search,
749         "search_help" => \&git_search_help,
750         "shortlog" => \&git_shortlog,
751         "summary" => \&git_summary,
752         "tag" => \&git_tag,
753         "tags" => \&git_tags,
754         "tree" => \&git_tree,
755         "snapshot" => \&git_snapshot,
756         "object" => \&git_object,
757         # those below don't need $project
758         "opml" => \&git_opml,
759         "project_list" => \&git_project_list,
760         "project_index" => \&git_project_index,
761 );
763 # finally, we have the hash of allowed extra_options for the commands that
764 # allow them
765 our %allowed_options = (
766         "--no-merges" => [ qw(rss atom log shortlog history) ],
767 );
769 # fill %input_params with the CGI parameters. All values except for 'opt'
770 # should be single values, but opt can be an array. We should probably
771 # build an array of parameters that can be multi-valued, but since for the time
772 # being it's only this one, we just single it out
773 sub evaluate_query_params {
774         our $cgi;
776         while (my ($name, $symbol) = each %cgi_param_mapping) {
777                 if ($symbol eq 'opt') {
778                         $input_params{$name} = [ $cgi->param($symbol) ];
779                 } else {
780                         $input_params{$name} = $cgi->param($symbol);
781                 }
782         }
785 # now read PATH_INFO and update the parameter list for missing parameters
786 sub evaluate_path_info {
787         return if defined $input_params{'project'};
788         return if !$path_info;
789         $path_info =~ s,^/+,,;
790         return if !$path_info;
792         # find which part of PATH_INFO is project
793         my $project = $path_info;
794         $project =~ s,/+$,,;
795         while ($project && !check_head_link("$projectroot/$project")) {
796                 $project =~ s,/*[^/]*$,,;
797         }
798         return unless $project;
799         $input_params{'project'} = $project;
801         # do not change any parameters if an action is given using the query string
802         return if $input_params{'action'};
803         $path_info =~ s,^\Q$project\E/*,,;
805         # next, check if we have an action
806         my $action = $path_info;
807         $action =~ s,/.*$,,;
808         if (exists $actions{$action}) {
809                 $path_info =~ s,^$action/*,,;
810                 $input_params{'action'} = $action;
811         }
813         # list of actions that want hash_base instead of hash, but can have no
814         # pathname (f) parameter
815         my @wants_base = (
816                 'tree',
817                 'history',
818         );
820         # we want to catch, among others
821         # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
822         my ($parentrefname, $parentpathname, $refname, $pathname) =
823                 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
825         # first, analyze the 'current' part
826         if (defined $pathname) {
827                 # we got "branch:filename" or "branch:dir/"
828                 # we could use git_get_type(branch:pathname), but:
829                 # - it needs $git_dir
830                 # - it does a git() call
831                 # - the convention of terminating directories with a slash
832                 #   makes it superfluous
833                 # - embedding the action in the PATH_INFO would make it even
834                 #   more superfluous
835                 $pathname =~ s,^/+,,;
836                 if (!$pathname || substr($pathname, -1) eq "/") {
837                         $input_params{'action'} ||= "tree";
838                         $pathname =~ s,/$,,;
839                 } else {
840                         # the default action depends on whether we had parent info
841                         # or not
842                         if ($parentrefname) {
843                                 $input_params{'action'} ||= "blobdiff_plain";
844                         } else {
845                                 $input_params{'action'} ||= "blob_plain";
846                         }
847                 }
848                 $input_params{'hash_base'} ||= $refname;
849                 $input_params{'file_name'} ||= $pathname;
850         } elsif (defined $refname) {
851                 # we got "branch". In this case we have to choose if we have to
852                 # set hash or hash_base.
853                 #
854                 # Most of the actions without a pathname only want hash to be
855                 # set, except for the ones specified in @wants_base that want
856                 # hash_base instead. It should also be noted that hand-crafted
857                 # links having 'history' as an action and no pathname or hash
858                 # set will fail, but that happens regardless of PATH_INFO.
859                 if (defined $parentrefname) {
860                         # if there is parent let the default be 'shortlog' action
861                         # (for http://git.example.com/repo.git/A..B links); if there
862                         # is no parent, dispatch will detect type of object and set
863                         # action appropriately if required (if action is not set)
864                         $input_params{'action'} ||= "shortlog";
865                 }
866                 if ($input_params{'action'} &&
867                     grep { $_ eq $input_params{'action'} } @wants_base) {
868                         $input_params{'hash_base'} ||= $refname;
869                 } else {
870                         $input_params{'hash'} ||= $refname;
871                 }
872         }
874         # next, handle the 'parent' part, if present
875         if (defined $parentrefname) {
876                 # a missing pathspec defaults to the 'current' filename, allowing e.g.
877                 # someproject/blobdiff/oldrev..newrev:/filename
878                 if ($parentpathname) {
879                         $parentpathname =~ s,^/+,,;
880                         $parentpathname =~ s,/$,,;
881                         $input_params{'file_parent'} ||= $parentpathname;
882                 } else {
883                         $input_params{'file_parent'} ||= $input_params{'file_name'};
884                 }
885                 # we assume that hash_parent_base is wanted if a path was specified,
886                 # or if the action wants hash_base instead of hash
887                 if (defined $input_params{'file_parent'} ||
888                         grep { $_ eq $input_params{'action'} } @wants_base) {
889                         $input_params{'hash_parent_base'} ||= $parentrefname;
890                 } else {
891                         $input_params{'hash_parent'} ||= $parentrefname;
892                 }
893         }
895         # for the snapshot action, we allow URLs in the form
896         # $project/snapshot/$hash.ext
897         # where .ext determines the snapshot and gets removed from the
898         # passed $refname to provide the $hash.
899         #
900         # To be able to tell that $refname includes the format extension, we
901         # require the following two conditions to be satisfied:
902         # - the hash input parameter MUST have been set from the $refname part
903         #   of the URL (i.e. they must be equal)
904         # - the snapshot format MUST NOT have been defined already (e.g. from
905         #   CGI parameter sf)
906         # It's also useless to try any matching unless $refname has a dot,
907         # so we check for that too
908         if (defined $input_params{'action'} &&
909                 $input_params{'action'} eq 'snapshot' &&
910                 defined $refname && index($refname, '.') != -1 &&
911                 $refname eq $input_params{'hash'} &&
912                 !defined $input_params{'snapshot_format'}) {
913                 # We loop over the known snapshot formats, checking for
914                 # extensions. Allowed extensions are both the defined suffix
915                 # (which includes the initial dot already) and the snapshot
916                 # format key itself, with a prepended dot
917                 while (my ($fmt, $opt) = each %known_snapshot_formats) {
918                         my $hash = $refname;
919                         unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
920                                 next;
921                         }
922                         my $sfx = $1;
923                         # a valid suffix was found, so set the snapshot format
924                         # and reset the hash parameter
925                         $input_params{'snapshot_format'} = $fmt;
926                         $input_params{'hash'} = $hash;
927                         # we also set the format suffix to the one requested
928                         # in the URL: this way a request for e.g. .tgz returns
929                         # a .tgz instead of a .tar.gz
930                         $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
931                         last;
932                 }
933         }
936 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
937      $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
938      $searchtext, $search_regexp);
939 sub evaluate_and_validate_params {
940         our $action = $input_params{'action'};
941         if (defined $action) {
942                 if (!validate_action($action)) {
943                         die_error(400, "Invalid action parameter");
944                 }
945         }
947         # parameters which are pathnames
948         our $project = $input_params{'project'};
949         if (defined $project) {
950                 if (!validate_project($project)) {
951                         undef $project;
952                         die_error(404, "No such project");
953                 }
954         }
956         our $file_name = $input_params{'file_name'};
957         if (defined $file_name) {
958                 if (!validate_pathname($file_name)) {
959                         die_error(400, "Invalid file parameter");
960                 }
961         }
963         our $file_parent = $input_params{'file_parent'};
964         if (defined $file_parent) {
965                 if (!validate_pathname($file_parent)) {
966                         die_error(400, "Invalid file parent parameter");
967                 }
968         }
970         # parameters which are refnames
971         our $hash = $input_params{'hash'};
972         if (defined $hash) {
973                 if (!validate_refname($hash)) {
974                         die_error(400, "Invalid hash parameter");
975                 }
976         }
978         our $hash_parent = $input_params{'hash_parent'};
979         if (defined $hash_parent) {
980                 if (!validate_refname($hash_parent)) {
981                         die_error(400, "Invalid hash parent parameter");
982                 }
983         }
985         our $hash_base = $input_params{'hash_base'};
986         if (defined $hash_base) {
987                 if (!validate_refname($hash_base)) {
988                         die_error(400, "Invalid hash base parameter");
989                 }
990         }
992         our @extra_options = @{$input_params{'extra_options'}};
993         # @extra_options is always defined, since it can only be (currently) set from
994         # CGI, and $cgi->param() returns the empty array in array context if the param
995         # is not set
996         foreach my $opt (@extra_options) {
997                 if (not exists $allowed_options{$opt}) {
998                         die_error(400, "Invalid option parameter");
999                 }
1000                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1001                         die_error(400, "Invalid option parameter for this action");
1002                 }
1003         }
1005         our $hash_parent_base = $input_params{'hash_parent_base'};
1006         if (defined $hash_parent_base) {
1007                 if (!validate_refname($hash_parent_base)) {
1008                         die_error(400, "Invalid hash parent base parameter");
1009                 }
1010         }
1012         # other parameters
1013         our $page = $input_params{'page'};
1014         if (defined $page) {
1015                 if ($page =~ m/[^0-9]/) {
1016                         die_error(400, "Invalid page parameter");
1017                 }
1018         }
1020         our $searchtype = $input_params{'searchtype'};
1021         if (defined $searchtype) {
1022                 if ($searchtype =~ m/[^a-z]/) {
1023                         die_error(400, "Invalid searchtype parameter");
1024                 }
1025         }
1027         our $search_use_regexp = $input_params{'search_use_regexp'};
1029         our $searchtext = $input_params{'searchtext'};
1030         our $search_regexp;
1031         if (defined $searchtext) {
1032                 if (length($searchtext) < 2) {
1033                         die_error(403, "At least two characters are required for search parameter");
1034                 }
1035                 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
1036         }
1039 # path to the current git repository
1040 our $git_dir;
1041 sub evaluate_git_dir {
1042         our $git_dir = "$projectroot/$project" if $project;
1045 our (@snapshot_fmts, $git_avatar);
1046 sub configure_gitweb_features {
1047         # list of supported snapshot formats
1048         our @snapshot_fmts = gitweb_get_feature('snapshot');
1049         @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1051         # check that the avatar feature is set to a known provider name,
1052         # and for each provider check if the dependencies are satisfied.
1053         # if the provider name is invalid or the dependencies are not met,
1054         # reset $git_avatar to the empty string.
1055         our ($git_avatar) = gitweb_get_feature('avatar');
1056         if ($git_avatar eq 'gravatar') {
1057                 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1058         } elsif ($git_avatar eq 'picon') {
1059                 # no dependencies
1060         } else {
1061                 $git_avatar = '';
1062         }
1065 # custom error handler: 'die <message>' is Internal Server Error
1066 sub handle_errors_html {
1067         my $msg = shift; # it is already HTML escaped
1069         # to avoid infinite loop where error occurs in die_error,
1070         # change handler to default handler, disabling handle_errors_html
1071         set_message("Error occured when inside die_error:\n$msg");
1073         # you cannot jump out of die_error when called as error handler;
1074         # the subroutine set via CGI::Carp::set_message is called _after_
1075         # HTTP headers are already written, so it cannot write them itself
1076         die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1078 set_message(\&handle_errors_html);
1080 # dispatch
1081 sub dispatch {
1082         if (!defined $action) {
1083                 if (defined $hash) {
1084                         $action = git_get_type($hash);
1085                 } elsif (defined $hash_base && defined $file_name) {
1086                         $action = git_get_type("$hash_base:$file_name");
1087                 } elsif (defined $project) {
1088                         $action = 'summary';
1089                 } else {
1090                         $action = 'project_list';
1091                 }
1092         }
1093         if (!defined($actions{$action})) {
1094                 die_error(400, "Unknown action");
1095         }
1096         if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1097             !$project) {
1098                 die_error(400, "Project needed");
1099         }
1100         $actions{$action}->();
1103 sub reset_timer {
1104         our $t0 = [ gettimeofday() ]
1105                 if defined $t0;
1106         our $number_of_git_cmds = 0;
1109 our $first_request = 1;
1110 sub run_request {
1111         reset_timer();
1113         evaluate_uri();
1114         if ($first_request) {
1115                 evaluate_gitweb_config();
1116                 evaluate_git_version();
1117         }
1118         if ($per_request_config) {
1119                 if (ref($per_request_config) eq 'CODE') {
1120                         $per_request_config->();
1121                 } elsif (!$first_request) {
1122                         evaluate_gitweb_config();
1123                 }
1124         }
1125         check_loadavg();
1127         # $projectroot and $projects_list might be set in gitweb config file
1128         $projects_list ||= $projectroot;
1130         evaluate_query_params();
1131         evaluate_path_info();
1132         evaluate_and_validate_params();
1133         evaluate_git_dir();
1135         configure_gitweb_features();
1137         dispatch();
1140 our $is_last_request = sub { 1 };
1141 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1142 our $CGI = 'CGI';
1143 our $cgi;
1144 sub configure_as_fcgi {
1145         require CGI::Fast;
1146         our $CGI = 'CGI::Fast';
1148         my $request_number = 0;
1149         # let each child service 100 requests
1150         our $is_last_request = sub { ++$request_number > 100 };
1152 sub evaluate_argv {
1153         my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1154         configure_as_fcgi()
1155                 if $script_name =~ /\.fcgi$/;
1157         return unless (@ARGV);
1159         require Getopt::Long;
1160         Getopt::Long::GetOptions(
1161                 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1162                 'nproc|n=i' => sub {
1163                         my ($arg, $val) = @_;
1164                         return unless eval { require FCGI::ProcManager; 1; };
1165                         my $proc_manager = FCGI::ProcManager->new({
1166                                 n_processes => $val,
1167                         });
1168                         our $pre_listen_hook    = sub { $proc_manager->pm_manage()        };
1169                         our $pre_dispatch_hook  = sub { $proc_manager->pm_pre_dispatch()  };
1170                         our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1171                 },
1172         );
1175 sub run {
1176         evaluate_argv();
1178         $first_request = 1;
1179         $pre_listen_hook->()
1180                 if $pre_listen_hook;
1182  REQUEST:
1183         while ($cgi = $CGI->new()) {
1184                 $pre_dispatch_hook->()
1185                         if $pre_dispatch_hook;
1187                 run_request();
1189                 $post_dispatch_hook->()
1190                         if $post_dispatch_hook;
1191                 $first_request = 0;
1193                 last REQUEST if ($is_last_request->());
1194         }
1196  DONE_GITWEB:
1197         1;
1200 run();
1202 if (defined caller) {
1203         # wrapped in a subroutine processing requests,
1204         # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1205         return;
1206 } else {
1207         # pure CGI script, serving single request
1208         exit;
1211 ## ======================================================================
1212 ## action links
1214 # possible values of extra options
1215 # -full => 0|1      - use absolute/full URL ($my_uri/$my_url as base)
1216 # -replay => 1      - start from a current view (replay with modifications)
1217 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1218 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1219 sub href {
1220         my %params = @_;
1221         # default is to use -absolute url() i.e. $my_uri
1222         my $href = $params{-full} ? $my_url : $my_uri;
1224         # implicit -replay, must be first of implicit params
1225         $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1227         $params{'project'} = $project unless exists $params{'project'};
1229         if ($params{-replay}) {
1230                 while (my ($name, $symbol) = each %cgi_param_mapping) {
1231                         if (!exists $params{$name}) {
1232                                 $params{$name} = $input_params{$name};
1233                         }
1234                 }
1235         }
1237         my $use_pathinfo = gitweb_check_feature('pathinfo');
1238         if (defined $params{'project'} &&
1239             (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1240                 # try to put as many parameters as possible in PATH_INFO:
1241                 #   - project name
1242                 #   - action
1243                 #   - hash_parent or hash_parent_base:/file_parent
1244                 #   - hash or hash_base:/filename
1245                 #   - the snapshot_format as an appropriate suffix
1247                 # When the script is the root DirectoryIndex for the domain,
1248                 # $href here would be something like http://gitweb.example.com/
1249                 # Thus, we strip any trailing / from $href, to spare us double
1250                 # slashes in the final URL
1251                 $href =~ s,/$,,;
1253                 # Then add the project name, if present
1254                 $href .= "/".esc_path_info($params{'project'});
1255                 delete $params{'project'};
1257                 # since we destructively absorb parameters, we keep this
1258                 # boolean that remembers if we're handling a snapshot
1259                 my $is_snapshot = $params{'action'} eq 'snapshot';
1261                 # Summary just uses the project path URL, any other action is
1262                 # added to the URL
1263                 if (defined $params{'action'}) {
1264                         $href .= "/".esc_path_info($params{'action'})
1265                                 unless $params{'action'} eq 'summary';
1266                         delete $params{'action'};
1267                 }
1269                 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1270                 # stripping nonexistent or useless pieces
1271                 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1272                         || $params{'hash_parent'} || $params{'hash'});
1273                 if (defined $params{'hash_base'}) {
1274                         if (defined $params{'hash_parent_base'}) {
1275                                 $href .= esc_path_info($params{'hash_parent_base'});
1276                                 # skip the file_parent if it's the same as the file_name
1277                                 if (defined $params{'file_parent'}) {
1278                                         if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1279                                                 delete $params{'file_parent'};
1280                                         } elsif ($params{'file_parent'} !~ /\.\./) {
1281                                                 $href .= ":/".esc_path_info($params{'file_parent'});
1282                                                 delete $params{'file_parent'};
1283                                         }
1284                                 }
1285                                 $href .= "..";
1286                                 delete $params{'hash_parent'};
1287                                 delete $params{'hash_parent_base'};
1288                         } elsif (defined $params{'hash_parent'}) {
1289                                 $href .= esc_path_info($params{'hash_parent'}). "..";
1290                                 delete $params{'hash_parent'};
1291                         }
1293                         $href .= esc_path_info($params{'hash_base'});
1294                         if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1295                                 $href .= ":/".esc_path_info($params{'file_name'});
1296                                 delete $params{'file_name'};
1297                         }
1298                         delete $params{'hash'};
1299                         delete $params{'hash_base'};
1300                 } elsif (defined $params{'hash'}) {
1301                         $href .= esc_path_info($params{'hash'});
1302                         delete $params{'hash'};
1303                 }
1305                 # If the action was a snapshot, we can absorb the
1306                 # snapshot_format parameter too
1307                 if ($is_snapshot) {
1308                         my $fmt = $params{'snapshot_format'};
1309                         # snapshot_format should always be defined when href()
1310                         # is called, but just in case some code forgets, we
1311                         # fall back to the default
1312                         $fmt ||= $snapshot_fmts[0];
1313                         $href .= $known_snapshot_formats{$fmt}{'suffix'};
1314                         delete $params{'snapshot_format'};
1315                 }
1316         }
1318         # now encode the parameters explicitly
1319         my @result = ();
1320         for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1321                 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1322                 if (defined $params{$name}) {
1323                         if (ref($params{$name}) eq "ARRAY") {
1324                                 foreach my $par (@{$params{$name}}) {
1325                                         push @result, $symbol . "=" . esc_param($par);
1326                                 }
1327                         } else {
1328                                 push @result, $symbol . "=" . esc_param($params{$name});
1329                         }
1330                 }
1331         }
1332         $href .= "?" . join(';', @result) if scalar @result;
1334         # final transformation: trailing spaces must be escaped (URI-encoded)
1335         $href =~ s/(\s+)$/CGI::escape($1)/e;
1337         if ($params{-anchor}) {
1338                 $href .= "#".esc_param($params{-anchor});
1339         }
1341         return $href;
1345 ## ======================================================================
1346 ## validation, quoting/unquoting and escaping
1348 sub validate_action {
1349         my $input = shift || return undef;
1350         return undef unless exists $actions{$input};
1351         return $input;
1354 sub validate_project {
1355         my $input = shift || return undef;
1356         if (!validate_pathname($input) ||
1357                 !(-d "$projectroot/$input") ||
1358                 !check_export_ok("$projectroot/$input") ||
1359                 ($strict_export && !project_in_list($input))) {
1360                 return undef;
1361         } else {
1362                 return $input;
1363         }
1366 sub validate_pathname {
1367         my $input = shift || return undef;
1369         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1370         # at the beginning, at the end, and between slashes.
1371         # also this catches doubled slashes
1372         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1373                 return undef;
1374         }
1375         # no null characters
1376         if ($input =~ m!\0!) {
1377                 return undef;
1378         }
1379         return $input;
1382 sub validate_refname {
1383         my $input = shift || return undef;
1385         # textual hashes are O.K.
1386         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1387                 return $input;
1388         }
1389         # it must be correct pathname
1390         $input = validate_pathname($input)
1391                 or return undef;
1392         # restrictions on ref name according to git-check-ref-format
1393         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1394                 return undef;
1395         }
1396         return $input;
1399 # decode sequences of octets in utf8 into Perl's internal form,
1400 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
1401 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1402 sub to_utf8 {
1403         my $str = shift;
1404         return undef unless defined $str;
1405         if (utf8::valid($str)) {
1406                 utf8::decode($str);
1407                 return $str;
1408         } else {
1409                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1410         }
1413 # quote unsafe chars, but keep the slash, even when it's not
1414 # correct, but quoted slashes look too horrible in bookmarks
1415 sub esc_param {
1416         my $str = shift;
1417         return undef unless defined $str;
1418         $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1419         $str =~ s/ /\+/g;
1420         return $str;
1423 # the quoting rules for path_info fragment are slightly different
1424 sub esc_path_info {
1425         my $str = shift;
1426         return undef unless defined $str;
1428         # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1429         $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1431         return $str;
1434 # quote unsafe chars in whole URL, so some characters cannot be quoted
1435 sub esc_url {
1436         my $str = shift;
1437         return undef unless defined $str;
1438         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1439         $str =~ s/ /\+/g;
1440         return $str;
1443 # quote unsafe characters in HTML attributes
1444 sub esc_attr {
1446         # for XHTML conformance escaping '"' to '&quot;' is not enough
1447         return esc_html(@_);
1450 # replace invalid utf8 character with SUBSTITUTION sequence
1451 sub esc_html {
1452         my $str = shift;
1453         my %opts = @_;
1455         return undef unless defined $str;
1457         $str = to_utf8($str);
1458         $str = $cgi->escapeHTML($str);
1459         if ($opts{'-nbsp'}) {
1460                 $str =~ s/ /&nbsp;/g;
1461         }
1462         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1463         return $str;
1466 # quote control characters and escape filename to HTML
1467 sub esc_path {
1468         my $str = shift;
1469         my %opts = @_;
1471         return undef unless defined $str;
1473         $str = to_utf8($str);
1474         $str = $cgi->escapeHTML($str);
1475         if ($opts{'-nbsp'}) {
1476                 $str =~ s/ /&nbsp;/g;
1477         }
1478         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1479         return $str;
1482 # Make control characters "printable", using character escape codes (CEC)
1483 sub quot_cec {
1484         my $cntrl = shift;
1485         my %opts = @_;
1486         my %es = ( # character escape codes, aka escape sequences
1487                 "\t" => '\t',   # tab            (HT)
1488                 "\n" => '\n',   # line feed      (LF)
1489                 "\r" => '\r',   # carrige return (CR)
1490                 "\f" => '\f',   # form feed      (FF)
1491                 "\b" => '\b',   # backspace      (BS)
1492                 "\a" => '\a',   # alarm (bell)   (BEL)
1493                 "\e" => '\e',   # escape         (ESC)
1494                 "\013" => '\v', # vertical tab   (VT)
1495                 "\000" => '\0', # nul character  (NUL)
1496         );
1497         my $chr = ( (exists $es{$cntrl})
1498                     ? $es{$cntrl}
1499                     : sprintf('\%2x', ord($cntrl)) );
1500         if ($opts{-nohtml}) {
1501                 return $chr;
1502         } else {
1503                 return "<span class=\"cntrl\">$chr</span>";
1504         }
1507 # Alternatively use unicode control pictures codepoints,
1508 # Unicode "printable representation" (PR)
1509 sub quot_upr {
1510         my $cntrl = shift;
1511         my %opts = @_;
1513         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1514         if ($opts{-nohtml}) {
1515                 return $chr;
1516         } else {
1517                 return "<span class=\"cntrl\">$chr</span>";
1518         }
1521 # git may return quoted and escaped filenames
1522 sub unquote {
1523         my $str = shift;
1525         sub unq {
1526                 my $seq = shift;
1527                 my %es = ( # character escape codes, aka escape sequences
1528                         't' => "\t",   # tab            (HT, TAB)
1529                         'n' => "\n",   # newline        (NL)
1530                         'r' => "\r",   # return         (CR)
1531                         'f' => "\f",   # form feed      (FF)
1532                         'b' => "\b",   # backspace      (BS)
1533                         'a' => "\a",   # alarm (bell)   (BEL)
1534                         'e' => "\e",   # escape         (ESC)
1535                         'v' => "\013", # vertical tab   (VT)
1536                 );
1538                 if ($seq =~ m/^[0-7]{1,3}$/) {
1539                         # octal char sequence
1540                         return chr(oct($seq));
1541                 } elsif (exists $es{$seq}) {
1542                         # C escape sequence, aka character escape code
1543                         return $es{$seq};
1544                 }
1545                 # quoted ordinary character
1546                 return $seq;
1547         }
1549         if ($str =~ m/^"(.*)"$/) {
1550                 # needs unquoting
1551                 $str = $1;
1552                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1553         }
1554         return $str;
1557 # escape tabs (convert tabs to spaces)
1558 sub untabify {
1559         my $line = shift;
1561         while ((my $pos = index($line, "\t")) != -1) {
1562                 if (my $count = (8 - ($pos % 8))) {
1563                         my $spaces = ' ' x $count;
1564                         $line =~ s/\t/$spaces/;
1565                 }
1566         }
1568         return $line;
1571 sub project_in_list {
1572         my $project = shift;
1573         my @list = git_get_projects_list();
1574         return @list && scalar(grep { $_->{'path'} eq $project } @list);
1577 ## ----------------------------------------------------------------------
1578 ## HTML aware string manipulation
1580 # Try to chop given string on a word boundary between position
1581 # $len and $len+$add_len. If there is no word boundary there,
1582 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1583 # (marking chopped part) would be longer than given string.
1584 sub chop_str {
1585         my $str = shift;
1586         my $len = shift;
1587         my $add_len = shift || 10;
1588         my $where = shift || 'right'; # 'left' | 'center' | 'right'
1590         # Make sure perl knows it is utf8 encoded so we don't
1591         # cut in the middle of a utf8 multibyte char.
1592         $str = to_utf8($str);
1594         # allow only $len chars, but don't cut a word if it would fit in $add_len
1595         # if it doesn't fit, cut it if it's still longer than the dots we would add
1596         # remove chopped character entities entirely
1598         # when chopping in the middle, distribute $len into left and right part
1599         # return early if chopping wouldn't make string shorter
1600         if ($where eq 'center') {
1601                 return $str if ($len + 5 >= length($str)); # filler is length 5
1602                 $len = int($len/2);
1603         } else {
1604                 return $str if ($len + 4 >= length($str)); # filler is length 4
1605         }
1607         # regexps: ending and beginning with word part up to $add_len
1608         my $endre = qr/.{$len}\w{0,$add_len}/;
1609         my $begre = qr/\w{0,$add_len}.{$len}/;
1611         if ($where eq 'left') {
1612                 $str =~ m/^(.*?)($begre)$/;
1613                 my ($lead, $body) = ($1, $2);
1614                 if (length($lead) > 4) {
1615                         $lead = " ...";
1616                 }
1617                 return "$lead$body";
1619         } elsif ($where eq 'center') {
1620                 $str =~ m/^($endre)(.*)$/;
1621                 my ($left, $str)  = ($1, $2);
1622                 $str =~ m/^(.*?)($begre)$/;
1623                 my ($mid, $right) = ($1, $2);
1624                 if (length($mid) > 5) {
1625                         $mid = " ... ";
1626                 }
1627                 return "$left$mid$right";
1629         } else {
1630                 $str =~ m/^($endre)(.*)$/;
1631                 my $body = $1;
1632                 my $tail = $2;
1633                 if (length($tail) > 4) {
1634                         $tail = "... ";
1635                 }
1636                 return "$body$tail";
1637         }
1640 # takes the same arguments as chop_str, but also wraps a <span> around the
1641 # result with a title attribute if it does get chopped. Additionally, the
1642 # string is HTML-escaped.
1643 sub chop_and_escape_str {
1644         my ($str) = @_;
1646         my $chopped = chop_str(@_);
1647         if ($chopped eq $str) {
1648                 return esc_html($chopped);
1649         } else {
1650                 $str =~ s/[[:cntrl:]]/?/g;
1651                 return $cgi->span({-title=>$str}, esc_html($chopped));
1652         }
1655 ## ----------------------------------------------------------------------
1656 ## functions returning short strings
1658 # CSS class for given age value (in seconds)
1659 sub age_class {
1660         my $age = shift;
1662         if (!defined $age) {
1663                 return "noage";
1664         } elsif ($age < 60*60*2) {
1665                 return "age0";
1666         } elsif ($age < 60*60*24*2) {
1667                 return "age1";
1668         } else {
1669                 return "age2";
1670         }
1673 # convert age in seconds to "nn units ago" string
1674 sub age_string {
1675         my $age = shift;
1676         my $age_str;
1678         if ($age > 60*60*24*365*2) {
1679                 $age_str = (int $age/60/60/24/365);
1680                 $age_str .= " years ago";
1681         } elsif ($age > 60*60*24*(365/12)*2) {
1682                 $age_str = int $age/60/60/24/(365/12);
1683                 $age_str .= " months ago";
1684         } elsif ($age > 60*60*24*7*2) {
1685                 $age_str = int $age/60/60/24/7;
1686                 $age_str .= " weeks ago";
1687         } elsif ($age > 60*60*24*2) {
1688                 $age_str = int $age/60/60/24;
1689                 $age_str .= " days ago";
1690         } elsif ($age > 60*60*2) {
1691                 $age_str = int $age/60/60;
1692                 $age_str .= " hours ago";
1693         } elsif ($age > 60*2) {
1694                 $age_str = int $age/60;
1695                 $age_str .= " min ago";
1696         } elsif ($age > 2) {
1697                 $age_str = int $age;
1698                 $age_str .= " sec ago";
1699         } else {
1700                 $age_str .= " right now";
1701         }
1702         return $age_str;
1705 use constant {
1706         S_IFINVALID => 0030000,
1707         S_IFGITLINK => 0160000,
1708 };
1710 # submodule/subproject, a commit object reference
1711 sub S_ISGITLINK {
1712         my $mode = shift;
1714         return (($mode & S_IFMT) == S_IFGITLINK)
1717 # convert file mode in octal to symbolic file mode string
1718 sub mode_str {
1719         my $mode = oct shift;
1721         if (S_ISGITLINK($mode)) {
1722                 return 'm---------';
1723         } elsif (S_ISDIR($mode & S_IFMT)) {
1724                 return 'drwxr-xr-x';
1725         } elsif (S_ISLNK($mode)) {
1726                 return 'lrwxrwxrwx';
1727         } elsif (S_ISREG($mode)) {
1728                 # git cares only about the executable bit
1729                 if ($mode & S_IXUSR) {
1730                         return '-rwxr-xr-x';
1731                 } else {
1732                         return '-rw-r--r--';
1733                 };
1734         } else {
1735                 return '----------';
1736         }
1739 # convert file mode in octal to file type string
1740 sub file_type {
1741         my $mode = shift;
1743         if ($mode !~ m/^[0-7]+$/) {
1744                 return $mode;
1745         } else {
1746                 $mode = oct $mode;
1747         }
1749         if (S_ISGITLINK($mode)) {
1750                 return "submodule";
1751         } elsif (S_ISDIR($mode & S_IFMT)) {
1752                 return "directory";
1753         } elsif (S_ISLNK($mode)) {
1754                 return "symlink";
1755         } elsif (S_ISREG($mode)) {
1756                 return "file";
1757         } else {
1758                 return "unknown";
1759         }
1762 # convert file mode in octal to file type description string
1763 sub file_type_long {
1764         my $mode = shift;
1766         if ($mode !~ m/^[0-7]+$/) {
1767                 return $mode;
1768         } else {
1769                 $mode = oct $mode;
1770         }
1772         if (S_ISGITLINK($mode)) {
1773                 return "submodule";
1774         } elsif (S_ISDIR($mode & S_IFMT)) {
1775                 return "directory";
1776         } elsif (S_ISLNK($mode)) {
1777                 return "symlink";
1778         } elsif (S_ISREG($mode)) {
1779                 if ($mode & S_IXUSR) {
1780                         return "executable";
1781                 } else {
1782                         return "file";
1783                 };
1784         } else {
1785                 return "unknown";
1786         }
1790 ## ----------------------------------------------------------------------
1791 ## functions returning short HTML fragments, or transforming HTML fragments
1792 ## which don't belong to other sections
1794 # format line of commit message.
1795 sub format_log_line_html {
1796         my $line = shift;
1798         $line = esc_html($line, -nbsp=>1);
1799         $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1800                 $cgi->a({-href => href(action=>"object", hash=>$1),
1801                                         -class => "text"}, $1);
1802         }eg;
1804         return $line;
1807 # format marker of refs pointing to given object
1809 # the destination action is chosen based on object type and current context:
1810 # - for annotated tags, we choose the tag view unless it's the current view
1811 #   already, in which case we go to shortlog view
1812 # - for other refs, we keep the current view if we're in history, shortlog or
1813 #   log view, and select shortlog otherwise
1814 sub format_ref_marker {
1815         my ($refs, $id) = @_;
1816         my $markers = '';
1818         if (defined $refs->{$id}) {
1819                 foreach my $ref (@{$refs->{$id}}) {
1820                         # this code exploits the fact that non-lightweight tags are the
1821                         # only indirect objects, and that they are the only objects for which
1822                         # we want to use tag instead of shortlog as action
1823                         my ($type, $name) = qw();
1824                         my $indirect = ($ref =~ s/\^\{\}$//);
1825                         # e.g. tags/v2.6.11 or heads/next
1826                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1827                                 $type = $1;
1828                                 $name = $2;
1829                         } else {
1830                                 $type = "ref";
1831                                 $name = $ref;
1832                         }
1834                         my $class = $type;
1835                         $class .= " indirect" if $indirect;
1837                         my $dest_action = "shortlog";
1839                         if ($indirect) {
1840                                 $dest_action = "tag" unless $action eq "tag";
1841                         } elsif ($action =~ /^(history|(short)?log)$/) {
1842                                 $dest_action = $action;
1843                         }
1845                         my $dest = "";
1846                         $dest .= "refs/" unless $ref =~ m!^refs/!;
1847                         $dest .= $ref;
1849                         my $link = $cgi->a({
1850                                 -href => href(
1851                                         action=>$dest_action,
1852                                         hash=>$dest
1853                                 )}, $name);
1855                         $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
1856                                 $link . "</span>";
1857                 }
1858         }
1860         if ($markers) {
1861                 return ' <span class="refs">'. $markers . '</span>';
1862         } else {
1863                 return "";
1864         }
1867 # format, perhaps shortened and with markers, title line
1868 sub format_subject_html {
1869         my ($long, $short, $href, $extra) = @_;
1870         $extra = '' unless defined($extra);
1872         if (length($short) < length($long)) {
1873                 $long =~ s/[[:cntrl:]]/?/g;
1874                 return $cgi->a({-href => $href, -class => "list subject",
1875                                 -title => to_utf8($long)},
1876                        esc_html($short)) . $extra;
1877         } else {
1878                 return $cgi->a({-href => $href, -class => "list subject"},
1879                        esc_html($long)) . $extra;
1880         }
1883 # Rather than recomputing the url for an email multiple times, we cache it
1884 # after the first hit. This gives a visible benefit in views where the avatar
1885 # for the same email is used repeatedly (e.g. shortlog).
1886 # The cache is shared by all avatar engines (currently gravatar only), which
1887 # are free to use it as preferred. Since only one avatar engine is used for any
1888 # given page, there's no risk for cache conflicts.
1889 our %avatar_cache = ();
1891 # Compute the picon url for a given email, by using the picon search service over at
1892 # http://www.cs.indiana.edu/picons/search.html
1893 sub picon_url {
1894         my $email = lc shift;
1895         if (!$avatar_cache{$email}) {
1896                 my ($user, $domain) = split('@', $email);
1897                 $avatar_cache{$email} =
1898                         "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1899                         "$domain/$user/" .
1900                         "users+domains+unknown/up/single";
1901         }
1902         return $avatar_cache{$email};
1905 # Compute the gravatar url for a given email, if it's not in the cache already.
1906 # Gravatar stores only the part of the URL before the size, since that's the
1907 # one computationally more expensive. This also allows reuse of the cache for
1908 # different sizes (for this particular engine).
1909 sub gravatar_url {
1910         my $email = lc shift;
1911         my $size = shift;
1912         $avatar_cache{$email} ||=
1913                 "http://www.gravatar.com/avatar/" .
1914                         Digest::MD5::md5_hex($email) . "?s=";
1915         return $avatar_cache{$email} . $size;
1918 # Insert an avatar for the given $email at the given $size if the feature
1919 # is enabled.
1920 sub git_get_avatar {
1921         my ($email, %opts) = @_;
1922         my $pre_white  = ($opts{-pad_before} ? "&nbsp;" : "");
1923         my $post_white = ($opts{-pad_after}  ? "&nbsp;" : "");
1924         $opts{-size} ||= 'default';
1925         my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1926         my $url = "";
1927         if ($git_avatar eq 'gravatar') {
1928                 $url = gravatar_url($email, $size);
1929         } elsif ($git_avatar eq 'picon') {
1930                 $url = picon_url($email);
1931         }
1932         # Other providers can be added by extending the if chain, defining $url
1933         # as needed. If no variant puts something in $url, we assume avatars
1934         # are completely disabled/unavailable.
1935         if ($url) {
1936                 return $pre_white .
1937                        "<img width=\"$size\" " .
1938                             "class=\"avatar\" " .
1939                             "src=\"".esc_url($url)."\" " .
1940                             "alt=\"\" " .
1941                        "/>" . $post_white;
1942         } else {
1943                 return "";
1944         }
1947 sub format_search_author {
1948         my ($author, $searchtype, $displaytext) = @_;
1949         my $have_search = gitweb_check_feature('search');
1951         if ($have_search) {
1952                 my $performed = "";
1953                 if ($searchtype eq 'author') {
1954                         $performed = "authored";
1955                 } elsif ($searchtype eq 'committer') {
1956                         $performed = "committed";
1957                 }
1959                 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1960                                 searchtext=>$author,
1961                                 searchtype=>$searchtype), class=>"list",
1962                                 title=>"Search for commits $performed by $author"},
1963                                 $displaytext);
1965         } else {
1966                 return $displaytext;
1967         }
1970 # format the author name of the given commit with the given tag
1971 # the author name is chopped and escaped according to the other
1972 # optional parameters (see chop_str).
1973 sub format_author_html {
1974         my $tag = shift;
1975         my $co = shift;
1976         my $author = chop_and_escape_str($co->{'author_name'}, @_);
1977         return "<$tag class=\"author\">" .
1978                format_search_author($co->{'author_name'}, "author",
1979                        git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1980                        $author) .
1981                "</$tag>";
1984 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1985 sub format_git_diff_header_line {
1986         my $line = shift;
1987         my $diffinfo = shift;
1988         my ($from, $to) = @_;
1990         if ($diffinfo->{'nparents'}) {
1991                 # combined diff
1992                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1993                 if ($to->{'href'}) {
1994                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1995                                          esc_path($to->{'file'}));
1996                 } else { # file was deleted (no href)
1997                         $line .= esc_path($to->{'file'});
1998                 }
1999         } else {
2000                 # "ordinary" diff
2001                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2002                 if ($from->{'href'}) {
2003                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2004                                          'a/' . esc_path($from->{'file'}));
2005                 } else { # file was added (no href)
2006                         $line .= 'a/' . esc_path($from->{'file'});
2007                 }
2008                 $line .= ' ';
2009                 if ($to->{'href'}) {
2010                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2011                                          'b/' . esc_path($to->{'file'}));
2012                 } else { # file was deleted
2013                         $line .= 'b/' . esc_path($to->{'file'});
2014                 }
2015         }
2017         return "<div class=\"diff header\">$line</div>\n";
2020 # format extended diff header line, before patch itself
2021 sub format_extended_diff_header_line {
2022         my $line = shift;
2023         my $diffinfo = shift;
2024         my ($from, $to) = @_;
2026         # match <path>
2027         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2028                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2029                                        esc_path($from->{'file'}));
2030         }
2031         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2032                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2033                                  esc_path($to->{'file'}));
2034         }
2035         # match single <mode>
2036         if ($line =~ m/\s(\d{6})$/) {
2037                 $line .= '<span class="info"> (' .
2038                          file_type_long($1) .
2039                          ')</span>';
2040         }
2041         # match <hash>
2042         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2043                 # can match only for combined diff
2044                 $line = 'index ';
2045                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2046                         if ($from->{'href'}[$i]) {
2047                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2048                                                   -class=>"hash"},
2049                                                  substr($diffinfo->{'from_id'}[$i],0,7));
2050                         } else {
2051                                 $line .= '0' x 7;
2052                         }
2053                         # separator
2054                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2055                 }
2056                 $line .= '..';
2057                 if ($to->{'href'}) {
2058                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2059                                          substr($diffinfo->{'to_id'},0,7));
2060                 } else {
2061                         $line .= '0' x 7;
2062                 }
2064         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2065                 # can match only for ordinary diff
2066                 my ($from_link, $to_link);
2067                 if ($from->{'href'}) {
2068                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2069                                              substr($diffinfo->{'from_id'},0,7));
2070                 } else {
2071                         $from_link = '0' x 7;
2072                 }
2073                 if ($to->{'href'}) {
2074                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2075                                            substr($diffinfo->{'to_id'},0,7));
2076                 } else {
2077                         $to_link = '0' x 7;
2078                 }
2079                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2080                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2081         }
2083         return $line . "<br/>\n";
2086 # format from-file/to-file diff header
2087 sub format_diff_from_to_header {
2088         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2089         my $line;
2090         my $result = '';
2092         $line = $from_line;
2093         #assert($line =~ m/^---/) if DEBUG;
2094         # no extra formatting for "^--- /dev/null"
2095         if (! $diffinfo->{'nparents'}) {
2096                 # ordinary (single parent) diff
2097                 if ($line =~ m!^--- "?a/!) {
2098                         if ($from->{'href'}) {
2099                                 $line = '--- a/' .
2100                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2101                                                 esc_path($from->{'file'}));
2102                         } else {
2103                                 $line = '--- a/' .
2104                                         esc_path($from->{'file'});
2105                         }
2106                 }
2107                 $result .= qq!<div class="diff from_file">$line</div>\n!;
2109         } else {
2110                 # combined diff (merge commit)
2111                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2112                         if ($from->{'href'}[$i]) {
2113                                 $line = '--- ' .
2114                                         $cgi->a({-href=>href(action=>"blobdiff",
2115                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
2116                                                              hash_parent_base=>$parents[$i],
2117                                                              file_parent=>$from->{'file'}[$i],
2118                                                              hash=>$diffinfo->{'to_id'},
2119                                                              hash_base=>$hash,
2120                                                              file_name=>$to->{'file'}),
2121                                                  -class=>"path",
2122                                                  -title=>"diff" . ($i+1)},
2123                                                 $i+1) .
2124                                         '/' .
2125                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2126                                                 esc_path($from->{'file'}[$i]));
2127                         } else {
2128                                 $line = '--- /dev/null';
2129                         }
2130                         $result .= qq!<div class="diff from_file">$line</div>\n!;
2131                 }
2132         }
2134         $line = $to_line;
2135         #assert($line =~ m/^\+\+\+/) if DEBUG;
2136         # no extra formatting for "^+++ /dev/null"
2137         if ($line =~ m!^\+\+\+ "?b/!) {
2138                 if ($to->{'href'}) {
2139                         $line = '+++ b/' .
2140                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2141                                         esc_path($to->{'file'}));
2142                 } else {
2143                         $line = '+++ b/' .
2144                                 esc_path($to->{'file'});
2145                 }
2146         }
2147         $result .= qq!<div class="diff to_file">$line</div>\n!;
2149         return $result;
2152 # create note for patch simplified by combined diff
2153 sub format_diff_cc_simplified {
2154         my ($diffinfo, @parents) = @_;
2155         my $result = '';
2157         $result .= "<div class=\"diff header\">" .
2158                    "diff --cc ";
2159         if (!is_deleted($diffinfo)) {
2160                 $result .= $cgi->a({-href => href(action=>"blob",
2161                                                   hash_base=>$hash,
2162                                                   hash=>$diffinfo->{'to_id'},
2163                                                   file_name=>$diffinfo->{'to_file'}),
2164                                     -class => "path"},
2165                                    esc_path($diffinfo->{'to_file'}));
2166         } else {
2167                 $result .= esc_path($diffinfo->{'to_file'});
2168         }
2169         $result .= "</div>\n" . # class="diff header"
2170                    "<div class=\"diff nodifferences\">" .
2171                    "Simple merge" .
2172                    "</div>\n"; # class="diff nodifferences"
2174         return $result;
2177 # format patch (diff) line (not to be used for diff headers)
2178 sub format_diff_line {
2179         my $line = shift;
2180         my ($from, $to) = @_;
2181         my $diff_class = "";
2183         chomp $line;
2185         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2186                 # combined diff
2187                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
2188                 if ($line =~ m/^\@{3}/) {
2189                         $diff_class = " chunk_header";
2190                 } elsif ($line =~ m/^\\/) {
2191                         $diff_class = " incomplete";
2192                 } elsif ($prefix =~ tr/+/+/) {
2193                         $diff_class = " add";
2194                 } elsif ($prefix =~ tr/-/-/) {
2195                         $diff_class = " rem";
2196                 }
2197         } else {
2198                 # assume ordinary diff
2199                 my $char = substr($line, 0, 1);
2200                 if ($char eq '+') {
2201                         $diff_class = " add";
2202                 } elsif ($char eq '-') {
2203                         $diff_class = " rem";
2204                 } elsif ($char eq '@') {
2205                         $diff_class = " chunk_header";
2206                 } elsif ($char eq "\\") {
2207                         $diff_class = " incomplete";
2208                 }
2209         }
2210         $line = untabify($line);
2211         if ($from && $to && $line =~ m/^\@{2} /) {
2212                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2213                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2215                 $from_lines = 0 unless defined $from_lines;
2216                 $to_lines   = 0 unless defined $to_lines;
2218                 if ($from->{'href'}) {
2219                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2220                                              -class=>"list"}, $from_text);
2221                 }
2222                 if ($to->{'href'}) {
2223                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2224                                              -class=>"list"}, $to_text);
2225                 }
2226                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2227                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2228                 return "<div class=\"diff$diff_class\">$line</div>\n";
2229         } elsif ($from && $to && $line =~ m/^\@{3}/) {
2230                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2231                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2233                 @from_text = split(' ', $ranges);
2234                 for (my $i = 0; $i < @from_text; ++$i) {
2235                         ($from_start[$i], $from_nlines[$i]) =
2236                                 (split(',', substr($from_text[$i], 1)), 0);
2237                 }
2239                 $to_text   = pop @from_text;
2240                 $to_start  = pop @from_start;
2241                 $to_nlines = pop @from_nlines;
2243                 $line = "<span class=\"chunk_info\">$prefix ";
2244                 for (my $i = 0; $i < @from_text; ++$i) {
2245                         if ($from->{'href'}[$i]) {
2246                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2247                                                   -class=>"list"}, $from_text[$i]);
2248                         } else {
2249                                 $line .= $from_text[$i];
2250                         }
2251                         $line .= " ";
2252                 }
2253                 if ($to->{'href'}) {
2254                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2255                                           -class=>"list"}, $to_text);
2256                 } else {
2257                         $line .= $to_text;
2258                 }
2259                 $line .= " $prefix</span>" .
2260                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2261                 return "<div class=\"diff$diff_class\">$line</div>\n";
2262         }
2263         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
2266 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2267 # linked.  Pass the hash of the tree/commit to snapshot.
2268 sub format_snapshot_links {
2269         my ($hash) = @_;
2270         my $num_fmts = @snapshot_fmts;
2271         if ($num_fmts > 1) {
2272                 # A parenthesized list of links bearing format names.
2273                 # e.g. "snapshot (_tar.gz_ _zip_)"
2274                 return "snapshot (" . join(' ', map
2275                         $cgi->a({
2276                                 -href => href(
2277                                         action=>"snapshot",
2278                                         hash=>$hash,
2279                                         snapshot_format=>$_
2280                                 )
2281                         }, $known_snapshot_formats{$_}{'display'})
2282                 , @snapshot_fmts) . ")";
2283         } elsif ($num_fmts == 1) {
2284                 # A single "snapshot" link whose tooltip bears the format name.
2285                 # i.e. "_snapshot_"
2286                 my ($fmt) = @snapshot_fmts;
2287                 return
2288                         $cgi->a({
2289                                 -href => href(
2290                                         action=>"snapshot",
2291                                         hash=>$hash,
2292                                         snapshot_format=>$fmt
2293                                 ),
2294                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2295                         }, "snapshot");
2296         } else { # $num_fmts == 0
2297                 return undef;
2298         }
2301 ## ......................................................................
2302 ## functions returning values to be passed, perhaps after some
2303 ## transformation, to other functions; e.g. returning arguments to href()
2305 # returns hash to be passed to href to generate gitweb URL
2306 # in -title key it returns description of link
2307 sub get_feed_info {
2308         my $format = shift || 'Atom';
2309         my %res = (action => lc($format));
2311         # feed links are possible only for project views
2312         return unless (defined $project);
2313         # some views should link to OPML, or to generic project feed,
2314         # or don't have specific feed yet (so they should use generic)
2315         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
2317         my $branch;
2318         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2319         # from tag links; this also makes possible to detect branch links
2320         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2321             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
2322                 $branch = $1;
2323         }
2324         # find log type for feed description (title)
2325         my $type = 'log';
2326         if (defined $file_name) {
2327                 $type  = "history of $file_name";
2328                 $type .= "/" if ($action eq 'tree');
2329                 $type .= " on '$branch'" if (defined $branch);
2330         } else {
2331                 $type = "log of $branch" if (defined $branch);
2332         }
2334         $res{-title} = $type;
2335         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2336         $res{'file_name'} = $file_name;
2338         return %res;
2341 ## ----------------------------------------------------------------------
2342 ## git utility subroutines, invoking git commands
2344 # returns path to the core git executable and the --git-dir parameter as list
2345 sub git_cmd {
2346         $number_of_git_cmds++;
2347         return $GIT, '--git-dir='.$git_dir;
2350 # quote the given arguments for passing them to the shell
2351 # quote_command("command", "arg 1", "arg with ' and ! characters")
2352 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2353 # Try to avoid using this function wherever possible.
2354 sub quote_command {
2355         return join(' ',
2356                 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2359 # get HEAD ref of given project as hash
2360 sub git_get_head_hash {
2361         return git_get_full_hash(shift, 'HEAD');
2364 sub git_get_full_hash {
2365         return git_get_hash(@_);
2368 sub git_get_short_hash {
2369         return git_get_hash(@_, '--short=7');
2372 sub git_get_hash {
2373         my ($project, $hash, @options) = @_;
2374         my $o_git_dir = $git_dir;
2375         my $retval = undef;
2376         $git_dir = "$projectroot/$project";
2377         if (open my $fd, '-|', git_cmd(), 'rev-parse',
2378             '--verify', '-q', @options, $hash) {
2379                 $retval = <$fd>;
2380                 chomp $retval if defined $retval;
2381                 close $fd;
2382         }
2383         if (defined $o_git_dir) {
2384                 $git_dir = $o_git_dir;
2385         }
2386         return $retval;
2389 # get type of given object
2390 sub git_get_type {
2391         my $hash = shift;
2393         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2394         my $type = <$fd>;
2395         close $fd or return;
2396         chomp $type;
2397         return $type;
2400 # repository configuration
2401 our $config_file = '';
2402 our %config;
2404 # store multiple values for single key as anonymous array reference
2405 # single values stored directly in the hash, not as [ <value> ]
2406 sub hash_set_multi {
2407         my ($hash, $key, $value) = @_;
2409         if (!exists $hash->{$key}) {
2410                 $hash->{$key} = $value;
2411         } elsif (!ref $hash->{$key}) {
2412                 $hash->{$key} = [ $hash->{$key}, $value ];
2413         } else {
2414                 push @{$hash->{$key}}, $value;
2415         }
2418 # return hash of git project configuration
2419 # optionally limited to some section, e.g. 'gitweb'
2420 sub git_parse_project_config {
2421         my $section_regexp = shift;
2422         my %config;
2424         local $/ = "\0";
2426         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2427                 or return;
2429         while (my $keyval = <$fh>) {
2430                 chomp $keyval;
2431                 my ($key, $value) = split(/\n/, $keyval, 2);
2433                 hash_set_multi(\%config, $key, $value)
2434                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2435         }
2436         close $fh;
2438         return %config;
2441 # convert config value to boolean: 'true' or 'false'
2442 # no value, number > 0, 'true' and 'yes' values are true
2443 # rest of values are treated as false (never as error)
2444 sub config_to_bool {
2445         my $val = shift;
2447         return 1 if !defined $val;             # section.key
2449         # strip leading and trailing whitespace
2450         $val =~ s/^\s+//;
2451         $val =~ s/\s+$//;
2453         return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
2454                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
2457 # convert config value to simple decimal number
2458 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2459 # to be multiplied by 1024, 1048576, or 1073741824
2460 sub config_to_int {
2461         my $val = shift;
2463         # strip leading and trailing whitespace
2464         $val =~ s/^\s+//;
2465         $val =~ s/\s+$//;
2467         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2468                 $unit = lc($unit);
2469                 # unknown unit is treated as 1
2470                 return $num * ($unit eq 'g' ? 1073741824 :
2471                                $unit eq 'm' ?    1048576 :
2472                                $unit eq 'k' ?       1024 : 1);
2473         }
2474         return $val;
2477 # convert config value to array reference, if needed
2478 sub config_to_multi {
2479         my $val = shift;
2481         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2484 sub git_get_project_config {
2485         my ($key, $type) = @_;
2487         return unless defined $git_dir;
2489         # key sanity check
2490         return unless ($key);
2491         $key =~ s/^gitweb\.//;
2492         return if ($key =~ m/\W/);
2494         # type sanity check
2495         if (defined $type) {
2496                 $type =~ s/^--//;
2497                 $type = undef
2498                         unless ($type eq 'bool' || $type eq 'int');
2499         }
2501         # get config
2502         if (!defined $config_file ||
2503             $config_file ne "$git_dir/config") {
2504                 %config = git_parse_project_config('gitweb');
2505                 $config_file = "$git_dir/config";
2506         }
2508         # check if config variable (key) exists
2509         return unless exists $config{"gitweb.$key"};
2511         # ensure given type
2512         if (!defined $type) {
2513                 return $config{"gitweb.$key"};
2514         } elsif ($type eq 'bool') {
2515                 # backward compatibility: 'git config --bool' returns true/false
2516                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2517         } elsif ($type eq 'int') {
2518                 return config_to_int($config{"gitweb.$key"});
2519         }
2520         return $config{"gitweb.$key"};
2523 # get hash of given path at given ref
2524 sub git_get_hash_by_path {
2525         my $base = shift;
2526         my $path = shift || return undef;
2527         my $type = shift;
2529         $path =~ s,/+$,,;
2531         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2532                 or die_error(500, "Open git-ls-tree failed");
2533         my $line = <$fd>;
2534         close $fd or return undef;
2536         if (!defined $line) {
2537                 # there is no tree or hash given by $path at $base
2538                 return undef;
2539         }
2541         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2542         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2543         if (defined $type && $type ne $2) {
2544                 # type doesn't match
2545                 return undef;
2546         }
2547         return $3;
2550 # get path of entry with given hash at given tree-ish (ref)
2551 # used to get 'from' filename for combined diff (merge commit) for renames
2552 sub git_get_path_by_hash {
2553         my $base = shift || return;
2554         my $hash = shift || return;
2556         local $/ = "\0";
2558         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2559                 or return undef;
2560         while (my $line = <$fd>) {
2561                 chomp $line;
2563                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2564                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2565                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2566                         close $fd;
2567                         return $1;
2568                 }
2569         }
2570         close $fd;
2571         return undef;
2574 ## ......................................................................
2575 ## git utility functions, directly accessing git repository
2577 sub git_get_project_description {
2578         my $path = shift;
2580         $git_dir = "$projectroot/$path";
2581         open my $fd, '<', "$git_dir/description"
2582                 or return git_get_project_config('description');
2583         my $descr = <$fd>;
2584         close $fd;
2585         if (defined $descr) {
2586                 chomp $descr;
2587         }
2588         return $descr;
2591 # supported formats:
2592 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2593 #   - if its contents is a number, use it as tag weight,
2594 #   - otherwise add a tag with weight 1
2595 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2596 #   the same value multiple times increases tag weight
2597 # * `gitweb.ctag' multi-valued repo config variable
2598 sub git_get_project_ctags {
2599         my $project = shift;
2600         my $ctags = {};
2602         $git_dir = "$projectroot/$project";
2603         if (opendir my $dh, "$git_dir/ctags") {
2604                 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2605                 foreach my $tagfile (@files) {
2606                         open my $ct, '<', $tagfile
2607                                 or next;
2608                         my $val = <$ct>;
2609                         chomp $val if $val;
2610                         close $ct;
2612                         (my $ctag = $tagfile) =~ s#.*/##;
2613                         if ($val =~ /\d+/) {
2614                                 $ctags->{$ctag} = $val;
2615                         } else {
2616                                 $ctags->{$ctag} = 1;
2617                         }
2618                 }
2619                 closedir $dh;
2621         } elsif (open my $fh, '<', "$git_dir/ctags") {
2622                 while (my $line = <$fh>) {
2623                         chomp $line;
2624                         $ctags->{$line}++ if $line;
2625                 }
2626                 close $fh;
2628         } else {
2629                 my $taglist = config_to_multi(git_get_project_config('ctag'));
2630                 foreach my $tag (@$taglist) {
2631                         $ctags->{$tag}++;
2632                 }
2633         }
2635         return $ctags;
2638 # return hash, where keys are content tags ('ctags'),
2639 # and values are sum of weights of given tag in every project
2640 sub git_gather_all_ctags {
2641         my $projects = shift;
2642         my $ctags = {};
2644         foreach my $p (@$projects) {
2645                 foreach my $ct (keys %{$p->{'ctags'}}) {
2646                         $ctags->{$ct} += $p->{'ctags'}->{$ct};
2647                 }
2648         }
2650         return $ctags;
2653 sub git_populate_project_tagcloud {
2654         my $ctags = shift;
2656         # First, merge different-cased tags; tags vote on casing
2657         my %ctags_lc;
2658         foreach (keys %$ctags) {
2659                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2660                 if (not $ctags_lc{lc $_}->{topcount}
2661                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2662                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2663                         $ctags_lc{lc $_}->{topname} = $_;
2664                 }
2665         }
2667         my $cloud;
2668         my $matched = $cgi->param('by_tag');
2669         if (eval { require HTML::TagCloud; 1; }) {
2670                 $cloud = HTML::TagCloud->new;
2671                 foreach my $ctag (sort keys %ctags_lc) {
2672                         # Pad the title with spaces so that the cloud looks
2673                         # less crammed.
2674                         my $title = esc_html($ctags_lc{$ctag}->{topname});
2675                         $title =~ s/ /&nbsp;/g;
2676                         $title =~ s/^/&nbsp;/g;
2677                         $title =~ s/$/&nbsp;/g;
2678                         if (defined $matched && $matched eq $ctag) {
2679                                 $title = qq(<span class="match">$title</span>);
2680                         }
2681                         $cloud->add($title, href(project=>undef, ctag=>$ctag),
2682                                     $ctags_lc{$ctag}->{count});
2683                 }
2684         } else {
2685                 $cloud = {};
2686                 foreach my $ctag (keys %ctags_lc) {
2687                         my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
2688                         if (defined $matched && $matched eq $ctag) {
2689                                 $title = qq(<span class="match">$title</span>);
2690                         }
2691                         $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2692                         $cloud->{$ctag}{ctag} =
2693                                 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
2694                 }
2695         }
2696         return $cloud;
2699 sub git_show_project_tagcloud {
2700         my ($cloud, $count) = @_;
2701         if (ref $cloud eq 'HTML::TagCloud') {
2702                 return $cloud->html_and_css($count);
2703         } else {
2704                 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2705                 return
2706                         '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2707                         join (', ', map {
2708                                 $cloud->{$_}->{'ctag'}
2709                         } splice(@tags, 0, $count)) .
2710                         '</div>';
2711         }
2714 sub git_get_project_url_list {
2715         my $path = shift;
2717         $git_dir = "$projectroot/$path";
2718         open my $fd, '<', "$git_dir/cloneurl"
2719                 or return wantarray ?
2720                 @{ config_to_multi(git_get_project_config('url')) } :
2721                    config_to_multi(git_get_project_config('url'));
2722         my @git_project_url_list = map { chomp; $_ } <$fd>;
2723         close $fd;
2725         return wantarray ? @git_project_url_list : \@git_project_url_list;
2728 sub git_get_projects_list {
2729         my $filter = shift || '';
2730         my @list;
2732         $filter =~ s/\.git$//;
2734         if (-d $projects_list) {
2735                 # search in directory
2736                 my $dir = $projects_list;
2737                 # remove the trailing "/"
2738                 $dir =~ s!/+$!!;
2739                 my $pfxlen = length("$projects_list");
2740                 my $pfxdepth = ($projects_list =~ tr!/!!);
2741                 # when filtering, search only given subdirectory
2742                 if ($filter) {
2743                         $dir .= "/$filter";
2744                         $dir =~ s!/+$!!;
2745                 }
2747                 File::Find::find({
2748                         follow_fast => 1, # follow symbolic links
2749                         follow_skip => 2, # ignore duplicates
2750                         dangling_symlinks => 0, # ignore dangling symlinks, silently
2751                         wanted => sub {
2752                                 # global variables
2753                                 our $project_maxdepth;
2754                                 our $projectroot;
2755                                 # skip project-list toplevel, if we get it.
2756                                 return if (m!^[/.]$!);
2757                                 # only directories can be git repositories
2758                                 return unless (-d $_);
2759                                 # don't traverse too deep (Find is super slow on os x)
2760                                 # $project_maxdepth excludes depth of $projectroot
2761                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2762                                         $File::Find::prune = 1;
2763                                         return;
2764                                 }
2766                                 my $path = substr($File::Find::name, $pfxlen + 1);
2767                                 # we check related file in $projectroot
2768                                 if (check_export_ok("$projectroot/$path")) {
2769                                         push @list, { path => $path };
2770                                         $File::Find::prune = 1;
2771                                 }
2772                         },
2773                 }, "$dir");
2775         } elsif (-f $projects_list) {
2776                 # read from file(url-encoded):
2777                 # 'git%2Fgit.git Linus+Torvalds'
2778                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2779                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2780                 open my $fd, '<', $projects_list or return;
2781         PROJECT:
2782                 while (my $line = <$fd>) {
2783                         chomp $line;
2784                         my ($path, $owner) = split ' ', $line;
2785                         $path = unescape($path);
2786                         $owner = unescape($owner);
2787                         if (!defined $path) {
2788                                 next;
2789                         }
2790                         # if $filter is rpovided, check if $path begins with $filter
2791                         if ($filter && $path !~ m!^\Q$filter\E/!) {
2792                                 next;
2793                         }
2794                         if (check_export_ok("$projectroot/$path")) {
2795                                 my $pr = {
2796                                         path => $path,
2797                                         owner => to_utf8($owner),
2798                                 };
2799                                 push @list, $pr;
2800                         }
2801                 }
2802                 close $fd;
2803         }
2804         return @list;
2807 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
2808 # as side effects it sets 'forks' field to list of forks for forked projects
2809 sub filter_forks_from_projects_list {
2810         my $projects = shift;
2812         my %trie; # prefix tree of directories (path components)
2813         # generate trie out of those directories that might contain forks
2814         foreach my $pr (@$projects) {
2815                 my $path = $pr->{'path'};
2816                 $path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
2817                 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
2818                 next unless ($path);      # skip '.git' repository: tests, git-instaweb
2819                 next unless (-d $path);   # containing directory exists
2820                 $pr->{'forks'} = [];      # there can be 0 or more forks of project
2822                 # add to trie
2823                 my @dirs = split('/', $path);
2824                 # walk the trie, until either runs out of components or out of trie
2825                 my $ref = \%trie;
2826                 while (scalar @dirs &&
2827                        exists($ref->{$dirs[0]})) {
2828                         $ref = $ref->{shift @dirs};
2829                 }
2830                 # create rest of trie structure from rest of components
2831                 foreach my $dir (@dirs) {
2832                         $ref = $ref->{$dir} = {};
2833                 }
2834                 # create end marker, store $pr as a data
2835                 $ref->{''} = $pr if (!exists $ref->{''});
2836         }
2838         # filter out forks, by finding shortest prefix match for paths
2839         my @filtered;
2840  PROJECT:
2841         foreach my $pr (@$projects) {
2842                 # trie lookup
2843                 my $ref = \%trie;
2844         DIR:
2845                 foreach my $dir (split('/', $pr->{'path'})) {
2846                         if (exists $ref->{''}) {
2847                                 # found [shortest] prefix, is a fork - skip it
2848                                 push @{$ref->{''}{'forks'}}, $pr;
2849                                 next PROJECT;
2850                         }
2851                         if (!exists $ref->{$dir}) {
2852                                 # not in trie, cannot have prefix, not a fork
2853                                 push @filtered, $pr;
2854                                 next PROJECT;
2855                         }
2856                         # If the dir is there, we just walk one step down the trie.
2857                         $ref = $ref->{$dir};
2858                 }
2859                 # we ran out of trie
2860                 # (shouldn't happen: it's either no match, or end marker)
2861                 push @filtered, $pr;
2862         }
2864         return @filtered;
2867 # note: fill_project_list_info must be run first,
2868 # for 'descr_long' and 'ctags' to be filled
2869 sub search_projects_list {
2870         my ($projlist, %opts) = @_;
2871         my $tagfilter  = $opts{'tagfilter'};
2872         my $searchtext = $opts{'searchtext'};
2874         return @$projlist
2875                 unless ($tagfilter || $searchtext);
2877         my @projects;
2878  PROJECT:
2879         foreach my $pr (@$projlist) {
2881                 if ($tagfilter) {
2882                         next unless ref($pr->{'ctags'}) eq 'HASH';
2883                         next unless
2884                                 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
2885                 }
2887                 if ($searchtext) {
2888                         next unless
2889                                 $pr->{'path'} =~ /$searchtext/ ||
2890                                 $pr->{'descr_long'} =~ /$searchtext/;
2891                 }
2893                 push @projects, $pr;
2894         }
2896         return @projects;
2899 our $gitweb_project_owner = undef;
2900 sub git_get_project_list_from_file {
2902         return if (defined $gitweb_project_owner);
2904         $gitweb_project_owner = {};
2905         # read from file (url-encoded):
2906         # 'git%2Fgit.git Linus+Torvalds'
2907         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2908         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2909         if (-f $projects_list) {
2910                 open(my $fd, '<', $projects_list);
2911                 while (my $line = <$fd>) {
2912                         chomp $line;
2913                         my ($pr, $ow) = split ' ', $line;
2914                         $pr = unescape($pr);
2915                         $ow = unescape($ow);
2916                         $gitweb_project_owner->{$pr} = to_utf8($ow);
2917                 }
2918                 close $fd;
2919         }
2922 sub git_get_project_owner {
2923         my $project = shift;
2924         my $owner;
2926         return undef unless $project;
2927         $git_dir = "$projectroot/$project";
2929         if (!defined $gitweb_project_owner) {
2930                 git_get_project_list_from_file();
2931         }
2933         if (exists $gitweb_project_owner->{$project}) {
2934                 $owner = $gitweb_project_owner->{$project};
2935         }
2936         if (!defined $owner){
2937                 $owner = git_get_project_config('owner');
2938         }
2939         if (!defined $owner) {
2940                 $owner = get_file_owner("$git_dir");
2941         }
2943         return $owner;
2946 sub git_get_last_activity {
2947         my ($path) = @_;
2948         my $fd;
2950         $git_dir = "$projectroot/$path";
2951         open($fd, "-|", git_cmd(), 'for-each-ref',
2952              '--format=%(committer)',
2953              '--sort=-committerdate',
2954              '--count=1',
2955              'refs/heads') or return;
2956         my $most_recent = <$fd>;
2957         close $fd or return;
2958         if (defined $most_recent &&
2959             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2960                 my $timestamp = $1;
2961                 my $age = time - $timestamp;
2962                 return ($age, age_string($age));
2963         }
2964         return (undef, undef);
2967 # Implementation note: when a single remote is wanted, we cannot use 'git
2968 # remote show -n' because that command always work (assuming it's a remote URL
2969 # if it's not defined), and we cannot use 'git remote show' because that would
2970 # try to make a network roundtrip. So the only way to find if that particular
2971 # remote is defined is to walk the list provided by 'git remote -v' and stop if
2972 # and when we find what we want.
2973 sub git_get_remotes_list {
2974         my $wanted = shift;
2975         my %remotes = ();
2977         open my $fd, '-|' , git_cmd(), 'remote', '-v';
2978         return unless $fd;
2979         while (my $remote = <$fd>) {
2980                 chomp $remote;
2981                 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
2982                 next if $wanted and not $remote eq $wanted;
2983                 my ($url, $key) = ($1, $2);
2985                 $remotes{$remote} ||= { 'heads' => () };
2986                 $remotes{$remote}{$key} = $url;
2987         }
2988         close $fd or return;
2989         return wantarray ? %remotes : \%remotes;
2992 # Takes a hash of remotes as first parameter and fills it by adding the
2993 # available remote heads for each of the indicated remotes.
2994 sub fill_remote_heads {
2995         my $remotes = shift;
2996         my @heads = map { "remotes/$_" } keys %$remotes;
2997         my @remoteheads = git_get_heads_list(undef, @heads);
2998         foreach my $remote (keys %$remotes) {
2999                 $remotes->{$remote}{'heads'} = [ grep {
3000                         $_->{'name'} =~ s!^$remote/!!
3001                         } @remoteheads ];
3002         }
3005 sub git_get_references {
3006         my $type = shift || "";
3007         my %refs;
3008         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3009         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3010         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3011                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
3012                 or return;
3014         while (my $line = <$fd>) {
3015                 chomp $line;
3016                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3017                         if (defined $refs{$1}) {
3018                                 push @{$refs{$1}}, $2;
3019                         } else {
3020                                 $refs{$1} = [ $2 ];
3021                         }
3022                 }
3023         }
3024         close $fd or return;
3025         return \%refs;
3028 sub git_get_rev_name_tags {
3029         my $hash = shift || return undef;
3031         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3032                 or return;
3033         my $name_rev = <$fd>;
3034         close $fd;
3036         if ($name_rev =~ m|^$hash tags/(.*)$|) {
3037                 return $1;
3038         } else {
3039                 # catches also '$hash undefined' output
3040                 return undef;
3041         }
3044 ## ----------------------------------------------------------------------
3045 ## parse to hash functions
3047 sub parse_date {
3048         my $epoch = shift;
3049         my $tz = shift || "-0000";
3051         my %date;
3052         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3053         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3054         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3055         $date{'hour'} = $hour;
3056         $date{'minute'} = $min;
3057         $date{'mday'} = $mday;
3058         $date{'day'} = $days[$wday];
3059         $date{'month'} = $months[$mon];
3060         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3061                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3062         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3063                              $mday, $months[$mon], $hour ,$min;
3064         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3065                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3067         my ($tz_sign, $tz_hour, $tz_min) =
3068                 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3069         $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3070         my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3071         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3072         $date{'hour_local'} = $hour;
3073         $date{'minute_local'} = $min;
3074         $date{'tz_local'} = $tz;
3075         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3076                                   1900+$year, $mon+1, $mday,
3077                                   $hour, $min, $sec, $tz);
3078         return %date;
3081 sub parse_tag {
3082         my $tag_id = shift;
3083         my %tag;
3084         my @comment;
3086         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3087         $tag{'id'} = $tag_id;
3088         while (my $line = <$fd>) {
3089                 chomp $line;
3090                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3091                         $tag{'object'} = $1;
3092                 } elsif ($line =~ m/^type (.+)$/) {
3093                         $tag{'type'} = $1;
3094                 } elsif ($line =~ m/^tag (.+)$/) {
3095                         $tag{'name'} = $1;
3096                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3097                         $tag{'author'} = $1;
3098                         $tag{'author_epoch'} = $2;
3099                         $tag{'author_tz'} = $3;
3100                         if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3101                                 $tag{'author_name'}  = $1;
3102                                 $tag{'author_email'} = $2;
3103                         } else {
3104                                 $tag{'author_name'} = $tag{'author'};
3105                         }
3106                 } elsif ($line =~ m/--BEGIN/) {
3107                         push @comment, $line;
3108                         last;
3109                 } elsif ($line eq "") {
3110                         last;
3111                 }
3112         }
3113         push @comment, <$fd>;
3114         $tag{'comment'} = \@comment;
3115         close $fd or return;
3116         if (!defined $tag{'name'}) {
3117                 return
3118         };
3119         return %tag
3122 sub parse_commit_text {
3123         my ($commit_text, $withparents) = @_;
3124         my @commit_lines = split '\n', $commit_text;
3125         my %co;
3127         pop @commit_lines; # Remove '\0'
3129         if (! @commit_lines) {
3130                 return;
3131         }
3133         my $header = shift @commit_lines;
3134         if ($header !~ m/^[0-9a-fA-F]{40}/) {
3135                 return;
3136         }
3137         ($co{'id'}, my @parents) = split ' ', $header;
3138         while (my $line = shift @commit_lines) {
3139                 last if $line eq "\n";
3140                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3141                         $co{'tree'} = $1;
3142                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3143                         push @parents, $1;
3144                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3145                         $co{'author'} = to_utf8($1);
3146                         $co{'author_epoch'} = $2;
3147                         $co{'author_tz'} = $3;
3148                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3149                                 $co{'author_name'}  = $1;
3150                                 $co{'author_email'} = $2;
3151                         } else {
3152                                 $co{'author_name'} = $co{'author'};
3153                         }
3154                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3155                         $co{'committer'} = to_utf8($1);
3156                         $co{'committer_epoch'} = $2;
3157                         $co{'committer_tz'} = $3;
3158                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3159                                 $co{'committer_name'}  = $1;
3160                                 $co{'committer_email'} = $2;
3161                         } else {
3162                                 $co{'committer_name'} = $co{'committer'};
3163                         }
3164                 }
3165         }
3166         if (!defined $co{'tree'}) {
3167                 return;
3168         };
3169         $co{'parents'} = \@parents;
3170         $co{'parent'} = $parents[0];
3172         foreach my $title (@commit_lines) {
3173                 $title =~ s/^    //;
3174                 if ($title ne "") {
3175                         $co{'title'} = chop_str($title, 80, 5);
3176                         # remove leading stuff of merges to make the interesting part visible
3177                         if (length($title) > 50) {
3178                                 $title =~ s/^Automatic //;
3179                                 $title =~ s/^merge (of|with) /Merge ... /i;
3180                                 if (length($title) > 50) {
3181                                         $title =~ s/(http|rsync):\/\///;
3182                                 }
3183                                 if (length($title) > 50) {
3184                                         $title =~ s/(master|www|rsync)\.//;
3185                                 }
3186                                 if (length($title) > 50) {
3187                                         $title =~ s/kernel.org:?//;
3188                                 }
3189                                 if (length($title) > 50) {
3190                                         $title =~ s/\/pub\/scm//;
3191                                 }
3192                         }
3193                         $co{'title_short'} = chop_str($title, 50, 5);
3194                         last;
3195                 }
3196         }
3197         if (! defined $co{'title'} || $co{'title'} eq "") {
3198                 $co{'title'} = $co{'title_short'} = '(no commit message)';
3199         }
3200         # remove added spaces
3201         foreach my $line (@commit_lines) {
3202                 $line =~ s/^    //;
3203         }
3204         $co{'comment'} = \@commit_lines;
3206         my $age = time - $co{'committer_epoch'};
3207         $co{'age'} = $age;
3208         $co{'age_string'} = age_string($age);
3209         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3210         if ($age > 60*60*24*7*2) {
3211                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3212                 $co{'age_string_age'} = $co{'age_string'};
3213         } else {
3214                 $co{'age_string_date'} = $co{'age_string'};
3215                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3216         }
3217         return %co;
3220 sub parse_commit {
3221         my ($commit_id) = @_;
3222         my %co;
3224         local $/ = "\0";
3226         open my $fd, "-|", git_cmd(), "rev-list",
3227                 "--parents",
3228                 "--header",
3229                 "--max-count=1",
3230                 $commit_id,
3231                 "--",
3232                 or die_error(500, "Open git-rev-list failed");
3233         %co = parse_commit_text(<$fd>, 1);
3234         close $fd;
3236         return %co;
3239 sub parse_commits {
3240         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3241         my @cos;
3243         $maxcount ||= 1;
3244         $skip ||= 0;
3246         local $/ = "\0";
3248         open my $fd, "-|", git_cmd(), "rev-list",
3249                 "--header",
3250                 @args,
3251                 ("--max-count=" . $maxcount),
3252                 ("--skip=" . $skip),
3253                 @extra_options,
3254                 $commit_id,
3255                 "--",
3256                 ($filename ? ($filename) : ())
3257                 or die_error(500, "Open git-rev-list failed");
3258         while (my $line = <$fd>) {
3259                 my %co = parse_commit_text($line);
3260                 push @cos, \%co;
3261         }
3262         close $fd;
3264         return wantarray ? @cos : \@cos;
3267 # parse line of git-diff-tree "raw" output
3268 sub parse_difftree_raw_line {
3269         my $line = shift;
3270         my %res;
3272         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
3273         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
3274         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3275                 $res{'from_mode'} = $1;
3276                 $res{'to_mode'} = $2;
3277                 $res{'from_id'} = $3;
3278                 $res{'to_id'} = $4;
3279                 $res{'status'} = $5;
3280                 $res{'similarity'} = $6;
3281                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3282                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3283                 } else {
3284                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3285                 }
3286         }
3287         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3288         # combined diff (for merge commit)
3289         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3290                 $res{'nparents'}  = length($1);
3291                 $res{'from_mode'} = [ split(' ', $2) ];
3292                 $res{'to_mode'} = pop @{$res{'from_mode'}};
3293                 $res{'from_id'} = [ split(' ', $3) ];
3294                 $res{'to_id'} = pop @{$res{'from_id'}};
3295                 $res{'status'} = [ split('', $4) ];
3296                 $res{'to_file'} = unquote($5);
3297         }
3298         # 'c512b523472485aef4fff9e57b229d9d243c967f'
3299         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3300                 $res{'commit'} = $1;
3301         }
3303         return wantarray ? %res : \%res;
3306 # wrapper: return parsed line of git-diff-tree "raw" output
3307 # (the argument might be raw line, or parsed info)
3308 sub parsed_difftree_line {
3309         my $line_or_ref = shift;
3311         if (ref($line_or_ref) eq "HASH") {
3312                 # pre-parsed (or generated by hand)
3313                 return $line_or_ref;
3314         } else {
3315                 return parse_difftree_raw_line($line_or_ref);
3316         }
3319 # parse line of git-ls-tree output
3320 sub parse_ls_tree_line {
3321         my $line = shift;
3322         my %opts = @_;
3323         my %res;
3325         if ($opts{'-l'}) {
3326                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717  panic.c'
3327                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3329                 $res{'mode'} = $1;
3330                 $res{'type'} = $2;
3331                 $res{'hash'} = $3;
3332                 $res{'size'} = $4;
3333                 if ($opts{'-z'}) {
3334                         $res{'name'} = $5;
3335                 } else {
3336                         $res{'name'} = unquote($5);
3337                 }
3338         } else {
3339                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
3340                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3342                 $res{'mode'} = $1;
3343                 $res{'type'} = $2;
3344                 $res{'hash'} = $3;
3345                 if ($opts{'-z'}) {
3346                         $res{'name'} = $4;
3347                 } else {
3348                         $res{'name'} = unquote($4);
3349                 }
3350         }
3352         return wantarray ? %res : \%res;
3355 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3356 sub parse_from_to_diffinfo {
3357         my ($diffinfo, $from, $to, @parents) = @_;
3359         if ($diffinfo->{'nparents'}) {
3360                 # combined diff
3361                 $from->{'file'} = [];
3362                 $from->{'href'} = [];
3363                 fill_from_file_info($diffinfo, @parents)
3364                         unless exists $diffinfo->{'from_file'};
3365                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3366                         $from->{'file'}[$i] =
3367                                 defined $diffinfo->{'from_file'}[$i] ?
3368                                         $diffinfo->{'from_file'}[$i] :
3369                                         $diffinfo->{'to_file'};
3370                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3371                                 $from->{'href'}[$i] = href(action=>"blob",
3372                                                            hash_base=>$parents[$i],
3373                                                            hash=>$diffinfo->{'from_id'}[$i],
3374                                                            file_name=>$from->{'file'}[$i]);
3375                         } else {
3376                                 $from->{'href'}[$i] = undef;
3377                         }
3378                 }
3379         } else {
3380                 # ordinary (not combined) diff
3381                 $from->{'file'} = $diffinfo->{'from_file'};
3382                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3383                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3384                                                hash=>$diffinfo->{'from_id'},
3385                                                file_name=>$from->{'file'});
3386                 } else {
3387                         delete $from->{'href'};
3388                 }
3389         }
3391         $to->{'file'} = $diffinfo->{'to_file'};
3392         if (!is_deleted($diffinfo)) { # file exists in result
3393                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3394                                      hash=>$diffinfo->{'to_id'},
3395                                      file_name=>$to->{'file'});
3396         } else {
3397                 delete $to->{'href'};
3398         }
3401 ## ......................................................................
3402 ## parse to array of hashes functions
3404 sub git_get_heads_list {
3405         my ($limit, @classes) = @_;
3406         @classes = ('heads') unless @classes;
3407         my @patterns = map { "refs/$_" } @classes;
3408         my @headslist;
3410         open my $fd, '-|', git_cmd(), 'for-each-ref',
3411                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3412                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3413                 @patterns
3414                 or return;
3415         while (my $line = <$fd>) {
3416                 my %ref_item;
3418                 chomp $line;
3419                 my ($refinfo, $committerinfo) = split(/\0/, $line);
3420                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3421                 my ($committer, $epoch, $tz) =
3422                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3423                 $ref_item{'fullname'}  = $name;
3424                 $name =~ s!^refs/(?:head|remote)s/!!;
3426                 $ref_item{'name'}  = $name;
3427                 $ref_item{'id'}    = $hash;
3428                 $ref_item{'title'} = $title || '(no commit message)';
3429                 $ref_item{'epoch'} = $epoch;
3430                 if ($epoch) {
3431                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3432                 } else {
3433                         $ref_item{'age'} = "unknown";
3434                 }
3436                 push @headslist, \%ref_item;
3437         }
3438         close $fd;
3440         return wantarray ? @headslist : \@headslist;
3443 sub git_get_tags_list {
3444         my $limit = shift;
3445         my @tagslist;
3447         open my $fd, '-|', git_cmd(), 'for-each-ref',
3448                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3449                 '--format=%(objectname) %(objecttype) %(refname) '.
3450                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3451                 'refs/tags'
3452                 or return;
3453         while (my $line = <$fd>) {
3454                 my %ref_item;
3456                 chomp $line;
3457                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3458                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3459                 my ($creator, $epoch, $tz) =
3460                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3461                 $ref_item{'fullname'} = $name;
3462                 $name =~ s!^refs/tags/!!;
3464                 $ref_item{'type'} = $type;
3465                 $ref_item{'id'} = $id;
3466                 $ref_item{'name'} = $name;
3467                 if ($type eq "tag") {
3468                         $ref_item{'subject'} = $title;
3469                         $ref_item{'reftype'} = $reftype;
3470                         $ref_item{'refid'}   = $refid;
3471                 } else {
3472                         $ref_item{'reftype'} = $type;
3473                         $ref_item{'refid'}   = $id;
3474                 }
3476                 if ($type eq "tag" || $type eq "commit") {
3477                         $ref_item{'epoch'} = $epoch;
3478                         if ($epoch) {
3479                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3480                         } else {
3481                                 $ref_item{'age'} = "unknown";
3482                         }
3483                 }
3485                 push @tagslist, \%ref_item;
3486         }
3487         close $fd;
3489         return wantarray ? @tagslist : \@tagslist;
3492 ## ----------------------------------------------------------------------
3493 ## filesystem-related functions
3495 sub get_file_owner {
3496         my $path = shift;
3498         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3499         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3500         if (!defined $gcos) {
3501                 return undef;
3502         }
3503         my $owner = $gcos;
3504         $owner =~ s/[,;].*$//;
3505         return to_utf8($owner);
3508 # assume that file exists
3509 sub insert_file {
3510         my $filename = shift;
3512         open my $fd, '<', $filename;
3513         print map { to_utf8($_) } <$fd>;
3514         close $fd;
3517 ## ......................................................................
3518 ## mimetype related functions
3520 sub mimetype_guess_file {
3521         my $filename = shift;
3522         my $mimemap = shift;
3523         -r $mimemap or return undef;
3525         my %mimemap;
3526         open(my $mh, '<', $mimemap) or return undef;
3527         while (<$mh>) {
3528                 next if m/^#/; # skip comments
3529                 my ($mimetype, $exts) = split(/\t+/);
3530                 if (defined $exts) {
3531                         my @exts = split(/\s+/, $exts);
3532                         foreach my $ext (@exts) {
3533                                 $mimemap{$ext} = $mimetype;
3534                         }
3535                 }
3536         }
3537         close($mh);
3539         $filename =~ /\.([^.]*)$/;
3540         return $mimemap{$1};
3543 sub mimetype_guess {
3544         my $filename = shift;
3545         my $mime;
3546         $filename =~ /\./ or return undef;
3548         if ($mimetypes_file) {
3549                 my $file = $mimetypes_file;
3550                 if ($file !~ m!^/!) { # if it is relative path
3551                         # it is relative to project
3552                         $file = "$projectroot/$project/$file";
3553                 }
3554                 $mime = mimetype_guess_file($filename, $file);
3555         }
3556         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3557         return $mime;
3560 sub blob_mimetype {
3561         my $fd = shift;
3562         my $filename = shift;
3564         if ($filename) {
3565                 my $mime = mimetype_guess($filename);
3566                 $mime and return $mime;
3567         }
3569         # just in case
3570         return $default_blob_plain_mimetype unless $fd;
3572         if (-T $fd) {
3573                 return 'text/plain';
3574         } elsif (! $filename) {
3575                 return 'application/octet-stream';
3576         } elsif ($filename =~ m/\.png$/i) {
3577                 return 'image/png';
3578         } elsif ($filename =~ m/\.gif$/i) {
3579                 return 'image/gif';
3580         } elsif ($filename =~ m/\.jpe?g$/i) {
3581                 return 'image/jpeg';
3582         } else {
3583                 return 'application/octet-stream';
3584         }
3587 sub blob_contenttype {
3588         my ($fd, $file_name, $type) = @_;
3590         $type ||= blob_mimetype($fd, $file_name);
3591         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3592                 $type .= "; charset=$default_text_plain_charset";
3593         }
3595         return $type;
3598 # guess file syntax for syntax highlighting; return undef if no highlighting
3599 # the name of syntax can (in the future) depend on syntax highlighter used
3600 sub guess_file_syntax {
3601         my ($highlight, $mimetype, $file_name) = @_;
3602         return undef unless ($highlight && defined $file_name);
3603         my $basename = basename($file_name, '.in');
3604         return $highlight_basename{$basename}
3605                 if exists $highlight_basename{$basename};
3607         $basename =~ /\.([^.]*)$/;
3608         my $ext = $1 or return undef;
3609         return $highlight_ext{$ext}
3610                 if exists $highlight_ext{$ext};
3612         return undef;
3615 # run highlighter and return FD of its output,
3616 # or return original FD if no highlighting
3617 sub run_highlighter {
3618         my ($fd, $highlight, $syntax) = @_;
3619         return $fd unless ($highlight && defined $syntax);
3621         close $fd;
3622         open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3623                   quote_command($highlight_bin).
3624                   " --replace-tabs=8 --fragment --syntax $syntax |"
3625                 or die_error(500, "Couldn't open file or run syntax highlighter");
3626         return $fd;
3629 ## ======================================================================
3630 ## functions printing HTML: header, footer, error page
3632 sub get_page_title {
3633         my $title = to_utf8($site_name);
3635         return $title unless (defined $project);
3636         $title .= " - " . to_utf8($project);
3638         return $title unless (defined $action);
3639         $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3641         return $title unless (defined $file_name);
3642         $title .= " - " . esc_path($file_name);
3643         if ($action eq "tree" && $file_name !~ m|/$|) {
3644                 $title .= "/";
3645         }
3647         return $title;
3650 sub print_feed_meta {
3651         if (defined $project) {
3652                 my %href_params = get_feed_info();
3653                 if (!exists $href_params{'-title'}) {
3654                         $href_params{'-title'} = 'log';
3655                 }
3657                 foreach my $format (qw(RSS Atom)) {
3658                         my $type = lc($format);
3659                         my %link_attr = (
3660                                 '-rel' => 'alternate',
3661                                 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3662                                 '-type' => "application/$type+xml"
3663                         );
3665                         $href_params{'action'} = $type;
3666                         $link_attr{'-href'} = href(%href_params);
3667                         print "<link ".
3668                               "rel=\"$link_attr{'-rel'}\" ".
3669                               "title=\"$link_attr{'-title'}\" ".
3670                               "href=\"$link_attr{'-href'}\" ".
3671                               "type=\"$link_attr{'-type'}\" ".
3672                               "/>\n";
3674                         $href_params{'extra_options'} = '--no-merges';
3675                         $link_attr{'-href'} = href(%href_params);
3676                         $link_attr{'-title'} .= ' (no merges)';
3677                         print "<link ".
3678                               "rel=\"$link_attr{'-rel'}\" ".
3679                               "title=\"$link_attr{'-title'}\" ".
3680                               "href=\"$link_attr{'-href'}\" ".
3681                               "type=\"$link_attr{'-type'}\" ".
3682                               "/>\n";
3683                 }
3685         } else {
3686                 printf('<link rel="alternate" title="%s projects list" '.
3687                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
3688                        esc_attr($site_name), href(project=>undef, action=>"project_index"));
3689                 printf('<link rel="alternate" title="%s projects feeds" '.
3690                        'href="%s" type="text/x-opml" />'."\n",
3691                        esc_attr($site_name), href(project=>undef, action=>"opml"));
3692         }
3695 sub git_header_html {
3696         my $status = shift || "200 OK";
3697         my $expires = shift;
3698         my %opts = @_;
3700         my $title = get_page_title();
3701         my $content_type;
3702         # require explicit support from the UA if we are to send the page as
3703         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3704         # we have to do this because MSIE sometimes globs '*/*', pretending to
3705         # support xhtml+xml but choking when it gets what it asked for.
3706         if (defined $cgi->http('HTTP_ACCEPT') &&
3707             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3708             $cgi->Accept('application/xhtml+xml') != 0) {
3709                 $content_type = 'application/xhtml+xml';
3710         } else {
3711                 $content_type = 'text/html';
3712         }
3713         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3714                            -status=> $status, -expires => $expires)
3715                 unless ($opts{'-no_http_header'});
3716         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3717         print <<EOF;
3718 <?xml version="1.0" encoding="utf-8"?>
3719 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3720 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3721 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3722 <!-- git core binaries version $git_version -->
3723 <head>
3724 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3725 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3726 <meta name="robots" content="index, nofollow"/>
3727 <title>$title</title>
3728 EOF
3729         # the stylesheet, favicon etc urls won't work correctly with path_info
3730         # unless we set the appropriate base URL
3731         if ($ENV{'PATH_INFO'}) {
3732                 print "<base href=\"".esc_url($base_url)."\" />\n";
3733         }
3734         # print out each stylesheet that exist, providing backwards capability
3735         # for those people who defined $stylesheet in a config file
3736         if (defined $stylesheet) {
3737                 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3738         } else {
3739                 foreach my $stylesheet (@stylesheets) {
3740                         next unless $stylesheet;
3741                         print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3742                 }
3743         }
3744         print_feed_meta()
3745                 if ($status eq '200 OK');
3746         if (defined $favicon) {
3747                 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3748         }
3750         print "</head>\n" .
3751               "<body>\n";
3753         if (defined $site_header && -f $site_header) {
3754                 insert_file($site_header);
3755         }
3757         print "<div class=\"page_header\">\n";
3758         if (defined $logo) {
3759                 print $cgi->a({-href => esc_url($logo_url),
3760                                -title => $logo_label},
3761                               $cgi->img({-src => esc_url($logo),
3762                                          -width => 72, -height => 27,
3763                                          -alt => "git",
3764                                          -class => "logo"}));
3765         }
3766         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3767         if (defined $project) {
3768                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3769                 if (defined $action) {
3770                         my $action_print = $action ;
3771                         if (defined $opts{-action_extra}) {
3772                                 $action_print = $cgi->a({-href => href(action=>$action)},
3773                                         $action);
3774                         }
3775                         print " / $action_print";
3776                 }
3777                 if (defined $opts{-action_extra}) {
3778                         print " / $opts{-action_extra}";
3779                 }
3780                 print "\n";
3781         }
3782         print "</div>\n";
3784         my $have_search = gitweb_check_feature('search');
3785         if (defined $project && $have_search) {
3786                 if (!defined $searchtext) {
3787                         $searchtext = "";
3788                 }
3789                 my $search_hash;
3790                 if (defined $hash_base) {
3791                         $search_hash = $hash_base;
3792                 } elsif (defined $hash) {
3793                         $search_hash = $hash;
3794                 } else {
3795                         $search_hash = "HEAD";
3796                 }
3797                 my $action = $my_uri;
3798                 my $use_pathinfo = gitweb_check_feature('pathinfo');
3799                 if ($use_pathinfo) {
3800                         $action .= "/".esc_url($project);
3801                 }
3802                 print $cgi->startform(-method => "get", -action => $action) .
3803                       "<div class=\"search\">\n" .
3804                       (!$use_pathinfo &&
3805                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3806                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3807                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3808                       $cgi->popup_menu(-name => 'st', -default => 'commit',
3809                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3810                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3811                       " search:\n",
3812                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3813                       "<span title=\"Extended regular expression\">" .
3814                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3815                                      -checked => $search_use_regexp) .
3816                       "</span>" .
3817                       "</div>" .
3818                       $cgi->end_form() . "\n";
3819         }
3822 sub git_footer_html {
3823         my $feed_class = 'rss_logo';
3825         print "<div class=\"page_footer\">\n";
3826         if (defined $project) {
3827                 my $descr = git_get_project_description($project);
3828                 if (defined $descr) {
3829                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3830                 }
3832                 my %href_params = get_feed_info();
3833                 if (!%href_params) {
3834                         $feed_class .= ' generic';
3835                 }
3836                 $href_params{'-title'} ||= 'log';
3838                 foreach my $format (qw(RSS Atom)) {
3839                         $href_params{'action'} = lc($format);
3840                         print $cgi->a({-href => href(%href_params),
3841                                       -title => "$href_params{'-title'} $format feed",
3842                                       -class => $feed_class}, $format)."\n";
3843                 }
3845         } else {
3846                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3847                               -class => $feed_class}, "OPML") . " ";
3848                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3849                               -class => $feed_class}, "TXT") . "\n";
3850         }
3851         print "</div>\n"; # class="page_footer"
3853         if (defined $t0 && gitweb_check_feature('timed')) {
3854                 print "<div id=\"generating_info\">\n";
3855                 print 'This page took '.
3856                       '<span id="generating_time" class="time_span">'.
3857                       tv_interval($t0, [ gettimeofday() ]).
3858                       ' seconds </span>'.
3859                       ' and '.
3860                       '<span id="generating_cmd">'.
3861                       $number_of_git_cmds.
3862                       '</span> git commands '.
3863                       " to generate.\n";
3864                 print "</div>\n"; # class="page_footer"
3865         }
3867         if (defined $site_footer && -f $site_footer) {
3868                 insert_file($site_footer);
3869         }
3871         print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
3872         if (defined $action &&
3873             $action eq 'blame_incremental') {
3874                 print qq!<script type="text/javascript">\n!.
3875                       qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3876                       qq!           "!. href() .qq!");\n!.
3877                       qq!</script>\n!;
3878         } elsif (gitweb_check_feature('javascript-actions')) {
3879                 print qq!<script type="text/javascript">\n!.
3880                       qq!window.onload = fixLinks;\n!.
3881                       qq!</script>\n!;
3882         }
3884         print "</body>\n" .
3885               "</html>";
3888 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3889 # Example: die_error(404, 'Hash not found')
3890 # By convention, use the following status codes (as defined in RFC 2616):
3891 # 400: Invalid or missing CGI parameters, or
3892 #      requested object exists but has wrong type.
3893 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3894 #      this server or project.
3895 # 404: Requested object/revision/project doesn't exist.
3896 # 500: The server isn't configured properly, or
3897 #      an internal error occurred (e.g. failed assertions caused by bugs), or
3898 #      an unknown error occurred (e.g. the git binary died unexpectedly).
3899 # 503: The server is currently unavailable (because it is overloaded,
3900 #      or down for maintenance).  Generally, this is a temporary state.
3901 sub die_error {
3902         my $status = shift || 500;
3903         my $error = esc_html(shift) || "Internal Server Error";
3904         my $extra = shift;
3905         my %opts = @_;
3907         my %http_responses = (
3908                 400 => '400 Bad Request',
3909                 403 => '403 Forbidden',
3910                 404 => '404 Not Found',
3911                 500 => '500 Internal Server Error',
3912                 503 => '503 Service Unavailable',
3913         );
3914         git_header_html($http_responses{$status}, undef, %opts);
3915         print <<EOF;
3916 <div class="page_body">
3917 <br /><br />
3918 $status - $error
3919 <br />
3920 EOF
3921         if (defined $extra) {
3922                 print "<hr />\n" .
3923                       "$extra\n";
3924         }
3925         print "</div>\n";
3927         git_footer_html();
3928         goto DONE_GITWEB
3929                 unless ($opts{'-error_handler'});
3932 ## ----------------------------------------------------------------------
3933 ## functions printing or outputting HTML: navigation
3935 sub git_print_page_nav {
3936         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3937         $extra = '' if !defined $extra; # pager or formats
3939         my @navs = qw(summary shortlog log commit commitdiff tree);
3940         if ($suppress) {
3941                 @navs = grep { $_ ne $suppress } @navs;
3942         }
3944         my %arg = map { $_ => {action=>$_} } @navs;
3945         if (defined $head) {
3946                 for (qw(commit commitdiff)) {
3947                         $arg{$_}{'hash'} = $head;
3948                 }
3949                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3950                         for (qw(shortlog log)) {
3951                                 $arg{$_}{'hash'} = $head;
3952                         }
3953                 }
3954         }
3956         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3957         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3959         my @actions = gitweb_get_feature('actions');
3960         my %repl = (
3961                 '%' => '%',
3962                 'n' => $project,         # project name
3963                 'f' => $git_dir,         # project path within filesystem
3964                 'h' => $treehead || '',  # current hash ('h' parameter)
3965                 'b' => $treebase || '',  # hash base ('hb' parameter)
3966         );
3967         while (@actions) {
3968                 my ($label, $link, $pos) = splice(@actions,0,3);
3969                 # insert
3970                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3971                 # munch munch
3972                 $link =~ s/%([%nfhb])/$repl{$1}/g;
3973                 $arg{$label}{'_href'} = $link;
3974         }
3976         print "<div class=\"page_nav\">\n" .
3977                 (join " | ",
3978                  map { $_ eq $current ?
3979                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3980                  } @navs);
3981         print "<br/>\n$extra<br/>\n" .
3982               "</div>\n";
3985 # returns a submenu for the nagivation of the refs views (tags, heads,
3986 # remotes) with the current view disabled and the remotes view only
3987 # available if the feature is enabled
3988 sub format_ref_views {
3989         my ($current) = @_;
3990         my @ref_views = qw{tags heads};
3991         push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
3992         return join " | ", map {
3993                 $_ eq $current ? $_ :
3994                 $cgi->a({-href => href(action=>$_)}, $_)
3995         } @ref_views
3998 sub format_paging_nav {
3999         my ($action, $page, $has_next_link) = @_;
4000         my $paging_nav;
4003         if ($page > 0) {
4004                 $paging_nav .=
4005                         $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4006                         " &sdot; " .
4007                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
4008                                  -accesskey => "p", -title => "Alt-p"}, "prev");
4009         } else {
4010                 $paging_nav .= "first &sdot; prev";
4011         }
4013         if ($has_next_link) {
4014                 $paging_nav .= " &sdot; " .
4015                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
4016                                  -accesskey => "n", -title => "Alt-n"}, "next");
4017         } else {
4018                 $paging_nav .= " &sdot; next";
4019         }
4021         return $paging_nav;
4024 ## ......................................................................
4025 ## functions printing or outputting HTML: div
4027 sub git_print_header_div {
4028         my ($action, $title, $hash, $hash_base) = @_;
4029         my %args = ();
4031         $args{'action'} = $action;
4032         $args{'hash'} = $hash if $hash;
4033         $args{'hash_base'} = $hash_base if $hash_base;
4035         print "<div class=\"header\">\n" .
4036               $cgi->a({-href => href(%args), -class => "title"},
4037               $title ? $title : $action) .
4038               "\n</div>\n";
4041 sub format_repo_url {
4042         my ($name, $url) = @_;
4043         return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4046 # Group output by placing it in a DIV element and adding a header.
4047 # Options for start_div() can be provided by passing a hash reference as the
4048 # first parameter to the function.
4049 # Options to git_print_header_div() can be provided by passing an array
4050 # reference. This must follow the options to start_div if they are present.
4051 # The content can be a scalar, which is output as-is, a scalar reference, which
4052 # is output after html escaping, an IO handle passed either as *handle or
4053 # *handle{IO}, or a function reference. In the latter case all following
4054 # parameters will be taken as argument to the content function call.
4055 sub git_print_section {
4056         my ($div_args, $header_args, $content);
4057         my $arg = shift;
4058         if (ref($arg) eq 'HASH') {
4059                 $div_args = $arg;
4060                 $arg = shift;
4061         }
4062         if (ref($arg) eq 'ARRAY') {
4063                 $header_args = $arg;
4064                 $arg = shift;
4065         }
4066         $content = $arg;
4068         print $cgi->start_div($div_args);
4069         git_print_header_div(@$header_args);
4071         if (ref($content) eq 'CODE') {
4072                 $content->(@_);
4073         } elsif (ref($content) eq 'SCALAR') {
4074                 print esc_html($$content);
4075         } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4076                 print <$content>;
4077         } elsif (!ref($content) && defined($content)) {
4078                 print $content;
4079         }
4081         print $cgi->end_div;
4084 sub print_local_time {
4085         print format_local_time(@_);
4088 sub format_local_time {
4089         my $localtime = '';
4090         my %date = @_;
4091         if ($date{'hour_local'} < 6) {
4092                 $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4093                         $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
4094         } else {
4095                 $localtime .= sprintf(" (%02d:%02d %s)",
4096                         $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
4097         }
4099         return $localtime;
4102 # Outputs the author name and date in long form
4103 sub git_print_authorship {
4104         my $co = shift;
4105         my %opts = @_;
4106         my $tag = $opts{-tag} || 'div';
4107         my $author = $co->{'author_name'};
4109         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4110         print "<$tag class=\"author_date\">" .
4111               format_search_author($author, "author", esc_html($author)) .
4112               " [$ad{'rfc2822'}";
4113         print_local_time(%ad) if ($opts{-localtime});
4114         print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
4115                   . "</$tag>\n";
4118 # Outputs table rows containing the full author or committer information,
4119 # in the format expected for 'commit' view (& similar).
4120 # Parameters are a commit hash reference, followed by the list of people
4121 # to output information for. If the list is empty it defaults to both
4122 # author and committer.
4123 sub git_print_authorship_rows {
4124         my $co = shift;
4125         # too bad we can't use @people = @_ || ('author', 'committer')
4126         my @people = @_;
4127         @people = ('author', 'committer') unless @people;
4128         foreach my $who (@people) {
4129                 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4130                 print "<tr><td>$who</td><td>" .
4131                       format_search_author($co->{"${who}_name"}, $who,
4132                                esc_html($co->{"${who}_name"})) . " " .
4133                       format_search_author($co->{"${who}_email"}, $who,
4134                                esc_html("<" . $co->{"${who}_email"} . ">")) .
4135                       "</td><td rowspan=\"2\">" .
4136                       git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4137                       "</td></tr>\n" .
4138                       "<tr>" .
4139                       "<td></td><td> $wd{'rfc2822'}";
4140                 print_local_time(%wd);
4141                 print "</td>" .
4142                       "</tr>\n";
4143         }
4146 sub git_print_page_path {
4147         my $name = shift;
4148         my $type = shift;
4149         my $hb = shift;
4152         print "<div class=\"page_path\">";
4153         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4154                       -title => 'tree root'}, to_utf8("[$project]"));
4155         print " / ";
4156         if (defined $name) {
4157                 my @dirname = split '/', $name;
4158                 my $basename = pop @dirname;
4159                 my $fullname = '';
4161                 foreach my $dir (@dirname) {
4162                         $fullname .= ($fullname ? '/' : '') . $dir;
4163                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4164                                                      hash_base=>$hb),
4165                                       -title => $fullname}, esc_path($dir));
4166                         print " / ";
4167                 }
4168                 if (defined $type && $type eq 'blob') {
4169                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4170                                                      hash_base=>$hb),
4171                                       -title => $name}, esc_path($basename));
4172                 } elsif (defined $type && $type eq 'tree') {
4173                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4174                                                      hash_base=>$hb),
4175                                       -title => $name}, esc_path($basename));
4176                         print " / ";
4177                 } else {
4178                         print esc_path($basename);
4179                 }
4180         }
4181         print "<br/></div>\n";
4184 sub git_print_log {
4185         my $log = shift;
4186         my %opts = @_;
4188         if ($opts{'-remove_title'}) {
4189                 # remove title, i.e. first line of log
4190                 shift @$log;
4191         }
4192         # remove leading empty lines
4193         while (defined $log->[0] && $log->[0] eq "") {
4194                 shift @$log;
4195         }
4197         # print log
4198         my $signoff = 0;
4199         my $empty = 0;
4200         foreach my $line (@$log) {
4201                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4202                         $signoff = 1;
4203                         $empty = 0;
4204                         if (! $opts{'-remove_signoff'}) {
4205                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4206                                 next;
4207                         } else {
4208                                 # remove signoff lines
4209                                 next;
4210                         }
4211                 } else {
4212                         $signoff = 0;
4213                 }
4215                 # print only one empty line
4216                 # do not print empty line after signoff
4217                 if ($line eq "") {
4218                         next if ($empty || $signoff);
4219                         $empty = 1;
4220                 } else {
4221                         $empty = 0;
4222                 }
4224                 print format_log_line_html($line) . "<br/>\n";
4225         }
4227         if ($opts{'-final_empty_line'}) {
4228                 # end with single empty line
4229                 print "<br/>\n" unless $empty;
4230         }
4233 # return link target (what link points to)
4234 sub git_get_link_target {
4235         my $hash = shift;
4236         my $link_target;
4238         # read link
4239         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4240                 or return;
4241         {
4242                 local $/ = undef;
4243                 $link_target = <$fd>;
4244         }
4245         close $fd
4246                 or return;
4248         return $link_target;
4251 # given link target, and the directory (basedir) the link is in,
4252 # return target of link relative to top directory (top tree);
4253 # return undef if it is not possible (including absolute links).
4254 sub normalize_link_target {
4255         my ($link_target, $basedir) = @_;
4257         # absolute symlinks (beginning with '/') cannot be normalized
4258         return if (substr($link_target, 0, 1) eq '/');
4260         # normalize link target to path from top (root) tree (dir)
4261         my $path;
4262         if ($basedir) {
4263                 $path = $basedir . '/' . $link_target;
4264         } else {
4265                 # we are in top (root) tree (dir)
4266                 $path = $link_target;
4267         }
4269         # remove //, /./, and /../
4270         my @path_parts;
4271         foreach my $part (split('/', $path)) {
4272                 # discard '.' and ''
4273                 next if (!$part || $part eq '.');
4274                 # handle '..'
4275                 if ($part eq '..') {
4276                         if (@path_parts) {
4277                                 pop @path_parts;
4278                         } else {
4279                                 # link leads outside repository (outside top dir)
4280                                 return;
4281                         }
4282                 } else {
4283                         push @path_parts, $part;
4284                 }
4285         }
4286         $path = join('/', @path_parts);
4288         return $path;
4291 # print tree entry (row of git_tree), but without encompassing <tr> element
4292 sub git_print_tree_entry {
4293         my ($t, $basedir, $hash_base, $have_blame) = @_;
4295         my %base_key = ();
4296         $base_key{'hash_base'} = $hash_base if defined $hash_base;
4298         # The format of a table row is: mode list link.  Where mode is
4299         # the mode of the entry, list is the name of the entry, an href,
4300         # and link is the action links of the entry.
4302         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4303         if (exists $t->{'size'}) {
4304                 print "<td class=\"size\">$t->{'size'}</td>\n";
4305         }
4306         if ($t->{'type'} eq "blob") {
4307                 print "<td class=\"list\">" .
4308                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4309                                                file_name=>"$basedir$t->{'name'}", %base_key),
4310                                 -class => "list"}, esc_path($t->{'name'}));
4311                 if (S_ISLNK(oct $t->{'mode'})) {
4312                         my $link_target = git_get_link_target($t->{'hash'});
4313                         if ($link_target) {
4314                                 my $norm_target = normalize_link_target($link_target, $basedir);
4315                                 if (defined $norm_target) {
4316                                         print " -> " .
4317                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4318                                                                      file_name=>$norm_target),
4319                                                        -title => $norm_target}, esc_path($link_target));
4320                                 } else {
4321                                         print " -> " . esc_path($link_target);
4322                                 }
4323                         }
4324                 }
4325                 print "</td>\n";
4326                 print "<td class=\"link\">";
4327                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4328                                              file_name=>"$basedir$t->{'name'}", %base_key)},
4329                               "blob");
4330                 if ($have_blame) {
4331                         print " | " .
4332                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4333                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
4334                                       "blame");
4335                 }
4336                 if (defined $hash_base) {
4337                         print " | " .
4338                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4339                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4340                                       "history");
4341                 }
4342                 print " | " .
4343                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4344                                                file_name=>"$basedir$t->{'name'}")},
4345                                 "raw");
4346                 print "</td>\n";
4348         } elsif ($t->{'type'} eq "tree") {
4349                 print "<td class=\"list\">";
4350                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4351                                              file_name=>"$basedir$t->{'name'}",
4352                                              %base_key)},
4353                               esc_path($t->{'name'}));
4354                 print "</td>\n";
4355                 print "<td class=\"link\">";
4356                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4357                                              file_name=>"$basedir$t->{'name'}",
4358                                              %base_key)},
4359                               "tree");
4360                 if (defined $hash_base) {
4361                         print " | " .
4362                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4363                                                      file_name=>"$basedir$t->{'name'}")},
4364                                       "history");
4365                 }
4366                 print "</td>\n";
4367         } else {
4368                 # unknown object: we can only present history for it
4369                 # (this includes 'commit' object, i.e. submodule support)
4370                 print "<td class=\"list\">" .
4371                       esc_path($t->{'name'}) .
4372                       "</td>\n";
4373                 print "<td class=\"link\">";
4374                 if (defined $hash_base) {
4375                         print $cgi->a({-href => href(action=>"history",
4376                                                      hash_base=>$hash_base,
4377                                                      file_name=>"$basedir$t->{'name'}")},
4378                                       "history");
4379                 }
4380                 print "</td>\n";
4381         }
4384 ## ......................................................................
4385 ## functions printing large fragments of HTML
4387 # get pre-image filenames for merge (combined) diff
4388 sub fill_from_file_info {
4389         my ($diff, @parents) = @_;
4391         $diff->{'from_file'} = [ ];
4392         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4393         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4394                 if ($diff->{'status'}[$i] eq 'R' ||
4395                     $diff->{'status'}[$i] eq 'C') {
4396                         $diff->{'from_file'}[$i] =
4397                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4398                 }
4399         }
4401         return $diff;
4404 # is current raw difftree line of file deletion
4405 sub is_deleted {
4406         my $diffinfo = shift;
4408         return $diffinfo->{'to_id'} eq ('0' x 40);
4411 # does patch correspond to [previous] difftree raw line
4412 # $diffinfo  - hashref of parsed raw diff format
4413 # $patchinfo - hashref of parsed patch diff format
4414 #              (the same keys as in $diffinfo)
4415 sub is_patch_split {
4416         my ($diffinfo, $patchinfo) = @_;
4418         return defined $diffinfo && defined $patchinfo
4419                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4423 sub git_difftree_body {
4424         my ($difftree, $hash, @parents) = @_;
4425         my ($parent) = $parents[0];
4426         my $have_blame = gitweb_check_feature('blame');
4427         print "<div class=\"list_head\">\n";
4428         if ($#{$difftree} > 10) {
4429                 print(($#{$difftree} + 1) . " files changed:\n");
4430         }
4431         print "</div>\n";
4433         print "<table class=\"" .
4434               (@parents > 1 ? "combined " : "") .
4435               "diff_tree\">\n";
4437         # header only for combined diff in 'commitdiff' view
4438         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4439         if ($has_header) {
4440                 # table header
4441                 print "<thead><tr>\n" .
4442                        "<th></th><th></th>\n"; # filename, patchN link
4443                 for (my $i = 0; $i < @parents; $i++) {
4444                         my $par = $parents[$i];
4445                         print "<th>" .
4446                               $cgi->a({-href => href(action=>"commitdiff",
4447                                                      hash=>$hash, hash_parent=>$par),
4448                                        -title => 'commitdiff to parent number ' .
4449                                                   ($i+1) . ': ' . substr($par,0,7)},
4450                                       $i+1) .
4451                               "&nbsp;</th>\n";
4452                 }
4453                 print "</tr></thead>\n<tbody>\n";
4454         }
4456         my $alternate = 1;
4457         my $patchno = 0;
4458         foreach my $line (@{$difftree}) {
4459                 my $diff = parsed_difftree_line($line);
4461                 if ($alternate) {
4462                         print "<tr class=\"dark\">\n";
4463                 } else {
4464                         print "<tr class=\"light\">\n";
4465                 }
4466                 $alternate ^= 1;
4468                 if (exists $diff->{'nparents'}) { # combined diff
4470                         fill_from_file_info($diff, @parents)
4471                                 unless exists $diff->{'from_file'};
4473                         if (!is_deleted($diff)) {
4474                                 # file exists in the result (child) commit
4475                                 print "<td>" .
4476                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4477                                                              file_name=>$diff->{'to_file'},
4478                                                              hash_base=>$hash),
4479                                               -class => "list"}, esc_path($diff->{'to_file'})) .
4480                                       "</td>\n";
4481                         } else {
4482                                 print "<td>" .
4483                                       esc_path($diff->{'to_file'}) .
4484                                       "</td>\n";
4485                         }
4487                         if ($action eq 'commitdiff') {
4488                                 # link to patch
4489                                 $patchno++;
4490                                 print "<td class=\"link\">" .
4491                                       $cgi->a({-href => href(-anchor=>"patch$patchno")},
4492                                               "patch") .
4493                                       " | " .
4494                                       "</td>\n";
4495                         }
4497                         my $has_history = 0;
4498                         my $not_deleted = 0;
4499                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4500                                 my $hash_parent = $parents[$i];
4501                                 my $from_hash = $diff->{'from_id'}[$i];
4502                                 my $from_path = $diff->{'from_file'}[$i];
4503                                 my $status = $diff->{'status'}[$i];
4505                                 $has_history ||= ($status ne 'A');
4506                                 $not_deleted ||= ($status ne 'D');
4508                                 if ($status eq 'A') {
4509                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
4510                                 } elsif ($status eq 'D') {
4511                                         print "<td class=\"link\">" .
4512                                               $cgi->a({-href => href(action=>"blob",
4513                                                                      hash_base=>$hash,
4514                                                                      hash=>$from_hash,
4515                                                                      file_name=>$from_path)},
4516                                                       "blob" . ($i+1)) .
4517                                               " | </td>\n";
4518                                 } else {
4519                                         if ($diff->{'to_id'} eq $from_hash) {
4520                                                 print "<td class=\"link nochange\">";
4521                                         } else {
4522                                                 print "<td class=\"link\">";
4523                                         }
4524                                         print $cgi->a({-href => href(action=>"blobdiff",
4525                                                                      hash=>$diff->{'to_id'},
4526                                                                      hash_parent=>$from_hash,
4527                                                                      hash_base=>$hash,
4528                                                                      hash_parent_base=>$hash_parent,
4529                                                                      file_name=>$diff->{'to_file'},
4530                                                                      file_parent=>$from_path)},
4531                                                       "diff" . ($i+1)) .
4532                                               " | </td>\n";
4533                                 }
4534                         }
4536                         print "<td class=\"link\">";
4537                         if ($not_deleted) {
4538                                 print $cgi->a({-href => href(action=>"blob",
4539                                                              hash=>$diff->{'to_id'},
4540                                                              file_name=>$diff->{'to_file'},
4541                                                              hash_base=>$hash)},
4542                                               "blob");
4543                                 print " | " if ($has_history);
4544                         }
4545                         if ($has_history) {
4546                                 print $cgi->a({-href => href(action=>"history",
4547                                                              file_name=>$diff->{'to_file'},
4548                                                              hash_base=>$hash)},
4549                                               "history");
4550                         }
4551                         print "</td>\n";
4553                         print "</tr>\n";
4554                         next; # instead of 'else' clause, to avoid extra indent
4555                 }
4556                 # else ordinary diff
4558                 my ($to_mode_oct, $to_mode_str, $to_file_type);
4559                 my ($from_mode_oct, $from_mode_str, $from_file_type);
4560                 if ($diff->{'to_mode'} ne ('0' x 6)) {
4561                         $to_mode_oct = oct $diff->{'to_mode'};
4562                         if (S_ISREG($to_mode_oct)) { # only for regular file
4563                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4564                         }
4565                         $to_file_type = file_type($diff->{'to_mode'});
4566                 }
4567                 if ($diff->{'from_mode'} ne ('0' x 6)) {
4568                         $from_mode_oct = oct $diff->{'from_mode'};
4569                         if (S_ISREG($from_mode_oct)) { # only for regular file
4570                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4571                         }
4572                         $from_file_type = file_type($diff->{'from_mode'});
4573                 }
4575                 if ($diff->{'status'} eq "A") { # created
4576                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4577                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
4578                         $mode_chng   .= "]</span>";
4579                         print "<td>";
4580                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4581                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4582                                       -class => "list"}, esc_path($diff->{'file'}));
4583                         print "</td>\n";
4584                         print "<td>$mode_chng</td>\n";
4585                         print "<td class=\"link\">";
4586                         if ($action eq 'commitdiff') {
4587                                 # link to patch
4588                                 $patchno++;
4589                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4590                                               "patch") .
4591                                       " | ";
4592                         }
4593                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4594                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4595                                       "blob");
4596                         print "</td>\n";
4598                 } elsif ($diff->{'status'} eq "D") { # deleted
4599                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4600                         print "<td>";
4601                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4602                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
4603                                        -class => "list"}, esc_path($diff->{'file'}));
4604                         print "</td>\n";
4605                         print "<td>$mode_chng</td>\n";
4606                         print "<td class=\"link\">";
4607                         if ($action eq 'commitdiff') {
4608                                 # link to patch
4609                                 $patchno++;
4610                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4611                                               "patch") .
4612                                       " | ";
4613                         }
4614                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4615                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
4616                                       "blob") . " | ";
4617                         if ($have_blame) {
4618                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4619                                                              file_name=>$diff->{'file'})},
4620                                               "blame") . " | ";
4621                         }
4622                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4623                                                      file_name=>$diff->{'file'})},
4624                                       "history");
4625                         print "</td>\n";
4627                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4628                         my $mode_chnge = "";
4629                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4630                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4631                                 if ($from_file_type ne $to_file_type) {
4632                                         $mode_chnge .= " from $from_file_type to $to_file_type";
4633                                 }
4634                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4635                                         if ($from_mode_str && $to_mode_str) {
4636                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4637                                         } elsif ($to_mode_str) {
4638                                                 $mode_chnge .= " mode: $to_mode_str";
4639                                         }
4640                                 }
4641                                 $mode_chnge .= "]</span>\n";
4642                         }
4643                         print "<td>";
4644                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4645                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4646                                       -class => "list"}, esc_path($diff->{'file'}));
4647                         print "</td>\n";
4648                         print "<td>$mode_chnge</td>\n";
4649                         print "<td class=\"link\">";
4650                         if ($action eq 'commitdiff') {
4651                                 # link to patch
4652                                 $patchno++;
4653                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4654                                               "patch") .
4655                                       " | ";
4656                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4657                                 # "commit" view and modified file (not onlu mode changed)
4658                                 print $cgi->a({-href => href(action=>"blobdiff",
4659                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4660                                                              hash_base=>$hash, hash_parent_base=>$parent,
4661                                                              file_name=>$diff->{'file'})},
4662                                               "diff") .
4663                                       " | ";
4664                         }
4665                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4666                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4667                                        "blob") . " | ";
4668                         if ($have_blame) {
4669                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4670                                                              file_name=>$diff->{'file'})},
4671                                               "blame") . " | ";
4672                         }
4673                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4674                                                      file_name=>$diff->{'file'})},
4675                                       "history");
4676                         print "</td>\n";
4678                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4679                         my %status_name = ('R' => 'moved', 'C' => 'copied');
4680                         my $nstatus = $status_name{$diff->{'status'}};
4681                         my $mode_chng = "";
4682                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4683                                 # mode also for directories, so we cannot use $to_mode_str
4684                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4685                         }
4686                         print "<td>" .
4687                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4688                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4689                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4690                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4691                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4692                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4693                                       -class => "list"}, esc_path($diff->{'from_file'})) .
4694                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4695                               "<td class=\"link\">";
4696                         if ($action eq 'commitdiff') {
4697                                 # link to patch
4698                                 $patchno++;
4699                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4700                                               "patch") .
4701                                       " | ";
4702                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4703                                 # "commit" view and modified file (not only pure rename or copy)
4704                                 print $cgi->a({-href => href(action=>"blobdiff",
4705                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4706                                                              hash_base=>$hash, hash_parent_base=>$parent,
4707                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4708                                               "diff") .
4709                                       " | ";
4710                         }
4711                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4712                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
4713                                       "blob") . " | ";
4714                         if ($have_blame) {
4715                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4716                                                              file_name=>$diff->{'to_file'})},
4717                                               "blame") . " | ";
4718                         }
4719                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4720                                                     file_name=>$diff->{'to_file'})},
4721                                       "history");
4722                         print "</td>\n";
4724                 } # we should not encounter Unmerged (U) or Unknown (X) status
4725                 print "</tr>\n";
4726         }
4727         print "</tbody>" if $has_header;
4728         print "</table>\n";
4731 sub git_patchset_body {
4732         my ($fd, $difftree, $hash, @hash_parents) = @_;
4733         my ($hash_parent) = $hash_parents[0];
4735         my $is_combined = (@hash_parents > 1);
4736         my $patch_idx = 0;
4737         my $patch_number = 0;
4738         my $patch_line;
4739         my $diffinfo;
4740         my $to_name;
4741         my (%from, %to);
4743         print "<div class=\"patchset\">\n";
4745         # skip to first patch
4746         while ($patch_line = <$fd>) {
4747                 chomp $patch_line;
4749                 last if ($patch_line =~ m/^diff /);
4750         }
4752  PATCH:
4753         while ($patch_line) {
4755                 # parse "git diff" header line
4756                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4757                         # $1 is from_name, which we do not use
4758                         $to_name = unquote($2);
4759                         $to_name =~ s!^b/!!;
4760                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4761                         # $1 is 'cc' or 'combined', which we do not use
4762                         $to_name = unquote($2);
4763                 } else {
4764                         $to_name = undef;
4765                 }
4767                 # check if current patch belong to current raw line
4768                 # and parse raw git-diff line if needed
4769                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4770                         # this is continuation of a split patch
4771                         print "<div class=\"patch cont\">\n";
4772                 } else {
4773                         # advance raw git-diff output if needed
4774                         $patch_idx++ if defined $diffinfo;
4776                         # read and prepare patch information
4777                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4779                         # compact combined diff output can have some patches skipped
4780                         # find which patch (using pathname of result) we are at now;
4781                         if ($is_combined) {
4782                                 while ($to_name ne $diffinfo->{'to_file'}) {
4783                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4784                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
4785                                               "</div>\n";  # class="patch"
4787                                         $patch_idx++;
4788                                         $patch_number++;
4790                                         last if $patch_idx > $#$difftree;
4791                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4792                                 }
4793                         }
4795                         # modifies %from, %to hashes
4796                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4798                         # this is first patch for raw difftree line with $patch_idx index
4799                         # we index @$difftree array from 0, but number patches from 1
4800                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4801                 }
4803                 # git diff header
4804                 #assert($patch_line =~ m/^diff /) if DEBUG;
4805                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4806                 $patch_number++;
4807                 # print "git diff" header
4808                 print format_git_diff_header_line($patch_line, $diffinfo,
4809                                                   \%from, \%to);
4811                 # print extended diff header
4812                 print "<div class=\"diff extended_header\">\n";
4813         EXTENDED_HEADER:
4814                 while ($patch_line = <$fd>) {
4815                         chomp $patch_line;
4817                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4819                         print format_extended_diff_header_line($patch_line, $diffinfo,
4820                                                                \%from, \%to);
4821                 }
4822                 print "</div>\n"; # class="diff extended_header"
4824                 # from-file/to-file diff header
4825                 if (! $patch_line) {
4826                         print "</div>\n"; # class="patch"
4827                         last PATCH;
4828                 }
4829                 next PATCH if ($patch_line =~ m/^diff /);
4830                 #assert($patch_line =~ m/^---/) if DEBUG;
4832                 my $last_patch_line = $patch_line;
4833                 $patch_line = <$fd>;
4834                 chomp $patch_line;
4835                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4837                 print format_diff_from_to_header($last_patch_line, $patch_line,
4838                                                  $diffinfo, \%from, \%to,
4839                                                  @hash_parents);
4841                 # the patch itself
4842         LINE:
4843                 while ($patch_line = <$fd>) {
4844                         chomp $patch_line;
4846                         next PATCH if ($patch_line =~ m/^diff /);
4848                         print format_diff_line($patch_line, \%from, \%to);
4849                 }
4851         } continue {
4852                 print "</div>\n"; # class="patch"
4853         }
4855         # for compact combined (--cc) format, with chunk and patch simplification
4856         # the patchset might be empty, but there might be unprocessed raw lines
4857         for (++$patch_idx if $patch_number > 0;
4858              $patch_idx < @$difftree;
4859              ++$patch_idx) {
4860                 # read and prepare patch information
4861                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4863                 # generate anchor for "patch" links in difftree / whatchanged part
4864                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4865                       format_diff_cc_simplified($diffinfo, @hash_parents) .
4866                       "</div>\n";  # class="patch"
4868                 $patch_number++;
4869         }
4871         if ($patch_number == 0) {
4872                 if (@hash_parents > 1) {
4873                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4874                 } else {
4875                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
4876                 }
4877         }
4879         print "</div>\n"; # class="patchset"
4882 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4884 # fills project list info (age, description, owner, forks) for each
4885 # project in the list, removing invalid projects from returned list
4886 # NOTE: modifies $projlist, but does not remove entries from it
4887 sub fill_project_list_info {
4888         my $projlist = shift;
4889         my @projects;
4891         my $show_ctags = gitweb_check_feature('ctags');
4892  PROJECT:
4893         foreach my $pr (@$projlist) {
4894                 my (@activity) = git_get_last_activity($pr->{'path'});
4895                 unless (@activity) {
4896                         next PROJECT;
4897                 }
4898                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4899                 if (!defined $pr->{'descr'}) {
4900                         my $descr = git_get_project_description($pr->{'path'}) || "";
4901                         $descr = to_utf8($descr);
4902                         $pr->{'descr_long'} = $descr;
4903                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4904                 }
4905                 if (!defined $pr->{'owner'}) {
4906                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4907                 }
4908                 if ($show_ctags) {
4909                         $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4910                 }
4911                 push @projects, $pr;
4912         }
4914         return @projects;
4917 sub sort_projects_list {
4918         my ($projlist, $order) = @_;
4919         my @projects;
4921         my %order_info = (
4922                 project => { key => 'path', type => 'str' },
4923                 descr => { key => 'descr_long', type => 'str' },
4924                 owner => { key => 'owner', type => 'str' },
4925                 age => { key => 'age', type => 'num' }
4926         );
4927         my $oi = $order_info{$order};
4928         return @$projlist unless defined $oi;
4929         if ($oi->{'type'} eq 'str') {
4930                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
4931         } else {
4932                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
4933         }
4935         return @projects;
4938 # print 'sort by' <th> element, generating 'sort by $name' replay link
4939 # if that order is not selected
4940 sub print_sort_th {
4941         print format_sort_th(@_);
4944 sub format_sort_th {
4945         my ($name, $order, $header) = @_;
4946         my $sort_th = "";
4947         $header ||= ucfirst($name);
4949         if ($order eq $name) {
4950                 $sort_th .= "<th>$header</th>\n";
4951         } else {
4952                 $sort_th .= "<th>" .
4953                             $cgi->a({-href => href(-replay=>1, order=>$name),
4954                                      -class => "header"}, $header) .
4955                             "</th>\n";
4956         }
4958         return $sort_th;
4961 sub git_project_list_body {
4962         # actually uses global variable $project
4963         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4964         my @projects = @$projlist;
4966         my $check_forks = gitweb_check_feature('forks');
4967         my $show_ctags  = gitweb_check_feature('ctags');
4968         my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
4969         $check_forks = undef
4970                 if ($tagfilter || $searchtext);
4972         # filtering out forks before filling info allows to do less work
4973         @projects = filter_forks_from_projects_list(\@projects)
4974                 if ($check_forks);
4975         @projects = fill_project_list_info(\@projects);
4976         # searching projects require filling to be run before it
4977         @projects = search_projects_list(\@projects,
4978                                          'searchtext' => $searchtext,
4979                                          'tagfilter'  => $tagfilter)
4980                 if ($tagfilter || $searchtext);
4982         $order ||= $default_projects_order;
4983         $from = 0 unless defined $from;
4984         $to = $#projects if (!defined $to || $#projects < $to);
4986         # short circuit
4987         if ($from > $to) {
4988                 print "<center>\n".
4989                       "<b>No such projects found</b><br />\n".
4990                       "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
4991                       "</center>\n<br />\n";
4992                 return;
4993         }
4995         @projects = sort_projects_list(\@projects, $order);
4997         if ($show_ctags) {
4998                 my $ctags = git_gather_all_ctags(\@projects);
4999                 my $cloud = git_populate_project_tagcloud($ctags);
5000                 print git_show_project_tagcloud($cloud, 64);
5001         }
5003         print "<table class=\"project_list\">\n";
5004         unless ($no_header) {
5005                 print "<tr>\n";
5006                 if ($check_forks) {
5007                         print "<th></th>\n";
5008                 }
5009                 print_sort_th('project', $order, 'Project');
5010                 print_sort_th('descr', $order, 'Description');
5011                 print_sort_th('owner', $order, 'Owner');
5012                 print_sort_th('age', $order, 'Last Change');
5013                 print "<th></th>\n" . # for links
5014                       "</tr>\n";
5015         }
5016         my $alternate = 1;
5017         for (my $i = $from; $i <= $to; $i++) {
5018                 my $pr = $projects[$i];
5020                 if ($alternate) {
5021                         print "<tr class=\"dark\">\n";
5022                 } else {
5023                         print "<tr class=\"light\">\n";
5024                 }
5025                 $alternate ^= 1;
5027                 if ($check_forks) {
5028                         print "<td>";
5029                         if ($pr->{'forks'}) {
5030                                 my $nforks = scalar @{$pr->{'forks'}};
5031                                 if ($nforks > 0) {
5032                                         print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5033                                                        -title => "$nforks forks"}, "+");
5034                                 } else {
5035                                         print $cgi->span({-title => "$nforks forks"}, "+");
5036                                 }
5037                         }
5038                         print "</td>\n";
5039                 }
5040                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5041                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
5042                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5043                                         -class => "list", -title => $pr->{'descr_long'}},
5044                                         esc_html($pr->{'descr'})) . "</td>\n" .
5045                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5046                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5047                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
5048                       "<td class=\"link\">" .
5049                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
5050                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5051                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5052                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5053                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5054                       "</td>\n" .
5055                       "</tr>\n";
5056         }
5057         if (defined $extra) {
5058                 print "<tr>\n";
5059                 if ($check_forks) {
5060                         print "<td></td>\n";
5061                 }
5062                 print "<td colspan=\"5\">$extra</td>\n" .
5063                       "</tr>\n";
5064         }
5065         print "</table>\n";
5068 sub git_log_body {
5069         # uses global variable $project
5070         my ($commitlist, $from, $to, $refs, $extra) = @_;
5072         $from = 0 unless defined $from;
5073         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5075         for (my $i = 0; $i <= $to; $i++) {
5076                 my %co = %{$commitlist->[$i]};
5077                 next if !%co;
5078                 my $commit = $co{'id'};
5079                 my $ref = format_ref_marker($refs, $commit);
5080                 git_print_header_div('commit',
5081                                "<span class=\"age\">$co{'age_string'}</span>" .
5082                                esc_html($co{'title'}) . $ref,
5083                                $commit);
5084                 print "<div class=\"title_text\">\n" .
5085                       "<div class=\"log_link\">\n" .
5086                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5087                       " | " .
5088                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5089                       " | " .
5090                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5091                       "<br/>\n" .
5092                       "</div>\n";
5093                       git_print_authorship(\%co, -tag => 'span');
5094                       print "<br/>\n</div>\n";
5096                 print "<div class=\"log_body\">\n";
5097                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5098                 print "</div>\n";
5099         }
5100         if ($extra) {
5101                 print "<div class=\"page_nav\">\n";
5102                 print "$extra\n";
5103                 print "</div>\n";
5104         }
5107 sub git_shortlog_body {
5108         # uses global variable $project
5109         my ($commitlist, $from, $to, $refs, $extra) = @_;
5111         $from = 0 unless defined $from;
5112         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5114         print "<table class=\"shortlog\">\n";
5115         my $alternate = 1;
5116         for (my $i = $from; $i <= $to; $i++) {
5117                 my %co = %{$commitlist->[$i]};
5118                 my $commit = $co{'id'};
5119                 my $ref = format_ref_marker($refs, $commit);
5120                 if ($alternate) {
5121                         print "<tr class=\"dark\">\n";
5122                 } else {
5123                         print "<tr class=\"light\">\n";
5124                 }
5125                 $alternate ^= 1;
5126                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5127                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5128                       format_author_html('td', \%co, 10) . "<td>";
5129                 print format_subject_html($co{'title'}, $co{'title_short'},
5130                                           href(action=>"commit", hash=>$commit), $ref);
5131                 print "</td>\n" .
5132                       "<td class=\"link\">" .
5133                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5134                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5135                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5136                 my $snapshot_links = format_snapshot_links($commit);
5137                 if (defined $snapshot_links) {
5138                         print " | " . $snapshot_links;
5139                 }
5140                 print "</td>\n" .
5141                       "</tr>\n";
5142         }
5143         if (defined $extra) {
5144                 print "<tr>\n" .
5145                       "<td colspan=\"4\">$extra</td>\n" .
5146                       "</tr>\n";
5147         }
5148         print "</table>\n";
5151 sub git_history_body {
5152         # Warning: assumes constant type (blob or tree) during history
5153         my ($commitlist, $from, $to, $refs, $extra,
5154             $file_name, $file_hash, $ftype) = @_;
5156         $from = 0 unless defined $from;
5157         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5159         print "<table class=\"history\">\n";
5160         my $alternate = 1;
5161         for (my $i = $from; $i <= $to; $i++) {
5162                 my %co = %{$commitlist->[$i]};
5163                 if (!%co) {
5164                         next;
5165                 }
5166                 my $commit = $co{'id'};
5168                 my $ref = format_ref_marker($refs, $commit);
5170                 if ($alternate) {
5171                         print "<tr class=\"dark\">\n";
5172                 } else {
5173                         print "<tr class=\"light\">\n";
5174                 }
5175                 $alternate ^= 1;
5176                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5177         # shortlog:   format_author_html('td', \%co, 10)
5178                       format_author_html('td', \%co, 15, 3) . "<td>";
5179                 # originally git_history used chop_str($co{'title'}, 50)
5180                 print format_subject_html($co{'title'}, $co{'title_short'},
5181                                           href(action=>"commit", hash=>$commit), $ref);
5182                 print "</td>\n" .
5183                       "<td class=\"link\">" .
5184                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5185                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5187                 if ($ftype eq 'blob') {
5188                         my $blob_current = $file_hash;
5189                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
5190                         if (defined $blob_current && defined $blob_parent &&
5191                                         $blob_current ne $blob_parent) {
5192                                 print " | " .
5193                                         $cgi->a({-href => href(action=>"blobdiff",
5194                                                                hash=>$blob_current, hash_parent=>$blob_parent,
5195                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
5196                                                                file_name=>$file_name)},
5197                                                 "diff to current");
5198                         }
5199                 }
5200                 print "</td>\n" .
5201                       "</tr>\n";
5202         }
5203         if (defined $extra) {
5204                 print "<tr>\n" .
5205                       "<td colspan=\"4\">$extra</td>\n" .
5206                       "</tr>\n";
5207         }
5208         print "</table>\n";
5211 sub git_tags_body {
5212         # uses global variable $project
5213         my ($taglist, $from, $to, $extra) = @_;
5214         $from = 0 unless defined $from;
5215         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5217         print "<table class=\"tags\">\n";
5218         my $alternate = 1;
5219         for (my $i = $from; $i <= $to; $i++) {
5220                 my $entry = $taglist->[$i];
5221                 my %tag = %$entry;
5222                 my $comment = $tag{'subject'};
5223                 my $comment_short;
5224                 if (defined $comment) {
5225                         $comment_short = chop_str($comment, 30, 5);
5226                 }
5227                 if ($alternate) {
5228                         print "<tr class=\"dark\">\n";
5229                 } else {
5230                         print "<tr class=\"light\">\n";
5231                 }
5232                 $alternate ^= 1;
5233                 if (defined $tag{'age'}) {
5234                         print "<td><i>$tag{'age'}</i></td>\n";
5235                 } else {
5236                         print "<td></td>\n";
5237                 }
5238                 print "<td>" .
5239                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5240                                -class => "list name"}, esc_html($tag{'name'})) .
5241                       "</td>\n" .
5242                       "<td>";
5243                 if (defined $comment) {
5244                         print format_subject_html($comment, $comment_short,
5245                                                   href(action=>"tag", hash=>$tag{'id'}));
5246                 }
5247                 print "</td>\n" .
5248                       "<td class=\"selflink\">";
5249                 if ($tag{'type'} eq "tag") {
5250                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5251                 } else {
5252                         print "&nbsp;";
5253                 }
5254                 print "</td>\n" .
5255                       "<td class=\"link\">" . " | " .
5256                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5257                 if ($tag{'reftype'} eq "commit") {
5258                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5259                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5260                 } elsif ($tag{'reftype'} eq "blob") {
5261                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5262                 }
5263                 print "</td>\n" .
5264                       "</tr>";
5265         }
5266         if (defined $extra) {
5267                 print "<tr>\n" .
5268                       "<td colspan=\"5\">$extra</td>\n" .
5269                       "</tr>\n";
5270         }
5271         print "</table>\n";
5274 sub git_heads_body {
5275         # uses global variable $project
5276         my ($headlist, $head, $from, $to, $extra) = @_;
5277         $from = 0 unless defined $from;
5278         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5280         print "<table class=\"heads\">\n";
5281         my $alternate = 1;
5282         for (my $i = $from; $i <= $to; $i++) {
5283                 my $entry = $headlist->[$i];
5284                 my %ref = %$entry;
5285                 my $curr = $ref{'id'} eq $head;
5286                 if ($alternate) {
5287                         print "<tr class=\"dark\">\n";
5288                 } else {
5289                         print "<tr class=\"light\">\n";
5290                 }
5291                 $alternate ^= 1;
5292                 print "<td><i>$ref{'age'}</i></td>\n" .
5293                       ($curr ? "<td class=\"current_head\">" : "<td>") .
5294                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5295                                -class => "list name"},esc_html($ref{'name'})) .
5296                       "</td>\n" .
5297                       "<td class=\"link\">" .
5298                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5299                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5300                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5301                       "</td>\n" .
5302                       "</tr>";
5303         }
5304         if (defined $extra) {
5305                 print "<tr>\n" .
5306                       "<td colspan=\"3\">$extra</td>\n" .
5307                       "</tr>\n";
5308         }
5309         print "</table>\n";
5312 # Display a single remote block
5313 sub git_remote_block {
5314         my ($remote, $rdata, $limit, $head) = @_;
5316         my $heads = $rdata->{'heads'};
5317         my $fetch = $rdata->{'fetch'};
5318         my $push = $rdata->{'push'};
5320         my $urls_table = "<table class=\"projects_list\">\n" ;
5322         if (defined $fetch) {
5323                 if ($fetch eq $push) {
5324                         $urls_table .= format_repo_url("URL", $fetch);
5325                 } else {
5326                         $urls_table .= format_repo_url("Fetch URL", $fetch);
5327                         $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5328                 }
5329         } elsif (defined $push) {
5330                 $urls_table .= format_repo_url("Push URL", $push);
5331         } else {
5332                 $urls_table .= format_repo_url("", "No remote URL");
5333         }
5335         $urls_table .= "</table>\n";
5337         my $dots;
5338         if (defined $limit && $limit < @$heads) {
5339                 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5340         }
5342         print $urls_table;
5343         git_heads_body($heads, $head, 0, $limit, $dots);
5346 # Display a list of remote names with the respective fetch and push URLs
5347 sub git_remotes_list {
5348         my ($remotedata, $limit) = @_;
5349         print "<table class=\"heads\">\n";
5350         my $alternate = 1;
5351         my @remotes = sort keys %$remotedata;
5353         my $limited = $limit && $limit < @remotes;
5355         $#remotes = $limit - 1 if $limited;
5357         while (my $remote = shift @remotes) {
5358                 my $rdata = $remotedata->{$remote};
5359                 my $fetch = $rdata->{'fetch'};
5360                 my $push = $rdata->{'push'};
5361                 if ($alternate) {
5362                         print "<tr class=\"dark\">\n";
5363                 } else {
5364                         print "<tr class=\"light\">\n";
5365                 }
5366                 $alternate ^= 1;
5367                 print "<td>" .
5368                       $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5369                                -class=> "list name"},esc_html($remote)) .
5370                       "</td>";
5371                 print "<td class=\"link\">" .
5372                       (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5373                       " | " .
5374                       (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5375                       "</td>";
5377                 print "</tr>\n";
5378         }
5380         if ($limited) {
5381                 print "<tr>\n" .
5382                       "<td colspan=\"3\">" .
5383                       $cgi->a({-href => href(action=>"remotes")}, "...") .
5384                       "</td>\n" . "</tr>\n";
5385         }
5387         print "</table>";
5390 # Display remote heads grouped by remote, unless there are too many
5391 # remotes, in which case we only display the remote names
5392 sub git_remotes_body {
5393         my ($remotedata, $limit, $head) = @_;
5394         if ($limit and $limit < keys %$remotedata) {
5395                 git_remotes_list($remotedata, $limit);
5396         } else {
5397                 fill_remote_heads($remotedata);
5398                 while (my ($remote, $rdata) = each %$remotedata) {
5399                         git_print_section({-class=>"remote", -id=>$remote},
5400                                 ["remotes", $remote, $remote], sub {
5401                                         git_remote_block($remote, $rdata, $limit, $head);
5402                                 });
5403                 }
5404         }
5407 sub git_search_grep_body {
5408         my ($commitlist, $from, $to, $extra) = @_;
5409         $from = 0 unless defined $from;
5410         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5412         print "<table class=\"commit_search\">\n";
5413         my $alternate = 1;
5414         for (my $i = $from; $i <= $to; $i++) {
5415                 my %co = %{$commitlist->[$i]};
5416                 if (!%co) {
5417                         next;
5418                 }
5419                 my $commit = $co{'id'};
5420                 if ($alternate) {
5421                         print "<tr class=\"dark\">\n";
5422                 } else {
5423                         print "<tr class=\"light\">\n";
5424                 }
5425                 $alternate ^= 1;
5426                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5427                       format_author_html('td', \%co, 15, 5) .
5428                       "<td>" .
5429                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5430                                -class => "list subject"},
5431                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5432                 my $comment = $co{'comment'};
5433                 foreach my $line (@$comment) {
5434                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
5435                                 my ($lead, $match, $trail) = ($1, $2, $3);
5436                                 $match = chop_str($match, 70, 5, 'center');
5437                                 my $contextlen = int((80 - length($match))/2);
5438                                 $contextlen = 30 if ($contextlen > 30);
5439                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
5440                                 $trail = chop_str($trail, $contextlen, 10, 'right');
5442                                 $lead  = esc_html($lead);
5443                                 $match = esc_html($match);
5444                                 $trail = esc_html($trail);
5446                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
5447                         }
5448                 }
5449                 print "</td>\n" .
5450                       "<td class=\"link\">" .
5451                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5452                       " | " .
5453                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
5454                       " | " .
5455                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5456                 print "</td>\n" .
5457                       "</tr>\n";
5458         }
5459         if (defined $extra) {
5460                 print "<tr>\n" .
5461                       "<td colspan=\"3\">$extra</td>\n" .
5462                       "</tr>\n";
5463         }
5464         print "</table>\n";
5467 ## ======================================================================
5468 ## ======================================================================
5469 ## actions
5471 sub git_project_list {
5472         my $order = $input_params{'order'};
5473         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5474                 die_error(400, "Unknown order parameter");
5475         }
5477         my @list = git_get_projects_list();
5478         if (!@list) {
5479                 die_error(404, "No projects found");
5480         }
5482         git_header_html();
5483         if (defined $home_text && -f $home_text) {
5484                 print "<div class=\"index_include\">\n";
5485                 insert_file($home_text);
5486                 print "</div>\n";
5487         }
5488         print $cgi->startform(-method => "get") .
5489               "<p class=\"projsearch\">Search:\n" .
5490               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5491               "</p>" .
5492               $cgi->end_form() . "\n";
5493         git_project_list_body(\@list, $order);
5494         git_footer_html();
5497 sub git_forks {
5498         my $order = $input_params{'order'};
5499         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5500                 die_error(400, "Unknown order parameter");
5501         }
5503         my @list = git_get_projects_list($project);
5504         if (!@list) {
5505                 die_error(404, "No forks found");
5506         }
5508         git_header_html();
5509         git_print_page_nav('','');
5510         git_print_header_div('summary', "$project forks");
5511         git_project_list_body(\@list, $order);
5512         git_footer_html();
5515 sub git_project_index {
5516         my @projects = git_get_projects_list();
5517         if (!@projects) {
5518                 die_error(404, "No projects found");
5519         }
5521         print $cgi->header(
5522                 -type => 'text/plain',
5523                 -charset => 'utf-8',
5524                 -content_disposition => 'inline; filename="index.aux"');
5526         foreach my $pr (@projects) {
5527                 if (!exists $pr->{'owner'}) {
5528                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5529                 }
5531                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5532                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5533                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5534                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5535                 $path  =~ s/ /\+/g;
5536                 $owner =~ s/ /\+/g;
5538                 print "$path $owner\n";
5539         }
5542 sub git_summary {
5543         my $descr = git_get_project_description($project) || "none";
5544         my %co = parse_commit("HEAD");
5545         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5546         my $head = $co{'id'};
5547         my $remote_heads = gitweb_check_feature('remote_heads');
5549         my $owner = git_get_project_owner($project);
5551         my $refs = git_get_references();
5552         # These get_*_list functions return one more to allow us to see if
5553         # there are more ...
5554         my @taglist  = git_get_tags_list(16);
5555         my @headlist = git_get_heads_list(16);
5556         my %remotedata = $remote_heads ? git_get_remotes_list() : ();
5557         my @forklist;
5558         my $check_forks = gitweb_check_feature('forks');
5560         if ($check_forks) {
5561                 # find forks of a project
5562                 @forklist = git_get_projects_list($project);
5563                 # filter out forks of forks
5564                 @forklist = filter_forks_from_projects_list(\@forklist)
5565                         if (@forklist);
5566         }
5568         git_header_html();
5569         git_print_page_nav('summary','', $head);
5571         print "<div class=\"title\">&nbsp;</div>\n";
5572         print "<table class=\"projects_list\">\n" .
5573               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5574               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5575         if (defined $cd{'rfc2822'}) {
5576                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5577         }
5579         # use per project git URL list in $projectroot/$project/cloneurl
5580         # or make project git URL from git base URL and project name
5581         my $url_tag = "URL";
5582         my @url_list = git_get_project_url_list($project);
5583         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5584         foreach my $git_url (@url_list) {
5585                 next unless $git_url;
5586                 print format_repo_url($url_tag, $git_url);
5587                 $url_tag = "";
5588         }
5590         # Tag cloud
5591         my $show_ctags = gitweb_check_feature('ctags');
5592         if ($show_ctags) {
5593                 my $ctags = git_get_project_ctags($project);
5594                 if (%$ctags) {
5595                         # without ability to add tags, don't show if there are none
5596                         my $cloud = git_populate_project_tagcloud($ctags);
5597                         print "<tr id=\"metadata_ctags\">" .
5598                               "<td>content tags</td>" .
5599                               "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
5600                               "</tr>\n";
5601                 }
5602         }
5604         print "</table>\n";
5606         # If XSS prevention is on, we don't include README.html.
5607         # TODO: Allow a readme in some safe format.
5608         if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5609                 print "<div class=\"title\">readme</div>\n" .
5610                       "<div class=\"readme\">\n";
5611                 insert_file("$projectroot/$project/README.html");
5612                 print "\n</div>\n"; # class="readme"
5613         }
5615         # we need to request one more than 16 (0..15) to check if
5616         # those 16 are all
5617         my @commitlist = $head ? parse_commits($head, 17) : ();
5618         if (@commitlist) {
5619                 git_print_header_div('shortlog');
5620                 git_shortlog_body(\@commitlist, 0, 15, $refs,
5621                                   $#commitlist <=  15 ? undef :
5622                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
5623         }
5625         if (@taglist) {
5626                 git_print_header_div('tags');
5627                 git_tags_body(\@taglist, 0, 15,
5628                               $#taglist <=  15 ? undef :
5629                               $cgi->a({-href => href(action=>"tags")}, "..."));
5630         }
5632         if (@headlist) {
5633                 git_print_header_div('heads');
5634                 git_heads_body(\@headlist, $head, 0, 15,
5635                                $#headlist <= 15 ? undef :
5636                                $cgi->a({-href => href(action=>"heads")}, "..."));
5637         }
5639         if (%remotedata) {
5640                 git_print_header_div('remotes');
5641                 git_remotes_body(\%remotedata, 15, $head);
5642         }
5644         if (@forklist) {
5645                 git_print_header_div('forks');
5646                 git_project_list_body(\@forklist, 'age', 0, 15,
5647                                       $#forklist <= 15 ? undef :
5648                                       $cgi->a({-href => href(action=>"forks")}, "..."),
5649                                       'no_header');
5650         }
5652         git_footer_html();
5655 sub git_tag {
5656         my %tag = parse_tag($hash);
5658         if (! %tag) {
5659                 die_error(404, "Unknown tag object");
5660         }
5662         my $head = git_get_head_hash($project);
5663         git_header_html();
5664         git_print_page_nav('','', $head,undef,$head);
5665         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5666         print "<div class=\"title_text\">\n" .
5667               "<table class=\"object_header\">\n" .
5668               "<tr>\n" .
5669               "<td>object</td>\n" .
5670               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5671                                $tag{'object'}) . "</td>\n" .
5672               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5673                                               $tag{'type'}) . "</td>\n" .
5674               "</tr>\n";
5675         if (defined($tag{'author'})) {
5676                 git_print_authorship_rows(\%tag, 'author');
5677         }
5678         print "</table>\n\n" .
5679               "</div>\n";
5680         print "<div class=\"page_body\">";
5681         my $comment = $tag{'comment'};
5682         foreach my $line (@$comment) {
5683                 chomp $line;
5684                 print esc_html($line, -nbsp=>1) . "<br/>\n";
5685         }
5686         print "</div>\n";
5687         git_footer_html();
5690 sub git_blame_common {
5691         my $format = shift || 'porcelain';
5692         if ($format eq 'porcelain' && $cgi->param('js')) {
5693                 $format = 'incremental';
5694                 $action = 'blame_incremental'; # for page title etc
5695         }
5697         # permissions
5698         gitweb_check_feature('blame')
5699                 or die_error(403, "Blame view not allowed");
5701         # error checking
5702         die_error(400, "No file name given") unless $file_name;
5703         $hash_base ||= git_get_head_hash($project);
5704         die_error(404, "Couldn't find base commit") unless $hash_base;
5705         my %co = parse_commit($hash_base)
5706                 or die_error(404, "Commit not found");
5707         my $ftype = "blob";
5708         if (!defined $hash) {
5709                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5710                         or die_error(404, "Error looking up file");
5711         } else {
5712                 $ftype = git_get_type($hash);
5713                 if ($ftype !~ "blob") {
5714                         die_error(400, "Object is not a blob");
5715                 }
5716         }
5718         my $fd;
5719         if ($format eq 'incremental') {
5720                 # get file contents (as base)
5721                 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5722                         or die_error(500, "Open git-cat-file failed");
5723         } elsif ($format eq 'data') {
5724                 # run git-blame --incremental
5725                 open $fd, "-|", git_cmd(), "blame", "--incremental",
5726                         $hash_base, "--", $file_name
5727                         or die_error(500, "Open git-blame --incremental failed");
5728         } else {
5729                 # run git-blame --porcelain
5730                 open $fd, "-|", git_cmd(), "blame", '-p',
5731                         $hash_base, '--', $file_name
5732                         or die_error(500, "Open git-blame --porcelain failed");
5733         }
5735         # incremental blame data returns early
5736         if ($format eq 'data') {
5737                 print $cgi->header(
5738                         -type=>"text/plain", -charset => "utf-8",
5739                         -status=> "200 OK");
5740                 local $| = 1; # output autoflush
5741                 print while <$fd>;
5742                 close $fd
5743                         or print "ERROR $!\n";
5745                 print 'END';
5746                 if (defined $t0 && gitweb_check_feature('timed')) {
5747                         print ' '.
5748                               tv_interval($t0, [ gettimeofday() ]).
5749                               ' '.$number_of_git_cmds;
5750                 }
5751                 print "\n";
5753                 return;
5754         }
5756         # page header
5757         git_header_html();
5758         my $formats_nav =
5759                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5760                         "blob") .
5761                 " | ";
5762         if ($format eq 'incremental') {
5763                 $formats_nav .=
5764                         $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5765                                 "blame") . " (non-incremental)";
5766         } else {
5767                 $formats_nav .=
5768                         $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5769                                 "blame") . " (incremental)";
5770         }
5771         $formats_nav .=
5772                 " | " .
5773                 $cgi->a({-href => href(action=>"history", -replay=>1)},
5774                         "history") .
5775                 " | " .
5776                 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5777                         "HEAD");
5778         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5779         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5780         git_print_page_path($file_name, $ftype, $hash_base);
5782         # page body
5783         if ($format eq 'incremental') {
5784                 print "<noscript>\n<div class=\"error\"><center><b>\n".
5785                       "This page requires JavaScript to run.\n Use ".
5786                       $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5787                               'this page').
5788                       " instead.\n".
5789                       "</b></center></div>\n</noscript>\n";
5791                 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5792         }
5794         print qq!<div class="page_body">\n!;
5795         print qq!<div id="progress_info">... / ...</div>\n!
5796                 if ($format eq 'incremental');
5797         print qq!<table id="blame_table" class="blame" width="100%">\n!.
5798               #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5799               qq!<thead>\n!.
5800               qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5801               qq!</thead>\n!.
5802               qq!<tbody>\n!;
5804         my @rev_color = qw(light dark);
5805         my $num_colors = scalar(@rev_color);
5806         my $current_color = 0;
5808         if ($format eq 'incremental') {
5809                 my $color_class = $rev_color[$current_color];
5811                 #contents of a file
5812                 my $linenr = 0;
5813         LINE:
5814                 while (my $line = <$fd>) {
5815                         chomp $line;
5816                         $linenr++;
5818                         print qq!<tr id="l$linenr" class="$color_class">!.
5819                               qq!<td class="sha1"><a href=""> </a></td>!.
5820                               qq!<td class="linenr">!.
5821                               qq!<a class="linenr" href="">$linenr</a></td>!;
5822                         print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5823                         print qq!</tr>\n!;
5824                 }
5826         } else { # porcelain, i.e. ordinary blame
5827                 my %metainfo = (); # saves information about commits
5829                 # blame data
5830         LINE:
5831                 while (my $line = <$fd>) {
5832                         chomp $line;
5833                         # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5834                         # no <lines in group> for subsequent lines in group of lines
5835                         my ($full_rev, $orig_lineno, $lineno, $group_size) =
5836                            ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5837                         if (!exists $metainfo{$full_rev}) {
5838                                 $metainfo{$full_rev} = { 'nprevious' => 0 };
5839                         }
5840                         my $meta = $metainfo{$full_rev};
5841                         my $data;
5842                         while ($data = <$fd>) {
5843                                 chomp $data;
5844                                 last if ($data =~ s/^\t//); # contents of line
5845                                 if ($data =~ /^(\S+)(?: (.*))?$/) {
5846                                         $meta->{$1} = $2 unless exists $meta->{$1};
5847                                 }
5848                                 if ($data =~ /^previous /) {
5849                                         $meta->{'nprevious'}++;
5850                                 }
5851                         }
5852                         my $short_rev = substr($full_rev, 0, 8);
5853                         my $author = $meta->{'author'};
5854                         my %date =
5855                                 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5856                         my $date = $date{'iso-tz'};
5857                         if ($group_size) {
5858                                 $current_color = ($current_color + 1) % $num_colors;
5859                         }
5860                         my $tr_class = $rev_color[$current_color];
5861                         $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5862                         $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5863                         $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5864                         print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5865                         if ($group_size) {
5866                                 print "<td class=\"sha1\"";
5867                                 print " title=\"". esc_html($author) . ", $date\"";
5868                                 print " rowspan=\"$group_size\"" if ($group_size > 1);
5869                                 print ">";
5870                                 print $cgi->a({-href => href(action=>"commit",
5871                                                              hash=>$full_rev,
5872                                                              file_name=>$file_name)},
5873                                               esc_html($short_rev));
5874                                 if ($group_size >= 2) {
5875                                         my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5876                                         if (@author_initials) {
5877                                                 print "<br />" .
5878                                                       esc_html(join('', @author_initials));
5879                                                 #           or join('.', ...)
5880                                         }
5881                                 }
5882                                 print "</td>\n";
5883                         }
5884                         # 'previous' <sha1 of parent commit> <filename at commit>
5885                         if (exists $meta->{'previous'} &&
5886                             $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5887                                 $meta->{'parent'} = $1;
5888                                 $meta->{'file_parent'} = unquote($2);
5889                         }
5890                         my $linenr_commit =
5891                                 exists($meta->{'parent'}) ?
5892                                 $meta->{'parent'} : $full_rev;
5893                         my $linenr_filename =
5894                                 exists($meta->{'file_parent'}) ?
5895                                 $meta->{'file_parent'} : unquote($meta->{'filename'});
5896                         my $blamed = href(action => 'blame',
5897                                           file_name => $linenr_filename,
5898                                           hash_base => $linenr_commit);
5899                         print "<td class=\"linenr\">";
5900                         print $cgi->a({ -href => "$blamed#l$orig_lineno",
5901                                         -class => "linenr" },
5902                                       esc_html($lineno));
5903                         print "</td>";
5904                         print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5905                         print "</tr>\n";
5906                 } # end while
5908         }
5910         # footer
5911         print "</tbody>\n".
5912               "</table>\n"; # class="blame"
5913         print "</div>\n";   # class="blame_body"
5914         close $fd
5915                 or print "Reading blob failed\n";
5917         git_footer_html();
5920 sub git_blame {
5921         git_blame_common();
5924 sub git_blame_incremental {
5925         git_blame_common('incremental');
5928 sub git_blame_data {
5929         git_blame_common('data');
5932 sub git_tags {
5933         my $head = git_get_head_hash($project);
5934         git_header_html();
5935         git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
5936         git_print_header_div('summary', $project);
5938         my @tagslist = git_get_tags_list();
5939         if (@tagslist) {
5940                 git_tags_body(\@tagslist);
5941         }
5942         git_footer_html();
5945 sub git_heads {
5946         my $head = git_get_head_hash($project);
5947         git_header_html();
5948         git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
5949         git_print_header_div('summary', $project);
5951         my @headslist = git_get_heads_list();
5952         if (@headslist) {
5953                 git_heads_body(\@headslist, $head);
5954         }
5955         git_footer_html();
5958 # used both for single remote view and for list of all the remotes
5959 sub git_remotes {
5960         gitweb_check_feature('remote_heads')
5961                 or die_error(403, "Remote heads view is disabled");
5963         my $head = git_get_head_hash($project);
5964         my $remote = $input_params{'hash'};
5966         my $remotedata = git_get_remotes_list($remote);
5967         die_error(500, "Unable to get remote information") unless defined $remotedata;
5969         unless (%$remotedata) {
5970                 die_error(404, defined $remote ?
5971                         "Remote $remote not found" :
5972                         "No remotes found");
5973         }
5975         git_header_html(undef, undef, -action_extra => $remote);
5976         git_print_page_nav('', '',  $head, undef, $head,
5977                 format_ref_views($remote ? '' : 'remotes'));
5979         fill_remote_heads($remotedata);
5980         if (defined $remote) {
5981                 git_print_header_div('remotes', "$remote remote for $project");
5982                 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
5983         } else {
5984                 git_print_header_div('summary', "$project remotes");
5985                 git_remotes_body($remotedata, undef, $head);
5986         }
5988         git_footer_html();
5991 sub git_blob_plain {
5992         my $type = shift;
5993         my $expires;
5995         if (!defined $hash) {
5996                 if (defined $file_name) {
5997                         my $base = $hash_base || git_get_head_hash($project);
5998                         $hash = git_get_hash_by_path($base, $file_name, "blob")
5999                                 or die_error(404, "Cannot find file");
6000                 } else {
6001                         die_error(400, "No file name defined");
6002                 }
6003         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6004                 # blobs defined by non-textual hash id's can be cached
6005                 $expires = "+1d";
6006         }
6008         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6009                 or die_error(500, "Open git-cat-file blob '$hash' failed");
6011         # content-type (can include charset)
6012         $type = blob_contenttype($fd, $file_name, $type);
6014         # "save as" filename, even when no $file_name is given
6015         my $save_as = "$hash";
6016         if (defined $file_name) {
6017                 $save_as = $file_name;
6018         } elsif ($type =~ m/^text\//) {
6019                 $save_as .= '.txt';
6020         }
6022         # With XSS prevention on, blobs of all types except a few known safe
6023         # ones are served with "Content-Disposition: attachment" to make sure
6024         # they don't run in our security domain.  For certain image types,
6025         # blob view writes an <img> tag referring to blob_plain view, and we
6026         # want to be sure not to break that by serving the image as an
6027         # attachment (though Firefox 3 doesn't seem to care).
6028         my $sandbox = $prevent_xss &&
6029                 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
6031         print $cgi->header(
6032                 -type => $type,
6033                 -expires => $expires,
6034                 -content_disposition =>
6035                         ($sandbox ? 'attachment' : 'inline')
6036                         . '; filename="' . $save_as . '"');
6037         local $/ = undef;
6038         binmode STDOUT, ':raw';
6039         print <$fd>;
6040         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6041         close $fd;
6044 sub git_blob {
6045         my $expires;
6047         if (!defined $hash) {
6048                 if (defined $file_name) {
6049                         my $base = $hash_base || git_get_head_hash($project);
6050                         $hash = git_get_hash_by_path($base, $file_name, "blob")
6051                                 or die_error(404, "Cannot find file");
6052                 } else {
6053                         die_error(400, "No file name defined");
6054                 }
6055         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6056                 # blobs defined by non-textual hash id's can be cached
6057                 $expires = "+1d";
6058         }
6060         my $have_blame = gitweb_check_feature('blame');
6061         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6062                 or die_error(500, "Couldn't cat $file_name, $hash");
6063         my $mimetype = blob_mimetype($fd, $file_name);
6064         # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6065         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6066                 close $fd;
6067                 return git_blob_plain($mimetype);
6068         }
6069         # we can have blame only for text/* mimetype
6070         $have_blame &&= ($mimetype =~ m!^text/!);
6072         my $highlight = gitweb_check_feature('highlight');
6073         my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6074         $fd = run_highlighter($fd, $highlight, $syntax)
6075                 if $syntax;
6077         git_header_html(undef, $expires);
6078         my $formats_nav = '';
6079         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6080                 if (defined $file_name) {
6081                         if ($have_blame) {
6082                                 $formats_nav .=
6083                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
6084                                                 "blame") .
6085                                         " | ";
6086                         }
6087                         $formats_nav .=
6088                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6089                                         "history") .
6090                                 " | " .
6091                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6092                                         "raw") .
6093                                 " | " .
6094                                 $cgi->a({-href => href(action=>"blob",
6095                                                        hash_base=>"HEAD", file_name=>$file_name)},
6096                                         "HEAD");
6097                 } else {
6098                         $formats_nav .=
6099                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6100                                         "raw");
6101                 }
6102                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6103                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6104         } else {
6105                 print "<div class=\"page_nav\">\n" .
6106                       "<br/><br/></div>\n" .
6107                       "<div class=\"title\">".esc_html($hash)."</div>\n";
6108         }
6109         git_print_page_path($file_name, "blob", $hash_base);
6110         print "<div class=\"page_body\">\n";
6111         if ($mimetype =~ m!^image/!) {
6112                 print qq!<img type="!.esc_attr($mimetype).qq!"!;
6113                 if ($file_name) {
6114                         print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6115                 }
6116                 print qq! src="! .
6117                       href(action=>"blob_plain", hash=>$hash,
6118                            hash_base=>$hash_base, file_name=>$file_name) .
6119                       qq!" />\n!;
6120         } else {
6121                 my $nr;
6122                 while (my $line = <$fd>) {
6123                         chomp $line;
6124                         $nr++;
6125                         $line = untabify($line);
6126                         printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6127                                $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
6128                 }
6129         }
6130         close $fd
6131                 or print "Reading blob failed.\n";
6132         print "</div>";
6133         git_footer_html();
6136 sub git_tree {
6137         if (!defined $hash_base) {
6138                 $hash_base = "HEAD";
6139         }
6140         if (!defined $hash) {
6141                 if (defined $file_name) {
6142                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6143                 } else {
6144                         $hash = $hash_base;
6145                 }
6146         }
6147         die_error(404, "No such tree") unless defined($hash);
6149         my $show_sizes = gitweb_check_feature('show-sizes');
6150         my $have_blame = gitweb_check_feature('blame');
6152         my @entries = ();
6153         {
6154                 local $/ = "\0";
6155                 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6156                         ($show_sizes ? '-l' : ()), @extra_options, $hash
6157                         or die_error(500, "Open git-ls-tree failed");
6158                 @entries = map { chomp; $_ } <$fd>;
6159                 close $fd
6160                         or die_error(404, "Reading tree failed");
6161         }
6163         my $refs = git_get_references();
6164         my $ref = format_ref_marker($refs, $hash_base);
6165         git_header_html();
6166         my $basedir = '';
6167         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6168                 my @views_nav = ();
6169                 if (defined $file_name) {
6170                         push @views_nav,
6171                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6172                                         "history"),
6173                                 $cgi->a({-href => href(action=>"tree",
6174                                                        hash_base=>"HEAD", file_name=>$file_name)},
6175                                         "HEAD"),
6176                 }
6177                 my $snapshot_links = format_snapshot_links($hash);
6178                 if (defined $snapshot_links) {
6179                         # FIXME: Should be available when we have no hash base as well.
6180                         push @views_nav, $snapshot_links;
6181                 }
6182                 git_print_page_nav('tree','', $hash_base, undef, undef,
6183                                    join(' | ', @views_nav));
6184                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
6185         } else {
6186                 undef $hash_base;
6187                 print "<div class=\"page_nav\">\n";
6188                 print "<br/><br/></div>\n";
6189                 print "<div class=\"title\">".esc_html($hash)."</div>\n";
6190         }
6191         if (defined $file_name) {
6192                 $basedir = $file_name;
6193                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
6194                         $basedir .= '/';
6195                 }
6196                 git_print_page_path($file_name, 'tree', $hash_base);
6197         }
6198         print "<div class=\"page_body\">\n";
6199         print "<table class=\"tree\">\n";
6200         my $alternate = 1;
6201         # '..' (top directory) link if possible
6202         if (defined $hash_base &&
6203             defined $file_name && $file_name =~ m![^/]+$!) {
6204                 if ($alternate) {
6205                         print "<tr class=\"dark\">\n";
6206                 } else {
6207                         print "<tr class=\"light\">\n";
6208                 }
6209                 $alternate ^= 1;
6211                 my $up = $file_name;
6212                 $up =~ s!/?[^/]+$!!;
6213                 undef $up unless $up;
6214                 # based on git_print_tree_entry
6215                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
6216                 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
6217                 print '<td class="list">';
6218                 print $cgi->a({-href => href(action=>"tree",
6219                                              hash_base=>$hash_base,
6220                                              file_name=>$up)},
6221                               "..");
6222                 print "</td>\n";
6223                 print "<td class=\"link\"></td>\n";
6225                 print "</tr>\n";
6226         }
6227         foreach my $line (@entries) {
6228                 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
6230                 if ($alternate) {
6231                         print "<tr class=\"dark\">\n";
6232                 } else {
6233                         print "<tr class=\"light\">\n";
6234                 }
6235                 $alternate ^= 1;
6237                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
6239                 print "</tr>\n";
6240         }
6241         print "</table>\n" .
6242               "</div>";
6243         git_footer_html();
6246 sub snapshot_name {
6247         my ($project, $hash) = @_;
6249         # path/to/project.git  -> project
6250         # path/to/project/.git -> project
6251         my $name = to_utf8($project);
6252         $name =~ s,([^/])/*\.git$,$1,;
6253         $name = basename($name);
6254         # sanitize name
6255         $name =~ s/[[:cntrl:]]/?/g;
6257         my $ver = $hash;
6258         if ($hash =~ /^[0-9a-fA-F]+$/) {
6259                 # shorten SHA-1 hash
6260                 my $full_hash = git_get_full_hash($project, $hash);
6261                 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6262                         $ver = git_get_short_hash($project, $hash);
6263                 }
6264         } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6265                 # tags don't need shortened SHA-1 hash
6266                 $ver = $1;
6267         } else {
6268                 # branches and other need shortened SHA-1 hash
6269                 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
6270                         $ver = $1;
6271                 }
6272                 $ver .= '-' . git_get_short_hash($project, $hash);
6273         }
6274         # in case of hierarchical branch names
6275         $ver =~ s!/!.!g;
6277         # name = project-version_string
6278         $name = "$name-$ver";
6280         return wantarray ? ($name, $name) : $name;
6283 sub git_snapshot {
6284         my $format = $input_params{'snapshot_format'};
6285         if (!@snapshot_fmts) {
6286                 die_error(403, "Snapshots not allowed");
6287         }
6288         # default to first supported snapshot format
6289         $format ||= $snapshot_fmts[0];
6290         if ($format !~ m/^[a-z0-9]+$/) {
6291                 die_error(400, "Invalid snapshot format parameter");
6292         } elsif (!exists($known_snapshot_formats{$format})) {
6293                 die_error(400, "Unknown snapshot format");
6294         } elsif ($known_snapshot_formats{$format}{'disabled'}) {
6295                 die_error(403, "Snapshot format not allowed");
6296         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
6297                 die_error(403, "Unsupported snapshot format");
6298         }
6300         my $type = git_get_type("$hash^{}");
6301         if (!$type) {
6302                 die_error(404, 'Object does not exist');
6303         }  elsif ($type eq 'blob') {
6304                 die_error(400, 'Object is not a tree-ish');
6305         }
6307         my ($name, $prefix) = snapshot_name($project, $hash);
6308         my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
6309         my $cmd = quote_command(
6310                 git_cmd(), 'archive',
6311                 "--format=$known_snapshot_formats{$format}{'format'}",
6312                 "--prefix=$prefix/", $hash);
6313         if (exists $known_snapshot_formats{$format}{'compressor'}) {
6314                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
6315         }
6317         $filename =~ s/(["\\])/\\$1/g;
6318         print $cgi->header(
6319                 -type => $known_snapshot_formats{$format}{'type'},
6320                 -content_disposition => 'inline; filename="' . $filename . '"',
6321                 -status => '200 OK');
6323         open my $fd, "-|", $cmd
6324                 or die_error(500, "Execute git-archive failed");
6325         binmode STDOUT, ':raw';
6326         print <$fd>;
6327         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6328         close $fd;
6331 sub git_log_generic {
6332         my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
6334         my $head = git_get_head_hash($project);
6335         if (!defined $base) {
6336                 $base = $head;
6337         }
6338         if (!defined $page) {
6339                 $page = 0;
6340         }
6341         my $refs = git_get_references();
6343         my $commit_hash = $base;
6344         if (defined $parent) {
6345                 $commit_hash = "$parent..$base";
6346         }
6347         my @commitlist =
6348                 parse_commits($commit_hash, 101, (100 * $page),
6349                               defined $file_name ? ($file_name, "--full-history") : ());
6351         my $ftype;
6352         if (!defined $file_hash && defined $file_name) {
6353                 # some commits could have deleted file in question,
6354                 # and not have it in tree, but one of them has to have it
6355                 for (my $i = 0; $i < @commitlist; $i++) {
6356                         $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
6357                         last if defined $file_hash;
6358                 }
6359         }
6360         if (defined $file_hash) {
6361                 $ftype = git_get_type($file_hash);
6362         }
6363         if (defined $file_name && !defined $ftype) {
6364                 die_error(500, "Unknown type of object");
6365         }
6366         my %co;
6367         if (defined $file_name) {
6368                 %co = parse_commit($base)
6369                         or die_error(404, "Unknown commit object");
6370         }
6373         my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
6374         my $next_link = '';
6375         if ($#commitlist >= 100) {
6376                 $next_link =
6377                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
6378                                  -accesskey => "n", -title => "Alt-n"}, "next");
6379         }
6380         my $patch_max = gitweb_get_feature('patches');
6381         if ($patch_max && !defined $file_name) {
6382                 if ($patch_max < 0 || @commitlist <= $patch_max) {
6383                         $paging_nav .= " &sdot; " .
6384                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6385                                         "patches");
6386                 }
6387         }
6389         git_header_html();
6390         git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
6391         if (defined $file_name) {
6392                 git_print_header_div('commit', esc_html($co{'title'}), $base);
6393         } else {
6394                 git_print_header_div('summary', $project)
6395         }
6396         git_print_page_path($file_name, $ftype, $hash_base)
6397                 if (defined $file_name);
6399         $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
6400                      $file_name, $file_hash, $ftype);
6402         git_footer_html();
6405 sub git_log {
6406         git_log_generic('log', \&git_log_body,
6407                         $hash, $hash_parent);
6410 sub git_commit {
6411         $hash ||= $hash_base || "HEAD";
6412         my %co = parse_commit($hash)
6413             or die_error(404, "Unknown commit object");
6415         my $parent  = $co{'parent'};
6416         my $parents = $co{'parents'}; # listref
6418         # we need to prepare $formats_nav before any parameter munging
6419         my $formats_nav;
6420         if (!defined $parent) {
6421                 # --root commitdiff
6422                 $formats_nav .= '(initial)';
6423         } elsif (@$parents == 1) {
6424                 # single parent commit
6425                 $formats_nav .=
6426                         '(parent: ' .
6427                         $cgi->a({-href => href(action=>"commit",
6428                                                hash=>$parent)},
6429                                 esc_html(substr($parent, 0, 7))) .
6430                         ')';
6431         } else {
6432                 # merge commit
6433                 $formats_nav .=
6434                         '(merge: ' .
6435                         join(' ', map {
6436                                 $cgi->a({-href => href(action=>"commit",
6437                                                        hash=>$_)},
6438                                         esc_html(substr($_, 0, 7)));
6439                         } @$parents ) .
6440                         ')';
6441         }
6442         if (gitweb_check_feature('patches') && @$parents <= 1) {
6443                 $formats_nav .= " | " .
6444                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
6445                                 "patch");
6446         }
6448         if (!defined $parent) {
6449                 $parent = "--root";
6450         }
6451         my @difftree;
6452         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
6453                 @diff_opts,
6454                 (@$parents <= 1 ? $parent : '-c'),
6455                 $hash, "--"
6456                 or die_error(500, "Open git-diff-tree failed");
6457         @difftree = map { chomp; $_ } <$fd>;
6458         close $fd or die_error(404, "Reading git-diff-tree failed");
6460         # non-textual hash id's can be cached
6461         my $expires;
6462         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6463                 $expires = "+1d";
6464         }
6465         my $refs = git_get_references();
6466         my $ref = format_ref_marker($refs, $co{'id'});
6468         git_header_html(undef, $expires);
6469         git_print_page_nav('commit', '',
6470                            $hash, $co{'tree'}, $hash,
6471                            $formats_nav);
6473         if (defined $co{'parent'}) {
6474                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
6475         } else {
6476                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
6477         }
6478         print "<div class=\"title_text\">\n" .
6479               "<table class=\"object_header\">\n";
6480         git_print_authorship_rows(\%co);
6481         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
6482         print "<tr>" .
6483               "<td>tree</td>" .
6484               "<td class=\"sha1\">" .
6485               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
6486                        class => "list"}, $co{'tree'}) .
6487               "</td>" .
6488               "<td class=\"link\">" .
6489               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
6490                       "tree");
6491         my $snapshot_links = format_snapshot_links($hash);
6492         if (defined $snapshot_links) {
6493                 print " | " . $snapshot_links;
6494         }
6495         print "</td>" .
6496               "</tr>\n";
6498         foreach my $par (@$parents) {
6499                 print "<tr>" .
6500                       "<td>parent</td>" .
6501                       "<td class=\"sha1\">" .
6502                       $cgi->a({-href => href(action=>"commit", hash=>$par),
6503                                class => "list"}, $par) .
6504                       "</td>" .
6505                       "<td class=\"link\">" .
6506                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
6507                       " | " .
6508                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
6509                       "</td>" .
6510                       "</tr>\n";
6511         }
6512         print "</table>".
6513               "</div>\n";
6515         print "<div class=\"page_body\">\n";
6516         git_print_log($co{'comment'});
6517         print "</div>\n";
6519         git_difftree_body(\@difftree, $hash, @$parents);
6521         git_footer_html();
6524 sub git_object {
6525         # object is defined by:
6526         # - hash or hash_base alone
6527         # - hash_base and file_name
6528         my $type;
6530         # - hash or hash_base alone
6531         if ($hash || ($hash_base && !defined $file_name)) {
6532                 my $object_id = $hash || $hash_base;
6534                 open my $fd, "-|", quote_command(
6535                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
6536                         or die_error(404, "Object does not exist");
6537                 $type = <$fd>;
6538                 chomp $type;
6539                 close $fd
6540                         or die_error(404, "Object does not exist");
6542         # - hash_base and file_name
6543         } elsif ($hash_base && defined $file_name) {
6544                 $file_name =~ s,/+$,,;
6546                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
6547                         or die_error(404, "Base object does not exist");
6549                 # here errors should not hapen
6550                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
6551                         or die_error(500, "Open git-ls-tree failed");
6552                 my $line = <$fd>;
6553                 close $fd;
6555                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
6556                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
6557                         die_error(404, "File or directory for given base does not exist");
6558                 }
6559                 $type = $2;
6560                 $hash = $3;
6561         } else {
6562                 die_error(400, "Not enough information to find object");
6563         }
6565         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
6566                                           hash=>$hash, hash_base=>$hash_base,
6567                                           file_name=>$file_name),
6568                              -status => '302 Found');
6571 sub git_blobdiff {
6572         my $format = shift || 'html';
6574         my $fd;
6575         my @difftree;
6576         my %diffinfo;
6577         my $expires;
6579         # preparing $fd and %diffinfo for git_patchset_body
6580         # new style URI
6581         if (defined $hash_base && defined $hash_parent_base) {
6582                 if (defined $file_name) {
6583                         # read raw output
6584                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6585                                 $hash_parent_base, $hash_base,
6586                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
6587                                 or die_error(500, "Open git-diff-tree failed");
6588                         @difftree = map { chomp; $_ } <$fd>;
6589                         close $fd
6590                                 or die_error(404, "Reading git-diff-tree failed");
6591                         @difftree
6592                                 or die_error(404, "Blob diff not found");
6594                 } elsif (defined $hash &&
6595                          $hash =~ /[0-9a-fA-F]{40}/) {
6596                         # try to find filename from $hash
6598                         # read filtered raw output
6599                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6600                                 $hash_parent_base, $hash_base, "--"
6601                                 or die_error(500, "Open git-diff-tree failed");
6602                         @difftree =
6603                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
6604                                 # $hash == to_id
6605                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
6606                                 map { chomp; $_ } <$fd>;
6607                         close $fd
6608                                 or die_error(404, "Reading git-diff-tree failed");
6609                         @difftree
6610                                 or die_error(404, "Blob diff not found");
6612                 } else {
6613                         die_error(400, "Missing one of the blob diff parameters");
6614                 }
6616                 if (@difftree > 1) {
6617                         die_error(400, "Ambiguous blob diff specification");
6618                 }
6620                 %diffinfo = parse_difftree_raw_line($difftree[0]);
6621                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
6622                 $file_name   ||= $diffinfo{'to_file'};
6624                 $hash_parent ||= $diffinfo{'from_id'};
6625                 $hash        ||= $diffinfo{'to_id'};
6627                 # non-textual hash id's can be cached
6628                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6629                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6630                         $expires = '+1d';
6631                 }
6633                 # open patch output
6634                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6635                         '-p', ($format eq 'html' ? "--full-index" : ()),
6636                         $hash_parent_base, $hash_base,
6637                         "--", (defined $file_parent ? $file_parent : ()), $file_name
6638                         or die_error(500, "Open git-diff-tree failed");
6639         }
6641         # old/legacy style URI -- not generated anymore since 1.4.3.
6642         if (!%diffinfo) {
6643                 die_error('404 Not Found', "Missing one of the blob diff parameters")
6644         }
6646         # header
6647         if ($format eq 'html') {
6648                 my $formats_nav =
6649                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6650                                 "raw");
6651                 git_header_html(undef, $expires);
6652                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6653                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6654                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6655                 } else {
6656                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6657                         print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
6658                 }
6659                 if (defined $file_name) {
6660                         git_print_page_path($file_name, "blob", $hash_base);
6661                 } else {
6662                         print "<div class=\"page_path\"></div>\n";
6663                 }
6665         } elsif ($format eq 'plain') {
6666                 print $cgi->header(
6667                         -type => 'text/plain',
6668                         -charset => 'utf-8',
6669                         -expires => $expires,
6670                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6672                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6674         } else {
6675                 die_error(400, "Unknown blobdiff format");
6676         }
6678         # patch
6679         if ($format eq 'html') {
6680                 print "<div class=\"page_body\">\n";
6682                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6683                 close $fd;
6685                 print "</div>\n"; # class="page_body"
6686                 git_footer_html();
6688         } else {
6689                 while (my $line = <$fd>) {
6690                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6691                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6693                         print $line;
6695                         last if $line =~ m!^\+\+\+!;
6696                 }
6697                 local $/ = undef;
6698                 print <$fd>;
6699                 close $fd;
6700         }
6703 sub git_blobdiff_plain {
6704         git_blobdiff('plain');
6707 sub git_commitdiff {
6708         my %params = @_;
6709         my $format = $params{-format} || 'html';
6711         my ($patch_max) = gitweb_get_feature('patches');
6712         if ($format eq 'patch') {
6713                 die_error(403, "Patch view not allowed") unless $patch_max;
6714         }
6716         $hash ||= $hash_base || "HEAD";
6717         my %co = parse_commit($hash)
6718             or die_error(404, "Unknown commit object");
6720         # choose format for commitdiff for merge
6721         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6722                 $hash_parent = '--cc';
6723         }
6724         # we need to prepare $formats_nav before almost any parameter munging
6725         my $formats_nav;
6726         if ($format eq 'html') {
6727                 $formats_nav =
6728                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6729                                 "raw");
6730                 if ($patch_max && @{$co{'parents'}} <= 1) {
6731                         $formats_nav .= " | " .
6732                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6733                                         "patch");
6734                 }
6736                 if (defined $hash_parent &&
6737                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
6738                         # commitdiff with two commits given
6739                         my $hash_parent_short = $hash_parent;
6740                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6741                                 $hash_parent_short = substr($hash_parent, 0, 7);
6742                         }
6743                         $formats_nav .=
6744                                 ' (from';
6745                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6746                                 if ($co{'parents'}[$i] eq $hash_parent) {
6747                                         $formats_nav .= ' parent ' . ($i+1);
6748                                         last;
6749                                 }
6750                         }
6751                         $formats_nav .= ': ' .
6752                                 $cgi->a({-href => href(action=>"commitdiff",
6753                                                        hash=>$hash_parent)},
6754                                         esc_html($hash_parent_short)) .
6755                                 ')';
6756                 } elsif (!$co{'parent'}) {
6757                         # --root commitdiff
6758                         $formats_nav .= ' (initial)';
6759                 } elsif (scalar @{$co{'parents'}} == 1) {
6760                         # single parent commit
6761                         $formats_nav .=
6762                                 ' (parent: ' .
6763                                 $cgi->a({-href => href(action=>"commitdiff",
6764                                                        hash=>$co{'parent'})},
6765                                         esc_html(substr($co{'parent'}, 0, 7))) .
6766                                 ')';
6767                 } else {
6768                         # merge commit
6769                         if ($hash_parent eq '--cc') {
6770                                 $formats_nav .= ' | ' .
6771                                         $cgi->a({-href => href(action=>"commitdiff",
6772                                                                hash=>$hash, hash_parent=>'-c')},
6773                                                 'combined');
6774                         } else { # $hash_parent eq '-c'
6775                                 $formats_nav .= ' | ' .
6776                                         $cgi->a({-href => href(action=>"commitdiff",
6777                                                                hash=>$hash, hash_parent=>'--cc')},
6778                                                 'compact');
6779                         }
6780                         $formats_nav .=
6781                                 ' (merge: ' .
6782                                 join(' ', map {
6783                                         $cgi->a({-href => href(action=>"commitdiff",
6784                                                                hash=>$_)},
6785                                                 esc_html(substr($_, 0, 7)));
6786                                 } @{$co{'parents'}} ) .
6787                                 ')';
6788                 }
6789         }
6791         my $hash_parent_param = $hash_parent;
6792         if (!defined $hash_parent_param) {
6793                 # --cc for multiple parents, --root for parentless
6794                 $hash_parent_param =
6795                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6796         }
6798         # read commitdiff
6799         my $fd;
6800         my @difftree;
6801         if ($format eq 'html') {
6802                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6803                         "--no-commit-id", "--patch-with-raw", "--full-index",
6804                         $hash_parent_param, $hash, "--"
6805                         or die_error(500, "Open git-diff-tree failed");
6807                 while (my $line = <$fd>) {
6808                         chomp $line;
6809                         # empty line ends raw part of diff-tree output
6810                         last unless $line;
6811                         push @difftree, scalar parse_difftree_raw_line($line);
6812                 }
6814         } elsif ($format eq 'plain') {
6815                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6816                         '-p', $hash_parent_param, $hash, "--"
6817                         or die_error(500, "Open git-diff-tree failed");
6818         } elsif ($format eq 'patch') {
6819                 # For commit ranges, we limit the output to the number of
6820                 # patches specified in the 'patches' feature.
6821                 # For single commits, we limit the output to a single patch,
6822                 # diverging from the git-format-patch default.
6823                 my @commit_spec = ();
6824                 if ($hash_parent) {
6825                         if ($patch_max > 0) {
6826                                 push @commit_spec, "-$patch_max";
6827                         }
6828                         push @commit_spec, '-n', "$hash_parent..$hash";
6829                 } else {
6830                         if ($params{-single}) {
6831                                 push @commit_spec, '-1';
6832                         } else {
6833                                 if ($patch_max > 0) {
6834                                         push @commit_spec, "-$patch_max";
6835                                 }
6836                                 push @commit_spec, "-n";
6837                         }
6838                         push @commit_spec, '--root', $hash;
6839                 }
6840                 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
6841                         '--encoding=utf8', '--stdout', @commit_spec
6842                         or die_error(500, "Open git-format-patch failed");
6843         } else {
6844                 die_error(400, "Unknown commitdiff format");
6845         }
6847         # non-textual hash id's can be cached
6848         my $expires;
6849         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6850                 $expires = "+1d";
6851         }
6853         # write commit message
6854         if ($format eq 'html') {
6855                 my $refs = git_get_references();
6856                 my $ref = format_ref_marker($refs, $co{'id'});
6858                 git_header_html(undef, $expires);
6859                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6860                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6861                 print "<div class=\"title_text\">\n" .
6862                       "<table class=\"object_header\">\n";
6863                 git_print_authorship_rows(\%co);
6864                 print "</table>".
6865                       "</div>\n";
6866                 print "<div class=\"page_body\">\n";
6867                 if (@{$co{'comment'}} > 1) {
6868                         print "<div class=\"log\">\n";
6869                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6870                         print "</div>\n"; # class="log"
6871                 }
6873         } elsif ($format eq 'plain') {
6874                 my $refs = git_get_references("tags");
6875                 my $tagname = git_get_rev_name_tags($hash);
6876                 my $filename = basename($project) . "-$hash.patch";
6878                 print $cgi->header(
6879                         -type => 'text/plain',
6880                         -charset => 'utf-8',
6881                         -expires => $expires,
6882                         -content_disposition => 'inline; filename="' . "$filename" . '"');
6883                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6884                 print "From: " . to_utf8($co{'author'}) . "\n";
6885                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6886                 print "Subject: " . to_utf8($co{'title'}) . "\n";
6888                 print "X-Git-Tag: $tagname\n" if $tagname;
6889                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6891                 foreach my $line (@{$co{'comment'}}) {
6892                         print to_utf8($line) . "\n";
6893                 }
6894                 print "---\n\n";
6895         } elsif ($format eq 'patch') {
6896                 my $filename = basename($project) . "-$hash.patch";
6898                 print $cgi->header(
6899                         -type => 'text/plain',
6900                         -charset => 'utf-8',
6901                         -expires => $expires,
6902                         -content_disposition => 'inline; filename="' . "$filename" . '"');
6903         }
6905         # write patch
6906         if ($format eq 'html') {
6907                 my $use_parents = !defined $hash_parent ||
6908                         $hash_parent eq '-c' || $hash_parent eq '--cc';
6909                 git_difftree_body(\@difftree, $hash,
6910                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
6911                 print "<br/>\n";
6913                 git_patchset_body($fd, \@difftree, $hash,
6914                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
6915                 close $fd;
6916                 print "</div>\n"; # class="page_body"
6917                 git_footer_html();
6919         } elsif ($format eq 'plain') {
6920                 local $/ = undef;
6921                 print <$fd>;
6922                 close $fd
6923                         or print "Reading git-diff-tree failed\n";
6924         } elsif ($format eq 'patch') {
6925                 local $/ = undef;
6926                 print <$fd>;
6927                 close $fd
6928                         or print "Reading git-format-patch failed\n";
6929         }
6932 sub git_commitdiff_plain {
6933         git_commitdiff(-format => 'plain');
6936 # format-patch-style patches
6937 sub git_patch {
6938         git_commitdiff(-format => 'patch', -single => 1);
6941 sub git_patches {
6942         git_commitdiff(-format => 'patch');
6945 sub git_history {
6946         git_log_generic('history', \&git_history_body,
6947                         $hash_base, $hash_parent_base,
6948                         $file_name, $hash);
6951 sub git_search {
6952         gitweb_check_feature('search') or die_error(403, "Search is disabled");
6953         if (!defined $searchtext) {
6954                 die_error(400, "Text field is empty");
6955         }
6956         if (!defined $hash) {
6957                 $hash = git_get_head_hash($project);
6958         }
6959         my %co = parse_commit($hash);
6960         if (!%co) {
6961                 die_error(404, "Unknown commit object");
6962         }
6963         if (!defined $page) {
6964                 $page = 0;
6965         }
6967         $searchtype ||= 'commit';
6968         if ($searchtype eq 'pickaxe') {
6969                 # pickaxe may take all resources of your box and run for several minutes
6970                 # with every query - so decide by yourself how public you make this feature
6971                 gitweb_check_feature('pickaxe')
6972                     or die_error(403, "Pickaxe is disabled");
6973         }
6974         if ($searchtype eq 'grep') {
6975                 gitweb_check_feature('grep')
6976                     or die_error(403, "Grep is disabled");
6977         }
6979         git_header_html();
6981         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6982                 my $greptype;
6983                 if ($searchtype eq 'commit') {
6984                         $greptype = "--grep=";
6985                 } elsif ($searchtype eq 'author') {
6986                         $greptype = "--author=";
6987                 } elsif ($searchtype eq 'committer') {
6988                         $greptype = "--committer=";
6989                 }
6990                 $greptype .= $searchtext;
6991                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6992                                                $greptype, '--regexp-ignore-case',
6993                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6995                 my $paging_nav = '';
6996                 if ($page > 0) {
6997                         $paging_nav .=
6998                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
6999                                                        searchtext=>$searchtext,
7000                                                        searchtype=>$searchtype)},
7001                                         "first");
7002                         $paging_nav .= " &sdot; " .
7003                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
7004                                          -accesskey => "p", -title => "Alt-p"}, "prev");
7005                 } else {
7006                         $paging_nav .= "first";
7007                         $paging_nav .= " &sdot; prev";
7008                 }
7009                 my $next_link = '';
7010                 if ($#commitlist >= 100) {
7011                         $next_link =
7012                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7013                                          -accesskey => "n", -title => "Alt-n"}, "next");
7014                         $paging_nav .= " &sdot; $next_link";
7015                 } else {
7016                         $paging_nav .= " &sdot; next";
7017                 }
7019                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
7020                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7021                 if ($page == 0 && !@commitlist) {
7022                         print "<p>No match.</p>\n";
7023                 } else {
7024                         git_search_grep_body(\@commitlist, 0, 99, $next_link);
7025                 }
7026         }
7028         if ($searchtype eq 'pickaxe') {
7029                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7030                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7032                 print "<table class=\"pickaxe search\">\n";
7033                 my $alternate = 1;
7034                 local $/ = "\n";
7035                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
7036                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7037                         ($search_use_regexp ? '--pickaxe-regex' : ());
7038                 undef %co;
7039                 my @files;
7040                 while (my $line = <$fd>) {
7041                         chomp $line;
7042                         next unless $line;
7044                         my %set = parse_difftree_raw_line($line);
7045                         if (defined $set{'commit'}) {
7046                                 # finish previous commit
7047                                 if (%co) {
7048                                         print "</td>\n" .
7049                                               "<td class=\"link\">" .
7050                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7051                                               " | " .
7052                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7053                                         print "</td>\n" .
7054                                               "</tr>\n";
7055                                 }
7057                                 if ($alternate) {
7058                                         print "<tr class=\"dark\">\n";
7059                                 } else {
7060                                         print "<tr class=\"light\">\n";
7061                                 }
7062                                 $alternate ^= 1;
7063                                 %co = parse_commit($set{'commit'});
7064                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
7065                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7066                                       "<td><i>$author</i></td>\n" .
7067                                       "<td>" .
7068                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7069                                               -class => "list subject"},
7070                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
7071                         } elsif (defined $set{'to_id'}) {
7072                                 next if ($set{'to_id'} =~ m/^0{40}$/);
7074                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
7075                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
7076                                               -class => "list"},
7077                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
7078                                       "<br/>\n";
7079                         }
7080                 }
7081                 close $fd;
7083                 # finish last commit (warning: repetition!)
7084                 if (%co) {
7085                         print "</td>\n" .
7086                               "<td class=\"link\">" .
7087                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7088                               " | " .
7089                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7090                         print "</td>\n" .
7091                               "</tr>\n";
7092                 }
7094                 print "</table>\n";
7095         }
7097         if ($searchtype eq 'grep') {
7098                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7099                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7101                 print "<table class=\"grep_search\">\n";
7102                 my $alternate = 1;
7103                 my $matches = 0;
7104                 local $/ = "\n";
7105                 open my $fd, "-|", git_cmd(), 'grep', '-n',
7106                         $search_use_regexp ? ('-E', '-i') : '-F',
7107                         $searchtext, $co{'tree'};
7108                 my $lastfile = '';
7109                 while (my $line = <$fd>) {
7110                         chomp $line;
7111                         my ($file, $lno, $ltext, $binary);
7112                         last if ($matches++ > 1000);
7113                         if ($line =~ /^Binary file (.+) matches$/) {
7114                                 $file = $1;
7115                                 $binary = 1;
7116                         } else {
7117                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
7118                         }
7119                         if ($file ne $lastfile) {
7120                                 $lastfile and print "</td></tr>\n";
7121                                 if ($alternate++) {
7122                                         print "<tr class=\"dark\">\n";
7123                                 } else {
7124                                         print "<tr class=\"light\">\n";
7125                                 }
7126                                 print "<td class=\"list\">".
7127                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
7128                                                                file_name=>"$file"),
7129                                                 -class => "list"}, esc_path($file));
7130                                 print "</td><td>\n";
7131                                 $lastfile = $file;
7132                         }
7133                         if ($binary) {
7134                                 print "<div class=\"binary\">Binary file</div>\n";
7135                         } else {
7136                                 $ltext = untabify($ltext);
7137                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7138                                         $ltext = esc_html($1, -nbsp=>1);
7139                                         $ltext .= '<span class="match">';
7140                                         $ltext .= esc_html($2, -nbsp=>1);
7141                                         $ltext .= '</span>';
7142                                         $ltext .= esc_html($3, -nbsp=>1);
7143                                 } else {
7144                                         $ltext = esc_html($ltext, -nbsp=>1);
7145                                 }
7146                                 print "<div class=\"pre\">" .
7147                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
7148                                                                file_name=>"$file").'#l'.$lno,
7149                                                 -class => "linenr"}, sprintf('%4i', $lno))
7150                                         . ' ' .  $ltext . "</div>\n";
7151                         }
7152                 }
7153                 if ($lastfile) {
7154                         print "</td></tr>\n";
7155                         if ($matches > 1000) {
7156                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7157                         }
7158                 } else {
7159                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
7160                 }
7161                 close $fd;
7163                 print "</table>\n";
7164         }
7165         git_footer_html();
7168 sub git_search_help {
7169         git_header_html();
7170         git_print_page_nav('','', $hash,$hash,$hash);
7171         print <<EOT;
7172 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7173 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7174 the pattern entered is recognized as the POSIX extended
7175 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7176 insensitive).</p>
7177 <dl>
7178 <dt><b>commit</b></dt>
7179 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7180 EOT
7181         my $have_grep = gitweb_check_feature('grep');
7182         if ($have_grep) {
7183                 print <<EOT;
7184 <dt><b>grep</b></dt>
7185 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7186     a different one) are searched for the given pattern. On large trees, this search can take
7187 a while and put some strain on the server, so please use it with some consideration. Note that
7188 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7189 case-sensitive.</dd>
7190 EOT
7191         }
7192         print <<EOT;
7193 <dt><b>author</b></dt>
7194 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7195 <dt><b>committer</b></dt>
7196 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7197 EOT
7198         my $have_pickaxe = gitweb_check_feature('pickaxe');
7199         if ($have_pickaxe) {
7200                 print <<EOT;
7201 <dt><b>pickaxe</b></dt>
7202 <dd>All commits that caused the string to appear or disappear from any file (changes that
7203 added, removed or "modified" the string) will be listed. This search can take a while and
7204 takes a lot of strain on the server, so please use it wisely. Note that since you may be
7205 interested even in changes just changing the case as well, this search is case sensitive.</dd>
7206 EOT
7207         }
7208         print "</dl>\n";
7209         git_footer_html();
7212 sub git_shortlog {
7213         git_log_generic('shortlog', \&git_shortlog_body,
7214                         $hash, $hash_parent);
7217 ## ......................................................................
7218 ## feeds (RSS, Atom; OPML)
7220 sub git_feed {
7221         my $format = shift || 'atom';
7222         my $have_blame = gitweb_check_feature('blame');
7224         # Atom: http://www.atomenabled.org/developers/syndication/
7225         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7226         if ($format ne 'rss' && $format ne 'atom') {
7227                 die_error(400, "Unknown web feed format");
7228         }
7230         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7231         my $head = $hash || 'HEAD';
7232         my @commitlist = parse_commits($head, 150, 0, $file_name);
7234         my %latest_commit;
7235         my %latest_date;
7236         my $content_type = "application/$format+xml";
7237         if (defined $cgi->http('HTTP_ACCEPT') &&
7238                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7239                 # browser (feed reader) prefers text/xml
7240                 $content_type = 'text/xml';
7241         }
7242         if (defined($commitlist[0])) {
7243                 %latest_commit = %{$commitlist[0]};
7244                 my $latest_epoch = $latest_commit{'committer_epoch'};
7245                 %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
7246                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7247                 if (defined $if_modified) {
7248                         my $since;
7249                         if (eval { require HTTP::Date; 1; }) {
7250                                 $since = HTTP::Date::str2time($if_modified);
7251                         } elsif (eval { require Time::ParseDate; 1; }) {
7252                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7253                         }
7254                         if (defined $since && $latest_epoch <= $since) {
7255                                 print $cgi->header(
7256                                         -type => $content_type,
7257                                         -charset => 'utf-8',
7258                                         -last_modified => $latest_date{'rfc2822'},
7259                                         -status => '304 Not Modified');
7260                                 return;
7261                         }
7262                 }
7263                 print $cgi->header(
7264                         -type => $content_type,
7265                         -charset => 'utf-8',
7266                         -last_modified => $latest_date{'rfc2822'});
7267         } else {
7268                 print $cgi->header(
7269                         -type => $content_type,
7270                         -charset => 'utf-8');
7271         }
7273         # Optimization: skip generating the body if client asks only
7274         # for Last-Modified date.
7275         return if ($cgi->request_method() eq 'HEAD');
7277         # header variables
7278         my $title = "$site_name - $project/$action";
7279         my $feed_type = 'log';
7280         if (defined $hash) {
7281                 $title .= " - '$hash'";
7282                 $feed_type = 'branch log';
7283                 if (defined $file_name) {
7284                         $title .= " :: $file_name";
7285                         $feed_type = 'history';
7286                 }
7287         } elsif (defined $file_name) {
7288                 $title .= " - $file_name";
7289                 $feed_type = 'history';
7290         }
7291         $title .= " $feed_type";
7292         my $descr = git_get_project_description($project);
7293         if (defined $descr) {
7294                 $descr = esc_html($descr);
7295         } else {
7296                 $descr = "$project " .
7297                          ($format eq 'rss' ? 'RSS' : 'Atom') .
7298                          " feed";
7299         }
7300         my $owner = git_get_project_owner($project);
7301         $owner = esc_html($owner);
7303         #header
7304         my $alt_url;
7305         if (defined $file_name) {
7306                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7307         } elsif (defined $hash) {
7308                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7309         } else {
7310                 $alt_url = href(-full=>1, action=>"summary");
7311         }
7312         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7313         if ($format eq 'rss') {
7314                 print <<XML;
7315 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7316 <channel>
7317 XML
7318                 print "<title>$title</title>\n" .
7319                       "<link>$alt_url</link>\n" .
7320                       "<description>$descr</description>\n" .
7321                       "<language>en</language>\n" .
7322                       # project owner is responsible for 'editorial' content
7323                       "<managingEditor>$owner</managingEditor>\n";
7324                 if (defined $logo || defined $favicon) {
7325                         # prefer the logo to the favicon, since RSS
7326                         # doesn't allow both
7327                         my $img = esc_url($logo || $favicon);
7328                         print "<image>\n" .
7329                               "<url>$img</url>\n" .
7330                               "<title>$title</title>\n" .
7331                               "<link>$alt_url</link>\n" .
7332                               "</image>\n";
7333                 }
7334                 if (%latest_date) {
7335                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7336                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7337                 }
7338                 print "<generator>gitweb v.$version/$git_version</generator>\n";
7339         } elsif ($format eq 'atom') {
7340                 print <<XML;
7341 <feed xmlns="http://www.w3.org/2005/Atom">
7342 XML
7343                 print "<title>$title</title>\n" .
7344                       "<subtitle>$descr</subtitle>\n" .
7345                       '<link rel="alternate" type="text/html" href="' .
7346                       $alt_url . '" />' . "\n" .
7347                       '<link rel="self" type="' . $content_type . '" href="' .
7348                       $cgi->self_url() . '" />' . "\n" .
7349                       "<id>" . href(-full=>1) . "</id>\n" .
7350                       # use project owner for feed author
7351                       "<author><name>$owner</name></author>\n";
7352                 if (defined $favicon) {
7353                         print "<icon>" . esc_url($favicon) . "</icon>\n";
7354                 }
7355                 if (defined $logo) {
7356                         # not twice as wide as tall: 72 x 27 pixels
7357                         print "<logo>" . esc_url($logo) . "</logo>\n";
7358                 }
7359                 if (! %latest_date) {
7360                         # dummy date to keep the feed valid until commits trickle in:
7361                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
7362                 } else {
7363                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
7364                 }
7365                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
7366         }
7368         # contents
7369         for (my $i = 0; $i <= $#commitlist; $i++) {
7370                 my %co = %{$commitlist[$i]};
7371                 my $commit = $co{'id'};
7372                 # we read 150, we always show 30 and the ones more recent than 48 hours
7373                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
7374                         last;
7375                 }
7376                 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
7378                 # get list of changed files
7379                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7380                         $co{'parent'} || "--root",
7381                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
7382                         or next;
7383                 my @difftree = map { chomp; $_ } <$fd>;
7384                 close $fd
7385                         or next;
7387                 # print element (entry, item)
7388                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7389                 if ($format eq 'rss') {
7390                         print "<item>\n" .
7391                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
7392                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
7393                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7394                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7395                               "<link>$co_url</link>\n" .
7396                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
7397                               "<content:encoded>" .
7398                               "<![CDATA[\n";
7399                 } elsif ($format eq 'atom') {
7400                         print "<entry>\n" .
7401                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7402                               "<updated>$cd{'iso-8601'}</updated>\n" .
7403                               "<author>\n" .
7404                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
7405                         if ($co{'author_email'}) {
7406                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
7407                         }
7408                         print "</author>\n" .
7409                               # use committer for contributor
7410                               "<contributor>\n" .
7411                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7412                         if ($co{'committer_email'}) {
7413                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7414                         }
7415                         print "</contributor>\n" .
7416                               "<published>$cd{'iso-8601'}</published>\n" .
7417                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
7418                               "<id>$co_url</id>\n" .
7419                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
7420                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
7421                 }
7422                 my $comment = $co{'comment'};
7423                 print "<pre>\n";
7424                 foreach my $line (@$comment) {
7425                         $line = esc_html($line);
7426                         print "$line\n";
7427                 }
7428                 print "</pre><ul>\n";
7429                 foreach my $difftree_line (@difftree) {
7430                         my %difftree = parse_difftree_raw_line($difftree_line);
7431                         next if !$difftree{'from_id'};
7433                         my $file = $difftree{'file'} || $difftree{'to_file'};
7435                         print "<li>" .
7436                               "[" .
7437                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
7438                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
7439                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
7440                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
7441                                       -title => "diff"}, 'D');
7442                         if ($have_blame) {
7443                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
7444                                                              file_name=>$file, hash_base=>$commit),
7445                                               -title => "blame"}, 'B');
7446                         }
7447                         # if this is not a feed of a file history
7448                         if (!defined $file_name || $file_name ne $file) {
7449                                 print $cgi->a({-href => href(-full=>1, action=>"history",
7450                                                              file_name=>$file, hash=>$commit),
7451                                               -title => "history"}, 'H');
7452                         }
7453                         $file = esc_path($file);
7454                         print "] ".
7455                               "$file</li>\n";
7456                 }
7457                 if ($format eq 'rss') {
7458                         print "</ul>]]>\n" .
7459                               "</content:encoded>\n" .
7460                               "</item>\n";
7461                 } elsif ($format eq 'atom') {
7462                         print "</ul>\n</div>\n" .
7463                               "</content>\n" .
7464                               "</entry>\n";
7465                 }
7466         }
7468         # end of feed
7469         if ($format eq 'rss') {
7470                 print "</channel>\n</rss>\n";
7471         } elsif ($format eq 'atom') {
7472                 print "</feed>\n";
7473         }
7476 sub git_rss {
7477         git_feed('rss');
7480 sub git_atom {
7481         git_feed('atom');
7484 sub git_opml {
7485         my @list = git_get_projects_list();
7486         if (!@list) {
7487                 die_error(404, "No projects found");
7488         }
7490         print $cgi->header(
7491                 -type => 'text/xml',
7492                 -charset => 'utf-8',
7493                 -content_disposition => 'inline; filename="opml.xml"');
7495         print <<XML;
7496 <?xml version="1.0" encoding="utf-8"?>
7497 <opml version="1.0">
7498 <head>
7499   <title>$site_name OPML Export</title>
7500 </head>
7501 <body>
7502 <outline text="git RSS feeds">
7503 XML
7505         foreach my $pr (@list) {
7506                 my %proj = %$pr;
7507                 my $head = git_get_head_hash($proj{'path'});
7508                 if (!defined $head) {
7509                         next;
7510                 }
7511                 $git_dir = "$projectroot/$proj{'path'}";
7512                 my %co = parse_commit($head);
7513                 if (!%co) {
7514                         next;
7515                 }
7517                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
7518                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7519                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
7520                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7521         }
7522         print <<XML;
7523 </outline>
7524 </body>
7525 </opml>
7526 XML