Code

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