Code

60cb772c5834255acabe5eb61d83787fc91f96a0
[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']},
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 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
627 sub evaluate_gitweb_config {
628         our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
629         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
630         # die if there are errors parsing config file
631         if (-e $GITWEB_CONFIG) {
632                 do $GITWEB_CONFIG;
633                 die $@ if $@;
634         } elsif (-e $GITWEB_CONFIG_SYSTEM) {
635                 do $GITWEB_CONFIG_SYSTEM;
636                 die $@ if $@;
637         }
640 # Get loadavg of system, to compare against $maxload.
641 # Currently it requires '/proc/loadavg' present to get loadavg;
642 # if it is not present it returns 0, which means no load checking.
643 sub get_loadavg {
644         if( -e '/proc/loadavg' ){
645                 open my $fd, '<', '/proc/loadavg'
646                         or return 0;
647                 my @load = split(/\s+/, scalar <$fd>);
648                 close $fd;
650                 # The first three columns measure CPU and IO utilization of the last one,
651                 # five, and 10 minute periods.  The fourth column shows the number of
652                 # currently running processes and the total number of processes in the m/n
653                 # format.  The last column displays the last process ID used.
654                 return $load[0] || 0;
655         }
656         # additional checks for load average should go here for things that don't export
657         # /proc/loadavg
659         return 0;
662 # version of the core git binary
663 our $git_version;
664 sub evaluate_git_version {
665         our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
666         $number_of_git_cmds++;
669 sub check_loadavg {
670         if (defined $maxload && get_loadavg() > $maxload) {
671                 die_error(503, "The load average on the server is too high");
672         }
675 # ======================================================================
676 # input validation and dispatch
678 # input parameters can be collected from a variety of sources (presently, CGI
679 # and PATH_INFO), so we define an %input_params hash that collects them all
680 # together during validation: this allows subsequent uses (e.g. href()) to be
681 # agnostic of the parameter origin
683 our %input_params = ();
685 # input parameters are stored with the long parameter name as key. This will
686 # also be used in the href subroutine to convert parameters to their CGI
687 # equivalent, and since the href() usage is the most frequent one, we store
688 # the name -> CGI key mapping here, instead of the reverse.
690 # XXX: Warning: If you touch this, check the search form for updating,
691 # too.
693 our @cgi_param_mapping = (
694         project => "p",
695         action => "a",
696         file_name => "f",
697         file_parent => "fp",
698         hash => "h",
699         hash_parent => "hp",
700         hash_base => "hb",
701         hash_parent_base => "hpb",
702         page => "pg",
703         order => "o",
704         searchtext => "s",
705         searchtype => "st",
706         snapshot_format => "sf",
707         extra_options => "opt",
708         search_use_regexp => "sr",
709         ctag => "by_tag",
710         # this must be last entry (for manipulation from JavaScript)
711         javascript => "js"
712 );
713 our %cgi_param_mapping = @cgi_param_mapping;
715 # we will also need to know the possible actions, for validation
716 our %actions = (
717         "blame" => \&git_blame,
718         "blame_incremental" => \&git_blame_incremental,
719         "blame_data" => \&git_blame_data,
720         "blobdiff" => \&git_blobdiff,
721         "blobdiff_plain" => \&git_blobdiff_plain,
722         "blob" => \&git_blob,
723         "blob_plain" => \&git_blob_plain,
724         "commitdiff" => \&git_commitdiff,
725         "commitdiff_plain" => \&git_commitdiff_plain,
726         "commit" => \&git_commit,
727         "forks" => \&git_forks,
728         "heads" => \&git_heads,
729         "history" => \&git_history,
730         "log" => \&git_log,
731         "patch" => \&git_patch,
732         "patches" => \&git_patches,
733         "remotes" => \&git_remotes,
734         "rss" => \&git_rss,
735         "atom" => \&git_atom,
736         "search" => \&git_search,
737         "search_help" => \&git_search_help,
738         "shortlog" => \&git_shortlog,
739         "summary" => \&git_summary,
740         "tag" => \&git_tag,
741         "tags" => \&git_tags,
742         "tree" => \&git_tree,
743         "snapshot" => \&git_snapshot,
744         "object" => \&git_object,
745         # those below don't need $project
746         "opml" => \&git_opml,
747         "project_list" => \&git_project_list,
748         "project_index" => \&git_project_index,
749 );
751 # finally, we have the hash of allowed extra_options for the commands that
752 # allow them
753 our %allowed_options = (
754         "--no-merges" => [ qw(rss atom log shortlog history) ],
755 );
757 # fill %input_params with the CGI parameters. All values except for 'opt'
758 # should be single values, but opt can be an array. We should probably
759 # build an array of parameters that can be multi-valued, but since for the time
760 # being it's only this one, we just single it out
761 sub evaluate_query_params {
762         our $cgi;
764         while (my ($name, $symbol) = each %cgi_param_mapping) {
765                 if ($symbol eq 'opt') {
766                         $input_params{$name} = [ $cgi->param($symbol) ];
767                 } else {
768                         $input_params{$name} = $cgi->param($symbol);
769                 }
770         }
773 # now read PATH_INFO and update the parameter list for missing parameters
774 sub evaluate_path_info {
775         return if defined $input_params{'project'};
776         return if !$path_info;
777         $path_info =~ s,^/+,,;
778         return if !$path_info;
780         # find which part of PATH_INFO is project
781         my $project = $path_info;
782         $project =~ s,/+$,,;
783         while ($project && !check_head_link("$projectroot/$project")) {
784                 $project =~ s,/*[^/]*$,,;
785         }
786         return unless $project;
787         $input_params{'project'} = $project;
789         # do not change any parameters if an action is given using the query string
790         return if $input_params{'action'};
791         $path_info =~ s,^\Q$project\E/*,,;
793         # next, check if we have an action
794         my $action = $path_info;
795         $action =~ s,/.*$,,;
796         if (exists $actions{$action}) {
797                 $path_info =~ s,^$action/*,,;
798                 $input_params{'action'} = $action;
799         }
801         # list of actions that want hash_base instead of hash, but can have no
802         # pathname (f) parameter
803         my @wants_base = (
804                 'tree',
805                 'history',
806         );
808         # we want to catch, among others
809         # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
810         my ($parentrefname, $parentpathname, $refname, $pathname) =
811                 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
813         # first, analyze the 'current' part
814         if (defined $pathname) {
815                 # we got "branch:filename" or "branch:dir/"
816                 # we could use git_get_type(branch:pathname), but:
817                 # - it needs $git_dir
818                 # - it does a git() call
819                 # - the convention of terminating directories with a slash
820                 #   makes it superfluous
821                 # - embedding the action in the PATH_INFO would make it even
822                 #   more superfluous
823                 $pathname =~ s,^/+,,;
824                 if (!$pathname || substr($pathname, -1) eq "/") {
825                         $input_params{'action'} ||= "tree";
826                         $pathname =~ s,/$,,;
827                 } else {
828                         # the default action depends on whether we had parent info
829                         # or not
830                         if ($parentrefname) {
831                                 $input_params{'action'} ||= "blobdiff_plain";
832                         } else {
833                                 $input_params{'action'} ||= "blob_plain";
834                         }
835                 }
836                 $input_params{'hash_base'} ||= $refname;
837                 $input_params{'file_name'} ||= $pathname;
838         } elsif (defined $refname) {
839                 # we got "branch". In this case we have to choose if we have to
840                 # set hash or hash_base.
841                 #
842                 # Most of the actions without a pathname only want hash to be
843                 # set, except for the ones specified in @wants_base that want
844                 # hash_base instead. It should also be noted that hand-crafted
845                 # links having 'history' as an action and no pathname or hash
846                 # set will fail, but that happens regardless of PATH_INFO.
847                 if (defined $parentrefname) {
848                         # if there is parent let the default be 'shortlog' action
849                         # (for http://git.example.com/repo.git/A..B links); if there
850                         # is no parent, dispatch will detect type of object and set
851                         # action appropriately if required (if action is not set)
852                         $input_params{'action'} ||= "shortlog";
853                 }
854                 if ($input_params{'action'} &&
855                     grep { $_ eq $input_params{'action'} } @wants_base) {
856                         $input_params{'hash_base'} ||= $refname;
857                 } else {
858                         $input_params{'hash'} ||= $refname;
859                 }
860         }
862         # next, handle the 'parent' part, if present
863         if (defined $parentrefname) {
864                 # a missing pathspec defaults to the 'current' filename, allowing e.g.
865                 # someproject/blobdiff/oldrev..newrev:/filename
866                 if ($parentpathname) {
867                         $parentpathname =~ s,^/+,,;
868                         $parentpathname =~ s,/$,,;
869                         $input_params{'file_parent'} ||= $parentpathname;
870                 } else {
871                         $input_params{'file_parent'} ||= $input_params{'file_name'};
872                 }
873                 # we assume that hash_parent_base is wanted if a path was specified,
874                 # or if the action wants hash_base instead of hash
875                 if (defined $input_params{'file_parent'} ||
876                         grep { $_ eq $input_params{'action'} } @wants_base) {
877                         $input_params{'hash_parent_base'} ||= $parentrefname;
878                 } else {
879                         $input_params{'hash_parent'} ||= $parentrefname;
880                 }
881         }
883         # for the snapshot action, we allow URLs in the form
884         # $project/snapshot/$hash.ext
885         # where .ext determines the snapshot and gets removed from the
886         # passed $refname to provide the $hash.
887         #
888         # To be able to tell that $refname includes the format extension, we
889         # require the following two conditions to be satisfied:
890         # - the hash input parameter MUST have been set from the $refname part
891         #   of the URL (i.e. they must be equal)
892         # - the snapshot format MUST NOT have been defined already (e.g. from
893         #   CGI parameter sf)
894         # It's also useless to try any matching unless $refname has a dot,
895         # so we check for that too
896         if (defined $input_params{'action'} &&
897                 $input_params{'action'} eq 'snapshot' &&
898                 defined $refname && index($refname, '.') != -1 &&
899                 $refname eq $input_params{'hash'} &&
900                 !defined $input_params{'snapshot_format'}) {
901                 # We loop over the known snapshot formats, checking for
902                 # extensions. Allowed extensions are both the defined suffix
903                 # (which includes the initial dot already) and the snapshot
904                 # format key itself, with a prepended dot
905                 while (my ($fmt, $opt) = each %known_snapshot_formats) {
906                         my $hash = $refname;
907                         unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
908                                 next;
909                         }
910                         my $sfx = $1;
911                         # a valid suffix was found, so set the snapshot format
912                         # and reset the hash parameter
913                         $input_params{'snapshot_format'} = $fmt;
914                         $input_params{'hash'} = $hash;
915                         # we also set the format suffix to the one requested
916                         # in the URL: this way a request for e.g. .tgz returns
917                         # a .tgz instead of a .tar.gz
918                         $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
919                         last;
920                 }
921         }
924 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
925      $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
926      $searchtext, $search_regexp);
927 sub evaluate_and_validate_params {
928         our $action = $input_params{'action'};
929         if (defined $action) {
930                 if (!validate_action($action)) {
931                         die_error(400, "Invalid action parameter");
932                 }
933         }
935         # parameters which are pathnames
936         our $project = $input_params{'project'};
937         if (defined $project) {
938                 if (!validate_project($project)) {
939                         undef $project;
940                         die_error(404, "No such project");
941                 }
942         }
944         our $file_name = $input_params{'file_name'};
945         if (defined $file_name) {
946                 if (!validate_pathname($file_name)) {
947                         die_error(400, "Invalid file parameter");
948                 }
949         }
951         our $file_parent = $input_params{'file_parent'};
952         if (defined $file_parent) {
953                 if (!validate_pathname($file_parent)) {
954                         die_error(400, "Invalid file parent parameter");
955                 }
956         }
958         # parameters which are refnames
959         our $hash = $input_params{'hash'};
960         if (defined $hash) {
961                 if (!validate_refname($hash)) {
962                         die_error(400, "Invalid hash parameter");
963                 }
964         }
966         our $hash_parent = $input_params{'hash_parent'};
967         if (defined $hash_parent) {
968                 if (!validate_refname($hash_parent)) {
969                         die_error(400, "Invalid hash parent parameter");
970                 }
971         }
973         our $hash_base = $input_params{'hash_base'};
974         if (defined $hash_base) {
975                 if (!validate_refname($hash_base)) {
976                         die_error(400, "Invalid hash base parameter");
977                 }
978         }
980         our @extra_options = @{$input_params{'extra_options'}};
981         # @extra_options is always defined, since it can only be (currently) set from
982         # CGI, and $cgi->param() returns the empty array in array context if the param
983         # is not set
984         foreach my $opt (@extra_options) {
985                 if (not exists $allowed_options{$opt}) {
986                         die_error(400, "Invalid option parameter");
987                 }
988                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
989                         die_error(400, "Invalid option parameter for this action");
990                 }
991         }
993         our $hash_parent_base = $input_params{'hash_parent_base'};
994         if (defined $hash_parent_base) {
995                 if (!validate_refname($hash_parent_base)) {
996                         die_error(400, "Invalid hash parent base parameter");
997                 }
998         }
1000         # other parameters
1001         our $page = $input_params{'page'};
1002         if (defined $page) {
1003                 if ($page =~ m/[^0-9]/) {
1004                         die_error(400, "Invalid page parameter");
1005                 }
1006         }
1008         our $searchtype = $input_params{'searchtype'};
1009         if (defined $searchtype) {
1010                 if ($searchtype =~ m/[^a-z]/) {
1011                         die_error(400, "Invalid searchtype parameter");
1012                 }
1013         }
1015         our $search_use_regexp = $input_params{'search_use_regexp'};
1017         our $searchtext = $input_params{'searchtext'};
1018         our $search_regexp;
1019         if (defined $searchtext) {
1020                 if (length($searchtext) < 2) {
1021                         die_error(403, "At least two characters are required for search parameter");
1022                 }
1023                 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
1024         }
1027 # path to the current git repository
1028 our $git_dir;
1029 sub evaluate_git_dir {
1030         our $git_dir = "$projectroot/$project" if $project;
1033 our (@snapshot_fmts, $git_avatar);
1034 sub configure_gitweb_features {
1035         # list of supported snapshot formats
1036         our @snapshot_fmts = gitweb_get_feature('snapshot');
1037         @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1039         # check that the avatar feature is set to a known provider name,
1040         # and for each provider check if the dependencies are satisfied.
1041         # if the provider name is invalid or the dependencies are not met,
1042         # reset $git_avatar to the empty string.
1043         our ($git_avatar) = gitweb_get_feature('avatar');
1044         if ($git_avatar eq 'gravatar') {
1045                 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1046         } elsif ($git_avatar eq 'picon') {
1047                 # no dependencies
1048         } else {
1049                 $git_avatar = '';
1050         }
1053 # custom error handler: 'die <message>' is Internal Server Error
1054 sub handle_errors_html {
1055         my $msg = shift; # it is already HTML escaped
1057         # to avoid infinite loop where error occurs in die_error,
1058         # change handler to default handler, disabling handle_errors_html
1059         set_message("Error occured when inside die_error:\n$msg");
1061         # you cannot jump out of die_error when called as error handler;
1062         # the subroutine set via CGI::Carp::set_message is called _after_
1063         # HTTP headers are already written, so it cannot write them itself
1064         die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1066 set_message(\&handle_errors_html);
1068 # dispatch
1069 sub dispatch {
1070         if (!defined $action) {
1071                 if (defined $hash) {
1072                         $action = git_get_type($hash);
1073                 } elsif (defined $hash_base && defined $file_name) {
1074                         $action = git_get_type("$hash_base:$file_name");
1075                 } elsif (defined $project) {
1076                         $action = 'summary';
1077                 } else {
1078                         $action = 'project_list';
1079                 }
1080         }
1081         if (!defined($actions{$action})) {
1082                 die_error(400, "Unknown action");
1083         }
1084         if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1085             !$project) {
1086                 die_error(400, "Project needed");
1087         }
1088         $actions{$action}->();
1091 sub reset_timer {
1092         our $t0 = [ gettimeofday() ]
1093                 if defined $t0;
1094         our $number_of_git_cmds = 0;
1097 our $first_request = 1;
1098 sub run_request {
1099         reset_timer();
1101         evaluate_uri();
1102         if ($first_request) {
1103                 evaluate_gitweb_config();
1104                 evaluate_git_version();
1105         }
1106         if ($per_request_config) {
1107                 if (ref($per_request_config) eq 'CODE') {
1108                         $per_request_config->();
1109                 } elsif (!$first_request) {
1110                         evaluate_gitweb_config();
1111                 }
1112         }
1113         check_loadavg();
1115         # $projectroot and $projects_list might be set in gitweb config file
1116         $projects_list ||= $projectroot;
1118         evaluate_query_params();
1119         evaluate_path_info();
1120         evaluate_and_validate_params();
1121         evaluate_git_dir();
1123         configure_gitweb_features();
1125         dispatch();
1128 our $is_last_request = sub { 1 };
1129 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1130 our $CGI = 'CGI';
1131 our $cgi;
1132 sub configure_as_fcgi {
1133         require CGI::Fast;
1134         our $CGI = 'CGI::Fast';
1136         my $request_number = 0;
1137         # let each child service 100 requests
1138         our $is_last_request = sub { ++$request_number > 100 };
1140 sub evaluate_argv {
1141         my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1142         configure_as_fcgi()
1143                 if $script_name =~ /\.fcgi$/;
1145         return unless (@ARGV);
1147         require Getopt::Long;
1148         Getopt::Long::GetOptions(
1149                 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1150                 'nproc|n=i' => sub {
1151                         my ($arg, $val) = @_;
1152                         return unless eval { require FCGI::ProcManager; 1; };
1153                         my $proc_manager = FCGI::ProcManager->new({
1154                                 n_processes => $val,
1155                         });
1156                         our $pre_listen_hook    = sub { $proc_manager->pm_manage()        };
1157                         our $pre_dispatch_hook  = sub { $proc_manager->pm_pre_dispatch()  };
1158                         our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1159                 },
1160         );
1163 sub run {
1164         evaluate_argv();
1166         $first_request = 1;
1167         $pre_listen_hook->()
1168                 if $pre_listen_hook;
1170  REQUEST:
1171         while ($cgi = $CGI->new()) {
1172                 $pre_dispatch_hook->()
1173                         if $pre_dispatch_hook;
1175                 run_request();
1177                 $post_dispatch_hook->()
1178                         if $post_dispatch_hook;
1179                 $first_request = 0;
1181                 last REQUEST if ($is_last_request->());
1182         }
1184  DONE_GITWEB:
1185         1;
1188 run();
1190 if (defined caller) {
1191         # wrapped in a subroutine processing requests,
1192         # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1193         return;
1194 } else {
1195         # pure CGI script, serving single request
1196         exit;
1199 ## ======================================================================
1200 ## action links
1202 # possible values of extra options
1203 # -full => 0|1      - use absolute/full URL ($my_uri/$my_url as base)
1204 # -replay => 1      - start from a current view (replay with modifications)
1205 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1206 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1207 sub href {
1208         my %params = @_;
1209         # default is to use -absolute url() i.e. $my_uri
1210         my $href = $params{-full} ? $my_url : $my_uri;
1212         # implicit -replay, must be first of implicit params
1213         $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1215         $params{'project'} = $project unless exists $params{'project'};
1217         if ($params{-replay}) {
1218                 while (my ($name, $symbol) = each %cgi_param_mapping) {
1219                         if (!exists $params{$name}) {
1220                                 $params{$name} = $input_params{$name};
1221                         }
1222                 }
1223         }
1225         my $use_pathinfo = gitweb_check_feature('pathinfo');
1226         if (defined $params{'project'} &&
1227             (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1228                 # try to put as many parameters as possible in PATH_INFO:
1229                 #   - project name
1230                 #   - action
1231                 #   - hash_parent or hash_parent_base:/file_parent
1232                 #   - hash or hash_base:/filename
1233                 #   - the snapshot_format as an appropriate suffix
1235                 # When the script is the root DirectoryIndex for the domain,
1236                 # $href here would be something like http://gitweb.example.com/
1237                 # Thus, we strip any trailing / from $href, to spare us double
1238                 # slashes in the final URL
1239                 $href =~ s,/$,,;
1241                 # Then add the project name, if present
1242                 $href .= "/".esc_path_info($params{'project'});
1243                 delete $params{'project'};
1245                 # since we destructively absorb parameters, we keep this
1246                 # boolean that remembers if we're handling a snapshot
1247                 my $is_snapshot = $params{'action'} eq 'snapshot';
1249                 # Summary just uses the project path URL, any other action is
1250                 # added to the URL
1251                 if (defined $params{'action'}) {
1252                         $href .= "/".esc_path_info($params{'action'})
1253                                 unless $params{'action'} eq 'summary';
1254                         delete $params{'action'};
1255                 }
1257                 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1258                 # stripping nonexistent or useless pieces
1259                 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1260                         || $params{'hash_parent'} || $params{'hash'});
1261                 if (defined $params{'hash_base'}) {
1262                         if (defined $params{'hash_parent_base'}) {
1263                                 $href .= esc_path_info($params{'hash_parent_base'});
1264                                 # skip the file_parent if it's the same as the file_name
1265                                 if (defined $params{'file_parent'}) {
1266                                         if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1267                                                 delete $params{'file_parent'};
1268                                         } elsif ($params{'file_parent'} !~ /\.\./) {
1269                                                 $href .= ":/".esc_path_info($params{'file_parent'});
1270                                                 delete $params{'file_parent'};
1271                                         }
1272                                 }
1273                                 $href .= "..";
1274                                 delete $params{'hash_parent'};
1275                                 delete $params{'hash_parent_base'};
1276                         } elsif (defined $params{'hash_parent'}) {
1277                                 $href .= esc_path_info($params{'hash_parent'}). "..";
1278                                 delete $params{'hash_parent'};
1279                         }
1281                         $href .= esc_path_info($params{'hash_base'});
1282                         if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1283                                 $href .= ":/".esc_path_info($params{'file_name'});
1284                                 delete $params{'file_name'};
1285                         }
1286                         delete $params{'hash'};
1287                         delete $params{'hash_base'};
1288                 } elsif (defined $params{'hash'}) {
1289                         $href .= esc_path_info($params{'hash'});
1290                         delete $params{'hash'};
1291                 }
1293                 # If the action was a snapshot, we can absorb the
1294                 # snapshot_format parameter too
1295                 if ($is_snapshot) {
1296                         my $fmt = $params{'snapshot_format'};
1297                         # snapshot_format should always be defined when href()
1298                         # is called, but just in case some code forgets, we
1299                         # fall back to the default
1300                         $fmt ||= $snapshot_fmts[0];
1301                         $href .= $known_snapshot_formats{$fmt}{'suffix'};
1302                         delete $params{'snapshot_format'};
1303                 }
1304         }
1306         # now encode the parameters explicitly
1307         my @result = ();
1308         for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1309                 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1310                 if (defined $params{$name}) {
1311                         if (ref($params{$name}) eq "ARRAY") {
1312                                 foreach my $par (@{$params{$name}}) {
1313                                         push @result, $symbol . "=" . esc_param($par);
1314                                 }
1315                         } else {
1316                                 push @result, $symbol . "=" . esc_param($params{$name});
1317                         }
1318                 }
1319         }
1320         $href .= "?" . join(';', @result) if scalar @result;
1322         # final transformation: trailing spaces must be escaped (URI-encoded)
1323         $href =~ s/(\s+)$/CGI::escape($1)/e;
1325         if ($params{-anchor}) {
1326                 $href .= "#".esc_param($params{-anchor});
1327         }
1329         return $href;
1333 ## ======================================================================
1334 ## validation, quoting/unquoting and escaping
1336 sub validate_action {
1337         my $input = shift || return undef;
1338         return undef unless exists $actions{$input};
1339         return $input;
1342 sub validate_project {
1343         my $input = shift || return undef;
1344         if (!validate_pathname($input) ||
1345                 !(-d "$projectroot/$input") ||
1346                 !check_export_ok("$projectroot/$input") ||
1347                 ($strict_export && !project_in_list($input))) {
1348                 return undef;
1349         } else {
1350                 return $input;
1351         }
1354 sub validate_pathname {
1355         my $input = shift || return undef;
1357         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1358         # at the beginning, at the end, and between slashes.
1359         # also this catches doubled slashes
1360         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1361                 return undef;
1362         }
1363         # no null characters
1364         if ($input =~ m!\0!) {
1365                 return undef;
1366         }
1367         return $input;
1370 sub validate_refname {
1371         my $input = shift || return undef;
1373         # textual hashes are O.K.
1374         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1375                 return $input;
1376         }
1377         # it must be correct pathname
1378         $input = validate_pathname($input)
1379                 or return undef;
1380         # restrictions on ref name according to git-check-ref-format
1381         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1382                 return undef;
1383         }
1384         return $input;
1387 # decode sequences of octets in utf8 into Perl's internal form,
1388 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
1389 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1390 sub to_utf8 {
1391         my $str = shift;
1392         return undef unless defined $str;
1393         if (utf8::valid($str)) {
1394                 utf8::decode($str);
1395                 return $str;
1396         } else {
1397                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1398         }
1401 # quote unsafe chars, but keep the slash, even when it's not
1402 # correct, but quoted slashes look too horrible in bookmarks
1403 sub esc_param {
1404         my $str = shift;
1405         return undef unless defined $str;
1406         $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1407         $str =~ s/ /\+/g;
1408         return $str;
1411 # the quoting rules for path_info fragment are slightly different
1412 sub esc_path_info {
1413         my $str = shift;
1414         return undef unless defined $str;
1416         # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1417         $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1419         return $str;
1422 # quote unsafe chars in whole URL, so some characters cannot be quoted
1423 sub esc_url {
1424         my $str = shift;
1425         return undef unless defined $str;
1426         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1427         $str =~ s/ /\+/g;
1428         return $str;
1431 # quote unsafe characters in HTML attributes
1432 sub esc_attr {
1434         # for XHTML conformance escaping '"' to '&quot;' is not enough
1435         return esc_html(@_);
1438 # replace invalid utf8 character with SUBSTITUTION sequence
1439 sub esc_html {
1440         my $str = shift;
1441         my %opts = @_;
1443         return undef unless defined $str;
1445         $str = to_utf8($str);
1446         $str = $cgi->escapeHTML($str);
1447         if ($opts{'-nbsp'}) {
1448                 $str =~ s/ /&nbsp;/g;
1449         }
1450         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1451         return $str;
1454 # quote control characters and escape filename to HTML
1455 sub esc_path {
1456         my $str = shift;
1457         my %opts = @_;
1459         return undef unless defined $str;
1461         $str = to_utf8($str);
1462         $str = $cgi->escapeHTML($str);
1463         if ($opts{'-nbsp'}) {
1464                 $str =~ s/ /&nbsp;/g;
1465         }
1466         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1467         return $str;
1470 # Make control characters "printable", using character escape codes (CEC)
1471 sub quot_cec {
1472         my $cntrl = shift;
1473         my %opts = @_;
1474         my %es = ( # character escape codes, aka escape sequences
1475                 "\t" => '\t',   # tab            (HT)
1476                 "\n" => '\n',   # line feed      (LF)
1477                 "\r" => '\r',   # carrige return (CR)
1478                 "\f" => '\f',   # form feed      (FF)
1479                 "\b" => '\b',   # backspace      (BS)
1480                 "\a" => '\a',   # alarm (bell)   (BEL)
1481                 "\e" => '\e',   # escape         (ESC)
1482                 "\013" => '\v', # vertical tab   (VT)
1483                 "\000" => '\0', # nul character  (NUL)
1484         );
1485         my $chr = ( (exists $es{$cntrl})
1486                     ? $es{$cntrl}
1487                     : sprintf('\%2x', ord($cntrl)) );
1488         if ($opts{-nohtml}) {
1489                 return $chr;
1490         } else {
1491                 return "<span class=\"cntrl\">$chr</span>";
1492         }
1495 # Alternatively use unicode control pictures codepoints,
1496 # Unicode "printable representation" (PR)
1497 sub quot_upr {
1498         my $cntrl = shift;
1499         my %opts = @_;
1501         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1502         if ($opts{-nohtml}) {
1503                 return $chr;
1504         } else {
1505                 return "<span class=\"cntrl\">$chr</span>";
1506         }
1509 # git may return quoted and escaped filenames
1510 sub unquote {
1511         my $str = shift;
1513         sub unq {
1514                 my $seq = shift;
1515                 my %es = ( # character escape codes, aka escape sequences
1516                         't' => "\t",   # tab            (HT, TAB)
1517                         'n' => "\n",   # newline        (NL)
1518                         'r' => "\r",   # return         (CR)
1519                         'f' => "\f",   # form feed      (FF)
1520                         'b' => "\b",   # backspace      (BS)
1521                         'a' => "\a",   # alarm (bell)   (BEL)
1522                         'e' => "\e",   # escape         (ESC)
1523                         'v' => "\013", # vertical tab   (VT)
1524                 );
1526                 if ($seq =~ m/^[0-7]{1,3}$/) {
1527                         # octal char sequence
1528                         return chr(oct($seq));
1529                 } elsif (exists $es{$seq}) {
1530                         # C escape sequence, aka character escape code
1531                         return $es{$seq};
1532                 }
1533                 # quoted ordinary character
1534                 return $seq;
1535         }
1537         if ($str =~ m/^"(.*)"$/) {
1538                 # needs unquoting
1539                 $str = $1;
1540                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1541         }
1542         return $str;
1545 # escape tabs (convert tabs to spaces)
1546 sub untabify {
1547         my $line = shift;
1549         while ((my $pos = index($line, "\t")) != -1) {
1550                 if (my $count = (8 - ($pos % 8))) {
1551                         my $spaces = ' ' x $count;
1552                         $line =~ s/\t/$spaces/;
1553                 }
1554         }
1556         return $line;
1559 sub project_in_list {
1560         my $project = shift;
1561         my @list = git_get_projects_list();
1562         return @list && scalar(grep { $_->{'path'} eq $project } @list);
1565 ## ----------------------------------------------------------------------
1566 ## HTML aware string manipulation
1568 # Try to chop given string on a word boundary between position
1569 # $len and $len+$add_len. If there is no word boundary there,
1570 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1571 # (marking chopped part) would be longer than given string.
1572 sub chop_str {
1573         my $str = shift;
1574         my $len = shift;
1575         my $add_len = shift || 10;
1576         my $where = shift || 'right'; # 'left' | 'center' | 'right'
1578         # Make sure perl knows it is utf8 encoded so we don't
1579         # cut in the middle of a utf8 multibyte char.
1580         $str = to_utf8($str);
1582         # allow only $len chars, but don't cut a word if it would fit in $add_len
1583         # if it doesn't fit, cut it if it's still longer than the dots we would add
1584         # remove chopped character entities entirely
1586         # when chopping in the middle, distribute $len into left and right part
1587         # return early if chopping wouldn't make string shorter
1588         if ($where eq 'center') {
1589                 return $str if ($len + 5 >= length($str)); # filler is length 5
1590                 $len = int($len/2);
1591         } else {
1592                 return $str if ($len + 4 >= length($str)); # filler is length 4
1593         }
1595         # regexps: ending and beginning with word part up to $add_len
1596         my $endre = qr/.{$len}\w{0,$add_len}/;
1597         my $begre = qr/\w{0,$add_len}.{$len}/;
1599         if ($where eq 'left') {
1600                 $str =~ m/^(.*?)($begre)$/;
1601                 my ($lead, $body) = ($1, $2);
1602                 if (length($lead) > 4) {
1603                         $lead = " ...";
1604                 }
1605                 return "$lead$body";
1607         } elsif ($where eq 'center') {
1608                 $str =~ m/^($endre)(.*)$/;
1609                 my ($left, $str)  = ($1, $2);
1610                 $str =~ m/^(.*?)($begre)$/;
1611                 my ($mid, $right) = ($1, $2);
1612                 if (length($mid) > 5) {
1613                         $mid = " ... ";
1614                 }
1615                 return "$left$mid$right";
1617         } else {
1618                 $str =~ m/^($endre)(.*)$/;
1619                 my $body = $1;
1620                 my $tail = $2;
1621                 if (length($tail) > 4) {
1622                         $tail = "... ";
1623                 }
1624                 return "$body$tail";
1625         }
1628 # takes the same arguments as chop_str, but also wraps a <span> around the
1629 # result with a title attribute if it does get chopped. Additionally, the
1630 # string is HTML-escaped.
1631 sub chop_and_escape_str {
1632         my ($str) = @_;
1634         my $chopped = chop_str(@_);
1635         if ($chopped eq $str) {
1636                 return esc_html($chopped);
1637         } else {
1638                 $str =~ s/[[:cntrl:]]/?/g;
1639                 return $cgi->span({-title=>$str}, esc_html($chopped));
1640         }
1643 ## ----------------------------------------------------------------------
1644 ## functions returning short strings
1646 # CSS class for given age value (in seconds)
1647 sub age_class {
1648         my $age = shift;
1650         if (!defined $age) {
1651                 return "noage";
1652         } elsif ($age < 60*60*2) {
1653                 return "age0";
1654         } elsif ($age < 60*60*24*2) {
1655                 return "age1";
1656         } else {
1657                 return "age2";
1658         }
1661 # convert age in seconds to "nn units ago" string
1662 sub age_string {
1663         my $age = shift;
1664         my $age_str;
1666         if ($age > 60*60*24*365*2) {
1667                 $age_str = (int $age/60/60/24/365);
1668                 $age_str .= " years ago";
1669         } elsif ($age > 60*60*24*(365/12)*2) {
1670                 $age_str = int $age/60/60/24/(365/12);
1671                 $age_str .= " months ago";
1672         } elsif ($age > 60*60*24*7*2) {
1673                 $age_str = int $age/60/60/24/7;
1674                 $age_str .= " weeks ago";
1675         } elsif ($age > 60*60*24*2) {
1676                 $age_str = int $age/60/60/24;
1677                 $age_str .= " days ago";
1678         } elsif ($age > 60*60*2) {
1679                 $age_str = int $age/60/60;
1680                 $age_str .= " hours ago";
1681         } elsif ($age > 60*2) {
1682                 $age_str = int $age/60;
1683                 $age_str .= " min ago";
1684         } elsif ($age > 2) {
1685                 $age_str = int $age;
1686                 $age_str .= " sec ago";
1687         } else {
1688                 $age_str .= " right now";
1689         }
1690         return $age_str;
1693 use constant {
1694         S_IFINVALID => 0030000,
1695         S_IFGITLINK => 0160000,
1696 };
1698 # submodule/subproject, a commit object reference
1699 sub S_ISGITLINK {
1700         my $mode = shift;
1702         return (($mode & S_IFMT) == S_IFGITLINK)
1705 # convert file mode in octal to symbolic file mode string
1706 sub mode_str {
1707         my $mode = oct shift;
1709         if (S_ISGITLINK($mode)) {
1710                 return 'm---------';
1711         } elsif (S_ISDIR($mode & S_IFMT)) {
1712                 return 'drwxr-xr-x';
1713         } elsif (S_ISLNK($mode)) {
1714                 return 'lrwxrwxrwx';
1715         } elsif (S_ISREG($mode)) {
1716                 # git cares only about the executable bit
1717                 if ($mode & S_IXUSR) {
1718                         return '-rwxr-xr-x';
1719                 } else {
1720                         return '-rw-r--r--';
1721                 };
1722         } else {
1723                 return '----------';
1724         }
1727 # convert file mode in octal to file type string
1728 sub file_type {
1729         my $mode = shift;
1731         if ($mode !~ m/^[0-7]+$/) {
1732                 return $mode;
1733         } else {
1734                 $mode = oct $mode;
1735         }
1737         if (S_ISGITLINK($mode)) {
1738                 return "submodule";
1739         } elsif (S_ISDIR($mode & S_IFMT)) {
1740                 return "directory";
1741         } elsif (S_ISLNK($mode)) {
1742                 return "symlink";
1743         } elsif (S_ISREG($mode)) {
1744                 return "file";
1745         } else {
1746                 return "unknown";
1747         }
1750 # convert file mode in octal to file type description string
1751 sub file_type_long {
1752         my $mode = shift;
1754         if ($mode !~ m/^[0-7]+$/) {
1755                 return $mode;
1756         } else {
1757                 $mode = oct $mode;
1758         }
1760         if (S_ISGITLINK($mode)) {
1761                 return "submodule";
1762         } elsif (S_ISDIR($mode & S_IFMT)) {
1763                 return "directory";
1764         } elsif (S_ISLNK($mode)) {
1765                 return "symlink";
1766         } elsif (S_ISREG($mode)) {
1767                 if ($mode & S_IXUSR) {
1768                         return "executable";
1769                 } else {
1770                         return "file";
1771                 };
1772         } else {
1773                 return "unknown";
1774         }
1778 ## ----------------------------------------------------------------------
1779 ## functions returning short HTML fragments, or transforming HTML fragments
1780 ## which don't belong to other sections
1782 # format line of commit message.
1783 sub format_log_line_html {
1784         my $line = shift;
1786         $line = esc_html($line, -nbsp=>1);
1787         $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1788                 $cgi->a({-href => href(action=>"object", hash=>$1),
1789                                         -class => "text"}, $1);
1790         }eg;
1792         return $line;
1795 # format marker of refs pointing to given object
1797 # the destination action is chosen based on object type and current context:
1798 # - for annotated tags, we choose the tag view unless it's the current view
1799 #   already, in which case we go to shortlog view
1800 # - for other refs, we keep the current view if we're in history, shortlog or
1801 #   log view, and select shortlog otherwise
1802 sub format_ref_marker {
1803         my ($refs, $id) = @_;
1804         my $markers = '';
1806         if (defined $refs->{$id}) {
1807                 foreach my $ref (@{$refs->{$id}}) {
1808                         # this code exploits the fact that non-lightweight tags are the
1809                         # only indirect objects, and that they are the only objects for which
1810                         # we want to use tag instead of shortlog as action
1811                         my ($type, $name) = qw();
1812                         my $indirect = ($ref =~ s/\^\{\}$//);
1813                         # e.g. tags/v2.6.11 or heads/next
1814                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1815                                 $type = $1;
1816                                 $name = $2;
1817                         } else {
1818                                 $type = "ref";
1819                                 $name = $ref;
1820                         }
1822                         my $class = $type;
1823                         $class .= " indirect" if $indirect;
1825                         my $dest_action = "shortlog";
1827                         if ($indirect) {
1828                                 $dest_action = "tag" unless $action eq "tag";
1829                         } elsif ($action =~ /^(history|(short)?log)$/) {
1830                                 $dest_action = $action;
1831                         }
1833                         my $dest = "";
1834                         $dest .= "refs/" unless $ref =~ m!^refs/!;
1835                         $dest .= $ref;
1837                         my $link = $cgi->a({
1838                                 -href => href(
1839                                         action=>$dest_action,
1840                                         hash=>$dest
1841                                 )}, $name);
1843                         $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
1844                                 $link . "</span>";
1845                 }
1846         }
1848         if ($markers) {
1849                 return ' <span class="refs">'. $markers . '</span>';
1850         } else {
1851                 return "";
1852         }
1855 # format, perhaps shortened and with markers, title line
1856 sub format_subject_html {
1857         my ($long, $short, $href, $extra) = @_;
1858         $extra = '' unless defined($extra);
1860         if (length($short) < length($long)) {
1861                 $long =~ s/[[:cntrl:]]/?/g;
1862                 return $cgi->a({-href => $href, -class => "list subject",
1863                                 -title => to_utf8($long)},
1864                        esc_html($short)) . $extra;
1865         } else {
1866                 return $cgi->a({-href => $href, -class => "list subject"},
1867                        esc_html($long)) . $extra;
1868         }
1871 # Rather than recomputing the url for an email multiple times, we cache it
1872 # after the first hit. This gives a visible benefit in views where the avatar
1873 # for the same email is used repeatedly (e.g. shortlog).
1874 # The cache is shared by all avatar engines (currently gravatar only), which
1875 # are free to use it as preferred. Since only one avatar engine is used for any
1876 # given page, there's no risk for cache conflicts.
1877 our %avatar_cache = ();
1879 # Compute the picon url for a given email, by using the picon search service over at
1880 # http://www.cs.indiana.edu/picons/search.html
1881 sub picon_url {
1882         my $email = lc shift;
1883         if (!$avatar_cache{$email}) {
1884                 my ($user, $domain) = split('@', $email);
1885                 $avatar_cache{$email} =
1886                         "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1887                         "$domain/$user/" .
1888                         "users+domains+unknown/up/single";
1889         }
1890         return $avatar_cache{$email};
1893 # Compute the gravatar url for a given email, if it's not in the cache already.
1894 # Gravatar stores only the part of the URL before the size, since that's the
1895 # one computationally more expensive. This also allows reuse of the cache for
1896 # different sizes (for this particular engine).
1897 sub gravatar_url {
1898         my $email = lc shift;
1899         my $size = shift;
1900         $avatar_cache{$email} ||=
1901                 "http://www.gravatar.com/avatar/" .
1902                         Digest::MD5::md5_hex($email) . "?s=";
1903         return $avatar_cache{$email} . $size;
1906 # Insert an avatar for the given $email at the given $size if the feature
1907 # is enabled.
1908 sub git_get_avatar {
1909         my ($email, %opts) = @_;
1910         my $pre_white  = ($opts{-pad_before} ? "&nbsp;" : "");
1911         my $post_white = ($opts{-pad_after}  ? "&nbsp;" : "");
1912         $opts{-size} ||= 'default';
1913         my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1914         my $url = "";
1915         if ($git_avatar eq 'gravatar') {
1916                 $url = gravatar_url($email, $size);
1917         } elsif ($git_avatar eq 'picon') {
1918                 $url = picon_url($email);
1919         }
1920         # Other providers can be added by extending the if chain, defining $url
1921         # as needed. If no variant puts something in $url, we assume avatars
1922         # are completely disabled/unavailable.
1923         if ($url) {
1924                 return $pre_white .
1925                        "<img width=\"$size\" " .
1926                             "class=\"avatar\" " .
1927                             "src=\"".esc_url($url)."\" " .
1928                             "alt=\"\" " .
1929                        "/>" . $post_white;
1930         } else {
1931                 return "";
1932         }
1935 sub format_search_author {
1936         my ($author, $searchtype, $displaytext) = @_;
1937         my $have_search = gitweb_check_feature('search');
1939         if ($have_search) {
1940                 my $performed = "";
1941                 if ($searchtype eq 'author') {
1942                         $performed = "authored";
1943                 } elsif ($searchtype eq 'committer') {
1944                         $performed = "committed";
1945                 }
1947                 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1948                                 searchtext=>$author,
1949                                 searchtype=>$searchtype), class=>"list",
1950                                 title=>"Search for commits $performed by $author"},
1951                                 $displaytext);
1953         } else {
1954                 return $displaytext;
1955         }
1958 # format the author name of the given commit with the given tag
1959 # the author name is chopped and escaped according to the other
1960 # optional parameters (see chop_str).
1961 sub format_author_html {
1962         my $tag = shift;
1963         my $co = shift;
1964         my $author = chop_and_escape_str($co->{'author_name'}, @_);
1965         return "<$tag class=\"author\">" .
1966                format_search_author($co->{'author_name'}, "author",
1967                        git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1968                        $author) .
1969                "</$tag>";
1972 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1973 sub format_git_diff_header_line {
1974         my $line = shift;
1975         my $diffinfo = shift;
1976         my ($from, $to) = @_;
1978         if ($diffinfo->{'nparents'}) {
1979                 # combined diff
1980                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1981                 if ($to->{'href'}) {
1982                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1983                                          esc_path($to->{'file'}));
1984                 } else { # file was deleted (no href)
1985                         $line .= esc_path($to->{'file'});
1986                 }
1987         } else {
1988                 # "ordinary" diff
1989                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1990                 if ($from->{'href'}) {
1991                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1992                                          'a/' . esc_path($from->{'file'}));
1993                 } else { # file was added (no href)
1994                         $line .= 'a/' . esc_path($from->{'file'});
1995                 }
1996                 $line .= ' ';
1997                 if ($to->{'href'}) {
1998                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1999                                          'b/' . esc_path($to->{'file'}));
2000                 } else { # file was deleted
2001                         $line .= 'b/' . esc_path($to->{'file'});
2002                 }
2003         }
2005         return "<div class=\"diff header\">$line</div>\n";
2008 # format extended diff header line, before patch itself
2009 sub format_extended_diff_header_line {
2010         my $line = shift;
2011         my $diffinfo = shift;
2012         my ($from, $to) = @_;
2014         # match <path>
2015         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2016                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2017                                        esc_path($from->{'file'}));
2018         }
2019         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2020                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2021                                  esc_path($to->{'file'}));
2022         }
2023         # match single <mode>
2024         if ($line =~ m/\s(\d{6})$/) {
2025                 $line .= '<span class="info"> (' .
2026                          file_type_long($1) .
2027                          ')</span>';
2028         }
2029         # match <hash>
2030         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2031                 # can match only for combined diff
2032                 $line = 'index ';
2033                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2034                         if ($from->{'href'}[$i]) {
2035                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2036                                                   -class=>"hash"},
2037                                                  substr($diffinfo->{'from_id'}[$i],0,7));
2038                         } else {
2039                                 $line .= '0' x 7;
2040                         }
2041                         # separator
2042                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2043                 }
2044                 $line .= '..';
2045                 if ($to->{'href'}) {
2046                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2047                                          substr($diffinfo->{'to_id'},0,7));
2048                 } else {
2049                         $line .= '0' x 7;
2050                 }
2052         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2053                 # can match only for ordinary diff
2054                 my ($from_link, $to_link);
2055                 if ($from->{'href'}) {
2056                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2057                                              substr($diffinfo->{'from_id'},0,7));
2058                 } else {
2059                         $from_link = '0' x 7;
2060                 }
2061                 if ($to->{'href'}) {
2062                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2063                                            substr($diffinfo->{'to_id'},0,7));
2064                 } else {
2065                         $to_link = '0' x 7;
2066                 }
2067                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2068                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2069         }
2071         return $line . "<br/>\n";
2074 # format from-file/to-file diff header
2075 sub format_diff_from_to_header {
2076         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2077         my $line;
2078         my $result = '';
2080         $line = $from_line;
2081         #assert($line =~ m/^---/) if DEBUG;
2082         # no extra formatting for "^--- /dev/null"
2083         if (! $diffinfo->{'nparents'}) {
2084                 # ordinary (single parent) diff
2085                 if ($line =~ m!^--- "?a/!) {
2086                         if ($from->{'href'}) {
2087                                 $line = '--- a/' .
2088                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2089                                                 esc_path($from->{'file'}));
2090                         } else {
2091                                 $line = '--- a/' .
2092                                         esc_path($from->{'file'});
2093                         }
2094                 }
2095                 $result .= qq!<div class="diff from_file">$line</div>\n!;
2097         } else {
2098                 # combined diff (merge commit)
2099                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2100                         if ($from->{'href'}[$i]) {
2101                                 $line = '--- ' .
2102                                         $cgi->a({-href=>href(action=>"blobdiff",
2103                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
2104                                                              hash_parent_base=>$parents[$i],
2105                                                              file_parent=>$from->{'file'}[$i],
2106                                                              hash=>$diffinfo->{'to_id'},
2107                                                              hash_base=>$hash,
2108                                                              file_name=>$to->{'file'}),
2109                                                  -class=>"path",
2110                                                  -title=>"diff" . ($i+1)},
2111                                                 $i+1) .
2112                                         '/' .
2113                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2114                                                 esc_path($from->{'file'}[$i]));
2115                         } else {
2116                                 $line = '--- /dev/null';
2117                         }
2118                         $result .= qq!<div class="diff from_file">$line</div>\n!;
2119                 }
2120         }
2122         $line = $to_line;
2123         #assert($line =~ m/^\+\+\+/) if DEBUG;
2124         # no extra formatting for "^+++ /dev/null"
2125         if ($line =~ m!^\+\+\+ "?b/!) {
2126                 if ($to->{'href'}) {
2127                         $line = '+++ b/' .
2128                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2129                                         esc_path($to->{'file'}));
2130                 } else {
2131                         $line = '+++ b/' .
2132                                 esc_path($to->{'file'});
2133                 }
2134         }
2135         $result .= qq!<div class="diff to_file">$line</div>\n!;
2137         return $result;
2140 # create note for patch simplified by combined diff
2141 sub format_diff_cc_simplified {
2142         my ($diffinfo, @parents) = @_;
2143         my $result = '';
2145         $result .= "<div class=\"diff header\">" .
2146                    "diff --cc ";
2147         if (!is_deleted($diffinfo)) {
2148                 $result .= $cgi->a({-href => href(action=>"blob",
2149                                                   hash_base=>$hash,
2150                                                   hash=>$diffinfo->{'to_id'},
2151                                                   file_name=>$diffinfo->{'to_file'}),
2152                                     -class => "path"},
2153                                    esc_path($diffinfo->{'to_file'}));
2154         } else {
2155                 $result .= esc_path($diffinfo->{'to_file'});
2156         }
2157         $result .= "</div>\n" . # class="diff header"
2158                    "<div class=\"diff nodifferences\">" .
2159                    "Simple merge" .
2160                    "</div>\n"; # class="diff nodifferences"
2162         return $result;
2165 # format patch (diff) line (not to be used for diff headers)
2166 sub format_diff_line {
2167         my $line = shift;
2168         my ($from, $to) = @_;
2169         my $diff_class = "";
2171         chomp $line;
2173         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2174                 # combined diff
2175                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
2176                 if ($line =~ m/^\@{3}/) {
2177                         $diff_class = " chunk_header";
2178                 } elsif ($line =~ m/^\\/) {
2179                         $diff_class = " incomplete";
2180                 } elsif ($prefix =~ tr/+/+/) {
2181                         $diff_class = " add";
2182                 } elsif ($prefix =~ tr/-/-/) {
2183                         $diff_class = " rem";
2184                 }
2185         } else {
2186                 # assume ordinary diff
2187                 my $char = substr($line, 0, 1);
2188                 if ($char eq '+') {
2189                         $diff_class = " add";
2190                 } elsif ($char eq '-') {
2191                         $diff_class = " rem";
2192                 } elsif ($char eq '@') {
2193                         $diff_class = " chunk_header";
2194                 } elsif ($char eq "\\") {
2195                         $diff_class = " incomplete";
2196                 }
2197         }
2198         $line = untabify($line);
2199         if ($from && $to && $line =~ m/^\@{2} /) {
2200                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2201                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2203                 $from_lines = 0 unless defined $from_lines;
2204                 $to_lines   = 0 unless defined $to_lines;
2206                 if ($from->{'href'}) {
2207                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2208                                              -class=>"list"}, $from_text);
2209                 }
2210                 if ($to->{'href'}) {
2211                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2212                                              -class=>"list"}, $to_text);
2213                 }
2214                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2215                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2216                 return "<div class=\"diff$diff_class\">$line</div>\n";
2217         } elsif ($from && $to && $line =~ m/^\@{3}/) {
2218                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2219                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2221                 @from_text = split(' ', $ranges);
2222                 for (my $i = 0; $i < @from_text; ++$i) {
2223                         ($from_start[$i], $from_nlines[$i]) =
2224                                 (split(',', substr($from_text[$i], 1)), 0);
2225                 }
2227                 $to_text   = pop @from_text;
2228                 $to_start  = pop @from_start;
2229                 $to_nlines = pop @from_nlines;
2231                 $line = "<span class=\"chunk_info\">$prefix ";
2232                 for (my $i = 0; $i < @from_text; ++$i) {
2233                         if ($from->{'href'}[$i]) {
2234                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2235                                                   -class=>"list"}, $from_text[$i]);
2236                         } else {
2237                                 $line .= $from_text[$i];
2238                         }
2239                         $line .= " ";
2240                 }
2241                 if ($to->{'href'}) {
2242                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2243                                           -class=>"list"}, $to_text);
2244                 } else {
2245                         $line .= $to_text;
2246                 }
2247                 $line .= " $prefix</span>" .
2248                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2249                 return "<div class=\"diff$diff_class\">$line</div>\n";
2250         }
2251         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
2254 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2255 # linked.  Pass the hash of the tree/commit to snapshot.
2256 sub format_snapshot_links {
2257         my ($hash) = @_;
2258         my $num_fmts = @snapshot_fmts;
2259         if ($num_fmts > 1) {
2260                 # A parenthesized list of links bearing format names.
2261                 # e.g. "snapshot (_tar.gz_ _zip_)"
2262                 return "snapshot (" . join(' ', map
2263                         $cgi->a({
2264                                 -href => href(
2265                                         action=>"snapshot",
2266                                         hash=>$hash,
2267                                         snapshot_format=>$_
2268                                 )
2269                         }, $known_snapshot_formats{$_}{'display'})
2270                 , @snapshot_fmts) . ")";
2271         } elsif ($num_fmts == 1) {
2272                 # A single "snapshot" link whose tooltip bears the format name.
2273                 # i.e. "_snapshot_"
2274                 my ($fmt) = @snapshot_fmts;
2275                 return
2276                         $cgi->a({
2277                                 -href => href(
2278                                         action=>"snapshot",
2279                                         hash=>$hash,
2280                                         snapshot_format=>$fmt
2281                                 ),
2282                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2283                         }, "snapshot");
2284         } else { # $num_fmts == 0
2285                 return undef;
2286         }
2289 ## ......................................................................
2290 ## functions returning values to be passed, perhaps after some
2291 ## transformation, to other functions; e.g. returning arguments to href()
2293 # returns hash to be passed to href to generate gitweb URL
2294 # in -title key it returns description of link
2295 sub get_feed_info {
2296         my $format = shift || 'Atom';
2297         my %res = (action => lc($format));
2299         # feed links are possible only for project views
2300         return unless (defined $project);
2301         # some views should link to OPML, or to generic project feed,
2302         # or don't have specific feed yet (so they should use generic)
2303         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
2305         my $branch;
2306         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2307         # from tag links; this also makes possible to detect branch links
2308         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2309             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
2310                 $branch = $1;
2311         }
2312         # find log type for feed description (title)
2313         my $type = 'log';
2314         if (defined $file_name) {
2315                 $type  = "history of $file_name";
2316                 $type .= "/" if ($action eq 'tree');
2317                 $type .= " on '$branch'" if (defined $branch);
2318         } else {
2319                 $type = "log of $branch" if (defined $branch);
2320         }
2322         $res{-title} = $type;
2323         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2324         $res{'file_name'} = $file_name;
2326         return %res;
2329 ## ----------------------------------------------------------------------
2330 ## git utility subroutines, invoking git commands
2332 # returns path to the core git executable and the --git-dir parameter as list
2333 sub git_cmd {
2334         $number_of_git_cmds++;
2335         return $GIT, '--git-dir='.$git_dir;
2338 # quote the given arguments for passing them to the shell
2339 # quote_command("command", "arg 1", "arg with ' and ! characters")
2340 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2341 # Try to avoid using this function wherever possible.
2342 sub quote_command {
2343         return join(' ',
2344                 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2347 # get HEAD ref of given project as hash
2348 sub git_get_head_hash {
2349         return git_get_full_hash(shift, 'HEAD');
2352 sub git_get_full_hash {
2353         return git_get_hash(@_);
2356 sub git_get_short_hash {
2357         return git_get_hash(@_, '--short=7');
2360 sub git_get_hash {
2361         my ($project, $hash, @options) = @_;
2362         my $o_git_dir = $git_dir;
2363         my $retval = undef;
2364         $git_dir = "$projectroot/$project";
2365         if (open my $fd, '-|', git_cmd(), 'rev-parse',
2366             '--verify', '-q', @options, $hash) {
2367                 $retval = <$fd>;
2368                 chomp $retval if defined $retval;
2369                 close $fd;
2370         }
2371         if (defined $o_git_dir) {
2372                 $git_dir = $o_git_dir;
2373         }
2374         return $retval;
2377 # get type of given object
2378 sub git_get_type {
2379         my $hash = shift;
2381         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2382         my $type = <$fd>;
2383         close $fd or return;
2384         chomp $type;
2385         return $type;
2388 # repository configuration
2389 our $config_file = '';
2390 our %config;
2392 # store multiple values for single key as anonymous array reference
2393 # single values stored directly in the hash, not as [ <value> ]
2394 sub hash_set_multi {
2395         my ($hash, $key, $value) = @_;
2397         if (!exists $hash->{$key}) {
2398                 $hash->{$key} = $value;
2399         } elsif (!ref $hash->{$key}) {
2400                 $hash->{$key} = [ $hash->{$key}, $value ];
2401         } else {
2402                 push @{$hash->{$key}}, $value;
2403         }
2406 # return hash of git project configuration
2407 # optionally limited to some section, e.g. 'gitweb'
2408 sub git_parse_project_config {
2409         my $section_regexp = shift;
2410         my %config;
2412         local $/ = "\0";
2414         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2415                 or return;
2417         while (my $keyval = <$fh>) {
2418                 chomp $keyval;
2419                 my ($key, $value) = split(/\n/, $keyval, 2);
2421                 hash_set_multi(\%config, $key, $value)
2422                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2423         }
2424         close $fh;
2426         return %config;
2429 # convert config value to boolean: 'true' or 'false'
2430 # no value, number > 0, 'true' and 'yes' values are true
2431 # rest of values are treated as false (never as error)
2432 sub config_to_bool {
2433         my $val = shift;
2435         return 1 if !defined $val;             # section.key
2437         # strip leading and trailing whitespace
2438         $val =~ s/^\s+//;
2439         $val =~ s/\s+$//;
2441         return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
2442                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
2445 # convert config value to simple decimal number
2446 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2447 # to be multiplied by 1024, 1048576, or 1073741824
2448 sub config_to_int {
2449         my $val = shift;
2451         # strip leading and trailing whitespace
2452         $val =~ s/^\s+//;
2453         $val =~ s/\s+$//;
2455         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2456                 $unit = lc($unit);
2457                 # unknown unit is treated as 1
2458                 return $num * ($unit eq 'g' ? 1073741824 :
2459                                $unit eq 'm' ?    1048576 :
2460                                $unit eq 'k' ?       1024 : 1);
2461         }
2462         return $val;
2465 # convert config value to array reference, if needed
2466 sub config_to_multi {
2467         my $val = shift;
2469         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2472 sub git_get_project_config {
2473         my ($key, $type) = @_;
2475         return unless defined $git_dir;
2477         # key sanity check
2478         return unless ($key);
2479         $key =~ s/^gitweb\.//;
2480         return if ($key =~ m/\W/);
2482         # type sanity check
2483         if (defined $type) {
2484                 $type =~ s/^--//;
2485                 $type = undef
2486                         unless ($type eq 'bool' || $type eq 'int');
2487         }
2489         # get config
2490         if (!defined $config_file ||
2491             $config_file ne "$git_dir/config") {
2492                 %config = git_parse_project_config('gitweb');
2493                 $config_file = "$git_dir/config";
2494         }
2496         # check if config variable (key) exists
2497         return unless exists $config{"gitweb.$key"};
2499         # ensure given type
2500         if (!defined $type) {
2501                 return $config{"gitweb.$key"};
2502         } elsif ($type eq 'bool') {
2503                 # backward compatibility: 'git config --bool' returns true/false
2504                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2505         } elsif ($type eq 'int') {
2506                 return config_to_int($config{"gitweb.$key"});
2507         }
2508         return $config{"gitweb.$key"};
2511 # get hash of given path at given ref
2512 sub git_get_hash_by_path {
2513         my $base = shift;
2514         my $path = shift || return undef;
2515         my $type = shift;
2517         $path =~ s,/+$,,;
2519         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2520                 or die_error(500, "Open git-ls-tree failed");
2521         my $line = <$fd>;
2522         close $fd or return undef;
2524         if (!defined $line) {
2525                 # there is no tree or hash given by $path at $base
2526                 return undef;
2527         }
2529         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2530         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2531         if (defined $type && $type ne $2) {
2532                 # type doesn't match
2533                 return undef;
2534         }
2535         return $3;
2538 # get path of entry with given hash at given tree-ish (ref)
2539 # used to get 'from' filename for combined diff (merge commit) for renames
2540 sub git_get_path_by_hash {
2541         my $base = shift || return;
2542         my $hash = shift || return;
2544         local $/ = "\0";
2546         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2547                 or return undef;
2548         while (my $line = <$fd>) {
2549                 chomp $line;
2551                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2552                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2553                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2554                         close $fd;
2555                         return $1;
2556                 }
2557         }
2558         close $fd;
2559         return undef;
2562 ## ......................................................................
2563 ## git utility functions, directly accessing git repository
2565 sub git_get_project_description {
2566         my $path = shift;
2568         $git_dir = "$projectroot/$path";
2569         open my $fd, '<', "$git_dir/description"
2570                 or return git_get_project_config('description');
2571         my $descr = <$fd>;
2572         close $fd;
2573         if (defined $descr) {
2574                 chomp $descr;
2575         }
2576         return $descr;
2579 # supported formats:
2580 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2581 #   - if its contents is a number, use it as tag weight,
2582 #   - otherwise add a tag with weight 1
2583 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2584 #   the same value multiple times increases tag weight
2585 # * `gitweb.ctag' multi-valued repo config variable
2586 sub git_get_project_ctags {
2587         my $project = shift;
2588         my $ctags = {};
2590         $git_dir = "$projectroot/$project";
2591         if (opendir my $dh, "$git_dir/ctags") {
2592                 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2593                 foreach my $tagfile (@files) {
2594                         open my $ct, '<', $tagfile
2595                                 or next;
2596                         my $val = <$ct>;
2597                         chomp $val if $val;
2598                         close $ct;
2600                         (my $ctag = $tagfile) =~ s#.*/##;
2601                         if ($val =~ /\d+/) {
2602                                 $ctags->{$ctag} = $val;
2603                         } else {
2604                                 $ctags->{$ctag} = 1;
2605                         }
2606                 }
2607                 closedir $dh;
2609         } elsif (open my $fh, '<', "$git_dir/ctags") {
2610                 while (my $line = <$fh>) {
2611                         chomp $line;
2612                         $ctags->{$line}++ if $line;
2613                 }
2614                 close $fh;
2616         } else {
2617                 my $taglist = config_to_multi(git_get_project_config('ctag'));
2618                 foreach my $tag (@$taglist) {
2619                         $ctags->{$tag}++;
2620                 }
2621         }
2623         return $ctags;
2626 # return hash, where keys are content tags ('ctags'),
2627 # and values are sum of weights of given tag in every project
2628 sub git_gather_all_ctags {
2629         my $projects = shift;
2630         my $ctags = {};
2632         foreach my $p (@$projects) {
2633                 foreach my $ct (keys %{$p->{'ctags'}}) {
2634                         $ctags->{$ct} += $p->{'ctags'}->{$ct};
2635                 }
2636         }
2638         return $ctags;
2641 sub git_populate_project_tagcloud {
2642         my $ctags = shift;
2644         # First, merge different-cased tags; tags vote on casing
2645         my %ctags_lc;
2646         foreach (keys %$ctags) {
2647                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2648                 if (not $ctags_lc{lc $_}->{topcount}
2649                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2650                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2651                         $ctags_lc{lc $_}->{topname} = $_;
2652                 }
2653         }
2655         my $cloud;
2656         if (eval { require HTML::TagCloud; 1; }) {
2657                 $cloud = HTML::TagCloud->new;
2658                 foreach my $ctag (sort keys %ctags_lc) {
2659                         # Pad the title with spaces so that the cloud looks
2660                         # less crammed.
2661                         my $title = esc_html($ctags_lc{$ctag}->{topname});
2662                         $title =~ s/ /&nbsp;/g;
2663                         $title =~ s/^/&nbsp;/g;
2664                         $title =~ s/$/&nbsp;/g;
2665                         $cloud->add($title, href(project=>undef, ctag=>$ctag),
2666                                     $ctags_lc{$ctag}->{count});
2667                 }
2668         } else {
2669                 $cloud = {};
2670                 foreach my $ctag (keys %ctags_lc) {
2671                         my $title = $ctags_lc{$ctag}->{topname};
2672                         $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2673                         $cloud->{$ctag}{ctag} =
2674                                 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)},
2675                                   esc_html($title, -nbsp=>1));
2676                 }
2677         }
2678         return $cloud;
2681 sub git_show_project_tagcloud {
2682         my ($cloud, $count) = @_;
2683         if (ref $cloud eq 'HTML::TagCloud') {
2684                 return $cloud->html_and_css($count);
2685         } else {
2686                 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2687                 return
2688                         '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2689                         join (', ', map {
2690                                 $cloud->{$_}->{'ctag'}
2691                         } splice(@tags, 0, $count)) .
2692                         '</div>';
2693         }
2696 sub git_get_project_url_list {
2697         my $path = shift;
2699         $git_dir = "$projectroot/$path";
2700         open my $fd, '<', "$git_dir/cloneurl"
2701                 or return wantarray ?
2702                 @{ config_to_multi(git_get_project_config('url')) } :
2703                    config_to_multi(git_get_project_config('url'));
2704         my @git_project_url_list = map { chomp; $_ } <$fd>;
2705         close $fd;
2707         return wantarray ? @git_project_url_list : \@git_project_url_list;
2710 sub git_get_projects_list {
2711         my $filter = shift || '';
2712         my @list;
2714         $filter =~ s/\.git$//;
2716         if (-d $projects_list) {
2717                 # search in directory
2718                 my $dir = $projects_list;
2719                 # remove the trailing "/"
2720                 $dir =~ s!/+$!!;
2721                 my $pfxlen = length("$projects_list");
2722                 my $pfxdepth = ($projects_list =~ tr!/!!);
2723                 # when filtering, search only given subdirectory
2724                 if ($filter) {
2725                         $dir .= "/$filter";
2726                         $dir =~ s!/+$!!;
2727                 }
2729                 File::Find::find({
2730                         follow_fast => 1, # follow symbolic links
2731                         follow_skip => 2, # ignore duplicates
2732                         dangling_symlinks => 0, # ignore dangling symlinks, silently
2733                         wanted => sub {
2734                                 # global variables
2735                                 our $project_maxdepth;
2736                                 our $projectroot;
2737                                 # skip project-list toplevel, if we get it.
2738                                 return if (m!^[/.]$!);
2739                                 # only directories can be git repositories
2740                                 return unless (-d $_);
2741                                 # don't traverse too deep (Find is super slow on os x)
2742                                 # $project_maxdepth excludes depth of $projectroot
2743                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2744                                         $File::Find::prune = 1;
2745                                         return;
2746                                 }
2748                                 my $path = substr($File::Find::name, $pfxlen + 1);
2749                                 # we check related file in $projectroot
2750                                 if (check_export_ok("$projectroot/$path")) {
2751                                         push @list, { path => $path };
2752                                         $File::Find::prune = 1;
2753                                 }
2754                         },
2755                 }, "$dir");
2757         } elsif (-f $projects_list) {
2758                 # read from file(url-encoded):
2759                 # 'git%2Fgit.git Linus+Torvalds'
2760                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2761                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2762                 open my $fd, '<', $projects_list or return;
2763         PROJECT:
2764                 while (my $line = <$fd>) {
2765                         chomp $line;
2766                         my ($path, $owner) = split ' ', $line;
2767                         $path = unescape($path);
2768                         $owner = unescape($owner);
2769                         if (!defined $path) {
2770                                 next;
2771                         }
2772                         # if $filter is rpovided, check if $path begins with $filter
2773                         if ($filter && $path !~ m!^\Q$filter\E/!) {
2774                                 next;
2775                         }
2776                         if (check_export_ok("$projectroot/$path")) {
2777                                 my $pr = {
2778                                         path => $path,
2779                                         owner => to_utf8($owner),
2780                                 };
2781                                 push @list, $pr;
2782                         }
2783                 }
2784                 close $fd;
2785         }
2786         return @list;
2789 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
2790 # as side effects it sets 'forks' field to list of forks for forked projects
2791 sub filter_forks_from_projects_list {
2792         my $projects = shift;
2794         my %trie; # prefix tree of directories (path components)
2795         # generate trie out of those directories that might contain forks
2796         foreach my $pr (@$projects) {
2797                 my $path = $pr->{'path'};
2798                 $path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
2799                 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
2800                 next unless ($path);      # skip '.git' repository: tests, git-instaweb
2801                 next unless (-d $path);   # containing directory exists
2802                 $pr->{'forks'} = [];      # there can be 0 or more forks of project
2804                 # add to trie
2805                 my @dirs = split('/', $path);
2806                 # walk the trie, until either runs out of components or out of trie
2807                 my $ref = \%trie;
2808                 while (scalar @dirs &&
2809                        exists($ref->{$dirs[0]})) {
2810                         $ref = $ref->{shift @dirs};
2811                 }
2812                 # create rest of trie structure from rest of components
2813                 foreach my $dir (@dirs) {
2814                         $ref = $ref->{$dir} = {};
2815                 }
2816                 # create end marker, store $pr as a data
2817                 $ref->{''} = $pr if (!exists $ref->{''});
2818         }
2820         # filter out forks, by finding shortest prefix match for paths
2821         my @filtered;
2822  PROJECT:
2823         foreach my $pr (@$projects) {
2824                 # trie lookup
2825                 my $ref = \%trie;
2826         DIR:
2827                 foreach my $dir (split('/', $pr->{'path'})) {
2828                         if (exists $ref->{''}) {
2829                                 # found [shortest] prefix, is a fork - skip it
2830                                 push @{$ref->{''}{'forks'}}, $pr;
2831                                 next PROJECT;
2832                         }
2833                         if (!exists $ref->{$dir}) {
2834                                 # not in trie, cannot have prefix, not a fork
2835                                 push @filtered, $pr;
2836                                 next PROJECT;
2837                         }
2838                         # If the dir is there, we just walk one step down the trie.
2839                         $ref = $ref->{$dir};
2840                 }
2841                 # we ran out of trie
2842                 # (shouldn't happen: it's either no match, or end marker)
2843                 push @filtered, $pr;
2844         }
2846         return @filtered;
2849 # note: fill_project_list_info must be run first,
2850 # for 'descr_long' and 'ctags' to be filled
2851 sub search_projects_list {
2852         my ($projlist, %opts) = @_;
2853         my $tagfilter  = $opts{'tagfilter'};
2854         my $searchtext = $opts{'searchtext'};
2856         return @$projlist
2857                 unless ($tagfilter || $searchtext);
2859         my @projects;
2860  PROJECT:
2861         foreach my $pr (@$projlist) {
2863                 if ($tagfilter) {
2864                         next unless ref($pr->{'ctags'}) eq 'HASH';
2865                         next unless
2866                                 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
2867                 }
2869                 if ($searchtext) {
2870                         next unless
2871                                 $pr->{'path'} =~ /$searchtext/ ||
2872                                 $pr->{'descr_long'} =~ /$searchtext/;
2873                 }
2875                 push @projects, $pr;
2876         }
2878         return @projects;
2881 our $gitweb_project_owner = undef;
2882 sub git_get_project_list_from_file {
2884         return if (defined $gitweb_project_owner);
2886         $gitweb_project_owner = {};
2887         # read from file (url-encoded):
2888         # 'git%2Fgit.git Linus+Torvalds'
2889         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2890         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2891         if (-f $projects_list) {
2892                 open(my $fd, '<', $projects_list);
2893                 while (my $line = <$fd>) {
2894                         chomp $line;
2895                         my ($pr, $ow) = split ' ', $line;
2896                         $pr = unescape($pr);
2897                         $ow = unescape($ow);
2898                         $gitweb_project_owner->{$pr} = to_utf8($ow);
2899                 }
2900                 close $fd;
2901         }
2904 sub git_get_project_owner {
2905         my $project = shift;
2906         my $owner;
2908         return undef unless $project;
2909         $git_dir = "$projectroot/$project";
2911         if (!defined $gitweb_project_owner) {
2912                 git_get_project_list_from_file();
2913         }
2915         if (exists $gitweb_project_owner->{$project}) {
2916                 $owner = $gitweb_project_owner->{$project};
2917         }
2918         if (!defined $owner){
2919                 $owner = git_get_project_config('owner');
2920         }
2921         if (!defined $owner) {
2922                 $owner = get_file_owner("$git_dir");
2923         }
2925         return $owner;
2928 sub git_get_last_activity {
2929         my ($path) = @_;
2930         my $fd;
2932         $git_dir = "$projectroot/$path";
2933         open($fd, "-|", git_cmd(), 'for-each-ref',
2934              '--format=%(committer)',
2935              '--sort=-committerdate',
2936              '--count=1',
2937              'refs/heads') or return;
2938         my $most_recent = <$fd>;
2939         close $fd or return;
2940         if (defined $most_recent &&
2941             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2942                 my $timestamp = $1;
2943                 my $age = time - $timestamp;
2944                 return ($age, age_string($age));
2945         }
2946         return (undef, undef);
2949 # Implementation note: when a single remote is wanted, we cannot use 'git
2950 # remote show -n' because that command always work (assuming it's a remote URL
2951 # if it's not defined), and we cannot use 'git remote show' because that would
2952 # try to make a network roundtrip. So the only way to find if that particular
2953 # remote is defined is to walk the list provided by 'git remote -v' and stop if
2954 # and when we find what we want.
2955 sub git_get_remotes_list {
2956         my $wanted = shift;
2957         my %remotes = ();
2959         open my $fd, '-|' , git_cmd(), 'remote', '-v';
2960         return unless $fd;
2961         while (my $remote = <$fd>) {
2962                 chomp $remote;
2963                 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
2964                 next if $wanted and not $remote eq $wanted;
2965                 my ($url, $key) = ($1, $2);
2967                 $remotes{$remote} ||= { 'heads' => () };
2968                 $remotes{$remote}{$key} = $url;
2969         }
2970         close $fd or return;
2971         return wantarray ? %remotes : \%remotes;
2974 # Takes a hash of remotes as first parameter and fills it by adding the
2975 # available remote heads for each of the indicated remotes.
2976 sub fill_remote_heads {
2977         my $remotes = shift;
2978         my @heads = map { "remotes/$_" } keys %$remotes;
2979         my @remoteheads = git_get_heads_list(undef, @heads);
2980         foreach my $remote (keys %$remotes) {
2981                 $remotes->{$remote}{'heads'} = [ grep {
2982                         $_->{'name'} =~ s!^$remote/!!
2983                         } @remoteheads ];
2984         }
2987 sub git_get_references {
2988         my $type = shift || "";
2989         my %refs;
2990         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2991         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2992         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2993                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2994                 or return;
2996         while (my $line = <$fd>) {
2997                 chomp $line;
2998                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2999                         if (defined $refs{$1}) {
3000                                 push @{$refs{$1}}, $2;
3001                         } else {
3002                                 $refs{$1} = [ $2 ];
3003                         }
3004                 }
3005         }
3006         close $fd or return;
3007         return \%refs;
3010 sub git_get_rev_name_tags {
3011         my $hash = shift || return undef;
3013         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3014                 or return;
3015         my $name_rev = <$fd>;
3016         close $fd;
3018         if ($name_rev =~ m|^$hash tags/(.*)$|) {
3019                 return $1;
3020         } else {
3021                 # catches also '$hash undefined' output
3022                 return undef;
3023         }
3026 ## ----------------------------------------------------------------------
3027 ## parse to hash functions
3029 sub parse_date {
3030         my $epoch = shift;
3031         my $tz = shift || "-0000";
3033         my %date;
3034         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3035         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3036         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3037         $date{'hour'} = $hour;
3038         $date{'minute'} = $min;
3039         $date{'mday'} = $mday;
3040         $date{'day'} = $days[$wday];
3041         $date{'month'} = $months[$mon];
3042         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3043                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3044         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3045                              $mday, $months[$mon], $hour ,$min;
3046         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3047                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3049         my ($tz_sign, $tz_hour, $tz_min) =
3050                 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3051         $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3052         my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3053         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3054         $date{'hour_local'} = $hour;
3055         $date{'minute_local'} = $min;
3056         $date{'tz_local'} = $tz;
3057         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3058                                   1900+$year, $mon+1, $mday,
3059                                   $hour, $min, $sec, $tz);
3060         return %date;
3063 sub parse_tag {
3064         my $tag_id = shift;
3065         my %tag;
3066         my @comment;
3068         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3069         $tag{'id'} = $tag_id;
3070         while (my $line = <$fd>) {
3071                 chomp $line;
3072                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3073                         $tag{'object'} = $1;
3074                 } elsif ($line =~ m/^type (.+)$/) {
3075                         $tag{'type'} = $1;
3076                 } elsif ($line =~ m/^tag (.+)$/) {
3077                         $tag{'name'} = $1;
3078                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3079                         $tag{'author'} = $1;
3080                         $tag{'author_epoch'} = $2;
3081                         $tag{'author_tz'} = $3;
3082                         if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3083                                 $tag{'author_name'}  = $1;
3084                                 $tag{'author_email'} = $2;
3085                         } else {
3086                                 $tag{'author_name'} = $tag{'author'};
3087                         }
3088                 } elsif ($line =~ m/--BEGIN/) {
3089                         push @comment, $line;
3090                         last;
3091                 } elsif ($line eq "") {
3092                         last;
3093                 }
3094         }
3095         push @comment, <$fd>;
3096         $tag{'comment'} = \@comment;
3097         close $fd or return;
3098         if (!defined $tag{'name'}) {
3099                 return
3100         };
3101         return %tag
3104 sub parse_commit_text {
3105         my ($commit_text, $withparents) = @_;
3106         my @commit_lines = split '\n', $commit_text;
3107         my %co;
3109         pop @commit_lines; # Remove '\0'
3111         if (! @commit_lines) {
3112                 return;
3113         }
3115         my $header = shift @commit_lines;
3116         if ($header !~ m/^[0-9a-fA-F]{40}/) {
3117                 return;
3118         }
3119         ($co{'id'}, my @parents) = split ' ', $header;
3120         while (my $line = shift @commit_lines) {
3121                 last if $line eq "\n";
3122                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3123                         $co{'tree'} = $1;
3124                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3125                         push @parents, $1;
3126                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3127                         $co{'author'} = to_utf8($1);
3128                         $co{'author_epoch'} = $2;
3129                         $co{'author_tz'} = $3;
3130                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3131                                 $co{'author_name'}  = $1;
3132                                 $co{'author_email'} = $2;
3133                         } else {
3134                                 $co{'author_name'} = $co{'author'};
3135                         }
3136                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3137                         $co{'committer'} = to_utf8($1);
3138                         $co{'committer_epoch'} = $2;
3139                         $co{'committer_tz'} = $3;
3140                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3141                                 $co{'committer_name'}  = $1;
3142                                 $co{'committer_email'} = $2;
3143                         } else {
3144                                 $co{'committer_name'} = $co{'committer'};
3145                         }
3146                 }
3147         }
3148         if (!defined $co{'tree'}) {
3149                 return;
3150         };
3151         $co{'parents'} = \@parents;
3152         $co{'parent'} = $parents[0];
3154         foreach my $title (@commit_lines) {
3155                 $title =~ s/^    //;
3156                 if ($title ne "") {
3157                         $co{'title'} = chop_str($title, 80, 5);
3158                         # remove leading stuff of merges to make the interesting part visible
3159                         if (length($title) > 50) {
3160                                 $title =~ s/^Automatic //;
3161                                 $title =~ s/^merge (of|with) /Merge ... /i;
3162                                 if (length($title) > 50) {
3163                                         $title =~ s/(http|rsync):\/\///;
3164                                 }
3165                                 if (length($title) > 50) {
3166                                         $title =~ s/(master|www|rsync)\.//;
3167                                 }
3168                                 if (length($title) > 50) {
3169                                         $title =~ s/kernel.org:?//;
3170                                 }
3171                                 if (length($title) > 50) {
3172                                         $title =~ s/\/pub\/scm//;
3173                                 }
3174                         }
3175                         $co{'title_short'} = chop_str($title, 50, 5);
3176                         last;
3177                 }
3178         }
3179         if (! defined $co{'title'} || $co{'title'} eq "") {
3180                 $co{'title'} = $co{'title_short'} = '(no commit message)';
3181         }
3182         # remove added spaces
3183         foreach my $line (@commit_lines) {
3184                 $line =~ s/^    //;
3185         }
3186         $co{'comment'} = \@commit_lines;
3188         my $age = time - $co{'committer_epoch'};
3189         $co{'age'} = $age;
3190         $co{'age_string'} = age_string($age);
3191         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3192         if ($age > 60*60*24*7*2) {
3193                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3194                 $co{'age_string_age'} = $co{'age_string'};
3195         } else {
3196                 $co{'age_string_date'} = $co{'age_string'};
3197                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3198         }
3199         return %co;
3202 sub parse_commit {
3203         my ($commit_id) = @_;
3204         my %co;
3206         local $/ = "\0";
3208         open my $fd, "-|", git_cmd(), "rev-list",
3209                 "--parents",
3210                 "--header",
3211                 "--max-count=1",
3212                 $commit_id,
3213                 "--",
3214                 or die_error(500, "Open git-rev-list failed");
3215         %co = parse_commit_text(<$fd>, 1);
3216         close $fd;
3218         return %co;
3221 sub parse_commits {
3222         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3223         my @cos;
3225         $maxcount ||= 1;
3226         $skip ||= 0;
3228         local $/ = "\0";
3230         open my $fd, "-|", git_cmd(), "rev-list",
3231                 "--header",
3232                 @args,
3233                 ("--max-count=" . $maxcount),
3234                 ("--skip=" . $skip),
3235                 @extra_options,
3236                 $commit_id,
3237                 "--",
3238                 ($filename ? ($filename) : ())
3239                 or die_error(500, "Open git-rev-list failed");
3240         while (my $line = <$fd>) {
3241                 my %co = parse_commit_text($line);
3242                 push @cos, \%co;
3243         }
3244         close $fd;
3246         return wantarray ? @cos : \@cos;
3249 # parse line of git-diff-tree "raw" output
3250 sub parse_difftree_raw_line {
3251         my $line = shift;
3252         my %res;
3254         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
3255         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
3256         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3257                 $res{'from_mode'} = $1;
3258                 $res{'to_mode'} = $2;
3259                 $res{'from_id'} = $3;
3260                 $res{'to_id'} = $4;
3261                 $res{'status'} = $5;
3262                 $res{'similarity'} = $6;
3263                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3264                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3265                 } else {
3266                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3267                 }
3268         }
3269         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3270         # combined diff (for merge commit)
3271         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3272                 $res{'nparents'}  = length($1);
3273                 $res{'from_mode'} = [ split(' ', $2) ];
3274                 $res{'to_mode'} = pop @{$res{'from_mode'}};
3275                 $res{'from_id'} = [ split(' ', $3) ];
3276                 $res{'to_id'} = pop @{$res{'from_id'}};
3277                 $res{'status'} = [ split('', $4) ];
3278                 $res{'to_file'} = unquote($5);
3279         }
3280         # 'c512b523472485aef4fff9e57b229d9d243c967f'
3281         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3282                 $res{'commit'} = $1;
3283         }
3285         return wantarray ? %res : \%res;
3288 # wrapper: return parsed line of git-diff-tree "raw" output
3289 # (the argument might be raw line, or parsed info)
3290 sub parsed_difftree_line {
3291         my $line_or_ref = shift;
3293         if (ref($line_or_ref) eq "HASH") {
3294                 # pre-parsed (or generated by hand)
3295                 return $line_or_ref;
3296         } else {
3297                 return parse_difftree_raw_line($line_or_ref);
3298         }
3301 # parse line of git-ls-tree output
3302 sub parse_ls_tree_line {
3303         my $line = shift;
3304         my %opts = @_;
3305         my %res;
3307         if ($opts{'-l'}) {
3308                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717  panic.c'
3309                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3311                 $res{'mode'} = $1;
3312                 $res{'type'} = $2;
3313                 $res{'hash'} = $3;
3314                 $res{'size'} = $4;
3315                 if ($opts{'-z'}) {
3316                         $res{'name'} = $5;
3317                 } else {
3318                         $res{'name'} = unquote($5);
3319                 }
3320         } else {
3321                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
3322                 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3324                 $res{'mode'} = $1;
3325                 $res{'type'} = $2;
3326                 $res{'hash'} = $3;
3327                 if ($opts{'-z'}) {
3328                         $res{'name'} = $4;
3329                 } else {
3330                         $res{'name'} = unquote($4);
3331                 }
3332         }
3334         return wantarray ? %res : \%res;
3337 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3338 sub parse_from_to_diffinfo {
3339         my ($diffinfo, $from, $to, @parents) = @_;
3341         if ($diffinfo->{'nparents'}) {
3342                 # combined diff
3343                 $from->{'file'} = [];
3344                 $from->{'href'} = [];
3345                 fill_from_file_info($diffinfo, @parents)
3346                         unless exists $diffinfo->{'from_file'};
3347                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3348                         $from->{'file'}[$i] =
3349                                 defined $diffinfo->{'from_file'}[$i] ?
3350                                         $diffinfo->{'from_file'}[$i] :
3351                                         $diffinfo->{'to_file'};
3352                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3353                                 $from->{'href'}[$i] = href(action=>"blob",
3354                                                            hash_base=>$parents[$i],
3355                                                            hash=>$diffinfo->{'from_id'}[$i],
3356                                                            file_name=>$from->{'file'}[$i]);
3357                         } else {
3358                                 $from->{'href'}[$i] = undef;
3359                         }
3360                 }
3361         } else {
3362                 # ordinary (not combined) diff
3363                 $from->{'file'} = $diffinfo->{'from_file'};
3364                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3365                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3366                                                hash=>$diffinfo->{'from_id'},
3367                                                file_name=>$from->{'file'});
3368                 } else {
3369                         delete $from->{'href'};
3370                 }
3371         }
3373         $to->{'file'} = $diffinfo->{'to_file'};
3374         if (!is_deleted($diffinfo)) { # file exists in result
3375                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3376                                      hash=>$diffinfo->{'to_id'},
3377                                      file_name=>$to->{'file'});
3378         } else {
3379                 delete $to->{'href'};
3380         }
3383 ## ......................................................................
3384 ## parse to array of hashes functions
3386 sub git_get_heads_list {
3387         my ($limit, @classes) = @_;
3388         @classes = ('heads') unless @classes;
3389         my @patterns = map { "refs/$_" } @classes;
3390         my @headslist;
3392         open my $fd, '-|', git_cmd(), 'for-each-ref',
3393                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3394                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3395                 @patterns
3396                 or return;
3397         while (my $line = <$fd>) {
3398                 my %ref_item;
3400                 chomp $line;
3401                 my ($refinfo, $committerinfo) = split(/\0/, $line);
3402                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3403                 my ($committer, $epoch, $tz) =
3404                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3405                 $ref_item{'fullname'}  = $name;
3406                 $name =~ s!^refs/(?:head|remote)s/!!;
3408                 $ref_item{'name'}  = $name;
3409                 $ref_item{'id'}    = $hash;
3410                 $ref_item{'title'} = $title || '(no commit message)';
3411                 $ref_item{'epoch'} = $epoch;
3412                 if ($epoch) {
3413                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3414                 } else {
3415                         $ref_item{'age'} = "unknown";
3416                 }
3418                 push @headslist, \%ref_item;
3419         }
3420         close $fd;
3422         return wantarray ? @headslist : \@headslist;
3425 sub git_get_tags_list {
3426         my $limit = shift;
3427         my @tagslist;
3429         open my $fd, '-|', git_cmd(), 'for-each-ref',
3430                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3431                 '--format=%(objectname) %(objecttype) %(refname) '.
3432                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3433                 'refs/tags'
3434                 or return;
3435         while (my $line = <$fd>) {
3436                 my %ref_item;
3438                 chomp $line;
3439                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3440                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3441                 my ($creator, $epoch, $tz) =
3442                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3443                 $ref_item{'fullname'} = $name;
3444                 $name =~ s!^refs/tags/!!;
3446                 $ref_item{'type'} = $type;
3447                 $ref_item{'id'} = $id;
3448                 $ref_item{'name'} = $name;
3449                 if ($type eq "tag") {
3450                         $ref_item{'subject'} = $title;
3451                         $ref_item{'reftype'} = $reftype;
3452                         $ref_item{'refid'}   = $refid;
3453                 } else {
3454                         $ref_item{'reftype'} = $type;
3455                         $ref_item{'refid'}   = $id;
3456                 }
3458                 if ($type eq "tag" || $type eq "commit") {
3459                         $ref_item{'epoch'} = $epoch;
3460                         if ($epoch) {
3461                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3462                         } else {
3463                                 $ref_item{'age'} = "unknown";
3464                         }
3465                 }
3467                 push @tagslist, \%ref_item;
3468         }
3469         close $fd;
3471         return wantarray ? @tagslist : \@tagslist;
3474 ## ----------------------------------------------------------------------
3475 ## filesystem-related functions
3477 sub get_file_owner {
3478         my $path = shift;
3480         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3481         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3482         if (!defined $gcos) {
3483                 return undef;
3484         }
3485         my $owner = $gcos;
3486         $owner =~ s/[,;].*$//;
3487         return to_utf8($owner);
3490 # assume that file exists
3491 sub insert_file {
3492         my $filename = shift;
3494         open my $fd, '<', $filename;
3495         print map { to_utf8($_) } <$fd>;
3496         close $fd;
3499 ## ......................................................................
3500 ## mimetype related functions
3502 sub mimetype_guess_file {
3503         my $filename = shift;
3504         my $mimemap = shift;
3505         -r $mimemap or return undef;
3507         my %mimemap;
3508         open(my $mh, '<', $mimemap) or return undef;
3509         while (<$mh>) {
3510                 next if m/^#/; # skip comments
3511                 my ($mimetype, $exts) = split(/\t+/);
3512                 if (defined $exts) {
3513                         my @exts = split(/\s+/, $exts);
3514                         foreach my $ext (@exts) {
3515                                 $mimemap{$ext} = $mimetype;
3516                         }
3517                 }
3518         }
3519         close($mh);
3521         $filename =~ /\.([^.]*)$/;
3522         return $mimemap{$1};
3525 sub mimetype_guess {
3526         my $filename = shift;
3527         my $mime;
3528         $filename =~ /\./ or return undef;
3530         if ($mimetypes_file) {
3531                 my $file = $mimetypes_file;
3532                 if ($file !~ m!^/!) { # if it is relative path
3533                         # it is relative to project
3534                         $file = "$projectroot/$project/$file";
3535                 }
3536                 $mime = mimetype_guess_file($filename, $file);
3537         }
3538         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3539         return $mime;
3542 sub blob_mimetype {
3543         my $fd = shift;
3544         my $filename = shift;
3546         if ($filename) {
3547                 my $mime = mimetype_guess($filename);
3548                 $mime and return $mime;
3549         }
3551         # just in case
3552         return $default_blob_plain_mimetype unless $fd;
3554         if (-T $fd) {
3555                 return 'text/plain';
3556         } elsif (! $filename) {
3557                 return 'application/octet-stream';
3558         } elsif ($filename =~ m/\.png$/i) {
3559                 return 'image/png';
3560         } elsif ($filename =~ m/\.gif$/i) {
3561                 return 'image/gif';
3562         } elsif ($filename =~ m/\.jpe?g$/i) {
3563                 return 'image/jpeg';
3564         } else {
3565                 return 'application/octet-stream';
3566         }
3569 sub blob_contenttype {
3570         my ($fd, $file_name, $type) = @_;
3572         $type ||= blob_mimetype($fd, $file_name);
3573         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3574                 $type .= "; charset=$default_text_plain_charset";
3575         }
3577         return $type;
3580 # guess file syntax for syntax highlighting; return undef if no highlighting
3581 # the name of syntax can (in the future) depend on syntax highlighter used
3582 sub guess_file_syntax {
3583         my ($highlight, $mimetype, $file_name) = @_;
3584         return undef unless ($highlight && defined $file_name);
3585         my $basename = basename($file_name, '.in');
3586         return $highlight_basename{$basename}
3587                 if exists $highlight_basename{$basename};
3589         $basename =~ /\.([^.]*)$/;
3590         my $ext = $1 or return undef;
3591         return $highlight_ext{$ext}
3592                 if exists $highlight_ext{$ext};
3594         return undef;
3597 # run highlighter and return FD of its output,
3598 # or return original FD if no highlighting
3599 sub run_highlighter {
3600         my ($fd, $highlight, $syntax) = @_;
3601         return $fd unless ($highlight && defined $syntax);
3603         close $fd;
3604         open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3605                   quote_command($highlight_bin).
3606                   " --replace-tabs=8 --fragment --syntax $syntax |"
3607                 or die_error(500, "Couldn't open file or run syntax highlighter");
3608         return $fd;
3611 ## ======================================================================
3612 ## functions printing HTML: header, footer, error page
3614 sub get_page_title {
3615         my $title = to_utf8($site_name);
3617         return $title unless (defined $project);
3618         $title .= " - " . to_utf8($project);
3620         return $title unless (defined $action);
3621         $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3623         return $title unless (defined $file_name);
3624         $title .= " - " . esc_path($file_name);
3625         if ($action eq "tree" && $file_name !~ m|/$|) {
3626                 $title .= "/";
3627         }
3629         return $title;
3632 sub print_feed_meta {
3633         if (defined $project) {
3634                 my %href_params = get_feed_info();
3635                 if (!exists $href_params{'-title'}) {
3636                         $href_params{'-title'} = 'log';
3637                 }
3639                 foreach my $format (qw(RSS Atom)) {
3640                         my $type = lc($format);
3641                         my %link_attr = (
3642                                 '-rel' => 'alternate',
3643                                 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3644                                 '-type' => "application/$type+xml"
3645                         );
3647                         $href_params{'action'} = $type;
3648                         $link_attr{'-href'} = href(%href_params);
3649                         print "<link ".
3650                               "rel=\"$link_attr{'-rel'}\" ".
3651                               "title=\"$link_attr{'-title'}\" ".
3652                               "href=\"$link_attr{'-href'}\" ".
3653                               "type=\"$link_attr{'-type'}\" ".
3654                               "/>\n";
3656                         $href_params{'extra_options'} = '--no-merges';
3657                         $link_attr{'-href'} = href(%href_params);
3658                         $link_attr{'-title'} .= ' (no merges)';
3659                         print "<link ".
3660                               "rel=\"$link_attr{'-rel'}\" ".
3661                               "title=\"$link_attr{'-title'}\" ".
3662                               "href=\"$link_attr{'-href'}\" ".
3663                               "type=\"$link_attr{'-type'}\" ".
3664                               "/>\n";
3665                 }
3667         } else {
3668                 printf('<link rel="alternate" title="%s projects list" '.
3669                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
3670                        esc_attr($site_name), href(project=>undef, action=>"project_index"));
3671                 printf('<link rel="alternate" title="%s projects feeds" '.
3672                        'href="%s" type="text/x-opml" />'."\n",
3673                        esc_attr($site_name), href(project=>undef, action=>"opml"));
3674         }
3677 sub git_header_html {
3678         my $status = shift || "200 OK";
3679         my $expires = shift;
3680         my %opts = @_;
3682         my $title = get_page_title();
3683         my $content_type;
3684         # require explicit support from the UA if we are to send the page as
3685         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3686         # we have to do this because MSIE sometimes globs '*/*', pretending to
3687         # support xhtml+xml but choking when it gets what it asked for.
3688         if (defined $cgi->http('HTTP_ACCEPT') &&
3689             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3690             $cgi->Accept('application/xhtml+xml') != 0) {
3691                 $content_type = 'application/xhtml+xml';
3692         } else {
3693                 $content_type = 'text/html';
3694         }
3695         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3696                            -status=> $status, -expires => $expires)
3697                 unless ($opts{'-no_http_header'});
3698         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3699         print <<EOF;
3700 <?xml version="1.0" encoding="utf-8"?>
3701 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3702 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3703 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3704 <!-- git core binaries version $git_version -->
3705 <head>
3706 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3707 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3708 <meta name="robots" content="index, nofollow"/>
3709 <title>$title</title>
3710 EOF
3711         # the stylesheet, favicon etc urls won't work correctly with path_info
3712         # unless we set the appropriate base URL
3713         if ($ENV{'PATH_INFO'}) {
3714                 print "<base href=\"".esc_url($base_url)."\" />\n";
3715         }
3716         # print out each stylesheet that exist, providing backwards capability
3717         # for those people who defined $stylesheet in a config file
3718         if (defined $stylesheet) {
3719                 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3720         } else {
3721                 foreach my $stylesheet (@stylesheets) {
3722                         next unless $stylesheet;
3723                         print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3724                 }
3725         }
3726         print_feed_meta()
3727                 if ($status eq '200 OK');
3728         if (defined $favicon) {
3729                 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3730         }
3732         print "</head>\n" .
3733               "<body>\n";
3735         if (defined $site_header && -f $site_header) {
3736                 insert_file($site_header);
3737         }
3739         print "<div class=\"page_header\">\n";
3740         if (defined $logo) {
3741                 print $cgi->a({-href => esc_url($logo_url),
3742                                -title => $logo_label},
3743                               $cgi->img({-src => esc_url($logo),
3744                                          -width => 72, -height => 27,
3745                                          -alt => "git",
3746                                          -class => "logo"}));
3747         }
3748         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3749         if (defined $project) {
3750                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3751                 if (defined $action) {
3752                         my $action_print = $action ;
3753                         if (defined $opts{-action_extra}) {
3754                                 $action_print = $cgi->a({-href => href(action=>$action)},
3755                                         $action);
3756                         }
3757                         print " / $action_print";
3758                 }
3759                 if (defined $opts{-action_extra}) {
3760                         print " / $opts{-action_extra}";
3761                 }
3762                 print "\n";
3763         }
3764         print "</div>\n";
3766         my $have_search = gitweb_check_feature('search');
3767         if (defined $project && $have_search) {
3768                 if (!defined $searchtext) {
3769                         $searchtext = "";
3770                 }
3771                 my $search_hash;
3772                 if (defined $hash_base) {
3773                         $search_hash = $hash_base;
3774                 } elsif (defined $hash) {
3775                         $search_hash = $hash;
3776                 } else {
3777                         $search_hash = "HEAD";
3778                 }
3779                 my $action = $my_uri;
3780                 my $use_pathinfo = gitweb_check_feature('pathinfo');
3781                 if ($use_pathinfo) {
3782                         $action .= "/".esc_url($project);
3783                 }
3784                 print $cgi->startform(-method => "get", -action => $action) .
3785                       "<div class=\"search\">\n" .
3786                       (!$use_pathinfo &&
3787                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3788                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3789                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3790                       $cgi->popup_menu(-name => 'st', -default => 'commit',
3791                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3792                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3793                       " search:\n",
3794                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3795                       "<span title=\"Extended regular expression\">" .
3796                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3797                                      -checked => $search_use_regexp) .
3798                       "</span>" .
3799                       "</div>" .
3800                       $cgi->end_form() . "\n";
3801         }
3804 sub git_footer_html {
3805         my $feed_class = 'rss_logo';
3807         print "<div class=\"page_footer\">\n";
3808         if (defined $project) {
3809                 my $descr = git_get_project_description($project);
3810                 if (defined $descr) {
3811                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3812                 }
3814                 my %href_params = get_feed_info();
3815                 if (!%href_params) {
3816                         $feed_class .= ' generic';
3817                 }
3818                 $href_params{'-title'} ||= 'log';
3820                 foreach my $format (qw(RSS Atom)) {
3821                         $href_params{'action'} = lc($format);
3822                         print $cgi->a({-href => href(%href_params),
3823                                       -title => "$href_params{'-title'} $format feed",
3824                                       -class => $feed_class}, $format)."\n";
3825                 }
3827         } else {
3828                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3829                               -class => $feed_class}, "OPML") . " ";
3830                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3831                               -class => $feed_class}, "TXT") . "\n";
3832         }
3833         print "</div>\n"; # class="page_footer"
3835         if (defined $t0 && gitweb_check_feature('timed')) {
3836                 print "<div id=\"generating_info\">\n";
3837                 print 'This page took '.
3838                       '<span id="generating_time" class="time_span">'.
3839                       tv_interval($t0, [ gettimeofday() ]).
3840                       ' seconds </span>'.
3841                       ' and '.
3842                       '<span id="generating_cmd">'.
3843                       $number_of_git_cmds.
3844                       '</span> git commands '.
3845                       " to generate.\n";
3846                 print "</div>\n"; # class="page_footer"
3847         }
3849         if (defined $site_footer && -f $site_footer) {
3850                 insert_file($site_footer);
3851         }
3853         print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
3854         if (defined $action &&
3855             $action eq 'blame_incremental') {
3856                 print qq!<script type="text/javascript">\n!.
3857                       qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3858                       qq!           "!. href() .qq!");\n!.
3859                       qq!</script>\n!;
3860         } elsif (gitweb_check_feature('javascript-actions')) {
3861                 print qq!<script type="text/javascript">\n!.
3862                       qq!window.onload = fixLinks;\n!.
3863                       qq!</script>\n!;
3864         }
3866         print "</body>\n" .
3867               "</html>";
3870 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3871 # Example: die_error(404, 'Hash not found')
3872 # By convention, use the following status codes (as defined in RFC 2616):
3873 # 400: Invalid or missing CGI parameters, or
3874 #      requested object exists but has wrong type.
3875 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3876 #      this server or project.
3877 # 404: Requested object/revision/project doesn't exist.
3878 # 500: The server isn't configured properly, or
3879 #      an internal error occurred (e.g. failed assertions caused by bugs), or
3880 #      an unknown error occurred (e.g. the git binary died unexpectedly).
3881 # 503: The server is currently unavailable (because it is overloaded,
3882 #      or down for maintenance).  Generally, this is a temporary state.
3883 sub die_error {
3884         my $status = shift || 500;
3885         my $error = esc_html(shift) || "Internal Server Error";
3886         my $extra = shift;
3887         my %opts = @_;
3889         my %http_responses = (
3890                 400 => '400 Bad Request',
3891                 403 => '403 Forbidden',
3892                 404 => '404 Not Found',
3893                 500 => '500 Internal Server Error',
3894                 503 => '503 Service Unavailable',
3895         );
3896         git_header_html($http_responses{$status}, undef, %opts);
3897         print <<EOF;
3898 <div class="page_body">
3899 <br /><br />
3900 $status - $error
3901 <br />
3902 EOF
3903         if (defined $extra) {
3904                 print "<hr />\n" .
3905                       "$extra\n";
3906         }
3907         print "</div>\n";
3909         git_footer_html();
3910         goto DONE_GITWEB
3911                 unless ($opts{'-error_handler'});
3914 ## ----------------------------------------------------------------------
3915 ## functions printing or outputting HTML: navigation
3917 sub git_print_page_nav {
3918         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3919         $extra = '' if !defined $extra; # pager or formats
3921         my @navs = qw(summary shortlog log commit commitdiff tree);
3922         if ($suppress) {
3923                 @navs = grep { $_ ne $suppress } @navs;
3924         }
3926         my %arg = map { $_ => {action=>$_} } @navs;
3927         if (defined $head) {
3928                 for (qw(commit commitdiff)) {
3929                         $arg{$_}{'hash'} = $head;
3930                 }
3931                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3932                         for (qw(shortlog log)) {
3933                                 $arg{$_}{'hash'} = $head;
3934                         }
3935                 }
3936         }
3938         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3939         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3941         my @actions = gitweb_get_feature('actions');
3942         my %repl = (
3943                 '%' => '%',
3944                 'n' => $project,         # project name
3945                 'f' => $git_dir,         # project path within filesystem
3946                 'h' => $treehead || '',  # current hash ('h' parameter)
3947                 'b' => $treebase || '',  # hash base ('hb' parameter)
3948         );
3949         while (@actions) {
3950                 my ($label, $link, $pos) = splice(@actions,0,3);
3951                 # insert
3952                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3953                 # munch munch
3954                 $link =~ s/%([%nfhb])/$repl{$1}/g;
3955                 $arg{$label}{'_href'} = $link;
3956         }
3958         print "<div class=\"page_nav\">\n" .
3959                 (join " | ",
3960                  map { $_ eq $current ?
3961                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3962                  } @navs);
3963         print "<br/>\n$extra<br/>\n" .
3964               "</div>\n";
3967 # returns a submenu for the nagivation of the refs views (tags, heads,
3968 # remotes) with the current view disabled and the remotes view only
3969 # available if the feature is enabled
3970 sub format_ref_views {
3971         my ($current) = @_;
3972         my @ref_views = qw{tags heads};
3973         push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
3974         return join " | ", map {
3975                 $_ eq $current ? $_ :
3976                 $cgi->a({-href => href(action=>$_)}, $_)
3977         } @ref_views
3980 sub format_paging_nav {
3981         my ($action, $page, $has_next_link) = @_;
3982         my $paging_nav;
3985         if ($page > 0) {
3986                 $paging_nav .=
3987                         $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
3988                         " &sdot; " .
3989                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
3990                                  -accesskey => "p", -title => "Alt-p"}, "prev");
3991         } else {
3992                 $paging_nav .= "first &sdot; prev";
3993         }
3995         if ($has_next_link) {
3996                 $paging_nav .= " &sdot; " .
3997                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
3998                                  -accesskey => "n", -title => "Alt-n"}, "next");
3999         } else {
4000                 $paging_nav .= " &sdot; next";
4001         }
4003         return $paging_nav;
4006 ## ......................................................................
4007 ## functions printing or outputting HTML: div
4009 sub git_print_header_div {
4010         my ($action, $title, $hash, $hash_base) = @_;
4011         my %args = ();
4013         $args{'action'} = $action;
4014         $args{'hash'} = $hash if $hash;
4015         $args{'hash_base'} = $hash_base if $hash_base;
4017         print "<div class=\"header\">\n" .
4018               $cgi->a({-href => href(%args), -class => "title"},
4019               $title ? $title : $action) .
4020               "\n</div>\n";
4023 sub format_repo_url {
4024         my ($name, $url) = @_;
4025         return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4028 # Group output by placing it in a DIV element and adding a header.
4029 # Options for start_div() can be provided by passing a hash reference as the
4030 # first parameter to the function.
4031 # Options to git_print_header_div() can be provided by passing an array
4032 # reference. This must follow the options to start_div if they are present.
4033 # The content can be a scalar, which is output as-is, a scalar reference, which
4034 # is output after html escaping, an IO handle passed either as *handle or
4035 # *handle{IO}, or a function reference. In the latter case all following
4036 # parameters will be taken as argument to the content function call.
4037 sub git_print_section {
4038         my ($div_args, $header_args, $content);
4039         my $arg = shift;
4040         if (ref($arg) eq 'HASH') {
4041                 $div_args = $arg;
4042                 $arg = shift;
4043         }
4044         if (ref($arg) eq 'ARRAY') {
4045                 $header_args = $arg;
4046                 $arg = shift;
4047         }
4048         $content = $arg;
4050         print $cgi->start_div($div_args);
4051         git_print_header_div(@$header_args);
4053         if (ref($content) eq 'CODE') {
4054                 $content->(@_);
4055         } elsif (ref($content) eq 'SCALAR') {
4056                 print esc_html($$content);
4057         } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4058                 print <$content>;
4059         } elsif (!ref($content) && defined($content)) {
4060                 print $content;
4061         }
4063         print $cgi->end_div;
4066 sub print_local_time {
4067         print format_local_time(@_);
4070 sub format_local_time {
4071         my $localtime = '';
4072         my %date = @_;
4073         if ($date{'hour_local'} < 6) {
4074                 $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4075                         $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
4076         } else {
4077                 $localtime .= sprintf(" (%02d:%02d %s)",
4078                         $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
4079         }
4081         return $localtime;
4084 # Outputs the author name and date in long form
4085 sub git_print_authorship {
4086         my $co = shift;
4087         my %opts = @_;
4088         my $tag = $opts{-tag} || 'div';
4089         my $author = $co->{'author_name'};
4091         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4092         print "<$tag class=\"author_date\">" .
4093               format_search_author($author, "author", esc_html($author)) .
4094               " [$ad{'rfc2822'}";
4095         print_local_time(%ad) if ($opts{-localtime});
4096         print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
4097                   . "</$tag>\n";
4100 # Outputs table rows containing the full author or committer information,
4101 # in the format expected for 'commit' view (& similar).
4102 # Parameters are a commit hash reference, followed by the list of people
4103 # to output information for. If the list is empty it defaults to both
4104 # author and committer.
4105 sub git_print_authorship_rows {
4106         my $co = shift;
4107         # too bad we can't use @people = @_ || ('author', 'committer')
4108         my @people = @_;
4109         @people = ('author', 'committer') unless @people;
4110         foreach my $who (@people) {
4111                 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4112                 print "<tr><td>$who</td><td>" .
4113                       format_search_author($co->{"${who}_name"}, $who,
4114                                esc_html($co->{"${who}_name"})) . " " .
4115                       format_search_author($co->{"${who}_email"}, $who,
4116                                esc_html("<" . $co->{"${who}_email"} . ">")) .
4117                       "</td><td rowspan=\"2\">" .
4118                       git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4119                       "</td></tr>\n" .
4120                       "<tr>" .
4121                       "<td></td><td> $wd{'rfc2822'}";
4122                 print_local_time(%wd);
4123                 print "</td>" .
4124                       "</tr>\n";
4125         }
4128 sub git_print_page_path {
4129         my $name = shift;
4130         my $type = shift;
4131         my $hb = shift;
4134         print "<div class=\"page_path\">";
4135         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4136                       -title => 'tree root'}, to_utf8("[$project]"));
4137         print " / ";
4138         if (defined $name) {
4139                 my @dirname = split '/', $name;
4140                 my $basename = pop @dirname;
4141                 my $fullname = '';
4143                 foreach my $dir (@dirname) {
4144                         $fullname .= ($fullname ? '/' : '') . $dir;
4145                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4146                                                      hash_base=>$hb),
4147                                       -title => $fullname}, esc_path($dir));
4148                         print " / ";
4149                 }
4150                 if (defined $type && $type eq 'blob') {
4151                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4152                                                      hash_base=>$hb),
4153                                       -title => $name}, esc_path($basename));
4154                 } elsif (defined $type && $type eq 'tree') {
4155                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4156                                                      hash_base=>$hb),
4157                                       -title => $name}, esc_path($basename));
4158                         print " / ";
4159                 } else {
4160                         print esc_path($basename);
4161                 }
4162         }
4163         print "<br/></div>\n";
4166 sub git_print_log {
4167         my $log = shift;
4168         my %opts = @_;
4170         if ($opts{'-remove_title'}) {
4171                 # remove title, i.e. first line of log
4172                 shift @$log;
4173         }
4174         # remove leading empty lines
4175         while (defined $log->[0] && $log->[0] eq "") {
4176                 shift @$log;
4177         }
4179         # print log
4180         my $signoff = 0;
4181         my $empty = 0;
4182         foreach my $line (@$log) {
4183                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4184                         $signoff = 1;
4185                         $empty = 0;
4186                         if (! $opts{'-remove_signoff'}) {
4187                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4188                                 next;
4189                         } else {
4190                                 # remove signoff lines
4191                                 next;
4192                         }
4193                 } else {
4194                         $signoff = 0;
4195                 }
4197                 # print only one empty line
4198                 # do not print empty line after signoff
4199                 if ($line eq "") {
4200                         next if ($empty || $signoff);
4201                         $empty = 1;
4202                 } else {
4203                         $empty = 0;
4204                 }
4206                 print format_log_line_html($line) . "<br/>\n";
4207         }
4209         if ($opts{'-final_empty_line'}) {
4210                 # end with single empty line
4211                 print "<br/>\n" unless $empty;
4212         }
4215 # return link target (what link points to)
4216 sub git_get_link_target {
4217         my $hash = shift;
4218         my $link_target;
4220         # read link
4221         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4222                 or return;
4223         {
4224                 local $/ = undef;
4225                 $link_target = <$fd>;
4226         }
4227         close $fd
4228                 or return;
4230         return $link_target;
4233 # given link target, and the directory (basedir) the link is in,
4234 # return target of link relative to top directory (top tree);
4235 # return undef if it is not possible (including absolute links).
4236 sub normalize_link_target {
4237         my ($link_target, $basedir) = @_;
4239         # absolute symlinks (beginning with '/') cannot be normalized
4240         return if (substr($link_target, 0, 1) eq '/');
4242         # normalize link target to path from top (root) tree (dir)
4243         my $path;
4244         if ($basedir) {
4245                 $path = $basedir . '/' . $link_target;
4246         } else {
4247                 # we are in top (root) tree (dir)
4248                 $path = $link_target;
4249         }
4251         # remove //, /./, and /../
4252         my @path_parts;
4253         foreach my $part (split('/', $path)) {
4254                 # discard '.' and ''
4255                 next if (!$part || $part eq '.');
4256                 # handle '..'
4257                 if ($part eq '..') {
4258                         if (@path_parts) {
4259                                 pop @path_parts;
4260                         } else {
4261                                 # link leads outside repository (outside top dir)
4262                                 return;
4263                         }
4264                 } else {
4265                         push @path_parts, $part;
4266                 }
4267         }
4268         $path = join('/', @path_parts);
4270         return $path;
4273 # print tree entry (row of git_tree), but without encompassing <tr> element
4274 sub git_print_tree_entry {
4275         my ($t, $basedir, $hash_base, $have_blame) = @_;
4277         my %base_key = ();
4278         $base_key{'hash_base'} = $hash_base if defined $hash_base;
4280         # The format of a table row is: mode list link.  Where mode is
4281         # the mode of the entry, list is the name of the entry, an href,
4282         # and link is the action links of the entry.
4284         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4285         if (exists $t->{'size'}) {
4286                 print "<td class=\"size\">$t->{'size'}</td>\n";
4287         }
4288         if ($t->{'type'} eq "blob") {
4289                 print "<td class=\"list\">" .
4290                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4291                                                file_name=>"$basedir$t->{'name'}", %base_key),
4292                                 -class => "list"}, esc_path($t->{'name'}));
4293                 if (S_ISLNK(oct $t->{'mode'})) {
4294                         my $link_target = git_get_link_target($t->{'hash'});
4295                         if ($link_target) {
4296                                 my $norm_target = normalize_link_target($link_target, $basedir);
4297                                 if (defined $norm_target) {
4298                                         print " -> " .
4299                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4300                                                                      file_name=>$norm_target),
4301                                                        -title => $norm_target}, esc_path($link_target));
4302                                 } else {
4303                                         print " -> " . esc_path($link_target);
4304                                 }
4305                         }
4306                 }
4307                 print "</td>\n";
4308                 print "<td class=\"link\">";
4309                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4310                                              file_name=>"$basedir$t->{'name'}", %base_key)},
4311                               "blob");
4312                 if ($have_blame) {
4313                         print " | " .
4314                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4315                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
4316                                       "blame");
4317                 }
4318                 if (defined $hash_base) {
4319                         print " | " .
4320                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4321                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4322                                       "history");
4323                 }
4324                 print " | " .
4325                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4326                                                file_name=>"$basedir$t->{'name'}")},
4327                                 "raw");
4328                 print "</td>\n";
4330         } elsif ($t->{'type'} eq "tree") {
4331                 print "<td class=\"list\">";
4332                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4333                                              file_name=>"$basedir$t->{'name'}",
4334                                              %base_key)},
4335                               esc_path($t->{'name'}));
4336                 print "</td>\n";
4337                 print "<td class=\"link\">";
4338                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4339                                              file_name=>"$basedir$t->{'name'}",
4340                                              %base_key)},
4341                               "tree");
4342                 if (defined $hash_base) {
4343                         print " | " .
4344                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4345                                                      file_name=>"$basedir$t->{'name'}")},
4346                                       "history");
4347                 }
4348                 print "</td>\n";
4349         } else {
4350                 # unknown object: we can only present history for it
4351                 # (this includes 'commit' object, i.e. submodule support)
4352                 print "<td class=\"list\">" .
4353                       esc_path($t->{'name'}) .
4354                       "</td>\n";
4355                 print "<td class=\"link\">";
4356                 if (defined $hash_base) {
4357                         print $cgi->a({-href => href(action=>"history",
4358                                                      hash_base=>$hash_base,
4359                                                      file_name=>"$basedir$t->{'name'}")},
4360                                       "history");
4361                 }
4362                 print "</td>\n";
4363         }
4366 ## ......................................................................
4367 ## functions printing large fragments of HTML
4369 # get pre-image filenames for merge (combined) diff
4370 sub fill_from_file_info {
4371         my ($diff, @parents) = @_;
4373         $diff->{'from_file'} = [ ];
4374         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4375         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4376                 if ($diff->{'status'}[$i] eq 'R' ||
4377                     $diff->{'status'}[$i] eq 'C') {
4378                         $diff->{'from_file'}[$i] =
4379                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4380                 }
4381         }
4383         return $diff;
4386 # is current raw difftree line of file deletion
4387 sub is_deleted {
4388         my $diffinfo = shift;
4390         return $diffinfo->{'to_id'} eq ('0' x 40);
4393 # does patch correspond to [previous] difftree raw line
4394 # $diffinfo  - hashref of parsed raw diff format
4395 # $patchinfo - hashref of parsed patch diff format
4396 #              (the same keys as in $diffinfo)
4397 sub is_patch_split {
4398         my ($diffinfo, $patchinfo) = @_;
4400         return defined $diffinfo && defined $patchinfo
4401                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4405 sub git_difftree_body {
4406         my ($difftree, $hash, @parents) = @_;
4407         my ($parent) = $parents[0];
4408         my $have_blame = gitweb_check_feature('blame');
4409         print "<div class=\"list_head\">\n";
4410         if ($#{$difftree} > 10) {
4411                 print(($#{$difftree} + 1) . " files changed:\n");
4412         }
4413         print "</div>\n";
4415         print "<table class=\"" .
4416               (@parents > 1 ? "combined " : "") .
4417               "diff_tree\">\n";
4419         # header only for combined diff in 'commitdiff' view
4420         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4421         if ($has_header) {
4422                 # table header
4423                 print "<thead><tr>\n" .
4424                        "<th></th><th></th>\n"; # filename, patchN link
4425                 for (my $i = 0; $i < @parents; $i++) {
4426                         my $par = $parents[$i];
4427                         print "<th>" .
4428                               $cgi->a({-href => href(action=>"commitdiff",
4429                                                      hash=>$hash, hash_parent=>$par),
4430                                        -title => 'commitdiff to parent number ' .
4431                                                   ($i+1) . ': ' . substr($par,0,7)},
4432                                       $i+1) .
4433                               "&nbsp;</th>\n";
4434                 }
4435                 print "</tr></thead>\n<tbody>\n";
4436         }
4438         my $alternate = 1;
4439         my $patchno = 0;
4440         foreach my $line (@{$difftree}) {
4441                 my $diff = parsed_difftree_line($line);
4443                 if ($alternate) {
4444                         print "<tr class=\"dark\">\n";
4445                 } else {
4446                         print "<tr class=\"light\">\n";
4447                 }
4448                 $alternate ^= 1;
4450                 if (exists $diff->{'nparents'}) { # combined diff
4452                         fill_from_file_info($diff, @parents)
4453                                 unless exists $diff->{'from_file'};
4455                         if (!is_deleted($diff)) {
4456                                 # file exists in the result (child) commit
4457                                 print "<td>" .
4458                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4459                                                              file_name=>$diff->{'to_file'},
4460                                                              hash_base=>$hash),
4461                                               -class => "list"}, esc_path($diff->{'to_file'})) .
4462                                       "</td>\n";
4463                         } else {
4464                                 print "<td>" .
4465                                       esc_path($diff->{'to_file'}) .
4466                                       "</td>\n";
4467                         }
4469                         if ($action eq 'commitdiff') {
4470                                 # link to patch
4471                                 $patchno++;
4472                                 print "<td class=\"link\">" .
4473                                       $cgi->a({-href => href(-anchor=>"patch$patchno")},
4474                                               "patch") .
4475                                       " | " .
4476                                       "</td>\n";
4477                         }
4479                         my $has_history = 0;
4480                         my $not_deleted = 0;
4481                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4482                                 my $hash_parent = $parents[$i];
4483                                 my $from_hash = $diff->{'from_id'}[$i];
4484                                 my $from_path = $diff->{'from_file'}[$i];
4485                                 my $status = $diff->{'status'}[$i];
4487                                 $has_history ||= ($status ne 'A');
4488                                 $not_deleted ||= ($status ne 'D');
4490                                 if ($status eq 'A') {
4491                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
4492                                 } elsif ($status eq 'D') {
4493                                         print "<td class=\"link\">" .
4494                                               $cgi->a({-href => href(action=>"blob",
4495                                                                      hash_base=>$hash,
4496                                                                      hash=>$from_hash,
4497                                                                      file_name=>$from_path)},
4498                                                       "blob" . ($i+1)) .
4499                                               " | </td>\n";
4500                                 } else {
4501                                         if ($diff->{'to_id'} eq $from_hash) {
4502                                                 print "<td class=\"link nochange\">";
4503                                         } else {
4504                                                 print "<td class=\"link\">";
4505                                         }
4506                                         print $cgi->a({-href => href(action=>"blobdiff",
4507                                                                      hash=>$diff->{'to_id'},
4508                                                                      hash_parent=>$from_hash,
4509                                                                      hash_base=>$hash,
4510                                                                      hash_parent_base=>$hash_parent,
4511                                                                      file_name=>$diff->{'to_file'},
4512                                                                      file_parent=>$from_path)},
4513                                                       "diff" . ($i+1)) .
4514                                               " | </td>\n";
4515                                 }
4516                         }
4518                         print "<td class=\"link\">";
4519                         if ($not_deleted) {
4520                                 print $cgi->a({-href => href(action=>"blob",
4521                                                              hash=>$diff->{'to_id'},
4522                                                              file_name=>$diff->{'to_file'},
4523                                                              hash_base=>$hash)},
4524                                               "blob");
4525                                 print " | " if ($has_history);
4526                         }
4527                         if ($has_history) {
4528                                 print $cgi->a({-href => href(action=>"history",
4529                                                              file_name=>$diff->{'to_file'},
4530                                                              hash_base=>$hash)},
4531                                               "history");
4532                         }
4533                         print "</td>\n";
4535                         print "</tr>\n";
4536                         next; # instead of 'else' clause, to avoid extra indent
4537                 }
4538                 # else ordinary diff
4540                 my ($to_mode_oct, $to_mode_str, $to_file_type);
4541                 my ($from_mode_oct, $from_mode_str, $from_file_type);
4542                 if ($diff->{'to_mode'} ne ('0' x 6)) {
4543                         $to_mode_oct = oct $diff->{'to_mode'};
4544                         if (S_ISREG($to_mode_oct)) { # only for regular file
4545                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4546                         }
4547                         $to_file_type = file_type($diff->{'to_mode'});
4548                 }
4549                 if ($diff->{'from_mode'} ne ('0' x 6)) {
4550                         $from_mode_oct = oct $diff->{'from_mode'};
4551                         if (S_ISREG($from_mode_oct)) { # only for regular file
4552                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4553                         }
4554                         $from_file_type = file_type($diff->{'from_mode'});
4555                 }
4557                 if ($diff->{'status'} eq "A") { # created
4558                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4559                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
4560                         $mode_chng   .= "]</span>";
4561                         print "<td>";
4562                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4563                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4564                                       -class => "list"}, esc_path($diff->{'file'}));
4565                         print "</td>\n";
4566                         print "<td>$mode_chng</td>\n";
4567                         print "<td class=\"link\">";
4568                         if ($action eq 'commitdiff') {
4569                                 # link to patch
4570                                 $patchno++;
4571                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4572                                               "patch") .
4573                                       " | ";
4574                         }
4575                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4576                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4577                                       "blob");
4578                         print "</td>\n";
4580                 } elsif ($diff->{'status'} eq "D") { # deleted
4581                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4582                         print "<td>";
4583                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4584                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
4585                                        -class => "list"}, esc_path($diff->{'file'}));
4586                         print "</td>\n";
4587                         print "<td>$mode_chng</td>\n";
4588                         print "<td class=\"link\">";
4589                         if ($action eq 'commitdiff') {
4590                                 # link to patch
4591                                 $patchno++;
4592                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4593                                               "patch") .
4594                                       " | ";
4595                         }
4596                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4597                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
4598                                       "blob") . " | ";
4599                         if ($have_blame) {
4600                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4601                                                              file_name=>$diff->{'file'})},
4602                                               "blame") . " | ";
4603                         }
4604                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4605                                                      file_name=>$diff->{'file'})},
4606                                       "history");
4607                         print "</td>\n";
4609                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4610                         my $mode_chnge = "";
4611                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4612                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4613                                 if ($from_file_type ne $to_file_type) {
4614                                         $mode_chnge .= " from $from_file_type to $to_file_type";
4615                                 }
4616                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4617                                         if ($from_mode_str && $to_mode_str) {
4618                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4619                                         } elsif ($to_mode_str) {
4620                                                 $mode_chnge .= " mode: $to_mode_str";
4621                                         }
4622                                 }
4623                                 $mode_chnge .= "]</span>\n";
4624                         }
4625                         print "<td>";
4626                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4627                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
4628                                       -class => "list"}, esc_path($diff->{'file'}));
4629                         print "</td>\n";
4630                         print "<td>$mode_chnge</td>\n";
4631                         print "<td class=\"link\">";
4632                         if ($action eq 'commitdiff') {
4633                                 # link to patch
4634                                 $patchno++;
4635                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4636                                               "patch") .
4637                                       " | ";
4638                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4639                                 # "commit" view and modified file (not onlu mode changed)
4640                                 print $cgi->a({-href => href(action=>"blobdiff",
4641                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4642                                                              hash_base=>$hash, hash_parent_base=>$parent,
4643                                                              file_name=>$diff->{'file'})},
4644                                               "diff") .
4645                                       " | ";
4646                         }
4647                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4648                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
4649                                        "blob") . " | ";
4650                         if ($have_blame) {
4651                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4652                                                              file_name=>$diff->{'file'})},
4653                                               "blame") . " | ";
4654                         }
4655                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4656                                                      file_name=>$diff->{'file'})},
4657                                       "history");
4658                         print "</td>\n";
4660                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4661                         my %status_name = ('R' => 'moved', 'C' => 'copied');
4662                         my $nstatus = $status_name{$diff->{'status'}};
4663                         my $mode_chng = "";
4664                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4665                                 # mode also for directories, so we cannot use $to_mode_str
4666                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4667                         }
4668                         print "<td>" .
4669                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4670                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4671                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4672                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4673                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4674                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4675                                       -class => "list"}, esc_path($diff->{'from_file'})) .
4676                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4677                               "<td class=\"link\">";
4678                         if ($action eq 'commitdiff') {
4679                                 # link to patch
4680                                 $patchno++;
4681                                 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4682                                               "patch") .
4683                                       " | ";
4684                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4685                                 # "commit" view and modified file (not only pure rename or copy)
4686                                 print $cgi->a({-href => href(action=>"blobdiff",
4687                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4688                                                              hash_base=>$hash, hash_parent_base=>$parent,
4689                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4690                                               "diff") .
4691                                       " | ";
4692                         }
4693                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4694                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
4695                                       "blob") . " | ";
4696                         if ($have_blame) {
4697                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4698                                                              file_name=>$diff->{'to_file'})},
4699                                               "blame") . " | ";
4700                         }
4701                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4702                                                     file_name=>$diff->{'to_file'})},
4703                                       "history");
4704                         print "</td>\n";
4706                 } # we should not encounter Unmerged (U) or Unknown (X) status
4707                 print "</tr>\n";
4708         }
4709         print "</tbody>" if $has_header;
4710         print "</table>\n";
4713 sub git_patchset_body {
4714         my ($fd, $difftree, $hash, @hash_parents) = @_;
4715         my ($hash_parent) = $hash_parents[0];
4717         my $is_combined = (@hash_parents > 1);
4718         my $patch_idx = 0;
4719         my $patch_number = 0;
4720         my $patch_line;
4721         my $diffinfo;
4722         my $to_name;
4723         my (%from, %to);
4725         print "<div class=\"patchset\">\n";
4727         # skip to first patch
4728         while ($patch_line = <$fd>) {
4729                 chomp $patch_line;
4731                 last if ($patch_line =~ m/^diff /);
4732         }
4734  PATCH:
4735         while ($patch_line) {
4737                 # parse "git diff" header line
4738                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4739                         # $1 is from_name, which we do not use
4740                         $to_name = unquote($2);
4741                         $to_name =~ s!^b/!!;
4742                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4743                         # $1 is 'cc' or 'combined', which we do not use
4744                         $to_name = unquote($2);
4745                 } else {
4746                         $to_name = undef;
4747                 }
4749                 # check if current patch belong to current raw line
4750                 # and parse raw git-diff line if needed
4751                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4752                         # this is continuation of a split patch
4753                         print "<div class=\"patch cont\">\n";
4754                 } else {
4755                         # advance raw git-diff output if needed
4756                         $patch_idx++ if defined $diffinfo;
4758                         # read and prepare patch information
4759                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4761                         # compact combined diff output can have some patches skipped
4762                         # find which patch (using pathname of result) we are at now;
4763                         if ($is_combined) {
4764                                 while ($to_name ne $diffinfo->{'to_file'}) {
4765                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4766                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
4767                                               "</div>\n";  # class="patch"
4769                                         $patch_idx++;
4770                                         $patch_number++;
4772                                         last if $patch_idx > $#$difftree;
4773                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4774                                 }
4775                         }
4777                         # modifies %from, %to hashes
4778                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4780                         # this is first patch for raw difftree line with $patch_idx index
4781                         # we index @$difftree array from 0, but number patches from 1
4782                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4783                 }
4785                 # git diff header
4786                 #assert($patch_line =~ m/^diff /) if DEBUG;
4787                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4788                 $patch_number++;
4789                 # print "git diff" header
4790                 print format_git_diff_header_line($patch_line, $diffinfo,
4791                                                   \%from, \%to);
4793                 # print extended diff header
4794                 print "<div class=\"diff extended_header\">\n";
4795         EXTENDED_HEADER:
4796                 while ($patch_line = <$fd>) {
4797                         chomp $patch_line;
4799                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4801                         print format_extended_diff_header_line($patch_line, $diffinfo,
4802                                                                \%from, \%to);
4803                 }
4804                 print "</div>\n"; # class="diff extended_header"
4806                 # from-file/to-file diff header
4807                 if (! $patch_line) {
4808                         print "</div>\n"; # class="patch"
4809                         last PATCH;
4810                 }
4811                 next PATCH if ($patch_line =~ m/^diff /);
4812                 #assert($patch_line =~ m/^---/) if DEBUG;
4814                 my $last_patch_line = $patch_line;
4815                 $patch_line = <$fd>;
4816                 chomp $patch_line;
4817                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4819                 print format_diff_from_to_header($last_patch_line, $patch_line,
4820                                                  $diffinfo, \%from, \%to,
4821                                                  @hash_parents);
4823                 # the patch itself
4824         LINE:
4825                 while ($patch_line = <$fd>) {
4826                         chomp $patch_line;
4828                         next PATCH if ($patch_line =~ m/^diff /);
4830                         print format_diff_line($patch_line, \%from, \%to);
4831                 }
4833         } continue {
4834                 print "</div>\n"; # class="patch"
4835         }
4837         # for compact combined (--cc) format, with chunk and patch simplification
4838         # the patchset might be empty, but there might be unprocessed raw lines
4839         for (++$patch_idx if $patch_number > 0;
4840              $patch_idx < @$difftree;
4841              ++$patch_idx) {
4842                 # read and prepare patch information
4843                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4845                 # generate anchor for "patch" links in difftree / whatchanged part
4846                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4847                       format_diff_cc_simplified($diffinfo, @hash_parents) .
4848                       "</div>\n";  # class="patch"
4850                 $patch_number++;
4851         }
4853         if ($patch_number == 0) {
4854                 if (@hash_parents > 1) {
4855                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4856                 } else {
4857                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
4858                 }
4859         }
4861         print "</div>\n"; # class="patchset"
4864 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4866 # fills project list info (age, description, owner, forks) for each
4867 # project in the list, removing invalid projects from returned list
4868 # NOTE: modifies $projlist, but does not remove entries from it
4869 sub fill_project_list_info {
4870         my $projlist = shift;
4871         my @projects;
4873         my $show_ctags = gitweb_check_feature('ctags');
4874  PROJECT:
4875         foreach my $pr (@$projlist) {
4876                 my (@activity) = git_get_last_activity($pr->{'path'});
4877                 unless (@activity) {
4878                         next PROJECT;
4879                 }
4880                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4881                 if (!defined $pr->{'descr'}) {
4882                         my $descr = git_get_project_description($pr->{'path'}) || "";
4883                         $descr = to_utf8($descr);
4884                         $pr->{'descr_long'} = $descr;
4885                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4886                 }
4887                 if (!defined $pr->{'owner'}) {
4888                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4889                 }
4890                 if ($show_ctags) {
4891                         $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4892                 }
4893                 push @projects, $pr;
4894         }
4896         return @projects;
4899 sub sort_projects_list {
4900         my ($projlist, $order) = @_;
4901         my @projects;
4903         my %order_info = (
4904                 project => { key => 'path', type => 'str' },
4905                 descr => { key => 'descr_long', type => 'str' },
4906                 owner => { key => 'owner', type => 'str' },
4907                 age => { key => 'age', type => 'num' }
4908         );
4909         my $oi = $order_info{$order};
4910         return @$projlist unless defined $oi;
4911         if ($oi->{'type'} eq 'str') {
4912                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
4913         } else {
4914                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
4915         }
4917         return @projects;
4920 # print 'sort by' <th> element, generating 'sort by $name' replay link
4921 # if that order is not selected
4922 sub print_sort_th {
4923         print format_sort_th(@_);
4926 sub format_sort_th {
4927         my ($name, $order, $header) = @_;
4928         my $sort_th = "";
4929         $header ||= ucfirst($name);
4931         if ($order eq $name) {
4932                 $sort_th .= "<th>$header</th>\n";
4933         } else {
4934                 $sort_th .= "<th>" .
4935                             $cgi->a({-href => href(-replay=>1, order=>$name),
4936                                      -class => "header"}, $header) .
4937                             "</th>\n";
4938         }
4940         return $sort_th;
4943 sub git_project_list_body {
4944         # actually uses global variable $project
4945         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4946         my @projects = @$projlist;
4948         my $check_forks = gitweb_check_feature('forks');
4949         my $show_ctags  = gitweb_check_feature('ctags');
4950         my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
4951         $check_forks = undef
4952                 if ($tagfilter || $searchtext);
4954         # filtering out forks before filling info allows to do less work
4955         @projects = filter_forks_from_projects_list(\@projects)
4956                 if ($check_forks);
4957         @projects = fill_project_list_info(\@projects);
4958         # searching projects require filling to be run before it
4959         @projects = search_projects_list(\@projects,
4960                                          'searchtext' => $searchtext,
4961                                          'tagfilter'  => $tagfilter)
4962                 if ($tagfilter || $searchtext);
4964         $order ||= $default_projects_order;
4965         $from = 0 unless defined $from;
4966         $to = $#projects if (!defined $to || $#projects < $to);
4968         # short circuit
4969         if ($from > $to) {
4970                 print "<center>\n".
4971                       "<b>No such projects found</b><br />\n".
4972                       "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
4973                       "</center>\n<br />\n";
4974                 return;
4975         }
4977         @projects = sort_projects_list(\@projects, $order);
4979         if ($show_ctags) {
4980                 my $ctags = git_gather_all_ctags(\@projects);
4981                 my $cloud = git_populate_project_tagcloud($ctags);
4982                 print git_show_project_tagcloud($cloud, 64);
4983         }
4985         print "<table class=\"project_list\">\n";
4986         unless ($no_header) {
4987                 print "<tr>\n";
4988                 if ($check_forks) {
4989                         print "<th></th>\n";
4990                 }
4991                 print_sort_th('project', $order, 'Project');
4992                 print_sort_th('descr', $order, 'Description');
4993                 print_sort_th('owner', $order, 'Owner');
4994                 print_sort_th('age', $order, 'Last Change');
4995                 print "<th></th>\n" . # for links
4996                       "</tr>\n";
4997         }
4998         my $alternate = 1;
4999         for (my $i = $from; $i <= $to; $i++) {
5000                 my $pr = $projects[$i];
5002                 if ($alternate) {
5003                         print "<tr class=\"dark\">\n";
5004                 } else {
5005                         print "<tr class=\"light\">\n";
5006                 }
5007                 $alternate ^= 1;
5009                 if ($check_forks) {
5010                         print "<td>";
5011                         if ($pr->{'forks'}) {
5012                                 my $nforks = scalar @{$pr->{'forks'}};
5013                                 if ($nforks > 0) {
5014                                         print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5015                                                        -title => "$nforks forks"}, "+");
5016                                 } else {
5017                                         print $cgi->span({-title => "$nforks forks"}, "+");
5018                                 }
5019                         }
5020                         print "</td>\n";
5021                 }
5022                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5023                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
5024                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5025                                         -class => "list", -title => $pr->{'descr_long'}},
5026                                         esc_html($pr->{'descr'})) . "</td>\n" .
5027                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5028                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5029                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
5030                       "<td class=\"link\">" .
5031                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
5032                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5033                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5034                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5035                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5036                       "</td>\n" .
5037                       "</tr>\n";
5038         }
5039         if (defined $extra) {
5040                 print "<tr>\n";
5041                 if ($check_forks) {
5042                         print "<td></td>\n";
5043                 }
5044                 print "<td colspan=\"5\">$extra</td>\n" .
5045                       "</tr>\n";
5046         }
5047         print "</table>\n";
5050 sub git_log_body {
5051         # uses global variable $project
5052         my ($commitlist, $from, $to, $refs, $extra) = @_;
5054         $from = 0 unless defined $from;
5055         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5057         for (my $i = 0; $i <= $to; $i++) {
5058                 my %co = %{$commitlist->[$i]};
5059                 next if !%co;
5060                 my $commit = $co{'id'};
5061                 my $ref = format_ref_marker($refs, $commit);
5062                 git_print_header_div('commit',
5063                                "<span class=\"age\">$co{'age_string'}</span>" .
5064                                esc_html($co{'title'}) . $ref,
5065                                $commit);
5066                 print "<div class=\"title_text\">\n" .
5067                       "<div class=\"log_link\">\n" .
5068                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5069                       " | " .
5070                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5071                       " | " .
5072                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5073                       "<br/>\n" .
5074                       "</div>\n";
5075                       git_print_authorship(\%co, -tag => 'span');
5076                       print "<br/>\n</div>\n";
5078                 print "<div class=\"log_body\">\n";
5079                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5080                 print "</div>\n";
5081         }
5082         if ($extra) {
5083                 print "<div class=\"page_nav\">\n";
5084                 print "$extra\n";
5085                 print "</div>\n";
5086         }
5089 sub git_shortlog_body {
5090         # uses global variable $project
5091         my ($commitlist, $from, $to, $refs, $extra) = @_;
5093         $from = 0 unless defined $from;
5094         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5096         print "<table class=\"shortlog\">\n";
5097         my $alternate = 1;
5098         for (my $i = $from; $i <= $to; $i++) {
5099                 my %co = %{$commitlist->[$i]};
5100                 my $commit = $co{'id'};
5101                 my $ref = format_ref_marker($refs, $commit);
5102                 if ($alternate) {
5103                         print "<tr class=\"dark\">\n";
5104                 } else {
5105                         print "<tr class=\"light\">\n";
5106                 }
5107                 $alternate ^= 1;
5108                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5109                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5110                       format_author_html('td', \%co, 10) . "<td>";
5111                 print format_subject_html($co{'title'}, $co{'title_short'},
5112                                           href(action=>"commit", hash=>$commit), $ref);
5113                 print "</td>\n" .
5114                       "<td class=\"link\">" .
5115                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5116                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5117                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5118                 my $snapshot_links = format_snapshot_links($commit);
5119                 if (defined $snapshot_links) {
5120                         print " | " . $snapshot_links;
5121                 }
5122                 print "</td>\n" .
5123                       "</tr>\n";
5124         }
5125         if (defined $extra) {
5126                 print "<tr>\n" .
5127                       "<td colspan=\"4\">$extra</td>\n" .
5128                       "</tr>\n";
5129         }
5130         print "</table>\n";
5133 sub git_history_body {
5134         # Warning: assumes constant type (blob or tree) during history
5135         my ($commitlist, $from, $to, $refs, $extra,
5136             $file_name, $file_hash, $ftype) = @_;
5138         $from = 0 unless defined $from;
5139         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5141         print "<table class=\"history\">\n";
5142         my $alternate = 1;
5143         for (my $i = $from; $i <= $to; $i++) {
5144                 my %co = %{$commitlist->[$i]};
5145                 if (!%co) {
5146                         next;
5147                 }
5148                 my $commit = $co{'id'};
5150                 my $ref = format_ref_marker($refs, $commit);
5152                 if ($alternate) {
5153                         print "<tr class=\"dark\">\n";
5154                 } else {
5155                         print "<tr class=\"light\">\n";
5156                 }
5157                 $alternate ^= 1;
5158                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5159         # shortlog:   format_author_html('td', \%co, 10)
5160                       format_author_html('td', \%co, 15, 3) . "<td>";
5161                 # originally git_history used chop_str($co{'title'}, 50)
5162                 print format_subject_html($co{'title'}, $co{'title_short'},
5163                                           href(action=>"commit", hash=>$commit), $ref);
5164                 print "</td>\n" .
5165                       "<td class=\"link\">" .
5166                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5167                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5169                 if ($ftype eq 'blob') {
5170                         my $blob_current = $file_hash;
5171                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
5172                         if (defined $blob_current && defined $blob_parent &&
5173                                         $blob_current ne $blob_parent) {
5174                                 print " | " .
5175                                         $cgi->a({-href => href(action=>"blobdiff",
5176                                                                hash=>$blob_current, hash_parent=>$blob_parent,
5177                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
5178                                                                file_name=>$file_name)},
5179                                                 "diff to current");
5180                         }
5181                 }
5182                 print "</td>\n" .
5183                       "</tr>\n";
5184         }
5185         if (defined $extra) {
5186                 print "<tr>\n" .
5187                       "<td colspan=\"4\">$extra</td>\n" .
5188                       "</tr>\n";
5189         }
5190         print "</table>\n";
5193 sub git_tags_body {
5194         # uses global variable $project
5195         my ($taglist, $from, $to, $extra) = @_;
5196         $from = 0 unless defined $from;
5197         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5199         print "<table class=\"tags\">\n";
5200         my $alternate = 1;
5201         for (my $i = $from; $i <= $to; $i++) {
5202                 my $entry = $taglist->[$i];
5203                 my %tag = %$entry;
5204                 my $comment = $tag{'subject'};
5205                 my $comment_short;
5206                 if (defined $comment) {
5207                         $comment_short = chop_str($comment, 30, 5);
5208                 }
5209                 if ($alternate) {
5210                         print "<tr class=\"dark\">\n";
5211                 } else {
5212                         print "<tr class=\"light\">\n";
5213                 }
5214                 $alternate ^= 1;
5215                 if (defined $tag{'age'}) {
5216                         print "<td><i>$tag{'age'}</i></td>\n";
5217                 } else {
5218                         print "<td></td>\n";
5219                 }
5220                 print "<td>" .
5221                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5222                                -class => "list name"}, esc_html($tag{'name'})) .
5223                       "</td>\n" .
5224                       "<td>";
5225                 if (defined $comment) {
5226                         print format_subject_html($comment, $comment_short,
5227                                                   href(action=>"tag", hash=>$tag{'id'}));
5228                 }
5229                 print "</td>\n" .
5230                       "<td class=\"selflink\">";
5231                 if ($tag{'type'} eq "tag") {
5232                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5233                 } else {
5234                         print "&nbsp;";
5235                 }
5236                 print "</td>\n" .
5237                       "<td class=\"link\">" . " | " .
5238                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5239                 if ($tag{'reftype'} eq "commit") {
5240                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5241                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5242                 } elsif ($tag{'reftype'} eq "blob") {
5243                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5244                 }
5245                 print "</td>\n" .
5246                       "</tr>";
5247         }
5248         if (defined $extra) {
5249                 print "<tr>\n" .
5250                       "<td colspan=\"5\">$extra</td>\n" .
5251                       "</tr>\n";
5252         }
5253         print "</table>\n";
5256 sub git_heads_body {
5257         # uses global variable $project
5258         my ($headlist, $head, $from, $to, $extra) = @_;
5259         $from = 0 unless defined $from;
5260         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5262         print "<table class=\"heads\">\n";
5263         my $alternate = 1;
5264         for (my $i = $from; $i <= $to; $i++) {
5265                 my $entry = $headlist->[$i];
5266                 my %ref = %$entry;
5267                 my $curr = $ref{'id'} eq $head;
5268                 if ($alternate) {
5269                         print "<tr class=\"dark\">\n";
5270                 } else {
5271                         print "<tr class=\"light\">\n";
5272                 }
5273                 $alternate ^= 1;
5274                 print "<td><i>$ref{'age'}</i></td>\n" .
5275                       ($curr ? "<td class=\"current_head\">" : "<td>") .
5276                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5277                                -class => "list name"},esc_html($ref{'name'})) .
5278                       "</td>\n" .
5279                       "<td class=\"link\">" .
5280                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5281                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5282                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5283                       "</td>\n" .
5284                       "</tr>";
5285         }
5286         if (defined $extra) {
5287                 print "<tr>\n" .
5288                       "<td colspan=\"3\">$extra</td>\n" .
5289                       "</tr>\n";
5290         }
5291         print "</table>\n";
5294 # Display a single remote block
5295 sub git_remote_block {
5296         my ($remote, $rdata, $limit, $head) = @_;
5298         my $heads = $rdata->{'heads'};
5299         my $fetch = $rdata->{'fetch'};
5300         my $push = $rdata->{'push'};
5302         my $urls_table = "<table class=\"projects_list\">\n" ;
5304         if (defined $fetch) {
5305                 if ($fetch eq $push) {
5306                         $urls_table .= format_repo_url("URL", $fetch);
5307                 } else {
5308                         $urls_table .= format_repo_url("Fetch URL", $fetch);
5309                         $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5310                 }
5311         } elsif (defined $push) {
5312                 $urls_table .= format_repo_url("Push URL", $push);
5313         } else {
5314                 $urls_table .= format_repo_url("", "No remote URL");
5315         }
5317         $urls_table .= "</table>\n";
5319         my $dots;
5320         if (defined $limit && $limit < @$heads) {
5321                 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5322         }
5324         print $urls_table;
5325         git_heads_body($heads, $head, 0, $limit, $dots);
5328 # Display a list of remote names with the respective fetch and push URLs
5329 sub git_remotes_list {
5330         my ($remotedata, $limit) = @_;
5331         print "<table class=\"heads\">\n";
5332         my $alternate = 1;
5333         my @remotes = sort keys %$remotedata;
5335         my $limited = $limit && $limit < @remotes;
5337         $#remotes = $limit - 1 if $limited;
5339         while (my $remote = shift @remotes) {
5340                 my $rdata = $remotedata->{$remote};
5341                 my $fetch = $rdata->{'fetch'};
5342                 my $push = $rdata->{'push'};
5343                 if ($alternate) {
5344                         print "<tr class=\"dark\">\n";
5345                 } else {
5346                         print "<tr class=\"light\">\n";
5347                 }
5348                 $alternate ^= 1;
5349                 print "<td>" .
5350                       $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5351                                -class=> "list name"},esc_html($remote)) .
5352                       "</td>";
5353                 print "<td class=\"link\">" .
5354                       (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5355                       " | " .
5356                       (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5357                       "</td>";
5359                 print "</tr>\n";
5360         }
5362         if ($limited) {
5363                 print "<tr>\n" .
5364                       "<td colspan=\"3\">" .
5365                       $cgi->a({-href => href(action=>"remotes")}, "...") .
5366                       "</td>\n" . "</tr>\n";
5367         }
5369         print "</table>";
5372 # Display remote heads grouped by remote, unless there are too many
5373 # remotes, in which case we only display the remote names
5374 sub git_remotes_body {
5375         my ($remotedata, $limit, $head) = @_;
5376         if ($limit and $limit < keys %$remotedata) {
5377                 git_remotes_list($remotedata, $limit);
5378         } else {
5379                 fill_remote_heads($remotedata);
5380                 while (my ($remote, $rdata) = each %$remotedata) {
5381                         git_print_section({-class=>"remote", -id=>$remote},
5382                                 ["remotes", $remote, $remote], sub {
5383                                         git_remote_block($remote, $rdata, $limit, $head);
5384                                 });
5385                 }
5386         }
5389 sub git_search_grep_body {
5390         my ($commitlist, $from, $to, $extra) = @_;
5391         $from = 0 unless defined $from;
5392         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5394         print "<table class=\"commit_search\">\n";
5395         my $alternate = 1;
5396         for (my $i = $from; $i <= $to; $i++) {
5397                 my %co = %{$commitlist->[$i]};
5398                 if (!%co) {
5399                         next;
5400                 }
5401                 my $commit = $co{'id'};
5402                 if ($alternate) {
5403                         print "<tr class=\"dark\">\n";
5404                 } else {
5405                         print "<tr class=\"light\">\n";
5406                 }
5407                 $alternate ^= 1;
5408                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5409                       format_author_html('td', \%co, 15, 5) .
5410                       "<td>" .
5411                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5412                                -class => "list subject"},
5413                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5414                 my $comment = $co{'comment'};
5415                 foreach my $line (@$comment) {
5416                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
5417                                 my ($lead, $match, $trail) = ($1, $2, $3);
5418                                 $match = chop_str($match, 70, 5, 'center');
5419                                 my $contextlen = int((80 - length($match))/2);
5420                                 $contextlen = 30 if ($contextlen > 30);
5421                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
5422                                 $trail = chop_str($trail, $contextlen, 10, 'right');
5424                                 $lead  = esc_html($lead);
5425                                 $match = esc_html($match);
5426                                 $trail = esc_html($trail);
5428                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
5429                         }
5430                 }
5431                 print "</td>\n" .
5432                       "<td class=\"link\">" .
5433                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5434                       " | " .
5435                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
5436                       " | " .
5437                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5438                 print "</td>\n" .
5439                       "</tr>\n";
5440         }
5441         if (defined $extra) {
5442                 print "<tr>\n" .
5443                       "<td colspan=\"3\">$extra</td>\n" .
5444                       "</tr>\n";
5445         }
5446         print "</table>\n";
5449 ## ======================================================================
5450 ## ======================================================================
5451 ## actions
5453 sub git_project_list {
5454         my $order = $input_params{'order'};
5455         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5456                 die_error(400, "Unknown order parameter");
5457         }
5459         my @list = git_get_projects_list();
5460         if (!@list) {
5461                 die_error(404, "No projects found");
5462         }
5464         git_header_html();
5465         if (defined $home_text && -f $home_text) {
5466                 print "<div class=\"index_include\">\n";
5467                 insert_file($home_text);
5468                 print "</div>\n";
5469         }
5470         print $cgi->startform(-method => "get") .
5471               "<p class=\"projsearch\">Search:\n" .
5472               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5473               "</p>" .
5474               $cgi->end_form() . "\n";
5475         git_project_list_body(\@list, $order);
5476         git_footer_html();
5479 sub git_forks {
5480         my $order = $input_params{'order'};
5481         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5482                 die_error(400, "Unknown order parameter");
5483         }
5485         my @list = git_get_projects_list($project);
5486         if (!@list) {
5487                 die_error(404, "No forks found");
5488         }
5490         git_header_html();
5491         git_print_page_nav('','');
5492         git_print_header_div('summary', "$project forks");
5493         git_project_list_body(\@list, $order);
5494         git_footer_html();
5497 sub git_project_index {
5498         my @projects = git_get_projects_list();
5499         if (!@projects) {
5500                 die_error(404, "No projects found");
5501         }
5503         print $cgi->header(
5504                 -type => 'text/plain',
5505                 -charset => 'utf-8',
5506                 -content_disposition => 'inline; filename="index.aux"');
5508         foreach my $pr (@projects) {
5509                 if (!exists $pr->{'owner'}) {
5510                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5511                 }
5513                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5514                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5515                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5516                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5517                 $path  =~ s/ /\+/g;
5518                 $owner =~ s/ /\+/g;
5520                 print "$path $owner\n";
5521         }
5524 sub git_summary {
5525         my $descr = git_get_project_description($project) || "none";
5526         my %co = parse_commit("HEAD");
5527         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5528         my $head = $co{'id'};
5529         my $remote_heads = gitweb_check_feature('remote_heads');
5531         my $owner = git_get_project_owner($project);
5533         my $refs = git_get_references();
5534         # These get_*_list functions return one more to allow us to see if
5535         # there are more ...
5536         my @taglist  = git_get_tags_list(16);
5537         my @headlist = git_get_heads_list(16);
5538         my %remotedata = $remote_heads ? git_get_remotes_list() : ();
5539         my @forklist;
5540         my $check_forks = gitweb_check_feature('forks');
5542         if ($check_forks) {
5543                 # find forks of a project
5544                 @forklist = git_get_projects_list($project);
5545                 # filter out forks of forks
5546                 @forklist = filter_forks_from_projects_list(\@forklist)
5547                         if (@forklist);
5548         }
5550         git_header_html();
5551         git_print_page_nav('summary','', $head);
5553         print "<div class=\"title\">&nbsp;</div>\n";
5554         print "<table class=\"projects_list\">\n" .
5555               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5556               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5557         if (defined $cd{'rfc2822'}) {
5558                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5559         }
5561         # use per project git URL list in $projectroot/$project/cloneurl
5562         # or make project git URL from git base URL and project name
5563         my $url_tag = "URL";
5564         my @url_list = git_get_project_url_list($project);
5565         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5566         foreach my $git_url (@url_list) {
5567                 next unless $git_url;
5568                 print format_repo_url($url_tag, $git_url);
5569                 $url_tag = "";
5570         }
5572         # Tag cloud
5573         my $show_ctags = gitweb_check_feature('ctags');
5574         if ($show_ctags) {
5575                 my $ctags = git_get_project_ctags($project);
5576                 if (%$ctags) {
5577                         # without ability to add tags, don't show if there are none
5578                         my $cloud = git_populate_project_tagcloud($ctags);
5579                         print "<tr id=\"metadata_ctags\">" .
5580                               "<td>content tags</td>" .
5581                               "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
5582                               "</tr>\n";
5583                 }
5584         }
5586         print "</table>\n";
5588         # If XSS prevention is on, we don't include README.html.
5589         # TODO: Allow a readme in some safe format.
5590         if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5591                 print "<div class=\"title\">readme</div>\n" .
5592                       "<div class=\"readme\">\n";
5593                 insert_file("$projectroot/$project/README.html");
5594                 print "\n</div>\n"; # class="readme"
5595         }
5597         # we need to request one more than 16 (0..15) to check if
5598         # those 16 are all
5599         my @commitlist = $head ? parse_commits($head, 17) : ();
5600         if (@commitlist) {
5601                 git_print_header_div('shortlog');
5602                 git_shortlog_body(\@commitlist, 0, 15, $refs,
5603                                   $#commitlist <=  15 ? undef :
5604                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
5605         }
5607         if (@taglist) {
5608                 git_print_header_div('tags');
5609                 git_tags_body(\@taglist, 0, 15,
5610                               $#taglist <=  15 ? undef :
5611                               $cgi->a({-href => href(action=>"tags")}, "..."));
5612         }
5614         if (@headlist) {
5615                 git_print_header_div('heads');
5616                 git_heads_body(\@headlist, $head, 0, 15,
5617                                $#headlist <= 15 ? undef :
5618                                $cgi->a({-href => href(action=>"heads")}, "..."));
5619         }
5621         if (%remotedata) {
5622                 git_print_header_div('remotes');
5623                 git_remotes_body(\%remotedata, 15, $head);
5624         }
5626         if (@forklist) {
5627                 git_print_header_div('forks');
5628                 git_project_list_body(\@forklist, 'age', 0, 15,
5629                                       $#forklist <= 15 ? undef :
5630                                       $cgi->a({-href => href(action=>"forks")}, "..."),
5631                                       'no_header');
5632         }
5634         git_footer_html();
5637 sub git_tag {
5638         my %tag = parse_tag($hash);
5640         if (! %tag) {
5641                 die_error(404, "Unknown tag object");
5642         }
5644         my $head = git_get_head_hash($project);
5645         git_header_html();
5646         git_print_page_nav('','', $head,undef,$head);
5647         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5648         print "<div class=\"title_text\">\n" .
5649               "<table class=\"object_header\">\n" .
5650               "<tr>\n" .
5651               "<td>object</td>\n" .
5652               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5653                                $tag{'object'}) . "</td>\n" .
5654               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5655                                               $tag{'type'}) . "</td>\n" .
5656               "</tr>\n";
5657         if (defined($tag{'author'})) {
5658                 git_print_authorship_rows(\%tag, 'author');
5659         }
5660         print "</table>\n\n" .
5661               "</div>\n";
5662         print "<div class=\"page_body\">";
5663         my $comment = $tag{'comment'};
5664         foreach my $line (@$comment) {
5665                 chomp $line;
5666                 print esc_html($line, -nbsp=>1) . "<br/>\n";
5667         }
5668         print "</div>\n";
5669         git_footer_html();
5672 sub git_blame_common {
5673         my $format = shift || 'porcelain';
5674         if ($format eq 'porcelain' && $cgi->param('js')) {
5675                 $format = 'incremental';
5676                 $action = 'blame_incremental'; # for page title etc
5677         }
5679         # permissions
5680         gitweb_check_feature('blame')
5681                 or die_error(403, "Blame view not allowed");
5683         # error checking
5684         die_error(400, "No file name given") unless $file_name;
5685         $hash_base ||= git_get_head_hash($project);
5686         die_error(404, "Couldn't find base commit") unless $hash_base;
5687         my %co = parse_commit($hash_base)
5688                 or die_error(404, "Commit not found");
5689         my $ftype = "blob";
5690         if (!defined $hash) {
5691                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5692                         or die_error(404, "Error looking up file");
5693         } else {
5694                 $ftype = git_get_type($hash);
5695                 if ($ftype !~ "blob") {
5696                         die_error(400, "Object is not a blob");
5697                 }
5698         }
5700         my $fd;
5701         if ($format eq 'incremental') {
5702                 # get file contents (as base)
5703                 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5704                         or die_error(500, "Open git-cat-file failed");
5705         } elsif ($format eq 'data') {
5706                 # run git-blame --incremental
5707                 open $fd, "-|", git_cmd(), "blame", "--incremental",
5708                         $hash_base, "--", $file_name
5709                         or die_error(500, "Open git-blame --incremental failed");
5710         } else {
5711                 # run git-blame --porcelain
5712                 open $fd, "-|", git_cmd(), "blame", '-p',
5713                         $hash_base, '--', $file_name
5714                         or die_error(500, "Open git-blame --porcelain failed");
5715         }
5717         # incremental blame data returns early
5718         if ($format eq 'data') {
5719                 print $cgi->header(
5720                         -type=>"text/plain", -charset => "utf-8",
5721                         -status=> "200 OK");
5722                 local $| = 1; # output autoflush
5723                 print while <$fd>;
5724                 close $fd
5725                         or print "ERROR $!\n";
5727                 print 'END';
5728                 if (defined $t0 && gitweb_check_feature('timed')) {
5729                         print ' '.
5730                               tv_interval($t0, [ gettimeofday() ]).
5731                               ' '.$number_of_git_cmds;
5732                 }
5733                 print "\n";
5735                 return;
5736         }
5738         # page header
5739         git_header_html();
5740         my $formats_nav =
5741                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5742                         "blob") .
5743                 " | ";
5744         if ($format eq 'incremental') {
5745                 $formats_nav .=
5746                         $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5747                                 "blame") . " (non-incremental)";
5748         } else {
5749                 $formats_nav .=
5750                         $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5751                                 "blame") . " (incremental)";
5752         }
5753         $formats_nav .=
5754                 " | " .
5755                 $cgi->a({-href => href(action=>"history", -replay=>1)},
5756                         "history") .
5757                 " | " .
5758                 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5759                         "HEAD");
5760         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5761         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5762         git_print_page_path($file_name, $ftype, $hash_base);
5764         # page body
5765         if ($format eq 'incremental') {
5766                 print "<noscript>\n<div class=\"error\"><center><b>\n".
5767                       "This page requires JavaScript to run.\n Use ".
5768                       $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5769                               'this page').
5770                       " instead.\n".
5771                       "</b></center></div>\n</noscript>\n";
5773                 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5774         }
5776         print qq!<div class="page_body">\n!;
5777         print qq!<div id="progress_info">... / ...</div>\n!
5778                 if ($format eq 'incremental');
5779         print qq!<table id="blame_table" class="blame" width="100%">\n!.
5780               #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5781               qq!<thead>\n!.
5782               qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5783               qq!</thead>\n!.
5784               qq!<tbody>\n!;
5786         my @rev_color = qw(light dark);
5787         my $num_colors = scalar(@rev_color);
5788         my $current_color = 0;
5790         if ($format eq 'incremental') {
5791                 my $color_class = $rev_color[$current_color];
5793                 #contents of a file
5794                 my $linenr = 0;
5795         LINE:
5796                 while (my $line = <$fd>) {
5797                         chomp $line;
5798                         $linenr++;
5800                         print qq!<tr id="l$linenr" class="$color_class">!.
5801                               qq!<td class="sha1"><a href=""> </a></td>!.
5802                               qq!<td class="linenr">!.
5803                               qq!<a class="linenr" href="">$linenr</a></td>!;
5804                         print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5805                         print qq!</tr>\n!;
5806                 }
5808         } else { # porcelain, i.e. ordinary blame
5809                 my %metainfo = (); # saves information about commits
5811                 # blame data
5812         LINE:
5813                 while (my $line = <$fd>) {
5814                         chomp $line;
5815                         # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5816                         # no <lines in group> for subsequent lines in group of lines
5817                         my ($full_rev, $orig_lineno, $lineno, $group_size) =
5818                            ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5819                         if (!exists $metainfo{$full_rev}) {
5820                                 $metainfo{$full_rev} = { 'nprevious' => 0 };
5821                         }
5822                         my $meta = $metainfo{$full_rev};
5823                         my $data;
5824                         while ($data = <$fd>) {
5825                                 chomp $data;
5826                                 last if ($data =~ s/^\t//); # contents of line
5827                                 if ($data =~ /^(\S+)(?: (.*))?$/) {
5828                                         $meta->{$1} = $2 unless exists $meta->{$1};
5829                                 }
5830                                 if ($data =~ /^previous /) {
5831                                         $meta->{'nprevious'}++;
5832                                 }
5833                         }
5834                         my $short_rev = substr($full_rev, 0, 8);
5835                         my $author = $meta->{'author'};
5836                         my %date =
5837                                 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5838                         my $date = $date{'iso-tz'};
5839                         if ($group_size) {
5840                                 $current_color = ($current_color + 1) % $num_colors;
5841                         }
5842                         my $tr_class = $rev_color[$current_color];
5843                         $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5844                         $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5845                         $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5846                         print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5847                         if ($group_size) {
5848                                 print "<td class=\"sha1\"";
5849                                 print " title=\"". esc_html($author) . ", $date\"";
5850                                 print " rowspan=\"$group_size\"" if ($group_size > 1);
5851                                 print ">";
5852                                 print $cgi->a({-href => href(action=>"commit",
5853                                                              hash=>$full_rev,
5854                                                              file_name=>$file_name)},
5855                                               esc_html($short_rev));
5856                                 if ($group_size >= 2) {
5857                                         my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5858                                         if (@author_initials) {
5859                                                 print "<br />" .
5860                                                       esc_html(join('', @author_initials));
5861                                                 #           or join('.', ...)
5862                                         }
5863                                 }
5864                                 print "</td>\n";
5865                         }
5866                         # 'previous' <sha1 of parent commit> <filename at commit>
5867                         if (exists $meta->{'previous'} &&
5868                             $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5869                                 $meta->{'parent'} = $1;
5870                                 $meta->{'file_parent'} = unquote($2);
5871                         }
5872                         my $linenr_commit =
5873                                 exists($meta->{'parent'}) ?
5874                                 $meta->{'parent'} : $full_rev;
5875                         my $linenr_filename =
5876                                 exists($meta->{'file_parent'}) ?
5877                                 $meta->{'file_parent'} : unquote($meta->{'filename'});
5878                         my $blamed = href(action => 'blame',
5879                                           file_name => $linenr_filename,
5880                                           hash_base => $linenr_commit);
5881                         print "<td class=\"linenr\">";
5882                         print $cgi->a({ -href => "$blamed#l$orig_lineno",
5883                                         -class => "linenr" },
5884                                       esc_html($lineno));
5885                         print "</td>";
5886                         print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5887                         print "</tr>\n";
5888                 } # end while
5890         }
5892         # footer
5893         print "</tbody>\n".
5894               "</table>\n"; # class="blame"
5895         print "</div>\n";   # class="blame_body"
5896         close $fd
5897                 or print "Reading blob failed\n";
5899         git_footer_html();
5902 sub git_blame {
5903         git_blame_common();
5906 sub git_blame_incremental {
5907         git_blame_common('incremental');
5910 sub git_blame_data {
5911         git_blame_common('data');
5914 sub git_tags {
5915         my $head = git_get_head_hash($project);
5916         git_header_html();
5917         git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
5918         git_print_header_div('summary', $project);
5920         my @tagslist = git_get_tags_list();
5921         if (@tagslist) {
5922                 git_tags_body(\@tagslist);
5923         }
5924         git_footer_html();
5927 sub git_heads {
5928         my $head = git_get_head_hash($project);
5929         git_header_html();
5930         git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
5931         git_print_header_div('summary', $project);
5933         my @headslist = git_get_heads_list();
5934         if (@headslist) {
5935                 git_heads_body(\@headslist, $head);
5936         }
5937         git_footer_html();
5940 # used both for single remote view and for list of all the remotes
5941 sub git_remotes {
5942         gitweb_check_feature('remote_heads')
5943                 or die_error(403, "Remote heads view is disabled");
5945         my $head = git_get_head_hash($project);
5946         my $remote = $input_params{'hash'};
5948         my $remotedata = git_get_remotes_list($remote);
5949         die_error(500, "Unable to get remote information") unless defined $remotedata;
5951         unless (%$remotedata) {
5952                 die_error(404, defined $remote ?
5953                         "Remote $remote not found" :
5954                         "No remotes found");
5955         }
5957         git_header_html(undef, undef, -action_extra => $remote);
5958         git_print_page_nav('', '',  $head, undef, $head,
5959                 format_ref_views($remote ? '' : 'remotes'));
5961         fill_remote_heads($remotedata);
5962         if (defined $remote) {
5963                 git_print_header_div('remotes', "$remote remote for $project");
5964                 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
5965         } else {
5966                 git_print_header_div('summary', "$project remotes");
5967                 git_remotes_body($remotedata, undef, $head);
5968         }
5970         git_footer_html();
5973 sub git_blob_plain {
5974         my $type = shift;
5975         my $expires;
5977         if (!defined $hash) {
5978                 if (defined $file_name) {
5979                         my $base = $hash_base || git_get_head_hash($project);
5980                         $hash = git_get_hash_by_path($base, $file_name, "blob")
5981                                 or die_error(404, "Cannot find file");
5982                 } else {
5983                         die_error(400, "No file name defined");
5984                 }
5985         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5986                 # blobs defined by non-textual hash id's can be cached
5987                 $expires = "+1d";
5988         }
5990         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5991                 or die_error(500, "Open git-cat-file blob '$hash' failed");
5993         # content-type (can include charset)
5994         $type = blob_contenttype($fd, $file_name, $type);
5996         # "save as" filename, even when no $file_name is given
5997         my $save_as = "$hash";
5998         if (defined $file_name) {
5999                 $save_as = $file_name;
6000         } elsif ($type =~ m/^text\//) {
6001                 $save_as .= '.txt';
6002         }
6004         # With XSS prevention on, blobs of all types except a few known safe
6005         # ones are served with "Content-Disposition: attachment" to make sure
6006         # they don't run in our security domain.  For certain image types,
6007         # blob view writes an <img> tag referring to blob_plain view, and we
6008         # want to be sure not to break that by serving the image as an
6009         # attachment (though Firefox 3 doesn't seem to care).
6010         my $sandbox = $prevent_xss &&
6011                 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
6013         print $cgi->header(
6014                 -type => $type,
6015                 -expires => $expires,
6016                 -content_disposition =>
6017                         ($sandbox ? 'attachment' : 'inline')
6018                         . '; filename="' . $save_as . '"');
6019         local $/ = undef;
6020         binmode STDOUT, ':raw';
6021         print <$fd>;
6022         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6023         close $fd;
6026 sub git_blob {
6027         my $expires;
6029         if (!defined $hash) {
6030                 if (defined $file_name) {
6031                         my $base = $hash_base || git_get_head_hash($project);
6032                         $hash = git_get_hash_by_path($base, $file_name, "blob")
6033                                 or die_error(404, "Cannot find file");
6034                 } else {
6035                         die_error(400, "No file name defined");
6036                 }
6037         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6038                 # blobs defined by non-textual hash id's can be cached
6039                 $expires = "+1d";
6040         }
6042         my $have_blame = gitweb_check_feature('blame');
6043         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6044                 or die_error(500, "Couldn't cat $file_name, $hash");
6045         my $mimetype = blob_mimetype($fd, $file_name);
6046         # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6047         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6048                 close $fd;
6049                 return git_blob_plain($mimetype);
6050         }
6051         # we can have blame only for text/* mimetype
6052         $have_blame &&= ($mimetype =~ m!^text/!);
6054         my $highlight = gitweb_check_feature('highlight');
6055         my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6056         $fd = run_highlighter($fd, $highlight, $syntax)
6057                 if $syntax;
6059         git_header_html(undef, $expires);
6060         my $formats_nav = '';
6061         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6062                 if (defined $file_name) {
6063                         if ($have_blame) {
6064                                 $formats_nav .=
6065                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
6066                                                 "blame") .
6067                                         " | ";
6068                         }
6069                         $formats_nav .=
6070                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6071                                         "history") .
6072                                 " | " .
6073                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6074                                         "raw") .
6075                                 " | " .
6076                                 $cgi->a({-href => href(action=>"blob",
6077                                                        hash_base=>"HEAD", file_name=>$file_name)},
6078                                         "HEAD");
6079                 } else {
6080                         $formats_nav .=
6081                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6082                                         "raw");
6083                 }
6084                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6085                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6086         } else {
6087                 print "<div class=\"page_nav\">\n" .
6088                       "<br/><br/></div>\n" .
6089                       "<div class=\"title\">".esc_html($hash)."</div>\n";
6090         }
6091         git_print_page_path($file_name, "blob", $hash_base);
6092         print "<div class=\"page_body\">\n";
6093         if ($mimetype =~ m!^image/!) {
6094                 print qq!<img type="!.esc_attr($mimetype).qq!"!;
6095                 if ($file_name) {
6096                         print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6097                 }
6098                 print qq! src="! .
6099                       href(action=>"blob_plain", hash=>$hash,
6100                            hash_base=>$hash_base, file_name=>$file_name) .
6101                       qq!" />\n!;
6102         } else {
6103                 my $nr;
6104                 while (my $line = <$fd>) {
6105                         chomp $line;
6106                         $nr++;
6107                         $line = untabify($line);
6108                         printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6109                                $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
6110                 }
6111         }
6112         close $fd
6113                 or print "Reading blob failed.\n";
6114         print "</div>";
6115         git_footer_html();
6118 sub git_tree {
6119         if (!defined $hash_base) {
6120                 $hash_base = "HEAD";
6121         }
6122         if (!defined $hash) {
6123                 if (defined $file_name) {
6124                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6125                 } else {
6126                         $hash = $hash_base;
6127                 }
6128         }
6129         die_error(404, "No such tree") unless defined($hash);
6131         my $show_sizes = gitweb_check_feature('show-sizes');
6132         my $have_blame = gitweb_check_feature('blame');
6134         my @entries = ();
6135         {
6136                 local $/ = "\0";
6137                 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6138                         ($show_sizes ? '-l' : ()), @extra_options, $hash
6139                         or die_error(500, "Open git-ls-tree failed");
6140                 @entries = map { chomp; $_ } <$fd>;
6141                 close $fd
6142                         or die_error(404, "Reading tree failed");
6143         }
6145         my $refs = git_get_references();
6146         my $ref = format_ref_marker($refs, $hash_base);
6147         git_header_html();
6148         my $basedir = '';
6149         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6150                 my @views_nav = ();
6151                 if (defined $file_name) {
6152                         push @views_nav,
6153                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
6154                                         "history"),
6155                                 $cgi->a({-href => href(action=>"tree",
6156                                                        hash_base=>"HEAD", file_name=>$file_name)},
6157                                         "HEAD"),
6158                 }
6159                 my $snapshot_links = format_snapshot_links($hash);
6160                 if (defined $snapshot_links) {
6161                         # FIXME: Should be available when we have no hash base as well.
6162                         push @views_nav, $snapshot_links;
6163                 }
6164                 git_print_page_nav('tree','', $hash_base, undef, undef,
6165                                    join(' | ', @views_nav));
6166                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
6167         } else {
6168                 undef $hash_base;
6169                 print "<div class=\"page_nav\">\n";
6170                 print "<br/><br/></div>\n";
6171                 print "<div class=\"title\">".esc_html($hash)."</div>\n";
6172         }
6173         if (defined $file_name) {
6174                 $basedir = $file_name;
6175                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
6176                         $basedir .= '/';
6177                 }
6178                 git_print_page_path($file_name, 'tree', $hash_base);
6179         }
6180         print "<div class=\"page_body\">\n";
6181         print "<table class=\"tree\">\n";
6182         my $alternate = 1;
6183         # '..' (top directory) link if possible
6184         if (defined $hash_base &&
6185             defined $file_name && $file_name =~ m![^/]+$!) {
6186                 if ($alternate) {
6187                         print "<tr class=\"dark\">\n";
6188                 } else {
6189                         print "<tr class=\"light\">\n";
6190                 }
6191                 $alternate ^= 1;
6193                 my $up = $file_name;
6194                 $up =~ s!/?[^/]+$!!;
6195                 undef $up unless $up;
6196                 # based on git_print_tree_entry
6197                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
6198                 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
6199                 print '<td class="list">';
6200                 print $cgi->a({-href => href(action=>"tree",
6201                                              hash_base=>$hash_base,
6202                                              file_name=>$up)},
6203                               "..");
6204                 print "</td>\n";
6205                 print "<td class=\"link\"></td>\n";
6207                 print "</tr>\n";
6208         }
6209         foreach my $line (@entries) {
6210                 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
6212                 if ($alternate) {
6213                         print "<tr class=\"dark\">\n";
6214                 } else {
6215                         print "<tr class=\"light\">\n";
6216                 }
6217                 $alternate ^= 1;
6219                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
6221                 print "</tr>\n";
6222         }
6223         print "</table>\n" .
6224               "</div>";
6225         git_footer_html();
6228 sub snapshot_name {
6229         my ($project, $hash) = @_;
6231         # path/to/project.git  -> project
6232         # path/to/project/.git -> project
6233         my $name = to_utf8($project);
6234         $name =~ s,([^/])/*\.git$,$1,;
6235         $name = basename($name);
6236         # sanitize name
6237         $name =~ s/[[:cntrl:]]/?/g;
6239         my $ver = $hash;
6240         if ($hash =~ /^[0-9a-fA-F]+$/) {
6241                 # shorten SHA-1 hash
6242                 my $full_hash = git_get_full_hash($project, $hash);
6243                 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6244                         $ver = git_get_short_hash($project, $hash);
6245                 }
6246         } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6247                 # tags don't need shortened SHA-1 hash
6248                 $ver = $1;
6249         } else {
6250                 # branches and other need shortened SHA-1 hash
6251                 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
6252                         $ver = $1;
6253                 }
6254                 $ver .= '-' . git_get_short_hash($project, $hash);
6255         }
6256         # in case of hierarchical branch names
6257         $ver =~ s!/!.!g;
6259         # name = project-version_string
6260         $name = "$name-$ver";
6262         return wantarray ? ($name, $name) : $name;
6265 sub git_snapshot {
6266         my $format = $input_params{'snapshot_format'};
6267         if (!@snapshot_fmts) {
6268                 die_error(403, "Snapshots not allowed");
6269         }
6270         # default to first supported snapshot format
6271         $format ||= $snapshot_fmts[0];
6272         if ($format !~ m/^[a-z0-9]+$/) {
6273                 die_error(400, "Invalid snapshot format parameter");
6274         } elsif (!exists($known_snapshot_formats{$format})) {
6275                 die_error(400, "Unknown snapshot format");
6276         } elsif ($known_snapshot_formats{$format}{'disabled'}) {
6277                 die_error(403, "Snapshot format not allowed");
6278         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
6279                 die_error(403, "Unsupported snapshot format");
6280         }
6282         my $type = git_get_type("$hash^{}");
6283         if (!$type) {
6284                 die_error(404, 'Object does not exist');
6285         }  elsif ($type eq 'blob') {
6286                 die_error(400, 'Object is not a tree-ish');
6287         }
6289         my ($name, $prefix) = snapshot_name($project, $hash);
6290         my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
6291         my $cmd = quote_command(
6292                 git_cmd(), 'archive',
6293                 "--format=$known_snapshot_formats{$format}{'format'}",
6294                 "--prefix=$prefix/", $hash);
6295         if (exists $known_snapshot_formats{$format}{'compressor'}) {
6296                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
6297         }
6299         $filename =~ s/(["\\])/\\$1/g;
6300         print $cgi->header(
6301                 -type => $known_snapshot_formats{$format}{'type'},
6302                 -content_disposition => 'inline; filename="' . $filename . '"',
6303                 -status => '200 OK');
6305         open my $fd, "-|", $cmd
6306                 or die_error(500, "Execute git-archive failed");
6307         binmode STDOUT, ':raw';
6308         print <$fd>;
6309         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6310         close $fd;
6313 sub git_log_generic {
6314         my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
6316         my $head = git_get_head_hash($project);
6317         if (!defined $base) {
6318                 $base = $head;
6319         }
6320         if (!defined $page) {
6321                 $page = 0;
6322         }
6323         my $refs = git_get_references();
6325         my $commit_hash = $base;
6326         if (defined $parent) {
6327                 $commit_hash = "$parent..$base";
6328         }
6329         my @commitlist =
6330                 parse_commits($commit_hash, 101, (100 * $page),
6331                               defined $file_name ? ($file_name, "--full-history") : ());
6333         my $ftype;
6334         if (!defined $file_hash && defined $file_name) {
6335                 # some commits could have deleted file in question,
6336                 # and not have it in tree, but one of them has to have it
6337                 for (my $i = 0; $i < @commitlist; $i++) {
6338                         $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
6339                         last if defined $file_hash;
6340                 }
6341         }
6342         if (defined $file_hash) {
6343                 $ftype = git_get_type($file_hash);
6344         }
6345         if (defined $file_name && !defined $ftype) {
6346                 die_error(500, "Unknown type of object");
6347         }
6348         my %co;
6349         if (defined $file_name) {
6350                 %co = parse_commit($base)
6351                         or die_error(404, "Unknown commit object");
6352         }
6355         my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
6356         my $next_link = '';
6357         if ($#commitlist >= 100) {
6358                 $next_link =
6359                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
6360                                  -accesskey => "n", -title => "Alt-n"}, "next");
6361         }
6362         my $patch_max = gitweb_get_feature('patches');
6363         if ($patch_max && !defined $file_name) {
6364                 if ($patch_max < 0 || @commitlist <= $patch_max) {
6365                         $paging_nav .= " &sdot; " .
6366                                 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6367                                         "patches");
6368                 }
6369         }
6371         git_header_html();
6372         git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
6373         if (defined $file_name) {
6374                 git_print_header_div('commit', esc_html($co{'title'}), $base);
6375         } else {
6376                 git_print_header_div('summary', $project)
6377         }
6378         git_print_page_path($file_name, $ftype, $hash_base)
6379                 if (defined $file_name);
6381         $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
6382                      $file_name, $file_hash, $ftype);
6384         git_footer_html();
6387 sub git_log {
6388         git_log_generic('log', \&git_log_body,
6389                         $hash, $hash_parent);
6392 sub git_commit {
6393         $hash ||= $hash_base || "HEAD";
6394         my %co = parse_commit($hash)
6395             or die_error(404, "Unknown commit object");
6397         my $parent  = $co{'parent'};
6398         my $parents = $co{'parents'}; # listref
6400         # we need to prepare $formats_nav before any parameter munging
6401         my $formats_nav;
6402         if (!defined $parent) {
6403                 # --root commitdiff
6404                 $formats_nav .= '(initial)';
6405         } elsif (@$parents == 1) {
6406                 # single parent commit
6407                 $formats_nav .=
6408                         '(parent: ' .
6409                         $cgi->a({-href => href(action=>"commit",
6410                                                hash=>$parent)},
6411                                 esc_html(substr($parent, 0, 7))) .
6412                         ')';
6413         } else {
6414                 # merge commit
6415                 $formats_nav .=
6416                         '(merge: ' .
6417                         join(' ', map {
6418                                 $cgi->a({-href => href(action=>"commit",
6419                                                        hash=>$_)},
6420                                         esc_html(substr($_, 0, 7)));
6421                         } @$parents ) .
6422                         ')';
6423         }
6424         if (gitweb_check_feature('patches') && @$parents <= 1) {
6425                 $formats_nav .= " | " .
6426                         $cgi->a({-href => href(action=>"patch", -replay=>1)},
6427                                 "patch");
6428         }
6430         if (!defined $parent) {
6431                 $parent = "--root";
6432         }
6433         my @difftree;
6434         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
6435                 @diff_opts,
6436                 (@$parents <= 1 ? $parent : '-c'),
6437                 $hash, "--"
6438                 or die_error(500, "Open git-diff-tree failed");
6439         @difftree = map { chomp; $_ } <$fd>;
6440         close $fd or die_error(404, "Reading git-diff-tree failed");
6442         # non-textual hash id's can be cached
6443         my $expires;
6444         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6445                 $expires = "+1d";
6446         }
6447         my $refs = git_get_references();
6448         my $ref = format_ref_marker($refs, $co{'id'});
6450         git_header_html(undef, $expires);
6451         git_print_page_nav('commit', '',
6452                            $hash, $co{'tree'}, $hash,
6453                            $formats_nav);
6455         if (defined $co{'parent'}) {
6456                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
6457         } else {
6458                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
6459         }
6460         print "<div class=\"title_text\">\n" .
6461               "<table class=\"object_header\">\n";
6462         git_print_authorship_rows(\%co);
6463         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
6464         print "<tr>" .
6465               "<td>tree</td>" .
6466               "<td class=\"sha1\">" .
6467               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
6468                        class => "list"}, $co{'tree'}) .
6469               "</td>" .
6470               "<td class=\"link\">" .
6471               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
6472                       "tree");
6473         my $snapshot_links = format_snapshot_links($hash);
6474         if (defined $snapshot_links) {
6475                 print " | " . $snapshot_links;
6476         }
6477         print "</td>" .
6478               "</tr>\n";
6480         foreach my $par (@$parents) {
6481                 print "<tr>" .
6482                       "<td>parent</td>" .
6483                       "<td class=\"sha1\">" .
6484                       $cgi->a({-href => href(action=>"commit", hash=>$par),
6485                                class => "list"}, $par) .
6486                       "</td>" .
6487                       "<td class=\"link\">" .
6488                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
6489                       " | " .
6490                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
6491                       "</td>" .
6492                       "</tr>\n";
6493         }
6494         print "</table>".
6495               "</div>\n";
6497         print "<div class=\"page_body\">\n";
6498         git_print_log($co{'comment'});
6499         print "</div>\n";
6501         git_difftree_body(\@difftree, $hash, @$parents);
6503         git_footer_html();
6506 sub git_object {
6507         # object is defined by:
6508         # - hash or hash_base alone
6509         # - hash_base and file_name
6510         my $type;
6512         # - hash or hash_base alone
6513         if ($hash || ($hash_base && !defined $file_name)) {
6514                 my $object_id = $hash || $hash_base;
6516                 open my $fd, "-|", quote_command(
6517                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
6518                         or die_error(404, "Object does not exist");
6519                 $type = <$fd>;
6520                 chomp $type;
6521                 close $fd
6522                         or die_error(404, "Object does not exist");
6524         # - hash_base and file_name
6525         } elsif ($hash_base && defined $file_name) {
6526                 $file_name =~ s,/+$,,;
6528                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
6529                         or die_error(404, "Base object does not exist");
6531                 # here errors should not hapen
6532                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
6533                         or die_error(500, "Open git-ls-tree failed");
6534                 my $line = <$fd>;
6535                 close $fd;
6537                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
6538                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
6539                         die_error(404, "File or directory for given base does not exist");
6540                 }
6541                 $type = $2;
6542                 $hash = $3;
6543         } else {
6544                 die_error(400, "Not enough information to find object");
6545         }
6547         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
6548                                           hash=>$hash, hash_base=>$hash_base,
6549                                           file_name=>$file_name),
6550                              -status => '302 Found');
6553 sub git_blobdiff {
6554         my $format = shift || 'html';
6556         my $fd;
6557         my @difftree;
6558         my %diffinfo;
6559         my $expires;
6561         # preparing $fd and %diffinfo for git_patchset_body
6562         # new style URI
6563         if (defined $hash_base && defined $hash_parent_base) {
6564                 if (defined $file_name) {
6565                         # read raw output
6566                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6567                                 $hash_parent_base, $hash_base,
6568                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
6569                                 or die_error(500, "Open git-diff-tree failed");
6570                         @difftree = map { chomp; $_ } <$fd>;
6571                         close $fd
6572                                 or die_error(404, "Reading git-diff-tree failed");
6573                         @difftree
6574                                 or die_error(404, "Blob diff not found");
6576                 } elsif (defined $hash &&
6577                          $hash =~ /[0-9a-fA-F]{40}/) {
6578                         # try to find filename from $hash
6580                         # read filtered raw output
6581                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6582                                 $hash_parent_base, $hash_base, "--"
6583                                 or die_error(500, "Open git-diff-tree failed");
6584                         @difftree =
6585                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
6586                                 # $hash == to_id
6587                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
6588                                 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                 } else {
6595                         die_error(400, "Missing one of the blob diff parameters");
6596                 }
6598                 if (@difftree > 1) {
6599                         die_error(400, "Ambiguous blob diff specification");
6600                 }
6602                 %diffinfo = parse_difftree_raw_line($difftree[0]);
6603                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
6604                 $file_name   ||= $diffinfo{'to_file'};
6606                 $hash_parent ||= $diffinfo{'from_id'};
6607                 $hash        ||= $diffinfo{'to_id'};
6609                 # non-textual hash id's can be cached
6610                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6611                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6612                         $expires = '+1d';
6613                 }
6615                 # open patch output
6616                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6617                         '-p', ($format eq 'html' ? "--full-index" : ()),
6618                         $hash_parent_base, $hash_base,
6619                         "--", (defined $file_parent ? $file_parent : ()), $file_name
6620                         or die_error(500, "Open git-diff-tree failed");
6621         }
6623         # old/legacy style URI -- not generated anymore since 1.4.3.
6624         if (!%diffinfo) {
6625                 die_error('404 Not Found', "Missing one of the blob diff parameters")
6626         }
6628         # header
6629         if ($format eq 'html') {
6630                 my $formats_nav =
6631                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6632                                 "raw");
6633                 git_header_html(undef, $expires);
6634                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6635                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6636                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6637                 } else {
6638                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6639                         print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
6640                 }
6641                 if (defined $file_name) {
6642                         git_print_page_path($file_name, "blob", $hash_base);
6643                 } else {
6644                         print "<div class=\"page_path\"></div>\n";
6645                 }
6647         } elsif ($format eq 'plain') {
6648                 print $cgi->header(
6649                         -type => 'text/plain',
6650                         -charset => 'utf-8',
6651                         -expires => $expires,
6652                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6654                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6656         } else {
6657                 die_error(400, "Unknown blobdiff format");
6658         }
6660         # patch
6661         if ($format eq 'html') {
6662                 print "<div class=\"page_body\">\n";
6664                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6665                 close $fd;
6667                 print "</div>\n"; # class="page_body"
6668                 git_footer_html();
6670         } else {
6671                 while (my $line = <$fd>) {
6672                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6673                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6675                         print $line;
6677                         last if $line =~ m!^\+\+\+!;
6678                 }
6679                 local $/ = undef;
6680                 print <$fd>;
6681                 close $fd;
6682         }
6685 sub git_blobdiff_plain {
6686         git_blobdiff('plain');
6689 sub git_commitdiff {
6690         my %params = @_;
6691         my $format = $params{-format} || 'html';
6693         my ($patch_max) = gitweb_get_feature('patches');
6694         if ($format eq 'patch') {
6695                 die_error(403, "Patch view not allowed") unless $patch_max;
6696         }
6698         $hash ||= $hash_base || "HEAD";
6699         my %co = parse_commit($hash)
6700             or die_error(404, "Unknown commit object");
6702         # choose format for commitdiff for merge
6703         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6704                 $hash_parent = '--cc';
6705         }
6706         # we need to prepare $formats_nav before almost any parameter munging
6707         my $formats_nav;
6708         if ($format eq 'html') {
6709                 $formats_nav =
6710                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6711                                 "raw");
6712                 if ($patch_max && @{$co{'parents'}} <= 1) {
6713                         $formats_nav .= " | " .
6714                                 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6715                                         "patch");
6716                 }
6718                 if (defined $hash_parent &&
6719                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
6720                         # commitdiff with two commits given
6721                         my $hash_parent_short = $hash_parent;
6722                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6723                                 $hash_parent_short = substr($hash_parent, 0, 7);
6724                         }
6725                         $formats_nav .=
6726                                 ' (from';
6727                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6728                                 if ($co{'parents'}[$i] eq $hash_parent) {
6729                                         $formats_nav .= ' parent ' . ($i+1);
6730                                         last;
6731                                 }
6732                         }
6733                         $formats_nav .= ': ' .
6734                                 $cgi->a({-href => href(action=>"commitdiff",
6735                                                        hash=>$hash_parent)},
6736                                         esc_html($hash_parent_short)) .
6737                                 ')';
6738                 } elsif (!$co{'parent'}) {
6739                         # --root commitdiff
6740                         $formats_nav .= ' (initial)';
6741                 } elsif (scalar @{$co{'parents'}} == 1) {
6742                         # single parent commit
6743                         $formats_nav .=
6744                                 ' (parent: ' .
6745                                 $cgi->a({-href => href(action=>"commitdiff",
6746                                                        hash=>$co{'parent'})},
6747                                         esc_html(substr($co{'parent'}, 0, 7))) .
6748                                 ')';
6749                 } else {
6750                         # merge commit
6751                         if ($hash_parent eq '--cc') {
6752                                 $formats_nav .= ' | ' .
6753                                         $cgi->a({-href => href(action=>"commitdiff",
6754                                                                hash=>$hash, hash_parent=>'-c')},
6755                                                 'combined');
6756                         } else { # $hash_parent eq '-c'
6757                                 $formats_nav .= ' | ' .
6758                                         $cgi->a({-href => href(action=>"commitdiff",
6759                                                                hash=>$hash, hash_parent=>'--cc')},
6760                                                 'compact');
6761                         }
6762                         $formats_nav .=
6763                                 ' (merge: ' .
6764                                 join(' ', map {
6765                                         $cgi->a({-href => href(action=>"commitdiff",
6766                                                                hash=>$_)},
6767                                                 esc_html(substr($_, 0, 7)));
6768                                 } @{$co{'parents'}} ) .
6769                                 ')';
6770                 }
6771         }
6773         my $hash_parent_param = $hash_parent;
6774         if (!defined $hash_parent_param) {
6775                 # --cc for multiple parents, --root for parentless
6776                 $hash_parent_param =
6777                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6778         }
6780         # read commitdiff
6781         my $fd;
6782         my @difftree;
6783         if ($format eq 'html') {
6784                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6785                         "--no-commit-id", "--patch-with-raw", "--full-index",
6786                         $hash_parent_param, $hash, "--"
6787                         or die_error(500, "Open git-diff-tree failed");
6789                 while (my $line = <$fd>) {
6790                         chomp $line;
6791                         # empty line ends raw part of diff-tree output
6792                         last unless $line;
6793                         push @difftree, scalar parse_difftree_raw_line($line);
6794                 }
6796         } elsif ($format eq 'plain') {
6797                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6798                         '-p', $hash_parent_param, $hash, "--"
6799                         or die_error(500, "Open git-diff-tree failed");
6800         } elsif ($format eq 'patch') {
6801                 # For commit ranges, we limit the output to the number of
6802                 # patches specified in the 'patches' feature.
6803                 # For single commits, we limit the output to a single patch,
6804                 # diverging from the git-format-patch default.
6805                 my @commit_spec = ();
6806                 if ($hash_parent) {
6807                         if ($patch_max > 0) {
6808                                 push @commit_spec, "-$patch_max";
6809                         }
6810                         push @commit_spec, '-n', "$hash_parent..$hash";
6811                 } else {
6812                         if ($params{-single}) {
6813                                 push @commit_spec, '-1';
6814                         } else {
6815                                 if ($patch_max > 0) {
6816                                         push @commit_spec, "-$patch_max";
6817                                 }
6818                                 push @commit_spec, "-n";
6819                         }
6820                         push @commit_spec, '--root', $hash;
6821                 }
6822                 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
6823                         '--encoding=utf8', '--stdout', @commit_spec
6824                         or die_error(500, "Open git-format-patch failed");
6825         } else {
6826                 die_error(400, "Unknown commitdiff format");
6827         }
6829         # non-textual hash id's can be cached
6830         my $expires;
6831         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6832                 $expires = "+1d";
6833         }
6835         # write commit message
6836         if ($format eq 'html') {
6837                 my $refs = git_get_references();
6838                 my $ref = format_ref_marker($refs, $co{'id'});
6840                 git_header_html(undef, $expires);
6841                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6842                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6843                 print "<div class=\"title_text\">\n" .
6844                       "<table class=\"object_header\">\n";
6845                 git_print_authorship_rows(\%co);
6846                 print "</table>".
6847                       "</div>\n";
6848                 print "<div class=\"page_body\">\n";
6849                 if (@{$co{'comment'}} > 1) {
6850                         print "<div class=\"log\">\n";
6851                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6852                         print "</div>\n"; # class="log"
6853                 }
6855         } elsif ($format eq 'plain') {
6856                 my $refs = git_get_references("tags");
6857                 my $tagname = git_get_rev_name_tags($hash);
6858                 my $filename = basename($project) . "-$hash.patch";
6860                 print $cgi->header(
6861                         -type => 'text/plain',
6862                         -charset => 'utf-8',
6863                         -expires => $expires,
6864                         -content_disposition => 'inline; filename="' . "$filename" . '"');
6865                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6866                 print "From: " . to_utf8($co{'author'}) . "\n";
6867                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6868                 print "Subject: " . to_utf8($co{'title'}) . "\n";
6870                 print "X-Git-Tag: $tagname\n" if $tagname;
6871                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6873                 foreach my $line (@{$co{'comment'}}) {
6874                         print to_utf8($line) . "\n";
6875                 }
6876                 print "---\n\n";
6877         } elsif ($format eq 'patch') {
6878                 my $filename = basename($project) . "-$hash.patch";
6880                 print $cgi->header(
6881                         -type => 'text/plain',
6882                         -charset => 'utf-8',
6883                         -expires => $expires,
6884                         -content_disposition => 'inline; filename="' . "$filename" . '"');
6885         }
6887         # write patch
6888         if ($format eq 'html') {
6889                 my $use_parents = !defined $hash_parent ||
6890                         $hash_parent eq '-c' || $hash_parent eq '--cc';
6891                 git_difftree_body(\@difftree, $hash,
6892                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
6893                 print "<br/>\n";
6895                 git_patchset_body($fd, \@difftree, $hash,
6896                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
6897                 close $fd;
6898                 print "</div>\n"; # class="page_body"
6899                 git_footer_html();
6901         } elsif ($format eq 'plain') {
6902                 local $/ = undef;
6903                 print <$fd>;
6904                 close $fd
6905                         or print "Reading git-diff-tree failed\n";
6906         } elsif ($format eq 'patch') {
6907                 local $/ = undef;
6908                 print <$fd>;
6909                 close $fd
6910                         or print "Reading git-format-patch failed\n";
6911         }
6914 sub git_commitdiff_plain {
6915         git_commitdiff(-format => 'plain');
6918 # format-patch-style patches
6919 sub git_patch {
6920         git_commitdiff(-format => 'patch', -single => 1);
6923 sub git_patches {
6924         git_commitdiff(-format => 'patch');
6927 sub git_history {
6928         git_log_generic('history', \&git_history_body,
6929                         $hash_base, $hash_parent_base,
6930                         $file_name, $hash);
6933 sub git_search {
6934         gitweb_check_feature('search') or die_error(403, "Search is disabled");
6935         if (!defined $searchtext) {
6936                 die_error(400, "Text field is empty");
6937         }
6938         if (!defined $hash) {
6939                 $hash = git_get_head_hash($project);
6940         }
6941         my %co = parse_commit($hash);
6942         if (!%co) {
6943                 die_error(404, "Unknown commit object");
6944         }
6945         if (!defined $page) {
6946                 $page = 0;
6947         }
6949         $searchtype ||= 'commit';
6950         if ($searchtype eq 'pickaxe') {
6951                 # pickaxe may take all resources of your box and run for several minutes
6952                 # with every query - so decide by yourself how public you make this feature
6953                 gitweb_check_feature('pickaxe')
6954                     or die_error(403, "Pickaxe is disabled");
6955         }
6956         if ($searchtype eq 'grep') {
6957                 gitweb_check_feature('grep')
6958                     or die_error(403, "Grep is disabled");
6959         }
6961         git_header_html();
6963         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6964                 my $greptype;
6965                 if ($searchtype eq 'commit') {
6966                         $greptype = "--grep=";
6967                 } elsif ($searchtype eq 'author') {
6968                         $greptype = "--author=";
6969                 } elsif ($searchtype eq 'committer') {
6970                         $greptype = "--committer=";
6971                 }
6972                 $greptype .= $searchtext;
6973                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6974                                                $greptype, '--regexp-ignore-case',
6975                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6977                 my $paging_nav = '';
6978                 if ($page > 0) {
6979                         $paging_nav .=
6980                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
6981                                                        searchtext=>$searchtext,
6982                                                        searchtype=>$searchtype)},
6983                                         "first");
6984                         $paging_nav .= " &sdot; " .
6985                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6986                                          -accesskey => "p", -title => "Alt-p"}, "prev");
6987                 } else {
6988                         $paging_nav .= "first";
6989                         $paging_nav .= " &sdot; prev";
6990                 }
6991                 my $next_link = '';
6992                 if ($#commitlist >= 100) {
6993                         $next_link =
6994                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6995                                          -accesskey => "n", -title => "Alt-n"}, "next");
6996                         $paging_nav .= " &sdot; $next_link";
6997                 } else {
6998                         $paging_nav .= " &sdot; next";
6999                 }
7001                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
7002                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7003                 if ($page == 0 && !@commitlist) {
7004                         print "<p>No match.</p>\n";
7005                 } else {
7006                         git_search_grep_body(\@commitlist, 0, 99, $next_link);
7007                 }
7008         }
7010         if ($searchtype eq 'pickaxe') {
7011                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7012                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7014                 print "<table class=\"pickaxe search\">\n";
7015                 my $alternate = 1;
7016                 local $/ = "\n";
7017                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
7018                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7019                         ($search_use_regexp ? '--pickaxe-regex' : ());
7020                 undef %co;
7021                 my @files;
7022                 while (my $line = <$fd>) {
7023                         chomp $line;
7024                         next unless $line;
7026                         my %set = parse_difftree_raw_line($line);
7027                         if (defined $set{'commit'}) {
7028                                 # finish previous commit
7029                                 if (%co) {
7030                                         print "</td>\n" .
7031                                               "<td class=\"link\">" .
7032                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7033                                               " | " .
7034                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7035                                         print "</td>\n" .
7036                                               "</tr>\n";
7037                                 }
7039                                 if ($alternate) {
7040                                         print "<tr class=\"dark\">\n";
7041                                 } else {
7042                                         print "<tr class=\"light\">\n";
7043                                 }
7044                                 $alternate ^= 1;
7045                                 %co = parse_commit($set{'commit'});
7046                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
7047                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7048                                       "<td><i>$author</i></td>\n" .
7049                                       "<td>" .
7050                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7051                                               -class => "list subject"},
7052                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
7053                         } elsif (defined $set{'to_id'}) {
7054                                 next if ($set{'to_id'} =~ m/^0{40}$/);
7056                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
7057                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
7058                                               -class => "list"},
7059                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
7060                                       "<br/>\n";
7061                         }
7062                 }
7063                 close $fd;
7065                 # finish last commit (warning: repetition!)
7066                 if (%co) {
7067                         print "</td>\n" .
7068                               "<td class=\"link\">" .
7069                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7070                               " | " .
7071                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7072                         print "</td>\n" .
7073                               "</tr>\n";
7074                 }
7076                 print "</table>\n";
7077         }
7079         if ($searchtype eq 'grep') {
7080                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7081                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7083                 print "<table class=\"grep_search\">\n";
7084                 my $alternate = 1;
7085                 my $matches = 0;
7086                 local $/ = "\n";
7087                 open my $fd, "-|", git_cmd(), 'grep', '-n',
7088                         $search_use_regexp ? ('-E', '-i') : '-F',
7089                         $searchtext, $co{'tree'};
7090                 my $lastfile = '';
7091                 while (my $line = <$fd>) {
7092                         chomp $line;
7093                         my ($file, $lno, $ltext, $binary);
7094                         last if ($matches++ > 1000);
7095                         if ($line =~ /^Binary file (.+) matches$/) {
7096                                 $file = $1;
7097                                 $binary = 1;
7098                         } else {
7099                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
7100                         }
7101                         if ($file ne $lastfile) {
7102                                 $lastfile and print "</td></tr>\n";
7103                                 if ($alternate++) {
7104                                         print "<tr class=\"dark\">\n";
7105                                 } else {
7106                                         print "<tr class=\"light\">\n";
7107                                 }
7108                                 print "<td class=\"list\">".
7109                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
7110                                                                file_name=>"$file"),
7111                                                 -class => "list"}, esc_path($file));
7112                                 print "</td><td>\n";
7113                                 $lastfile = $file;
7114                         }
7115                         if ($binary) {
7116                                 print "<div class=\"binary\">Binary file</div>\n";
7117                         } else {
7118                                 $ltext = untabify($ltext);
7119                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7120                                         $ltext = esc_html($1, -nbsp=>1);
7121                                         $ltext .= '<span class="match">';
7122                                         $ltext .= esc_html($2, -nbsp=>1);
7123                                         $ltext .= '</span>';
7124                                         $ltext .= esc_html($3, -nbsp=>1);
7125                                 } else {
7126                                         $ltext = esc_html($ltext, -nbsp=>1);
7127                                 }
7128                                 print "<div class=\"pre\">" .
7129                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
7130                                                                file_name=>"$file").'#l'.$lno,
7131                                                 -class => "linenr"}, sprintf('%4i', $lno))
7132                                         . ' ' .  $ltext . "</div>\n";
7133                         }
7134                 }
7135                 if ($lastfile) {
7136                         print "</td></tr>\n";
7137                         if ($matches > 1000) {
7138                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7139                         }
7140                 } else {
7141                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
7142                 }
7143                 close $fd;
7145                 print "</table>\n";
7146         }
7147         git_footer_html();
7150 sub git_search_help {
7151         git_header_html();
7152         git_print_page_nav('','', $hash,$hash,$hash);
7153         print <<EOT;
7154 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7155 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7156 the pattern entered is recognized as the POSIX extended
7157 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7158 insensitive).</p>
7159 <dl>
7160 <dt><b>commit</b></dt>
7161 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7162 EOT
7163         my $have_grep = gitweb_check_feature('grep');
7164         if ($have_grep) {
7165                 print <<EOT;
7166 <dt><b>grep</b></dt>
7167 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7168     a different one) are searched for the given pattern. On large trees, this search can take
7169 a while and put some strain on the server, so please use it with some consideration. Note that
7170 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7171 case-sensitive.</dd>
7172 EOT
7173         }
7174         print <<EOT;
7175 <dt><b>author</b></dt>
7176 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7177 <dt><b>committer</b></dt>
7178 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7179 EOT
7180         my $have_pickaxe = gitweb_check_feature('pickaxe');
7181         if ($have_pickaxe) {
7182                 print <<EOT;
7183 <dt><b>pickaxe</b></dt>
7184 <dd>All commits that caused the string to appear or disappear from any file (changes that
7185 added, removed or "modified" the string) will be listed. This search can take a while and
7186 takes a lot of strain on the server, so please use it wisely. Note that since you may be
7187 interested even in changes just changing the case as well, this search is case sensitive.</dd>
7188 EOT
7189         }
7190         print "</dl>\n";
7191         git_footer_html();
7194 sub git_shortlog {
7195         git_log_generic('shortlog', \&git_shortlog_body,
7196                         $hash, $hash_parent);
7199 ## ......................................................................
7200 ## feeds (RSS, Atom; OPML)
7202 sub git_feed {
7203         my $format = shift || 'atom';
7204         my $have_blame = gitweb_check_feature('blame');
7206         # Atom: http://www.atomenabled.org/developers/syndication/
7207         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7208         if ($format ne 'rss' && $format ne 'atom') {
7209                 die_error(400, "Unknown web feed format");
7210         }
7212         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7213         my $head = $hash || 'HEAD';
7214         my @commitlist = parse_commits($head, 150, 0, $file_name);
7216         my %latest_commit;
7217         my %latest_date;
7218         my $content_type = "application/$format+xml";
7219         if (defined $cgi->http('HTTP_ACCEPT') &&
7220                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7221                 # browser (feed reader) prefers text/xml
7222                 $content_type = 'text/xml';
7223         }
7224         if (defined($commitlist[0])) {
7225                 %latest_commit = %{$commitlist[0]};
7226                 my $latest_epoch = $latest_commit{'committer_epoch'};
7227                 %latest_date   = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
7228                 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7229                 if (defined $if_modified) {
7230                         my $since;
7231                         if (eval { require HTTP::Date; 1; }) {
7232                                 $since = HTTP::Date::str2time($if_modified);
7233                         } elsif (eval { require Time::ParseDate; 1; }) {
7234                                 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7235                         }
7236                         if (defined $since && $latest_epoch <= $since) {
7237                                 print $cgi->header(
7238                                         -type => $content_type,
7239                                         -charset => 'utf-8',
7240                                         -last_modified => $latest_date{'rfc2822'},
7241                                         -status => '304 Not Modified');
7242                                 return;
7243                         }
7244                 }
7245                 print $cgi->header(
7246                         -type => $content_type,
7247                         -charset => 'utf-8',
7248                         -last_modified => $latest_date{'rfc2822'});
7249         } else {
7250                 print $cgi->header(
7251                         -type => $content_type,
7252                         -charset => 'utf-8');
7253         }
7255         # Optimization: skip generating the body if client asks only
7256         # for Last-Modified date.
7257         return if ($cgi->request_method() eq 'HEAD');
7259         # header variables
7260         my $title = "$site_name - $project/$action";
7261         my $feed_type = 'log';
7262         if (defined $hash) {
7263                 $title .= " - '$hash'";
7264                 $feed_type = 'branch log';
7265                 if (defined $file_name) {
7266                         $title .= " :: $file_name";
7267                         $feed_type = 'history';
7268                 }
7269         } elsif (defined $file_name) {
7270                 $title .= " - $file_name";
7271                 $feed_type = 'history';
7272         }
7273         $title .= " $feed_type";
7274         my $descr = git_get_project_description($project);
7275         if (defined $descr) {
7276                 $descr = esc_html($descr);
7277         } else {
7278                 $descr = "$project " .
7279                          ($format eq 'rss' ? 'RSS' : 'Atom') .
7280                          " feed";
7281         }
7282         my $owner = git_get_project_owner($project);
7283         $owner = esc_html($owner);
7285         #header
7286         my $alt_url;
7287         if (defined $file_name) {
7288                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7289         } elsif (defined $hash) {
7290                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7291         } else {
7292                 $alt_url = href(-full=>1, action=>"summary");
7293         }
7294         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7295         if ($format eq 'rss') {
7296                 print <<XML;
7297 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7298 <channel>
7299 XML
7300                 print "<title>$title</title>\n" .
7301                       "<link>$alt_url</link>\n" .
7302                       "<description>$descr</description>\n" .
7303                       "<language>en</language>\n" .
7304                       # project owner is responsible for 'editorial' content
7305                       "<managingEditor>$owner</managingEditor>\n";
7306                 if (defined $logo || defined $favicon) {
7307                         # prefer the logo to the favicon, since RSS
7308                         # doesn't allow both
7309                         my $img = esc_url($logo || $favicon);
7310                         print "<image>\n" .
7311                               "<url>$img</url>\n" .
7312                               "<title>$title</title>\n" .
7313                               "<link>$alt_url</link>\n" .
7314                               "</image>\n";
7315                 }
7316                 if (%latest_date) {
7317                         print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7318                         print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7319                 }
7320                 print "<generator>gitweb v.$version/$git_version</generator>\n";
7321         } elsif ($format eq 'atom') {
7322                 print <<XML;
7323 <feed xmlns="http://www.w3.org/2005/Atom">
7324 XML
7325                 print "<title>$title</title>\n" .
7326                       "<subtitle>$descr</subtitle>\n" .
7327                       '<link rel="alternate" type="text/html" href="' .
7328                       $alt_url . '" />' . "\n" .
7329                       '<link rel="self" type="' . $content_type . '" href="' .
7330                       $cgi->self_url() . '" />' . "\n" .
7331                       "<id>" . href(-full=>1) . "</id>\n" .
7332                       # use project owner for feed author
7333                       "<author><name>$owner</name></author>\n";
7334                 if (defined $favicon) {
7335                         print "<icon>" . esc_url($favicon) . "</icon>\n";
7336                 }
7337                 if (defined $logo) {
7338                         # not twice as wide as tall: 72 x 27 pixels
7339                         print "<logo>" . esc_url($logo) . "</logo>\n";
7340                 }
7341                 if (! %latest_date) {
7342                         # dummy date to keep the feed valid until commits trickle in:
7343                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
7344                 } else {
7345                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
7346                 }
7347                 print "<generator version='$version/$git_version'>gitweb</generator>\n";
7348         }
7350         # contents
7351         for (my $i = 0; $i <= $#commitlist; $i++) {
7352                 my %co = %{$commitlist[$i]};
7353                 my $commit = $co{'id'};
7354                 # we read 150, we always show 30 and the ones more recent than 48 hours
7355                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
7356                         last;
7357                 }
7358                 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
7360                 # get list of changed files
7361                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7362                         $co{'parent'} || "--root",
7363                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
7364                         or next;
7365                 my @difftree = map { chomp; $_ } <$fd>;
7366                 close $fd
7367                         or next;
7369                 # print element (entry, item)
7370                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7371                 if ($format eq 'rss') {
7372                         print "<item>\n" .
7373                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
7374                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
7375                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7376                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7377                               "<link>$co_url</link>\n" .
7378                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
7379                               "<content:encoded>" .
7380                               "<![CDATA[\n";
7381                 } elsif ($format eq 'atom') {
7382                         print "<entry>\n" .
7383                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7384                               "<updated>$cd{'iso-8601'}</updated>\n" .
7385                               "<author>\n" .
7386                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
7387                         if ($co{'author_email'}) {
7388                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
7389                         }
7390                         print "</author>\n" .
7391                               # use committer for contributor
7392                               "<contributor>\n" .
7393                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7394                         if ($co{'committer_email'}) {
7395                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7396                         }
7397                         print "</contributor>\n" .
7398                               "<published>$cd{'iso-8601'}</published>\n" .
7399                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
7400                               "<id>$co_url</id>\n" .
7401                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
7402                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
7403                 }
7404                 my $comment = $co{'comment'};
7405                 print "<pre>\n";
7406                 foreach my $line (@$comment) {
7407                         $line = esc_html($line);
7408                         print "$line\n";
7409                 }
7410                 print "</pre><ul>\n";
7411                 foreach my $difftree_line (@difftree) {
7412                         my %difftree = parse_difftree_raw_line($difftree_line);
7413                         next if !$difftree{'from_id'};
7415                         my $file = $difftree{'file'} || $difftree{'to_file'};
7417                         print "<li>" .
7418                               "[" .
7419                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
7420                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
7421                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
7422                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
7423                                       -title => "diff"}, 'D');
7424                         if ($have_blame) {
7425                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
7426                                                              file_name=>$file, hash_base=>$commit),
7427                                               -title => "blame"}, 'B');
7428                         }
7429                         # if this is not a feed of a file history
7430                         if (!defined $file_name || $file_name ne $file) {
7431                                 print $cgi->a({-href => href(-full=>1, action=>"history",
7432                                                              file_name=>$file, hash=>$commit),
7433                                               -title => "history"}, 'H');
7434                         }
7435                         $file = esc_path($file);
7436                         print "] ".
7437                               "$file</li>\n";
7438                 }
7439                 if ($format eq 'rss') {
7440                         print "</ul>]]>\n" .
7441                               "</content:encoded>\n" .
7442                               "</item>\n";
7443                 } elsif ($format eq 'atom') {
7444                         print "</ul>\n</div>\n" .
7445                               "</content>\n" .
7446                               "</entry>\n";
7447                 }
7448         }
7450         # end of feed
7451         if ($format eq 'rss') {
7452                 print "</channel>\n</rss>\n";
7453         } elsif ($format eq 'atom') {
7454                 print "</feed>\n";
7455         }
7458 sub git_rss {
7459         git_feed('rss');
7462 sub git_atom {
7463         git_feed('atom');
7466 sub git_opml {
7467         my @list = git_get_projects_list();
7468         if (!@list) {
7469                 die_error(404, "No projects found");
7470         }
7472         print $cgi->header(
7473                 -type => 'text/xml',
7474                 -charset => 'utf-8',
7475                 -content_disposition => 'inline; filename="opml.xml"');
7477         print <<XML;
7478 <?xml version="1.0" encoding="utf-8"?>
7479 <opml version="1.0">
7480 <head>
7481   <title>$site_name OPML Export</title>
7482 </head>
7483 <body>
7484 <outline text="git RSS feeds">
7485 XML
7487         foreach my $pr (@list) {
7488                 my %proj = %$pr;
7489                 my $head = git_get_head_hash($proj{'path'});
7490                 if (!defined $head) {
7491                         next;
7492                 }
7493                 $git_dir = "$projectroot/$proj{'path'}";
7494                 my %co = parse_commit($head);
7495                 if (!%co) {
7496                         next;
7497                 }
7499                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
7500                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7501                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
7502                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7503         }
7504         print <<XML;
7505 </outline>
7506 </body>
7507 </opml>
7508 XML