Code

gitweb: Fix two 'uninitialized value' warnings in git_tree()
[git.git] / gitweb / gitweb.perl
1 #!/usr/bin/perl
3 # gitweb - simple web interface to track changes in git repositories
4 #
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
7 #
8 # This program is licensed under the GPLv2
10 use strict;
11 use warnings;
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser);
15 use Encode;
16 use Fcntl ':mode';
17 use File::Find qw();
18 use File::Basename qw(basename);
19 binmode STDOUT, ':utf8';
21 BEGIN {
22         CGI->compile() if $ENV{'MOD_PERL'};
23 }
25 our $cgi = new CGI;
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute => 1);
30 # if we're called with PATH_INFO, we have to strip that
31 # from the URL to find our real URL
32 if (my $path_info = $ENV{"PATH_INFO"}) {
33         $my_url =~ s,\Q$path_info\E$,,;
34         $my_uri =~ s,\Q$path_info\E$,,;
35 }
37 # core git executable to use
38 # this can just be "git" if your webserver has a sensible PATH
39 our $GIT = "++GIT_BINDIR++/git";
41 # absolute fs-path which will be prepended to the project path
42 #our $projectroot = "/pub/scm";
43 our $projectroot = "++GITWEB_PROJECTROOT++";
45 # fs traversing limit for getting project list
46 # the number is relative to the projectroot
47 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
49 # target of the home link on top of all pages
50 our $home_link = $my_uri || "/";
52 # string of the home link on top of all pages
53 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
55 # name of your site or organization to appear in page titles
56 # replace this with something more descriptive for clearer bookmarks
57 our $site_name = "++GITWEB_SITENAME++"
58                  || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
60 # filename of html text to include at top of each page
61 our $site_header = "++GITWEB_SITE_HEADER++";
62 # html text to include at home page
63 our $home_text = "++GITWEB_HOMETEXT++";
64 # filename of html text to include at bottom of each page
65 our $site_footer = "++GITWEB_SITE_FOOTER++";
67 # URI of stylesheets
68 our @stylesheets = ("++GITWEB_CSS++");
69 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
70 our $stylesheet = undef;
71 # URI of GIT logo (72x27 size)
72 our $logo = "++GITWEB_LOGO++";
73 # URI of GIT favicon, assumed to be image/png type
74 our $favicon = "++GITWEB_FAVICON++";
76 # URI and label (title) of GIT logo link
77 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
78 #our $logo_label = "git documentation";
79 our $logo_url = "http://git.or.cz/";
80 our $logo_label = "git homepage";
82 # source of projects list
83 our $projects_list = "++GITWEB_LIST++";
85 # the width (in characters) of the projects list "Description" column
86 our $projects_list_description_width = 25;
88 # default order of projects list
89 # valid values are none, project, descr, owner, and age
90 our $default_projects_order = "project";
92 # show repository only if this file exists
93 # (only effective if this variable evaluates to true)
94 our $export_ok = "++GITWEB_EXPORT_OK++";
96 # only allow viewing of repositories also shown on the overview page
97 our $strict_export = "++GITWEB_STRICT_EXPORT++";
99 # list of git base URLs used for URL to where fetch project from,
100 # i.e. full URL is "$git_base_url/$project"
101 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
103 # default blob_plain mimetype and default charset for text/plain blob
104 our $default_blob_plain_mimetype = 'text/plain';
105 our $default_text_plain_charset  = undef;
107 # file to use for guessing MIME types before trying /etc/mime.types
108 # (relative to the current git repository)
109 our $mimetypes_file = undef;
111 # assume this charset if line contains non-UTF-8 characters;
112 # it should be valid encoding (see Encoding::Supported(3pm) for list),
113 # for which encoding all byte sequences are valid, for example
114 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
115 # could be even 'utf-8' for the old behavior)
116 our $fallback_encoding = 'latin1';
118 # rename detection options for git-diff and git-diff-tree
119 # - default is '-M', with the cost proportional to
120 #   (number of removed files) * (number of new files).
121 # - more costly is '-C' (which implies '-M'), with the cost proportional to
122 #   (number of changed files + number of removed files) * (number of new files)
123 # - even more costly is '-C', '--find-copies-harder' with cost
124 #   (number of files in the original tree) * (number of new files)
125 # - one might want to include '-B' option, e.g. '-B', '-M'
126 our @diff_opts = ('-M'); # taken from git_commit
128 # information about snapshot formats that gitweb is capable of serving
129 our %known_snapshot_formats = (
130         # name => {
131         #       'display' => display name,
132         #       'type' => mime type,
133         #       'suffix' => filename suffix,
134         #       'format' => --format for git-archive,
135         #       'compressor' => [compressor command and arguments]
136         #                       (array reference, optional)}
137         #
138         'tgz' => {
139                 'display' => 'tar.gz',
140                 'type' => 'application/x-gzip',
141                 'suffix' => '.tar.gz',
142                 'format' => 'tar',
143                 'compressor' => ['gzip']},
145         'tbz2' => {
146                 'display' => 'tar.bz2',
147                 'type' => 'application/x-bzip2',
148                 'suffix' => '.tar.bz2',
149                 'format' => 'tar',
150                 'compressor' => ['bzip2']},
152         'zip' => {
153                 'display' => 'zip',
154                 'type' => 'application/x-zip',
155                 'suffix' => '.zip',
156                 'format' => 'zip'},
157 );
159 # Aliases so we understand old gitweb.snapshot values in repository
160 # configuration.
161 our %known_snapshot_format_aliases = (
162         'gzip'  => 'tgz',
163         'bzip2' => 'tbz2',
165         # backward compatibility: legacy gitweb config support
166         'x-gzip' => undef, 'gz' => undef,
167         'x-bzip2' => undef, 'bz2' => undef,
168         'x-zip' => undef, '' => undef,
169 );
171 # You define site-wide feature defaults here; override them with
172 # $GITWEB_CONFIG as necessary.
173 our %feature = (
174         # feature => {
175         #       'sub' => feature-sub (subroutine),
176         #       'override' => allow-override (boolean),
177         #       'default' => [ default options...] (array reference)}
178         #
179         # if feature is overridable (it means that allow-override has true value),
180         # then feature-sub will be called with default options as parameters;
181         # return value of feature-sub indicates if to enable specified feature
182         #
183         # if there is no 'sub' key (no feature-sub), then feature cannot be
184         # overriden
185         #
186         # use gitweb_check_feature(<feature>) to check if <feature> is enabled
188         # Enable the 'blame' blob view, showing the last commit that modified
189         # each line in the file. This can be very CPU-intensive.
191         # To enable system wide have in $GITWEB_CONFIG
192         # $feature{'blame'}{'default'} = [1];
193         # To have project specific config enable override in $GITWEB_CONFIG
194         # $feature{'blame'}{'override'} = 1;
195         # and in project config gitweb.blame = 0|1;
196         'blame' => {
197                 'sub' => \&feature_blame,
198                 'override' => 0,
199                 'default' => [0]},
201         # Enable the 'snapshot' link, providing a compressed archive of any
202         # tree. This can potentially generate high traffic if you have large
203         # project.
205         # Value is a list of formats defined in %known_snapshot_formats that
206         # you wish to offer.
207         # To disable system wide have in $GITWEB_CONFIG
208         # $feature{'snapshot'}{'default'} = [];
209         # To have project specific config enable override in $GITWEB_CONFIG
210         # $feature{'snapshot'}{'override'} = 1;
211         # and in project config, a comma-separated list of formats or "none"
212         # to disable.  Example: gitweb.snapshot = tbz2,zip;
213         'snapshot' => {
214                 'sub' => \&feature_snapshot,
215                 'override' => 0,
216                 'default' => ['tgz']},
218         # Enable text search, which will list the commits which match author,
219         # committer or commit text to a given string.  Enabled by default.
220         # Project specific override is not supported.
221         'search' => {
222                 'override' => 0,
223                 'default' => [1]},
225         # Enable grep search, which will list the files in currently selected
226         # tree containing the given string. Enabled by default. This can be
227         # potentially CPU-intensive, of course.
229         # To enable system wide have in $GITWEB_CONFIG
230         # $feature{'grep'}{'default'} = [1];
231         # To have project specific config enable override in $GITWEB_CONFIG
232         # $feature{'grep'}{'override'} = 1;
233         # and in project config gitweb.grep = 0|1;
234         'grep' => {
235                 'override' => 0,
236                 'default' => [1]},
238         # Enable the pickaxe search, which will list the commits that modified
239         # a given string in a file. This can be practical and quite faster
240         # alternative to 'blame', but still potentially CPU-intensive.
242         # To enable system wide have in $GITWEB_CONFIG
243         # $feature{'pickaxe'}{'default'} = [1];
244         # To have project specific config enable override in $GITWEB_CONFIG
245         # $feature{'pickaxe'}{'override'} = 1;
246         # and in project config gitweb.pickaxe = 0|1;
247         'pickaxe' => {
248                 'sub' => \&feature_pickaxe,
249                 'override' => 0,
250                 'default' => [1]},
252         # Make gitweb use an alternative format of the URLs which can be
253         # more readable and natural-looking: project name is embedded
254         # directly in the path and the query string contains other
255         # auxiliary information. All gitweb installations recognize
256         # URL in either format; this configures in which formats gitweb
257         # generates links.
259         # To enable system wide have in $GITWEB_CONFIG
260         # $feature{'pathinfo'}{'default'} = [1];
261         # Project specific override is not supported.
263         # Note that you will need to change the default location of CSS,
264         # favicon, logo and possibly other files to an absolute URL. Also,
265         # if gitweb.cgi serves as your indexfile, you will need to force
266         # $my_uri to contain the script name in your $GITWEB_CONFIG.
267         'pathinfo' => {
268                 'override' => 0,
269                 'default' => [0]},
271         # Make gitweb consider projects in project root subdirectories
272         # to be forks of existing projects. Given project $projname.git,
273         # projects matching $projname/*.git will not be shown in the main
274         # projects list, instead a '+' mark will be added to $projname
275         # there and a 'forks' view will be enabled for the project, listing
276         # all the forks. If project list is taken from a file, forks have
277         # to be listed after the main project.
279         # To enable system wide have in $GITWEB_CONFIG
280         # $feature{'forks'}{'default'} = [1];
281         # Project specific override is not supported.
282         'forks' => {
283                 'override' => 0,
284                 'default' => [0]},
285 );
287 sub gitweb_check_feature {
288         my ($name) = @_;
289         return unless exists $feature{$name};
290         my ($sub, $override, @defaults) = (
291                 $feature{$name}{'sub'},
292                 $feature{$name}{'override'},
293                 @{$feature{$name}{'default'}});
294         if (!$override) { return @defaults; }
295         if (!defined $sub) {
296                 warn "feature $name is not overrideable";
297                 return @defaults;
298         }
299         return $sub->(@defaults);
302 sub feature_blame {
303         my ($val) = git_get_project_config('blame', '--bool');
305         if ($val eq 'true') {
306                 return 1;
307         } elsif ($val eq 'false') {
308                 return 0;
309         }
311         return $_[0];
314 sub feature_snapshot {
315         my (@fmts) = @_;
317         my ($val) = git_get_project_config('snapshot');
319         if ($val) {
320                 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
321         }
323         return @fmts;
326 sub feature_grep {
327         my ($val) = git_get_project_config('grep', '--bool');
329         if ($val eq 'true') {
330                 return (1);
331         } elsif ($val eq 'false') {
332                 return (0);
333         }
335         return ($_[0]);
338 sub feature_pickaxe {
339         my ($val) = git_get_project_config('pickaxe', '--bool');
341         if ($val eq 'true') {
342                 return (1);
343         } elsif ($val eq 'false') {
344                 return (0);
345         }
347         return ($_[0]);
350 # checking HEAD file with -e is fragile if the repository was
351 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
352 # and then pruned.
353 sub check_head_link {
354         my ($dir) = @_;
355         my $headfile = "$dir/HEAD";
356         return ((-e $headfile) ||
357                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
360 sub check_export_ok {
361         my ($dir) = @_;
362         return (check_head_link($dir) &&
363                 (!$export_ok || -e "$dir/$export_ok"));
366 # process alternate names for backward compatibility
367 # filter out unsupported (unknown) snapshot formats
368 sub filter_snapshot_fmts {
369         my @fmts = @_;
371         @fmts = map {
372                 exists $known_snapshot_format_aliases{$_} ?
373                        $known_snapshot_format_aliases{$_} : $_} @fmts;
374         @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
378 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
379 if (-e $GITWEB_CONFIG) {
380         do $GITWEB_CONFIG;
381 } else {
382         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
383         do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
386 # version of the core git binary
387 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
389 $projects_list ||= $projectroot;
391 # ======================================================================
392 # input validation and dispatch
393 our $action = $cgi->param('a');
394 if (defined $action) {
395         if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
396                 die_error(400, "Invalid action parameter");
397         }
400 # parameters which are pathnames
401 our $project = $cgi->param('p');
402 if (defined $project) {
403         if (!validate_pathname($project) ||
404             !(-d "$projectroot/$project") ||
405             !check_head_link("$projectroot/$project") ||
406             ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
407             ($strict_export && !project_in_list($project))) {
408                 undef $project;
409                 die_error(404, "No such project");
410         }
413 our $file_name = $cgi->param('f');
414 if (defined $file_name) {
415         if (!validate_pathname($file_name)) {
416                 die_error(400, "Invalid file parameter");
417         }
420 our $file_parent = $cgi->param('fp');
421 if (defined $file_parent) {
422         if (!validate_pathname($file_parent)) {
423                 die_error(400, "Invalid file parent parameter");
424         }
427 # parameters which are refnames
428 our $hash = $cgi->param('h');
429 if (defined $hash) {
430         if (!validate_refname($hash)) {
431                 die_error(400, "Invalid hash parameter");
432         }
435 our $hash_parent = $cgi->param('hp');
436 if (defined $hash_parent) {
437         if (!validate_refname($hash_parent)) {
438                 die_error(400, "Invalid hash parent parameter");
439         }
442 our $hash_base = $cgi->param('hb');
443 if (defined $hash_base) {
444         if (!validate_refname($hash_base)) {
445                 die_error(400, "Invalid hash base parameter");
446         }
449 my %allowed_options = (
450         "--no-merges" => [ qw(rss atom log shortlog history) ],
451 );
453 our @extra_options = $cgi->param('opt');
454 if (defined @extra_options) {
455         foreach my $opt (@extra_options) {
456                 if (not exists $allowed_options{$opt}) {
457                         die_error(400, "Invalid option parameter");
458                 }
459                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
460                         die_error(400, "Invalid option parameter for this action");
461                 }
462         }
465 our $hash_parent_base = $cgi->param('hpb');
466 if (defined $hash_parent_base) {
467         if (!validate_refname($hash_parent_base)) {
468                 die_error(400, "Invalid hash parent base parameter");
469         }
472 # other parameters
473 our $page = $cgi->param('pg');
474 if (defined $page) {
475         if ($page =~ m/[^0-9]/) {
476                 die_error(400, "Invalid page parameter");
477         }
480 our $searchtype = $cgi->param('st');
481 if (defined $searchtype) {
482         if ($searchtype =~ m/[^a-z]/) {
483                 die_error(400, "Invalid searchtype parameter");
484         }
487 our $search_use_regexp = $cgi->param('sr');
489 our $searchtext = $cgi->param('s');
490 our $search_regexp;
491 if (defined $searchtext) {
492         if (length($searchtext) < 2) {
493                 die_error(403, "At least two characters are required for search parameter");
494         }
495         $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
498 # now read PATH_INFO and use it as alternative to parameters
499 sub evaluate_path_info {
500         return if defined $project;
501         my $path_info = $ENV{"PATH_INFO"};
502         return if !$path_info;
503         $path_info =~ s,^/+,,;
504         return if !$path_info;
505         # find which part of PATH_INFO is project
506         $project = $path_info;
507         $project =~ s,/+$,,;
508         while ($project && !check_head_link("$projectroot/$project")) {
509                 $project =~ s,/*[^/]*$,,;
510         }
511         # validate project
512         $project = validate_pathname($project);
513         if (!$project ||
514             ($export_ok && !-e "$projectroot/$project/$export_ok") ||
515             ($strict_export && !project_in_list($project))) {
516                 undef $project;
517                 return;
518         }
519         # do not change any parameters if an action is given using the query string
520         return if $action;
521         $path_info =~ s,^\Q$project\E/*,,;
522         my ($refname, $pathname) = split(/:/, $path_info, 2);
523         if (defined $pathname) {
524                 # we got "project.git/branch:filename" or "project.git/branch:dir/"
525                 # we could use git_get_type(branch:pathname), but it needs $git_dir
526                 $pathname =~ s,^/+,,;
527                 if (!$pathname || substr($pathname, -1) eq "/") {
528                         $action  ||= "tree";
529                         $pathname =~ s,/$,,;
530                 } else {
531                         $action  ||= "blob_plain";
532                 }
533                 $hash_base ||= validate_refname($refname);
534                 $file_name ||= validate_pathname($pathname);
535         } elsif (defined $refname) {
536                 # we got "project.git/branch"
537                 $action ||= "shortlog";
538                 $hash   ||= validate_refname($refname);
539         }
541 evaluate_path_info();
543 # path to the current git repository
544 our $git_dir;
545 $git_dir = "$projectroot/$project" if $project;
547 # dispatch
548 my %actions = (
549         "blame" => \&git_blame,
550         "blobdiff" => \&git_blobdiff,
551         "blobdiff_plain" => \&git_blobdiff_plain,
552         "blob" => \&git_blob,
553         "blob_plain" => \&git_blob_plain,
554         "commitdiff" => \&git_commitdiff,
555         "commitdiff_plain" => \&git_commitdiff_plain,
556         "commit" => \&git_commit,
557         "forks" => \&git_forks,
558         "heads" => \&git_heads,
559         "history" => \&git_history,
560         "log" => \&git_log,
561         "rss" => \&git_rss,
562         "atom" => \&git_atom,
563         "search" => \&git_search,
564         "search_help" => \&git_search_help,
565         "shortlog" => \&git_shortlog,
566         "summary" => \&git_summary,
567         "tag" => \&git_tag,
568         "tags" => \&git_tags,
569         "tree" => \&git_tree,
570         "snapshot" => \&git_snapshot,
571         "object" => \&git_object,
572         # those below don't need $project
573         "opml" => \&git_opml,
574         "project_list" => \&git_project_list,
575         "project_index" => \&git_project_index,
576 );
578 if (!defined $action) {
579         if (defined $hash) {
580                 $action = git_get_type($hash);
581         } elsif (defined $hash_base && defined $file_name) {
582                 $action = git_get_type("$hash_base:$file_name");
583         } elsif (defined $project) {
584                 $action = 'summary';
585         } else {
586                 $action = 'project_list';
587         }
589 if (!defined($actions{$action})) {
590         die_error(400, "Unknown action");
592 if ($action !~ m/^(opml|project_list|project_index)$/ &&
593     !$project) {
594         die_error(400, "Project needed");
596 $actions{$action}->();
597 exit;
599 ## ======================================================================
600 ## action links
602 sub href (%) {
603         my %params = @_;
604         # default is to use -absolute url() i.e. $my_uri
605         my $href = $params{-full} ? $my_url : $my_uri;
607         # XXX: Warning: If you touch this, check the search form for updating,
608         # too.
610         my @mapping = (
611                 project => "p",
612                 action => "a",
613                 file_name => "f",
614                 file_parent => "fp",
615                 hash => "h",
616                 hash_parent => "hp",
617                 hash_base => "hb",
618                 hash_parent_base => "hpb",
619                 page => "pg",
620                 order => "o",
621                 searchtext => "s",
622                 searchtype => "st",
623                 snapshot_format => "sf",
624                 extra_options => "opt",
625                 search_use_regexp => "sr",
626         );
627         my %mapping = @mapping;
629         $params{'project'} = $project unless exists $params{'project'};
631         if ($params{-replay}) {
632                 while (my ($name, $symbol) = each %mapping) {
633                         if (!exists $params{$name}) {
634                                 # to allow for multivalued params we use arrayref form
635                                 $params{$name} = [ $cgi->param($symbol) ];
636                         }
637                 }
638         }
640         my ($use_pathinfo) = gitweb_check_feature('pathinfo');
641         if ($use_pathinfo) {
642                 # use PATH_INFO for project name
643                 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
644                 delete $params{'project'};
646                 # Summary just uses the project path URL
647                 if (defined $params{'action'} && $params{'action'} eq 'summary') {
648                         delete $params{'action'};
649                 }
650         }
652         # now encode the parameters explicitly
653         my @result = ();
654         for (my $i = 0; $i < @mapping; $i += 2) {
655                 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
656                 if (defined $params{$name}) {
657                         if (ref($params{$name}) eq "ARRAY") {
658                                 foreach my $par (@{$params{$name}}) {
659                                         push @result, $symbol . "=" . esc_param($par);
660                                 }
661                         } else {
662                                 push @result, $symbol . "=" . esc_param($params{$name});
663                         }
664                 }
665         }
666         $href .= "?" . join(';', @result) if scalar @result;
668         return $href;
672 ## ======================================================================
673 ## validation, quoting/unquoting and escaping
675 sub validate_pathname {
676         my $input = shift || return undef;
678         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
679         # at the beginning, at the end, and between slashes.
680         # also this catches doubled slashes
681         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
682                 return undef;
683         }
684         # no null characters
685         if ($input =~ m!\0!) {
686                 return undef;
687         }
688         return $input;
691 sub validate_refname {
692         my $input = shift || return undef;
694         # textual hashes are O.K.
695         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
696                 return $input;
697         }
698         # it must be correct pathname
699         $input = validate_pathname($input)
700                 or return undef;
701         # restrictions on ref name according to git-check-ref-format
702         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
703                 return undef;
704         }
705         return $input;
708 # decode sequences of octets in utf8 into Perl's internal form,
709 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
710 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
711 sub to_utf8 {
712         my $str = shift;
713         if (utf8::valid($str)) {
714                 utf8::decode($str);
715                 return $str;
716         } else {
717                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
718         }
721 # quote unsafe chars, but keep the slash, even when it's not
722 # correct, but quoted slashes look too horrible in bookmarks
723 sub esc_param {
724         my $str = shift;
725         $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
726         $str =~ s/\+/%2B/g;
727         $str =~ s/ /\+/g;
728         return $str;
731 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
732 sub esc_url {
733         my $str = shift;
734         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
735         $str =~ s/\+/%2B/g;
736         $str =~ s/ /\+/g;
737         return $str;
740 # replace invalid utf8 character with SUBSTITUTION sequence
741 sub esc_html ($;%) {
742         my $str = shift;
743         my %opts = @_;
745         $str = to_utf8($str);
746         $str = $cgi->escapeHTML($str);
747         if ($opts{'-nbsp'}) {
748                 $str =~ s/ /&nbsp;/g;
749         }
750         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
751         return $str;
754 # quote control characters and escape filename to HTML
755 sub esc_path {
756         my $str = shift;
757         my %opts = @_;
759         $str = to_utf8($str);
760         $str = $cgi->escapeHTML($str);
761         if ($opts{'-nbsp'}) {
762                 $str =~ s/ /&nbsp;/g;
763         }
764         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
765         return $str;
768 # Make control characters "printable", using character escape codes (CEC)
769 sub quot_cec {
770         my $cntrl = shift;
771         my %opts = @_;
772         my %es = ( # character escape codes, aka escape sequences
773                 "\t" => '\t',   # tab            (HT)
774                 "\n" => '\n',   # line feed      (LF)
775                 "\r" => '\r',   # carrige return (CR)
776                 "\f" => '\f',   # form feed      (FF)
777                 "\b" => '\b',   # backspace      (BS)
778                 "\a" => '\a',   # alarm (bell)   (BEL)
779                 "\e" => '\e',   # escape         (ESC)
780                 "\013" => '\v', # vertical tab   (VT)
781                 "\000" => '\0', # nul character  (NUL)
782         );
783         my $chr = ( (exists $es{$cntrl})
784                     ? $es{$cntrl}
785                     : sprintf('\%03o', ord($cntrl)) );
786         if ($opts{-nohtml}) {
787                 return $chr;
788         } else {
789                 return "<span class=\"cntrl\">$chr</span>";
790         }
793 # Alternatively use unicode control pictures codepoints,
794 # Unicode "printable representation" (PR)
795 sub quot_upr {
796         my $cntrl = shift;
797         my %opts = @_;
799         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
800         if ($opts{-nohtml}) {
801                 return $chr;
802         } else {
803                 return "<span class=\"cntrl\">$chr</span>";
804         }
807 # git may return quoted and escaped filenames
808 sub unquote {
809         my $str = shift;
811         sub unq {
812                 my $seq = shift;
813                 my %es = ( # character escape codes, aka escape sequences
814                         't' => "\t",   # tab            (HT, TAB)
815                         'n' => "\n",   # newline        (NL)
816                         'r' => "\r",   # return         (CR)
817                         'f' => "\f",   # form feed      (FF)
818                         'b' => "\b",   # backspace      (BS)
819                         'a' => "\a",   # alarm (bell)   (BEL)
820                         'e' => "\e",   # escape         (ESC)
821                         'v' => "\013", # vertical tab   (VT)
822                 );
824                 if ($seq =~ m/^[0-7]{1,3}$/) {
825                         # octal char sequence
826                         return chr(oct($seq));
827                 } elsif (exists $es{$seq}) {
828                         # C escape sequence, aka character escape code
829                         return $es{$seq};
830                 }
831                 # quoted ordinary character
832                 return $seq;
833         }
835         if ($str =~ m/^"(.*)"$/) {
836                 # needs unquoting
837                 $str = $1;
838                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
839         }
840         return $str;
843 # escape tabs (convert tabs to spaces)
844 sub untabify {
845         my $line = shift;
847         while ((my $pos = index($line, "\t")) != -1) {
848                 if (my $count = (8 - ($pos % 8))) {
849                         my $spaces = ' ' x $count;
850                         $line =~ s/\t/$spaces/;
851                 }
852         }
854         return $line;
857 sub project_in_list {
858         my $project = shift;
859         my @list = git_get_projects_list();
860         return @list && scalar(grep { $_->{'path'} eq $project } @list);
863 ## ----------------------------------------------------------------------
864 ## HTML aware string manipulation
866 # Try to chop given string on a word boundary between position
867 # $len and $len+$add_len. If there is no word boundary there,
868 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
869 # (marking chopped part) would be longer than given string.
870 sub chop_str {
871         my $str = shift;
872         my $len = shift;
873         my $add_len = shift || 10;
874         my $where = shift || 'right'; # 'left' | 'center' | 'right'
876         # Make sure perl knows it is utf8 encoded so we don't
877         # cut in the middle of a utf8 multibyte char.
878         $str = to_utf8($str);
880         # allow only $len chars, but don't cut a word if it would fit in $add_len
881         # if it doesn't fit, cut it if it's still longer than the dots we would add
882         # remove chopped character entities entirely
884         # when chopping in the middle, distribute $len into left and right part
885         # return early if chopping wouldn't make string shorter
886         if ($where eq 'center') {
887                 return $str if ($len + 5 >= length($str)); # filler is length 5
888                 $len = int($len/2);
889         } else {
890                 return $str if ($len + 4 >= length($str)); # filler is length 4
891         }
893         # regexps: ending and beginning with word part up to $add_len
894         my $endre = qr/.{$len}\w{0,$add_len}/;
895         my $begre = qr/\w{0,$add_len}.{$len}/;
897         if ($where eq 'left') {
898                 $str =~ m/^(.*?)($begre)$/;
899                 my ($lead, $body) = ($1, $2);
900                 if (length($lead) > 4) {
901                         $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
902                         $lead = " ...";
903                 }
904                 return "$lead$body";
906         } elsif ($where eq 'center') {
907                 $str =~ m/^($endre)(.*)$/;
908                 my ($left, $str)  = ($1, $2);
909                 $str =~ m/^(.*?)($begre)$/;
910                 my ($mid, $right) = ($1, $2);
911                 if (length($mid) > 5) {
912                         $left  =~ s/&[^;]*$//;
913                         $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
914                         $mid = " ... ";
915                 }
916                 return "$left$mid$right";
918         } else {
919                 $str =~ m/^($endre)(.*)$/;
920                 my $body = $1;
921                 my $tail = $2;
922                 if (length($tail) > 4) {
923                         $body =~ s/&[^;]*$//;
924                         $tail = "... ";
925                 }
926                 return "$body$tail";
927         }
930 # takes the same arguments as chop_str, but also wraps a <span> around the
931 # result with a title attribute if it does get chopped. Additionally, the
932 # string is HTML-escaped.
933 sub chop_and_escape_str {
934         my ($str) = @_;
936         my $chopped = chop_str(@_);
937         if ($chopped eq $str) {
938                 return esc_html($chopped);
939         } else {
940                 $str =~ s/([[:cntrl:]])/?/g;
941                 return $cgi->span({-title=>$str}, esc_html($chopped));
942         }
945 ## ----------------------------------------------------------------------
946 ## functions returning short strings
948 # CSS class for given age value (in seconds)
949 sub age_class {
950         my $age = shift;
952         if (!defined $age) {
953                 return "noage";
954         } elsif ($age < 60*60*2) {
955                 return "age0";
956         } elsif ($age < 60*60*24*2) {
957                 return "age1";
958         } else {
959                 return "age2";
960         }
963 # convert age in seconds to "nn units ago" string
964 sub age_string {
965         my $age = shift;
966         my $age_str;
968         if ($age > 60*60*24*365*2) {
969                 $age_str = (int $age/60/60/24/365);
970                 $age_str .= " years ago";
971         } elsif ($age > 60*60*24*(365/12)*2) {
972                 $age_str = int $age/60/60/24/(365/12);
973                 $age_str .= " months ago";
974         } elsif ($age > 60*60*24*7*2) {
975                 $age_str = int $age/60/60/24/7;
976                 $age_str .= " weeks ago";
977         } elsif ($age > 60*60*24*2) {
978                 $age_str = int $age/60/60/24;
979                 $age_str .= " days ago";
980         } elsif ($age > 60*60*2) {
981                 $age_str = int $age/60/60;
982                 $age_str .= " hours ago";
983         } elsif ($age > 60*2) {
984                 $age_str = int $age/60;
985                 $age_str .= " min ago";
986         } elsif ($age > 2) {
987                 $age_str = int $age;
988                 $age_str .= " sec ago";
989         } else {
990                 $age_str .= " right now";
991         }
992         return $age_str;
995 use constant {
996         S_IFINVALID => 0030000,
997         S_IFGITLINK => 0160000,
998 };
1000 # submodule/subproject, a commit object reference
1001 sub S_ISGITLINK($) {
1002         my $mode = shift;
1004         return (($mode & S_IFMT) == S_IFGITLINK)
1007 # convert file mode in octal to symbolic file mode string
1008 sub mode_str {
1009         my $mode = oct shift;
1011         if (S_ISGITLINK($mode)) {
1012                 return 'm---------';
1013         } elsif (S_ISDIR($mode & S_IFMT)) {
1014                 return 'drwxr-xr-x';
1015         } elsif (S_ISLNK($mode)) {
1016                 return 'lrwxrwxrwx';
1017         } elsif (S_ISREG($mode)) {
1018                 # git cares only about the executable bit
1019                 if ($mode & S_IXUSR) {
1020                         return '-rwxr-xr-x';
1021                 } else {
1022                         return '-rw-r--r--';
1023                 };
1024         } else {
1025                 return '----------';
1026         }
1029 # convert file mode in octal to file type string
1030 sub file_type {
1031         my $mode = shift;
1033         if ($mode !~ m/^[0-7]+$/) {
1034                 return $mode;
1035         } else {
1036                 $mode = oct $mode;
1037         }
1039         if (S_ISGITLINK($mode)) {
1040                 return "submodule";
1041         } elsif (S_ISDIR($mode & S_IFMT)) {
1042                 return "directory";
1043         } elsif (S_ISLNK($mode)) {
1044                 return "symlink";
1045         } elsif (S_ISREG($mode)) {
1046                 return "file";
1047         } else {
1048                 return "unknown";
1049         }
1052 # convert file mode in octal to file type description string
1053 sub file_type_long {
1054         my $mode = shift;
1056         if ($mode !~ m/^[0-7]+$/) {
1057                 return $mode;
1058         } else {
1059                 $mode = oct $mode;
1060         }
1062         if (S_ISGITLINK($mode)) {
1063                 return "submodule";
1064         } elsif (S_ISDIR($mode & S_IFMT)) {
1065                 return "directory";
1066         } elsif (S_ISLNK($mode)) {
1067                 return "symlink";
1068         } elsif (S_ISREG($mode)) {
1069                 if ($mode & S_IXUSR) {
1070                         return "executable";
1071                 } else {
1072                         return "file";
1073                 };
1074         } else {
1075                 return "unknown";
1076         }
1080 ## ----------------------------------------------------------------------
1081 ## functions returning short HTML fragments, or transforming HTML fragments
1082 ## which don't belong to other sections
1084 # format line of commit message.
1085 sub format_log_line_html {
1086         my $line = shift;
1088         $line = esc_html($line, -nbsp=>1);
1089         if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1090                 my $hash_text = $1;
1091                 my $link =
1092                         $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1093                                 -class => "text"}, $hash_text);
1094                 $line =~ s/$hash_text/$link/;
1095         }
1096         return $line;
1099 # format marker of refs pointing to given object
1100 sub format_ref_marker {
1101         my ($refs, $id) = @_;
1102         my $markers = '';
1104         if (defined $refs->{$id}) {
1105                 foreach my $ref (@{$refs->{$id}}) {
1106                         my ($type, $name) = qw();
1107                         # e.g. tags/v2.6.11 or heads/next
1108                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1109                                 $type = $1;
1110                                 $name = $2;
1111                         } else {
1112                                 $type = "ref";
1113                                 $name = $ref;
1114                         }
1116                         $markers .= " <span class=\"$type\" title=\"$ref\">" .
1117                                     esc_html($name) . "</span>";
1118                 }
1119         }
1121         if ($markers) {
1122                 return ' <span class="refs">'. $markers . '</span>';
1123         } else {
1124                 return "";
1125         }
1128 # format, perhaps shortened and with markers, title line
1129 sub format_subject_html {
1130         my ($long, $short, $href, $extra) = @_;
1131         $extra = '' unless defined($extra);
1133         if (length($short) < length($long)) {
1134                 return $cgi->a({-href => $href, -class => "list subject",
1135                                 -title => to_utf8($long)},
1136                        esc_html($short) . $extra);
1137         } else {
1138                 return $cgi->a({-href => $href, -class => "list subject"},
1139                        esc_html($long)  . $extra);
1140         }
1143 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1144 sub format_git_diff_header_line {
1145         my $line = shift;
1146         my $diffinfo = shift;
1147         my ($from, $to) = @_;
1149         if ($diffinfo->{'nparents'}) {
1150                 # combined diff
1151                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1152                 if ($to->{'href'}) {
1153                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1154                                          esc_path($to->{'file'}));
1155                 } else { # file was deleted (no href)
1156                         $line .= esc_path($to->{'file'});
1157                 }
1158         } else {
1159                 # "ordinary" diff
1160                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1161                 if ($from->{'href'}) {
1162                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1163                                          'a/' . esc_path($from->{'file'}));
1164                 } else { # file was added (no href)
1165                         $line .= 'a/' . esc_path($from->{'file'});
1166                 }
1167                 $line .= ' ';
1168                 if ($to->{'href'}) {
1169                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1170                                          'b/' . esc_path($to->{'file'}));
1171                 } else { # file was deleted
1172                         $line .= 'b/' . esc_path($to->{'file'});
1173                 }
1174         }
1176         return "<div class=\"diff header\">$line</div>\n";
1179 # format extended diff header line, before patch itself
1180 sub format_extended_diff_header_line {
1181         my $line = shift;
1182         my $diffinfo = shift;
1183         my ($from, $to) = @_;
1185         # match <path>
1186         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1187                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1188                                        esc_path($from->{'file'}));
1189         }
1190         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1191                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1192                                  esc_path($to->{'file'}));
1193         }
1194         # match single <mode>
1195         if ($line =~ m/\s(\d{6})$/) {
1196                 $line .= '<span class="info"> (' .
1197                          file_type_long($1) .
1198                          ')</span>';
1199         }
1200         # match <hash>
1201         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1202                 # can match only for combined diff
1203                 $line = 'index ';
1204                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1205                         if ($from->{'href'}[$i]) {
1206                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1207                                                   -class=>"hash"},
1208                                                  substr($diffinfo->{'from_id'}[$i],0,7));
1209                         } else {
1210                                 $line .= '0' x 7;
1211                         }
1212                         # separator
1213                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1214                 }
1215                 $line .= '..';
1216                 if ($to->{'href'}) {
1217                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1218                                          substr($diffinfo->{'to_id'},0,7));
1219                 } else {
1220                         $line .= '0' x 7;
1221                 }
1223         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1224                 # can match only for ordinary diff
1225                 my ($from_link, $to_link);
1226                 if ($from->{'href'}) {
1227                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1228                                              substr($diffinfo->{'from_id'},0,7));
1229                 } else {
1230                         $from_link = '0' x 7;
1231                 }
1232                 if ($to->{'href'}) {
1233                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1234                                            substr($diffinfo->{'to_id'},0,7));
1235                 } else {
1236                         $to_link = '0' x 7;
1237                 }
1238                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1239                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1240         }
1242         return $line . "<br/>\n";
1245 # format from-file/to-file diff header
1246 sub format_diff_from_to_header {
1247         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1248         my $line;
1249         my $result = '';
1251         $line = $from_line;
1252         #assert($line =~ m/^---/) if DEBUG;
1253         # no extra formatting for "^--- /dev/null"
1254         if (! $diffinfo->{'nparents'}) {
1255                 # ordinary (single parent) diff
1256                 if ($line =~ m!^--- "?a/!) {
1257                         if ($from->{'href'}) {
1258                                 $line = '--- a/' .
1259                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1260                                                 esc_path($from->{'file'}));
1261                         } else {
1262                                 $line = '--- a/' .
1263                                         esc_path($from->{'file'});
1264                         }
1265                 }
1266                 $result .= qq!<div class="diff from_file">$line</div>\n!;
1268         } else {
1269                 # combined diff (merge commit)
1270                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1271                         if ($from->{'href'}[$i]) {
1272                                 $line = '--- ' .
1273                                         $cgi->a({-href=>href(action=>"blobdiff",
1274                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
1275                                                              hash_parent_base=>$parents[$i],
1276                                                              file_parent=>$from->{'file'}[$i],
1277                                                              hash=>$diffinfo->{'to_id'},
1278                                                              hash_base=>$hash,
1279                                                              file_name=>$to->{'file'}),
1280                                                  -class=>"path",
1281                                                  -title=>"diff" . ($i+1)},
1282                                                 $i+1) .
1283                                         '/' .
1284                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1285                                                 esc_path($from->{'file'}[$i]));
1286                         } else {
1287                                 $line = '--- /dev/null';
1288                         }
1289                         $result .= qq!<div class="diff from_file">$line</div>\n!;
1290                 }
1291         }
1293         $line = $to_line;
1294         #assert($line =~ m/^\+\+\+/) if DEBUG;
1295         # no extra formatting for "^+++ /dev/null"
1296         if ($line =~ m!^\+\+\+ "?b/!) {
1297                 if ($to->{'href'}) {
1298                         $line = '+++ b/' .
1299                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1300                                         esc_path($to->{'file'}));
1301                 } else {
1302                         $line = '+++ b/' .
1303                                 esc_path($to->{'file'});
1304                 }
1305         }
1306         $result .= qq!<div class="diff to_file">$line</div>\n!;
1308         return $result;
1311 # create note for patch simplified by combined diff
1312 sub format_diff_cc_simplified {
1313         my ($diffinfo, @parents) = @_;
1314         my $result = '';
1316         $result .= "<div class=\"diff header\">" .
1317                    "diff --cc ";
1318         if (!is_deleted($diffinfo)) {
1319                 $result .= $cgi->a({-href => href(action=>"blob",
1320                                                   hash_base=>$hash,
1321                                                   hash=>$diffinfo->{'to_id'},
1322                                                   file_name=>$diffinfo->{'to_file'}),
1323                                     -class => "path"},
1324                                    esc_path($diffinfo->{'to_file'}));
1325         } else {
1326                 $result .= esc_path($diffinfo->{'to_file'});
1327         }
1328         $result .= "</div>\n" . # class="diff header"
1329                    "<div class=\"diff nodifferences\">" .
1330                    "Simple merge" .
1331                    "</div>\n"; # class="diff nodifferences"
1333         return $result;
1336 # format patch (diff) line (not to be used for diff headers)
1337 sub format_diff_line {
1338         my $line = shift;
1339         my ($from, $to) = @_;
1340         my $diff_class = "";
1342         chomp $line;
1344         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1345                 # combined diff
1346                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1347                 if ($line =~ m/^\@{3}/) {
1348                         $diff_class = " chunk_header";
1349                 } elsif ($line =~ m/^\\/) {
1350                         $diff_class = " incomplete";
1351                 } elsif ($prefix =~ tr/+/+/) {
1352                         $diff_class = " add";
1353                 } elsif ($prefix =~ tr/-/-/) {
1354                         $diff_class = " rem";
1355                 }
1356         } else {
1357                 # assume ordinary diff
1358                 my $char = substr($line, 0, 1);
1359                 if ($char eq '+') {
1360                         $diff_class = " add";
1361                 } elsif ($char eq '-') {
1362                         $diff_class = " rem";
1363                 } elsif ($char eq '@') {
1364                         $diff_class = " chunk_header";
1365                 } elsif ($char eq "\\") {
1366                         $diff_class = " incomplete";
1367                 }
1368         }
1369         $line = untabify($line);
1370         if ($from && $to && $line =~ m/^\@{2} /) {
1371                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1372                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1374                 $from_lines = 0 unless defined $from_lines;
1375                 $to_lines   = 0 unless defined $to_lines;
1377                 if ($from->{'href'}) {
1378                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1379                                              -class=>"list"}, $from_text);
1380                 }
1381                 if ($to->{'href'}) {
1382                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1383                                              -class=>"list"}, $to_text);
1384                 }
1385                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1386                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1387                 return "<div class=\"diff$diff_class\">$line</div>\n";
1388         } elsif ($from && $to && $line =~ m/^\@{3}/) {
1389                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1390                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1392                 @from_text = split(' ', $ranges);
1393                 for (my $i = 0; $i < @from_text; ++$i) {
1394                         ($from_start[$i], $from_nlines[$i]) =
1395                                 (split(',', substr($from_text[$i], 1)), 0);
1396                 }
1398                 $to_text   = pop @from_text;
1399                 $to_start  = pop @from_start;
1400                 $to_nlines = pop @from_nlines;
1402                 $line = "<span class=\"chunk_info\">$prefix ";
1403                 for (my $i = 0; $i < @from_text; ++$i) {
1404                         if ($from->{'href'}[$i]) {
1405                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1406                                                   -class=>"list"}, $from_text[$i]);
1407                         } else {
1408                                 $line .= $from_text[$i];
1409                         }
1410                         $line .= " ";
1411                 }
1412                 if ($to->{'href'}) {
1413                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1414                                           -class=>"list"}, $to_text);
1415                 } else {
1416                         $line .= $to_text;
1417                 }
1418                 $line .= " $prefix</span>" .
1419                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1420                 return "<div class=\"diff$diff_class\">$line</div>\n";
1421         }
1422         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1425 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1426 # linked.  Pass the hash of the tree/commit to snapshot.
1427 sub format_snapshot_links {
1428         my ($hash) = @_;
1429         my @snapshot_fmts = gitweb_check_feature('snapshot');
1430         @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1431         my $num_fmts = @snapshot_fmts;
1432         if ($num_fmts > 1) {
1433                 # A parenthesized list of links bearing format names.
1434                 # e.g. "snapshot (_tar.gz_ _zip_)"
1435                 return "snapshot (" . join(' ', map
1436                         $cgi->a({
1437                                 -href => href(
1438                                         action=>"snapshot",
1439                                         hash=>$hash,
1440                                         snapshot_format=>$_
1441                                 )
1442                         }, $known_snapshot_formats{$_}{'display'})
1443                 , @snapshot_fmts) . ")";
1444         } elsif ($num_fmts == 1) {
1445                 # A single "snapshot" link whose tooltip bears the format name.
1446                 # i.e. "_snapshot_"
1447                 my ($fmt) = @snapshot_fmts;
1448                 return
1449                         $cgi->a({
1450                                 -href => href(
1451                                         action=>"snapshot",
1452                                         hash=>$hash,
1453                                         snapshot_format=>$fmt
1454                                 ),
1455                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1456                         }, "snapshot");
1457         } else { # $num_fmts == 0
1458                 return undef;
1459         }
1462 ## ......................................................................
1463 ## functions returning values to be passed, perhaps after some
1464 ## transformation, to other functions; e.g. returning arguments to href()
1466 # returns hash to be passed to href to generate gitweb URL
1467 # in -title key it returns description of link
1468 sub get_feed_info {
1469         my $format = shift || 'Atom';
1470         my %res = (action => lc($format));
1472         # feed links are possible only for project views
1473         return unless (defined $project);
1474         # some views should link to OPML, or to generic project feed,
1475         # or don't have specific feed yet (so they should use generic)
1476         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1478         my $branch;
1479         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1480         # from tag links; this also makes possible to detect branch links
1481         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1482             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
1483                 $branch = $1;
1484         }
1485         # find log type for feed description (title)
1486         my $type = 'log';
1487         if (defined $file_name) {
1488                 $type  = "history of $file_name";
1489                 $type .= "/" if ($action eq 'tree');
1490                 $type .= " on '$branch'" if (defined $branch);
1491         } else {
1492                 $type = "log of $branch" if (defined $branch);
1493         }
1495         $res{-title} = $type;
1496         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1497         $res{'file_name'} = $file_name;
1499         return %res;
1502 ## ----------------------------------------------------------------------
1503 ## git utility subroutines, invoking git commands
1505 # returns path to the core git executable and the --git-dir parameter as list
1506 sub git_cmd {
1507         return $GIT, '--git-dir='.$git_dir;
1510 # quote the given arguments for passing them to the shell
1511 # quote_command("command", "arg 1", "arg with ' and ! characters")
1512 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1513 # Try to avoid using this function wherever possible.
1514 sub quote_command {
1515         return join(' ',
1516                     map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1519 # get HEAD ref of given project as hash
1520 sub git_get_head_hash {
1521         my $project = shift;
1522         my $o_git_dir = $git_dir;
1523         my $retval = undef;
1524         $git_dir = "$projectroot/$project";
1525         if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1526                 my $head = <$fd>;
1527                 close $fd;
1528                 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1529                         $retval = $1;
1530                 }
1531         }
1532         if (defined $o_git_dir) {
1533                 $git_dir = $o_git_dir;
1534         }
1535         return $retval;
1538 # get type of given object
1539 sub git_get_type {
1540         my $hash = shift;
1542         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1543         my $type = <$fd>;
1544         close $fd or return;
1545         chomp $type;
1546         return $type;
1549 # repository configuration
1550 our $config_file = '';
1551 our %config;
1553 # store multiple values for single key as anonymous array reference
1554 # single values stored directly in the hash, not as [ <value> ]
1555 sub hash_set_multi {
1556         my ($hash, $key, $value) = @_;
1558         if (!exists $hash->{$key}) {
1559                 $hash->{$key} = $value;
1560         } elsif (!ref $hash->{$key}) {
1561                 $hash->{$key} = [ $hash->{$key}, $value ];
1562         } else {
1563                 push @{$hash->{$key}}, $value;
1564         }
1567 # return hash of git project configuration
1568 # optionally limited to some section, e.g. 'gitweb'
1569 sub git_parse_project_config {
1570         my $section_regexp = shift;
1571         my %config;
1573         local $/ = "\0";
1575         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1576                 or return;
1578         while (my $keyval = <$fh>) {
1579                 chomp $keyval;
1580                 my ($key, $value) = split(/\n/, $keyval, 2);
1582                 hash_set_multi(\%config, $key, $value)
1583                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1584         }
1585         close $fh;
1587         return %config;
1590 # convert config value to boolean, 'true' or 'false'
1591 # no value, number > 0, 'true' and 'yes' values are true
1592 # rest of values are treated as false (never as error)
1593 sub config_to_bool {
1594         my $val = shift;
1596         # strip leading and trailing whitespace
1597         $val =~ s/^\s+//;
1598         $val =~ s/\s+$//;
1600         return (!defined $val ||               # section.key
1601                 ($val =~ /^\d+$/ && $val) ||   # section.key = 1
1602                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
1605 # convert config value to simple decimal number
1606 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1607 # to be multiplied by 1024, 1048576, or 1073741824
1608 sub config_to_int {
1609         my $val = shift;
1611         # strip leading and trailing whitespace
1612         $val =~ s/^\s+//;
1613         $val =~ s/\s+$//;
1615         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1616                 $unit = lc($unit);
1617                 # unknown unit is treated as 1
1618                 return $num * ($unit eq 'g' ? 1073741824 :
1619                                $unit eq 'm' ?    1048576 :
1620                                $unit eq 'k' ?       1024 : 1);
1621         }
1622         return $val;
1625 # convert config value to array reference, if needed
1626 sub config_to_multi {
1627         my $val = shift;
1629         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1632 sub git_get_project_config {
1633         my ($key, $type) = @_;
1635         # key sanity check
1636         return unless ($key);
1637         $key =~ s/^gitweb\.//;
1638         return if ($key =~ m/\W/);
1640         # type sanity check
1641         if (defined $type) {
1642                 $type =~ s/^--//;
1643                 $type = undef
1644                         unless ($type eq 'bool' || $type eq 'int');
1645         }
1647         # get config
1648         if (!defined $config_file ||
1649             $config_file ne "$git_dir/config") {
1650                 %config = git_parse_project_config('gitweb');
1651                 $config_file = "$git_dir/config";
1652         }
1654         # ensure given type
1655         if (!defined $type) {
1656                 return $config{"gitweb.$key"};
1657         } elsif ($type eq 'bool') {
1658                 # backward compatibility: 'git config --bool' returns true/false
1659                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1660         } elsif ($type eq 'int') {
1661                 return config_to_int($config{"gitweb.$key"});
1662         }
1663         return $config{"gitweb.$key"};
1666 # get hash of given path at given ref
1667 sub git_get_hash_by_path {
1668         my $base = shift;
1669         my $path = shift || return undef;
1670         my $type = shift;
1672         $path =~ s,/+$,,;
1674         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1675                 or die_error(500, "Open git-ls-tree failed");
1676         my $line = <$fd>;
1677         close $fd or return undef;
1679         if (!defined $line) {
1680                 # there is no tree or hash given by $path at $base
1681                 return undef;
1682         }
1684         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
1685         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1686         if (defined $type && $type ne $2) {
1687                 # type doesn't match
1688                 return undef;
1689         }
1690         return $3;
1693 # get path of entry with given hash at given tree-ish (ref)
1694 # used to get 'from' filename for combined diff (merge commit) for renames
1695 sub git_get_path_by_hash {
1696         my $base = shift || return;
1697         my $hash = shift || return;
1699         local $/ = "\0";
1701         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1702                 or return undef;
1703         while (my $line = <$fd>) {
1704                 chomp $line;
1706                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
1707                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
1708                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1709                         close $fd;
1710                         return $1;
1711                 }
1712         }
1713         close $fd;
1714         return undef;
1717 ## ......................................................................
1718 ## git utility functions, directly accessing git repository
1720 sub git_get_project_description {
1721         my $path = shift;
1723         $git_dir = "$projectroot/$path";
1724         open my $fd, "$git_dir/description"
1725                 or return git_get_project_config('description');
1726         my $descr = <$fd>;
1727         close $fd;
1728         if (defined $descr) {
1729                 chomp $descr;
1730         }
1731         return $descr;
1734 sub git_get_project_url_list {
1735         my $path = shift;
1737         $git_dir = "$projectroot/$path";
1738         open my $fd, "$git_dir/cloneurl"
1739                 or return wantarray ?
1740                 @{ config_to_multi(git_get_project_config('url')) } :
1741                    config_to_multi(git_get_project_config('url'));
1742         my @git_project_url_list = map { chomp; $_ } <$fd>;
1743         close $fd;
1745         return wantarray ? @git_project_url_list : \@git_project_url_list;
1748 sub git_get_projects_list {
1749         my ($filter) = @_;
1750         my @list;
1752         $filter ||= '';
1753         $filter =~ s/\.git$//;
1755         my ($check_forks) = gitweb_check_feature('forks');
1757         if (-d $projects_list) {
1758                 # search in directory
1759                 my $dir = $projects_list . ($filter ? "/$filter" : '');
1760                 # remove the trailing "/"
1761                 $dir =~ s!/+$!!;
1762                 my $pfxlen = length("$dir");
1763                 my $pfxdepth = ($dir =~ tr!/!!);
1765                 File::Find::find({
1766                         follow_fast => 1, # follow symbolic links
1767                         follow_skip => 2, # ignore duplicates
1768                         dangling_symlinks => 0, # ignore dangling symlinks, silently
1769                         wanted => sub {
1770                                 # skip project-list toplevel, if we get it.
1771                                 return if (m!^[/.]$!);
1772                                 # only directories can be git repositories
1773                                 return unless (-d $_);
1774                                 # don't traverse too deep (Find is super slow on os x)
1775                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1776                                         $File::Find::prune = 1;
1777                                         return;
1778                                 }
1780                                 my $subdir = substr($File::Find::name, $pfxlen + 1);
1781                                 # we check related file in $projectroot
1782                                 if ($check_forks and $subdir =~ m#/.#) {
1783                                         $File::Find::prune = 1;
1784                                 } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
1785                                         push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1786                                         $File::Find::prune = 1;
1787                                 }
1788                         },
1789                 }, "$dir");
1791         } elsif (-f $projects_list) {
1792                 # read from file(url-encoded):
1793                 # 'git%2Fgit.git Linus+Torvalds'
1794                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1795                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1796                 my %paths;
1797                 open my ($fd), $projects_list or return;
1798         PROJECT:
1799                 while (my $line = <$fd>) {
1800                         chomp $line;
1801                         my ($path, $owner) = split ' ', $line;
1802                         $path = unescape($path);
1803                         $owner = unescape($owner);
1804                         if (!defined $path) {
1805                                 next;
1806                         }
1807                         if ($filter ne '') {
1808                                 # looking for forks;
1809                                 my $pfx = substr($path, 0, length($filter));
1810                                 if ($pfx ne $filter) {
1811                                         next PROJECT;
1812                                 }
1813                                 my $sfx = substr($path, length($filter));
1814                                 if ($sfx !~ /^\/.*\.git$/) {
1815                                         next PROJECT;
1816                                 }
1817                         } elsif ($check_forks) {
1818                         PATH:
1819                                 foreach my $filter (keys %paths) {
1820                                         # looking for forks;
1821                                         my $pfx = substr($path, 0, length($filter));
1822                                         if ($pfx ne $filter) {
1823                                                 next PATH;
1824                                         }
1825                                         my $sfx = substr($path, length($filter));
1826                                         if ($sfx !~ /^\/.*\.git$/) {
1827                                                 next PATH;
1828                                         }
1829                                         # is a fork, don't include it in
1830                                         # the list
1831                                         next PROJECT;
1832                                 }
1833                         }
1834                         if (check_export_ok("$projectroot/$path")) {
1835                                 my $pr = {
1836                                         path => $path,
1837                                         owner => to_utf8($owner),
1838                                 };
1839                                 push @list, $pr;
1840                                 (my $forks_path = $path) =~ s/\.git$//;
1841                                 $paths{$forks_path}++;
1842                         }
1843                 }
1844                 close $fd;
1845         }
1846         return @list;
1849 our $gitweb_project_owner = undef;
1850 sub git_get_project_list_from_file {
1852         return if (defined $gitweb_project_owner);
1854         $gitweb_project_owner = {};
1855         # read from file (url-encoded):
1856         # 'git%2Fgit.git Linus+Torvalds'
1857         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1858         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1859         if (-f $projects_list) {
1860                 open (my $fd , $projects_list);
1861                 while (my $line = <$fd>) {
1862                         chomp $line;
1863                         my ($pr, $ow) = split ' ', $line;
1864                         $pr = unescape($pr);
1865                         $ow = unescape($ow);
1866                         $gitweb_project_owner->{$pr} = to_utf8($ow);
1867                 }
1868                 close $fd;
1869         }
1872 sub git_get_project_owner {
1873         my $project = shift;
1874         my $owner;
1876         return undef unless $project;
1877         $git_dir = "$projectroot/$project";
1879         if (!defined $gitweb_project_owner) {
1880                 git_get_project_list_from_file();
1881         }
1883         if (exists $gitweb_project_owner->{$project}) {
1884                 $owner = $gitweb_project_owner->{$project};
1885         }
1886         if (!defined $owner){
1887                 $owner = git_get_project_config('owner');
1888         }
1889         if (!defined $owner) {
1890                 $owner = get_file_owner("$git_dir");
1891         }
1893         return $owner;
1896 sub git_get_last_activity {
1897         my ($path) = @_;
1898         my $fd;
1900         $git_dir = "$projectroot/$path";
1901         open($fd, "-|", git_cmd(), 'for-each-ref',
1902              '--format=%(committer)',
1903              '--sort=-committerdate',
1904              '--count=1',
1905              'refs/heads') or return;
1906         my $most_recent = <$fd>;
1907         close $fd or return;
1908         if (defined $most_recent &&
1909             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1910                 my $timestamp = $1;
1911                 my $age = time - $timestamp;
1912                 return ($age, age_string($age));
1913         }
1914         return (undef, undef);
1917 sub git_get_references {
1918         my $type = shift || "";
1919         my %refs;
1920         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1921         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1922         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
1923                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
1924                 or return;
1926         while (my $line = <$fd>) {
1927                 chomp $line;
1928                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
1929                         if (defined $refs{$1}) {
1930                                 push @{$refs{$1}}, $2;
1931                         } else {
1932                                 $refs{$1} = [ $2 ];
1933                         }
1934                 }
1935         }
1936         close $fd or return;
1937         return \%refs;
1940 sub git_get_rev_name_tags {
1941         my $hash = shift || return undef;
1943         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
1944                 or return;
1945         my $name_rev = <$fd>;
1946         close $fd;
1948         if ($name_rev =~ m|^$hash tags/(.*)$|) {
1949                 return $1;
1950         } else {
1951                 # catches also '$hash undefined' output
1952                 return undef;
1953         }
1956 ## ----------------------------------------------------------------------
1957 ## parse to hash functions
1959 sub parse_date {
1960         my $epoch = shift;
1961         my $tz = shift || "-0000";
1963         my %date;
1964         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
1965         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
1966         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
1967         $date{'hour'} = $hour;
1968         $date{'minute'} = $min;
1969         $date{'mday'} = $mday;
1970         $date{'day'} = $days[$wday];
1971         $date{'month'} = $months[$mon];
1972         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
1973                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
1974         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
1975                              $mday, $months[$mon], $hour ,$min;
1976         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
1977                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
1979         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
1980         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
1981         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
1982         $date{'hour_local'} = $hour;
1983         $date{'minute_local'} = $min;
1984         $date{'tz_local'} = $tz;
1985         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
1986                                   1900+$year, $mon+1, $mday,
1987                                   $hour, $min, $sec, $tz);
1988         return %date;
1991 sub parse_tag {
1992         my $tag_id = shift;
1993         my %tag;
1994         my @comment;
1996         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
1997         $tag{'id'} = $tag_id;
1998         while (my $line = <$fd>) {
1999                 chomp $line;
2000                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2001                         $tag{'object'} = $1;
2002                 } elsif ($line =~ m/^type (.+)$/) {
2003                         $tag{'type'} = $1;
2004                 } elsif ($line =~ m/^tag (.+)$/) {
2005                         $tag{'name'} = $1;
2006                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2007                         $tag{'author'} = $1;
2008                         $tag{'epoch'} = $2;
2009                         $tag{'tz'} = $3;
2010                 } elsif ($line =~ m/--BEGIN/) {
2011                         push @comment, $line;
2012                         last;
2013                 } elsif ($line eq "") {
2014                         last;
2015                 }
2016         }
2017         push @comment, <$fd>;
2018         $tag{'comment'} = \@comment;
2019         close $fd or return;
2020         if (!defined $tag{'name'}) {
2021                 return
2022         };
2023         return %tag
2026 sub parse_commit_text {
2027         my ($commit_text, $withparents) = @_;
2028         my @commit_lines = split '\n', $commit_text;
2029         my %co;
2031         pop @commit_lines; # Remove '\0'
2033         if (! @commit_lines) {
2034                 return;
2035         }
2037         my $header = shift @commit_lines;
2038         if ($header !~ m/^[0-9a-fA-F]{40}/) {
2039                 return;
2040         }
2041         ($co{'id'}, my @parents) = split ' ', $header;
2042         while (my $line = shift @commit_lines) {
2043                 last if $line eq "\n";
2044                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2045                         $co{'tree'} = $1;
2046                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2047                         push @parents, $1;
2048                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2049                         $co{'author'} = $1;
2050                         $co{'author_epoch'} = $2;
2051                         $co{'author_tz'} = $3;
2052                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2053                                 $co{'author_name'}  = $1;
2054                                 $co{'author_email'} = $2;
2055                         } else {
2056                                 $co{'author_name'} = $co{'author'};
2057                         }
2058                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2059                         $co{'committer'} = $1;
2060                         $co{'committer_epoch'} = $2;
2061                         $co{'committer_tz'} = $3;
2062                         $co{'committer_name'} = $co{'committer'};
2063                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2064                                 $co{'committer_name'}  = $1;
2065                                 $co{'committer_email'} = $2;
2066                         } else {
2067                                 $co{'committer_name'} = $co{'committer'};
2068                         }
2069                 }
2070         }
2071         if (!defined $co{'tree'}) {
2072                 return;
2073         };
2074         $co{'parents'} = \@parents;
2075         $co{'parent'} = $parents[0];
2077         foreach my $title (@commit_lines) {
2078                 $title =~ s/^    //;
2079                 if ($title ne "") {
2080                         $co{'title'} = chop_str($title, 80, 5);
2081                         # remove leading stuff of merges to make the interesting part visible
2082                         if (length($title) > 50) {
2083                                 $title =~ s/^Automatic //;
2084                                 $title =~ s/^merge (of|with) /Merge ... /i;
2085                                 if (length($title) > 50) {
2086                                         $title =~ s/(http|rsync):\/\///;
2087                                 }
2088                                 if (length($title) > 50) {
2089                                         $title =~ s/(master|www|rsync)\.//;
2090                                 }
2091                                 if (length($title) > 50) {
2092                                         $title =~ s/kernel.org:?//;
2093                                 }
2094                                 if (length($title) > 50) {
2095                                         $title =~ s/\/pub\/scm//;
2096                                 }
2097                         }
2098                         $co{'title_short'} = chop_str($title, 50, 5);
2099                         last;
2100                 }
2101         }
2102         if (! defined $co{'title'} || $co{'title'} eq "") {
2103                 $co{'title'} = $co{'title_short'} = '(no commit message)';
2104         }
2105         # remove added spaces
2106         foreach my $line (@commit_lines) {
2107                 $line =~ s/^    //;
2108         }
2109         $co{'comment'} = \@commit_lines;
2111         my $age = time - $co{'committer_epoch'};
2112         $co{'age'} = $age;
2113         $co{'age_string'} = age_string($age);
2114         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2115         if ($age > 60*60*24*7*2) {
2116                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2117                 $co{'age_string_age'} = $co{'age_string'};
2118         } else {
2119                 $co{'age_string_date'} = $co{'age_string'};
2120                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2121         }
2122         return %co;
2125 sub parse_commit {
2126         my ($commit_id) = @_;
2127         my %co;
2129         local $/ = "\0";
2131         open my $fd, "-|", git_cmd(), "rev-list",
2132                 "--parents",
2133                 "--header",
2134                 "--max-count=1",
2135                 $commit_id,
2136                 "--",
2137                 or die_error(500, "Open git-rev-list failed");
2138         %co = parse_commit_text(<$fd>, 1);
2139         close $fd;
2141         return %co;
2144 sub parse_commits {
2145         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2146         my @cos;
2148         $maxcount ||= 1;
2149         $skip ||= 0;
2151         local $/ = "\0";
2153         open my $fd, "-|", git_cmd(), "rev-list",
2154                 "--header",
2155                 @args,
2156                 ("--max-count=" . $maxcount),
2157                 ("--skip=" . $skip),
2158                 @extra_options,
2159                 $commit_id,
2160                 "--",
2161                 ($filename ? ($filename) : ())
2162                 or die_error(500, "Open git-rev-list failed");
2163         while (my $line = <$fd>) {
2164                 my %co = parse_commit_text($line);
2165                 push @cos, \%co;
2166         }
2167         close $fd;
2169         return wantarray ? @cos : \@cos;
2172 # parse line of git-diff-tree "raw" output
2173 sub parse_difftree_raw_line {
2174         my $line = shift;
2175         my %res;
2177         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2178         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2179         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2180                 $res{'from_mode'} = $1;
2181                 $res{'to_mode'} = $2;
2182                 $res{'from_id'} = $3;
2183                 $res{'to_id'} = $4;
2184                 $res{'status'} = $5;
2185                 $res{'similarity'} = $6;
2186                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2187                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2188                 } else {
2189                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2190                 }
2191         }
2192         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2193         # combined diff (for merge commit)
2194         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2195                 $res{'nparents'}  = length($1);
2196                 $res{'from_mode'} = [ split(' ', $2) ];
2197                 $res{'to_mode'} = pop @{$res{'from_mode'}};
2198                 $res{'from_id'} = [ split(' ', $3) ];
2199                 $res{'to_id'} = pop @{$res{'from_id'}};
2200                 $res{'status'} = [ split('', $4) ];
2201                 $res{'to_file'} = unquote($5);
2202         }
2203         # 'c512b523472485aef4fff9e57b229d9d243c967f'
2204         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2205                 $res{'commit'} = $1;
2206         }
2208         return wantarray ? %res : \%res;
2211 # wrapper: return parsed line of git-diff-tree "raw" output
2212 # (the argument might be raw line, or parsed info)
2213 sub parsed_difftree_line {
2214         my $line_or_ref = shift;
2216         if (ref($line_or_ref) eq "HASH") {
2217                 # pre-parsed (or generated by hand)
2218                 return $line_or_ref;
2219         } else {
2220                 return parse_difftree_raw_line($line_or_ref);
2221         }
2224 # parse line of git-ls-tree output
2225 sub parse_ls_tree_line ($;%) {
2226         my $line = shift;
2227         my %opts = @_;
2228         my %res;
2230         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2231         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2233         $res{'mode'} = $1;
2234         $res{'type'} = $2;
2235         $res{'hash'} = $3;
2236         if ($opts{'-z'}) {
2237                 $res{'name'} = $4;
2238         } else {
2239                 $res{'name'} = unquote($4);
2240         }
2242         return wantarray ? %res : \%res;
2245 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2246 sub parse_from_to_diffinfo {
2247         my ($diffinfo, $from, $to, @parents) = @_;
2249         if ($diffinfo->{'nparents'}) {
2250                 # combined diff
2251                 $from->{'file'} = [];
2252                 $from->{'href'} = [];
2253                 fill_from_file_info($diffinfo, @parents)
2254                         unless exists $diffinfo->{'from_file'};
2255                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2256                         $from->{'file'}[$i] =
2257                                 defined $diffinfo->{'from_file'}[$i] ?
2258                                         $diffinfo->{'from_file'}[$i] :
2259                                         $diffinfo->{'to_file'};
2260                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2261                                 $from->{'href'}[$i] = href(action=>"blob",
2262                                                            hash_base=>$parents[$i],
2263                                                            hash=>$diffinfo->{'from_id'}[$i],
2264                                                            file_name=>$from->{'file'}[$i]);
2265                         } else {
2266                                 $from->{'href'}[$i] = undef;
2267                         }
2268                 }
2269         } else {
2270                 # ordinary (not combined) diff
2271                 $from->{'file'} = $diffinfo->{'from_file'};
2272                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2273                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2274                                                hash=>$diffinfo->{'from_id'},
2275                                                file_name=>$from->{'file'});
2276                 } else {
2277                         delete $from->{'href'};
2278                 }
2279         }
2281         $to->{'file'} = $diffinfo->{'to_file'};
2282         if (!is_deleted($diffinfo)) { # file exists in result
2283                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2284                                      hash=>$diffinfo->{'to_id'},
2285                                      file_name=>$to->{'file'});
2286         } else {
2287                 delete $to->{'href'};
2288         }
2291 ## ......................................................................
2292 ## parse to array of hashes functions
2294 sub git_get_heads_list {
2295         my $limit = shift;
2296         my @headslist;
2298         open my $fd, '-|', git_cmd(), 'for-each-ref',
2299                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2300                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2301                 'refs/heads'
2302                 or return;
2303         while (my $line = <$fd>) {
2304                 my %ref_item;
2306                 chomp $line;
2307                 my ($refinfo, $committerinfo) = split(/\0/, $line);
2308                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2309                 my ($committer, $epoch, $tz) =
2310                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2311                 $ref_item{'fullname'}  = $name;
2312                 $name =~ s!^refs/heads/!!;
2314                 $ref_item{'name'}  = $name;
2315                 $ref_item{'id'}    = $hash;
2316                 $ref_item{'title'} = $title || '(no commit message)';
2317                 $ref_item{'epoch'} = $epoch;
2318                 if ($epoch) {
2319                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2320                 } else {
2321                         $ref_item{'age'} = "unknown";
2322                 }
2324                 push @headslist, \%ref_item;
2325         }
2326         close $fd;
2328         return wantarray ? @headslist : \@headslist;
2331 sub git_get_tags_list {
2332         my $limit = shift;
2333         my @tagslist;
2335         open my $fd, '-|', git_cmd(), 'for-each-ref',
2336                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2337                 '--format=%(objectname) %(objecttype) %(refname) '.
2338                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2339                 'refs/tags'
2340                 or return;
2341         while (my $line = <$fd>) {
2342                 my %ref_item;
2344                 chomp $line;
2345                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2346                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2347                 my ($creator, $epoch, $tz) =
2348                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2349                 $ref_item{'fullname'} = $name;
2350                 $name =~ s!^refs/tags/!!;
2352                 $ref_item{'type'} = $type;
2353                 $ref_item{'id'} = $id;
2354                 $ref_item{'name'} = $name;
2355                 if ($type eq "tag") {
2356                         $ref_item{'subject'} = $title;
2357                         $ref_item{'reftype'} = $reftype;
2358                         $ref_item{'refid'}   = $refid;
2359                 } else {
2360                         $ref_item{'reftype'} = $type;
2361                         $ref_item{'refid'}   = $id;
2362                 }
2364                 if ($type eq "tag" || $type eq "commit") {
2365                         $ref_item{'epoch'} = $epoch;
2366                         if ($epoch) {
2367                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2368                         } else {
2369                                 $ref_item{'age'} = "unknown";
2370                         }
2371                 }
2373                 push @tagslist, \%ref_item;
2374         }
2375         close $fd;
2377         return wantarray ? @tagslist : \@tagslist;
2380 ## ----------------------------------------------------------------------
2381 ## filesystem-related functions
2383 sub get_file_owner {
2384         my $path = shift;
2386         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2387         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2388         if (!defined $gcos) {
2389                 return undef;
2390         }
2391         my $owner = $gcos;
2392         $owner =~ s/[,;].*$//;
2393         return to_utf8($owner);
2396 ## ......................................................................
2397 ## mimetype related functions
2399 sub mimetype_guess_file {
2400         my $filename = shift;
2401         my $mimemap = shift;
2402         -r $mimemap or return undef;
2404         my %mimemap;
2405         open(MIME, $mimemap) or return undef;
2406         while (<MIME>) {
2407                 next if m/^#/; # skip comments
2408                 my ($mime, $exts) = split(/\t+/);
2409                 if (defined $exts) {
2410                         my @exts = split(/\s+/, $exts);
2411                         foreach my $ext (@exts) {
2412                                 $mimemap{$ext} = $mime;
2413                         }
2414                 }
2415         }
2416         close(MIME);
2418         $filename =~ /\.([^.]*)$/;
2419         return $mimemap{$1};
2422 sub mimetype_guess {
2423         my $filename = shift;
2424         my $mime;
2425         $filename =~ /\./ or return undef;
2427         if ($mimetypes_file) {
2428                 my $file = $mimetypes_file;
2429                 if ($file !~ m!^/!) { # if it is relative path
2430                         # it is relative to project
2431                         $file = "$projectroot/$project/$file";
2432                 }
2433                 $mime = mimetype_guess_file($filename, $file);
2434         }
2435         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2436         return $mime;
2439 sub blob_mimetype {
2440         my $fd = shift;
2441         my $filename = shift;
2443         if ($filename) {
2444                 my $mime = mimetype_guess($filename);
2445                 $mime and return $mime;
2446         }
2448         # just in case
2449         return $default_blob_plain_mimetype unless $fd;
2451         if (-T $fd) {
2452                 return 'text/plain';
2453         } elsif (! $filename) {
2454                 return 'application/octet-stream';
2455         } elsif ($filename =~ m/\.png$/i) {
2456                 return 'image/png';
2457         } elsif ($filename =~ m/\.gif$/i) {
2458                 return 'image/gif';
2459         } elsif ($filename =~ m/\.jpe?g$/i) {
2460                 return 'image/jpeg';
2461         } else {
2462                 return 'application/octet-stream';
2463         }
2466 sub blob_contenttype {
2467         my ($fd, $file_name, $type) = @_;
2469         $type ||= blob_mimetype($fd, $file_name);
2470         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2471                 $type .= "; charset=$default_text_plain_charset";
2472         }
2474         return $type;
2477 ## ======================================================================
2478 ## functions printing HTML: header, footer, error page
2480 sub git_header_html {
2481         my $status = shift || "200 OK";
2482         my $expires = shift;
2484         my $title = "$site_name";
2485         if (defined $project) {
2486                 $title .= " - " . to_utf8($project);
2487                 if (defined $action) {
2488                         $title .= "/$action";
2489                         if (defined $file_name) {
2490                                 $title .= " - " . esc_path($file_name);
2491                                 if ($action eq "tree" && $file_name !~ m|/$|) {
2492                                         $title .= "/";
2493                                 }
2494                         }
2495                 }
2496         }
2497         my $content_type;
2498         # require explicit support from the UA if we are to send the page as
2499         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2500         # we have to do this because MSIE sometimes globs '*/*', pretending to
2501         # support xhtml+xml but choking when it gets what it asked for.
2502         if (defined $cgi->http('HTTP_ACCEPT') &&
2503             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2504             $cgi->Accept('application/xhtml+xml') != 0) {
2505                 $content_type = 'application/xhtml+xml';
2506         } else {
2507                 $content_type = 'text/html';
2508         }
2509         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2510                            -status=> $status, -expires => $expires);
2511         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2512         print <<EOF;
2513 <?xml version="1.0" encoding="utf-8"?>
2514 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2515 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2516 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2517 <!-- git core binaries version $git_version -->
2518 <head>
2519 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2520 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2521 <meta name="robots" content="index, nofollow"/>
2522 <title>$title</title>
2523 EOF
2524 # print out each stylesheet that exist
2525         if (defined $stylesheet) {
2526 #provides backwards capability for those people who define style sheet in a config file
2527                 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2528         } else {
2529                 foreach my $stylesheet (@stylesheets) {
2530                         next unless $stylesheet;
2531                         print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2532                 }
2533         }
2534         if (defined $project) {
2535                 my %href_params = get_feed_info();
2536                 if (!exists $href_params{'-title'}) {
2537                         $href_params{'-title'} = 'log';
2538                 }
2540                 foreach my $format qw(RSS Atom) {
2541                         my $type = lc($format);
2542                         my %link_attr = (
2543                                 '-rel' => 'alternate',
2544                                 '-title' => "$project - $href_params{'-title'} - $format feed",
2545                                 '-type' => "application/$type+xml"
2546                         );
2548                         $href_params{'action'} = $type;
2549                         $link_attr{'-href'} = href(%href_params);
2550                         print "<link ".
2551                               "rel=\"$link_attr{'-rel'}\" ".
2552                               "title=\"$link_attr{'-title'}\" ".
2553                               "href=\"$link_attr{'-href'}\" ".
2554                               "type=\"$link_attr{'-type'}\" ".
2555                               "/>\n";
2557                         $href_params{'extra_options'} = '--no-merges';
2558                         $link_attr{'-href'} = href(%href_params);
2559                         $link_attr{'-title'} .= ' (no merges)';
2560                         print "<link ".
2561                               "rel=\"$link_attr{'-rel'}\" ".
2562                               "title=\"$link_attr{'-title'}\" ".
2563                               "href=\"$link_attr{'-href'}\" ".
2564                               "type=\"$link_attr{'-type'}\" ".
2565                               "/>\n";
2566                 }
2568         } else {
2569                 printf('<link rel="alternate" title="%s projects list" '.
2570                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
2571                        $site_name, href(project=>undef, action=>"project_index"));
2572                 printf('<link rel="alternate" title="%s projects feeds" '.
2573                        'href="%s" type="text/x-opml" />'."\n",
2574                        $site_name, href(project=>undef, action=>"opml"));
2575         }
2576         if (defined $favicon) {
2577                 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2578         }
2580         print "</head>\n" .
2581               "<body>\n";
2583         if (-f $site_header) {
2584                 open (my $fd, $site_header);
2585                 print <$fd>;
2586                 close $fd;
2587         }
2589         print "<div class=\"page_header\">\n" .
2590               $cgi->a({-href => esc_url($logo_url),
2591                        -title => $logo_label},
2592                       qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2593         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2594         if (defined $project) {
2595                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2596                 if (defined $action) {
2597                         print " / $action";
2598                 }
2599                 print "\n";
2600         }
2601         print "</div>\n";
2603         my ($have_search) = gitweb_check_feature('search');
2604         if (defined $project && $have_search) {
2605                 if (!defined $searchtext) {
2606                         $searchtext = "";
2607                 }
2608                 my $search_hash;
2609                 if (defined $hash_base) {
2610                         $search_hash = $hash_base;
2611                 } elsif (defined $hash) {
2612                         $search_hash = $hash;
2613                 } else {
2614                         $search_hash = "HEAD";
2615                 }
2616                 my $action = $my_uri;
2617                 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2618                 if ($use_pathinfo) {
2619                         $action .= "/".esc_url($project);
2620                 }
2621                 print $cgi->startform(-method => "get", -action => $action) .
2622                       "<div class=\"search\">\n" .
2623                       (!$use_pathinfo &&
2624                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2625                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2626                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2627                       $cgi->popup_menu(-name => 'st', -default => 'commit',
2628                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2629                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2630                       " search:\n",
2631                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2632                       "<span title=\"Extended regular expression\">" .
2633                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2634                                      -checked => $search_use_regexp) .
2635                       "</span>" .
2636                       "</div>" .
2637                       $cgi->end_form() . "\n";
2638         }
2641 sub git_footer_html {
2642         my $feed_class = 'rss_logo';
2644         print "<div class=\"page_footer\">\n";
2645         if (defined $project) {
2646                 my $descr = git_get_project_description($project);
2647                 if (defined $descr) {
2648                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2649                 }
2651                 my %href_params = get_feed_info();
2652                 if (!%href_params) {
2653                         $feed_class .= ' generic';
2654                 }
2655                 $href_params{'-title'} ||= 'log';
2657                 foreach my $format qw(RSS Atom) {
2658                         $href_params{'action'} = lc($format);
2659                         print $cgi->a({-href => href(%href_params),
2660                                       -title => "$href_params{'-title'} $format feed",
2661                                       -class => $feed_class}, $format)."\n";
2662                 }
2664         } else {
2665                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2666                               -class => $feed_class}, "OPML") . " ";
2667                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2668                               -class => $feed_class}, "TXT") . "\n";
2669         }
2670         print "</div>\n"; # class="page_footer"
2672         if (-f $site_footer) {
2673                 open (my $fd, $site_footer);
2674                 print <$fd>;
2675                 close $fd;
2676         }
2678         print "</body>\n" .
2679               "</html>";
2682 # die_error(<http_status_code>, <error_message>)
2683 # Example: die_error(404, 'Hash not found')
2684 # By convention, use the following status codes (as defined in RFC 2616):
2685 # 400: Invalid or missing CGI parameters, or
2686 #      requested object exists but has wrong type.
2687 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2688 #      this server or project.
2689 # 404: Requested object/revision/project doesn't exist.
2690 # 500: The server isn't configured properly, or
2691 #      an internal error occurred (e.g. failed assertions caused by bugs), or
2692 #      an unknown error occurred (e.g. the git binary died unexpectedly).
2693 sub die_error {
2694         my $status = shift || 500;
2695         my $error = shift || "Internal server error";
2697         my %http_responses = (400 => '400 Bad Request',
2698                               403 => '403 Forbidden',
2699                               404 => '404 Not Found',
2700                               500 => '500 Internal Server Error');
2701         git_header_html($http_responses{$status});
2702         print <<EOF;
2703 <div class="page_body">
2704 <br /><br />
2705 $status - $error
2706 <br />
2707 </div>
2708 EOF
2709         git_footer_html();
2710         exit;
2713 ## ----------------------------------------------------------------------
2714 ## functions printing or outputting HTML: navigation
2716 sub git_print_page_nav {
2717         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2718         $extra = '' if !defined $extra; # pager or formats
2720         my @navs = qw(summary shortlog log commit commitdiff tree);
2721         if ($suppress) {
2722                 @navs = grep { $_ ne $suppress } @navs;
2723         }
2725         my %arg = map { $_ => {action=>$_} } @navs;
2726         if (defined $head) {
2727                 for (qw(commit commitdiff)) {
2728                         $arg{$_}{'hash'} = $head;
2729                 }
2730                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2731                         for (qw(shortlog log)) {
2732                                 $arg{$_}{'hash'} = $head;
2733                         }
2734                 }
2735         }
2736         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2737         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2739         print "<div class=\"page_nav\">\n" .
2740                 (join " | ",
2741                  map { $_ eq $current ?
2742                        $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2743                  } @navs);
2744         print "<br/>\n$extra<br/>\n" .
2745               "</div>\n";
2748 sub format_paging_nav {
2749         my ($action, $hash, $head, $page, $has_next_link) = @_;
2750         my $paging_nav;
2753         if ($hash ne $head || $page) {
2754                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2755         } else {
2756                 $paging_nav .= "HEAD";
2757         }
2759         if ($page > 0) {
2760                 $paging_nav .= " &sdot; " .
2761                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
2762                                  -accesskey => "p", -title => "Alt-p"}, "prev");
2763         } else {
2764                 $paging_nav .= " &sdot; prev";
2765         }
2767         if ($has_next_link) {
2768                 $paging_nav .= " &sdot; " .
2769                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
2770                                  -accesskey => "n", -title => "Alt-n"}, "next");
2771         } else {
2772                 $paging_nav .= " &sdot; next";
2773         }
2775         return $paging_nav;
2778 ## ......................................................................
2779 ## functions printing or outputting HTML: div
2781 sub git_print_header_div {
2782         my ($action, $title, $hash, $hash_base) = @_;
2783         my %args = ();
2785         $args{'action'} = $action;
2786         $args{'hash'} = $hash if $hash;
2787         $args{'hash_base'} = $hash_base if $hash_base;
2789         print "<div class=\"header\">\n" .
2790               $cgi->a({-href => href(%args), -class => "title"},
2791               $title ? $title : $action) .
2792               "\n</div>\n";
2795 #sub git_print_authorship (\%) {
2796 sub git_print_authorship {
2797         my $co = shift;
2799         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2800         print "<div class=\"author_date\">" .
2801               esc_html($co->{'author_name'}) .
2802               " [$ad{'rfc2822'}";
2803         if ($ad{'hour_local'} < 6) {
2804                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2805                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2806         } else {
2807                 printf(" (%02d:%02d %s)",
2808                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2809         }
2810         print "]</div>\n";
2813 sub git_print_page_path {
2814         my $name = shift;
2815         my $type = shift;
2816         my $hb = shift;
2819         print "<div class=\"page_path\">";
2820         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2821                       -title => 'tree root'}, to_utf8("[$project]"));
2822         print " / ";
2823         if (defined $name) {
2824                 my @dirname = split '/', $name;
2825                 my $basename = pop @dirname;
2826                 my $fullname = '';
2828                 foreach my $dir (@dirname) {
2829                         $fullname .= ($fullname ? '/' : '') . $dir;
2830                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2831                                                      hash_base=>$hb),
2832                                       -title => $fullname}, esc_path($dir));
2833                         print " / ";
2834                 }
2835                 if (defined $type && $type eq 'blob') {
2836                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2837                                                      hash_base=>$hb),
2838                                       -title => $name}, esc_path($basename));
2839                 } elsif (defined $type && $type eq 'tree') {
2840                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2841                                                      hash_base=>$hb),
2842                                       -title => $name}, esc_path($basename));
2843                         print " / ";
2844                 } else {
2845                         print esc_path($basename);
2846                 }
2847         }
2848         print "<br/></div>\n";
2851 # sub git_print_log (\@;%) {
2852 sub git_print_log ($;%) {
2853         my $log = shift;
2854         my %opts = @_;
2856         if ($opts{'-remove_title'}) {
2857                 # remove title, i.e. first line of log
2858                 shift @$log;
2859         }
2860         # remove leading empty lines
2861         while (defined $log->[0] && $log->[0] eq "") {
2862                 shift @$log;
2863         }
2865         # print log
2866         my $signoff = 0;
2867         my $empty = 0;
2868         foreach my $line (@$log) {
2869                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2870                         $signoff = 1;
2871                         $empty = 0;
2872                         if (! $opts{'-remove_signoff'}) {
2873                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2874                                 next;
2875                         } else {
2876                                 # remove signoff lines
2877                                 next;
2878                         }
2879                 } else {
2880                         $signoff = 0;
2881                 }
2883                 # print only one empty line
2884                 # do not print empty line after signoff
2885                 if ($line eq "") {
2886                         next if ($empty || $signoff);
2887                         $empty = 1;
2888                 } else {
2889                         $empty = 0;
2890                 }
2892                 print format_log_line_html($line) . "<br/>\n";
2893         }
2895         if ($opts{'-final_empty_line'}) {
2896                 # end with single empty line
2897                 print "<br/>\n" unless $empty;
2898         }
2901 # return link target (what link points to)
2902 sub git_get_link_target {
2903         my $hash = shift;
2904         my $link_target;
2906         # read link
2907         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2908                 or return;
2909         {
2910                 local $/;
2911                 $link_target = <$fd>;
2912         }
2913         close $fd
2914                 or return;
2916         return $link_target;
2919 # given link target, and the directory (basedir) the link is in,
2920 # return target of link relative to top directory (top tree);
2921 # return undef if it is not possible (including absolute links).
2922 sub normalize_link_target {
2923         my ($link_target, $basedir, $hash_base) = @_;
2925         # we can normalize symlink target only if $hash_base is provided
2926         return unless $hash_base;
2928         # absolute symlinks (beginning with '/') cannot be normalized
2929         return if (substr($link_target, 0, 1) eq '/');
2931         # normalize link target to path from top (root) tree (dir)
2932         my $path;
2933         if ($basedir) {
2934                 $path = $basedir . '/' . $link_target;
2935         } else {
2936                 # we are in top (root) tree (dir)
2937                 $path = $link_target;
2938         }
2940         # remove //, /./, and /../
2941         my @path_parts;
2942         foreach my $part (split('/', $path)) {
2943                 # discard '.' and ''
2944                 next if (!$part || $part eq '.');
2945                 # handle '..'
2946                 if ($part eq '..') {
2947                         if (@path_parts) {
2948                                 pop @path_parts;
2949                         } else {
2950                                 # link leads outside repository (outside top dir)
2951                                 return;
2952                         }
2953                 } else {
2954                         push @path_parts, $part;
2955                 }
2956         }
2957         $path = join('/', @path_parts);
2959         return $path;
2962 # print tree entry (row of git_tree), but without encompassing <tr> element
2963 sub git_print_tree_entry {
2964         my ($t, $basedir, $hash_base, $have_blame) = @_;
2966         my %base_key = ();
2967         $base_key{'hash_base'} = $hash_base if defined $hash_base;
2969         # The format of a table row is: mode list link.  Where mode is
2970         # the mode of the entry, list is the name of the entry, an href,
2971         # and link is the action links of the entry.
2973         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
2974         if ($t->{'type'} eq "blob") {
2975                 print "<td class=\"list\">" .
2976                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
2977                                                file_name=>"$basedir$t->{'name'}", %base_key),
2978                                 -class => "list"}, esc_path($t->{'name'}));
2979                 if (S_ISLNK(oct $t->{'mode'})) {
2980                         my $link_target = git_get_link_target($t->{'hash'});
2981                         if ($link_target) {
2982                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
2983                                 if (defined $norm_target) {
2984                                         print " -> " .
2985                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
2986                                                                      file_name=>$norm_target),
2987                                                        -title => $norm_target}, esc_path($link_target));
2988                                 } else {
2989                                         print " -> " . esc_path($link_target);
2990                                 }
2991                         }
2992                 }
2993                 print "</td>\n";
2994                 print "<td class=\"link\">";
2995                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
2996                                              file_name=>"$basedir$t->{'name'}", %base_key)},
2997                               "blob");
2998                 if ($have_blame) {
2999                         print " | " .
3000                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3001                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3002                                       "blame");
3003                 }
3004                 if (defined $hash_base) {
3005                         print " | " .
3006                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3007                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3008                                       "history");
3009                 }
3010                 print " | " .
3011                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3012                                                file_name=>"$basedir$t->{'name'}")},
3013                                 "raw");
3014                 print "</td>\n";
3016         } elsif ($t->{'type'} eq "tree") {
3017                 print "<td class=\"list\">";
3018                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3019                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3020                               esc_path($t->{'name'}));
3021                 print "</td>\n";
3022                 print "<td class=\"link\">";
3023                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3024                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3025                               "tree");
3026                 if (defined $hash_base) {
3027                         print " | " .
3028                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3029                                                      file_name=>"$basedir$t->{'name'}")},
3030                                       "history");
3031                 }
3032                 print "</td>\n";
3033         } else {
3034                 # unknown object: we can only present history for it
3035                 # (this includes 'commit' object, i.e. submodule support)
3036                 print "<td class=\"list\">" .
3037                       esc_path($t->{'name'}) .
3038                       "</td>\n";
3039                 print "<td class=\"link\">";
3040                 if (defined $hash_base) {
3041                         print $cgi->a({-href => href(action=>"history",
3042                                                      hash_base=>$hash_base,
3043                                                      file_name=>"$basedir$t->{'name'}")},
3044                                       "history");
3045                 }
3046                 print "</td>\n";
3047         }
3050 ## ......................................................................
3051 ## functions printing large fragments of HTML
3053 # get pre-image filenames for merge (combined) diff
3054 sub fill_from_file_info {
3055         my ($diff, @parents) = @_;
3057         $diff->{'from_file'} = [ ];
3058         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3059         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3060                 if ($diff->{'status'}[$i] eq 'R' ||
3061                     $diff->{'status'}[$i] eq 'C') {
3062                         $diff->{'from_file'}[$i] =
3063                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3064                 }
3065         }
3067         return $diff;
3070 # is current raw difftree line of file deletion
3071 sub is_deleted {
3072         my $diffinfo = shift;
3074         return $diffinfo->{'to_id'} eq ('0' x 40);
3077 # does patch correspond to [previous] difftree raw line
3078 # $diffinfo  - hashref of parsed raw diff format
3079 # $patchinfo - hashref of parsed patch diff format
3080 #              (the same keys as in $diffinfo)
3081 sub is_patch_split {
3082         my ($diffinfo, $patchinfo) = @_;
3084         return defined $diffinfo && defined $patchinfo
3085                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3089 sub git_difftree_body {
3090         my ($difftree, $hash, @parents) = @_;
3091         my ($parent) = $parents[0];
3092         my ($have_blame) = gitweb_check_feature('blame');
3093         print "<div class=\"list_head\">\n";
3094         if ($#{$difftree} > 10) {
3095                 print(($#{$difftree} + 1) . " files changed:\n");
3096         }
3097         print "</div>\n";
3099         print "<table class=\"" .
3100               (@parents > 1 ? "combined " : "") .
3101               "diff_tree\">\n";
3103         # header only for combined diff in 'commitdiff' view
3104         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3105         if ($has_header) {
3106                 # table header
3107                 print "<thead><tr>\n" .
3108                        "<th></th><th></th>\n"; # filename, patchN link
3109                 for (my $i = 0; $i < @parents; $i++) {
3110                         my $par = $parents[$i];
3111                         print "<th>" .
3112                               $cgi->a({-href => href(action=>"commitdiff",
3113                                                      hash=>$hash, hash_parent=>$par),
3114                                        -title => 'commitdiff to parent number ' .
3115                                                   ($i+1) . ': ' . substr($par,0,7)},
3116                                       $i+1) .
3117                               "&nbsp;</th>\n";
3118                 }
3119                 print "</tr></thead>\n<tbody>\n";
3120         }
3122         my $alternate = 1;
3123         my $patchno = 0;
3124         foreach my $line (@{$difftree}) {
3125                 my $diff = parsed_difftree_line($line);
3127                 if ($alternate) {
3128                         print "<tr class=\"dark\">\n";
3129                 } else {
3130                         print "<tr class=\"light\">\n";
3131                 }
3132                 $alternate ^= 1;
3134                 if (exists $diff->{'nparents'}) { # combined diff
3136                         fill_from_file_info($diff, @parents)
3137                                 unless exists $diff->{'from_file'};
3139                         if (!is_deleted($diff)) {
3140                                 # file exists in the result (child) commit
3141                                 print "<td>" .
3142                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3143                                                              file_name=>$diff->{'to_file'},
3144                                                              hash_base=>$hash),
3145                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3146                                       "</td>\n";
3147                         } else {
3148                                 print "<td>" .
3149                                       esc_path($diff->{'to_file'}) .
3150                                       "</td>\n";
3151                         }
3153                         if ($action eq 'commitdiff') {
3154                                 # link to patch
3155                                 $patchno++;
3156                                 print "<td class=\"link\">" .
3157                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3158                                       " | " .
3159                                       "</td>\n";
3160                         }
3162                         my $has_history = 0;
3163                         my $not_deleted = 0;
3164                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3165                                 my $hash_parent = $parents[$i];
3166                                 my $from_hash = $diff->{'from_id'}[$i];
3167                                 my $from_path = $diff->{'from_file'}[$i];
3168                                 my $status = $diff->{'status'}[$i];
3170                                 $has_history ||= ($status ne 'A');
3171                                 $not_deleted ||= ($status ne 'D');
3173                                 if ($status eq 'A') {
3174                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3175                                 } elsif ($status eq 'D') {
3176                                         print "<td class=\"link\">" .
3177                                               $cgi->a({-href => href(action=>"blob",
3178                                                                      hash_base=>$hash,
3179                                                                      hash=>$from_hash,
3180                                                                      file_name=>$from_path)},
3181                                                       "blob" . ($i+1)) .
3182                                               " | </td>\n";
3183                                 } else {
3184                                         if ($diff->{'to_id'} eq $from_hash) {
3185                                                 print "<td class=\"link nochange\">";
3186                                         } else {
3187                                                 print "<td class=\"link\">";
3188                                         }
3189                                         print $cgi->a({-href => href(action=>"blobdiff",
3190                                                                      hash=>$diff->{'to_id'},
3191                                                                      hash_parent=>$from_hash,
3192                                                                      hash_base=>$hash,
3193                                                                      hash_parent_base=>$hash_parent,
3194                                                                      file_name=>$diff->{'to_file'},
3195                                                                      file_parent=>$from_path)},
3196                                                       "diff" . ($i+1)) .
3197                                               " | </td>\n";
3198                                 }
3199                         }
3201                         print "<td class=\"link\">";
3202                         if ($not_deleted) {
3203                                 print $cgi->a({-href => href(action=>"blob",
3204                                                              hash=>$diff->{'to_id'},
3205                                                              file_name=>$diff->{'to_file'},
3206                                                              hash_base=>$hash)},
3207                                               "blob");
3208                                 print " | " if ($has_history);
3209                         }
3210                         if ($has_history) {
3211                                 print $cgi->a({-href => href(action=>"history",
3212                                                              file_name=>$diff->{'to_file'},
3213                                                              hash_base=>$hash)},
3214                                               "history");
3215                         }
3216                         print "</td>\n";
3218                         print "</tr>\n";
3219                         next; # instead of 'else' clause, to avoid extra indent
3220                 }
3221                 # else ordinary diff
3223                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3224                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3225                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3226                         $to_mode_oct = oct $diff->{'to_mode'};
3227                         if (S_ISREG($to_mode_oct)) { # only for regular file
3228                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3229                         }
3230                         $to_file_type = file_type($diff->{'to_mode'});
3231                 }
3232                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3233                         $from_mode_oct = oct $diff->{'from_mode'};
3234                         if (S_ISREG($to_mode_oct)) { # only for regular file
3235                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3236                         }
3237                         $from_file_type = file_type($diff->{'from_mode'});
3238                 }
3240                 if ($diff->{'status'} eq "A") { # created
3241                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3242                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3243                         $mode_chng   .= "]</span>";
3244                         print "<td>";
3245                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3246                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3247                                       -class => "list"}, esc_path($diff->{'file'}));
3248                         print "</td>\n";
3249                         print "<td>$mode_chng</td>\n";
3250                         print "<td class=\"link\">";
3251                         if ($action eq 'commitdiff') {
3252                                 # link to patch
3253                                 $patchno++;
3254                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3255                                 print " | ";
3256                         }
3257                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3258                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3259                                       "blob");
3260                         print "</td>\n";
3262                 } elsif ($diff->{'status'} eq "D") { # deleted
3263                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3264                         print "<td>";
3265                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3266                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3267                                        -class => "list"}, esc_path($diff->{'file'}));
3268                         print "</td>\n";
3269                         print "<td>$mode_chng</td>\n";
3270                         print "<td class=\"link\">";
3271                         if ($action eq 'commitdiff') {
3272                                 # link to patch
3273                                 $patchno++;
3274                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3275                                 print " | ";
3276                         }
3277                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3278                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3279                                       "blob") . " | ";
3280                         if ($have_blame) {
3281                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3282                                                              file_name=>$diff->{'file'})},
3283                                               "blame") . " | ";
3284                         }
3285                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3286                                                      file_name=>$diff->{'file'})},
3287                                       "history");
3288                         print "</td>\n";
3290                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3291                         my $mode_chnge = "";
3292                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3293                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3294                                 if ($from_file_type ne $to_file_type) {
3295                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3296                                 }
3297                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3298                                         if ($from_mode_str && $to_mode_str) {
3299                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3300                                         } elsif ($to_mode_str) {
3301                                                 $mode_chnge .= " mode: $to_mode_str";
3302                                         }
3303                                 }
3304                                 $mode_chnge .= "]</span>\n";
3305                         }
3306                         print "<td>";
3307                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3308                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3309                                       -class => "list"}, esc_path($diff->{'file'}));
3310                         print "</td>\n";
3311                         print "<td>$mode_chnge</td>\n";
3312                         print "<td class=\"link\">";
3313                         if ($action eq 'commitdiff') {
3314                                 # link to patch
3315                                 $patchno++;
3316                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3317                                       " | ";
3318                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3319                                 # "commit" view and modified file (not onlu mode changed)
3320                                 print $cgi->a({-href => href(action=>"blobdiff",
3321                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3322                                                              hash_base=>$hash, hash_parent_base=>$parent,
3323                                                              file_name=>$diff->{'file'})},
3324                                               "diff") .
3325                                       " | ";
3326                         }
3327                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3328                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3329                                        "blob") . " | ";
3330                         if ($have_blame) {
3331                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3332                                                              file_name=>$diff->{'file'})},
3333                                               "blame") . " | ";
3334                         }
3335                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3336                                                      file_name=>$diff->{'file'})},
3337                                       "history");
3338                         print "</td>\n";
3340                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3341                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3342                         my $nstatus = $status_name{$diff->{'status'}};
3343                         my $mode_chng = "";
3344                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3345                                 # mode also for directories, so we cannot use $to_mode_str
3346                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3347                         }
3348                         print "<td>" .
3349                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3350                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3351                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3352                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3353                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3354                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3355                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3356                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3357                               "<td class=\"link\">";
3358                         if ($action eq 'commitdiff') {
3359                                 # link to patch
3360                                 $patchno++;
3361                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3362                                       " | ";
3363                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3364                                 # "commit" view and modified file (not only pure rename or copy)
3365                                 print $cgi->a({-href => href(action=>"blobdiff",
3366                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3367                                                              hash_base=>$hash, hash_parent_base=>$parent,
3368                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3369                                               "diff") .
3370                                       " | ";
3371                         }
3372                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3373                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3374                                       "blob") . " | ";
3375                         if ($have_blame) {
3376                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3377                                                              file_name=>$diff->{'to_file'})},
3378                                               "blame") . " | ";
3379                         }
3380                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3381                                                     file_name=>$diff->{'to_file'})},
3382                                       "history");
3383                         print "</td>\n";
3385                 } # we should not encounter Unmerged (U) or Unknown (X) status
3386                 print "</tr>\n";
3387         }
3388         print "</tbody>" if $has_header;
3389         print "</table>\n";
3392 sub git_patchset_body {
3393         my ($fd, $difftree, $hash, @hash_parents) = @_;
3394         my ($hash_parent) = $hash_parents[0];
3396         my $is_combined = (@hash_parents > 1);
3397         my $patch_idx = 0;
3398         my $patch_number = 0;
3399         my $patch_line;
3400         my $diffinfo;
3401         my $to_name;
3402         my (%from, %to);
3404         print "<div class=\"patchset\">\n";
3406         # skip to first patch
3407         while ($patch_line = <$fd>) {
3408                 chomp $patch_line;
3410                 last if ($patch_line =~ m/^diff /);
3411         }
3413  PATCH:
3414         while ($patch_line) {
3416                 # parse "git diff" header line
3417                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3418                         # $1 is from_name, which we do not use
3419                         $to_name = unquote($2);
3420                         $to_name =~ s!^b/!!;
3421                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3422                         # $1 is 'cc' or 'combined', which we do not use
3423                         $to_name = unquote($2);
3424                 } else {
3425                         $to_name = undef;
3426                 }
3428                 # check if current patch belong to current raw line
3429                 # and parse raw git-diff line if needed
3430                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3431                         # this is continuation of a split patch
3432                         print "<div class=\"patch cont\">\n";
3433                 } else {
3434                         # advance raw git-diff output if needed
3435                         $patch_idx++ if defined $diffinfo;
3437                         # read and prepare patch information
3438                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3440                         # compact combined diff output can have some patches skipped
3441                         # find which patch (using pathname of result) we are at now;
3442                         if ($is_combined) {
3443                                 while ($to_name ne $diffinfo->{'to_file'}) {
3444                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3445                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3446                                               "</div>\n";  # class="patch"
3448                                         $patch_idx++;
3449                                         $patch_number++;
3451                                         last if $patch_idx > $#$difftree;
3452                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3453                                 }
3454                         }
3456                         # modifies %from, %to hashes
3457                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3459                         # this is first patch for raw difftree line with $patch_idx index
3460                         # we index @$difftree array from 0, but number patches from 1
3461                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3462                 }
3464                 # git diff header
3465                 #assert($patch_line =~ m/^diff /) if DEBUG;
3466                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3467                 $patch_number++;
3468                 # print "git diff" header
3469                 print format_git_diff_header_line($patch_line, $diffinfo,
3470                                                   \%from, \%to);
3472                 # print extended diff header
3473                 print "<div class=\"diff extended_header\">\n";
3474         EXTENDED_HEADER:
3475                 while ($patch_line = <$fd>) {
3476                         chomp $patch_line;
3478                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3480                         print format_extended_diff_header_line($patch_line, $diffinfo,
3481                                                                \%from, \%to);
3482                 }
3483                 print "</div>\n"; # class="diff extended_header"
3485                 # from-file/to-file diff header
3486                 if (! $patch_line) {
3487                         print "</div>\n"; # class="patch"
3488                         last PATCH;
3489                 }
3490                 next PATCH if ($patch_line =~ m/^diff /);
3491                 #assert($patch_line =~ m/^---/) if DEBUG;
3493                 my $last_patch_line = $patch_line;
3494                 $patch_line = <$fd>;
3495                 chomp $patch_line;
3496                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3498                 print format_diff_from_to_header($last_patch_line, $patch_line,
3499                                                  $diffinfo, \%from, \%to,
3500                                                  @hash_parents);
3502                 # the patch itself
3503         LINE:
3504                 while ($patch_line = <$fd>) {
3505                         chomp $patch_line;
3507                         next PATCH if ($patch_line =~ m/^diff /);
3509                         print format_diff_line($patch_line, \%from, \%to);
3510                 }
3512         } continue {
3513                 print "</div>\n"; # class="patch"
3514         }
3516         # for compact combined (--cc) format, with chunk and patch simpliciaction
3517         # patchset might be empty, but there might be unprocessed raw lines
3518         for (++$patch_idx if $patch_number > 0;
3519              $patch_idx < @$difftree;
3520              ++$patch_idx) {
3521                 # read and prepare patch information
3522                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3524                 # generate anchor for "patch" links in difftree / whatchanged part
3525                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3526                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3527                       "</div>\n";  # class="patch"
3529                 $patch_number++;
3530         }
3532         if ($patch_number == 0) {
3533                 if (@hash_parents > 1) {
3534                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3535                 } else {
3536                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3537                 }
3538         }
3540         print "</div>\n"; # class="patchset"
3543 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3545 # fills project list info (age, description, owner, forks) for each
3546 # project in the list, removing invalid projects from returned list
3547 # NOTE: modifies $projlist, but does not remove entries from it
3548 sub fill_project_list_info {
3549         my ($projlist, $check_forks) = @_;
3550         my @projects;
3552  PROJECT:
3553         foreach my $pr (@$projlist) {
3554                 my (@activity) = git_get_last_activity($pr->{'path'});
3555                 unless (@activity) {
3556                         next PROJECT;
3557                 }
3558                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3559                 if (!defined $pr->{'descr'}) {
3560                         my $descr = git_get_project_description($pr->{'path'}) || "";
3561                         $descr = to_utf8($descr);
3562                         $pr->{'descr_long'} = $descr;
3563                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3564                 }
3565                 if (!defined $pr->{'owner'}) {
3566                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3567                 }
3568                 if ($check_forks) {
3569                         my $pname = $pr->{'path'};
3570                         if (($pname =~ s/\.git$//) &&
3571                             ($pname !~ /\/$/) &&
3572                             (-d "$projectroot/$pname")) {
3573                                 $pr->{'forks'} = "-d $projectroot/$pname";
3574                         }       else {
3575                                 $pr->{'forks'} = 0;
3576                         }
3577                 }
3578                 push @projects, $pr;
3579         }
3581         return @projects;
3584 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3585 # (changing $list), or generating 'sort by $name' replay link otherwise
3586 sub print_sort_th {
3587         my ($str_sort, $name, $order, $key, $header, $list) = @_;
3588         $key    ||= $name;
3589         $header ||= ucfirst($name);
3591         if ($order eq $name) {
3592                 if ($str_sort) {
3593                         @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
3594                 } else {
3595                         @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
3596                 }
3597                 print "<th>$header</th>\n";
3598         } else {
3599                 print "<th>" .
3600                       $cgi->a({-href => href(-replay=>1, order=>$name),
3601                                -class => "header"}, $header) .
3602                       "</th>\n";
3603         }
3606 sub print_sort_th_str {
3607         print_sort_th(1, @_);
3610 sub print_sort_th_num {
3611         print_sort_th(0, @_);
3614 sub git_project_list_body {
3615         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3617         my ($check_forks) = gitweb_check_feature('forks');
3618         my @projects = fill_project_list_info($projlist, $check_forks);
3620         $order ||= $default_projects_order;
3621         $from = 0 unless defined $from;
3622         $to = $#projects if (!defined $to || $#projects < $to);
3624         print "<table class=\"project_list\">\n";
3625         unless ($no_header) {
3626                 print "<tr>\n";
3627                 if ($check_forks) {
3628                         print "<th></th>\n";
3629                 }
3630                 print_sort_th_str('project', $order, 'path',
3631                                   'Project', \@projects);
3632                 print_sort_th_str('descr', $order, 'descr_long',
3633                                   'Description', \@projects);
3634                 print_sort_th_str('owner', $order, 'owner',
3635                                   'Owner', \@projects);
3636                 print_sort_th_num('age', $order, 'age',
3637                                   'Last Change', \@projects);
3638                 print "<th></th>\n" . # for links
3639                       "</tr>\n";
3640         }
3641         my $alternate = 1;
3642         for (my $i = $from; $i <= $to; $i++) {
3643                 my $pr = $projects[$i];
3644                 if ($alternate) {
3645                         print "<tr class=\"dark\">\n";
3646                 } else {
3647                         print "<tr class=\"light\">\n";
3648                 }
3649                 $alternate ^= 1;
3650                 if ($check_forks) {
3651                         print "<td>";
3652                         if ($pr->{'forks'}) {
3653                                 print "<!-- $pr->{'forks'} -->\n";
3654                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3655                         }
3656                         print "</td>\n";
3657                 }
3658                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3659                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3660                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3661                                         -class => "list", -title => $pr->{'descr_long'}},
3662                                         esc_html($pr->{'descr'})) . "</td>\n" .
3663                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3664                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3665                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3666                       "<td class=\"link\">" .
3667                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3668                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3669                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3670                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3671                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3672                       "</td>\n" .
3673                       "</tr>\n";
3674         }
3675         if (defined $extra) {
3676                 print "<tr>\n";
3677                 if ($check_forks) {
3678                         print "<td></td>\n";
3679                 }
3680                 print "<td colspan=\"5\">$extra</td>\n" .
3681                       "</tr>\n";
3682         }
3683         print "</table>\n";
3686 sub git_shortlog_body {
3687         # uses global variable $project
3688         my ($commitlist, $from, $to, $refs, $extra) = @_;
3690         $from = 0 unless defined $from;
3691         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3693         print "<table class=\"shortlog\">\n";
3694         my $alternate = 1;
3695         for (my $i = $from; $i <= $to; $i++) {
3696                 my %co = %{$commitlist->[$i]};
3697                 my $commit = $co{'id'};
3698                 my $ref = format_ref_marker($refs, $commit);
3699                 if ($alternate) {
3700                         print "<tr class=\"dark\">\n";
3701                 } else {
3702                         print "<tr class=\"light\">\n";
3703                 }
3704                 $alternate ^= 1;
3705                 my $author = chop_and_escape_str($co{'author_name'}, 10);
3706                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3707                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3708                       "<td><i>" . $author . "</i></td>\n" .
3709                       "<td>";
3710                 print format_subject_html($co{'title'}, $co{'title_short'},
3711                                           href(action=>"commit", hash=>$commit), $ref);
3712                 print "</td>\n" .
3713                       "<td class=\"link\">" .
3714                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3715                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3716                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3717                 my $snapshot_links = format_snapshot_links($commit);
3718                 if (defined $snapshot_links) {
3719                         print " | " . $snapshot_links;
3720                 }
3721                 print "</td>\n" .
3722                       "</tr>\n";
3723         }
3724         if (defined $extra) {
3725                 print "<tr>\n" .
3726                       "<td colspan=\"4\">$extra</td>\n" .
3727                       "</tr>\n";
3728         }
3729         print "</table>\n";
3732 sub git_history_body {
3733         # Warning: assumes constant type (blob or tree) during history
3734         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3736         $from = 0 unless defined $from;
3737         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3739         print "<table class=\"history\">\n";
3740         my $alternate = 1;
3741         for (my $i = $from; $i <= $to; $i++) {
3742                 my %co = %{$commitlist->[$i]};
3743                 if (!%co) {
3744                         next;
3745                 }
3746                 my $commit = $co{'id'};
3748                 my $ref = format_ref_marker($refs, $commit);
3750                 if ($alternate) {
3751                         print "<tr class=\"dark\">\n";
3752                 } else {
3753                         print "<tr class=\"light\">\n";
3754                 }
3755                 $alternate ^= 1;
3756         # shortlog uses      chop_str($co{'author_name'}, 10)
3757                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3758                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3759                       "<td><i>" . $author . "</i></td>\n" .
3760                       "<td>";
3761                 # originally git_history used chop_str($co{'title'}, 50)
3762                 print format_subject_html($co{'title'}, $co{'title_short'},
3763                                           href(action=>"commit", hash=>$commit), $ref);
3764                 print "</td>\n" .
3765                       "<td class=\"link\">" .
3766                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3767                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3769                 if ($ftype eq 'blob') {
3770                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3771                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3772                         if (defined $blob_current && defined $blob_parent &&
3773                                         $blob_current ne $blob_parent) {
3774                                 print " | " .
3775                                         $cgi->a({-href => href(action=>"blobdiff",
3776                                                                hash=>$blob_current, hash_parent=>$blob_parent,
3777                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
3778                                                                file_name=>$file_name)},
3779                                                 "diff to current");
3780                         }
3781                 }
3782                 print "</td>\n" .
3783                       "</tr>\n";
3784         }
3785         if (defined $extra) {
3786                 print "<tr>\n" .
3787                       "<td colspan=\"4\">$extra</td>\n" .
3788                       "</tr>\n";
3789         }
3790         print "</table>\n";
3793 sub git_tags_body {
3794         # uses global variable $project
3795         my ($taglist, $from, $to, $extra) = @_;
3796         $from = 0 unless defined $from;
3797         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3799         print "<table class=\"tags\">\n";
3800         my $alternate = 1;
3801         for (my $i = $from; $i <= $to; $i++) {
3802                 my $entry = $taglist->[$i];
3803                 my %tag = %$entry;
3804                 my $comment = $tag{'subject'};
3805                 my $comment_short;
3806                 if (defined $comment) {
3807                         $comment_short = chop_str($comment, 30, 5);
3808                 }
3809                 if ($alternate) {
3810                         print "<tr class=\"dark\">\n";
3811                 } else {
3812                         print "<tr class=\"light\">\n";
3813                 }
3814                 $alternate ^= 1;
3815                 if (defined $tag{'age'}) {
3816                         print "<td><i>$tag{'age'}</i></td>\n";
3817                 } else {
3818                         print "<td></td>\n";
3819                 }
3820                 print "<td>" .
3821                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3822                                -class => "list name"}, esc_html($tag{'name'})) .
3823                       "</td>\n" .
3824                       "<td>";
3825                 if (defined $comment) {
3826                         print format_subject_html($comment, $comment_short,
3827                                                   href(action=>"tag", hash=>$tag{'id'}));
3828                 }
3829                 print "</td>\n" .
3830                       "<td class=\"selflink\">";
3831                 if ($tag{'type'} eq "tag") {
3832                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3833                 } else {
3834                         print "&nbsp;";
3835                 }
3836                 print "</td>\n" .
3837                       "<td class=\"link\">" . " | " .
3838                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3839                 if ($tag{'reftype'} eq "commit") {
3840                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3841                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3842                 } elsif ($tag{'reftype'} eq "blob") {
3843                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3844                 }
3845                 print "</td>\n" .
3846                       "</tr>";
3847         }
3848         if (defined $extra) {
3849                 print "<tr>\n" .
3850                       "<td colspan=\"5\">$extra</td>\n" .
3851                       "</tr>\n";
3852         }
3853         print "</table>\n";
3856 sub git_heads_body {
3857         # uses global variable $project
3858         my ($headlist, $head, $from, $to, $extra) = @_;
3859         $from = 0 unless defined $from;
3860         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3862         print "<table class=\"heads\">\n";
3863         my $alternate = 1;
3864         for (my $i = $from; $i <= $to; $i++) {
3865                 my $entry = $headlist->[$i];
3866                 my %ref = %$entry;
3867                 my $curr = $ref{'id'} eq $head;
3868                 if ($alternate) {
3869                         print "<tr class=\"dark\">\n";
3870                 } else {
3871                         print "<tr class=\"light\">\n";
3872                 }
3873                 $alternate ^= 1;
3874                 print "<td><i>$ref{'age'}</i></td>\n" .
3875                       ($curr ? "<td class=\"current_head\">" : "<td>") .
3876                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
3877                                -class => "list name"},esc_html($ref{'name'})) .
3878                       "</td>\n" .
3879                       "<td class=\"link\">" .
3880                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
3881                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
3882                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
3883                       "</td>\n" .
3884                       "</tr>";
3885         }
3886         if (defined $extra) {
3887                 print "<tr>\n" .
3888                       "<td colspan=\"3\">$extra</td>\n" .
3889                       "</tr>\n";
3890         }
3891         print "</table>\n";
3894 sub git_search_grep_body {
3895         my ($commitlist, $from, $to, $extra) = @_;
3896         $from = 0 unless defined $from;
3897         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3899         print "<table class=\"commit_search\">\n";
3900         my $alternate = 1;
3901         for (my $i = $from; $i <= $to; $i++) {
3902                 my %co = %{$commitlist->[$i]};
3903                 if (!%co) {
3904                         next;
3905                 }
3906                 my $commit = $co{'id'};
3907                 if ($alternate) {
3908                         print "<tr class=\"dark\">\n";
3909                 } else {
3910                         print "<tr class=\"light\">\n";
3911                 }
3912                 $alternate ^= 1;
3913                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
3914                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3915                       "<td><i>" . $author . "</i></td>\n" .
3916                       "<td>" .
3917                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3918                                -class => "list subject"},
3919                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
3920                 my $comment = $co{'comment'};
3921                 foreach my $line (@$comment) {
3922                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3923                                 my ($lead, $match, $trail) = ($1, $2, $3);
3924                                 $match = chop_str($match, 70, 5, 'center');
3925                                 my $contextlen = int((80 - length($match))/2);
3926                                 $contextlen = 30 if ($contextlen > 30);
3927                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
3928                                 $trail = chop_str($trail, $contextlen, 10, 'right');
3930                                 $lead  = esc_html($lead);
3931                                 $match = esc_html($match);
3932                                 $trail = esc_html($trail);
3934                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
3935                         }
3936                 }
3937                 print "</td>\n" .
3938                       "<td class=\"link\">" .
3939                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3940                       " | " .
3941                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
3942                       " | " .
3943                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3944                 print "</td>\n" .
3945                       "</tr>\n";
3946         }
3947         if (defined $extra) {
3948                 print "<tr>\n" .
3949                       "<td colspan=\"3\">$extra</td>\n" .
3950                       "</tr>\n";
3951         }
3952         print "</table>\n";
3955 ## ======================================================================
3956 ## ======================================================================
3957 ## actions
3959 sub git_project_list {
3960         my $order = $cgi->param('o');
3961         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3962                 die_error(400, "Unknown order parameter");
3963         }
3965         my @list = git_get_projects_list();
3966         if (!@list) {
3967                 die_error(404, "No projects found");
3968         }
3970         git_header_html();
3971         if (-f $home_text) {
3972                 print "<div class=\"index_include\">\n";
3973                 open (my $fd, $home_text);
3974                 print <$fd>;
3975                 close $fd;
3976                 print "</div>\n";
3977         }
3978         git_project_list_body(\@list, $order);
3979         git_footer_html();
3982 sub git_forks {
3983         my $order = $cgi->param('o');
3984         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3985                 die_error(400, "Unknown order parameter");
3986         }
3988         my @list = git_get_projects_list($project);
3989         if (!@list) {
3990                 die_error(404, "No forks found");
3991         }
3993         git_header_html();
3994         git_print_page_nav('','');
3995         git_print_header_div('summary', "$project forks");
3996         git_project_list_body(\@list, $order);
3997         git_footer_html();
4000 sub git_project_index {
4001         my @projects = git_get_projects_list($project);
4003         print $cgi->header(
4004                 -type => 'text/plain',
4005                 -charset => 'utf-8',
4006                 -content_disposition => 'inline; filename="index.aux"');
4008         foreach my $pr (@projects) {
4009                 if (!exists $pr->{'owner'}) {
4010                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4011                 }
4013                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4014                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4015                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4016                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4017                 $path  =~ s/ /\+/g;
4018                 $owner =~ s/ /\+/g;
4020                 print "$path $owner\n";
4021         }
4024 sub git_summary {
4025         my $descr = git_get_project_description($project) || "none";
4026         my %co = parse_commit("HEAD");
4027         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4028         my $head = $co{'id'};
4030         my $owner = git_get_project_owner($project);
4032         my $refs = git_get_references();
4033         # These get_*_list functions return one more to allow us to see if
4034         # there are more ...
4035         my @taglist  = git_get_tags_list(16);
4036         my @headlist = git_get_heads_list(16);
4037         my @forklist;
4038         my ($check_forks) = gitweb_check_feature('forks');
4040         if ($check_forks) {
4041                 @forklist = git_get_projects_list($project);
4042         }
4044         git_header_html();
4045         git_print_page_nav('summary','', $head);
4047         print "<div class=\"title\">&nbsp;</div>\n";
4048         print "<table class=\"projects_list\">\n" .
4049               "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4050               "<tr><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4051         if (defined $cd{'rfc2822'}) {
4052                 print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4053         }
4055         # use per project git URL list in $projectroot/$project/cloneurl
4056         # or make project git URL from git base URL and project name
4057         my $url_tag = "URL";
4058         my @url_list = git_get_project_url_list($project);
4059         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4060         foreach my $git_url (@url_list) {
4061                 next unless $git_url;
4062                 print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
4063                 $url_tag = "";
4064         }
4065         print "</table>\n";
4067         if (-s "$projectroot/$project/README.html") {
4068                 if (open my $fd, "$projectroot/$project/README.html") {
4069                         print "<div class=\"title\">readme</div>\n" .
4070                               "<div class=\"readme\">\n";
4071                         print $_ while (<$fd>);
4072                         print "\n</div>\n"; # class="readme"
4073                         close $fd;
4074                 }
4075         }
4077         # we need to request one more than 16 (0..15) to check if
4078         # those 16 are all
4079         my @commitlist = $head ? parse_commits($head, 17) : ();
4080         if (@commitlist) {
4081                 git_print_header_div('shortlog');
4082                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4083                                   $#commitlist <=  15 ? undef :
4084                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4085         }
4087         if (@taglist) {
4088                 git_print_header_div('tags');
4089                 git_tags_body(\@taglist, 0, 15,
4090                               $#taglist <=  15 ? undef :
4091                               $cgi->a({-href => href(action=>"tags")}, "..."));
4092         }
4094         if (@headlist) {
4095                 git_print_header_div('heads');
4096                 git_heads_body(\@headlist, $head, 0, 15,
4097                                $#headlist <= 15 ? undef :
4098                                $cgi->a({-href => href(action=>"heads")}, "..."));
4099         }
4101         if (@forklist) {
4102                 git_print_header_div('forks');
4103                 git_project_list_body(\@forklist, undef, 0, 15,
4104                                       $#forklist <= 15 ? undef :
4105                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4106                                       'noheader');
4107         }
4109         git_footer_html();
4112 sub git_tag {
4113         my $head = git_get_head_hash($project);
4114         git_header_html();
4115         git_print_page_nav('','', $head,undef,$head);
4116         my %tag = parse_tag($hash);
4118         if (! %tag) {
4119                 die_error(404, "Unknown tag object");
4120         }
4122         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4123         print "<div class=\"title_text\">\n" .
4124               "<table class=\"object_header\">\n" .
4125               "<tr>\n" .
4126               "<td>object</td>\n" .
4127               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4128                                $tag{'object'}) . "</td>\n" .
4129               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4130                                               $tag{'type'}) . "</td>\n" .
4131               "</tr>\n";
4132         if (defined($tag{'author'})) {
4133                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4134                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4135                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4136                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4137                         "</td></tr>\n";
4138         }
4139         print "</table>\n\n" .
4140               "</div>\n";
4141         print "<div class=\"page_body\">";
4142         my $comment = $tag{'comment'};
4143         foreach my $line (@$comment) {
4144                 chomp $line;
4145                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4146         }
4147         print "</div>\n";
4148         git_footer_html();
4151 sub git_blame {
4152         my $fd;
4153         my $ftype;
4155         gitweb_check_feature('blame')
4156             or die_error(403, "Blame view not allowed");
4158         die_error(400, "No file name given") unless $file_name;
4159         $hash_base ||= git_get_head_hash($project);
4160         die_error(404, "Couldn't find base commit") unless ($hash_base);
4161         my %co = parse_commit($hash_base)
4162                 or die_error(404, "Commit not found");
4163         if (!defined $hash) {
4164                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4165                         or die_error(404, "Error looking up file");
4166         }
4167         $ftype = git_get_type($hash);
4168         if ($ftype !~ "blob") {
4169                 die_error(400, "Object is not a blob");
4170         }
4171         open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4172               $file_name, $hash_base)
4173                 or die_error(500, "Open git-blame failed");
4174         git_header_html();
4175         my $formats_nav =
4176                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4177                         "blob") .
4178                 " | " .
4179                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4180                         "history") .
4181                 " | " .
4182                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4183                         "HEAD");
4184         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4185         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4186         git_print_page_path($file_name, $ftype, $hash_base);
4187         my @rev_color = (qw(light2 dark2));
4188         my $num_colors = scalar(@rev_color);
4189         my $current_color = 0;
4190         my $last_rev;
4191         print <<HTML;
4192 <div class="page_body">
4193 <table class="blame">
4194 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4195 HTML
4196         my %metainfo = ();
4197         while (1) {
4198                 $_ = <$fd>;
4199                 last unless defined $_;
4200                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4201                     /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4202                 if (!exists $metainfo{$full_rev}) {
4203                         $metainfo{$full_rev} = {};
4204                 }
4205                 my $meta = $metainfo{$full_rev};
4206                 while (<$fd>) {
4207                         last if (s/^\t//);
4208                         if (/^(\S+) (.*)$/) {
4209                                 $meta->{$1} = $2;
4210                         }
4211                 }
4212                 my $data = $_;
4213                 chomp $data;
4214                 my $rev = substr($full_rev, 0, 8);
4215                 my $author = $meta->{'author'};
4216                 my %date = parse_date($meta->{'author-time'},
4217                                       $meta->{'author-tz'});
4218                 my $date = $date{'iso-tz'};
4219                 if ($group_size) {
4220                         $current_color = ++$current_color % $num_colors;
4221                 }
4222                 print "<tr class=\"$rev_color[$current_color]\">\n";
4223                 if ($group_size) {
4224                         print "<td class=\"sha1\"";
4225                         print " title=\"". esc_html($author) . ", $date\"";
4226                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4227                         print ">";
4228                         print $cgi->a({-href => href(action=>"commit",
4229                                                      hash=>$full_rev,
4230                                                      file_name=>$file_name)},
4231                                       esc_html($rev));
4232                         print "</td>\n";
4233                 }
4234                 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4235                         or die_error(500, "Open git-rev-parse failed");
4236                 my $parent_commit = <$dd>;
4237                 close $dd;
4238                 chomp($parent_commit);
4239                 my $blamed = href(action => 'blame',
4240                                   file_name => $meta->{'filename'},
4241                                   hash_base => $parent_commit);
4242                 print "<td class=\"linenr\">";
4243                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4244                                 -id => "l$lineno",
4245                                 -class => "linenr" },
4246                               esc_html($lineno));
4247                 print "</td>";
4248                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4249                 print "</tr>\n";
4250         }
4251         print "</table>\n";
4252         print "</div>";
4253         close $fd
4254                 or print "Reading blob failed\n";
4255         git_footer_html();
4258 sub git_tags {
4259         my $head = git_get_head_hash($project);
4260         git_header_html();
4261         git_print_page_nav('','', $head,undef,$head);
4262         git_print_header_div('summary', $project);
4264         my @tagslist = git_get_tags_list();
4265         if (@tagslist) {
4266                 git_tags_body(\@tagslist);
4267         }
4268         git_footer_html();
4271 sub git_heads {
4272         my $head = git_get_head_hash($project);
4273         git_header_html();
4274         git_print_page_nav('','', $head,undef,$head);
4275         git_print_header_div('summary', $project);
4277         my @headslist = git_get_heads_list();
4278         if (@headslist) {
4279                 git_heads_body(\@headslist, $head);
4280         }
4281         git_footer_html();
4284 sub git_blob_plain {
4285         my $type = shift;
4286         my $expires;
4288         if (!defined $hash) {
4289                 if (defined $file_name) {
4290                         my $base = $hash_base || git_get_head_hash($project);
4291                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4292                                 or die_error(404, "Cannot find file");
4293                 } else {
4294                         die_error(400, "No file name defined");
4295                 }
4296         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4297                 # blobs defined by non-textual hash id's can be cached
4298                 $expires = "+1d";
4299         }
4301         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4302                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4304         # content-type (can include charset)
4305         $type = blob_contenttype($fd, $file_name, $type);
4307         # "save as" filename, even when no $file_name is given
4308         my $save_as = "$hash";
4309         if (defined $file_name) {
4310                 $save_as = $file_name;
4311         } elsif ($type =~ m/^text\//) {
4312                 $save_as .= '.txt';
4313         }
4315         print $cgi->header(
4316                 -type => $type,
4317                 -expires => $expires,
4318                 -content_disposition => 'inline; filename="' . $save_as . '"');
4319         undef $/;
4320         binmode STDOUT, ':raw';
4321         print <$fd>;
4322         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4323         $/ = "\n";
4324         close $fd;
4327 sub git_blob {
4328         my $expires;
4330         if (!defined $hash) {
4331                 if (defined $file_name) {
4332                         my $base = $hash_base || git_get_head_hash($project);
4333                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4334                                 or die_error(404, "Cannot find file");
4335                 } else {
4336                         die_error(400, "No file name defined");
4337                 }
4338         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4339                 # blobs defined by non-textual hash id's can be cached
4340                 $expires = "+1d";
4341         }
4343         my ($have_blame) = gitweb_check_feature('blame');
4344         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4345                 or die_error(500, "Couldn't cat $file_name, $hash");
4346         my $mimetype = blob_mimetype($fd, $file_name);
4347         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4348                 close $fd;
4349                 return git_blob_plain($mimetype);
4350         }
4351         # we can have blame only for text/* mimetype
4352         $have_blame &&= ($mimetype =~ m!^text/!);
4354         git_header_html(undef, $expires);
4355         my $formats_nav = '';
4356         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4357                 if (defined $file_name) {
4358                         if ($have_blame) {
4359                                 $formats_nav .=
4360                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
4361                                                 "blame") .
4362                                         " | ";
4363                         }
4364                         $formats_nav .=
4365                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4366                                         "history") .
4367                                 " | " .
4368                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4369                                         "raw") .
4370                                 " | " .
4371                                 $cgi->a({-href => href(action=>"blob",
4372                                                        hash_base=>"HEAD", file_name=>$file_name)},
4373                                         "HEAD");
4374                 } else {
4375                         $formats_nav .=
4376                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4377                                         "raw");
4378                 }
4379                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4380                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4381         } else {
4382                 print "<div class=\"page_nav\">\n" .
4383                       "<br/><br/></div>\n" .
4384                       "<div class=\"title\">$hash</div>\n";
4385         }
4386         git_print_page_path($file_name, "blob", $hash_base);
4387         print "<div class=\"page_body\">\n";
4388         if ($mimetype =~ m!^image/!) {
4389                 print qq!<img type="$mimetype"!;
4390                 if ($file_name) {
4391                         print qq! alt="$file_name" title="$file_name"!;
4392                 }
4393                 print qq! src="! .
4394                       href(action=>"blob_plain", hash=>$hash,
4395                            hash_base=>$hash_base, file_name=>$file_name) .
4396                       qq!" />\n!;
4397         } else {
4398                 my $nr;
4399                 while (my $line = <$fd>) {
4400                         chomp $line;
4401                         $nr++;
4402                         $line = untabify($line);
4403                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4404                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4405                 }
4406         }
4407         close $fd
4408                 or print "Reading blob failed.\n";
4409         print "</div>";
4410         git_footer_html();
4413 sub git_tree {
4414         if (!defined $hash_base) {
4415                 $hash_base = "HEAD";
4416         }
4417         if (!defined $hash) {
4418                 if (defined $file_name) {
4419                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4420                 } else {
4421                         $hash = $hash_base;
4422                 }
4423         }
4424         die_error(404, "No such tree") unless defined($hash);
4425         $/ = "\0";
4426         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4427                 or die_error(500, "Open git-ls-tree failed");
4428         my @entries = map { chomp; $_ } <$fd>;
4429         close $fd or die_error(404, "Reading tree failed");
4430         $/ = "\n";
4432         my $refs = git_get_references();
4433         my $ref = format_ref_marker($refs, $hash_base);
4434         git_header_html();
4435         my $basedir = '';
4436         my ($have_blame) = gitweb_check_feature('blame');
4437         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4438                 my @views_nav = ();
4439                 if (defined $file_name) {
4440                         push @views_nav,
4441                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4442                                         "history"),
4443                                 $cgi->a({-href => href(action=>"tree",
4444                                                        hash_base=>"HEAD", file_name=>$file_name)},
4445                                         "HEAD"),
4446                 }
4447                 my $snapshot_links = format_snapshot_links($hash);
4448                 if (defined $snapshot_links) {
4449                         # FIXME: Should be available when we have no hash base as well.
4450                         push @views_nav, $snapshot_links;
4451                 }
4452                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4453                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4454         } else {
4455                 undef $hash_base;
4456                 print "<div class=\"page_nav\">\n";
4457                 print "<br/><br/></div>\n";
4458                 print "<div class=\"title\">$hash</div>\n";
4459         }
4460         if (defined $file_name) {
4461                 $basedir = $file_name;
4462                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4463                         $basedir .= '/';
4464                 }
4465                 git_print_page_path($file_name, 'tree', $hash_base);
4466         }
4467         print "<div class=\"page_body\">\n";
4468         print "<table class=\"tree\">\n";
4469         my $alternate = 1;
4470         # '..' (top directory) link if possible
4471         if (defined $hash_base &&
4472             defined $file_name && $file_name =~ m![^/]+$!) {
4473                 if ($alternate) {
4474                         print "<tr class=\"dark\">\n";
4475                 } else {
4476                         print "<tr class=\"light\">\n";
4477                 }
4478                 $alternate ^= 1;
4480                 my $up = $file_name;
4481                 $up =~ s!/?[^/]+$!!;
4482                 undef $up unless $up;
4483                 # based on git_print_tree_entry
4484                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4485                 print '<td class="list">';
4486                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4487                                              file_name=>$up)},
4488                               "..");
4489                 print "</td>\n";
4490                 print "<td class=\"link\"></td>\n";
4492                 print "</tr>\n";
4493         }
4494         foreach my $line (@entries) {
4495                 my %t = parse_ls_tree_line($line, -z => 1);
4497                 if ($alternate) {
4498                         print "<tr class=\"dark\">\n";
4499                 } else {
4500                         print "<tr class=\"light\">\n";
4501                 }
4502                 $alternate ^= 1;
4504                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4506                 print "</tr>\n";
4507         }
4508         print "</table>\n" .
4509               "</div>";
4510         git_footer_html();
4513 sub git_snapshot {
4514         my @supported_fmts = gitweb_check_feature('snapshot');
4515         @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4517         my $format = $cgi->param('sf');
4518         if (!@supported_fmts) {
4519                 die_error(403, "Snapshots not allowed");
4520         }
4521         # default to first supported snapshot format
4522         $format ||= $supported_fmts[0];
4523         if ($format !~ m/^[a-z0-9]+$/) {
4524                 die_error(400, "Invalid snapshot format parameter");
4525         } elsif (!exists($known_snapshot_formats{$format})) {
4526                 die_error(400, "Unknown snapshot format");
4527         } elsif (!grep($_ eq $format, @supported_fmts)) {
4528                 die_error(403, "Unsupported snapshot format");
4529         }
4531         if (!defined $hash) {
4532                 $hash = git_get_head_hash($project);
4533         }
4535         my $name = $project;
4536         $name =~ s,([^/])/*\.git$,$1,;
4537         $name = basename($name);
4538         my $filename = to_utf8($name);
4539         $name =~ s/\047/\047\\\047\047/g;
4540         my $cmd;
4541         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4542         $cmd = quote_command(
4543                 git_cmd(), 'archive',
4544                 "--format=$known_snapshot_formats{$format}{'format'}",
4545                 "--prefix=$name/", $hash);
4546         if (exists $known_snapshot_formats{$format}{'compressor'}) {
4547                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4548         }
4550         print $cgi->header(
4551                 -type => $known_snapshot_formats{$format}{'type'},
4552                 -content_disposition => 'inline; filename="' . "$filename" . '"',
4553                 -status => '200 OK');
4555         open my $fd, "-|", $cmd
4556                 or die_error(500, "Execute git-archive failed");
4557         binmode STDOUT, ':raw';
4558         print <$fd>;
4559         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4560         close $fd;
4563 sub git_log {
4564         my $head = git_get_head_hash($project);
4565         if (!defined $hash) {
4566                 $hash = $head;
4567         }
4568         if (!defined $page) {
4569                 $page = 0;
4570         }
4571         my $refs = git_get_references();
4573         my @commitlist = parse_commits($hash, 101, (100 * $page));
4575         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4577         git_header_html();
4578         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4580         if (!@commitlist) {
4581                 my %co = parse_commit($hash);
4583                 git_print_header_div('summary', $project);
4584                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4585         }
4586         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4587         for (my $i = 0; $i <= $to; $i++) {
4588                 my %co = %{$commitlist[$i]};
4589                 next if !%co;
4590                 my $commit = $co{'id'};
4591                 my $ref = format_ref_marker($refs, $commit);
4592                 my %ad = parse_date($co{'author_epoch'});
4593                 git_print_header_div('commit',
4594                                "<span class=\"age\">$co{'age_string'}</span>" .
4595                                esc_html($co{'title'}) . $ref,
4596                                $commit);
4597                 print "<div class=\"title_text\">\n" .
4598                       "<div class=\"log_link\">\n" .
4599                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4600                       " | " .
4601                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4602                       " | " .
4603                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4604                       "<br/>\n" .
4605                       "</div>\n" .
4606                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4607                       "</div>\n";
4609                 print "<div class=\"log_body\">\n";
4610                 git_print_log($co{'comment'}, -final_empty_line=> 1);
4611                 print "</div>\n";
4612         }
4613         if ($#commitlist >= 100) {
4614                 print "<div class=\"page_nav\">\n";
4615                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4616                                -accesskey => "n", -title => "Alt-n"}, "next");
4617                 print "</div>\n";
4618         }
4619         git_footer_html();
4622 sub git_commit {
4623         $hash ||= $hash_base || "HEAD";
4624         my %co = parse_commit($hash)
4625             or die_error(404, "Unknown commit object");
4626         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4627         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4629         my $parent  = $co{'parent'};
4630         my $parents = $co{'parents'}; # listref
4632         # we need to prepare $formats_nav before any parameter munging
4633         my $formats_nav;
4634         if (!defined $parent) {
4635                 # --root commitdiff
4636                 $formats_nav .= '(initial)';
4637         } elsif (@$parents == 1) {
4638                 # single parent commit
4639                 $formats_nav .=
4640                         '(parent: ' .
4641                         $cgi->a({-href => href(action=>"commit",
4642                                                hash=>$parent)},
4643                                 esc_html(substr($parent, 0, 7))) .
4644                         ')';
4645         } else {
4646                 # merge commit
4647                 $formats_nav .=
4648                         '(merge: ' .
4649                         join(' ', map {
4650                                 $cgi->a({-href => href(action=>"commit",
4651                                                        hash=>$_)},
4652                                         esc_html(substr($_, 0, 7)));
4653                         } @$parents ) .
4654                         ')';
4655         }
4657         if (!defined $parent) {
4658                 $parent = "--root";
4659         }
4660         my @difftree;
4661         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4662                 @diff_opts,
4663                 (@$parents <= 1 ? $parent : '-c'),
4664                 $hash, "--"
4665                 or die_error(500, "Open git-diff-tree failed");
4666         @difftree = map { chomp; $_ } <$fd>;
4667         close $fd or die_error(404, "Reading git-diff-tree failed");
4669         # non-textual hash id's can be cached
4670         my $expires;
4671         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4672                 $expires = "+1d";
4673         }
4674         my $refs = git_get_references();
4675         my $ref = format_ref_marker($refs, $co{'id'});
4677         git_header_html(undef, $expires);
4678         git_print_page_nav('commit', '',
4679                            $hash, $co{'tree'}, $hash,
4680                            $formats_nav);
4682         if (defined $co{'parent'}) {
4683                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4684         } else {
4685                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4686         }
4687         print "<div class=\"title_text\">\n" .
4688               "<table class=\"object_header\">\n";
4689         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4690               "<tr>" .
4691               "<td></td><td> $ad{'rfc2822'}";
4692         if ($ad{'hour_local'} < 6) {
4693                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4694                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4695         } else {
4696                 printf(" (%02d:%02d %s)",
4697                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4698         }
4699         print "</td>" .
4700               "</tr>\n";
4701         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4702         print "<tr><td></td><td> $cd{'rfc2822'}" .
4703               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4704               "</td></tr>\n";
4705         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4706         print "<tr>" .
4707               "<td>tree</td>" .
4708               "<td class=\"sha1\">" .
4709               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4710                        class => "list"}, $co{'tree'}) .
4711               "</td>" .
4712               "<td class=\"link\">" .
4713               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4714                       "tree");
4715         my $snapshot_links = format_snapshot_links($hash);
4716         if (defined $snapshot_links) {
4717                 print " | " . $snapshot_links;
4718         }
4719         print "</td>" .
4720               "</tr>\n";
4722         foreach my $par (@$parents) {
4723                 print "<tr>" .
4724                       "<td>parent</td>" .
4725                       "<td class=\"sha1\">" .
4726                       $cgi->a({-href => href(action=>"commit", hash=>$par),
4727                                class => "list"}, $par) .
4728                       "</td>" .
4729                       "<td class=\"link\">" .
4730                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4731                       " | " .
4732                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4733                       "</td>" .
4734                       "</tr>\n";
4735         }
4736         print "</table>".
4737               "</div>\n";
4739         print "<div class=\"page_body\">\n";
4740         git_print_log($co{'comment'});
4741         print "</div>\n";
4743         git_difftree_body(\@difftree, $hash, @$parents);
4745         git_footer_html();
4748 sub git_object {
4749         # object is defined by:
4750         # - hash or hash_base alone
4751         # - hash_base and file_name
4752         my $type;
4754         # - hash or hash_base alone
4755         if ($hash || ($hash_base && !defined $file_name)) {
4756                 my $object_id = $hash || $hash_base;
4758                 open my $fd, "-|", quote_command(
4759                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4760                         or die_error(404, "Object does not exist");
4761                 $type = <$fd>;
4762                 chomp $type;
4763                 close $fd
4764                         or die_error(404, "Object does not exist");
4766         # - hash_base and file_name
4767         } elsif ($hash_base && defined $file_name) {
4768                 $file_name =~ s,/+$,,;
4770                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4771                         or die_error(404, "Base object does not exist");
4773                 # here errors should not hapen
4774                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4775                         or die_error(500, "Open git-ls-tree failed");
4776                 my $line = <$fd>;
4777                 close $fd;
4779                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4780                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4781                         die_error(404, "File or directory for given base does not exist");
4782                 }
4783                 $type = $2;
4784                 $hash = $3;
4785         } else {
4786                 die_error(400, "Not enough information to find object");
4787         }
4789         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4790                                           hash=>$hash, hash_base=>$hash_base,
4791                                           file_name=>$file_name),
4792                              -status => '302 Found');
4795 sub git_blobdiff {
4796         my $format = shift || 'html';
4798         my $fd;
4799         my @difftree;
4800         my %diffinfo;
4801         my $expires;
4803         # preparing $fd and %diffinfo for git_patchset_body
4804         # new style URI
4805         if (defined $hash_base && defined $hash_parent_base) {
4806                 if (defined $file_name) {
4807                         # read raw output
4808                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4809                                 $hash_parent_base, $hash_base,
4810                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
4811                                 or die_error(500, "Open git-diff-tree failed");
4812                         @difftree = map { chomp; $_ } <$fd>;
4813                         close $fd
4814                                 or die_error(404, "Reading git-diff-tree failed");
4815                         @difftree
4816                                 or die_error(404, "Blob diff not found");
4818                 } elsif (defined $hash &&
4819                          $hash =~ /[0-9a-fA-F]{40}/) {
4820                         # try to find filename from $hash
4822                         # read filtered raw output
4823                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4824                                 $hash_parent_base, $hash_base, "--"
4825                                 or die_error(500, "Open git-diff-tree failed");
4826                         @difftree =
4827                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
4828                                 # $hash == to_id
4829                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4830                                 map { chomp; $_ } <$fd>;
4831                         close $fd
4832                                 or die_error(404, "Reading git-diff-tree failed");
4833                         @difftree
4834                                 or die_error(404, "Blob diff not found");
4836                 } else {
4837                         die_error(400, "Missing one of the blob diff parameters");
4838                 }
4840                 if (@difftree > 1) {
4841                         die_error(400, "Ambiguous blob diff specification");
4842                 }
4844                 %diffinfo = parse_difftree_raw_line($difftree[0]);
4845                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4846                 $file_name   ||= $diffinfo{'to_file'};
4848                 $hash_parent ||= $diffinfo{'from_id'};
4849                 $hash        ||= $diffinfo{'to_id'};
4851                 # non-textual hash id's can be cached
4852                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4853                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4854                         $expires = '+1d';
4855                 }
4857                 # open patch output
4858                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4859                         '-p', ($format eq 'html' ? "--full-index" : ()),
4860                         $hash_parent_base, $hash_base,
4861                         "--", (defined $file_parent ? $file_parent : ()), $file_name
4862                         or die_error(500, "Open git-diff-tree failed");
4863         }
4865         # old/legacy style URI
4866         if (!%diffinfo && # if new style URI failed
4867             defined $hash && defined $hash_parent) {
4868                 # fake git-diff-tree raw output
4869                 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4870                 $diffinfo{'from_id'} = $hash_parent;
4871                 $diffinfo{'to_id'}   = $hash;
4872                 if (defined $file_name) {
4873                         if (defined $file_parent) {
4874                                 $diffinfo{'status'} = '2';
4875                                 $diffinfo{'from_file'} = $file_parent;
4876                                 $diffinfo{'to_file'}   = $file_name;
4877                         } else { # assume not renamed
4878                                 $diffinfo{'status'} = '1';
4879                                 $diffinfo{'from_file'} = $file_name;
4880                                 $diffinfo{'to_file'}   = $file_name;
4881                         }
4882                 } else { # no filename given
4883                         $diffinfo{'status'} = '2';
4884                         $diffinfo{'from_file'} = $hash_parent;
4885                         $diffinfo{'to_file'}   = $hash;
4886                 }
4888                 # non-textual hash id's can be cached
4889                 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4890                     $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4891                         $expires = '+1d';
4892                 }
4894                 # open patch output
4895                 open $fd, "-|", git_cmd(), "diff", @diff_opts,
4896                         '-p', ($format eq 'html' ? "--full-index" : ()),
4897                         $hash_parent, $hash, "--"
4898                         or die_error(500, "Open git-diff failed");
4899         } else  {
4900                 die_error(400, "Missing one of the blob diff parameters")
4901                         unless %diffinfo;
4902         }
4904         # header
4905         if ($format eq 'html') {
4906                 my $formats_nav =
4907                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
4908                                 "raw");
4909                 git_header_html(undef, $expires);
4910                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4911                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4912                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4913                 } else {
4914                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4915                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4916                 }
4917                 if (defined $file_name) {
4918                         git_print_page_path($file_name, "blob", $hash_base);
4919                 } else {
4920                         print "<div class=\"page_path\"></div>\n";
4921                 }
4923         } elsif ($format eq 'plain') {
4924                 print $cgi->header(
4925                         -type => 'text/plain',
4926                         -charset => 'utf-8',
4927                         -expires => $expires,
4928                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
4930                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4932         } else {
4933                 die_error(400, "Unknown blobdiff format");
4934         }
4936         # patch
4937         if ($format eq 'html') {
4938                 print "<div class=\"page_body\">\n";
4940                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
4941                 close $fd;
4943                 print "</div>\n"; # class="page_body"
4944                 git_footer_html();
4946         } else {
4947                 while (my $line = <$fd>) {
4948                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
4949                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
4951                         print $line;
4953                         last if $line =~ m!^\+\+\+!;
4954                 }
4955                 local $/ = undef;
4956                 print <$fd>;
4957                 close $fd;
4958         }
4961 sub git_blobdiff_plain {
4962         git_blobdiff('plain');
4965 sub git_commitdiff {
4966         my $format = shift || 'html';
4967         $hash ||= $hash_base || "HEAD";
4968         my %co = parse_commit($hash)
4969             or die_error(404, "Unknown commit object");
4971         # choose format for commitdiff for merge
4972         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
4973                 $hash_parent = '--cc';
4974         }
4975         # we need to prepare $formats_nav before almost any parameter munging
4976         my $formats_nav;
4977         if ($format eq 'html') {
4978                 $formats_nav =
4979                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
4980                                 "raw");
4982                 if (defined $hash_parent &&
4983                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
4984                         # commitdiff with two commits given
4985                         my $hash_parent_short = $hash_parent;
4986                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4987                                 $hash_parent_short = substr($hash_parent, 0, 7);
4988                         }
4989                         $formats_nav .=
4990                                 ' (from';
4991                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
4992                                 if ($co{'parents'}[$i] eq $hash_parent) {
4993                                         $formats_nav .= ' parent ' . ($i+1);
4994                                         last;
4995                                 }
4996                         }
4997                         $formats_nav .= ': ' .
4998                                 $cgi->a({-href => href(action=>"commitdiff",
4999                                                        hash=>$hash_parent)},
5000                                         esc_html($hash_parent_short)) .
5001                                 ')';
5002                 } elsif (!$co{'parent'}) {
5003                         # --root commitdiff
5004                         $formats_nav .= ' (initial)';
5005                 } elsif (scalar @{$co{'parents'}} == 1) {
5006                         # single parent commit
5007                         $formats_nav .=
5008                                 ' (parent: ' .
5009                                 $cgi->a({-href => href(action=>"commitdiff",
5010                                                        hash=>$co{'parent'})},
5011                                         esc_html(substr($co{'parent'}, 0, 7))) .
5012                                 ')';
5013                 } else {
5014                         # merge commit
5015                         if ($hash_parent eq '--cc') {
5016                                 $formats_nav .= ' | ' .
5017                                         $cgi->a({-href => href(action=>"commitdiff",
5018                                                                hash=>$hash, hash_parent=>'-c')},
5019                                                 'combined');
5020                         } else { # $hash_parent eq '-c'
5021                                 $formats_nav .= ' | ' .
5022                                         $cgi->a({-href => href(action=>"commitdiff",
5023                                                                hash=>$hash, hash_parent=>'--cc')},
5024                                                 'compact');
5025                         }
5026                         $formats_nav .=
5027                                 ' (merge: ' .
5028                                 join(' ', map {
5029                                         $cgi->a({-href => href(action=>"commitdiff",
5030                                                                hash=>$_)},
5031                                                 esc_html(substr($_, 0, 7)));
5032                                 } @{$co{'parents'}} ) .
5033                                 ')';
5034                 }
5035         }
5037         my $hash_parent_param = $hash_parent;
5038         if (!defined $hash_parent_param) {
5039                 # --cc for multiple parents, --root for parentless
5040                 $hash_parent_param =
5041                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5042         }
5044         # read commitdiff
5045         my $fd;
5046         my @difftree;
5047         if ($format eq 'html') {
5048                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5049                         "--no-commit-id", "--patch-with-raw", "--full-index",
5050                         $hash_parent_param, $hash, "--"
5051                         or die_error(500, "Open git-diff-tree failed");
5053                 while (my $line = <$fd>) {
5054                         chomp $line;
5055                         # empty line ends raw part of diff-tree output
5056                         last unless $line;
5057                         push @difftree, scalar parse_difftree_raw_line($line);
5058                 }
5060         } elsif ($format eq 'plain') {
5061                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5062                         '-p', $hash_parent_param, $hash, "--"
5063                         or die_error(500, "Open git-diff-tree failed");
5065         } else {
5066                 die_error(400, "Unknown commitdiff format");
5067         }
5069         # non-textual hash id's can be cached
5070         my $expires;
5071         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5072                 $expires = "+1d";
5073         }
5075         # write commit message
5076         if ($format eq 'html') {
5077                 my $refs = git_get_references();
5078                 my $ref = format_ref_marker($refs, $co{'id'});
5080                 git_header_html(undef, $expires);
5081                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5082                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5083                 git_print_authorship(\%co);
5084                 print "<div class=\"page_body\">\n";
5085                 if (@{$co{'comment'}} > 1) {
5086                         print "<div class=\"log\">\n";
5087                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5088                         print "</div>\n"; # class="log"
5089                 }
5091         } elsif ($format eq 'plain') {
5092                 my $refs = git_get_references("tags");
5093                 my $tagname = git_get_rev_name_tags($hash);
5094                 my $filename = basename($project) . "-$hash.patch";
5096                 print $cgi->header(
5097                         -type => 'text/plain',
5098                         -charset => 'utf-8',
5099                         -expires => $expires,
5100                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5101                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5102                 print "From: " . to_utf8($co{'author'}) . "\n";
5103                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5104                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5106                 print "X-Git-Tag: $tagname\n" if $tagname;
5107                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5109                 foreach my $line (@{$co{'comment'}}) {
5110                         print to_utf8($line) . "\n";
5111                 }
5112                 print "---\n\n";
5113         }
5115         # write patch
5116         if ($format eq 'html') {
5117                 my $use_parents = !defined $hash_parent ||
5118                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5119                 git_difftree_body(\@difftree, $hash,
5120                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5121                 print "<br/>\n";
5123                 git_patchset_body($fd, \@difftree, $hash,
5124                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5125                 close $fd;
5126                 print "</div>\n"; # class="page_body"
5127                 git_footer_html();
5129         } elsif ($format eq 'plain') {
5130                 local $/ = undef;
5131                 print <$fd>;
5132                 close $fd
5133                         or print "Reading git-diff-tree failed\n";
5134         }
5137 sub git_commitdiff_plain {
5138         git_commitdiff('plain');
5141 sub git_history {
5142         if (!defined $hash_base) {
5143                 $hash_base = git_get_head_hash($project);
5144         }
5145         if (!defined $page) {
5146                 $page = 0;
5147         }
5148         my $ftype;
5149         my %co = parse_commit($hash_base)
5150             or die_error(404, "Unknown commit object");
5152         my $refs = git_get_references();
5153         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5155         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5156                                        $file_name, "--full-history")
5157             or die_error(404, "No such file or directory on given branch");
5159         if (!defined $hash && defined $file_name) {
5160                 # some commits could have deleted file in question,
5161                 # and not have it in tree, but one of them has to have it
5162                 for (my $i = 0; $i <= @commitlist; $i++) {
5163                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5164                         last if defined $hash;
5165                 }
5166         }
5167         if (defined $hash) {
5168                 $ftype = git_get_type($hash);
5169         }
5170         if (!defined $ftype) {
5171                 die_error(500, "Unknown type of object");
5172         }
5174         my $paging_nav = '';
5175         if ($page > 0) {
5176                 $paging_nav .=
5177                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5178                                                file_name=>$file_name)},
5179                                 "first");
5180                 $paging_nav .= " &sdot; " .
5181                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5182                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5183         } else {
5184                 $paging_nav .= "first";
5185                 $paging_nav .= " &sdot; prev";
5186         }
5187         my $next_link = '';
5188         if ($#commitlist >= 100) {
5189                 $next_link =
5190                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5191                                  -accesskey => "n", -title => "Alt-n"}, "next");
5192                 $paging_nav .= " &sdot; $next_link";
5193         } else {
5194                 $paging_nav .= " &sdot; next";
5195         }
5197         git_header_html();
5198         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5199         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5200         git_print_page_path($file_name, $ftype, $hash_base);
5202         git_history_body(\@commitlist, 0, 99,
5203                          $refs, $hash_base, $ftype, $next_link);
5205         git_footer_html();
5208 sub git_search {
5209         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5210         if (!defined $searchtext) {
5211                 die_error(400, "Text field is empty");
5212         }
5213         if (!defined $hash) {
5214                 $hash = git_get_head_hash($project);
5215         }
5216         my %co = parse_commit($hash);
5217         if (!%co) {
5218                 die_error(404, "Unknown commit object");
5219         }
5220         if (!defined $page) {
5221                 $page = 0;
5222         }
5224         $searchtype ||= 'commit';
5225         if ($searchtype eq 'pickaxe') {
5226                 # pickaxe may take all resources of your box and run for several minutes
5227                 # with every query - so decide by yourself how public you make this feature
5228                 gitweb_check_feature('pickaxe')
5229                     or die_error(403, "Pickaxe is disabled");
5230         }
5231         if ($searchtype eq 'grep') {
5232                 gitweb_check_feature('grep')
5233                     or die_error(403, "Grep is disabled");
5234         }
5236         git_header_html();
5238         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5239                 my $greptype;
5240                 if ($searchtype eq 'commit') {
5241                         $greptype = "--grep=";
5242                 } elsif ($searchtype eq 'author') {
5243                         $greptype = "--author=";
5244                 } elsif ($searchtype eq 'committer') {
5245                         $greptype = "--committer=";
5246                 }
5247                 $greptype .= $searchtext;
5248                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5249                                                $greptype, '--regexp-ignore-case',
5250                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5252                 my $paging_nav = '';
5253                 if ($page > 0) {
5254                         $paging_nav .=
5255                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5256                                                        searchtext=>$searchtext,
5257                                                        searchtype=>$searchtype)},
5258                                         "first");
5259                         $paging_nav .= " &sdot; " .
5260                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5261                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5262                 } else {
5263                         $paging_nav .= "first";
5264                         $paging_nav .= " &sdot; prev";
5265                 }
5266                 my $next_link = '';
5267                 if ($#commitlist >= 100) {
5268                         $next_link =
5269                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5270                                          -accesskey => "n", -title => "Alt-n"}, "next");
5271                         $paging_nav .= " &sdot; $next_link";
5272                 } else {
5273                         $paging_nav .= " &sdot; next";
5274                 }
5276                 if ($#commitlist >= 100) {
5277                 }
5279                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5280                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5281                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5282         }
5284         if ($searchtype eq 'pickaxe') {
5285                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5286                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5288                 print "<table class=\"pickaxe search\">\n";
5289                 my $alternate = 1;
5290                 $/ = "\n";
5291                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5292                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5293                         ($search_use_regexp ? '--pickaxe-regex' : ());
5294                 undef %co;
5295                 my @files;
5296                 while (my $line = <$fd>) {
5297                         chomp $line;
5298                         next unless $line;
5300                         my %set = parse_difftree_raw_line($line);
5301                         if (defined $set{'commit'}) {
5302                                 # finish previous commit
5303                                 if (%co) {
5304                                         print "</td>\n" .
5305                                               "<td class=\"link\">" .
5306                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5307                                               " | " .
5308                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5309                                         print "</td>\n" .
5310                                               "</tr>\n";
5311                                 }
5313                                 if ($alternate) {
5314                                         print "<tr class=\"dark\">\n";
5315                                 } else {
5316                                         print "<tr class=\"light\">\n";
5317                                 }
5318                                 $alternate ^= 1;
5319                                 %co = parse_commit($set{'commit'});
5320                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5321                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5322                                       "<td><i>$author</i></td>\n" .
5323                                       "<td>" .
5324                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5325                                               -class => "list subject"},
5326                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5327                         } elsif (defined $set{'to_id'}) {
5328                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5330                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5331                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5332                                               -class => "list"},
5333                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5334                                       "<br/>\n";
5335                         }
5336                 }
5337                 close $fd;
5339                 # finish last commit (warning: repetition!)
5340                 if (%co) {
5341                         print "</td>\n" .
5342                               "<td class=\"link\">" .
5343                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5344                               " | " .
5345                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5346                         print "</td>\n" .
5347                               "</tr>\n";
5348                 }
5350                 print "</table>\n";
5351         }
5353         if ($searchtype eq 'grep') {
5354                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5355                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5357                 print "<table class=\"grep_search\">\n";
5358                 my $alternate = 1;
5359                 my $matches = 0;
5360                 $/ = "\n";
5361                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5362                         $search_use_regexp ? ('-E', '-i') : '-F',
5363                         $searchtext, $co{'tree'};
5364                 my $lastfile = '';
5365                 while (my $line = <$fd>) {
5366                         chomp $line;
5367                         my ($file, $lno, $ltext, $binary);
5368                         last if ($matches++ > 1000);
5369                         if ($line =~ /^Binary file (.+) matches$/) {
5370                                 $file = $1;
5371                                 $binary = 1;
5372                         } else {
5373                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5374                         }
5375                         if ($file ne $lastfile) {
5376                                 $lastfile and print "</td></tr>\n";
5377                                 if ($alternate++) {
5378                                         print "<tr class=\"dark\">\n";
5379                                 } else {
5380                                         print "<tr class=\"light\">\n";
5381                                 }
5382                                 print "<td class=\"list\">".
5383                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5384                                                                file_name=>"$file"),
5385                                                 -class => "list"}, esc_path($file));
5386                                 print "</td><td>\n";
5387                                 $lastfile = $file;
5388                         }
5389                         if ($binary) {
5390                                 print "<div class=\"binary\">Binary file</div>\n";
5391                         } else {
5392                                 $ltext = untabify($ltext);
5393                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5394                                         $ltext = esc_html($1, -nbsp=>1);
5395                                         $ltext .= '<span class="match">';
5396                                         $ltext .= esc_html($2, -nbsp=>1);
5397                                         $ltext .= '</span>';
5398                                         $ltext .= esc_html($3, -nbsp=>1);
5399                                 } else {
5400                                         $ltext = esc_html($ltext, -nbsp=>1);
5401                                 }
5402                                 print "<div class=\"pre\">" .
5403                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5404                                                                file_name=>"$file").'#l'.$lno,
5405                                                 -class => "linenr"}, sprintf('%4i', $lno))
5406                                         . ' ' .  $ltext . "</div>\n";
5407                         }
5408                 }
5409                 if ($lastfile) {
5410                         print "</td></tr>\n";
5411                         if ($matches > 1000) {
5412                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5413                         }
5414                 } else {
5415                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5416                 }
5417                 close $fd;
5419                 print "</table>\n";
5420         }
5421         git_footer_html();
5424 sub git_search_help {
5425         git_header_html();
5426         git_print_page_nav('','', $hash,$hash,$hash);
5427         print <<EOT;
5428 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5429 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5430 the pattern entered is recognized as the POSIX extended
5431 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5432 insensitive).</p>
5433 <dl>
5434 <dt><b>commit</b></dt>
5435 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5436 EOT
5437         my ($have_grep) = gitweb_check_feature('grep');
5438         if ($have_grep) {
5439                 print <<EOT;
5440 <dt><b>grep</b></dt>
5441 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5442     a different one) are searched for the given pattern. On large trees, this search can take
5443 a while and put some strain on the server, so please use it with some consideration. Note that
5444 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5445 case-sensitive.</dd>
5446 EOT
5447         }
5448         print <<EOT;
5449 <dt><b>author</b></dt>
5450 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5451 <dt><b>committer</b></dt>
5452 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5453 EOT
5454         my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5455         if ($have_pickaxe) {
5456                 print <<EOT;
5457 <dt><b>pickaxe</b></dt>
5458 <dd>All commits that caused the string to appear or disappear from any file (changes that
5459 added, removed or "modified" the string) will be listed. This search can take a while and
5460 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5461 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5462 EOT
5463         }
5464         print "</dl>\n";
5465         git_footer_html();
5468 sub git_shortlog {
5469         my $head = git_get_head_hash($project);
5470         if (!defined $hash) {
5471                 $hash = $head;
5472         }
5473         if (!defined $page) {
5474                 $page = 0;
5475         }
5476         my $refs = git_get_references();
5478         my @commitlist = parse_commits($hash, 101, (100 * $page));
5480         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5481         my $next_link = '';
5482         if ($#commitlist >= 100) {
5483                 $next_link =
5484                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5485                                  -accesskey => "n", -title => "Alt-n"}, "next");
5486         }
5488         git_header_html();
5489         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5490         git_print_header_div('summary', $project);
5492         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5494         git_footer_html();
5497 ## ......................................................................
5498 ## feeds (RSS, Atom; OPML)
5500 sub git_feed {
5501         my $format = shift || 'atom';
5502         my ($have_blame) = gitweb_check_feature('blame');
5504         # Atom: http://www.atomenabled.org/developers/syndication/
5505         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5506         if ($format ne 'rss' && $format ne 'atom') {
5507                 die_error(400, "Unknown web feed format");
5508         }
5510         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5511         my $head = $hash || 'HEAD';
5512         my @commitlist = parse_commits($head, 150, 0, $file_name);
5514         my %latest_commit;
5515         my %latest_date;
5516         my $content_type = "application/$format+xml";
5517         if (defined $cgi->http('HTTP_ACCEPT') &&
5518                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5519                 # browser (feed reader) prefers text/xml
5520                 $content_type = 'text/xml';
5521         }
5522         if (defined($commitlist[0])) {
5523                 %latest_commit = %{$commitlist[0]};
5524                 %latest_date   = parse_date($latest_commit{'author_epoch'});
5525                 print $cgi->header(
5526                         -type => $content_type,
5527                         -charset => 'utf-8',
5528                         -last_modified => $latest_date{'rfc2822'});
5529         } else {
5530                 print $cgi->header(
5531                         -type => $content_type,
5532                         -charset => 'utf-8');
5533         }
5535         # Optimization: skip generating the body if client asks only
5536         # for Last-Modified date.
5537         return if ($cgi->request_method() eq 'HEAD');
5539         # header variables
5540         my $title = "$site_name - $project/$action";
5541         my $feed_type = 'log';
5542         if (defined $hash) {
5543                 $title .= " - '$hash'";
5544                 $feed_type = 'branch log';
5545                 if (defined $file_name) {
5546                         $title .= " :: $file_name";
5547                         $feed_type = 'history';
5548                 }
5549         } elsif (defined $file_name) {
5550                 $title .= " - $file_name";
5551                 $feed_type = 'history';
5552         }
5553         $title .= " $feed_type";
5554         my $descr = git_get_project_description($project);
5555         if (defined $descr) {
5556                 $descr = esc_html($descr);
5557         } else {
5558                 $descr = "$project " .
5559                          ($format eq 'rss' ? 'RSS' : 'Atom') .
5560                          " feed";
5561         }
5562         my $owner = git_get_project_owner($project);
5563         $owner = esc_html($owner);
5565         #header
5566         my $alt_url;
5567         if (defined $file_name) {
5568                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5569         } elsif (defined $hash) {
5570                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5571         } else {
5572                 $alt_url = href(-full=>1, action=>"summary");
5573         }
5574         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5575         if ($format eq 'rss') {
5576                 print <<XML;
5577 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5578 <channel>
5579 XML
5580                 print "<title>$title</title>\n" .
5581                       "<link>$alt_url</link>\n" .
5582                       "<description>$descr</description>\n" .
5583                       "<language>en</language>\n";
5584         } elsif ($format eq 'atom') {
5585                 print <<XML;
5586 <feed xmlns="http://www.w3.org/2005/Atom">
5587 XML
5588                 print "<title>$title</title>\n" .
5589                       "<subtitle>$descr</subtitle>\n" .
5590                       '<link rel="alternate" type="text/html" href="' .
5591                       $alt_url . '" />' . "\n" .
5592                       '<link rel="self" type="' . $content_type . '" href="' .
5593                       $cgi->self_url() . '" />' . "\n" .
5594                       "<id>" . href(-full=>1) . "</id>\n" .
5595                       # use project owner for feed author
5596                       "<author><name>$owner</name></author>\n";
5597                 if (defined $favicon) {
5598                         print "<icon>" . esc_url($favicon) . "</icon>\n";
5599                 }
5600                 if (defined $logo_url) {
5601                         # not twice as wide as tall: 72 x 27 pixels
5602                         print "<logo>" . esc_url($logo) . "</logo>\n";
5603                 }
5604                 if (! %latest_date) {
5605                         # dummy date to keep the feed valid until commits trickle in:
5606                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
5607                 } else {
5608                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
5609                 }
5610         }
5612         # contents
5613         for (my $i = 0; $i <= $#commitlist; $i++) {
5614                 my %co = %{$commitlist[$i]};
5615                 my $commit = $co{'id'};
5616                 # we read 150, we always show 30 and the ones more recent than 48 hours
5617                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5618                         last;
5619                 }
5620                 my %cd = parse_date($co{'author_epoch'});
5622                 # get list of changed files
5623                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5624                         $co{'parent'} || "--root",
5625                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
5626                         or next;
5627                 my @difftree = map { chomp; $_ } <$fd>;
5628                 close $fd
5629                         or next;
5631                 # print element (entry, item)
5632                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5633                 if ($format eq 'rss') {
5634                         print "<item>\n" .
5635                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
5636                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
5637                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5638                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5639                               "<link>$co_url</link>\n" .
5640                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
5641                               "<content:encoded>" .
5642                               "<![CDATA[\n";
5643                 } elsif ($format eq 'atom') {
5644                         print "<entry>\n" .
5645                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5646                               "<updated>$cd{'iso-8601'}</updated>\n" .
5647                               "<author>\n" .
5648                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5649                         if ($co{'author_email'}) {
5650                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5651                         }
5652                         print "</author>\n" .
5653                               # use committer for contributor
5654                               "<contributor>\n" .
5655                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5656                         if ($co{'committer_email'}) {
5657                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5658                         }
5659                         print "</contributor>\n" .
5660                               "<published>$cd{'iso-8601'}</published>\n" .
5661                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5662                               "<id>$co_url</id>\n" .
5663                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5664                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5665                 }
5666                 my $comment = $co{'comment'};
5667                 print "<pre>\n";
5668                 foreach my $line (@$comment) {
5669                         $line = esc_html($line);
5670                         print "$line\n";
5671                 }
5672                 print "</pre><ul>\n";
5673                 foreach my $difftree_line (@difftree) {
5674                         my %difftree = parse_difftree_raw_line($difftree_line);
5675                         next if !$difftree{'from_id'};
5677                         my $file = $difftree{'file'} || $difftree{'to_file'};
5679                         print "<li>" .
5680                               "[" .
5681                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5682                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5683                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5684                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
5685                                       -title => "diff"}, 'D');
5686                         if ($have_blame) {
5687                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
5688                                                              file_name=>$file, hash_base=>$commit),
5689                                               -title => "blame"}, 'B');
5690                         }
5691                         # if this is not a feed of a file history
5692                         if (!defined $file_name || $file_name ne $file) {
5693                                 print $cgi->a({-href => href(-full=>1, action=>"history",
5694                                                              file_name=>$file, hash=>$commit),
5695                                               -title => "history"}, 'H');
5696                         }
5697                         $file = esc_path($file);
5698                         print "] ".
5699                               "$file</li>\n";
5700                 }
5701                 if ($format eq 'rss') {
5702                         print "</ul>]]>\n" .
5703                               "</content:encoded>\n" .
5704                               "</item>\n";
5705                 } elsif ($format eq 'atom') {
5706                         print "</ul>\n</div>\n" .
5707                               "</content>\n" .
5708                               "</entry>\n";
5709                 }
5710         }
5712         # end of feed
5713         if ($format eq 'rss') {
5714                 print "</channel>\n</rss>\n";
5715         }       elsif ($format eq 'atom') {
5716                 print "</feed>\n";
5717         }
5720 sub git_rss {
5721         git_feed('rss');
5724 sub git_atom {
5725         git_feed('atom');
5728 sub git_opml {
5729         my @list = git_get_projects_list();
5731         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5732         print <<XML;
5733 <?xml version="1.0" encoding="utf-8"?>
5734 <opml version="1.0">
5735 <head>
5736   <title>$site_name OPML Export</title>
5737 </head>
5738 <body>
5739 <outline text="git RSS feeds">
5740 XML
5742         foreach my $pr (@list) {
5743                 my %proj = %$pr;
5744                 my $head = git_get_head_hash($proj{'path'});
5745                 if (!defined $head) {
5746                         next;
5747                 }
5748                 $git_dir = "$projectroot/$proj{'path'}";
5749                 my %co = parse_commit($head);
5750                 if (!%co) {
5751                         next;
5752                 }
5754                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5755                 my $rss  = "$my_url?p=$proj{'path'};a=rss";
5756                 my $html = "$my_url?p=$proj{'path'};a=summary";
5757                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5758         }
5759         print <<XML;
5760 </outline>
5761 </body>
5762 </opml>
5763 XML