Code

GIT 1.6.0.6
[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                 'sub' => \&feature_grep,
236                 'override' => 0,
237                 'default' => [1]},
239         # Enable the pickaxe search, which will list the commits that modified
240         # a given string in a file. This can be practical and quite faster
241         # alternative to 'blame', but still potentially CPU-intensive.
243         # To enable system wide have in $GITWEB_CONFIG
244         # $feature{'pickaxe'}{'default'} = [1];
245         # To have project specific config enable override in $GITWEB_CONFIG
246         # $feature{'pickaxe'}{'override'} = 1;
247         # and in project config gitweb.pickaxe = 0|1;
248         'pickaxe' => {
249                 'sub' => \&feature_pickaxe,
250                 'override' => 0,
251                 'default' => [1]},
253         # Make gitweb use an alternative format of the URLs which can be
254         # more readable and natural-looking: project name is embedded
255         # directly in the path and the query string contains other
256         # auxiliary information. All gitweb installations recognize
257         # URL in either format; this configures in which formats gitweb
258         # generates links.
260         # To enable system wide have in $GITWEB_CONFIG
261         # $feature{'pathinfo'}{'default'} = [1];
262         # Project specific override is not supported.
264         # Note that you will need to change the default location of CSS,
265         # favicon, logo and possibly other files to an absolute URL. Also,
266         # if gitweb.cgi serves as your indexfile, you will need to force
267         # $my_uri to contain the script name in your $GITWEB_CONFIG.
268         'pathinfo' => {
269                 'override' => 0,
270                 'default' => [0]},
272         # Make gitweb consider projects in project root subdirectories
273         # to be forks of existing projects. Given project $projname.git,
274         # projects matching $projname/*.git will not be shown in the main
275         # projects list, instead a '+' mark will be added to $projname
276         # there and a 'forks' view will be enabled for the project, listing
277         # all the forks. If project list is taken from a file, forks have
278         # to be listed after the main project.
280         # To enable system wide have in $GITWEB_CONFIG
281         # $feature{'forks'}{'default'} = [1];
282         # Project specific override is not supported.
283         'forks' => {
284                 'override' => 0,
285                 'default' => [0]},
286 );
288 sub gitweb_check_feature {
289         my ($name) = @_;
290         return unless exists $feature{$name};
291         my ($sub, $override, @defaults) = (
292                 $feature{$name}{'sub'},
293                 $feature{$name}{'override'},
294                 @{$feature{$name}{'default'}});
295         if (!$override) { return @defaults; }
296         if (!defined $sub) {
297                 warn "feature $name is not overrideable";
298                 return @defaults;
299         }
300         return $sub->(@defaults);
303 sub feature_blame {
304         my ($val) = git_get_project_config('blame', '--bool');
306         if ($val eq 'true') {
307                 return 1;
308         } elsif ($val eq 'false') {
309                 return 0;
310         }
312         return $_[0];
315 sub feature_snapshot {
316         my (@fmts) = @_;
318         my ($val) = git_get_project_config('snapshot');
320         if ($val) {
321                 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
322         }
324         return @fmts;
327 sub feature_grep {
328         my ($val) = git_get_project_config('grep', '--bool');
330         if ($val eq 'true') {
331                 return (1);
332         } elsif ($val eq 'false') {
333                 return (0);
334         }
336         return ($_[0]);
339 sub feature_pickaxe {
340         my ($val) = git_get_project_config('pickaxe', '--bool');
342         if ($val eq 'true') {
343                 return (1);
344         } elsif ($val eq 'false') {
345                 return (0);
346         }
348         return ($_[0]);
351 # checking HEAD file with -e is fragile if the repository was
352 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
353 # and then pruned.
354 sub check_head_link {
355         my ($dir) = @_;
356         my $headfile = "$dir/HEAD";
357         return ((-e $headfile) ||
358                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
361 sub check_export_ok {
362         my ($dir) = @_;
363         return (check_head_link($dir) &&
364                 (!$export_ok || -e "$dir/$export_ok"));
367 # process alternate names for backward compatibility
368 # filter out unsupported (unknown) snapshot formats
369 sub filter_snapshot_fmts {
370         my @fmts = @_;
372         @fmts = map {
373                 exists $known_snapshot_format_aliases{$_} ?
374                        $known_snapshot_format_aliases{$_} : $_} @fmts;
375         @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
379 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
380 if (-e $GITWEB_CONFIG) {
381         do $GITWEB_CONFIG;
382 } else {
383         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
384         do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
387 # version of the core git binary
388 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
390 $projects_list ||= $projectroot;
392 # ======================================================================
393 # input validation and dispatch
394 our $action = $cgi->param('a');
395 if (defined $action) {
396         if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
397                 die_error(400, "Invalid action parameter");
398         }
401 # parameters which are pathnames
402 our $project = $cgi->param('p');
403 if (defined $project) {
404         if (!validate_pathname($project) ||
405             !(-d "$projectroot/$project") ||
406             !check_head_link("$projectroot/$project") ||
407             ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
408             ($strict_export && !project_in_list($project))) {
409                 undef $project;
410                 die_error(404, "No such project");
411         }
414 our $file_name = $cgi->param('f');
415 if (defined $file_name) {
416         if (!validate_pathname($file_name)) {
417                 die_error(400, "Invalid file parameter");
418         }
421 our $file_parent = $cgi->param('fp');
422 if (defined $file_parent) {
423         if (!validate_pathname($file_parent)) {
424                 die_error(400, "Invalid file parent parameter");
425         }
428 # parameters which are refnames
429 our $hash = $cgi->param('h');
430 if (defined $hash) {
431         if (!validate_refname($hash)) {
432                 die_error(400, "Invalid hash parameter");
433         }
436 our $hash_parent = $cgi->param('hp');
437 if (defined $hash_parent) {
438         if (!validate_refname($hash_parent)) {
439                 die_error(400, "Invalid hash parent parameter");
440         }
443 our $hash_base = $cgi->param('hb');
444 if (defined $hash_base) {
445         if (!validate_refname($hash_base)) {
446                 die_error(400, "Invalid hash base parameter");
447         }
450 my %allowed_options = (
451         "--no-merges" => [ qw(rss atom log shortlog history) ],
452 );
454 our @extra_options = $cgi->param('opt');
455 if (defined @extra_options) {
456         foreach my $opt (@extra_options) {
457                 if (not exists $allowed_options{$opt}) {
458                         die_error(400, "Invalid option parameter");
459                 }
460                 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
461                         die_error(400, "Invalid option parameter for this action");
462                 }
463         }
466 our $hash_parent_base = $cgi->param('hpb');
467 if (defined $hash_parent_base) {
468         if (!validate_refname($hash_parent_base)) {
469                 die_error(400, "Invalid hash parent base parameter");
470         }
473 # other parameters
474 our $page = $cgi->param('pg');
475 if (defined $page) {
476         if ($page =~ m/[^0-9]/) {
477                 die_error(400, "Invalid page parameter");
478         }
481 our $searchtype = $cgi->param('st');
482 if (defined $searchtype) {
483         if ($searchtype =~ m/[^a-z]/) {
484                 die_error(400, "Invalid searchtype parameter");
485         }
488 our $search_use_regexp = $cgi->param('sr');
490 our $searchtext = $cgi->param('s');
491 our $search_regexp;
492 if (defined $searchtext) {
493         if (length($searchtext) < 2) {
494                 die_error(403, "At least two characters are required for search parameter");
495         }
496         $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
499 # now read PATH_INFO and use it as alternative to parameters
500 sub evaluate_path_info {
501         return if defined $project;
502         my $path_info = $ENV{"PATH_INFO"};
503         return if !$path_info;
504         $path_info =~ s,^/+,,;
505         return if !$path_info;
506         # find which part of PATH_INFO is project
507         $project = $path_info;
508         $project =~ s,/+$,,;
509         while ($project && !check_head_link("$projectroot/$project")) {
510                 $project =~ s,/*[^/]*$,,;
511         }
512         # validate project
513         $project = validate_pathname($project);
514         if (!$project ||
515             ($export_ok && !-e "$projectroot/$project/$export_ok") ||
516             ($strict_export && !project_in_list($project))) {
517                 undef $project;
518                 return;
519         }
520         # do not change any parameters if an action is given using the query string
521         return if $action;
522         $path_info =~ s,^\Q$project\E/*,,;
523         my ($refname, $pathname) = split(/:/, $path_info, 2);
524         if (defined $pathname) {
525                 # we got "project.git/branch:filename" or "project.git/branch:dir/"
526                 # we could use git_get_type(branch:pathname), but it needs $git_dir
527                 $pathname =~ s,^/+,,;
528                 if (!$pathname || substr($pathname, -1) eq "/") {
529                         $action  ||= "tree";
530                         $pathname =~ s,/$,,;
531                 } else {
532                         $action  ||= "blob_plain";
533                 }
534                 $hash_base ||= validate_refname($refname);
535                 $file_name ||= validate_pathname($pathname);
536         } elsif (defined $refname) {
537                 # we got "project.git/branch"
538                 $action ||= "shortlog";
539                 $hash   ||= validate_refname($refname);
540         }
542 evaluate_path_info();
544 # path to the current git repository
545 our $git_dir;
546 $git_dir = "$projectroot/$project" if $project;
548 # dispatch
549 my %actions = (
550         "blame" => \&git_blame,
551         "blobdiff" => \&git_blobdiff,
552         "blobdiff_plain" => \&git_blobdiff_plain,
553         "blob" => \&git_blob,
554         "blob_plain" => \&git_blob_plain,
555         "commitdiff" => \&git_commitdiff,
556         "commitdiff_plain" => \&git_commitdiff_plain,
557         "commit" => \&git_commit,
558         "forks" => \&git_forks,
559         "heads" => \&git_heads,
560         "history" => \&git_history,
561         "log" => \&git_log,
562         "rss" => \&git_rss,
563         "atom" => \&git_atom,
564         "search" => \&git_search,
565         "search_help" => \&git_search_help,
566         "shortlog" => \&git_shortlog,
567         "summary" => \&git_summary,
568         "tag" => \&git_tag,
569         "tags" => \&git_tags,
570         "tree" => \&git_tree,
571         "snapshot" => \&git_snapshot,
572         "object" => \&git_object,
573         # those below don't need $project
574         "opml" => \&git_opml,
575         "project_list" => \&git_project_list,
576         "project_index" => \&git_project_index,
577 );
579 if (!defined $action) {
580         if (defined $hash) {
581                 $action = git_get_type($hash);
582         } elsif (defined $hash_base && defined $file_name) {
583                 $action = git_get_type("$hash_base:$file_name");
584         } elsif (defined $project) {
585                 $action = 'summary';
586         } else {
587                 $action = 'project_list';
588         }
590 if (!defined($actions{$action})) {
591         die_error(400, "Unknown action");
593 if ($action !~ m/^(opml|project_list|project_index)$/ &&
594     !$project) {
595         die_error(400, "Project needed");
597 $actions{$action}->();
598 exit;
600 ## ======================================================================
601 ## action links
603 sub href (%) {
604         my %params = @_;
605         # default is to use -absolute url() i.e. $my_uri
606         my $href = $params{-full} ? $my_url : $my_uri;
608         # XXX: Warning: If you touch this, check the search form for updating,
609         # too.
611         my @mapping = (
612                 project => "p",
613                 action => "a",
614                 file_name => "f",
615                 file_parent => "fp",
616                 hash => "h",
617                 hash_parent => "hp",
618                 hash_base => "hb",
619                 hash_parent_base => "hpb",
620                 page => "pg",
621                 order => "o",
622                 searchtext => "s",
623                 searchtype => "st",
624                 snapshot_format => "sf",
625                 extra_options => "opt",
626                 search_use_regexp => "sr",
627         );
628         my %mapping = @mapping;
630         $params{'project'} = $project unless exists $params{'project'};
632         if ($params{-replay}) {
633                 while (my ($name, $symbol) = each %mapping) {
634                         if (!exists $params{$name}) {
635                                 # to allow for multivalued params we use arrayref form
636                                 $params{$name} = [ $cgi->param($symbol) ];
637                         }
638                 }
639         }
641         my ($use_pathinfo) = gitweb_check_feature('pathinfo');
642         if ($use_pathinfo) {
643                 # use PATH_INFO for project name
644                 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
645                 delete $params{'project'};
647                 # Summary just uses the project path URL
648                 if (defined $params{'action'} && $params{'action'} eq 'summary') {
649                         delete $params{'action'};
650                 }
651         }
653         # now encode the parameters explicitly
654         my @result = ();
655         for (my $i = 0; $i < @mapping; $i += 2) {
656                 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
657                 if (defined $params{$name}) {
658                         if (ref($params{$name}) eq "ARRAY") {
659                                 foreach my $par (@{$params{$name}}) {
660                                         push @result, $symbol . "=" . esc_param($par);
661                                 }
662                         } else {
663                                 push @result, $symbol . "=" . esc_param($params{$name});
664                         }
665                 }
666         }
667         $href .= "?" . join(';', @result) if scalar @result;
669         return $href;
673 ## ======================================================================
674 ## validation, quoting/unquoting and escaping
676 sub validate_pathname {
677         my $input = shift || return undef;
679         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
680         # at the beginning, at the end, and between slashes.
681         # also this catches doubled slashes
682         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
683                 return undef;
684         }
685         # no null characters
686         if ($input =~ m!\0!) {
687                 return undef;
688         }
689         return $input;
692 sub validate_refname {
693         my $input = shift || return undef;
695         # textual hashes are O.K.
696         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
697                 return $input;
698         }
699         # it must be correct pathname
700         $input = validate_pathname($input)
701                 or return undef;
702         # restrictions on ref name according to git-check-ref-format
703         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
704                 return undef;
705         }
706         return $input;
709 # decode sequences of octets in utf8 into Perl's internal form,
710 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
711 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
712 sub to_utf8 {
713         my $str = shift;
714         if (utf8::valid($str)) {
715                 utf8::decode($str);
716                 return $str;
717         } else {
718                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
719         }
722 # quote unsafe chars, but keep the slash, even when it's not
723 # correct, but quoted slashes look too horrible in bookmarks
724 sub esc_param {
725         my $str = shift;
726         $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
727         $str =~ s/\+/%2B/g;
728         $str =~ s/ /\+/g;
729         return $str;
732 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
733 sub esc_url {
734         my $str = shift;
735         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
736         $str =~ s/\+/%2B/g;
737         $str =~ s/ /\+/g;
738         return $str;
741 # replace invalid utf8 character with SUBSTITUTION sequence
742 sub esc_html ($;%) {
743         my $str = shift;
744         my %opts = @_;
746         $str = to_utf8($str);
747         $str = $cgi->escapeHTML($str);
748         if ($opts{'-nbsp'}) {
749                 $str =~ s/ /&nbsp;/g;
750         }
751         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
752         return $str;
755 # quote control characters and escape filename to HTML
756 sub esc_path {
757         my $str = shift;
758         my %opts = @_;
760         $str = to_utf8($str);
761         $str = $cgi->escapeHTML($str);
762         if ($opts{'-nbsp'}) {
763                 $str =~ s/ /&nbsp;/g;
764         }
765         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
766         return $str;
769 # Make control characters "printable", using character escape codes (CEC)
770 sub quot_cec {
771         my $cntrl = shift;
772         my %opts = @_;
773         my %es = ( # character escape codes, aka escape sequences
774                 "\t" => '\t',   # tab            (HT)
775                 "\n" => '\n',   # line feed      (LF)
776                 "\r" => '\r',   # carrige return (CR)
777                 "\f" => '\f',   # form feed      (FF)
778                 "\b" => '\b',   # backspace      (BS)
779                 "\a" => '\a',   # alarm (bell)   (BEL)
780                 "\e" => '\e',   # escape         (ESC)
781                 "\013" => '\v', # vertical tab   (VT)
782                 "\000" => '\0', # nul character  (NUL)
783         );
784         my $chr = ( (exists $es{$cntrl})
785                     ? $es{$cntrl}
786                     : sprintf('\%03o', ord($cntrl)) );
787         if ($opts{-nohtml}) {
788                 return $chr;
789         } else {
790                 return "<span class=\"cntrl\">$chr</span>";
791         }
794 # Alternatively use unicode control pictures codepoints,
795 # Unicode "printable representation" (PR)
796 sub quot_upr {
797         my $cntrl = shift;
798         my %opts = @_;
800         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
801         if ($opts{-nohtml}) {
802                 return $chr;
803         } else {
804                 return "<span class=\"cntrl\">$chr</span>";
805         }
808 # git may return quoted and escaped filenames
809 sub unquote {
810         my $str = shift;
812         sub unq {
813                 my $seq = shift;
814                 my %es = ( # character escape codes, aka escape sequences
815                         't' => "\t",   # tab            (HT, TAB)
816                         'n' => "\n",   # newline        (NL)
817                         'r' => "\r",   # return         (CR)
818                         'f' => "\f",   # form feed      (FF)
819                         'b' => "\b",   # backspace      (BS)
820                         'a' => "\a",   # alarm (bell)   (BEL)
821                         'e' => "\e",   # escape         (ESC)
822                         'v' => "\013", # vertical tab   (VT)
823                 );
825                 if ($seq =~ m/^[0-7]{1,3}$/) {
826                         # octal char sequence
827                         return chr(oct($seq));
828                 } elsif (exists $es{$seq}) {
829                         # C escape sequence, aka character escape code
830                         return $es{$seq};
831                 }
832                 # quoted ordinary character
833                 return $seq;
834         }
836         if ($str =~ m/^"(.*)"$/) {
837                 # needs unquoting
838                 $str = $1;
839                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
840         }
841         return $str;
844 # escape tabs (convert tabs to spaces)
845 sub untabify {
846         my $line = shift;
848         while ((my $pos = index($line, "\t")) != -1) {
849                 if (my $count = (8 - ($pos % 8))) {
850                         my $spaces = ' ' x $count;
851                         $line =~ s/\t/$spaces/;
852                 }
853         }
855         return $line;
858 sub project_in_list {
859         my $project = shift;
860         my @list = git_get_projects_list();
861         return @list && scalar(grep { $_->{'path'} eq $project } @list);
864 ## ----------------------------------------------------------------------
865 ## HTML aware string manipulation
867 # Try to chop given string on a word boundary between position
868 # $len and $len+$add_len. If there is no word boundary there,
869 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
870 # (marking chopped part) would be longer than given string.
871 sub chop_str {
872         my $str = shift;
873         my $len = shift;
874         my $add_len = shift || 10;
875         my $where = shift || 'right'; # 'left' | 'center' | 'right'
877         # Make sure perl knows it is utf8 encoded so we don't
878         # cut in the middle of a utf8 multibyte char.
879         $str = to_utf8($str);
881         # allow only $len chars, but don't cut a word if it would fit in $add_len
882         # if it doesn't fit, cut it if it's still longer than the dots we would add
883         # remove chopped character entities entirely
885         # when chopping in the middle, distribute $len into left and right part
886         # return early if chopping wouldn't make string shorter
887         if ($where eq 'center') {
888                 return $str if ($len + 5 >= length($str)); # filler is length 5
889                 $len = int($len/2);
890         } else {
891                 return $str if ($len + 4 >= length($str)); # filler is length 4
892         }
894         # regexps: ending and beginning with word part up to $add_len
895         my $endre = qr/.{$len}\w{0,$add_len}/;
896         my $begre = qr/\w{0,$add_len}.{$len}/;
898         if ($where eq 'left') {
899                 $str =~ m/^(.*?)($begre)$/;
900                 my ($lead, $body) = ($1, $2);
901                 if (length($lead) > 4) {
902                         $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
903                         $lead = " ...";
904                 }
905                 return "$lead$body";
907         } elsif ($where eq 'center') {
908                 $str =~ m/^($endre)(.*)$/;
909                 my ($left, $str)  = ($1, $2);
910                 $str =~ m/^(.*?)($begre)$/;
911                 my ($mid, $right) = ($1, $2);
912                 if (length($mid) > 5) {
913                         $left  =~ s/&[^;]*$//;
914                         $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
915                         $mid = " ... ";
916                 }
917                 return "$left$mid$right";
919         } else {
920                 $str =~ m/^($endre)(.*)$/;
921                 my $body = $1;
922                 my $tail = $2;
923                 if (length($tail) > 4) {
924                         $body =~ s/&[^;]*$//;
925                         $tail = "... ";
926                 }
927                 return "$body$tail";
928         }
931 # takes the same arguments as chop_str, but also wraps a <span> around the
932 # result with a title attribute if it does get chopped. Additionally, the
933 # string is HTML-escaped.
934 sub chop_and_escape_str {
935         my ($str) = @_;
937         my $chopped = chop_str(@_);
938         if ($chopped eq $str) {
939                 return esc_html($chopped);
940         } else {
941                 $str =~ s/([[:cntrl:]])/?/g;
942                 return $cgi->span({-title=>$str}, esc_html($chopped));
943         }
946 ## ----------------------------------------------------------------------
947 ## functions returning short strings
949 # CSS class for given age value (in seconds)
950 sub age_class {
951         my $age = shift;
953         if (!defined $age) {
954                 return "noage";
955         } elsif ($age < 60*60*2) {
956                 return "age0";
957         } elsif ($age < 60*60*24*2) {
958                 return "age1";
959         } else {
960                 return "age2";
961         }
964 # convert age in seconds to "nn units ago" string
965 sub age_string {
966         my $age = shift;
967         my $age_str;
969         if ($age > 60*60*24*365*2) {
970                 $age_str = (int $age/60/60/24/365);
971                 $age_str .= " years ago";
972         } elsif ($age > 60*60*24*(365/12)*2) {
973                 $age_str = int $age/60/60/24/(365/12);
974                 $age_str .= " months ago";
975         } elsif ($age > 60*60*24*7*2) {
976                 $age_str = int $age/60/60/24/7;
977                 $age_str .= " weeks ago";
978         } elsif ($age > 60*60*24*2) {
979                 $age_str = int $age/60/60/24;
980                 $age_str .= " days ago";
981         } elsif ($age > 60*60*2) {
982                 $age_str = int $age/60/60;
983                 $age_str .= " hours ago";
984         } elsif ($age > 60*2) {
985                 $age_str = int $age/60;
986                 $age_str .= " min ago";
987         } elsif ($age > 2) {
988                 $age_str = int $age;
989                 $age_str .= " sec ago";
990         } else {
991                 $age_str .= " right now";
992         }
993         return $age_str;
996 use constant {
997         S_IFINVALID => 0030000,
998         S_IFGITLINK => 0160000,
999 };
1001 # submodule/subproject, a commit object reference
1002 sub S_ISGITLINK($) {
1003         my $mode = shift;
1005         return (($mode & S_IFMT) == S_IFGITLINK)
1008 # convert file mode in octal to symbolic file mode string
1009 sub mode_str {
1010         my $mode = oct shift;
1012         if (S_ISGITLINK($mode)) {
1013                 return 'm---------';
1014         } elsif (S_ISDIR($mode & S_IFMT)) {
1015                 return 'drwxr-xr-x';
1016         } elsif (S_ISLNK($mode)) {
1017                 return 'lrwxrwxrwx';
1018         } elsif (S_ISREG($mode)) {
1019                 # git cares only about the executable bit
1020                 if ($mode & S_IXUSR) {
1021                         return '-rwxr-xr-x';
1022                 } else {
1023                         return '-rw-r--r--';
1024                 };
1025         } else {
1026                 return '----------';
1027         }
1030 # convert file mode in octal to file type string
1031 sub file_type {
1032         my $mode = shift;
1034         if ($mode !~ m/^[0-7]+$/) {
1035                 return $mode;
1036         } else {
1037                 $mode = oct $mode;
1038         }
1040         if (S_ISGITLINK($mode)) {
1041                 return "submodule";
1042         } elsif (S_ISDIR($mode & S_IFMT)) {
1043                 return "directory";
1044         } elsif (S_ISLNK($mode)) {
1045                 return "symlink";
1046         } elsif (S_ISREG($mode)) {
1047                 return "file";
1048         } else {
1049                 return "unknown";
1050         }
1053 # convert file mode in octal to file type description string
1054 sub file_type_long {
1055         my $mode = shift;
1057         if ($mode !~ m/^[0-7]+$/) {
1058                 return $mode;
1059         } else {
1060                 $mode = oct $mode;
1061         }
1063         if (S_ISGITLINK($mode)) {
1064                 return "submodule";
1065         } elsif (S_ISDIR($mode & S_IFMT)) {
1066                 return "directory";
1067         } elsif (S_ISLNK($mode)) {
1068                 return "symlink";
1069         } elsif (S_ISREG($mode)) {
1070                 if ($mode & S_IXUSR) {
1071                         return "executable";
1072                 } else {
1073                         return "file";
1074                 };
1075         } else {
1076                 return "unknown";
1077         }
1081 ## ----------------------------------------------------------------------
1082 ## functions returning short HTML fragments, or transforming HTML fragments
1083 ## which don't belong to other sections
1085 # format line of commit message.
1086 sub format_log_line_html {
1087         my $line = shift;
1089         $line = esc_html($line, -nbsp=>1);
1090         if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1091                 my $hash_text = $1;
1092                 my $link =
1093                         $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1094                                 -class => "text"}, $hash_text);
1095                 $line =~ s/$hash_text/$link/;
1096         }
1097         return $line;
1100 # format marker of refs pointing to given object
1101 sub format_ref_marker {
1102         my ($refs, $id) = @_;
1103         my $markers = '';
1105         if (defined $refs->{$id}) {
1106                 foreach my $ref (@{$refs->{$id}}) {
1107                         my ($type, $name) = qw();
1108                         # e.g. tags/v2.6.11 or heads/next
1109                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1110                                 $type = $1;
1111                                 $name = $2;
1112                         } else {
1113                                 $type = "ref";
1114                                 $name = $ref;
1115                         }
1117                         $markers .= " <span class=\"$type\" title=\"$ref\">" .
1118                                     esc_html($name) . "</span>";
1119                 }
1120         }
1122         if ($markers) {
1123                 return ' <span class="refs">'. $markers . '</span>';
1124         } else {
1125                 return "";
1126         }
1129 # format, perhaps shortened and with markers, title line
1130 sub format_subject_html {
1131         my ($long, $short, $href, $extra) = @_;
1132         $extra = '' unless defined($extra);
1134         if (length($short) < length($long)) {
1135                 return $cgi->a({-href => $href, -class => "list subject",
1136                                 -title => to_utf8($long)},
1137                        esc_html($short) . $extra);
1138         } else {
1139                 return $cgi->a({-href => $href, -class => "list subject"},
1140                        esc_html($long)  . $extra);
1141         }
1144 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1145 sub format_git_diff_header_line {
1146         my $line = shift;
1147         my $diffinfo = shift;
1148         my ($from, $to) = @_;
1150         if ($diffinfo->{'nparents'}) {
1151                 # combined diff
1152                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1153                 if ($to->{'href'}) {
1154                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1155                                          esc_path($to->{'file'}));
1156                 } else { # file was deleted (no href)
1157                         $line .= esc_path($to->{'file'});
1158                 }
1159         } else {
1160                 # "ordinary" diff
1161                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1162                 if ($from->{'href'}) {
1163                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1164                                          'a/' . esc_path($from->{'file'}));
1165                 } else { # file was added (no href)
1166                         $line .= 'a/' . esc_path($from->{'file'});
1167                 }
1168                 $line .= ' ';
1169                 if ($to->{'href'}) {
1170                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1171                                          'b/' . esc_path($to->{'file'}));
1172                 } else { # file was deleted
1173                         $line .= 'b/' . esc_path($to->{'file'});
1174                 }
1175         }
1177         return "<div class=\"diff header\">$line</div>\n";
1180 # format extended diff header line, before patch itself
1181 sub format_extended_diff_header_line {
1182         my $line = shift;
1183         my $diffinfo = shift;
1184         my ($from, $to) = @_;
1186         # match <path>
1187         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1188                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1189                                        esc_path($from->{'file'}));
1190         }
1191         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1192                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1193                                  esc_path($to->{'file'}));
1194         }
1195         # match single <mode>
1196         if ($line =~ m/\s(\d{6})$/) {
1197                 $line .= '<span class="info"> (' .
1198                          file_type_long($1) .
1199                          ')</span>';
1200         }
1201         # match <hash>
1202         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1203                 # can match only for combined diff
1204                 $line = 'index ';
1205                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1206                         if ($from->{'href'}[$i]) {
1207                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1208                                                   -class=>"hash"},
1209                                                  substr($diffinfo->{'from_id'}[$i],0,7));
1210                         } else {
1211                                 $line .= '0' x 7;
1212                         }
1213                         # separator
1214                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1215                 }
1216                 $line .= '..';
1217                 if ($to->{'href'}) {
1218                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1219                                          substr($diffinfo->{'to_id'},0,7));
1220                 } else {
1221                         $line .= '0' x 7;
1222                 }
1224         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1225                 # can match only for ordinary diff
1226                 my ($from_link, $to_link);
1227                 if ($from->{'href'}) {
1228                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1229                                              substr($diffinfo->{'from_id'},0,7));
1230                 } else {
1231                         $from_link = '0' x 7;
1232                 }
1233                 if ($to->{'href'}) {
1234                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1235                                            substr($diffinfo->{'to_id'},0,7));
1236                 } else {
1237                         $to_link = '0' x 7;
1238                 }
1239                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1240                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1241         }
1243         return $line . "<br/>\n";
1246 # format from-file/to-file diff header
1247 sub format_diff_from_to_header {
1248         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1249         my $line;
1250         my $result = '';
1252         $line = $from_line;
1253         #assert($line =~ m/^---/) if DEBUG;
1254         # no extra formatting for "^--- /dev/null"
1255         if (! $diffinfo->{'nparents'}) {
1256                 # ordinary (single parent) diff
1257                 if ($line =~ m!^--- "?a/!) {
1258                         if ($from->{'href'}) {
1259                                 $line = '--- a/' .
1260                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1261                                                 esc_path($from->{'file'}));
1262                         } else {
1263                                 $line = '--- a/' .
1264                                         esc_path($from->{'file'});
1265                         }
1266                 }
1267                 $result .= qq!<div class="diff from_file">$line</div>\n!;
1269         } else {
1270                 # combined diff (merge commit)
1271                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1272                         if ($from->{'href'}[$i]) {
1273                                 $line = '--- ' .
1274                                         $cgi->a({-href=>href(action=>"blobdiff",
1275                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
1276                                                              hash_parent_base=>$parents[$i],
1277                                                              file_parent=>$from->{'file'}[$i],
1278                                                              hash=>$diffinfo->{'to_id'},
1279                                                              hash_base=>$hash,
1280                                                              file_name=>$to->{'file'}),
1281                                                  -class=>"path",
1282                                                  -title=>"diff" . ($i+1)},
1283                                                 $i+1) .
1284                                         '/' .
1285                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1286                                                 esc_path($from->{'file'}[$i]));
1287                         } else {
1288                                 $line = '--- /dev/null';
1289                         }
1290                         $result .= qq!<div class="diff from_file">$line</div>\n!;
1291                 }
1292         }
1294         $line = $to_line;
1295         #assert($line =~ m/^\+\+\+/) if DEBUG;
1296         # no extra formatting for "^+++ /dev/null"
1297         if ($line =~ m!^\+\+\+ "?b/!) {
1298                 if ($to->{'href'}) {
1299                         $line = '+++ b/' .
1300                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1301                                         esc_path($to->{'file'}));
1302                 } else {
1303                         $line = '+++ b/' .
1304                                 esc_path($to->{'file'});
1305                 }
1306         }
1307         $result .= qq!<div class="diff to_file">$line</div>\n!;
1309         return $result;
1312 # create note for patch simplified by combined diff
1313 sub format_diff_cc_simplified {
1314         my ($diffinfo, @parents) = @_;
1315         my $result = '';
1317         $result .= "<div class=\"diff header\">" .
1318                    "diff --cc ";
1319         if (!is_deleted($diffinfo)) {
1320                 $result .= $cgi->a({-href => href(action=>"blob",
1321                                                   hash_base=>$hash,
1322                                                   hash=>$diffinfo->{'to_id'},
1323                                                   file_name=>$diffinfo->{'to_file'}),
1324                                     -class => "path"},
1325                                    esc_path($diffinfo->{'to_file'}));
1326         } else {
1327                 $result .= esc_path($diffinfo->{'to_file'});
1328         }
1329         $result .= "</div>\n" . # class="diff header"
1330                    "<div class=\"diff nodifferences\">" .
1331                    "Simple merge" .
1332                    "</div>\n"; # class="diff nodifferences"
1334         return $result;
1337 # format patch (diff) line (not to be used for diff headers)
1338 sub format_diff_line {
1339         my $line = shift;
1340         my ($from, $to) = @_;
1341         my $diff_class = "";
1343         chomp $line;
1345         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1346                 # combined diff
1347                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1348                 if ($line =~ m/^\@{3}/) {
1349                         $diff_class = " chunk_header";
1350                 } elsif ($line =~ m/^\\/) {
1351                         $diff_class = " incomplete";
1352                 } elsif ($prefix =~ tr/+/+/) {
1353                         $diff_class = " add";
1354                 } elsif ($prefix =~ tr/-/-/) {
1355                         $diff_class = " rem";
1356                 }
1357         } else {
1358                 # assume ordinary diff
1359                 my $char = substr($line, 0, 1);
1360                 if ($char eq '+') {
1361                         $diff_class = " add";
1362                 } elsif ($char eq '-') {
1363                         $diff_class = " rem";
1364                 } elsif ($char eq '@') {
1365                         $diff_class = " chunk_header";
1366                 } elsif ($char eq "\\") {
1367                         $diff_class = " incomplete";
1368                 }
1369         }
1370         $line = untabify($line);
1371         if ($from && $to && $line =~ m/^\@{2} /) {
1372                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1373                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1375                 $from_lines = 0 unless defined $from_lines;
1376                 $to_lines   = 0 unless defined $to_lines;
1378                 if ($from->{'href'}) {
1379                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1380                                              -class=>"list"}, $from_text);
1381                 }
1382                 if ($to->{'href'}) {
1383                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1384                                              -class=>"list"}, $to_text);
1385                 }
1386                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1387                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1388                 return "<div class=\"diff$diff_class\">$line</div>\n";
1389         } elsif ($from && $to && $line =~ m/^\@{3}/) {
1390                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1391                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1393                 @from_text = split(' ', $ranges);
1394                 for (my $i = 0; $i < @from_text; ++$i) {
1395                         ($from_start[$i], $from_nlines[$i]) =
1396                                 (split(',', substr($from_text[$i], 1)), 0);
1397                 }
1399                 $to_text   = pop @from_text;
1400                 $to_start  = pop @from_start;
1401                 $to_nlines = pop @from_nlines;
1403                 $line = "<span class=\"chunk_info\">$prefix ";
1404                 for (my $i = 0; $i < @from_text; ++$i) {
1405                         if ($from->{'href'}[$i]) {
1406                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1407                                                   -class=>"list"}, $from_text[$i]);
1408                         } else {
1409                                 $line .= $from_text[$i];
1410                         }
1411                         $line .= " ";
1412                 }
1413                 if ($to->{'href'}) {
1414                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1415                                           -class=>"list"}, $to_text);
1416                 } else {
1417                         $line .= $to_text;
1418                 }
1419                 $line .= " $prefix</span>" .
1420                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1421                 return "<div class=\"diff$diff_class\">$line</div>\n";
1422         }
1423         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1426 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1427 # linked.  Pass the hash of the tree/commit to snapshot.
1428 sub format_snapshot_links {
1429         my ($hash) = @_;
1430         my @snapshot_fmts = gitweb_check_feature('snapshot');
1431         @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1432         my $num_fmts = @snapshot_fmts;
1433         if ($num_fmts > 1) {
1434                 # A parenthesized list of links bearing format names.
1435                 # e.g. "snapshot (_tar.gz_ _zip_)"
1436                 return "snapshot (" . join(' ', map
1437                         $cgi->a({
1438                                 -href => href(
1439                                         action=>"snapshot",
1440                                         hash=>$hash,
1441                                         snapshot_format=>$_
1442                                 )
1443                         }, $known_snapshot_formats{$_}{'display'})
1444                 , @snapshot_fmts) . ")";
1445         } elsif ($num_fmts == 1) {
1446                 # A single "snapshot" link whose tooltip bears the format name.
1447                 # i.e. "_snapshot_"
1448                 my ($fmt) = @snapshot_fmts;
1449                 return
1450                         $cgi->a({
1451                                 -href => href(
1452                                         action=>"snapshot",
1453                                         hash=>$hash,
1454                                         snapshot_format=>$fmt
1455                                 ),
1456                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1457                         }, "snapshot");
1458         } else { # $num_fmts == 0
1459                 return undef;
1460         }
1463 ## ......................................................................
1464 ## functions returning values to be passed, perhaps after some
1465 ## transformation, to other functions; e.g. returning arguments to href()
1467 # returns hash to be passed to href to generate gitweb URL
1468 # in -title key it returns description of link
1469 sub get_feed_info {
1470         my $format = shift || 'Atom';
1471         my %res = (action => lc($format));
1473         # feed links are possible only for project views
1474         return unless (defined $project);
1475         # some views should link to OPML, or to generic project feed,
1476         # or don't have specific feed yet (so they should use generic)
1477         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1479         my $branch;
1480         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1481         # from tag links; this also makes possible to detect branch links
1482         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1483             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
1484                 $branch = $1;
1485         }
1486         # find log type for feed description (title)
1487         my $type = 'log';
1488         if (defined $file_name) {
1489                 $type  = "history of $file_name";
1490                 $type .= "/" if ($action eq 'tree');
1491                 $type .= " on '$branch'" if (defined $branch);
1492         } else {
1493                 $type = "log of $branch" if (defined $branch);
1494         }
1496         $res{-title} = $type;
1497         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1498         $res{'file_name'} = $file_name;
1500         return %res;
1503 ## ----------------------------------------------------------------------
1504 ## git utility subroutines, invoking git commands
1506 # returns path to the core git executable and the --git-dir parameter as list
1507 sub git_cmd {
1508         return $GIT, '--git-dir='.$git_dir;
1511 # quote the given arguments for passing them to the shell
1512 # quote_command("command", "arg 1", "arg with ' and ! characters")
1513 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1514 # Try to avoid using this function wherever possible.
1515 sub quote_command {
1516         return join(' ',
1517                     map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1520 # get HEAD ref of given project as hash
1521 sub git_get_head_hash {
1522         my $project = shift;
1523         my $o_git_dir = $git_dir;
1524         my $retval = undef;
1525         $git_dir = "$projectroot/$project";
1526         if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1527                 my $head = <$fd>;
1528                 close $fd;
1529                 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1530                         $retval = $1;
1531                 }
1532         }
1533         if (defined $o_git_dir) {
1534                 $git_dir = $o_git_dir;
1535         }
1536         return $retval;
1539 # get type of given object
1540 sub git_get_type {
1541         my $hash = shift;
1543         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1544         my $type = <$fd>;
1545         close $fd or return;
1546         chomp $type;
1547         return $type;
1550 # repository configuration
1551 our $config_file = '';
1552 our %config;
1554 # store multiple values for single key as anonymous array reference
1555 # single values stored directly in the hash, not as [ <value> ]
1556 sub hash_set_multi {
1557         my ($hash, $key, $value) = @_;
1559         if (!exists $hash->{$key}) {
1560                 $hash->{$key} = $value;
1561         } elsif (!ref $hash->{$key}) {
1562                 $hash->{$key} = [ $hash->{$key}, $value ];
1563         } else {
1564                 push @{$hash->{$key}}, $value;
1565         }
1568 # return hash of git project configuration
1569 # optionally limited to some section, e.g. 'gitweb'
1570 sub git_parse_project_config {
1571         my $section_regexp = shift;
1572         my %config;
1574         local $/ = "\0";
1576         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1577                 or return;
1579         while (my $keyval = <$fh>) {
1580                 chomp $keyval;
1581                 my ($key, $value) = split(/\n/, $keyval, 2);
1583                 hash_set_multi(\%config, $key, $value)
1584                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1585         }
1586         close $fh;
1588         return %config;
1591 # convert config value to boolean, 'true' or 'false'
1592 # no value, number > 0, 'true' and 'yes' values are true
1593 # rest of values are treated as false (never as error)
1594 sub config_to_bool {
1595         my $val = shift;
1597         # strip leading and trailing whitespace
1598         $val =~ s/^\s+//;
1599         $val =~ s/\s+$//;
1601         return (!defined $val ||               # section.key
1602                 ($val =~ /^\d+$/ && $val) ||   # section.key = 1
1603                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
1606 # convert config value to simple decimal number
1607 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1608 # to be multiplied by 1024, 1048576, or 1073741824
1609 sub config_to_int {
1610         my $val = shift;
1612         # strip leading and trailing whitespace
1613         $val =~ s/^\s+//;
1614         $val =~ s/\s+$//;
1616         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1617                 $unit = lc($unit);
1618                 # unknown unit is treated as 1
1619                 return $num * ($unit eq 'g' ? 1073741824 :
1620                                $unit eq 'm' ?    1048576 :
1621                                $unit eq 'k' ?       1024 : 1);
1622         }
1623         return $val;
1626 # convert config value to array reference, if needed
1627 sub config_to_multi {
1628         my $val = shift;
1630         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1633 sub git_get_project_config {
1634         my ($key, $type) = @_;
1636         # key sanity check
1637         return unless ($key);
1638         $key =~ s/^gitweb\.//;
1639         return if ($key =~ m/\W/);
1641         # type sanity check
1642         if (defined $type) {
1643                 $type =~ s/^--//;
1644                 $type = undef
1645                         unless ($type eq 'bool' || $type eq 'int');
1646         }
1648         # get config
1649         if (!defined $config_file ||
1650             $config_file ne "$git_dir/config") {
1651                 %config = git_parse_project_config('gitweb');
1652                 $config_file = "$git_dir/config";
1653         }
1655         # ensure given type
1656         if (!defined $type) {
1657                 return $config{"gitweb.$key"};
1658         } elsif ($type eq 'bool') {
1659                 # backward compatibility: 'git config --bool' returns true/false
1660                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1661         } elsif ($type eq 'int') {
1662                 return config_to_int($config{"gitweb.$key"});
1663         }
1664         return $config{"gitweb.$key"};
1667 # get hash of given path at given ref
1668 sub git_get_hash_by_path {
1669         my $base = shift;
1670         my $path = shift || return undef;
1671         my $type = shift;
1673         $path =~ s,/+$,,;
1675         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1676                 or die_error(500, "Open git-ls-tree failed");
1677         my $line = <$fd>;
1678         close $fd or return undef;
1680         if (!defined $line) {
1681                 # there is no tree or hash given by $path at $base
1682                 return undef;
1683         }
1685         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
1686         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1687         if (defined $type && $type ne $2) {
1688                 # type doesn't match
1689                 return undef;
1690         }
1691         return $3;
1694 # get path of entry with given hash at given tree-ish (ref)
1695 # used to get 'from' filename for combined diff (merge commit) for renames
1696 sub git_get_path_by_hash {
1697         my $base = shift || return;
1698         my $hash = shift || return;
1700         local $/ = "\0";
1702         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1703                 or return undef;
1704         while (my $line = <$fd>) {
1705                 chomp $line;
1707                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
1708                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
1709                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1710                         close $fd;
1711                         return $1;
1712                 }
1713         }
1714         close $fd;
1715         return undef;
1718 ## ......................................................................
1719 ## git utility functions, directly accessing git repository
1721 sub git_get_project_description {
1722         my $path = shift;
1724         $git_dir = "$projectroot/$path";
1725         open my $fd, "$git_dir/description"
1726                 or return git_get_project_config('description');
1727         my $descr = <$fd>;
1728         close $fd;
1729         if (defined $descr) {
1730                 chomp $descr;
1731         }
1732         return $descr;
1735 sub git_get_project_url_list {
1736         my $path = shift;
1738         $git_dir = "$projectroot/$path";
1739         open my $fd, "$git_dir/cloneurl"
1740                 or return wantarray ?
1741                 @{ config_to_multi(git_get_project_config('url')) } :
1742                    config_to_multi(git_get_project_config('url'));
1743         my @git_project_url_list = map { chomp; $_ } <$fd>;
1744         close $fd;
1746         return wantarray ? @git_project_url_list : \@git_project_url_list;
1749 sub git_get_projects_list {
1750         my ($filter) = @_;
1751         my @list;
1753         $filter ||= '';
1754         $filter =~ s/\.git$//;
1756         my ($check_forks) = gitweb_check_feature('forks');
1758         if (-d $projects_list) {
1759                 # search in directory
1760                 my $dir = $projects_list . ($filter ? "/$filter" : '');
1761                 # remove the trailing "/"
1762                 $dir =~ s!/+$!!;
1763                 my $pfxlen = length("$dir");
1764                 my $pfxdepth = ($dir =~ tr!/!!);
1766                 File::Find::find({
1767                         follow_fast => 1, # follow symbolic links
1768                         follow_skip => 2, # ignore duplicates
1769                         dangling_symlinks => 0, # ignore dangling symlinks, silently
1770                         wanted => sub {
1771                                 # skip project-list toplevel, if we get it.
1772                                 return if (m!^[/.]$!);
1773                                 # only directories can be git repositories
1774                                 return unless (-d $_);
1775                                 # don't traverse too deep (Find is super slow on os x)
1776                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1777                                         $File::Find::prune = 1;
1778                                         return;
1779                                 }
1781                                 my $subdir = substr($File::Find::name, $pfxlen + 1);
1782                                 # we check related file in $projectroot
1783                                 if ($check_forks and $subdir =~ m#/.#) {
1784                                         $File::Find::prune = 1;
1785                                 } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
1786                                         push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1787                                         $File::Find::prune = 1;
1788                                 }
1789                         },
1790                 }, "$dir");
1792         } elsif (-f $projects_list) {
1793                 # read from file(url-encoded):
1794                 # 'git%2Fgit.git Linus+Torvalds'
1795                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1796                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1797                 my %paths;
1798                 open my ($fd), $projects_list or return;
1799         PROJECT:
1800                 while (my $line = <$fd>) {
1801                         chomp $line;
1802                         my ($path, $owner) = split ' ', $line;
1803                         $path = unescape($path);
1804                         $owner = unescape($owner);
1805                         if (!defined $path) {
1806                                 next;
1807                         }
1808                         if ($filter ne '') {
1809                                 # looking for forks;
1810                                 my $pfx = substr($path, 0, length($filter));
1811                                 if ($pfx ne $filter) {
1812                                         next PROJECT;
1813                                 }
1814                                 my $sfx = substr($path, length($filter));
1815                                 if ($sfx !~ /^\/.*\.git$/) {
1816                                         next PROJECT;
1817                                 }
1818                         } elsif ($check_forks) {
1819                         PATH:
1820                                 foreach my $filter (keys %paths) {
1821                                         # looking for forks;
1822                                         my $pfx = substr($path, 0, length($filter));
1823                                         if ($pfx ne $filter) {
1824                                                 next PATH;
1825                                         }
1826                                         my $sfx = substr($path, length($filter));
1827                                         if ($sfx !~ /^\/.*\.git$/) {
1828                                                 next PATH;
1829                                         }
1830                                         # is a fork, don't include it in
1831                                         # the list
1832                                         next PROJECT;
1833                                 }
1834                         }
1835                         if (check_export_ok("$projectroot/$path")) {
1836                                 my $pr = {
1837                                         path => $path,
1838                                         owner => to_utf8($owner),
1839                                 };
1840                                 push @list, $pr;
1841                                 (my $forks_path = $path) =~ s/\.git$//;
1842                                 $paths{$forks_path}++;
1843                         }
1844                 }
1845                 close $fd;
1846         }
1847         return @list;
1850 our $gitweb_project_owner = undef;
1851 sub git_get_project_list_from_file {
1853         return if (defined $gitweb_project_owner);
1855         $gitweb_project_owner = {};
1856         # read from file (url-encoded):
1857         # 'git%2Fgit.git Linus+Torvalds'
1858         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1859         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1860         if (-f $projects_list) {
1861                 open (my $fd , $projects_list);
1862                 while (my $line = <$fd>) {
1863                         chomp $line;
1864                         my ($pr, $ow) = split ' ', $line;
1865                         $pr = unescape($pr);
1866                         $ow = unescape($ow);
1867                         $gitweb_project_owner->{$pr} = to_utf8($ow);
1868                 }
1869                 close $fd;
1870         }
1873 sub git_get_project_owner {
1874         my $project = shift;
1875         my $owner;
1877         return undef unless $project;
1878         $git_dir = "$projectroot/$project";
1880         if (!defined $gitweb_project_owner) {
1881                 git_get_project_list_from_file();
1882         }
1884         if (exists $gitweb_project_owner->{$project}) {
1885                 $owner = $gitweb_project_owner->{$project};
1886         }
1887         if (!defined $owner){
1888                 $owner = git_get_project_config('owner');
1889         }
1890         if (!defined $owner) {
1891                 $owner = get_file_owner("$git_dir");
1892         }
1894         return $owner;
1897 sub git_get_last_activity {
1898         my ($path) = @_;
1899         my $fd;
1901         $git_dir = "$projectroot/$path";
1902         open($fd, "-|", git_cmd(), 'for-each-ref',
1903              '--format=%(committer)',
1904              '--sort=-committerdate',
1905              '--count=1',
1906              'refs/heads') or return;
1907         my $most_recent = <$fd>;
1908         close $fd or return;
1909         if (defined $most_recent &&
1910             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1911                 my $timestamp = $1;
1912                 my $age = time - $timestamp;
1913                 return ($age, age_string($age));
1914         }
1915         return (undef, undef);
1918 sub git_get_references {
1919         my $type = shift || "";
1920         my %refs;
1921         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1922         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1923         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
1924                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
1925                 or return;
1927         while (my $line = <$fd>) {
1928                 chomp $line;
1929                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
1930                         if (defined $refs{$1}) {
1931                                 push @{$refs{$1}}, $2;
1932                         } else {
1933                                 $refs{$1} = [ $2 ];
1934                         }
1935                 }
1936         }
1937         close $fd or return;
1938         return \%refs;
1941 sub git_get_rev_name_tags {
1942         my $hash = shift || return undef;
1944         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
1945                 or return;
1946         my $name_rev = <$fd>;
1947         close $fd;
1949         if ($name_rev =~ m|^$hash tags/(.*)$|) {
1950                 return $1;
1951         } else {
1952                 # catches also '$hash undefined' output
1953                 return undef;
1954         }
1957 ## ----------------------------------------------------------------------
1958 ## parse to hash functions
1960 sub parse_date {
1961         my $epoch = shift;
1962         my $tz = shift || "-0000";
1964         my %date;
1965         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
1966         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
1967         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
1968         $date{'hour'} = $hour;
1969         $date{'minute'} = $min;
1970         $date{'mday'} = $mday;
1971         $date{'day'} = $days[$wday];
1972         $date{'month'} = $months[$mon];
1973         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
1974                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
1975         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
1976                              $mday, $months[$mon], $hour ,$min;
1977         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
1978                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
1980         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
1981         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
1982         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
1983         $date{'hour_local'} = $hour;
1984         $date{'minute_local'} = $min;
1985         $date{'tz_local'} = $tz;
1986         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
1987                                   1900+$year, $mon+1, $mday,
1988                                   $hour, $min, $sec, $tz);
1989         return %date;
1992 sub parse_tag {
1993         my $tag_id = shift;
1994         my %tag;
1995         my @comment;
1997         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
1998         $tag{'id'} = $tag_id;
1999         while (my $line = <$fd>) {
2000                 chomp $line;
2001                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2002                         $tag{'object'} = $1;
2003                 } elsif ($line =~ m/^type (.+)$/) {
2004                         $tag{'type'} = $1;
2005                 } elsif ($line =~ m/^tag (.+)$/) {
2006                         $tag{'name'} = $1;
2007                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2008                         $tag{'author'} = $1;
2009                         $tag{'epoch'} = $2;
2010                         $tag{'tz'} = $3;
2011                 } elsif ($line =~ m/--BEGIN/) {
2012                         push @comment, $line;
2013                         last;
2014                 } elsif ($line eq "") {
2015                         last;
2016                 }
2017         }
2018         push @comment, <$fd>;
2019         $tag{'comment'} = \@comment;
2020         close $fd or return;
2021         if (!defined $tag{'name'}) {
2022                 return
2023         };
2024         return %tag
2027 sub parse_commit_text {
2028         my ($commit_text, $withparents) = @_;
2029         my @commit_lines = split '\n', $commit_text;
2030         my %co;
2032         pop @commit_lines; # Remove '\0'
2034         if (! @commit_lines) {
2035                 return;
2036         }
2038         my $header = shift @commit_lines;
2039         if ($header !~ m/^[0-9a-fA-F]{40}/) {
2040                 return;
2041         }
2042         ($co{'id'}, my @parents) = split ' ', $header;
2043         while (my $line = shift @commit_lines) {
2044                 last if $line eq "\n";
2045                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2046                         $co{'tree'} = $1;
2047                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2048                         push @parents, $1;
2049                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2050                         $co{'author'} = $1;
2051                         $co{'author_epoch'} = $2;
2052                         $co{'author_tz'} = $3;
2053                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2054                                 $co{'author_name'}  = $1;
2055                                 $co{'author_email'} = $2;
2056                         } else {
2057                                 $co{'author_name'} = $co{'author'};
2058                         }
2059                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2060                         $co{'committer'} = $1;
2061                         $co{'committer_epoch'} = $2;
2062                         $co{'committer_tz'} = $3;
2063                         $co{'committer_name'} = $co{'committer'};
2064                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2065                                 $co{'committer_name'}  = $1;
2066                                 $co{'committer_email'} = $2;
2067                         } else {
2068                                 $co{'committer_name'} = $co{'committer'};
2069                         }
2070                 }
2071         }
2072         if (!defined $co{'tree'}) {
2073                 return;
2074         };
2075         $co{'parents'} = \@parents;
2076         $co{'parent'} = $parents[0];
2078         foreach my $title (@commit_lines) {
2079                 $title =~ s/^    //;
2080                 if ($title ne "") {
2081                         $co{'title'} = chop_str($title, 80, 5);
2082                         # remove leading stuff of merges to make the interesting part visible
2083                         if (length($title) > 50) {
2084                                 $title =~ s/^Automatic //;
2085                                 $title =~ s/^merge (of|with) /Merge ... /i;
2086                                 if (length($title) > 50) {
2087                                         $title =~ s/(http|rsync):\/\///;
2088                                 }
2089                                 if (length($title) > 50) {
2090                                         $title =~ s/(master|www|rsync)\.//;
2091                                 }
2092                                 if (length($title) > 50) {
2093                                         $title =~ s/kernel.org:?//;
2094                                 }
2095                                 if (length($title) > 50) {
2096                                         $title =~ s/\/pub\/scm//;
2097                                 }
2098                         }
2099                         $co{'title_short'} = chop_str($title, 50, 5);
2100                         last;
2101                 }
2102         }
2103         if (! defined $co{'title'} || $co{'title'} eq "") {
2104                 $co{'title'} = $co{'title_short'} = '(no commit message)';
2105         }
2106         # remove added spaces
2107         foreach my $line (@commit_lines) {
2108                 $line =~ s/^    //;
2109         }
2110         $co{'comment'} = \@commit_lines;
2112         my $age = time - $co{'committer_epoch'};
2113         $co{'age'} = $age;
2114         $co{'age_string'} = age_string($age);
2115         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2116         if ($age > 60*60*24*7*2) {
2117                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2118                 $co{'age_string_age'} = $co{'age_string'};
2119         } else {
2120                 $co{'age_string_date'} = $co{'age_string'};
2121                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2122         }
2123         return %co;
2126 sub parse_commit {
2127         my ($commit_id) = @_;
2128         my %co;
2130         local $/ = "\0";
2132         open my $fd, "-|", git_cmd(), "rev-list",
2133                 "--parents",
2134                 "--header",
2135                 "--max-count=1",
2136                 $commit_id,
2137                 "--",
2138                 or die_error(500, "Open git-rev-list failed");
2139         %co = parse_commit_text(<$fd>, 1);
2140         close $fd;
2142         return %co;
2145 sub parse_commits {
2146         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2147         my @cos;
2149         $maxcount ||= 1;
2150         $skip ||= 0;
2152         local $/ = "\0";
2154         open my $fd, "-|", git_cmd(), "rev-list",
2155                 "--header",
2156                 @args,
2157                 ("--max-count=" . $maxcount),
2158                 ("--skip=" . $skip),
2159                 @extra_options,
2160                 $commit_id,
2161                 "--",
2162                 ($filename ? ($filename) : ())
2163                 or die_error(500, "Open git-rev-list failed");
2164         while (my $line = <$fd>) {
2165                 my %co = parse_commit_text($line);
2166                 push @cos, \%co;
2167         }
2168         close $fd;
2170         return wantarray ? @cos : \@cos;
2173 # parse line of git-diff-tree "raw" output
2174 sub parse_difftree_raw_line {
2175         my $line = shift;
2176         my %res;
2178         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2179         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2180         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2181                 $res{'from_mode'} = $1;
2182                 $res{'to_mode'} = $2;
2183                 $res{'from_id'} = $3;
2184                 $res{'to_id'} = $4;
2185                 $res{'status'} = $5;
2186                 $res{'similarity'} = $6;
2187                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2188                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2189                 } else {
2190                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2191                 }
2192         }
2193         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2194         # combined diff (for merge commit)
2195         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2196                 $res{'nparents'}  = length($1);
2197                 $res{'from_mode'} = [ split(' ', $2) ];
2198                 $res{'to_mode'} = pop @{$res{'from_mode'}};
2199                 $res{'from_id'} = [ split(' ', $3) ];
2200                 $res{'to_id'} = pop @{$res{'from_id'}};
2201                 $res{'status'} = [ split('', $4) ];
2202                 $res{'to_file'} = unquote($5);
2203         }
2204         # 'c512b523472485aef4fff9e57b229d9d243c967f'
2205         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2206                 $res{'commit'} = $1;
2207         }
2209         return wantarray ? %res : \%res;
2212 # wrapper: return parsed line of git-diff-tree "raw" output
2213 # (the argument might be raw line, or parsed info)
2214 sub parsed_difftree_line {
2215         my $line_or_ref = shift;
2217         if (ref($line_or_ref) eq "HASH") {
2218                 # pre-parsed (or generated by hand)
2219                 return $line_or_ref;
2220         } else {
2221                 return parse_difftree_raw_line($line_or_ref);
2222         }
2225 # parse line of git-ls-tree output
2226 sub parse_ls_tree_line ($;%) {
2227         my $line = shift;
2228         my %opts = @_;
2229         my %res;
2231         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2232         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2234         $res{'mode'} = $1;
2235         $res{'type'} = $2;
2236         $res{'hash'} = $3;
2237         if ($opts{'-z'}) {
2238                 $res{'name'} = $4;
2239         } else {
2240                 $res{'name'} = unquote($4);
2241         }
2243         return wantarray ? %res : \%res;
2246 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2247 sub parse_from_to_diffinfo {
2248         my ($diffinfo, $from, $to, @parents) = @_;
2250         if ($diffinfo->{'nparents'}) {
2251                 # combined diff
2252                 $from->{'file'} = [];
2253                 $from->{'href'} = [];
2254                 fill_from_file_info($diffinfo, @parents)
2255                         unless exists $diffinfo->{'from_file'};
2256                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2257                         $from->{'file'}[$i] =
2258                                 defined $diffinfo->{'from_file'}[$i] ?
2259                                         $diffinfo->{'from_file'}[$i] :
2260                                         $diffinfo->{'to_file'};
2261                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2262                                 $from->{'href'}[$i] = href(action=>"blob",
2263                                                            hash_base=>$parents[$i],
2264                                                            hash=>$diffinfo->{'from_id'}[$i],
2265                                                            file_name=>$from->{'file'}[$i]);
2266                         } else {
2267                                 $from->{'href'}[$i] = undef;
2268                         }
2269                 }
2270         } else {
2271                 # ordinary (not combined) diff
2272                 $from->{'file'} = $diffinfo->{'from_file'};
2273                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2274                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2275                                                hash=>$diffinfo->{'from_id'},
2276                                                file_name=>$from->{'file'});
2277                 } else {
2278                         delete $from->{'href'};
2279                 }
2280         }
2282         $to->{'file'} = $diffinfo->{'to_file'};
2283         if (!is_deleted($diffinfo)) { # file exists in result
2284                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2285                                      hash=>$diffinfo->{'to_id'},
2286                                      file_name=>$to->{'file'});
2287         } else {
2288                 delete $to->{'href'};
2289         }
2292 ## ......................................................................
2293 ## parse to array of hashes functions
2295 sub git_get_heads_list {
2296         my $limit = shift;
2297         my @headslist;
2299         open my $fd, '-|', git_cmd(), 'for-each-ref',
2300                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2301                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2302                 'refs/heads'
2303                 or return;
2304         while (my $line = <$fd>) {
2305                 my %ref_item;
2307                 chomp $line;
2308                 my ($refinfo, $committerinfo) = split(/\0/, $line);
2309                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2310                 my ($committer, $epoch, $tz) =
2311                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2312                 $ref_item{'fullname'}  = $name;
2313                 $name =~ s!^refs/heads/!!;
2315                 $ref_item{'name'}  = $name;
2316                 $ref_item{'id'}    = $hash;
2317                 $ref_item{'title'} = $title || '(no commit message)';
2318                 $ref_item{'epoch'} = $epoch;
2319                 if ($epoch) {
2320                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2321                 } else {
2322                         $ref_item{'age'} = "unknown";
2323                 }
2325                 push @headslist, \%ref_item;
2326         }
2327         close $fd;
2329         return wantarray ? @headslist : \@headslist;
2332 sub git_get_tags_list {
2333         my $limit = shift;
2334         my @tagslist;
2336         open my $fd, '-|', git_cmd(), 'for-each-ref',
2337                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2338                 '--format=%(objectname) %(objecttype) %(refname) '.
2339                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2340                 'refs/tags'
2341                 or return;
2342         while (my $line = <$fd>) {
2343                 my %ref_item;
2345                 chomp $line;
2346                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2347                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2348                 my ($creator, $epoch, $tz) =
2349                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2350                 $ref_item{'fullname'} = $name;
2351                 $name =~ s!^refs/tags/!!;
2353                 $ref_item{'type'} = $type;
2354                 $ref_item{'id'} = $id;
2355                 $ref_item{'name'} = $name;
2356                 if ($type eq "tag") {
2357                         $ref_item{'subject'} = $title;
2358                         $ref_item{'reftype'} = $reftype;
2359                         $ref_item{'refid'}   = $refid;
2360                 } else {
2361                         $ref_item{'reftype'} = $type;
2362                         $ref_item{'refid'}   = $id;
2363                 }
2365                 if ($type eq "tag" || $type eq "commit") {
2366                         $ref_item{'epoch'} = $epoch;
2367                         if ($epoch) {
2368                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2369                         } else {
2370                                 $ref_item{'age'} = "unknown";
2371                         }
2372                 }
2374                 push @tagslist, \%ref_item;
2375         }
2376         close $fd;
2378         return wantarray ? @tagslist : \@tagslist;
2381 ## ----------------------------------------------------------------------
2382 ## filesystem-related functions
2384 sub get_file_owner {
2385         my $path = shift;
2387         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2388         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2389         if (!defined $gcos) {
2390                 return undef;
2391         }
2392         my $owner = $gcos;
2393         $owner =~ s/[,;].*$//;
2394         return to_utf8($owner);
2397 ## ......................................................................
2398 ## mimetype related functions
2400 sub mimetype_guess_file {
2401         my $filename = shift;
2402         my $mimemap = shift;
2403         -r $mimemap or return undef;
2405         my %mimemap;
2406         open(MIME, $mimemap) or return undef;
2407         while (<MIME>) {
2408                 next if m/^#/; # skip comments
2409                 my ($mime, $exts) = split(/\t+/);
2410                 if (defined $exts) {
2411                         my @exts = split(/\s+/, $exts);
2412                         foreach my $ext (@exts) {
2413                                 $mimemap{$ext} = $mime;
2414                         }
2415                 }
2416         }
2417         close(MIME);
2419         $filename =~ /\.([^.]*)$/;
2420         return $mimemap{$1};
2423 sub mimetype_guess {
2424         my $filename = shift;
2425         my $mime;
2426         $filename =~ /\./ or return undef;
2428         if ($mimetypes_file) {
2429                 my $file = $mimetypes_file;
2430                 if ($file !~ m!^/!) { # if it is relative path
2431                         # it is relative to project
2432                         $file = "$projectroot/$project/$file";
2433                 }
2434                 $mime = mimetype_guess_file($filename, $file);
2435         }
2436         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2437         return $mime;
2440 sub blob_mimetype {
2441         my $fd = shift;
2442         my $filename = shift;
2444         if ($filename) {
2445                 my $mime = mimetype_guess($filename);
2446                 $mime and return $mime;
2447         }
2449         # just in case
2450         return $default_blob_plain_mimetype unless $fd;
2452         if (-T $fd) {
2453                 return 'text/plain';
2454         } elsif (! $filename) {
2455                 return 'application/octet-stream';
2456         } elsif ($filename =~ m/\.png$/i) {
2457                 return 'image/png';
2458         } elsif ($filename =~ m/\.gif$/i) {
2459                 return 'image/gif';
2460         } elsif ($filename =~ m/\.jpe?g$/i) {
2461                 return 'image/jpeg';
2462         } else {
2463                 return 'application/octet-stream';
2464         }
2467 sub blob_contenttype {
2468         my ($fd, $file_name, $type) = @_;
2470         $type ||= blob_mimetype($fd, $file_name);
2471         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2472                 $type .= "; charset=$default_text_plain_charset";
2473         }
2475         return $type;
2478 ## ======================================================================
2479 ## functions printing HTML: header, footer, error page
2481 sub git_header_html {
2482         my $status = shift || "200 OK";
2483         my $expires = shift;
2485         my $title = "$site_name";
2486         if (defined $project) {
2487                 $title .= " - " . to_utf8($project);
2488                 if (defined $action) {
2489                         $title .= "/$action";
2490                         if (defined $file_name) {
2491                                 $title .= " - " . esc_path($file_name);
2492                                 if ($action eq "tree" && $file_name !~ m|/$|) {
2493                                         $title .= "/";
2494                                 }
2495                         }
2496                 }
2497         }
2498         my $content_type;
2499         # require explicit support from the UA if we are to send the page as
2500         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2501         # we have to do this because MSIE sometimes globs '*/*', pretending to
2502         # support xhtml+xml but choking when it gets what it asked for.
2503         if (defined $cgi->http('HTTP_ACCEPT') &&
2504             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2505             $cgi->Accept('application/xhtml+xml') != 0) {
2506                 $content_type = 'application/xhtml+xml';
2507         } else {
2508                 $content_type = 'text/html';
2509         }
2510         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2511                            -status=> $status, -expires => $expires);
2512         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2513         print <<EOF;
2514 <?xml version="1.0" encoding="utf-8"?>
2515 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2516 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2517 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2518 <!-- git core binaries version $git_version -->
2519 <head>
2520 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2521 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2522 <meta name="robots" content="index, nofollow"/>
2523 <title>$title</title>
2524 EOF
2525 # print out each stylesheet that exist
2526         if (defined $stylesheet) {
2527 #provides backwards capability for those people who define style sheet in a config file
2528                 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2529         } else {
2530                 foreach my $stylesheet (@stylesheets) {
2531                         next unless $stylesheet;
2532                         print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2533                 }
2534         }
2535         if (defined $project) {
2536                 my %href_params = get_feed_info();
2537                 if (!exists $href_params{'-title'}) {
2538                         $href_params{'-title'} = 'log';
2539                 }
2541                 foreach my $format qw(RSS Atom) {
2542                         my $type = lc($format);
2543                         my %link_attr = (
2544                                 '-rel' => 'alternate',
2545                                 '-title' => "$project - $href_params{'-title'} - $format feed",
2546                                 '-type' => "application/$type+xml"
2547                         );
2549                         $href_params{'action'} = $type;
2550                         $link_attr{'-href'} = href(%href_params);
2551                         print "<link ".
2552                               "rel=\"$link_attr{'-rel'}\" ".
2553                               "title=\"$link_attr{'-title'}\" ".
2554                               "href=\"$link_attr{'-href'}\" ".
2555                               "type=\"$link_attr{'-type'}\" ".
2556                               "/>\n";
2558                         $href_params{'extra_options'} = '--no-merges';
2559                         $link_attr{'-href'} = href(%href_params);
2560                         $link_attr{'-title'} .= ' (no merges)';
2561                         print "<link ".
2562                               "rel=\"$link_attr{'-rel'}\" ".
2563                               "title=\"$link_attr{'-title'}\" ".
2564                               "href=\"$link_attr{'-href'}\" ".
2565                               "type=\"$link_attr{'-type'}\" ".
2566                               "/>\n";
2567                 }
2569         } else {
2570                 printf('<link rel="alternate" title="%s projects list" '.
2571                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
2572                        $site_name, href(project=>undef, action=>"project_index"));
2573                 printf('<link rel="alternate" title="%s projects feeds" '.
2574                        'href="%s" type="text/x-opml" />'."\n",
2575                        $site_name, href(project=>undef, action=>"opml"));
2576         }
2577         if (defined $favicon) {
2578                 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2579         }
2581         print "</head>\n" .
2582               "<body>\n";
2584         if (-f $site_header) {
2585                 open (my $fd, $site_header);
2586                 print <$fd>;
2587                 close $fd;
2588         }
2590         print "<div class=\"page_header\">\n" .
2591               $cgi->a({-href => esc_url($logo_url),
2592                        -title => $logo_label},
2593                       qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2594         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2595         if (defined $project) {
2596                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2597                 if (defined $action) {
2598                         print " / $action";
2599                 }
2600                 print "\n";
2601         }
2602         print "</div>\n";
2604         my ($have_search) = gitweb_check_feature('search');
2605         if (defined $project && $have_search) {
2606                 if (!defined $searchtext) {
2607                         $searchtext = "";
2608                 }
2609                 my $search_hash;
2610                 if (defined $hash_base) {
2611                         $search_hash = $hash_base;
2612                 } elsif (defined $hash) {
2613                         $search_hash = $hash;
2614                 } else {
2615                         $search_hash = "HEAD";
2616                 }
2617                 my $action = $my_uri;
2618                 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2619                 if ($use_pathinfo) {
2620                         $action .= "/".esc_url($project);
2621                 }
2622                 print $cgi->startform(-method => "get", -action => $action) .
2623                       "<div class=\"search\">\n" .
2624                       (!$use_pathinfo &&
2625                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2626                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2627                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2628                       $cgi->popup_menu(-name => 'st', -default => 'commit',
2629                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2630                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2631                       " search:\n",
2632                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2633                       "<span title=\"Extended regular expression\">" .
2634                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2635                                      -checked => $search_use_regexp) .
2636                       "</span>" .
2637                       "</div>" .
2638                       $cgi->end_form() . "\n";
2639         }
2642 sub git_footer_html {
2643         my $feed_class = 'rss_logo';
2645         print "<div class=\"page_footer\">\n";
2646         if (defined $project) {
2647                 my $descr = git_get_project_description($project);
2648                 if (defined $descr) {
2649                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2650                 }
2652                 my %href_params = get_feed_info();
2653                 if (!%href_params) {
2654                         $feed_class .= ' generic';
2655                 }
2656                 $href_params{'-title'} ||= 'log';
2658                 foreach my $format qw(RSS Atom) {
2659                         $href_params{'action'} = lc($format);
2660                         print $cgi->a({-href => href(%href_params),
2661                                       -title => "$href_params{'-title'} $format feed",
2662                                       -class => $feed_class}, $format)."\n";
2663                 }
2665         } else {
2666                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2667                               -class => $feed_class}, "OPML") . " ";
2668                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2669                               -class => $feed_class}, "TXT") . "\n";
2670         }
2671         print "</div>\n"; # class="page_footer"
2673         if (-f $site_footer) {
2674                 open (my $fd, $site_footer);
2675                 print <$fd>;
2676                 close $fd;
2677         }
2679         print "</body>\n" .
2680               "</html>";
2683 # die_error(<http_status_code>, <error_message>)
2684 # Example: die_error(404, 'Hash not found')
2685 # By convention, use the following status codes (as defined in RFC 2616):
2686 # 400: Invalid or missing CGI parameters, or
2687 #      requested object exists but has wrong type.
2688 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2689 #      this server or project.
2690 # 404: Requested object/revision/project doesn't exist.
2691 # 500: The server isn't configured properly, or
2692 #      an internal error occurred (e.g. failed assertions caused by bugs), or
2693 #      an unknown error occurred (e.g. the git binary died unexpectedly).
2694 sub die_error {
2695         my $status = shift || 500;
2696         my $error = shift || "Internal server error";
2698         my %http_responses = (400 => '400 Bad Request',
2699                               403 => '403 Forbidden',
2700                               404 => '404 Not Found',
2701                               500 => '500 Internal Server Error');
2702         git_header_html($http_responses{$status});
2703         print <<EOF;
2704 <div class="page_body">
2705 <br /><br />
2706 $status - $error
2707 <br />
2708 </div>
2709 EOF
2710         git_footer_html();
2711         exit;
2714 ## ----------------------------------------------------------------------
2715 ## functions printing or outputting HTML: navigation
2717 sub git_print_page_nav {
2718         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2719         $extra = '' if !defined $extra; # pager or formats
2721         my @navs = qw(summary shortlog log commit commitdiff tree);
2722         if ($suppress) {
2723                 @navs = grep { $_ ne $suppress } @navs;
2724         }
2726         my %arg = map { $_ => {action=>$_} } @navs;
2727         if (defined $head) {
2728                 for (qw(commit commitdiff)) {
2729                         $arg{$_}{'hash'} = $head;
2730                 }
2731                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2732                         for (qw(shortlog log)) {
2733                                 $arg{$_}{'hash'} = $head;
2734                         }
2735                 }
2736         }
2737         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2738         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2740         print "<div class=\"page_nav\">\n" .
2741                 (join " | ",
2742                  map { $_ eq $current ?
2743                        $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2744                  } @navs);
2745         print "<br/>\n$extra<br/>\n" .
2746               "</div>\n";
2749 sub format_paging_nav {
2750         my ($action, $hash, $head, $page, $has_next_link) = @_;
2751         my $paging_nav;
2754         if ($hash ne $head || $page) {
2755                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2756         } else {
2757                 $paging_nav .= "HEAD";
2758         }
2760         if ($page > 0) {
2761                 $paging_nav .= " &sdot; " .
2762                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
2763                                  -accesskey => "p", -title => "Alt-p"}, "prev");
2764         } else {
2765                 $paging_nav .= " &sdot; prev";
2766         }
2768         if ($has_next_link) {
2769                 $paging_nav .= " &sdot; " .
2770                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
2771                                  -accesskey => "n", -title => "Alt-n"}, "next");
2772         } else {
2773                 $paging_nav .= " &sdot; next";
2774         }
2776         return $paging_nav;
2779 ## ......................................................................
2780 ## functions printing or outputting HTML: div
2782 sub git_print_header_div {
2783         my ($action, $title, $hash, $hash_base) = @_;
2784         my %args = ();
2786         $args{'action'} = $action;
2787         $args{'hash'} = $hash if $hash;
2788         $args{'hash_base'} = $hash_base if $hash_base;
2790         print "<div class=\"header\">\n" .
2791               $cgi->a({-href => href(%args), -class => "title"},
2792               $title ? $title : $action) .
2793               "\n</div>\n";
2796 #sub git_print_authorship (\%) {
2797 sub git_print_authorship {
2798         my $co = shift;
2800         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2801         print "<div class=\"author_date\">" .
2802               esc_html($co->{'author_name'}) .
2803               " [$ad{'rfc2822'}";
2804         if ($ad{'hour_local'} < 6) {
2805                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2806                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2807         } else {
2808                 printf(" (%02d:%02d %s)",
2809                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2810         }
2811         print "]</div>\n";
2814 sub git_print_page_path {
2815         my $name = shift;
2816         my $type = shift;
2817         my $hb = shift;
2820         print "<div class=\"page_path\">";
2821         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2822                       -title => 'tree root'}, to_utf8("[$project]"));
2823         print " / ";
2824         if (defined $name) {
2825                 my @dirname = split '/', $name;
2826                 my $basename = pop @dirname;
2827                 my $fullname = '';
2829                 foreach my $dir (@dirname) {
2830                         $fullname .= ($fullname ? '/' : '') . $dir;
2831                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2832                                                      hash_base=>$hb),
2833                                       -title => $fullname}, esc_path($dir));
2834                         print " / ";
2835                 }
2836                 if (defined $type && $type eq 'blob') {
2837                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2838                                                      hash_base=>$hb),
2839                                       -title => $name}, esc_path($basename));
2840                 } elsif (defined $type && $type eq 'tree') {
2841                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2842                                                      hash_base=>$hb),
2843                                       -title => $name}, esc_path($basename));
2844                         print " / ";
2845                 } else {
2846                         print esc_path($basename);
2847                 }
2848         }
2849         print "<br/></div>\n";
2852 # sub git_print_log (\@;%) {
2853 sub git_print_log ($;%) {
2854         my $log = shift;
2855         my %opts = @_;
2857         if ($opts{'-remove_title'}) {
2858                 # remove title, i.e. first line of log
2859                 shift @$log;
2860         }
2861         # remove leading empty lines
2862         while (defined $log->[0] && $log->[0] eq "") {
2863                 shift @$log;
2864         }
2866         # print log
2867         my $signoff = 0;
2868         my $empty = 0;
2869         foreach my $line (@$log) {
2870                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2871                         $signoff = 1;
2872                         $empty = 0;
2873                         if (! $opts{'-remove_signoff'}) {
2874                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2875                                 next;
2876                         } else {
2877                                 # remove signoff lines
2878                                 next;
2879                         }
2880                 } else {
2881                         $signoff = 0;
2882                 }
2884                 # print only one empty line
2885                 # do not print empty line after signoff
2886                 if ($line eq "") {
2887                         next if ($empty || $signoff);
2888                         $empty = 1;
2889                 } else {
2890                         $empty = 0;
2891                 }
2893                 print format_log_line_html($line) . "<br/>\n";
2894         }
2896         if ($opts{'-final_empty_line'}) {
2897                 # end with single empty line
2898                 print "<br/>\n" unless $empty;
2899         }
2902 # return link target (what link points to)
2903 sub git_get_link_target {
2904         my $hash = shift;
2905         my $link_target;
2907         # read link
2908         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2909                 or return;
2910         {
2911                 local $/;
2912                 $link_target = <$fd>;
2913         }
2914         close $fd
2915                 or return;
2917         return $link_target;
2920 # given link target, and the directory (basedir) the link is in,
2921 # return target of link relative to top directory (top tree);
2922 # return undef if it is not possible (including absolute links).
2923 sub normalize_link_target {
2924         my ($link_target, $basedir, $hash_base) = @_;
2926         # we can normalize symlink target only if $hash_base is provided
2927         return unless $hash_base;
2929         # absolute symlinks (beginning with '/') cannot be normalized
2930         return if (substr($link_target, 0, 1) eq '/');
2932         # normalize link target to path from top (root) tree (dir)
2933         my $path;
2934         if ($basedir) {
2935                 $path = $basedir . '/' . $link_target;
2936         } else {
2937                 # we are in top (root) tree (dir)
2938                 $path = $link_target;
2939         }
2941         # remove //, /./, and /../
2942         my @path_parts;
2943         foreach my $part (split('/', $path)) {
2944                 # discard '.' and ''
2945                 next if (!$part || $part eq '.');
2946                 # handle '..'
2947                 if ($part eq '..') {
2948                         if (@path_parts) {
2949                                 pop @path_parts;
2950                         } else {
2951                                 # link leads outside repository (outside top dir)
2952                                 return;
2953                         }
2954                 } else {
2955                         push @path_parts, $part;
2956                 }
2957         }
2958         $path = join('/', @path_parts);
2960         return $path;
2963 # print tree entry (row of git_tree), but without encompassing <tr> element
2964 sub git_print_tree_entry {
2965         my ($t, $basedir, $hash_base, $have_blame) = @_;
2967         my %base_key = ();
2968         $base_key{'hash_base'} = $hash_base if defined $hash_base;
2970         # The format of a table row is: mode list link.  Where mode is
2971         # the mode of the entry, list is the name of the entry, an href,
2972         # and link is the action links of the entry.
2974         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
2975         if ($t->{'type'} eq "blob") {
2976                 print "<td class=\"list\">" .
2977                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
2978                                                file_name=>"$basedir$t->{'name'}", %base_key),
2979                                 -class => "list"}, esc_path($t->{'name'}));
2980                 if (S_ISLNK(oct $t->{'mode'})) {
2981                         my $link_target = git_get_link_target($t->{'hash'});
2982                         if ($link_target) {
2983                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
2984                                 if (defined $norm_target) {
2985                                         print " -> " .
2986                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
2987                                                                      file_name=>$norm_target),
2988                                                        -title => $norm_target}, esc_path($link_target));
2989                                 } else {
2990                                         print " -> " . esc_path($link_target);
2991                                 }
2992                         }
2993                 }
2994                 print "</td>\n";
2995                 print "<td class=\"link\">";
2996                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
2997                                              file_name=>"$basedir$t->{'name'}", %base_key)},
2998                               "blob");
2999                 if ($have_blame) {
3000                         print " | " .
3001                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3002                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3003                                       "blame");
3004                 }
3005                 if (defined $hash_base) {
3006                         print " | " .
3007                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3008                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3009                                       "history");
3010                 }
3011                 print " | " .
3012                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3013                                                file_name=>"$basedir$t->{'name'}")},
3014                                 "raw");
3015                 print "</td>\n";
3017         } elsif ($t->{'type'} eq "tree") {
3018                 print "<td class=\"list\">";
3019                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3020                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3021                               esc_path($t->{'name'}));
3022                 print "</td>\n";
3023                 print "<td class=\"link\">";
3024                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3025                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3026                               "tree");
3027                 if (defined $hash_base) {
3028                         print " | " .
3029                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3030                                                      file_name=>"$basedir$t->{'name'}")},
3031                                       "history");
3032                 }
3033                 print "</td>\n";
3034         } else {
3035                 # unknown object: we can only present history for it
3036                 # (this includes 'commit' object, i.e. submodule support)
3037                 print "<td class=\"list\">" .
3038                       esc_path($t->{'name'}) .
3039                       "</td>\n";
3040                 print "<td class=\"link\">";
3041                 if (defined $hash_base) {
3042                         print $cgi->a({-href => href(action=>"history",
3043                                                      hash_base=>$hash_base,
3044                                                      file_name=>"$basedir$t->{'name'}")},
3045                                       "history");
3046                 }
3047                 print "</td>\n";
3048         }
3051 ## ......................................................................
3052 ## functions printing large fragments of HTML
3054 # get pre-image filenames for merge (combined) diff
3055 sub fill_from_file_info {
3056         my ($diff, @parents) = @_;
3058         $diff->{'from_file'} = [ ];
3059         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3060         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3061                 if ($diff->{'status'}[$i] eq 'R' ||
3062                     $diff->{'status'}[$i] eq 'C') {
3063                         $diff->{'from_file'}[$i] =
3064                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3065                 }
3066         }
3068         return $diff;
3071 # is current raw difftree line of file deletion
3072 sub is_deleted {
3073         my $diffinfo = shift;
3075         return $diffinfo->{'to_id'} eq ('0' x 40);
3078 # does patch correspond to [previous] difftree raw line
3079 # $diffinfo  - hashref of parsed raw diff format
3080 # $patchinfo - hashref of parsed patch diff format
3081 #              (the same keys as in $diffinfo)
3082 sub is_patch_split {
3083         my ($diffinfo, $patchinfo) = @_;
3085         return defined $diffinfo && defined $patchinfo
3086                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3090 sub git_difftree_body {
3091         my ($difftree, $hash, @parents) = @_;
3092         my ($parent) = $parents[0];
3093         my ($have_blame) = gitweb_check_feature('blame');
3094         print "<div class=\"list_head\">\n";
3095         if ($#{$difftree} > 10) {
3096                 print(($#{$difftree} + 1) . " files changed:\n");
3097         }
3098         print "</div>\n";
3100         print "<table class=\"" .
3101               (@parents > 1 ? "combined " : "") .
3102               "diff_tree\">\n";
3104         # header only for combined diff in 'commitdiff' view
3105         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3106         if ($has_header) {
3107                 # table header
3108                 print "<thead><tr>\n" .
3109                        "<th></th><th></th>\n"; # filename, patchN link
3110                 for (my $i = 0; $i < @parents; $i++) {
3111                         my $par = $parents[$i];
3112                         print "<th>" .
3113                               $cgi->a({-href => href(action=>"commitdiff",
3114                                                      hash=>$hash, hash_parent=>$par),
3115                                        -title => 'commitdiff to parent number ' .
3116                                                   ($i+1) . ': ' . substr($par,0,7)},
3117                                       $i+1) .
3118                               "&nbsp;</th>\n";
3119                 }
3120                 print "</tr></thead>\n<tbody>\n";
3121         }
3123         my $alternate = 1;
3124         my $patchno = 0;
3125         foreach my $line (@{$difftree}) {
3126                 my $diff = parsed_difftree_line($line);
3128                 if ($alternate) {
3129                         print "<tr class=\"dark\">\n";
3130                 } else {
3131                         print "<tr class=\"light\">\n";
3132                 }
3133                 $alternate ^= 1;
3135                 if (exists $diff->{'nparents'}) { # combined diff
3137                         fill_from_file_info($diff, @parents)
3138                                 unless exists $diff->{'from_file'};
3140                         if (!is_deleted($diff)) {
3141                                 # file exists in the result (child) commit
3142                                 print "<td>" .
3143                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3144                                                              file_name=>$diff->{'to_file'},
3145                                                              hash_base=>$hash),
3146                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3147                                       "</td>\n";
3148                         } else {
3149                                 print "<td>" .
3150                                       esc_path($diff->{'to_file'}) .
3151                                       "</td>\n";
3152                         }
3154                         if ($action eq 'commitdiff') {
3155                                 # link to patch
3156                                 $patchno++;
3157                                 print "<td class=\"link\">" .
3158                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3159                                       " | " .
3160                                       "</td>\n";
3161                         }
3163                         my $has_history = 0;
3164                         my $not_deleted = 0;
3165                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3166                                 my $hash_parent = $parents[$i];
3167                                 my $from_hash = $diff->{'from_id'}[$i];
3168                                 my $from_path = $diff->{'from_file'}[$i];
3169                                 my $status = $diff->{'status'}[$i];
3171                                 $has_history ||= ($status ne 'A');
3172                                 $not_deleted ||= ($status ne 'D');
3174                                 if ($status eq 'A') {
3175                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3176                                 } elsif ($status eq 'D') {
3177                                         print "<td class=\"link\">" .
3178                                               $cgi->a({-href => href(action=>"blob",
3179                                                                      hash_base=>$hash,
3180                                                                      hash=>$from_hash,
3181                                                                      file_name=>$from_path)},
3182                                                       "blob" . ($i+1)) .
3183                                               " | </td>\n";
3184                                 } else {
3185                                         if ($diff->{'to_id'} eq $from_hash) {
3186                                                 print "<td class=\"link nochange\">";
3187                                         } else {
3188                                                 print "<td class=\"link\">";
3189                                         }
3190                                         print $cgi->a({-href => href(action=>"blobdiff",
3191                                                                      hash=>$diff->{'to_id'},
3192                                                                      hash_parent=>$from_hash,
3193                                                                      hash_base=>$hash,
3194                                                                      hash_parent_base=>$hash_parent,
3195                                                                      file_name=>$diff->{'to_file'},
3196                                                                      file_parent=>$from_path)},
3197                                                       "diff" . ($i+1)) .
3198                                               " | </td>\n";
3199                                 }
3200                         }
3202                         print "<td class=\"link\">";
3203                         if ($not_deleted) {
3204                                 print $cgi->a({-href => href(action=>"blob",
3205                                                              hash=>$diff->{'to_id'},
3206                                                              file_name=>$diff->{'to_file'},
3207                                                              hash_base=>$hash)},
3208                                               "blob");
3209                                 print " | " if ($has_history);
3210                         }
3211                         if ($has_history) {
3212                                 print $cgi->a({-href => href(action=>"history",
3213                                                              file_name=>$diff->{'to_file'},
3214                                                              hash_base=>$hash)},
3215                                               "history");
3216                         }
3217                         print "</td>\n";
3219                         print "</tr>\n";
3220                         next; # instead of 'else' clause, to avoid extra indent
3221                 }
3222                 # else ordinary diff
3224                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3225                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3226                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3227                         $to_mode_oct = oct $diff->{'to_mode'};
3228                         if (S_ISREG($to_mode_oct)) { # only for regular file
3229                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3230                         }
3231                         $to_file_type = file_type($diff->{'to_mode'});
3232                 }
3233                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3234                         $from_mode_oct = oct $diff->{'from_mode'};
3235                         if (S_ISREG($to_mode_oct)) { # only for regular file
3236                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3237                         }
3238                         $from_file_type = file_type($diff->{'from_mode'});
3239                 }
3241                 if ($diff->{'status'} eq "A") { # created
3242                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3243                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3244                         $mode_chng   .= "]</span>";
3245                         print "<td>";
3246                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3247                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3248                                       -class => "list"}, esc_path($diff->{'file'}));
3249                         print "</td>\n";
3250                         print "<td>$mode_chng</td>\n";
3251                         print "<td class=\"link\">";
3252                         if ($action eq 'commitdiff') {
3253                                 # link to patch
3254                                 $patchno++;
3255                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3256                                 print " | ";
3257                         }
3258                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3259                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3260                                       "blob");
3261                         print "</td>\n";
3263                 } elsif ($diff->{'status'} eq "D") { # deleted
3264                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3265                         print "<td>";
3266                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3267                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3268                                        -class => "list"}, esc_path($diff->{'file'}));
3269                         print "</td>\n";
3270                         print "<td>$mode_chng</td>\n";
3271                         print "<td class=\"link\">";
3272                         if ($action eq 'commitdiff') {
3273                                 # link to patch
3274                                 $patchno++;
3275                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3276                                 print " | ";
3277                         }
3278                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3279                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3280                                       "blob") . " | ";
3281                         if ($have_blame) {
3282                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3283                                                              file_name=>$diff->{'file'})},
3284                                               "blame") . " | ";
3285                         }
3286                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3287                                                      file_name=>$diff->{'file'})},
3288                                       "history");
3289                         print "</td>\n";
3291                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3292                         my $mode_chnge = "";
3293                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3294                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3295                                 if ($from_file_type ne $to_file_type) {
3296                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3297                                 }
3298                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3299                                         if ($from_mode_str && $to_mode_str) {
3300                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3301                                         } elsif ($to_mode_str) {
3302                                                 $mode_chnge .= " mode: $to_mode_str";
3303                                         }
3304                                 }
3305                                 $mode_chnge .= "]</span>\n";
3306                         }
3307                         print "<td>";
3308                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3309                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3310                                       -class => "list"}, esc_path($diff->{'file'}));
3311                         print "</td>\n";
3312                         print "<td>$mode_chnge</td>\n";
3313                         print "<td class=\"link\">";
3314                         if ($action eq 'commitdiff') {
3315                                 # link to patch
3316                                 $patchno++;
3317                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3318                                       " | ";
3319                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3320                                 # "commit" view and modified file (not onlu mode changed)
3321                                 print $cgi->a({-href => href(action=>"blobdiff",
3322                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3323                                                              hash_base=>$hash, hash_parent_base=>$parent,
3324                                                              file_name=>$diff->{'file'})},
3325                                               "diff") .
3326                                       " | ";
3327                         }
3328                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3329                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3330                                        "blob") . " | ";
3331                         if ($have_blame) {
3332                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3333                                                              file_name=>$diff->{'file'})},
3334                                               "blame") . " | ";
3335                         }
3336                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3337                                                      file_name=>$diff->{'file'})},
3338                                       "history");
3339                         print "</td>\n";
3341                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3342                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3343                         my $nstatus = $status_name{$diff->{'status'}};
3344                         my $mode_chng = "";
3345                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3346                                 # mode also for directories, so we cannot use $to_mode_str
3347                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3348                         }
3349                         print "<td>" .
3350                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3351                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3352                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3353                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3354                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3355                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3356                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3357                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3358                               "<td class=\"link\">";
3359                         if ($action eq 'commitdiff') {
3360                                 # link to patch
3361                                 $patchno++;
3362                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3363                                       " | ";
3364                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3365                                 # "commit" view and modified file (not only pure rename or copy)
3366                                 print $cgi->a({-href => href(action=>"blobdiff",
3367                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3368                                                              hash_base=>$hash, hash_parent_base=>$parent,
3369                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3370                                               "diff") .
3371                                       " | ";
3372                         }
3373                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3374                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3375                                       "blob") . " | ";
3376                         if ($have_blame) {
3377                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3378                                                              file_name=>$diff->{'to_file'})},
3379                                               "blame") . " | ";
3380                         }
3381                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3382                                                     file_name=>$diff->{'to_file'})},
3383                                       "history");
3384                         print "</td>\n";
3386                 } # we should not encounter Unmerged (U) or Unknown (X) status
3387                 print "</tr>\n";
3388         }
3389         print "</tbody>" if $has_header;
3390         print "</table>\n";
3393 sub git_patchset_body {
3394         my ($fd, $difftree, $hash, @hash_parents) = @_;
3395         my ($hash_parent) = $hash_parents[0];
3397         my $is_combined = (@hash_parents > 1);
3398         my $patch_idx = 0;
3399         my $patch_number = 0;
3400         my $patch_line;
3401         my $diffinfo;
3402         my $to_name;
3403         my (%from, %to);
3405         print "<div class=\"patchset\">\n";
3407         # skip to first patch
3408         while ($patch_line = <$fd>) {
3409                 chomp $patch_line;
3411                 last if ($patch_line =~ m/^diff /);
3412         }
3414  PATCH:
3415         while ($patch_line) {
3417                 # parse "git diff" header line
3418                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3419                         # $1 is from_name, which we do not use
3420                         $to_name = unquote($2);
3421                         $to_name =~ s!^b/!!;
3422                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3423                         # $1 is 'cc' or 'combined', which we do not use
3424                         $to_name = unquote($2);
3425                 } else {
3426                         $to_name = undef;
3427                 }
3429                 # check if current patch belong to current raw line
3430                 # and parse raw git-diff line if needed
3431                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3432                         # this is continuation of a split patch
3433                         print "<div class=\"patch cont\">\n";
3434                 } else {
3435                         # advance raw git-diff output if needed
3436                         $patch_idx++ if defined $diffinfo;
3438                         # read and prepare patch information
3439                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3441                         # compact combined diff output can have some patches skipped
3442                         # find which patch (using pathname of result) we are at now;
3443                         if ($is_combined) {
3444                                 while ($to_name ne $diffinfo->{'to_file'}) {
3445                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3446                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3447                                               "</div>\n";  # class="patch"
3449                                         $patch_idx++;
3450                                         $patch_number++;
3452                                         last if $patch_idx > $#$difftree;
3453                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3454                                 }
3455                         }
3457                         # modifies %from, %to hashes
3458                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3460                         # this is first patch for raw difftree line with $patch_idx index
3461                         # we index @$difftree array from 0, but number patches from 1
3462                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3463                 }
3465                 # git diff header
3466                 #assert($patch_line =~ m/^diff /) if DEBUG;
3467                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3468                 $patch_number++;
3469                 # print "git diff" header
3470                 print format_git_diff_header_line($patch_line, $diffinfo,
3471                                                   \%from, \%to);
3473                 # print extended diff header
3474                 print "<div class=\"diff extended_header\">\n";
3475         EXTENDED_HEADER:
3476                 while ($patch_line = <$fd>) {
3477                         chomp $patch_line;
3479                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3481                         print format_extended_diff_header_line($patch_line, $diffinfo,
3482                                                                \%from, \%to);
3483                 }
3484                 print "</div>\n"; # class="diff extended_header"
3486                 # from-file/to-file diff header
3487                 if (! $patch_line) {
3488                         print "</div>\n"; # class="patch"
3489                         last PATCH;
3490                 }
3491                 next PATCH if ($patch_line =~ m/^diff /);
3492                 #assert($patch_line =~ m/^---/) if DEBUG;
3494                 my $last_patch_line = $patch_line;
3495                 $patch_line = <$fd>;
3496                 chomp $patch_line;
3497                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3499                 print format_diff_from_to_header($last_patch_line, $patch_line,
3500                                                  $diffinfo, \%from, \%to,
3501                                                  @hash_parents);
3503                 # the patch itself
3504         LINE:
3505                 while ($patch_line = <$fd>) {
3506                         chomp $patch_line;
3508                         next PATCH if ($patch_line =~ m/^diff /);
3510                         print format_diff_line($patch_line, \%from, \%to);
3511                 }
3513         } continue {
3514                 print "</div>\n"; # class="patch"
3515         }
3517         # for compact combined (--cc) format, with chunk and patch simpliciaction
3518         # patchset might be empty, but there might be unprocessed raw lines
3519         for (++$patch_idx if $patch_number > 0;
3520              $patch_idx < @$difftree;
3521              ++$patch_idx) {
3522                 # read and prepare patch information
3523                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3525                 # generate anchor for "patch" links in difftree / whatchanged part
3526                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3527                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3528                       "</div>\n";  # class="patch"
3530                 $patch_number++;
3531         }
3533         if ($patch_number == 0) {
3534                 if (@hash_parents > 1) {
3535                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3536                 } else {
3537                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3538                 }
3539         }
3541         print "</div>\n"; # class="patchset"
3544 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3546 # fills project list info (age, description, owner, forks) for each
3547 # project in the list, removing invalid projects from returned list
3548 # NOTE: modifies $projlist, but does not remove entries from it
3549 sub fill_project_list_info {
3550         my ($projlist, $check_forks) = @_;
3551         my @projects;
3553  PROJECT:
3554         foreach my $pr (@$projlist) {
3555                 my (@activity) = git_get_last_activity($pr->{'path'});
3556                 unless (@activity) {
3557                         next PROJECT;
3558                 }
3559                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3560                 if (!defined $pr->{'descr'}) {
3561                         my $descr = git_get_project_description($pr->{'path'}) || "";
3562                         $descr = to_utf8($descr);
3563                         $pr->{'descr_long'} = $descr;
3564                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3565                 }
3566                 if (!defined $pr->{'owner'}) {
3567                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3568                 }
3569                 if ($check_forks) {
3570                         my $pname = $pr->{'path'};
3571                         if (($pname =~ s/\.git$//) &&
3572                             ($pname !~ /\/$/) &&
3573                             (-d "$projectroot/$pname")) {
3574                                 $pr->{'forks'} = "-d $projectroot/$pname";
3575                         }       else {
3576                                 $pr->{'forks'} = 0;
3577                         }
3578                 }
3579                 push @projects, $pr;
3580         }
3582         return @projects;
3585 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3586 # (changing $list), or generating 'sort by $name' replay link otherwise
3587 sub print_sort_th {
3588         my ($str_sort, $name, $order, $key, $header, $list) = @_;
3589         $key    ||= $name;
3590         $header ||= ucfirst($name);
3592         if ($order eq $name) {
3593                 if ($str_sort) {
3594                         @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
3595                 } else {
3596                         @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
3597                 }
3598                 print "<th>$header</th>\n";
3599         } else {
3600                 print "<th>" .
3601                       $cgi->a({-href => href(-replay=>1, order=>$name),
3602                                -class => "header"}, $header) .
3603                       "</th>\n";
3604         }
3607 sub print_sort_th_str {
3608         print_sort_th(1, @_);
3611 sub print_sort_th_num {
3612         print_sort_th(0, @_);
3615 sub git_project_list_body {
3616         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3618         my ($check_forks) = gitweb_check_feature('forks');
3619         my @projects = fill_project_list_info($projlist, $check_forks);
3621         $order ||= $default_projects_order;
3622         $from = 0 unless defined $from;
3623         $to = $#projects if (!defined $to || $#projects < $to);
3625         print "<table class=\"project_list\">\n";
3626         unless ($no_header) {
3627                 print "<tr>\n";
3628                 if ($check_forks) {
3629                         print "<th></th>\n";
3630                 }
3631                 print_sort_th_str('project', $order, 'path',
3632                                   'Project', \@projects);
3633                 print_sort_th_str('descr', $order, 'descr_long',
3634                                   'Description', \@projects);
3635                 print_sort_th_str('owner', $order, 'owner',
3636                                   'Owner', \@projects);
3637                 print_sort_th_num('age', $order, 'age',
3638                                   'Last Change', \@projects);
3639                 print "<th></th>\n" . # for links
3640                       "</tr>\n";
3641         }
3642         my $alternate = 1;
3643         for (my $i = $from; $i <= $to; $i++) {
3644                 my $pr = $projects[$i];
3645                 if ($alternate) {
3646                         print "<tr class=\"dark\">\n";
3647                 } else {
3648                         print "<tr class=\"light\">\n";
3649                 }
3650                 $alternate ^= 1;
3651                 if ($check_forks) {
3652                         print "<td>";
3653                         if ($pr->{'forks'}) {
3654                                 print "<!-- $pr->{'forks'} -->\n";
3655                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3656                         }
3657                         print "</td>\n";
3658                 }
3659                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3660                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3661                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3662                                         -class => "list", -title => $pr->{'descr_long'}},
3663                                         esc_html($pr->{'descr'})) . "</td>\n" .
3664                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3665                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3666                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3667                       "<td class=\"link\">" .
3668                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
3669                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3670                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3671                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3672                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3673                       "</td>\n" .
3674                       "</tr>\n";
3675         }
3676         if (defined $extra) {
3677                 print "<tr>\n";
3678                 if ($check_forks) {
3679                         print "<td></td>\n";
3680                 }
3681                 print "<td colspan=\"5\">$extra</td>\n" .
3682                       "</tr>\n";
3683         }
3684         print "</table>\n";
3687 sub git_shortlog_body {
3688         # uses global variable $project
3689         my ($commitlist, $from, $to, $refs, $extra) = @_;
3691         $from = 0 unless defined $from;
3692         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3694         print "<table class=\"shortlog\">\n";
3695         my $alternate = 1;
3696         for (my $i = $from; $i <= $to; $i++) {
3697                 my %co = %{$commitlist->[$i]};
3698                 my $commit = $co{'id'};
3699                 my $ref = format_ref_marker($refs, $commit);
3700                 if ($alternate) {
3701                         print "<tr class=\"dark\">\n";
3702                 } else {
3703                         print "<tr class=\"light\">\n";
3704                 }
3705                 $alternate ^= 1;
3706                 my $author = chop_and_escape_str($co{'author_name'}, 10);
3707                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3708                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3709                       "<td><i>" . $author . "</i></td>\n" .
3710                       "<td>";
3711                 print format_subject_html($co{'title'}, $co{'title_short'},
3712                                           href(action=>"commit", hash=>$commit), $ref);
3713                 print "</td>\n" .
3714                       "<td class=\"link\">" .
3715                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3716                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3717                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3718                 my $snapshot_links = format_snapshot_links($commit);
3719                 if (defined $snapshot_links) {
3720                         print " | " . $snapshot_links;
3721                 }
3722                 print "</td>\n" .
3723                       "</tr>\n";
3724         }
3725         if (defined $extra) {
3726                 print "<tr>\n" .
3727                       "<td colspan=\"4\">$extra</td>\n" .
3728                       "</tr>\n";
3729         }
3730         print "</table>\n";
3733 sub git_history_body {
3734         # Warning: assumes constant type (blob or tree) during history
3735         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3737         $from = 0 unless defined $from;
3738         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3740         print "<table class=\"history\">\n";
3741         my $alternate = 1;
3742         for (my $i = $from; $i <= $to; $i++) {
3743                 my %co = %{$commitlist->[$i]};
3744                 if (!%co) {
3745                         next;
3746                 }
3747                 my $commit = $co{'id'};
3749                 my $ref = format_ref_marker($refs, $commit);
3751                 if ($alternate) {
3752                         print "<tr class=\"dark\">\n";
3753                 } else {
3754                         print "<tr class=\"light\">\n";
3755                 }
3756                 $alternate ^= 1;
3757         # shortlog uses      chop_str($co{'author_name'}, 10)
3758                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3759                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3760                       "<td><i>" . $author . "</i></td>\n" .
3761                       "<td>";
3762                 # originally git_history used chop_str($co{'title'}, 50)
3763                 print format_subject_html($co{'title'}, $co{'title_short'},
3764                                           href(action=>"commit", hash=>$commit), $ref);
3765                 print "</td>\n" .
3766                       "<td class=\"link\">" .
3767                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3768                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3770                 if ($ftype eq 'blob') {
3771                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3772                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
3773                         if (defined $blob_current && defined $blob_parent &&
3774                                         $blob_current ne $blob_parent) {
3775                                 print " | " .
3776                                         $cgi->a({-href => href(action=>"blobdiff",
3777                                                                hash=>$blob_current, hash_parent=>$blob_parent,
3778                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
3779                                                                file_name=>$file_name)},
3780                                                 "diff to current");
3781                         }
3782                 }
3783                 print "</td>\n" .
3784                       "</tr>\n";
3785         }
3786         if (defined $extra) {
3787                 print "<tr>\n" .
3788                       "<td colspan=\"4\">$extra</td>\n" .
3789                       "</tr>\n";
3790         }
3791         print "</table>\n";
3794 sub git_tags_body {
3795         # uses global variable $project
3796         my ($taglist, $from, $to, $extra) = @_;
3797         $from = 0 unless defined $from;
3798         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3800         print "<table class=\"tags\">\n";
3801         my $alternate = 1;
3802         for (my $i = $from; $i <= $to; $i++) {
3803                 my $entry = $taglist->[$i];
3804                 my %tag = %$entry;
3805                 my $comment = $tag{'subject'};
3806                 my $comment_short;
3807                 if (defined $comment) {
3808                         $comment_short = chop_str($comment, 30, 5);
3809                 }
3810                 if ($alternate) {
3811                         print "<tr class=\"dark\">\n";
3812                 } else {
3813                         print "<tr class=\"light\">\n";
3814                 }
3815                 $alternate ^= 1;
3816                 if (defined $tag{'age'}) {
3817                         print "<td><i>$tag{'age'}</i></td>\n";
3818                 } else {
3819                         print "<td></td>\n";
3820                 }
3821                 print "<td>" .
3822                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3823                                -class => "list name"}, esc_html($tag{'name'})) .
3824                       "</td>\n" .
3825                       "<td>";
3826                 if (defined $comment) {
3827                         print format_subject_html($comment, $comment_short,
3828                                                   href(action=>"tag", hash=>$tag{'id'}));
3829                 }
3830                 print "</td>\n" .
3831                       "<td class=\"selflink\">";
3832                 if ($tag{'type'} eq "tag") {
3833                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3834                 } else {
3835                         print "&nbsp;";
3836                 }
3837                 print "</td>\n" .
3838                       "<td class=\"link\">" . " | " .
3839                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3840                 if ($tag{'reftype'} eq "commit") {
3841                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3842                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3843                 } elsif ($tag{'reftype'} eq "blob") {
3844                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3845                 }
3846                 print "</td>\n" .
3847                       "</tr>";
3848         }
3849         if (defined $extra) {
3850                 print "<tr>\n" .
3851                       "<td colspan=\"5\">$extra</td>\n" .
3852                       "</tr>\n";
3853         }
3854         print "</table>\n";
3857 sub git_heads_body {
3858         # uses global variable $project
3859         my ($headlist, $head, $from, $to, $extra) = @_;
3860         $from = 0 unless defined $from;
3861         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3863         print "<table class=\"heads\">\n";
3864         my $alternate = 1;
3865         for (my $i = $from; $i <= $to; $i++) {
3866                 my $entry = $headlist->[$i];
3867                 my %ref = %$entry;
3868                 my $curr = $ref{'id'} eq $head;
3869                 if ($alternate) {
3870                         print "<tr class=\"dark\">\n";
3871                 } else {
3872                         print "<tr class=\"light\">\n";
3873                 }
3874                 $alternate ^= 1;
3875                 print "<td><i>$ref{'age'}</i></td>\n" .
3876                       ($curr ? "<td class=\"current_head\">" : "<td>") .
3877                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
3878                                -class => "list name"},esc_html($ref{'name'})) .
3879                       "</td>\n" .
3880                       "<td class=\"link\">" .
3881                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
3882                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
3883                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
3884                       "</td>\n" .
3885                       "</tr>";
3886         }
3887         if (defined $extra) {
3888                 print "<tr>\n" .
3889                       "<td colspan=\"3\">$extra</td>\n" .
3890                       "</tr>\n";
3891         }
3892         print "</table>\n";
3895 sub git_search_grep_body {
3896         my ($commitlist, $from, $to, $extra) = @_;
3897         $from = 0 unless defined $from;
3898         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3900         print "<table class=\"commit_search\">\n";
3901         my $alternate = 1;
3902         for (my $i = $from; $i <= $to; $i++) {
3903                 my %co = %{$commitlist->[$i]};
3904                 if (!%co) {
3905                         next;
3906                 }
3907                 my $commit = $co{'id'};
3908                 if ($alternate) {
3909                         print "<tr class=\"dark\">\n";
3910                 } else {
3911                         print "<tr class=\"light\">\n";
3912                 }
3913                 $alternate ^= 1;
3914                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
3915                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3916                       "<td><i>" . $author . "</i></td>\n" .
3917                       "<td>" .
3918                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3919                                -class => "list subject"},
3920                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
3921                 my $comment = $co{'comment'};
3922                 foreach my $line (@$comment) {
3923                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3924                                 my ($lead, $match, $trail) = ($1, $2, $3);
3925                                 $match = chop_str($match, 70, 5, 'center');
3926                                 my $contextlen = int((80 - length($match))/2);
3927                                 $contextlen = 30 if ($contextlen > 30);
3928                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
3929                                 $trail = chop_str($trail, $contextlen, 10, 'right');
3931                                 $lead  = esc_html($lead);
3932                                 $match = esc_html($match);
3933                                 $trail = esc_html($trail);
3935                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
3936                         }
3937                 }
3938                 print "</td>\n" .
3939                       "<td class=\"link\">" .
3940                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3941                       " | " .
3942                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
3943                       " | " .
3944                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3945                 print "</td>\n" .
3946                       "</tr>\n";
3947         }
3948         if (defined $extra) {
3949                 print "<tr>\n" .
3950                       "<td colspan=\"3\">$extra</td>\n" .
3951                       "</tr>\n";
3952         }
3953         print "</table>\n";
3956 ## ======================================================================
3957 ## ======================================================================
3958 ## actions
3960 sub git_project_list {
3961         my $order = $cgi->param('o');
3962         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3963                 die_error(400, "Unknown order parameter");
3964         }
3966         my @list = git_get_projects_list();
3967         if (!@list) {
3968                 die_error(404, "No projects found");
3969         }
3971         git_header_html();
3972         if (-f $home_text) {
3973                 print "<div class=\"index_include\">\n";
3974                 open (my $fd, $home_text);
3975                 print <$fd>;
3976                 close $fd;
3977                 print "</div>\n";
3978         }
3979         git_project_list_body(\@list, $order);
3980         git_footer_html();
3983 sub git_forks {
3984         my $order = $cgi->param('o');
3985         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3986                 die_error(400, "Unknown order parameter");
3987         }
3989         my @list = git_get_projects_list($project);
3990         if (!@list) {
3991                 die_error(404, "No forks found");
3992         }
3994         git_header_html();
3995         git_print_page_nav('','');
3996         git_print_header_div('summary', "$project forks");
3997         git_project_list_body(\@list, $order);
3998         git_footer_html();
4001 sub git_project_index {
4002         my @projects = git_get_projects_list($project);
4004         print $cgi->header(
4005                 -type => 'text/plain',
4006                 -charset => 'utf-8',
4007                 -content_disposition => 'inline; filename="index.aux"');
4009         foreach my $pr (@projects) {
4010                 if (!exists $pr->{'owner'}) {
4011                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4012                 }
4014                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4015                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4016                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4017                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4018                 $path  =~ s/ /\+/g;
4019                 $owner =~ s/ /\+/g;
4021                 print "$path $owner\n";
4022         }
4025 sub git_summary {
4026         my $descr = git_get_project_description($project) || "none";
4027         my %co = parse_commit("HEAD");
4028         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4029         my $head = $co{'id'};
4031         my $owner = git_get_project_owner($project);
4033         my $refs = git_get_references();
4034         # These get_*_list functions return one more to allow us to see if
4035         # there are more ...
4036         my @taglist  = git_get_tags_list(16);
4037         my @headlist = git_get_heads_list(16);
4038         my @forklist;
4039         my ($check_forks) = gitweb_check_feature('forks');
4041         if ($check_forks) {
4042                 @forklist = git_get_projects_list($project);
4043         }
4045         git_header_html();
4046         git_print_page_nav('summary','', $head);
4048         print "<div class=\"title\">&nbsp;</div>\n";
4049         print "<table class=\"projects_list\">\n" .
4050               "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4051               "<tr><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4052         if (defined $cd{'rfc2822'}) {
4053                 print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4054         }
4056         # use per project git URL list in $projectroot/$project/cloneurl
4057         # or make project git URL from git base URL and project name
4058         my $url_tag = "URL";
4059         my @url_list = git_get_project_url_list($project);
4060         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4061         foreach my $git_url (@url_list) {
4062                 next unless $git_url;
4063                 print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
4064                 $url_tag = "";
4065         }
4066         print "</table>\n";
4068         if (-s "$projectroot/$project/README.html") {
4069                 if (open my $fd, "$projectroot/$project/README.html") {
4070                         print "<div class=\"title\">readme</div>\n" .
4071                               "<div class=\"readme\">\n";
4072                         print $_ while (<$fd>);
4073                         print "\n</div>\n"; # class="readme"
4074                         close $fd;
4075                 }
4076         }
4078         # we need to request one more than 16 (0..15) to check if
4079         # those 16 are all
4080         my @commitlist = $head ? parse_commits($head, 17) : ();
4081         if (@commitlist) {
4082                 git_print_header_div('shortlog');
4083                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4084                                   $#commitlist <=  15 ? undef :
4085                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4086         }
4088         if (@taglist) {
4089                 git_print_header_div('tags');
4090                 git_tags_body(\@taglist, 0, 15,
4091                               $#taglist <=  15 ? undef :
4092                               $cgi->a({-href => href(action=>"tags")}, "..."));
4093         }
4095         if (@headlist) {
4096                 git_print_header_div('heads');
4097                 git_heads_body(\@headlist, $head, 0, 15,
4098                                $#headlist <= 15 ? undef :
4099                                $cgi->a({-href => href(action=>"heads")}, "..."));
4100         }
4102         if (@forklist) {
4103                 git_print_header_div('forks');
4104                 git_project_list_body(\@forklist, undef, 0, 15,
4105                                       $#forklist <= 15 ? undef :
4106                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4107                                       'noheader');
4108         }
4110         git_footer_html();
4113 sub git_tag {
4114         my $head = git_get_head_hash($project);
4115         git_header_html();
4116         git_print_page_nav('','', $head,undef,$head);
4117         my %tag = parse_tag($hash);
4119         if (! %tag) {
4120                 die_error(404, "Unknown tag object");
4121         }
4123         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4124         print "<div class=\"title_text\">\n" .
4125               "<table class=\"object_header\">\n" .
4126               "<tr>\n" .
4127               "<td>object</td>\n" .
4128               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4129                                $tag{'object'}) . "</td>\n" .
4130               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4131                                               $tag{'type'}) . "</td>\n" .
4132               "</tr>\n";
4133         if (defined($tag{'author'})) {
4134                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4135                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4136                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4137                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4138                         "</td></tr>\n";
4139         }
4140         print "</table>\n\n" .
4141               "</div>\n";
4142         print "<div class=\"page_body\">";
4143         my $comment = $tag{'comment'};
4144         foreach my $line (@$comment) {
4145                 chomp $line;
4146                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4147         }
4148         print "</div>\n";
4149         git_footer_html();
4152 sub git_blame {
4153         my $fd;
4154         my $ftype;
4156         gitweb_check_feature('blame')
4157             or die_error(403, "Blame view not allowed");
4159         die_error(400, "No file name given") unless $file_name;
4160         $hash_base ||= git_get_head_hash($project);
4161         die_error(404, "Couldn't find base commit") unless ($hash_base);
4162         my %co = parse_commit($hash_base)
4163                 or die_error(404, "Commit not found");
4164         if (!defined $hash) {
4165                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4166                         or die_error(404, "Error looking up file");
4167         }
4168         $ftype = git_get_type($hash);
4169         if ($ftype !~ "blob") {
4170                 die_error(400, "Object is not a blob");
4171         }
4172         open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4173               $file_name, $hash_base)
4174                 or die_error(500, "Open git-blame failed");
4175         git_header_html();
4176         my $formats_nav =
4177                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4178                         "blob") .
4179                 " | " .
4180                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4181                         "history") .
4182                 " | " .
4183                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4184                         "HEAD");
4185         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4186         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4187         git_print_page_path($file_name, $ftype, $hash_base);
4188         my @rev_color = (qw(light2 dark2));
4189         my $num_colors = scalar(@rev_color);
4190         my $current_color = 0;
4191         my $last_rev;
4192         print <<HTML;
4193 <div class="page_body">
4194 <table class="blame">
4195 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4196 HTML
4197         my %metainfo = ();
4198         while (1) {
4199                 $_ = <$fd>;
4200                 last unless defined $_;
4201                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4202                     /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4203                 if (!exists $metainfo{$full_rev}) {
4204                         $metainfo{$full_rev} = {};
4205                 }
4206                 my $meta = $metainfo{$full_rev};
4207                 while (<$fd>) {
4208                         last if (s/^\t//);
4209                         if (/^(\S+) (.*)$/) {
4210                                 $meta->{$1} = $2;
4211                         }
4212                 }
4213                 my $data = $_;
4214                 chomp $data;
4215                 my $rev = substr($full_rev, 0, 8);
4216                 my $author = $meta->{'author'};
4217                 my %date = parse_date($meta->{'author-time'},
4218                                       $meta->{'author-tz'});
4219                 my $date = $date{'iso-tz'};
4220                 if ($group_size) {
4221                         $current_color = ++$current_color % $num_colors;
4222                 }
4223                 print "<tr class=\"$rev_color[$current_color]\">\n";
4224                 if ($group_size) {
4225                         print "<td class=\"sha1\"";
4226                         print " title=\"". esc_html($author) . ", $date\"";
4227                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4228                         print ">";
4229                         print $cgi->a({-href => href(action=>"commit",
4230                                                      hash=>$full_rev,
4231                                                      file_name=>$file_name)},
4232                                       esc_html($rev));
4233                         print "</td>\n";
4234                 }
4235                 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4236                         or die_error(500, "Open git-rev-parse failed");
4237                 my $parent_commit = <$dd>;
4238                 close $dd;
4239                 chomp($parent_commit);
4240                 my $blamed = href(action => 'blame',
4241                                   file_name => $meta->{'filename'},
4242                                   hash_base => $parent_commit);
4243                 print "<td class=\"linenr\">";
4244                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4245                                 -id => "l$lineno",
4246                                 -class => "linenr" },
4247                               esc_html($lineno));
4248                 print "</td>";
4249                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4250                 print "</tr>\n";
4251         }
4252         print "</table>\n";
4253         print "</div>";
4254         close $fd
4255                 or print "Reading blob failed\n";
4256         git_footer_html();
4259 sub git_tags {
4260         my $head = git_get_head_hash($project);
4261         git_header_html();
4262         git_print_page_nav('','', $head,undef,$head);
4263         git_print_header_div('summary', $project);
4265         my @tagslist = git_get_tags_list();
4266         if (@tagslist) {
4267                 git_tags_body(\@tagslist);
4268         }
4269         git_footer_html();
4272 sub git_heads {
4273         my $head = git_get_head_hash($project);
4274         git_header_html();
4275         git_print_page_nav('','', $head,undef,$head);
4276         git_print_header_div('summary', $project);
4278         my @headslist = git_get_heads_list();
4279         if (@headslist) {
4280                 git_heads_body(\@headslist, $head);
4281         }
4282         git_footer_html();
4285 sub git_blob_plain {
4286         my $type = shift;
4287         my $expires;
4289         if (!defined $hash) {
4290                 if (defined $file_name) {
4291                         my $base = $hash_base || git_get_head_hash($project);
4292                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4293                                 or die_error(404, "Cannot find file");
4294                 } else {
4295                         die_error(400, "No file name defined");
4296                 }
4297         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4298                 # blobs defined by non-textual hash id's can be cached
4299                 $expires = "+1d";
4300         }
4302         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4303                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4305         # content-type (can include charset)
4306         $type = blob_contenttype($fd, $file_name, $type);
4308         # "save as" filename, even when no $file_name is given
4309         my $save_as = "$hash";
4310         if (defined $file_name) {
4311                 $save_as = $file_name;
4312         } elsif ($type =~ m/^text\//) {
4313                 $save_as .= '.txt';
4314         }
4316         print $cgi->header(
4317                 -type => $type,
4318                 -expires => $expires,
4319                 -content_disposition => 'inline; filename="' . $save_as . '"');
4320         undef $/;
4321         binmode STDOUT, ':raw';
4322         print <$fd>;
4323         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4324         $/ = "\n";
4325         close $fd;
4328 sub git_blob {
4329         my $expires;
4331         if (!defined $hash) {
4332                 if (defined $file_name) {
4333                         my $base = $hash_base || git_get_head_hash($project);
4334                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4335                                 or die_error(404, "Cannot find file");
4336                 } else {
4337                         die_error(400, "No file name defined");
4338                 }
4339         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4340                 # blobs defined by non-textual hash id's can be cached
4341                 $expires = "+1d";
4342         }
4344         my ($have_blame) = gitweb_check_feature('blame');
4345         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4346                 or die_error(500, "Couldn't cat $file_name, $hash");
4347         my $mimetype = blob_mimetype($fd, $file_name);
4348         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4349                 close $fd;
4350                 return git_blob_plain($mimetype);
4351         }
4352         # we can have blame only for text/* mimetype
4353         $have_blame &&= ($mimetype =~ m!^text/!);
4355         git_header_html(undef, $expires);
4356         my $formats_nav = '';
4357         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4358                 if (defined $file_name) {
4359                         if ($have_blame) {
4360                                 $formats_nav .=
4361                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
4362                                                 "blame") .
4363                                         " | ";
4364                         }
4365                         $formats_nav .=
4366                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4367                                         "history") .
4368                                 " | " .
4369                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4370                                         "raw") .
4371                                 " | " .
4372                                 $cgi->a({-href => href(action=>"blob",
4373                                                        hash_base=>"HEAD", file_name=>$file_name)},
4374                                         "HEAD");
4375                 } else {
4376                         $formats_nav .=
4377                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4378                                         "raw");
4379                 }
4380                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4381                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4382         } else {
4383                 print "<div class=\"page_nav\">\n" .
4384                       "<br/><br/></div>\n" .
4385                       "<div class=\"title\">$hash</div>\n";
4386         }
4387         git_print_page_path($file_name, "blob", $hash_base);
4388         print "<div class=\"page_body\">\n";
4389         if ($mimetype =~ m!^image/!) {
4390                 print qq!<img type="$mimetype"!;
4391                 if ($file_name) {
4392                         print qq! alt="$file_name" title="$file_name"!;
4393                 }
4394                 print qq! src="! .
4395                       href(action=>"blob_plain", hash=>$hash,
4396                            hash_base=>$hash_base, file_name=>$file_name) .
4397                       qq!" />\n!;
4398         } else {
4399                 my $nr;
4400                 while (my $line = <$fd>) {
4401                         chomp $line;
4402                         $nr++;
4403                         $line = untabify($line);
4404                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4405                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4406                 }
4407         }
4408         close $fd
4409                 or print "Reading blob failed.\n";
4410         print "</div>";
4411         git_footer_html();
4414 sub git_tree {
4415         if (!defined $hash_base) {
4416                 $hash_base = "HEAD";
4417         }
4418         if (!defined $hash) {
4419                 if (defined $file_name) {
4420                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4421                 } else {
4422                         $hash = $hash_base;
4423                 }
4424         }
4425         die_error(404, "No such tree") unless defined($hash);
4426         $/ = "\0";
4427         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4428                 or die_error(500, "Open git-ls-tree failed");
4429         my @entries = map { chomp; $_ } <$fd>;
4430         close $fd or die_error(404, "Reading tree failed");
4431         $/ = "\n";
4433         my $refs = git_get_references();
4434         my $ref = format_ref_marker($refs, $hash_base);
4435         git_header_html();
4436         my $basedir = '';
4437         my ($have_blame) = gitweb_check_feature('blame');
4438         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4439                 my @views_nav = ();
4440                 if (defined $file_name) {
4441                         push @views_nav,
4442                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4443                                         "history"),
4444                                 $cgi->a({-href => href(action=>"tree",
4445                                                        hash_base=>"HEAD", file_name=>$file_name)},
4446                                         "HEAD"),
4447                 }
4448                 my $snapshot_links = format_snapshot_links($hash);
4449                 if (defined $snapshot_links) {
4450                         # FIXME: Should be available when we have no hash base as well.
4451                         push @views_nav, $snapshot_links;
4452                 }
4453                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4454                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4455         } else {
4456                 undef $hash_base;
4457                 print "<div class=\"page_nav\">\n";
4458                 print "<br/><br/></div>\n";
4459                 print "<div class=\"title\">$hash</div>\n";
4460         }
4461         if (defined $file_name) {
4462                 $basedir = $file_name;
4463                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4464                         $basedir .= '/';
4465                 }
4466                 git_print_page_path($file_name, 'tree', $hash_base);
4467         }
4468         print "<div class=\"page_body\">\n";
4469         print "<table class=\"tree\">\n";
4470         my $alternate = 1;
4471         # '..' (top directory) link if possible
4472         if (defined $hash_base &&
4473             defined $file_name && $file_name =~ m![^/]+$!) {
4474                 if ($alternate) {
4475                         print "<tr class=\"dark\">\n";
4476                 } else {
4477                         print "<tr class=\"light\">\n";
4478                 }
4479                 $alternate ^= 1;
4481                 my $up = $file_name;
4482                 $up =~ s!/?[^/]+$!!;
4483                 undef $up unless $up;
4484                 # based on git_print_tree_entry
4485                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4486                 print '<td class="list">';
4487                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4488                                              file_name=>$up)},
4489                               "..");
4490                 print "</td>\n";
4491                 print "<td class=\"link\"></td>\n";
4493                 print "</tr>\n";
4494         }
4495         foreach my $line (@entries) {
4496                 my %t = parse_ls_tree_line($line, -z => 1);
4498                 if ($alternate) {
4499                         print "<tr class=\"dark\">\n";
4500                 } else {
4501                         print "<tr class=\"light\">\n";
4502                 }
4503                 $alternate ^= 1;
4505                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4507                 print "</tr>\n";
4508         }
4509         print "</table>\n" .
4510               "</div>";
4511         git_footer_html();
4514 sub git_snapshot {
4515         my @supported_fmts = gitweb_check_feature('snapshot');
4516         @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4518         my $format = $cgi->param('sf');
4519         if (!@supported_fmts) {
4520                 die_error(403, "Snapshots not allowed");
4521         }
4522         # default to first supported snapshot format
4523         $format ||= $supported_fmts[0];
4524         if ($format !~ m/^[a-z0-9]+$/) {
4525                 die_error(400, "Invalid snapshot format parameter");
4526         } elsif (!exists($known_snapshot_formats{$format})) {
4527                 die_error(400, "Unknown snapshot format");
4528         } elsif (!grep($_ eq $format, @supported_fmts)) {
4529                 die_error(403, "Unsupported snapshot format");
4530         }
4532         if (!defined $hash) {
4533                 $hash = git_get_head_hash($project);
4534         }
4536         my $name = $project;
4537         $name =~ s,([^/])/*\.git$,$1,;
4538         $name = basename($name);
4539         my $filename = to_utf8($name);
4540         $name =~ s/\047/\047\\\047\047/g;
4541         my $cmd;
4542         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4543         $cmd = quote_command(
4544                 git_cmd(), 'archive',
4545                 "--format=$known_snapshot_formats{$format}{'format'}",
4546                 "--prefix=$name/", $hash);
4547         if (exists $known_snapshot_formats{$format}{'compressor'}) {
4548                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4549         }
4551         print $cgi->header(
4552                 -type => $known_snapshot_formats{$format}{'type'},
4553                 -content_disposition => 'inline; filename="' . "$filename" . '"',
4554                 -status => '200 OK');
4556         open my $fd, "-|", $cmd
4557                 or die_error(500, "Execute git-archive failed");
4558         binmode STDOUT, ':raw';
4559         print <$fd>;
4560         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4561         close $fd;
4564 sub git_log {
4565         my $head = git_get_head_hash($project);
4566         if (!defined $hash) {
4567                 $hash = $head;
4568         }
4569         if (!defined $page) {
4570                 $page = 0;
4571         }
4572         my $refs = git_get_references();
4574         my @commitlist = parse_commits($hash, 101, (100 * $page));
4576         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4578         git_header_html();
4579         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4581         if (!@commitlist) {
4582                 my %co = parse_commit($hash);
4584                 git_print_header_div('summary', $project);
4585                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4586         }
4587         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4588         for (my $i = 0; $i <= $to; $i++) {
4589                 my %co = %{$commitlist[$i]};
4590                 next if !%co;
4591                 my $commit = $co{'id'};
4592                 my $ref = format_ref_marker($refs, $commit);
4593                 my %ad = parse_date($co{'author_epoch'});
4594                 git_print_header_div('commit',
4595                                "<span class=\"age\">$co{'age_string'}</span>" .
4596                                esc_html($co{'title'}) . $ref,
4597                                $commit);
4598                 print "<div class=\"title_text\">\n" .
4599                       "<div class=\"log_link\">\n" .
4600                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4601                       " | " .
4602                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4603                       " | " .
4604                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4605                       "<br/>\n" .
4606                       "</div>\n" .
4607                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
4608                       "</div>\n";
4610                 print "<div class=\"log_body\">\n";
4611                 git_print_log($co{'comment'}, -final_empty_line=> 1);
4612                 print "</div>\n";
4613         }
4614         if ($#commitlist >= 100) {
4615                 print "<div class=\"page_nav\">\n";
4616                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4617                                -accesskey => "n", -title => "Alt-n"}, "next");
4618                 print "</div>\n";
4619         }
4620         git_footer_html();
4623 sub git_commit {
4624         $hash ||= $hash_base || "HEAD";
4625         my %co = parse_commit($hash)
4626             or die_error(404, "Unknown commit object");
4627         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4628         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4630         my $parent  = $co{'parent'};
4631         my $parents = $co{'parents'}; # listref
4633         # we need to prepare $formats_nav before any parameter munging
4634         my $formats_nav;
4635         if (!defined $parent) {
4636                 # --root commitdiff
4637                 $formats_nav .= '(initial)';
4638         } elsif (@$parents == 1) {
4639                 # single parent commit
4640                 $formats_nav .=
4641                         '(parent: ' .
4642                         $cgi->a({-href => href(action=>"commit",
4643                                                hash=>$parent)},
4644                                 esc_html(substr($parent, 0, 7))) .
4645                         ')';
4646         } else {
4647                 # merge commit
4648                 $formats_nav .=
4649                         '(merge: ' .
4650                         join(' ', map {
4651                                 $cgi->a({-href => href(action=>"commit",
4652                                                        hash=>$_)},
4653                                         esc_html(substr($_, 0, 7)));
4654                         } @$parents ) .
4655                         ')';
4656         }
4658         if (!defined $parent) {
4659                 $parent = "--root";
4660         }
4661         my @difftree;
4662         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4663                 @diff_opts,
4664                 (@$parents <= 1 ? $parent : '-c'),
4665                 $hash, "--"
4666                 or die_error(500, "Open git-diff-tree failed");
4667         @difftree = map { chomp; $_ } <$fd>;
4668         close $fd or die_error(404, "Reading git-diff-tree failed");
4670         # non-textual hash id's can be cached
4671         my $expires;
4672         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4673                 $expires = "+1d";
4674         }
4675         my $refs = git_get_references();
4676         my $ref = format_ref_marker($refs, $co{'id'});
4678         git_header_html(undef, $expires);
4679         git_print_page_nav('commit', '',
4680                            $hash, $co{'tree'}, $hash,
4681                            $formats_nav);
4683         if (defined $co{'parent'}) {
4684                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4685         } else {
4686                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4687         }
4688         print "<div class=\"title_text\">\n" .
4689               "<table class=\"object_header\">\n";
4690         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4691               "<tr>" .
4692               "<td></td><td> $ad{'rfc2822'}";
4693         if ($ad{'hour_local'} < 6) {
4694                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4695                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4696         } else {
4697                 printf(" (%02d:%02d %s)",
4698                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4699         }
4700         print "</td>" .
4701               "</tr>\n";
4702         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4703         print "<tr><td></td><td> $cd{'rfc2822'}" .
4704               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4705               "</td></tr>\n";
4706         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4707         print "<tr>" .
4708               "<td>tree</td>" .
4709               "<td class=\"sha1\">" .
4710               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4711                        class => "list"}, $co{'tree'}) .
4712               "</td>" .
4713               "<td class=\"link\">" .
4714               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4715                       "tree");
4716         my $snapshot_links = format_snapshot_links($hash);
4717         if (defined $snapshot_links) {
4718                 print " | " . $snapshot_links;
4719         }
4720         print "</td>" .
4721               "</tr>\n";
4723         foreach my $par (@$parents) {
4724                 print "<tr>" .
4725                       "<td>parent</td>" .
4726                       "<td class=\"sha1\">" .
4727                       $cgi->a({-href => href(action=>"commit", hash=>$par),
4728                                class => "list"}, $par) .
4729                       "</td>" .
4730                       "<td class=\"link\">" .
4731                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4732                       " | " .
4733                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4734                       "</td>" .
4735                       "</tr>\n";
4736         }
4737         print "</table>".
4738               "</div>\n";
4740         print "<div class=\"page_body\">\n";
4741         git_print_log($co{'comment'});
4742         print "</div>\n";
4744         git_difftree_body(\@difftree, $hash, @$parents);
4746         git_footer_html();
4749 sub git_object {
4750         # object is defined by:
4751         # - hash or hash_base alone
4752         # - hash_base and file_name
4753         my $type;
4755         # - hash or hash_base alone
4756         if ($hash || ($hash_base && !defined $file_name)) {
4757                 my $object_id = $hash || $hash_base;
4759                 open my $fd, "-|", quote_command(
4760                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4761                         or die_error(404, "Object does not exist");
4762                 $type = <$fd>;
4763                 chomp $type;
4764                 close $fd
4765                         or die_error(404, "Object does not exist");
4767         # - hash_base and file_name
4768         } elsif ($hash_base && defined $file_name) {
4769                 $file_name =~ s,/+$,,;
4771                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4772                         or die_error(404, "Base object does not exist");
4774                 # here errors should not hapen
4775                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4776                         or die_error(500, "Open git-ls-tree failed");
4777                 my $line = <$fd>;
4778                 close $fd;
4780                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
4781                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4782                         die_error(404, "File or directory for given base does not exist");
4783                 }
4784                 $type = $2;
4785                 $hash = $3;
4786         } else {
4787                 die_error(400, "Not enough information to find object");
4788         }
4790         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4791                                           hash=>$hash, hash_base=>$hash_base,
4792                                           file_name=>$file_name),
4793                              -status => '302 Found');
4796 sub git_blobdiff {
4797         my $format = shift || 'html';
4799         my $fd;
4800         my @difftree;
4801         my %diffinfo;
4802         my $expires;
4804         # preparing $fd and %diffinfo for git_patchset_body
4805         # new style URI
4806         if (defined $hash_base && defined $hash_parent_base) {
4807                 if (defined $file_name) {
4808                         # read raw output
4809                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4810                                 $hash_parent_base, $hash_base,
4811                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
4812                                 or die_error(500, "Open git-diff-tree failed");
4813                         @difftree = map { chomp; $_ } <$fd>;
4814                         close $fd
4815                                 or die_error(404, "Reading git-diff-tree failed");
4816                         @difftree
4817                                 or die_error(404, "Blob diff not found");
4819                 } elsif (defined $hash &&
4820                          $hash =~ /[0-9a-fA-F]{40}/) {
4821                         # try to find filename from $hash
4823                         # read filtered raw output
4824                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4825                                 $hash_parent_base, $hash_base, "--"
4826                                 or die_error(500, "Open git-diff-tree failed");
4827                         @difftree =
4828                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
4829                                 # $hash == to_id
4830                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4831                                 map { chomp; $_ } <$fd>;
4832                         close $fd
4833                                 or die_error(404, "Reading git-diff-tree failed");
4834                         @difftree
4835                                 or die_error(404, "Blob diff not found");
4837                 } else {
4838                         die_error(400, "Missing one of the blob diff parameters");
4839                 }
4841                 if (@difftree > 1) {
4842                         die_error(400, "Ambiguous blob diff specification");
4843                 }
4845                 %diffinfo = parse_difftree_raw_line($difftree[0]);
4846                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4847                 $file_name   ||= $diffinfo{'to_file'};
4849                 $hash_parent ||= $diffinfo{'from_id'};
4850                 $hash        ||= $diffinfo{'to_id'};
4852                 # non-textual hash id's can be cached
4853                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4854                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4855                         $expires = '+1d';
4856                 }
4858                 # open patch output
4859                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4860                         '-p', ($format eq 'html' ? "--full-index" : ()),
4861                         $hash_parent_base, $hash_base,
4862                         "--", (defined $file_parent ? $file_parent : ()), $file_name
4863                         or die_error(500, "Open git-diff-tree failed");
4864         }
4866         # old/legacy style URI -- not generated anymore since 1.4.3.
4867         if (!%diffinfo) {
4868                 die_error('404 Not Found', "Missing one of the blob diff parameters")
4869         }
4871         # header
4872         if ($format eq 'html') {
4873                 my $formats_nav =
4874                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
4875                                 "raw");
4876                 git_header_html(undef, $expires);
4877                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4878                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4879                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4880                 } else {
4881                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4882                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4883                 }
4884                 if (defined $file_name) {
4885                         git_print_page_path($file_name, "blob", $hash_base);
4886                 } else {
4887                         print "<div class=\"page_path\"></div>\n";
4888                 }
4890         } elsif ($format eq 'plain') {
4891                 print $cgi->header(
4892                         -type => 'text/plain',
4893                         -charset => 'utf-8',
4894                         -expires => $expires,
4895                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
4897                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4899         } else {
4900                 die_error(400, "Unknown blobdiff format");
4901         }
4903         # patch
4904         if ($format eq 'html') {
4905                 print "<div class=\"page_body\">\n";
4907                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
4908                 close $fd;
4910                 print "</div>\n"; # class="page_body"
4911                 git_footer_html();
4913         } else {
4914                 while (my $line = <$fd>) {
4915                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
4916                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
4918                         print $line;
4920                         last if $line =~ m!^\+\+\+!;
4921                 }
4922                 local $/ = undef;
4923                 print <$fd>;
4924                 close $fd;
4925         }
4928 sub git_blobdiff_plain {
4929         git_blobdiff('plain');
4932 sub git_commitdiff {
4933         my $format = shift || 'html';
4934         $hash ||= $hash_base || "HEAD";
4935         my %co = parse_commit($hash)
4936             or die_error(404, "Unknown commit object");
4938         # choose format for commitdiff for merge
4939         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
4940                 $hash_parent = '--cc';
4941         }
4942         # we need to prepare $formats_nav before almost any parameter munging
4943         my $formats_nav;
4944         if ($format eq 'html') {
4945                 $formats_nav =
4946                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
4947                                 "raw");
4949                 if (defined $hash_parent &&
4950                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
4951                         # commitdiff with two commits given
4952                         my $hash_parent_short = $hash_parent;
4953                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4954                                 $hash_parent_short = substr($hash_parent, 0, 7);
4955                         }
4956                         $formats_nav .=
4957                                 ' (from';
4958                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
4959                                 if ($co{'parents'}[$i] eq $hash_parent) {
4960                                         $formats_nav .= ' parent ' . ($i+1);
4961                                         last;
4962                                 }
4963                         }
4964                         $formats_nav .= ': ' .
4965                                 $cgi->a({-href => href(action=>"commitdiff",
4966                                                        hash=>$hash_parent)},
4967                                         esc_html($hash_parent_short)) .
4968                                 ')';
4969                 } elsif (!$co{'parent'}) {
4970                         # --root commitdiff
4971                         $formats_nav .= ' (initial)';
4972                 } elsif (scalar @{$co{'parents'}} == 1) {
4973                         # single parent commit
4974                         $formats_nav .=
4975                                 ' (parent: ' .
4976                                 $cgi->a({-href => href(action=>"commitdiff",
4977                                                        hash=>$co{'parent'})},
4978                                         esc_html(substr($co{'parent'}, 0, 7))) .
4979                                 ')';
4980                 } else {
4981                         # merge commit
4982                         if ($hash_parent eq '--cc') {
4983                                 $formats_nav .= ' | ' .
4984                                         $cgi->a({-href => href(action=>"commitdiff",
4985                                                                hash=>$hash, hash_parent=>'-c')},
4986                                                 'combined');
4987                         } else { # $hash_parent eq '-c'
4988                                 $formats_nav .= ' | ' .
4989                                         $cgi->a({-href => href(action=>"commitdiff",
4990                                                                hash=>$hash, hash_parent=>'--cc')},
4991                                                 'compact');
4992                         }
4993                         $formats_nav .=
4994                                 ' (merge: ' .
4995                                 join(' ', map {
4996                                         $cgi->a({-href => href(action=>"commitdiff",
4997                                                                hash=>$_)},
4998                                                 esc_html(substr($_, 0, 7)));
4999                                 } @{$co{'parents'}} ) .
5000                                 ')';
5001                 }
5002         }
5004         my $hash_parent_param = $hash_parent;
5005         if (!defined $hash_parent_param) {
5006                 # --cc for multiple parents, --root for parentless
5007                 $hash_parent_param =
5008                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5009         }
5011         # read commitdiff
5012         my $fd;
5013         my @difftree;
5014         if ($format eq 'html') {
5015                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5016                         "--no-commit-id", "--patch-with-raw", "--full-index",
5017                         $hash_parent_param, $hash, "--"
5018                         or die_error(500, "Open git-diff-tree failed");
5020                 while (my $line = <$fd>) {
5021                         chomp $line;
5022                         # empty line ends raw part of diff-tree output
5023                         last unless $line;
5024                         push @difftree, scalar parse_difftree_raw_line($line);
5025                 }
5027         } elsif ($format eq 'plain') {
5028                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5029                         '-p', $hash_parent_param, $hash, "--"
5030                         or die_error(500, "Open git-diff-tree failed");
5032         } else {
5033                 die_error(400, "Unknown commitdiff format");
5034         }
5036         # non-textual hash id's can be cached
5037         my $expires;
5038         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5039                 $expires = "+1d";
5040         }
5042         # write commit message
5043         if ($format eq 'html') {
5044                 my $refs = git_get_references();
5045                 my $ref = format_ref_marker($refs, $co{'id'});
5047                 git_header_html(undef, $expires);
5048                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5049                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5050                 git_print_authorship(\%co);
5051                 print "<div class=\"page_body\">\n";
5052                 if (@{$co{'comment'}} > 1) {
5053                         print "<div class=\"log\">\n";
5054                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5055                         print "</div>\n"; # class="log"
5056                 }
5058         } elsif ($format eq 'plain') {
5059                 my $refs = git_get_references("tags");
5060                 my $tagname = git_get_rev_name_tags($hash);
5061                 my $filename = basename($project) . "-$hash.patch";
5063                 print $cgi->header(
5064                         -type => 'text/plain',
5065                         -charset => 'utf-8',
5066                         -expires => $expires,
5067                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5068                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5069                 print "From: " . to_utf8($co{'author'}) . "\n";
5070                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5071                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5073                 print "X-Git-Tag: $tagname\n" if $tagname;
5074                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5076                 foreach my $line (@{$co{'comment'}}) {
5077                         print to_utf8($line) . "\n";
5078                 }
5079                 print "---\n\n";
5080         }
5082         # write patch
5083         if ($format eq 'html') {
5084                 my $use_parents = !defined $hash_parent ||
5085                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5086                 git_difftree_body(\@difftree, $hash,
5087                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5088                 print "<br/>\n";
5090                 git_patchset_body($fd, \@difftree, $hash,
5091                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5092                 close $fd;
5093                 print "</div>\n"; # class="page_body"
5094                 git_footer_html();
5096         } elsif ($format eq 'plain') {
5097                 local $/ = undef;
5098                 print <$fd>;
5099                 close $fd
5100                         or print "Reading git-diff-tree failed\n";
5101         }
5104 sub git_commitdiff_plain {
5105         git_commitdiff('plain');
5108 sub git_history {
5109         if (!defined $hash_base) {
5110                 $hash_base = git_get_head_hash($project);
5111         }
5112         if (!defined $page) {
5113                 $page = 0;
5114         }
5115         my $ftype;
5116         my %co = parse_commit($hash_base)
5117             or die_error(404, "Unknown commit object");
5119         my $refs = git_get_references();
5120         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5122         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5123                                        $file_name, "--full-history")
5124             or die_error(404, "No such file or directory on given branch");
5126         if (!defined $hash && defined $file_name) {
5127                 # some commits could have deleted file in question,
5128                 # and not have it in tree, but one of them has to have it
5129                 for (my $i = 0; $i <= @commitlist; $i++) {
5130                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5131                         last if defined $hash;
5132                 }
5133         }
5134         if (defined $hash) {
5135                 $ftype = git_get_type($hash);
5136         }
5137         if (!defined $ftype) {
5138                 die_error(500, "Unknown type of object");
5139         }
5141         my $paging_nav = '';
5142         if ($page > 0) {
5143                 $paging_nav .=
5144                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5145                                                file_name=>$file_name)},
5146                                 "first");
5147                 $paging_nav .= " &sdot; " .
5148                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5149                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5150         } else {
5151                 $paging_nav .= "first";
5152                 $paging_nav .= " &sdot; prev";
5153         }
5154         my $next_link = '';
5155         if ($#commitlist >= 100) {
5156                 $next_link =
5157                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5158                                  -accesskey => "n", -title => "Alt-n"}, "next");
5159                 $paging_nav .= " &sdot; $next_link";
5160         } else {
5161                 $paging_nav .= " &sdot; next";
5162         }
5164         git_header_html();
5165         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5166         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5167         git_print_page_path($file_name, $ftype, $hash_base);
5169         git_history_body(\@commitlist, 0, 99,
5170                          $refs, $hash_base, $ftype, $next_link);
5172         git_footer_html();
5175 sub git_search {
5176         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5177         if (!defined $searchtext) {
5178                 die_error(400, "Text field is empty");
5179         }
5180         if (!defined $hash) {
5181                 $hash = git_get_head_hash($project);
5182         }
5183         my %co = parse_commit($hash);
5184         if (!%co) {
5185                 die_error(404, "Unknown commit object");
5186         }
5187         if (!defined $page) {
5188                 $page = 0;
5189         }
5191         $searchtype ||= 'commit';
5192         if ($searchtype eq 'pickaxe') {
5193                 # pickaxe may take all resources of your box and run for several minutes
5194                 # with every query - so decide by yourself how public you make this feature
5195                 gitweb_check_feature('pickaxe')
5196                     or die_error(403, "Pickaxe is disabled");
5197         }
5198         if ($searchtype eq 'grep') {
5199                 gitweb_check_feature('grep')
5200                     or die_error(403, "Grep is disabled");
5201         }
5203         git_header_html();
5205         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5206                 my $greptype;
5207                 if ($searchtype eq 'commit') {
5208                         $greptype = "--grep=";
5209                 } elsif ($searchtype eq 'author') {
5210                         $greptype = "--author=";
5211                 } elsif ($searchtype eq 'committer') {
5212                         $greptype = "--committer=";
5213                 }
5214                 $greptype .= $searchtext;
5215                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5216                                                $greptype, '--regexp-ignore-case',
5217                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5219                 my $paging_nav = '';
5220                 if ($page > 0) {
5221                         $paging_nav .=
5222                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5223                                                        searchtext=>$searchtext,
5224                                                        searchtype=>$searchtype)},
5225                                         "first");
5226                         $paging_nav .= " &sdot; " .
5227                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5228                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5229                 } else {
5230                         $paging_nav .= "first";
5231                         $paging_nav .= " &sdot; prev";
5232                 }
5233                 my $next_link = '';
5234                 if ($#commitlist >= 100) {
5235                         $next_link =
5236                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5237                                          -accesskey => "n", -title => "Alt-n"}, "next");
5238                         $paging_nav .= " &sdot; $next_link";
5239                 } else {
5240                         $paging_nav .= " &sdot; next";
5241                 }
5243                 if ($#commitlist >= 100) {
5244                 }
5246                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5247                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5248                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5249         }
5251         if ($searchtype eq 'pickaxe') {
5252                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5253                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5255                 print "<table class=\"pickaxe search\">\n";
5256                 my $alternate = 1;
5257                 $/ = "\n";
5258                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5259                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5260                         ($search_use_regexp ? '--pickaxe-regex' : ());
5261                 undef %co;
5262                 my @files;
5263                 while (my $line = <$fd>) {
5264                         chomp $line;
5265                         next unless $line;
5267                         my %set = parse_difftree_raw_line($line);
5268                         if (defined $set{'commit'}) {
5269                                 # finish previous commit
5270                                 if (%co) {
5271                                         print "</td>\n" .
5272                                               "<td class=\"link\">" .
5273                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5274                                               " | " .
5275                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5276                                         print "</td>\n" .
5277                                               "</tr>\n";
5278                                 }
5280                                 if ($alternate) {
5281                                         print "<tr class=\"dark\">\n";
5282                                 } else {
5283                                         print "<tr class=\"light\">\n";
5284                                 }
5285                                 $alternate ^= 1;
5286                                 %co = parse_commit($set{'commit'});
5287                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5288                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5289                                       "<td><i>$author</i></td>\n" .
5290                                       "<td>" .
5291                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5292                                               -class => "list subject"},
5293                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5294                         } elsif (defined $set{'to_id'}) {
5295                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5297                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5298                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5299                                               -class => "list"},
5300                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5301                                       "<br/>\n";
5302                         }
5303                 }
5304                 close $fd;
5306                 # finish last commit (warning: repetition!)
5307                 if (%co) {
5308                         print "</td>\n" .
5309                               "<td class=\"link\">" .
5310                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5311                               " | " .
5312                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5313                         print "</td>\n" .
5314                               "</tr>\n";
5315                 }
5317                 print "</table>\n";
5318         }
5320         if ($searchtype eq 'grep') {
5321                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5322                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5324                 print "<table class=\"grep_search\">\n";
5325                 my $alternate = 1;
5326                 my $matches = 0;
5327                 $/ = "\n";
5328                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5329                         $search_use_regexp ? ('-E', '-i') : '-F',
5330                         $searchtext, $co{'tree'};
5331                 my $lastfile = '';
5332                 while (my $line = <$fd>) {
5333                         chomp $line;
5334                         my ($file, $lno, $ltext, $binary);
5335                         last if ($matches++ > 1000);
5336                         if ($line =~ /^Binary file (.+) matches$/) {
5337                                 $file = $1;
5338                                 $binary = 1;
5339                         } else {
5340                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5341                         }
5342                         if ($file ne $lastfile) {
5343                                 $lastfile and print "</td></tr>\n";
5344                                 if ($alternate++) {
5345                                         print "<tr class=\"dark\">\n";
5346                                 } else {
5347                                         print "<tr class=\"light\">\n";
5348                                 }
5349                                 print "<td class=\"list\">".
5350                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5351                                                                file_name=>"$file"),
5352                                                 -class => "list"}, esc_path($file));
5353                                 print "</td><td>\n";
5354                                 $lastfile = $file;
5355                         }
5356                         if ($binary) {
5357                                 print "<div class=\"binary\">Binary file</div>\n";
5358                         } else {
5359                                 $ltext = untabify($ltext);
5360                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5361                                         $ltext = esc_html($1, -nbsp=>1);
5362                                         $ltext .= '<span class="match">';
5363                                         $ltext .= esc_html($2, -nbsp=>1);
5364                                         $ltext .= '</span>';
5365                                         $ltext .= esc_html($3, -nbsp=>1);
5366                                 } else {
5367                                         $ltext = esc_html($ltext, -nbsp=>1);
5368                                 }
5369                                 print "<div class=\"pre\">" .
5370                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5371                                                                file_name=>"$file").'#l'.$lno,
5372                                                 -class => "linenr"}, sprintf('%4i', $lno))
5373                                         . ' ' .  $ltext . "</div>\n";
5374                         }
5375                 }
5376                 if ($lastfile) {
5377                         print "</td></tr>\n";
5378                         if ($matches > 1000) {
5379                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5380                         }
5381                 } else {
5382                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5383                 }
5384                 close $fd;
5386                 print "</table>\n";
5387         }
5388         git_footer_html();
5391 sub git_search_help {
5392         git_header_html();
5393         git_print_page_nav('','', $hash,$hash,$hash);
5394         print <<EOT;
5395 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5396 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5397 the pattern entered is recognized as the POSIX extended
5398 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5399 insensitive).</p>
5400 <dl>
5401 <dt><b>commit</b></dt>
5402 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5403 EOT
5404         my ($have_grep) = gitweb_check_feature('grep');
5405         if ($have_grep) {
5406                 print <<EOT;
5407 <dt><b>grep</b></dt>
5408 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5409     a different one) are searched for the given pattern. On large trees, this search can take
5410 a while and put some strain on the server, so please use it with some consideration. Note that
5411 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5412 case-sensitive.</dd>
5413 EOT
5414         }
5415         print <<EOT;
5416 <dt><b>author</b></dt>
5417 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5418 <dt><b>committer</b></dt>
5419 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5420 EOT
5421         my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5422         if ($have_pickaxe) {
5423                 print <<EOT;
5424 <dt><b>pickaxe</b></dt>
5425 <dd>All commits that caused the string to appear or disappear from any file (changes that
5426 added, removed or "modified" the string) will be listed. This search can take a while and
5427 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5428 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5429 EOT
5430         }
5431         print "</dl>\n";
5432         git_footer_html();
5435 sub git_shortlog {
5436         my $head = git_get_head_hash($project);
5437         if (!defined $hash) {
5438                 $hash = $head;
5439         }
5440         if (!defined $page) {
5441                 $page = 0;
5442         }
5443         my $refs = git_get_references();
5445         my @commitlist = parse_commits($hash, 101, (100 * $page));
5447         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5448         my $next_link = '';
5449         if ($#commitlist >= 100) {
5450                 $next_link =
5451                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5452                                  -accesskey => "n", -title => "Alt-n"}, "next");
5453         }
5455         git_header_html();
5456         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5457         git_print_header_div('summary', $project);
5459         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5461         git_footer_html();
5464 ## ......................................................................
5465 ## feeds (RSS, Atom; OPML)
5467 sub git_feed {
5468         my $format = shift || 'atom';
5469         my ($have_blame) = gitweb_check_feature('blame');
5471         # Atom: http://www.atomenabled.org/developers/syndication/
5472         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5473         if ($format ne 'rss' && $format ne 'atom') {
5474                 die_error(400, "Unknown web feed format");
5475         }
5477         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5478         my $head = $hash || 'HEAD';
5479         my @commitlist = parse_commits($head, 150, 0, $file_name);
5481         my %latest_commit;
5482         my %latest_date;
5483         my $content_type = "application/$format+xml";
5484         if (defined $cgi->http('HTTP_ACCEPT') &&
5485                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5486                 # browser (feed reader) prefers text/xml
5487                 $content_type = 'text/xml';
5488         }
5489         if (defined($commitlist[0])) {
5490                 %latest_commit = %{$commitlist[0]};
5491                 %latest_date   = parse_date($latest_commit{'author_epoch'});
5492                 print $cgi->header(
5493                         -type => $content_type,
5494                         -charset => 'utf-8',
5495                         -last_modified => $latest_date{'rfc2822'});
5496         } else {
5497                 print $cgi->header(
5498                         -type => $content_type,
5499                         -charset => 'utf-8');
5500         }
5502         # Optimization: skip generating the body if client asks only
5503         # for Last-Modified date.
5504         return if ($cgi->request_method() eq 'HEAD');
5506         # header variables
5507         my $title = "$site_name - $project/$action";
5508         my $feed_type = 'log';
5509         if (defined $hash) {
5510                 $title .= " - '$hash'";
5511                 $feed_type = 'branch log';
5512                 if (defined $file_name) {
5513                         $title .= " :: $file_name";
5514                         $feed_type = 'history';
5515                 }
5516         } elsif (defined $file_name) {
5517                 $title .= " - $file_name";
5518                 $feed_type = 'history';
5519         }
5520         $title .= " $feed_type";
5521         my $descr = git_get_project_description($project);
5522         if (defined $descr) {
5523                 $descr = esc_html($descr);
5524         } else {
5525                 $descr = "$project " .
5526                          ($format eq 'rss' ? 'RSS' : 'Atom') .
5527                          " feed";
5528         }
5529         my $owner = git_get_project_owner($project);
5530         $owner = esc_html($owner);
5532         #header
5533         my $alt_url;
5534         if (defined $file_name) {
5535                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5536         } elsif (defined $hash) {
5537                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5538         } else {
5539                 $alt_url = href(-full=>1, action=>"summary");
5540         }
5541         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5542         if ($format eq 'rss') {
5543                 print <<XML;
5544 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5545 <channel>
5546 XML
5547                 print "<title>$title</title>\n" .
5548                       "<link>$alt_url</link>\n" .
5549                       "<description>$descr</description>\n" .
5550                       "<language>en</language>\n";
5551         } elsif ($format eq 'atom') {
5552                 print <<XML;
5553 <feed xmlns="http://www.w3.org/2005/Atom">
5554 XML
5555                 print "<title>$title</title>\n" .
5556                       "<subtitle>$descr</subtitle>\n" .
5557                       '<link rel="alternate" type="text/html" href="' .
5558                       $alt_url . '" />' . "\n" .
5559                       '<link rel="self" type="' . $content_type . '" href="' .
5560                       $cgi->self_url() . '" />' . "\n" .
5561                       "<id>" . href(-full=>1) . "</id>\n" .
5562                       # use project owner for feed author
5563                       "<author><name>$owner</name></author>\n";
5564                 if (defined $favicon) {
5565                         print "<icon>" . esc_url($favicon) . "</icon>\n";
5566                 }
5567                 if (defined $logo_url) {
5568                         # not twice as wide as tall: 72 x 27 pixels
5569                         print "<logo>" . esc_url($logo) . "</logo>\n";
5570                 }
5571                 if (! %latest_date) {
5572                         # dummy date to keep the feed valid until commits trickle in:
5573                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
5574                 } else {
5575                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
5576                 }
5577         }
5579         # contents
5580         for (my $i = 0; $i <= $#commitlist; $i++) {
5581                 my %co = %{$commitlist[$i]};
5582                 my $commit = $co{'id'};
5583                 # we read 150, we always show 30 and the ones more recent than 48 hours
5584                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5585                         last;
5586                 }
5587                 my %cd = parse_date($co{'author_epoch'});
5589                 # get list of changed files
5590                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5591                         $co{'parent'} || "--root",
5592                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
5593                         or next;
5594                 my @difftree = map { chomp; $_ } <$fd>;
5595                 close $fd
5596                         or next;
5598                 # print element (entry, item)
5599                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5600                 if ($format eq 'rss') {
5601                         print "<item>\n" .
5602                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
5603                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
5604                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5605                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5606                               "<link>$co_url</link>\n" .
5607                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
5608                               "<content:encoded>" .
5609                               "<![CDATA[\n";
5610                 } elsif ($format eq 'atom') {
5611                         print "<entry>\n" .
5612                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5613                               "<updated>$cd{'iso-8601'}</updated>\n" .
5614                               "<author>\n" .
5615                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
5616                         if ($co{'author_email'}) {
5617                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
5618                         }
5619                         print "</author>\n" .
5620                               # use committer for contributor
5621                               "<contributor>\n" .
5622                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5623                         if ($co{'committer_email'}) {
5624                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5625                         }
5626                         print "</contributor>\n" .
5627                               "<published>$cd{'iso-8601'}</published>\n" .
5628                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5629                               "<id>$co_url</id>\n" .
5630                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5631                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5632                 }
5633                 my $comment = $co{'comment'};
5634                 print "<pre>\n";
5635                 foreach my $line (@$comment) {
5636                         $line = esc_html($line);
5637                         print "$line\n";
5638                 }
5639                 print "</pre><ul>\n";
5640                 foreach my $difftree_line (@difftree) {
5641                         my %difftree = parse_difftree_raw_line($difftree_line);
5642                         next if !$difftree{'from_id'};
5644                         my $file = $difftree{'file'} || $difftree{'to_file'};
5646                         print "<li>" .
5647                               "[" .
5648                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5649                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5650                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5651                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
5652                                       -title => "diff"}, 'D');
5653                         if ($have_blame) {
5654                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
5655                                                              file_name=>$file, hash_base=>$commit),
5656                                               -title => "blame"}, 'B');
5657                         }
5658                         # if this is not a feed of a file history
5659                         if (!defined $file_name || $file_name ne $file) {
5660                                 print $cgi->a({-href => href(-full=>1, action=>"history",
5661                                                              file_name=>$file, hash=>$commit),
5662                                               -title => "history"}, 'H');
5663                         }
5664                         $file = esc_path($file);
5665                         print "] ".
5666                               "$file</li>\n";
5667                 }
5668                 if ($format eq 'rss') {
5669                         print "</ul>]]>\n" .
5670                               "</content:encoded>\n" .
5671                               "</item>\n";
5672                 } elsif ($format eq 'atom') {
5673                         print "</ul>\n</div>\n" .
5674                               "</content>\n" .
5675                               "</entry>\n";
5676                 }
5677         }
5679         # end of feed
5680         if ($format eq 'rss') {
5681                 print "</channel>\n</rss>\n";
5682         }       elsif ($format eq 'atom') {
5683                 print "</feed>\n";
5684         }
5687 sub git_rss {
5688         git_feed('rss');
5691 sub git_atom {
5692         git_feed('atom');
5695 sub git_opml {
5696         my @list = git_get_projects_list();
5698         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5699         print <<XML;
5700 <?xml version="1.0" encoding="utf-8"?>
5701 <opml version="1.0">
5702 <head>
5703   <title>$site_name OPML Export</title>
5704 </head>
5705 <body>
5706 <outline text="git RSS feeds">
5707 XML
5709         foreach my $pr (@list) {
5710                 my %proj = %$pr;
5711                 my $head = git_get_head_hash($proj{'path'});
5712                 if (!defined $head) {
5713                         next;
5714                 }
5715                 $git_dir = "$projectroot/$proj{'path'}";
5716                 my %co = parse_commit($head);
5717                 if (!%co) {
5718                         next;
5719                 }
5721                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5722                 my $rss  = "$my_url?p=$proj{'path'};a=rss";
5723                 my $html = "$my_url?p=$proj{'path'};a=summary";
5724                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5725         }
5726         print <<XML;
5727 </outline>
5728 </body>
5729 </opml>
5730 XML