Code

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