Code

Merge branch 'maint'
[git.git] / gitweb / gitweb.perl
1 #!/usr/bin/perl
3 # gitweb - simple web interface to track changes in git repositories
4 #
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
7 #
8 # This program is licensed under the GPLv2
10 use strict;
11 use warnings;
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser);
15 use Encode;
16 use Fcntl ':mode';
17 use File::Find qw();
18 use File::Basename qw(basename);
19 binmode STDOUT, ':utf8';
21 BEGIN {
22         CGI->compile() if $ENV{'MOD_PERL'};
23 }
25 our $cgi = new CGI;
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute => 1);
30 # if we're called with PATH_INFO, we have to strip that
31 # from the URL to find our real URL
32 # we make $path_info global because it's also used later on
33 our $path_info = $ENV{"PATH_INFO"};
34 if ($path_info) {
35         $my_url =~ s,\Q$path_info\E$,,;
36         $my_uri =~ s,\Q$path_info\E$,,;
37 }
39 # core git executable to use
40 # this can just be "git" if your webserver has a sensible PATH
41 our $GIT = "++GIT_BINDIR++/git";
43 # absolute fs-path which will be prepended to the project path
44 #our $projectroot = "/pub/scm";
45 our $projectroot = "++GITWEB_PROJECTROOT++";
47 # fs traversing limit for getting project list
48 # the number is relative to the projectroot
49 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
51 # target of the home link on top of all pages
52 our $home_link = $my_uri || "/";
54 # string of the home link on top of all pages
55 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
57 # name of your site or organization to appear in page titles
58 # replace this with something more descriptive for clearer bookmarks
59 our $site_name = "++GITWEB_SITENAME++"
60                  || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
62 # filename of html text to include at top of each page
63 our $site_header = "++GITWEB_SITE_HEADER++";
64 # html text to include at home page
65 our $home_text = "++GITWEB_HOMETEXT++";
66 # filename of html text to include at bottom of each page
67 our $site_footer = "++GITWEB_SITE_FOOTER++";
69 # URI of stylesheets
70 our @stylesheets = ("++GITWEB_CSS++");
71 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
72 our $stylesheet = undef;
73 # URI of GIT logo (72x27 size)
74 our $logo = "++GITWEB_LOGO++";
75 # URI of GIT favicon, assumed to be image/png type
76 our $favicon = "++GITWEB_FAVICON++";
78 # URI and label (title) of GIT logo link
79 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
80 #our $logo_label = "git documentation";
81 our $logo_url = "http://git.or.cz/";
82 our $logo_label = "git homepage";
84 # source of projects list
85 our $projects_list = "++GITWEB_LIST++";
87 # the width (in characters) of the projects list "Description" column
88 our $projects_list_description_width = 25;
90 # default order of projects list
91 # valid values are none, project, descr, owner, and age
92 our $default_projects_order = "project";
94 # show repository only if this file exists
95 # (only effective if this variable evaluates to true)
96 our $export_ok = "++GITWEB_EXPORT_OK++";
98 # show repository only if this subroutine returns true
99 # when given the path to the project, for example:
100 #    sub { return -e "$_[0]/git-daemon-export-ok"; }
101 our $export_auth_hook = undef;
103 # only allow viewing of repositories also shown on the overview page
104 our $strict_export = "++GITWEB_STRICT_EXPORT++";
106 # list of git base URLs used for URL to where fetch project from,
107 # i.e. full URL is "$git_base_url/$project"
108 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
110 # default blob_plain mimetype and default charset for text/plain blob
111 our $default_blob_plain_mimetype = 'text/plain';
112 our $default_text_plain_charset  = undef;
114 # file to use for guessing MIME types before trying /etc/mime.types
115 # (relative to the current git repository)
116 our $mimetypes_file = undef;
118 # assume this charset if line contains non-UTF-8 characters;
119 # it should be valid encoding (see Encoding::Supported(3pm) for list),
120 # for which encoding all byte sequences are valid, for example
121 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
122 # could be even 'utf-8' for the old behavior)
123 our $fallback_encoding = 'latin1';
125 # rename detection options for git-diff and git-diff-tree
126 # - default is '-M', with the cost proportional to
127 #   (number of removed files) * (number of new files).
128 # - more costly is '-C' (which implies '-M'), with the cost proportional to
129 #   (number of changed files + number of removed files) * (number of new files)
130 # - even more costly is '-C', '--find-copies-harder' with cost
131 #   (number of files in the original tree) * (number of new files)
132 # - one might want to include '-B' option, e.g. '-B', '-M'
133 our @diff_opts = ('-M'); # taken from git_commit
135 # information about snapshot formats that gitweb is capable of serving
136 our %known_snapshot_formats = (
137         # name => {
138         #       'display' => display name,
139         #       'type' => mime type,
140         #       'suffix' => filename suffix,
141         #       'format' => --format for git-archive,
142         #       'compressor' => [compressor command and arguments]
143         #                       (array reference, optional)}
144         #
145         'tgz' => {
146                 'display' => 'tar.gz',
147                 'type' => 'application/x-gzip',
148                 'suffix' => '.tar.gz',
149                 'format' => 'tar',
150                 'compressor' => ['gzip']},
152         'tbz2' => {
153                 'display' => 'tar.bz2',
154                 'type' => 'application/x-bzip2',
155                 'suffix' => '.tar.bz2',
156                 'format' => 'tar',
157                 'compressor' => ['bzip2']},
159         'zip' => {
160                 'display' => 'zip',
161                 'type' => 'application/x-zip',
162                 'suffix' => '.zip',
163                 'format' => 'zip'},
164 );
166 # Aliases so we understand old gitweb.snapshot values in repository
167 # configuration.
168 our %known_snapshot_format_aliases = (
169         'gzip'  => 'tgz',
170         'bzip2' => 'tbz2',
172         # backward compatibility: legacy gitweb config support
173         'x-gzip' => undef, 'gz' => undef,
174         'x-bzip2' => undef, 'bz2' => undef,
175         'x-zip' => undef, '' => undef,
176 );
178 # You define site-wide feature defaults here; override them with
179 # $GITWEB_CONFIG as necessary.
180 our %feature = (
181         # feature => {
182         #       'sub' => feature-sub (subroutine),
183         #       'override' => allow-override (boolean),
184         #       'default' => [ default options...] (array reference)}
185         #
186         # if feature is overridable (it means that allow-override has true value),
187         # then feature-sub will be called with default options as parameters;
188         # return value of feature-sub indicates if to enable specified feature
189         #
190         # if there is no 'sub' key (no feature-sub), then feature cannot be
191         # overriden
192         #
193         # use gitweb_get_feature(<feature>) to retrieve the <feature> value
194         # (an array) or gitweb_check_feature(<feature>) to check if <feature>
195         # is enabled
197         # Enable the 'blame' blob view, showing the last commit that modified
198         # each line in the file. This can be very CPU-intensive.
200         # To enable system wide have in $GITWEB_CONFIG
201         # $feature{'blame'}{'default'} = [1];
202         # To have project specific config enable override in $GITWEB_CONFIG
203         # $feature{'blame'}{'override'} = 1;
204         # and in project config gitweb.blame = 0|1;
205         'blame' => {
206                 'sub' => sub { feature_bool('blame', @_) },
207                 'override' => 0,
208                 'default' => [0]},
210         # Enable the 'snapshot' link, providing a compressed archive of any
211         # tree. This can potentially generate high traffic if you have large
212         # project.
214         # Value is a list of formats defined in %known_snapshot_formats that
215         # you wish to offer.
216         # To disable system wide have in $GITWEB_CONFIG
217         # $feature{'snapshot'}{'default'} = [];
218         # To have project specific config enable override in $GITWEB_CONFIG
219         # $feature{'snapshot'}{'override'} = 1;
220         # and in project config, a comma-separated list of formats or "none"
221         # to disable.  Example: gitweb.snapshot = tbz2,zip;
222         'snapshot' => {
223                 'sub' => \&feature_snapshot,
224                 'override' => 0,
225                 'default' => ['tgz']},
227         # Enable text search, which will list the commits which match author,
228         # committer or commit text to a given string.  Enabled by default.
229         # Project specific override is not supported.
230         'search' => {
231                 'override' => 0,
232                 'default' => [1]},
234         # Enable grep search, which will list the files in currently selected
235         # tree containing the given string. Enabled by default. This can be
236         # potentially CPU-intensive, of course.
238         # To enable system wide have in $GITWEB_CONFIG
239         # $feature{'grep'}{'default'} = [1];
240         # To have project specific config enable override in $GITWEB_CONFIG
241         # $feature{'grep'}{'override'} = 1;
242         # and in project config gitweb.grep = 0|1;
243         'grep' => {
244                 'sub' => sub { feature_bool('grep', @_) },
245                 'override' => 0,
246                 'default' => [1]},
248         # Enable the pickaxe search, which will list the commits that modified
249         # a given string in a file. This can be practical and quite faster
250         # alternative to 'blame', but still potentially CPU-intensive.
252         # To enable system wide have in $GITWEB_CONFIG
253         # $feature{'pickaxe'}{'default'} = [1];
254         # To have project specific config enable override in $GITWEB_CONFIG
255         # $feature{'pickaxe'}{'override'} = 1;
256         # and in project config gitweb.pickaxe = 0|1;
257         'pickaxe' => {
258                 'sub' => sub { feature_bool('pickaxe', @_) },
259                 'override' => 0,
260                 'default' => [1]},
262         # Make gitweb use an alternative format of the URLs which can be
263         # more readable and natural-looking: project name is embedded
264         # directly in the path and the query string contains other
265         # auxiliary information. All gitweb installations recognize
266         # URL in either format; this configures in which formats gitweb
267         # generates links.
269         # To enable system wide have in $GITWEB_CONFIG
270         # $feature{'pathinfo'}{'default'} = [1];
271         # Project specific override is not supported.
273         # Note that you will need to change the default location of CSS,
274         # favicon, logo and possibly other files to an absolute URL. Also,
275         # if gitweb.cgi serves as your indexfile, you will need to force
276         # $my_uri to contain the script name in your $GITWEB_CONFIG.
277         'pathinfo' => {
278                 'override' => 0,
279                 'default' => [0]},
281         # Make gitweb consider projects in project root subdirectories
282         # to be forks of existing projects. Given project $projname.git,
283         # projects matching $projname/*.git will not be shown in the main
284         # projects list, instead a '+' mark will be added to $projname
285         # there and a 'forks' view will be enabled for the project, listing
286         # all the forks. If project list is taken from a file, forks have
287         # to be listed after the main project.
289         # To enable system wide have in $GITWEB_CONFIG
290         # $feature{'forks'}{'default'} = [1];
291         # Project specific override is not supported.
292         'forks' => {
293                 'override' => 0,
294                 'default' => [0]},
296         # Insert custom links to the action bar of all project pages.
297         # This enables you mainly to link to third-party scripts integrating
298         # into gitweb; e.g. git-browser for graphical history representation
299         # or custom web-based repository administration interface.
301         # The 'default' value consists of a list of triplets in the form
302         # (label, link, position) where position is the label after which
303         # to insert the link and link is a format string where %n expands
304         # to the project name, %f to the project path within the filesystem,
305         # %h to the current hash (h gitweb parameter) and %b to the current
306         # hash base (hb gitweb parameter); %% expands to %.
308         # To enable system wide have in $GITWEB_CONFIG e.g.
309         # $feature{'actions'}{'default'} = [('graphiclog',
310         #       '/git-browser/by-commit.html?r=%n', 'summary')];
311         # Project specific override is not supported.
312         'actions' => {
313                 'override' => 0,
314                 'default' => []},
316         # Allow gitweb scan project content tags described in ctags/
317         # of project repository, and display the popular Web 2.0-ish
318         # "tag cloud" near the project list. Note that this is something
319         # COMPLETELY different from the normal Git tags.
321         # gitweb by itself can show existing tags, but it does not handle
322         # tagging itself; you need an external application for that.
323         # For an example script, check Girocco's cgi/tagproj.cgi.
324         # You may want to install the HTML::TagCloud Perl module to get
325         # a pretty tag cloud instead of just a list of tags.
327         # To enable system wide have in $GITWEB_CONFIG
328         # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
329         # Project specific override is not supported.
330         'ctags' => {
331                 'override' => 0,
332                 'default' => [0]},
333 );
335 sub gitweb_get_feature {
336         my ($name) = @_;
337         return unless exists $feature{$name};
338         my ($sub, $override, @defaults) = (
339                 $feature{$name}{'sub'},
340                 $feature{$name}{'override'},
341                 @{$feature{$name}{'default'}});
342         if (!$override) { return @defaults; }
343         if (!defined $sub) {
344                 warn "feature $name is not overrideable";
345                 return @defaults;
346         }
347         return $sub->(@defaults);
350 # A wrapper to check if a given feature is enabled.
351 # With this, you can say
353 #   my $bool_feat = gitweb_check_feature('bool_feat');
354 #   gitweb_check_feature('bool_feat') or somecode;
356 # instead of
358 #   my ($bool_feat) = gitweb_get_feature('bool_feat');
359 #   (gitweb_get_feature('bool_feat'))[0] or somecode;
361 sub gitweb_check_feature {
362         return (gitweb_get_feature(@_))[0];
366 sub feature_bool {
367         my $key = shift;
368         my ($val) = git_get_project_config($key, '--bool');
370         if ($val eq 'true') {
371                 return (1);
372         } elsif ($val eq 'false') {
373                 return (0);
374         }
376         return ($_[0]);
379 sub feature_snapshot {
380         my (@fmts) = @_;
382         my ($val) = git_get_project_config('snapshot');
384         if ($val) {
385                 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
386         }
388         return @fmts;
391 # checking HEAD file with -e is fragile if the repository was
392 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
393 # and then pruned.
394 sub check_head_link {
395         my ($dir) = @_;
396         my $headfile = "$dir/HEAD";
397         return ((-e $headfile) ||
398                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
401 sub check_export_ok {
402         my ($dir) = @_;
403         return (check_head_link($dir) &&
404                 (!$export_ok || -e "$dir/$export_ok") &&
405                 (!$export_auth_hook || $export_auth_hook->($dir)));
408 # process alternate names for backward compatibility
409 # filter out unsupported (unknown) snapshot formats
410 sub filter_snapshot_fmts {
411         my @fmts = @_;
413         @fmts = map {
414                 exists $known_snapshot_format_aliases{$_} ?
415                        $known_snapshot_format_aliases{$_} : $_} @fmts;
416         @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
420 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
421 if (-e $GITWEB_CONFIG) {
422         do $GITWEB_CONFIG;
423 } else {
424         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
425         do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
428 # version of the core git binary
429 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
431 $projects_list ||= $projectroot;
433 # ======================================================================
434 # input validation and dispatch
436 # input parameters can be collected from a variety of sources (presently, CGI
437 # and PATH_INFO), so we define an %input_params hash that collects them all
438 # together during validation: this allows subsequent uses (e.g. href()) to be
439 # agnostic of the parameter origin
441 our %input_params = ();
443 # input parameters are stored with the long parameter name as key. This will
444 # also be used in the href subroutine to convert parameters to their CGI
445 # equivalent, and since the href() usage is the most frequent one, we store
446 # the name -> CGI key mapping here, instead of the reverse.
448 # XXX: Warning: If you touch this, check the search form for updating,
449 # too.
451 our @cgi_param_mapping = (
452         project => "p",
453         action => "a",
454         file_name => "f",
455         file_parent => "fp",
456         hash => "h",
457         hash_parent => "hp",
458         hash_base => "hb",
459         hash_parent_base => "hpb",
460         page => "pg",
461         order => "o",
462         searchtext => "s",
463         searchtype => "st",
464         snapshot_format => "sf",
465         extra_options => "opt",
466         search_use_regexp => "sr",
467 );
468 our %cgi_param_mapping = @cgi_param_mapping;
470 # we will also need to know the possible actions, for validation
471 our %actions = (
472         "blame" => \&git_blame,
473         "blobdiff" => \&git_blobdiff,
474         "blobdiff_plain" => \&git_blobdiff_plain,
475         "blob" => \&git_blob,
476         "blob_plain" => \&git_blob_plain,
477         "commitdiff" => \&git_commitdiff,
478         "commitdiff_plain" => \&git_commitdiff_plain,
479         "commit" => \&git_commit,
480         "forks" => \&git_forks,
481         "heads" => \&git_heads,
482         "history" => \&git_history,
483         "log" => \&git_log,
484         "rss" => \&git_rss,
485         "atom" => \&git_atom,
486         "search" => \&git_search,
487         "search_help" => \&git_search_help,
488         "shortlog" => \&git_shortlog,
489         "summary" => \&git_summary,
490         "tag" => \&git_tag,
491         "tags" => \&git_tags,
492         "tree" => \&git_tree,
493         "snapshot" => \&git_snapshot,
494         "object" => \&git_object,
495         # those below don't need $project
496         "opml" => \&git_opml,
497         "project_list" => \&git_project_list,
498         "project_index" => \&git_project_index,
499 );
501 # finally, we have the hash of allowed extra_options for the commands that
502 # allow them
503 our %allowed_options = (
504         "--no-merges" => [ qw(rss atom log shortlog history) ],
505 );
507 # fill %input_params with the CGI parameters. All values except for 'opt'
508 # should be single values, but opt can be an array. We should probably
509 # build an array of parameters that can be multi-valued, but since for the time
510 # being it's only this one, we just single it out
511 while (my ($name, $symbol) = each %cgi_param_mapping) {
512         if ($symbol eq 'opt') {
513                 $input_params{$name} = [ $cgi->param($symbol) ];
514         } else {
515                 $input_params{$name} = $cgi->param($symbol);
516         }
519 # now read PATH_INFO and update the parameter list for missing parameters
520 sub evaluate_path_info {
521         return if defined $input_params{'project'};
522         return if !$path_info;
523         $path_info =~ s,^/+,,;
524         return if !$path_info;
526         # find which part of PATH_INFO is project
527         my $project = $path_info;
528         $project =~ s,/+$,,;
529         while ($project && !check_head_link("$projectroot/$project")) {
530                 $project =~ s,/*[^/]*$,,;
531         }
532         return unless $project;
533         $input_params{'project'} = $project;
535         # do not change any parameters if an action is given using the query string
536         return if $input_params{'action'};
537         $path_info =~ s,^\Q$project\E/*,,;
539         # next, check if we have an action
540         my $action = $path_info;
541         $action =~ s,/.*$,,;
542         if (exists $actions{$action}) {
543                 $path_info =~ s,^$action/*,,;
544                 $input_params{'action'} = $action;
545         }
547         # list of actions that want hash_base instead of hash, but can have no
548         # pathname (f) parameter
549         my @wants_base = (
550                 'tree',
551                 'history',
552         );
554         # we want to catch
555         # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
556         my ($parentrefname, $parentpathname, $refname, $pathname) =
557                 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
559         # first, analyze the 'current' part
560         if (defined $pathname) {
561                 # we got "branch:filename" or "branch:dir/"
562                 # we could use git_get_type(branch:pathname), but:
563                 # - it needs $git_dir
564                 # - it does a git() call
565                 # - the convention of terminating directories with a slash
566                 #   makes it superfluous
567                 # - embedding the action in the PATH_INFO would make it even
568                 #   more superfluous
569                 $pathname =~ s,^/+,,;
570                 if (!$pathname || substr($pathname, -1) eq "/") {
571                         $input_params{'action'} ||= "tree";
572                         $pathname =~ s,/$,,;
573                 } else {
574                         # the default action depends on whether we had parent info
575                         # or not
576                         if ($parentrefname) {
577                                 $input_params{'action'} ||= "blobdiff_plain";
578                         } else {
579                                 $input_params{'action'} ||= "blob_plain";
580                         }
581                 }
582                 $input_params{'hash_base'} ||= $refname;
583                 $input_params{'file_name'} ||= $pathname;
584         } elsif (defined $refname) {
585                 # we got "branch". In this case we have to choose if we have to
586                 # set hash or hash_base.
587                 #
588                 # Most of the actions without a pathname only want hash to be
589                 # set, except for the ones specified in @wants_base that want
590                 # hash_base instead. It should also be noted that hand-crafted
591                 # links having 'history' as an action and no pathname or hash
592                 # set will fail, but that happens regardless of PATH_INFO.
593                 $input_params{'action'} ||= "shortlog";
594                 if (grep { $_ eq $input_params{'action'} } @wants_base) {
595                         $input_params{'hash_base'} ||= $refname;
596                 } else {
597                         $input_params{'hash'} ||= $refname;
598                 }
599         }
601         # next, handle the 'parent' part, if present
602         if (defined $parentrefname) {
603                 # a missing pathspec defaults to the 'current' filename, allowing e.g.
604                 # someproject/blobdiff/oldrev..newrev:/filename
605                 if ($parentpathname) {
606                         $parentpathname =~ s,^/+,,;
607                         $parentpathname =~ s,/$,,;
608                         $input_params{'file_parent'} ||= $parentpathname;
609                 } else {
610                         $input_params{'file_parent'} ||= $input_params{'file_name'};
611                 }
612                 # we assume that hash_parent_base is wanted if a path was specified,
613                 # or if the action wants hash_base instead of hash
614                 if (defined $input_params{'file_parent'} ||
615                         grep { $_ eq $input_params{'action'} } @wants_base) {
616                         $input_params{'hash_parent_base'} ||= $parentrefname;
617                 } else {
618                         $input_params{'hash_parent'} ||= $parentrefname;
619                 }
620         }
622         # for the snapshot action, we allow URLs in the form
623         # $project/snapshot/$hash.ext
624         # where .ext determines the snapshot and gets removed from the
625         # passed $refname to provide the $hash.
626         #
627         # To be able to tell that $refname includes the format extension, we
628         # require the following two conditions to be satisfied:
629         # - the hash input parameter MUST have been set from the $refname part
630         #   of the URL (i.e. they must be equal)
631         # - the snapshot format MUST NOT have been defined already (e.g. from
632         #   CGI parameter sf)
633         # It's also useless to try any matching unless $refname has a dot,
634         # so we check for that too
635         if (defined $input_params{'action'} &&
636                 $input_params{'action'} eq 'snapshot' &&
637                 defined $refname && index($refname, '.') != -1 &&
638                 $refname eq $input_params{'hash'} &&
639                 !defined $input_params{'snapshot_format'}) {
640                 # We loop over the known snapshot formats, checking for
641                 # extensions. Allowed extensions are both the defined suffix
642                 # (which includes the initial dot already) and the snapshot
643                 # format key itself, with a prepended dot
644                 while (my ($fmt, %opt) = each %known_snapshot_formats) {
645                         my $hash = $refname;
646                         my $sfx;
647                         $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//;
648                         next unless $sfx = $1;
649                         # a valid suffix was found, so set the snapshot format
650                         # and reset the hash parameter
651                         $input_params{'snapshot_format'} = $fmt;
652                         $input_params{'hash'} = $hash;
653                         # we also set the format suffix to the one requested
654                         # in the URL: this way a request for e.g. .tgz returns
655                         # a .tgz instead of a .tar.gz
656                         $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
657                         last;
658                 }
659         }
661 evaluate_path_info();
663 our $action = $input_params{'action'};
664 if (defined $action) {
665         if (!validate_action($action)) {
666                 die_error(400, "Invalid action parameter");
667         }
670 # parameters which are pathnames
671 our $project = $input_params{'project'};
672 if (defined $project) {
673         if (!validate_project($project)) {
674                 undef $project;
675                 die_error(404, "No such project");
676         }
679 our $file_name = $input_params{'file_name'};
680 if (defined $file_name) {
681         if (!validate_pathname($file_name)) {
682                 die_error(400, "Invalid file parameter");
683         }
686 our $file_parent = $input_params{'file_parent'};
687 if (defined $file_parent) {
688         if (!validate_pathname($file_parent)) {
689                 die_error(400, "Invalid file parent parameter");
690         }
693 # parameters which are refnames
694 our $hash = $input_params{'hash'};
695 if (defined $hash) {
696         if (!validate_refname($hash)) {
697                 die_error(400, "Invalid hash parameter");
698         }
701 our $hash_parent = $input_params{'hash_parent'};
702 if (defined $hash_parent) {
703         if (!validate_refname($hash_parent)) {
704                 die_error(400, "Invalid hash parent parameter");
705         }
708 our $hash_base = $input_params{'hash_base'};
709 if (defined $hash_base) {
710         if (!validate_refname($hash_base)) {
711                 die_error(400, "Invalid hash base parameter");
712         }
715 our @extra_options = @{$input_params{'extra_options'}};
716 # @extra_options is always defined, since it can only be (currently) set from
717 # CGI, and $cgi->param() returns the empty array in array context if the param
718 # is not set
719 foreach my $opt (@extra_options) {
720         if (not exists $allowed_options{$opt}) {
721                 die_error(400, "Invalid option parameter");
722         }
723         if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
724                 die_error(400, "Invalid option parameter for this action");
725         }
728 our $hash_parent_base = $input_params{'hash_parent_base'};
729 if (defined $hash_parent_base) {
730         if (!validate_refname($hash_parent_base)) {
731                 die_error(400, "Invalid hash parent base parameter");
732         }
735 # other parameters
736 our $page = $input_params{'page'};
737 if (defined $page) {
738         if ($page =~ m/[^0-9]/) {
739                 die_error(400, "Invalid page parameter");
740         }
743 our $searchtype = $input_params{'searchtype'};
744 if (defined $searchtype) {
745         if ($searchtype =~ m/[^a-z]/) {
746                 die_error(400, "Invalid searchtype parameter");
747         }
750 our $search_use_regexp = $input_params{'search_use_regexp'};
752 our $searchtext = $input_params{'searchtext'};
753 our $search_regexp;
754 if (defined $searchtext) {
755         if (length($searchtext) < 2) {
756                 die_error(403, "At least two characters are required for search parameter");
757         }
758         $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
761 # path to the current git repository
762 our $git_dir;
763 $git_dir = "$projectroot/$project" if $project;
765 # list of supported snapshot formats
766 our @snapshot_fmts = gitweb_get_feature('snapshot');
767 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
769 # dispatch
770 if (!defined $action) {
771         if (defined $hash) {
772                 $action = git_get_type($hash);
773         } elsif (defined $hash_base && defined $file_name) {
774                 $action = git_get_type("$hash_base:$file_name");
775         } elsif (defined $project) {
776                 $action = 'summary';
777         } else {
778                 $action = 'project_list';
779         }
781 if (!defined($actions{$action})) {
782         die_error(400, "Unknown action");
784 if ($action !~ m/^(opml|project_list|project_index)$/ &&
785     !$project) {
786         die_error(400, "Project needed");
788 $actions{$action}->();
789 exit;
791 ## ======================================================================
792 ## action links
794 sub href (%) {
795         my %params = @_;
796         # default is to use -absolute url() i.e. $my_uri
797         my $href = $params{-full} ? $my_url : $my_uri;
799         $params{'project'} = $project unless exists $params{'project'};
801         if ($params{-replay}) {
802                 while (my ($name, $symbol) = each %cgi_param_mapping) {
803                         if (!exists $params{$name}) {
804                                 $params{$name} = $input_params{$name};
805                         }
806                 }
807         }
809         my $use_pathinfo = gitweb_check_feature('pathinfo');
810         if ($use_pathinfo) {
811                 # try to put as many parameters as possible in PATH_INFO:
812                 #   - project name
813                 #   - action
814                 #   - hash_parent or hash_parent_base:/file_parent
815                 #   - hash or hash_base:/filename
816                 #   - the snapshot_format as an appropriate suffix
818                 # When the script is the root DirectoryIndex for the domain,
819                 # $href here would be something like http://gitweb.example.com/
820                 # Thus, we strip any trailing / from $href, to spare us double
821                 # slashes in the final URL
822                 $href =~ s,/$,,;
824                 # Then add the project name, if present
825                 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
826                 delete $params{'project'};
828                 # since we destructively absorb parameters, we keep this
829                 # boolean that remembers if we're handling a snapshot
830                 my $is_snapshot = $params{'action'} eq 'snapshot';
832                 # Summary just uses the project path URL, any other action is
833                 # added to the URL
834                 if (defined $params{'action'}) {
835                         $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
836                         delete $params{'action'};
837                 }
839                 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
840                 # stripping nonexistent or useless pieces
841                 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
842                         || $params{'hash_parent'} || $params{'hash'});
843                 if (defined $params{'hash_base'}) {
844                         if (defined $params{'hash_parent_base'}) {
845                                 $href .= esc_url($params{'hash_parent_base'});
846                                 # skip the file_parent if it's the same as the file_name
847                                 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
848                                 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
849                                         $href .= ":/".esc_url($params{'file_parent'});
850                                         delete $params{'file_parent'};
851                                 }
852                                 $href .= "..";
853                                 delete $params{'hash_parent'};
854                                 delete $params{'hash_parent_base'};
855                         } elsif (defined $params{'hash_parent'}) {
856                                 $href .= esc_url($params{'hash_parent'}). "..";
857                                 delete $params{'hash_parent'};
858                         }
860                         $href .= esc_url($params{'hash_base'});
861                         if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
862                                 $href .= ":/".esc_url($params{'file_name'});
863                                 delete $params{'file_name'};
864                         }
865                         delete $params{'hash'};
866                         delete $params{'hash_base'};
867                 } elsif (defined $params{'hash'}) {
868                         $href .= esc_url($params{'hash'});
869                         delete $params{'hash'};
870                 }
872                 # If the action was a snapshot, we can absorb the
873                 # snapshot_format parameter too
874                 if ($is_snapshot) {
875                         my $fmt = $params{'snapshot_format'};
876                         # snapshot_format should always be defined when href()
877                         # is called, but just in case some code forgets, we
878                         # fall back to the default
879                         $fmt ||= $snapshot_fmts[0];
880                         $href .= $known_snapshot_formats{$fmt}{'suffix'};
881                         delete $params{'snapshot_format'};
882                 }
883         }
885         # now encode the parameters explicitly
886         my @result = ();
887         for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
888                 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
889                 if (defined $params{$name}) {
890                         if (ref($params{$name}) eq "ARRAY") {
891                                 foreach my $par (@{$params{$name}}) {
892                                         push @result, $symbol . "=" . esc_param($par);
893                                 }
894                         } else {
895                                 push @result, $symbol . "=" . esc_param($params{$name});
896                         }
897                 }
898         }
899         $href .= "?" . join(';', @result) if scalar @result;
901         return $href;
905 ## ======================================================================
906 ## validation, quoting/unquoting and escaping
908 sub validate_action {
909         my $input = shift || return undef;
910         return undef unless exists $actions{$input};
911         return $input;
914 sub validate_project {
915         my $input = shift || return undef;
916         if (!validate_pathname($input) ||
917                 !(-d "$projectroot/$input") ||
918                 !check_export_ok("$projectroot/$input") ||
919                 ($strict_export && !project_in_list($input))) {
920                 return undef;
921         } else {
922                 return $input;
923         }
926 sub validate_pathname {
927         my $input = shift || return undef;
929         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
930         # at the beginning, at the end, and between slashes.
931         # also this catches doubled slashes
932         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
933                 return undef;
934         }
935         # no null characters
936         if ($input =~ m!\0!) {
937                 return undef;
938         }
939         return $input;
942 sub validate_refname {
943         my $input = shift || return undef;
945         # textual hashes are O.K.
946         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
947                 return $input;
948         }
949         # it must be correct pathname
950         $input = validate_pathname($input)
951                 or return undef;
952         # restrictions on ref name according to git-check-ref-format
953         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
954                 return undef;
955         }
956         return $input;
959 # decode sequences of octets in utf8 into Perl's internal form,
960 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
961 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
962 sub to_utf8 {
963         my $str = shift;
964         if (utf8::valid($str)) {
965                 utf8::decode($str);
966                 return $str;
967         } else {
968                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
969         }
972 # quote unsafe chars, but keep the slash, even when it's not
973 # correct, but quoted slashes look too horrible in bookmarks
974 sub esc_param {
975         my $str = shift;
976         $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
977         $str =~ s/\+/%2B/g;
978         $str =~ s/ /\+/g;
979         return $str;
982 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
983 sub esc_url {
984         my $str = shift;
985         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
986         $str =~ s/\+/%2B/g;
987         $str =~ s/ /\+/g;
988         return $str;
991 # replace invalid utf8 character with SUBSTITUTION sequence
992 sub esc_html ($;%) {
993         my $str = shift;
994         my %opts = @_;
996         $str = to_utf8($str);
997         $str = $cgi->escapeHTML($str);
998         if ($opts{'-nbsp'}) {
999                 $str =~ s/ /&nbsp;/g;
1000         }
1001         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1002         return $str;
1005 # quote control characters and escape filename to HTML
1006 sub esc_path {
1007         my $str = shift;
1008         my %opts = @_;
1010         $str = to_utf8($str);
1011         $str = $cgi->escapeHTML($str);
1012         if ($opts{'-nbsp'}) {
1013                 $str =~ s/ /&nbsp;/g;
1014         }
1015         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1016         return $str;
1019 # Make control characters "printable", using character escape codes (CEC)
1020 sub quot_cec {
1021         my $cntrl = shift;
1022         my %opts = @_;
1023         my %es = ( # character escape codes, aka escape sequences
1024                 "\t" => '\t',   # tab            (HT)
1025                 "\n" => '\n',   # line feed      (LF)
1026                 "\r" => '\r',   # carrige return (CR)
1027                 "\f" => '\f',   # form feed      (FF)
1028                 "\b" => '\b',   # backspace      (BS)
1029                 "\a" => '\a',   # alarm (bell)   (BEL)
1030                 "\e" => '\e',   # escape         (ESC)
1031                 "\013" => '\v', # vertical tab   (VT)
1032                 "\000" => '\0', # nul character  (NUL)
1033         );
1034         my $chr = ( (exists $es{$cntrl})
1035                     ? $es{$cntrl}
1036                     : sprintf('\%2x', ord($cntrl)) );
1037         if ($opts{-nohtml}) {
1038                 return $chr;
1039         } else {
1040                 return "<span class=\"cntrl\">$chr</span>";
1041         }
1044 # Alternatively use unicode control pictures codepoints,
1045 # Unicode "printable representation" (PR)
1046 sub quot_upr {
1047         my $cntrl = shift;
1048         my %opts = @_;
1050         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1051         if ($opts{-nohtml}) {
1052                 return $chr;
1053         } else {
1054                 return "<span class=\"cntrl\">$chr</span>";
1055         }
1058 # git may return quoted and escaped filenames
1059 sub unquote {
1060         my $str = shift;
1062         sub unq {
1063                 my $seq = shift;
1064                 my %es = ( # character escape codes, aka escape sequences
1065                         't' => "\t",   # tab            (HT, TAB)
1066                         'n' => "\n",   # newline        (NL)
1067                         'r' => "\r",   # return         (CR)
1068                         'f' => "\f",   # form feed      (FF)
1069                         'b' => "\b",   # backspace      (BS)
1070                         'a' => "\a",   # alarm (bell)   (BEL)
1071                         'e' => "\e",   # escape         (ESC)
1072                         'v' => "\013", # vertical tab   (VT)
1073                 );
1075                 if ($seq =~ m/^[0-7]{1,3}$/) {
1076                         # octal char sequence
1077                         return chr(oct($seq));
1078                 } elsif (exists $es{$seq}) {
1079                         # C escape sequence, aka character escape code
1080                         return $es{$seq};
1081                 }
1082                 # quoted ordinary character
1083                 return $seq;
1084         }
1086         if ($str =~ m/^"(.*)"$/) {
1087                 # needs unquoting
1088                 $str = $1;
1089                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1090         }
1091         return $str;
1094 # escape tabs (convert tabs to spaces)
1095 sub untabify {
1096         my $line = shift;
1098         while ((my $pos = index($line, "\t")) != -1) {
1099                 if (my $count = (8 - ($pos % 8))) {
1100                         my $spaces = ' ' x $count;
1101                         $line =~ s/\t/$spaces/;
1102                 }
1103         }
1105         return $line;
1108 sub project_in_list {
1109         my $project = shift;
1110         my @list = git_get_projects_list();
1111         return @list && scalar(grep { $_->{'path'} eq $project } @list);
1114 ## ----------------------------------------------------------------------
1115 ## HTML aware string manipulation
1117 # Try to chop given string on a word boundary between position
1118 # $len and $len+$add_len. If there is no word boundary there,
1119 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1120 # (marking chopped part) would be longer than given string.
1121 sub chop_str {
1122         my $str = shift;
1123         my $len = shift;
1124         my $add_len = shift || 10;
1125         my $where = shift || 'right'; # 'left' | 'center' | 'right'
1127         # Make sure perl knows it is utf8 encoded so we don't
1128         # cut in the middle of a utf8 multibyte char.
1129         $str = to_utf8($str);
1131         # allow only $len chars, but don't cut a word if it would fit in $add_len
1132         # if it doesn't fit, cut it if it's still longer than the dots we would add
1133         # remove chopped character entities entirely
1135         # when chopping in the middle, distribute $len into left and right part
1136         # return early if chopping wouldn't make string shorter
1137         if ($where eq 'center') {
1138                 return $str if ($len + 5 >= length($str)); # filler is length 5
1139                 $len = int($len/2);
1140         } else {
1141                 return $str if ($len + 4 >= length($str)); # filler is length 4
1142         }
1144         # regexps: ending and beginning with word part up to $add_len
1145         my $endre = qr/.{$len}\w{0,$add_len}/;
1146         my $begre = qr/\w{0,$add_len}.{$len}/;
1148         if ($where eq 'left') {
1149                 $str =~ m/^(.*?)($begre)$/;
1150                 my ($lead, $body) = ($1, $2);
1151                 if (length($lead) > 4) {
1152                         $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1153                         $lead = " ...";
1154                 }
1155                 return "$lead$body";
1157         } elsif ($where eq 'center') {
1158                 $str =~ m/^($endre)(.*)$/;
1159                 my ($left, $str)  = ($1, $2);
1160                 $str =~ m/^(.*?)($begre)$/;
1161                 my ($mid, $right) = ($1, $2);
1162                 if (length($mid) > 5) {
1163                         $left  =~ s/&[^;]*$//;
1164                         $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1165                         $mid = " ... ";
1166                 }
1167                 return "$left$mid$right";
1169         } else {
1170                 $str =~ m/^($endre)(.*)$/;
1171                 my $body = $1;
1172                 my $tail = $2;
1173                 if (length($tail) > 4) {
1174                         $body =~ s/&[^;]*$//;
1175                         $tail = "... ";
1176                 }
1177                 return "$body$tail";
1178         }
1181 # takes the same arguments as chop_str, but also wraps a <span> around the
1182 # result with a title attribute if it does get chopped. Additionally, the
1183 # string is HTML-escaped.
1184 sub chop_and_escape_str {
1185         my ($str) = @_;
1187         my $chopped = chop_str(@_);
1188         if ($chopped eq $str) {
1189                 return esc_html($chopped);
1190         } else {
1191                 $str =~ s/([[:cntrl:]])/?/g;
1192                 return $cgi->span({-title=>$str}, esc_html($chopped));
1193         }
1196 ## ----------------------------------------------------------------------
1197 ## functions returning short strings
1199 # CSS class for given age value (in seconds)
1200 sub age_class {
1201         my $age = shift;
1203         if (!defined $age) {
1204                 return "noage";
1205         } elsif ($age < 60*60*2) {
1206                 return "age0";
1207         } elsif ($age < 60*60*24*2) {
1208                 return "age1";
1209         } else {
1210                 return "age2";
1211         }
1214 # convert age in seconds to "nn units ago" string
1215 sub age_string {
1216         my $age = shift;
1217         my $age_str;
1219         if ($age > 60*60*24*365*2) {
1220                 $age_str = (int $age/60/60/24/365);
1221                 $age_str .= " years ago";
1222         } elsif ($age > 60*60*24*(365/12)*2) {
1223                 $age_str = int $age/60/60/24/(365/12);
1224                 $age_str .= " months ago";
1225         } elsif ($age > 60*60*24*7*2) {
1226                 $age_str = int $age/60/60/24/7;
1227                 $age_str .= " weeks ago";
1228         } elsif ($age > 60*60*24*2) {
1229                 $age_str = int $age/60/60/24;
1230                 $age_str .= " days ago";
1231         } elsif ($age > 60*60*2) {
1232                 $age_str = int $age/60/60;
1233                 $age_str .= " hours ago";
1234         } elsif ($age > 60*2) {
1235                 $age_str = int $age/60;
1236                 $age_str .= " min ago";
1237         } elsif ($age > 2) {
1238                 $age_str = int $age;
1239                 $age_str .= " sec ago";
1240         } else {
1241                 $age_str .= " right now";
1242         }
1243         return $age_str;
1246 use constant {
1247         S_IFINVALID => 0030000,
1248         S_IFGITLINK => 0160000,
1249 };
1251 # submodule/subproject, a commit object reference
1252 sub S_ISGITLINK($) {
1253         my $mode = shift;
1255         return (($mode & S_IFMT) == S_IFGITLINK)
1258 # convert file mode in octal to symbolic file mode string
1259 sub mode_str {
1260         my $mode = oct shift;
1262         if (S_ISGITLINK($mode)) {
1263                 return 'm---------';
1264         } elsif (S_ISDIR($mode & S_IFMT)) {
1265                 return 'drwxr-xr-x';
1266         } elsif (S_ISLNK($mode)) {
1267                 return 'lrwxrwxrwx';
1268         } elsif (S_ISREG($mode)) {
1269                 # git cares only about the executable bit
1270                 if ($mode & S_IXUSR) {
1271                         return '-rwxr-xr-x';
1272                 } else {
1273                         return '-rw-r--r--';
1274                 };
1275         } else {
1276                 return '----------';
1277         }
1280 # convert file mode in octal to file type string
1281 sub file_type {
1282         my $mode = shift;
1284         if ($mode !~ m/^[0-7]+$/) {
1285                 return $mode;
1286         } else {
1287                 $mode = oct $mode;
1288         }
1290         if (S_ISGITLINK($mode)) {
1291                 return "submodule";
1292         } elsif (S_ISDIR($mode & S_IFMT)) {
1293                 return "directory";
1294         } elsif (S_ISLNK($mode)) {
1295                 return "symlink";
1296         } elsif (S_ISREG($mode)) {
1297                 return "file";
1298         } else {
1299                 return "unknown";
1300         }
1303 # convert file mode in octal to file type description string
1304 sub file_type_long {
1305         my $mode = shift;
1307         if ($mode !~ m/^[0-7]+$/) {
1308                 return $mode;
1309         } else {
1310                 $mode = oct $mode;
1311         }
1313         if (S_ISGITLINK($mode)) {
1314                 return "submodule";
1315         } elsif (S_ISDIR($mode & S_IFMT)) {
1316                 return "directory";
1317         } elsif (S_ISLNK($mode)) {
1318                 return "symlink";
1319         } elsif (S_ISREG($mode)) {
1320                 if ($mode & S_IXUSR) {
1321                         return "executable";
1322                 } else {
1323                         return "file";
1324                 };
1325         } else {
1326                 return "unknown";
1327         }
1331 ## ----------------------------------------------------------------------
1332 ## functions returning short HTML fragments, or transforming HTML fragments
1333 ## which don't belong to other sections
1335 # format line of commit message.
1336 sub format_log_line_html {
1337         my $line = shift;
1339         $line = esc_html($line, -nbsp=>1);
1340         if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1341                 my $hash_text = $1;
1342                 my $link =
1343                         $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1344                                 -class => "text"}, $hash_text);
1345                 $line =~ s/$hash_text/$link/;
1346         }
1347         return $line;
1350 # format marker of refs pointing to given object
1352 # the destination action is chosen based on object type and current context:
1353 # - for annotated tags, we choose the tag view unless it's the current view
1354 #   already, in which case we go to shortlog view
1355 # - for other refs, we keep the current view if we're in history, shortlog or
1356 #   log view, and select shortlog otherwise
1357 sub format_ref_marker {
1358         my ($refs, $id) = @_;
1359         my $markers = '';
1361         if (defined $refs->{$id}) {
1362                 foreach my $ref (@{$refs->{$id}}) {
1363                         # this code exploits the fact that non-lightweight tags are the
1364                         # only indirect objects, and that they are the only objects for which
1365                         # we want to use tag instead of shortlog as action
1366                         my ($type, $name) = qw();
1367                         my $indirect = ($ref =~ s/\^\{\}$//);
1368                         # e.g. tags/v2.6.11 or heads/next
1369                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1370                                 $type = $1;
1371                                 $name = $2;
1372                         } else {
1373                                 $type = "ref";
1374                                 $name = $ref;
1375                         }
1377                         my $class = $type;
1378                         $class .= " indirect" if $indirect;
1380                         my $dest_action = "shortlog";
1382                         if ($indirect) {
1383                                 $dest_action = "tag" unless $action eq "tag";
1384                         } elsif ($action =~ /^(history|(short)?log)$/) {
1385                                 $dest_action = $action;
1386                         }
1388                         my $dest = "";
1389                         $dest .= "refs/" unless $ref =~ m!^refs/!;
1390                         $dest .= $ref;
1392                         my $link = $cgi->a({
1393                                 -href => href(
1394                                         action=>$dest_action,
1395                                         hash=>$dest
1396                                 )}, $name);
1398                         $markers .= " <span class=\"$class\" title=\"$ref\">" .
1399                                 $link . "</span>";
1400                 }
1401         }
1403         if ($markers) {
1404                 return ' <span class="refs">'. $markers . '</span>';
1405         } else {
1406                 return "";
1407         }
1410 # format, perhaps shortened and with markers, title line
1411 sub format_subject_html {
1412         my ($long, $short, $href, $extra) = @_;
1413         $extra = '' unless defined($extra);
1415         if (length($short) < length($long)) {
1416                 return $cgi->a({-href => $href, -class => "list subject",
1417                                 -title => to_utf8($long)},
1418                        esc_html($short) . $extra);
1419         } else {
1420                 return $cgi->a({-href => $href, -class => "list subject"},
1421                        esc_html($long)  . $extra);
1422         }
1425 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1426 sub format_git_diff_header_line {
1427         my $line = shift;
1428         my $diffinfo = shift;
1429         my ($from, $to) = @_;
1431         if ($diffinfo->{'nparents'}) {
1432                 # combined diff
1433                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1434                 if ($to->{'href'}) {
1435                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1436                                          esc_path($to->{'file'}));
1437                 } else { # file was deleted (no href)
1438                         $line .= esc_path($to->{'file'});
1439                 }
1440         } else {
1441                 # "ordinary" diff
1442                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1443                 if ($from->{'href'}) {
1444                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1445                                          'a/' . esc_path($from->{'file'}));
1446                 } else { # file was added (no href)
1447                         $line .= 'a/' . esc_path($from->{'file'});
1448                 }
1449                 $line .= ' ';
1450                 if ($to->{'href'}) {
1451                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1452                                          'b/' . esc_path($to->{'file'}));
1453                 } else { # file was deleted
1454                         $line .= 'b/' . esc_path($to->{'file'});
1455                 }
1456         }
1458         return "<div class=\"diff header\">$line</div>\n";
1461 # format extended diff header line, before patch itself
1462 sub format_extended_diff_header_line {
1463         my $line = shift;
1464         my $diffinfo = shift;
1465         my ($from, $to) = @_;
1467         # match <path>
1468         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1469                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1470                                        esc_path($from->{'file'}));
1471         }
1472         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1473                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1474                                  esc_path($to->{'file'}));
1475         }
1476         # match single <mode>
1477         if ($line =~ m/\s(\d{6})$/) {
1478                 $line .= '<span class="info"> (' .
1479                          file_type_long($1) .
1480                          ')</span>';
1481         }
1482         # match <hash>
1483         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1484                 # can match only for combined diff
1485                 $line = 'index ';
1486                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1487                         if ($from->{'href'}[$i]) {
1488                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1489                                                   -class=>"hash"},
1490                                                  substr($diffinfo->{'from_id'}[$i],0,7));
1491                         } else {
1492                                 $line .= '0' x 7;
1493                         }
1494                         # separator
1495                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1496                 }
1497                 $line .= '..';
1498                 if ($to->{'href'}) {
1499                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1500                                          substr($diffinfo->{'to_id'},0,7));
1501                 } else {
1502                         $line .= '0' x 7;
1503                 }
1505         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1506                 # can match only for ordinary diff
1507                 my ($from_link, $to_link);
1508                 if ($from->{'href'}) {
1509                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1510                                              substr($diffinfo->{'from_id'},0,7));
1511                 } else {
1512                         $from_link = '0' x 7;
1513                 }
1514                 if ($to->{'href'}) {
1515                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1516                                            substr($diffinfo->{'to_id'},0,7));
1517                 } else {
1518                         $to_link = '0' x 7;
1519                 }
1520                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1521                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1522         }
1524         return $line . "<br/>\n";
1527 # format from-file/to-file diff header
1528 sub format_diff_from_to_header {
1529         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1530         my $line;
1531         my $result = '';
1533         $line = $from_line;
1534         #assert($line =~ m/^---/) if DEBUG;
1535         # no extra formatting for "^--- /dev/null"
1536         if (! $diffinfo->{'nparents'}) {
1537                 # ordinary (single parent) diff
1538                 if ($line =~ m!^--- "?a/!) {
1539                         if ($from->{'href'}) {
1540                                 $line = '--- a/' .
1541                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1542                                                 esc_path($from->{'file'}));
1543                         } else {
1544                                 $line = '--- a/' .
1545                                         esc_path($from->{'file'});
1546                         }
1547                 }
1548                 $result .= qq!<div class="diff from_file">$line</div>\n!;
1550         } else {
1551                 # combined diff (merge commit)
1552                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1553                         if ($from->{'href'}[$i]) {
1554                                 $line = '--- ' .
1555                                         $cgi->a({-href=>href(action=>"blobdiff",
1556                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
1557                                                              hash_parent_base=>$parents[$i],
1558                                                              file_parent=>$from->{'file'}[$i],
1559                                                              hash=>$diffinfo->{'to_id'},
1560                                                              hash_base=>$hash,
1561                                                              file_name=>$to->{'file'}),
1562                                                  -class=>"path",
1563                                                  -title=>"diff" . ($i+1)},
1564                                                 $i+1) .
1565                                         '/' .
1566                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1567                                                 esc_path($from->{'file'}[$i]));
1568                         } else {
1569                                 $line = '--- /dev/null';
1570                         }
1571                         $result .= qq!<div class="diff from_file">$line</div>\n!;
1572                 }
1573         }
1575         $line = $to_line;
1576         #assert($line =~ m/^\+\+\+/) if DEBUG;
1577         # no extra formatting for "^+++ /dev/null"
1578         if ($line =~ m!^\+\+\+ "?b/!) {
1579                 if ($to->{'href'}) {
1580                         $line = '+++ b/' .
1581                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1582                                         esc_path($to->{'file'}));
1583                 } else {
1584                         $line = '+++ b/' .
1585                                 esc_path($to->{'file'});
1586                 }
1587         }
1588         $result .= qq!<div class="diff to_file">$line</div>\n!;
1590         return $result;
1593 # create note for patch simplified by combined diff
1594 sub format_diff_cc_simplified {
1595         my ($diffinfo, @parents) = @_;
1596         my $result = '';
1598         $result .= "<div class=\"diff header\">" .
1599                    "diff --cc ";
1600         if (!is_deleted($diffinfo)) {
1601                 $result .= $cgi->a({-href => href(action=>"blob",
1602                                                   hash_base=>$hash,
1603                                                   hash=>$diffinfo->{'to_id'},
1604                                                   file_name=>$diffinfo->{'to_file'}),
1605                                     -class => "path"},
1606                                    esc_path($diffinfo->{'to_file'}));
1607         } else {
1608                 $result .= esc_path($diffinfo->{'to_file'});
1609         }
1610         $result .= "</div>\n" . # class="diff header"
1611                    "<div class=\"diff nodifferences\">" .
1612                    "Simple merge" .
1613                    "</div>\n"; # class="diff nodifferences"
1615         return $result;
1618 # format patch (diff) line (not to be used for diff headers)
1619 sub format_diff_line {
1620         my $line = shift;
1621         my ($from, $to) = @_;
1622         my $diff_class = "";
1624         chomp $line;
1626         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1627                 # combined diff
1628                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1629                 if ($line =~ m/^\@{3}/) {
1630                         $diff_class = " chunk_header";
1631                 } elsif ($line =~ m/^\\/) {
1632                         $diff_class = " incomplete";
1633                 } elsif ($prefix =~ tr/+/+/) {
1634                         $diff_class = " add";
1635                 } elsif ($prefix =~ tr/-/-/) {
1636                         $diff_class = " rem";
1637                 }
1638         } else {
1639                 # assume ordinary diff
1640                 my $char = substr($line, 0, 1);
1641                 if ($char eq '+') {
1642                         $diff_class = " add";
1643                 } elsif ($char eq '-') {
1644                         $diff_class = " rem";
1645                 } elsif ($char eq '@') {
1646                         $diff_class = " chunk_header";
1647                 } elsif ($char eq "\\") {
1648                         $diff_class = " incomplete";
1649                 }
1650         }
1651         $line = untabify($line);
1652         if ($from && $to && $line =~ m/^\@{2} /) {
1653                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1654                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1656                 $from_lines = 0 unless defined $from_lines;
1657                 $to_lines   = 0 unless defined $to_lines;
1659                 if ($from->{'href'}) {
1660                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1661                                              -class=>"list"}, $from_text);
1662                 }
1663                 if ($to->{'href'}) {
1664                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1665                                              -class=>"list"}, $to_text);
1666                 }
1667                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1668                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1669                 return "<div class=\"diff$diff_class\">$line</div>\n";
1670         } elsif ($from && $to && $line =~ m/^\@{3}/) {
1671                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1672                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1674                 @from_text = split(' ', $ranges);
1675                 for (my $i = 0; $i < @from_text; ++$i) {
1676                         ($from_start[$i], $from_nlines[$i]) =
1677                                 (split(',', substr($from_text[$i], 1)), 0);
1678                 }
1680                 $to_text   = pop @from_text;
1681                 $to_start  = pop @from_start;
1682                 $to_nlines = pop @from_nlines;
1684                 $line = "<span class=\"chunk_info\">$prefix ";
1685                 for (my $i = 0; $i < @from_text; ++$i) {
1686                         if ($from->{'href'}[$i]) {
1687                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1688                                                   -class=>"list"}, $from_text[$i]);
1689                         } else {
1690                                 $line .= $from_text[$i];
1691                         }
1692                         $line .= " ";
1693                 }
1694                 if ($to->{'href'}) {
1695                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1696                                           -class=>"list"}, $to_text);
1697                 } else {
1698                         $line .= $to_text;
1699                 }
1700                 $line .= " $prefix</span>" .
1701                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1702                 return "<div class=\"diff$diff_class\">$line</div>\n";
1703         }
1704         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1707 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1708 # linked.  Pass the hash of the tree/commit to snapshot.
1709 sub format_snapshot_links {
1710         my ($hash) = @_;
1711         my $num_fmts = @snapshot_fmts;
1712         if ($num_fmts > 1) {
1713                 # A parenthesized list of links bearing format names.
1714                 # e.g. "snapshot (_tar.gz_ _zip_)"
1715                 return "snapshot (" . join(' ', map
1716                         $cgi->a({
1717                                 -href => href(
1718                                         action=>"snapshot",
1719                                         hash=>$hash,
1720                                         snapshot_format=>$_
1721                                 )
1722                         }, $known_snapshot_formats{$_}{'display'})
1723                 , @snapshot_fmts) . ")";
1724         } elsif ($num_fmts == 1) {
1725                 # A single "snapshot" link whose tooltip bears the format name.
1726                 # i.e. "_snapshot_"
1727                 my ($fmt) = @snapshot_fmts;
1728                 return
1729                         $cgi->a({
1730                                 -href => href(
1731                                         action=>"snapshot",
1732                                         hash=>$hash,
1733                                         snapshot_format=>$fmt
1734                                 ),
1735                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1736                         }, "snapshot");
1737         } else { # $num_fmts == 0
1738                 return undef;
1739         }
1742 ## ......................................................................
1743 ## functions returning values to be passed, perhaps after some
1744 ## transformation, to other functions; e.g. returning arguments to href()
1746 # returns hash to be passed to href to generate gitweb URL
1747 # in -title key it returns description of link
1748 sub get_feed_info {
1749         my $format = shift || 'Atom';
1750         my %res = (action => lc($format));
1752         # feed links are possible only for project views
1753         return unless (defined $project);
1754         # some views should link to OPML, or to generic project feed,
1755         # or don't have specific feed yet (so they should use generic)
1756         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1758         my $branch;
1759         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1760         # from tag links; this also makes possible to detect branch links
1761         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1762             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
1763                 $branch = $1;
1764         }
1765         # find log type for feed description (title)
1766         my $type = 'log';
1767         if (defined $file_name) {
1768                 $type  = "history of $file_name";
1769                 $type .= "/" if ($action eq 'tree');
1770                 $type .= " on '$branch'" if (defined $branch);
1771         } else {
1772                 $type = "log of $branch" if (defined $branch);
1773         }
1775         $res{-title} = $type;
1776         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1777         $res{'file_name'} = $file_name;
1779         return %res;
1782 ## ----------------------------------------------------------------------
1783 ## git utility subroutines, invoking git commands
1785 # returns path to the core git executable and the --git-dir parameter as list
1786 sub git_cmd {
1787         return $GIT, '--git-dir='.$git_dir;
1790 # quote the given arguments for passing them to the shell
1791 # quote_command("command", "arg 1", "arg with ' and ! characters")
1792 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1793 # Try to avoid using this function wherever possible.
1794 sub quote_command {
1795         return join(' ',
1796                     map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1799 # get HEAD ref of given project as hash
1800 sub git_get_head_hash {
1801         my $project = shift;
1802         my $o_git_dir = $git_dir;
1803         my $retval = undef;
1804         $git_dir = "$projectroot/$project";
1805         if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1806                 my $head = <$fd>;
1807                 close $fd;
1808                 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1809                         $retval = $1;
1810                 }
1811         }
1812         if (defined $o_git_dir) {
1813                 $git_dir = $o_git_dir;
1814         }
1815         return $retval;
1818 # get type of given object
1819 sub git_get_type {
1820         my $hash = shift;
1822         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1823         my $type = <$fd>;
1824         close $fd or return;
1825         chomp $type;
1826         return $type;
1829 # repository configuration
1830 our $config_file = '';
1831 our %config;
1833 # store multiple values for single key as anonymous array reference
1834 # single values stored directly in the hash, not as [ <value> ]
1835 sub hash_set_multi {
1836         my ($hash, $key, $value) = @_;
1838         if (!exists $hash->{$key}) {
1839                 $hash->{$key} = $value;
1840         } elsif (!ref $hash->{$key}) {
1841                 $hash->{$key} = [ $hash->{$key}, $value ];
1842         } else {
1843                 push @{$hash->{$key}}, $value;
1844         }
1847 # return hash of git project configuration
1848 # optionally limited to some section, e.g. 'gitweb'
1849 sub git_parse_project_config {
1850         my $section_regexp = shift;
1851         my %config;
1853         local $/ = "\0";
1855         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1856                 or return;
1858         while (my $keyval = <$fh>) {
1859                 chomp $keyval;
1860                 my ($key, $value) = split(/\n/, $keyval, 2);
1862                 hash_set_multi(\%config, $key, $value)
1863                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1864         }
1865         close $fh;
1867         return %config;
1870 # convert config value to boolean, 'true' or 'false'
1871 # no value, number > 0, 'true' and 'yes' values are true
1872 # rest of values are treated as false (never as error)
1873 sub config_to_bool {
1874         my $val = shift;
1876         # strip leading and trailing whitespace
1877         $val =~ s/^\s+//;
1878         $val =~ s/\s+$//;
1880         return (!defined $val ||               # section.key
1881                 ($val =~ /^\d+$/ && $val) ||   # section.key = 1
1882                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
1885 # convert config value to simple decimal number
1886 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1887 # to be multiplied by 1024, 1048576, or 1073741824
1888 sub config_to_int {
1889         my $val = shift;
1891         # strip leading and trailing whitespace
1892         $val =~ s/^\s+//;
1893         $val =~ s/\s+$//;
1895         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1896                 $unit = lc($unit);
1897                 # unknown unit is treated as 1
1898                 return $num * ($unit eq 'g' ? 1073741824 :
1899                                $unit eq 'm' ?    1048576 :
1900                                $unit eq 'k' ?       1024 : 1);
1901         }
1902         return $val;
1905 # convert config value to array reference, if needed
1906 sub config_to_multi {
1907         my $val = shift;
1909         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1912 sub git_get_project_config {
1913         my ($key, $type) = @_;
1915         # key sanity check
1916         return unless ($key);
1917         $key =~ s/^gitweb\.//;
1918         return if ($key =~ m/\W/);
1920         # type sanity check
1921         if (defined $type) {
1922                 $type =~ s/^--//;
1923                 $type = undef
1924                         unless ($type eq 'bool' || $type eq 'int');
1925         }
1927         # get config
1928         if (!defined $config_file ||
1929             $config_file ne "$git_dir/config") {
1930                 %config = git_parse_project_config('gitweb');
1931                 $config_file = "$git_dir/config";
1932         }
1934         # ensure given type
1935         if (!defined $type) {
1936                 return $config{"gitweb.$key"};
1937         } elsif ($type eq 'bool') {
1938                 # backward compatibility: 'git config --bool' returns true/false
1939                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1940         } elsif ($type eq 'int') {
1941                 return config_to_int($config{"gitweb.$key"});
1942         }
1943         return $config{"gitweb.$key"};
1946 # get hash of given path at given ref
1947 sub git_get_hash_by_path {
1948         my $base = shift;
1949         my $path = shift || return undef;
1950         my $type = shift;
1952         $path =~ s,/+$,,;
1954         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1955                 or die_error(500, "Open git-ls-tree failed");
1956         my $line = <$fd>;
1957         close $fd or return undef;
1959         if (!defined $line) {
1960                 # there is no tree or hash given by $path at $base
1961                 return undef;
1962         }
1964         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
1965         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1966         if (defined $type && $type ne $2) {
1967                 # type doesn't match
1968                 return undef;
1969         }
1970         return $3;
1973 # get path of entry with given hash at given tree-ish (ref)
1974 # used to get 'from' filename for combined diff (merge commit) for renames
1975 sub git_get_path_by_hash {
1976         my $base = shift || return;
1977         my $hash = shift || return;
1979         local $/ = "\0";
1981         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1982                 or return undef;
1983         while (my $line = <$fd>) {
1984                 chomp $line;
1986                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
1987                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
1988                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1989                         close $fd;
1990                         return $1;
1991                 }
1992         }
1993         close $fd;
1994         return undef;
1997 ## ......................................................................
1998 ## git utility functions, directly accessing git repository
2000 sub git_get_project_description {
2001         my $path = shift;
2003         $git_dir = "$projectroot/$path";
2004         open my $fd, "$git_dir/description"
2005                 or return git_get_project_config('description');
2006         my $descr = <$fd>;
2007         close $fd;
2008         if (defined $descr) {
2009                 chomp $descr;
2010         }
2011         return $descr;
2014 sub git_get_project_ctags {
2015         my $path = shift;
2016         my $ctags = {};
2018         $git_dir = "$projectroot/$path";
2019         unless (opendir D, "$git_dir/ctags") {
2020                 return $ctags;
2021         }
2022         foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
2023                 open CT, $_ or next;
2024                 my $val = <CT>;
2025                 chomp $val;
2026                 close CT;
2027                 my $ctag = $_; $ctag =~ s#.*/##;
2028                 $ctags->{$ctag} = $val;
2029         }
2030         closedir D;
2031         $ctags;
2034 sub git_populate_project_tagcloud {
2035         my $ctags = shift;
2037         # First, merge different-cased tags; tags vote on casing
2038         my %ctags_lc;
2039         foreach (keys %$ctags) {
2040                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2041                 if (not $ctags_lc{lc $_}->{topcount}
2042                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2043                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2044                         $ctags_lc{lc $_}->{topname} = $_;
2045                 }
2046         }
2048         my $cloud;
2049         if (eval { require HTML::TagCloud; 1; }) {
2050                 $cloud = HTML::TagCloud->new;
2051                 foreach (sort keys %ctags_lc) {
2052                         # Pad the title with spaces so that the cloud looks
2053                         # less crammed.
2054                         my $title = $ctags_lc{$_}->{topname};
2055                         $title =~ s/ /&nbsp;/g;
2056                         $title =~ s/^/&nbsp;/g;
2057                         $title =~ s/$/&nbsp;/g;
2058                         $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2059                 }
2060         } else {
2061                 $cloud = \%ctags_lc;
2062         }
2063         $cloud;
2066 sub git_show_project_tagcloud {
2067         my ($cloud, $count) = @_;
2068         print STDERR ref($cloud)."..\n";
2069         if (ref $cloud eq 'HTML::TagCloud') {
2070                 return $cloud->html_and_css($count);
2071         } else {
2072                 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2073                 return '<p align="center">' . join (', ', map {
2074                         "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2075                 } splice(@tags, 0, $count)) . '</p>';
2076         }
2079 sub git_get_project_url_list {
2080         my $path = shift;
2082         $git_dir = "$projectroot/$path";
2083         open my $fd, "$git_dir/cloneurl"
2084                 or return wantarray ?
2085                 @{ config_to_multi(git_get_project_config('url')) } :
2086                    config_to_multi(git_get_project_config('url'));
2087         my @git_project_url_list = map { chomp; $_ } <$fd>;
2088         close $fd;
2090         return wantarray ? @git_project_url_list : \@git_project_url_list;
2093 sub git_get_projects_list {
2094         my ($filter) = @_;
2095         my @list;
2097         $filter ||= '';
2098         $filter =~ s/\.git$//;
2100         my $check_forks = gitweb_check_feature('forks');
2102         if (-d $projects_list) {
2103                 # search in directory
2104                 my $dir = $projects_list . ($filter ? "/$filter" : '');
2105                 # remove the trailing "/"
2106                 $dir =~ s!/+$!!;
2107                 my $pfxlen = length("$dir");
2108                 my $pfxdepth = ($dir =~ tr!/!!);
2110                 File::Find::find({
2111                         follow_fast => 1, # follow symbolic links
2112                         follow_skip => 2, # ignore duplicates
2113                         dangling_symlinks => 0, # ignore dangling symlinks, silently
2114                         wanted => sub {
2115                                 # skip project-list toplevel, if we get it.
2116                                 return if (m!^[/.]$!);
2117                                 # only directories can be git repositories
2118                                 return unless (-d $_);
2119                                 # don't traverse too deep (Find is super slow on os x)
2120                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2121                                         $File::Find::prune = 1;
2122                                         return;
2123                                 }
2125                                 my $subdir = substr($File::Find::name, $pfxlen + 1);
2126                                 # we check related file in $projectroot
2127                                 my $path = ($filter ? "$filter/" : '') . $subdir;
2128                                 if (check_export_ok("$projectroot/$path")) {
2129                                         push @list, { path => $path };
2130                                         $File::Find::prune = 1;
2131                                 }
2132                         },
2133                 }, "$dir");
2135         } elsif (-f $projects_list) {
2136                 # read from file(url-encoded):
2137                 # 'git%2Fgit.git Linus+Torvalds'
2138                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2139                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2140                 my %paths;
2141                 open my ($fd), $projects_list or return;
2142         PROJECT:
2143                 while (my $line = <$fd>) {
2144                         chomp $line;
2145                         my ($path, $owner) = split ' ', $line;
2146                         $path = unescape($path);
2147                         $owner = unescape($owner);
2148                         if (!defined $path) {
2149                                 next;
2150                         }
2151                         if ($filter ne '') {
2152                                 # looking for forks;
2153                                 my $pfx = substr($path, 0, length($filter));
2154                                 if ($pfx ne $filter) {
2155                                         next PROJECT;
2156                                 }
2157                                 my $sfx = substr($path, length($filter));
2158                                 if ($sfx !~ /^\/.*\.git$/) {
2159                                         next PROJECT;
2160                                 }
2161                         } elsif ($check_forks) {
2162                         PATH:
2163                                 foreach my $filter (keys %paths) {
2164                                         # looking for forks;
2165                                         my $pfx = substr($path, 0, length($filter));
2166                                         if ($pfx ne $filter) {
2167                                                 next PATH;
2168                                         }
2169                                         my $sfx = substr($path, length($filter));
2170                                         if ($sfx !~ /^\/.*\.git$/) {
2171                                                 next PATH;
2172                                         }
2173                                         # is a fork, don't include it in
2174                                         # the list
2175                                         next PROJECT;
2176                                 }
2177                         }
2178                         if (check_export_ok("$projectroot/$path")) {
2179                                 my $pr = {
2180                                         path => $path,
2181                                         owner => to_utf8($owner),
2182                                 };
2183                                 push @list, $pr;
2184                                 (my $forks_path = $path) =~ s/\.git$//;
2185                                 $paths{$forks_path}++;
2186                         }
2187                 }
2188                 close $fd;
2189         }
2190         return @list;
2193 our $gitweb_project_owner = undef;
2194 sub git_get_project_list_from_file {
2196         return if (defined $gitweb_project_owner);
2198         $gitweb_project_owner = {};
2199         # read from file (url-encoded):
2200         # 'git%2Fgit.git Linus+Torvalds'
2201         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2202         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2203         if (-f $projects_list) {
2204                 open (my $fd , $projects_list);
2205                 while (my $line = <$fd>) {
2206                         chomp $line;
2207                         my ($pr, $ow) = split ' ', $line;
2208                         $pr = unescape($pr);
2209                         $ow = unescape($ow);
2210                         $gitweb_project_owner->{$pr} = to_utf8($ow);
2211                 }
2212                 close $fd;
2213         }
2216 sub git_get_project_owner {
2217         my $project = shift;
2218         my $owner;
2220         return undef unless $project;
2221         $git_dir = "$projectroot/$project";
2223         if (!defined $gitweb_project_owner) {
2224                 git_get_project_list_from_file();
2225         }
2227         if (exists $gitweb_project_owner->{$project}) {
2228                 $owner = $gitweb_project_owner->{$project};
2229         }
2230         if (!defined $owner){
2231                 $owner = git_get_project_config('owner');
2232         }
2233         if (!defined $owner) {
2234                 $owner = get_file_owner("$git_dir");
2235         }
2237         return $owner;
2240 sub git_get_last_activity {
2241         my ($path) = @_;
2242         my $fd;
2244         $git_dir = "$projectroot/$path";
2245         open($fd, "-|", git_cmd(), 'for-each-ref',
2246              '--format=%(committer)',
2247              '--sort=-committerdate',
2248              '--count=1',
2249              'refs/heads') or return;
2250         my $most_recent = <$fd>;
2251         close $fd or return;
2252         if (defined $most_recent &&
2253             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2254                 my $timestamp = $1;
2255                 my $age = time - $timestamp;
2256                 return ($age, age_string($age));
2257         }
2258         return (undef, undef);
2261 sub git_get_references {
2262         my $type = shift || "";
2263         my %refs;
2264         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2265         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2266         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2267                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2268                 or return;
2270         while (my $line = <$fd>) {
2271                 chomp $line;
2272                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2273                         if (defined $refs{$1}) {
2274                                 push @{$refs{$1}}, $2;
2275                         } else {
2276                                 $refs{$1} = [ $2 ];
2277                         }
2278                 }
2279         }
2280         close $fd or return;
2281         return \%refs;
2284 sub git_get_rev_name_tags {
2285         my $hash = shift || return undef;
2287         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2288                 or return;
2289         my $name_rev = <$fd>;
2290         close $fd;
2292         if ($name_rev =~ m|^$hash tags/(.*)$|) {
2293                 return $1;
2294         } else {
2295                 # catches also '$hash undefined' output
2296                 return undef;
2297         }
2300 ## ----------------------------------------------------------------------
2301 ## parse to hash functions
2303 sub parse_date {
2304         my $epoch = shift;
2305         my $tz = shift || "-0000";
2307         my %date;
2308         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2309         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2310         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2311         $date{'hour'} = $hour;
2312         $date{'minute'} = $min;
2313         $date{'mday'} = $mday;
2314         $date{'day'} = $days[$wday];
2315         $date{'month'} = $months[$mon];
2316         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2317                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2318         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2319                              $mday, $months[$mon], $hour ,$min;
2320         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2321                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2323         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2324         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2325         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2326         $date{'hour_local'} = $hour;
2327         $date{'minute_local'} = $min;
2328         $date{'tz_local'} = $tz;
2329         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2330                                   1900+$year, $mon+1, $mday,
2331                                   $hour, $min, $sec, $tz);
2332         return %date;
2335 sub parse_tag {
2336         my $tag_id = shift;
2337         my %tag;
2338         my @comment;
2340         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2341         $tag{'id'} = $tag_id;
2342         while (my $line = <$fd>) {
2343                 chomp $line;
2344                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2345                         $tag{'object'} = $1;
2346                 } elsif ($line =~ m/^type (.+)$/) {
2347                         $tag{'type'} = $1;
2348                 } elsif ($line =~ m/^tag (.+)$/) {
2349                         $tag{'name'} = $1;
2350                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2351                         $tag{'author'} = $1;
2352                         $tag{'epoch'} = $2;
2353                         $tag{'tz'} = $3;
2354                 } elsif ($line =~ m/--BEGIN/) {
2355                         push @comment, $line;
2356                         last;
2357                 } elsif ($line eq "") {
2358                         last;
2359                 }
2360         }
2361         push @comment, <$fd>;
2362         $tag{'comment'} = \@comment;
2363         close $fd or return;
2364         if (!defined $tag{'name'}) {
2365                 return
2366         };
2367         return %tag
2370 sub parse_commit_text {
2371         my ($commit_text, $withparents) = @_;
2372         my @commit_lines = split '\n', $commit_text;
2373         my %co;
2375         pop @commit_lines; # Remove '\0'
2377         if (! @commit_lines) {
2378                 return;
2379         }
2381         my $header = shift @commit_lines;
2382         if ($header !~ m/^[0-9a-fA-F]{40}/) {
2383                 return;
2384         }
2385         ($co{'id'}, my @parents) = split ' ', $header;
2386         while (my $line = shift @commit_lines) {
2387                 last if $line eq "\n";
2388                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2389                         $co{'tree'} = $1;
2390                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2391                         push @parents, $1;
2392                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2393                         $co{'author'} = $1;
2394                         $co{'author_epoch'} = $2;
2395                         $co{'author_tz'} = $3;
2396                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2397                                 $co{'author_name'}  = $1;
2398                                 $co{'author_email'} = $2;
2399                         } else {
2400                                 $co{'author_name'} = $co{'author'};
2401                         }
2402                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2403                         $co{'committer'} = $1;
2404                         $co{'committer_epoch'} = $2;
2405                         $co{'committer_tz'} = $3;
2406                         $co{'committer_name'} = $co{'committer'};
2407                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2408                                 $co{'committer_name'}  = $1;
2409                                 $co{'committer_email'} = $2;
2410                         } else {
2411                                 $co{'committer_name'} = $co{'committer'};
2412                         }
2413                 }
2414         }
2415         if (!defined $co{'tree'}) {
2416                 return;
2417         };
2418         $co{'parents'} = \@parents;
2419         $co{'parent'} = $parents[0];
2421         foreach my $title (@commit_lines) {
2422                 $title =~ s/^    //;
2423                 if ($title ne "") {
2424                         $co{'title'} = chop_str($title, 80, 5);
2425                         # remove leading stuff of merges to make the interesting part visible
2426                         if (length($title) > 50) {
2427                                 $title =~ s/^Automatic //;
2428                                 $title =~ s/^merge (of|with) /Merge ... /i;
2429                                 if (length($title) > 50) {
2430                                         $title =~ s/(http|rsync):\/\///;
2431                                 }
2432                                 if (length($title) > 50) {
2433                                         $title =~ s/(master|www|rsync)\.//;
2434                                 }
2435                                 if (length($title) > 50) {
2436                                         $title =~ s/kernel.org:?//;
2437                                 }
2438                                 if (length($title) > 50) {
2439                                         $title =~ s/\/pub\/scm//;
2440                                 }
2441                         }
2442                         $co{'title_short'} = chop_str($title, 50, 5);
2443                         last;
2444                 }
2445         }
2446         if (! defined $co{'title'} || $co{'title'} eq "") {
2447                 $co{'title'} = $co{'title_short'} = '(no commit message)';
2448         }
2449         # remove added spaces
2450         foreach my $line (@commit_lines) {
2451                 $line =~ s/^    //;
2452         }
2453         $co{'comment'} = \@commit_lines;
2455         my $age = time - $co{'committer_epoch'};
2456         $co{'age'} = $age;
2457         $co{'age_string'} = age_string($age);
2458         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2459         if ($age > 60*60*24*7*2) {
2460                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2461                 $co{'age_string_age'} = $co{'age_string'};
2462         } else {
2463                 $co{'age_string_date'} = $co{'age_string'};
2464                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2465         }
2466         return %co;
2469 sub parse_commit {
2470         my ($commit_id) = @_;
2471         my %co;
2473         local $/ = "\0";
2475         open my $fd, "-|", git_cmd(), "rev-list",
2476                 "--parents",
2477                 "--header",
2478                 "--max-count=1",
2479                 $commit_id,
2480                 "--",
2481                 or die_error(500, "Open git-rev-list failed");
2482         %co = parse_commit_text(<$fd>, 1);
2483         close $fd;
2485         return %co;
2488 sub parse_commits {
2489         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2490         my @cos;
2492         $maxcount ||= 1;
2493         $skip ||= 0;
2495         local $/ = "\0";
2497         open my $fd, "-|", git_cmd(), "rev-list",
2498                 "--header",
2499                 @args,
2500                 ("--max-count=" . $maxcount),
2501                 ("--skip=" . $skip),
2502                 @extra_options,
2503                 $commit_id,
2504                 "--",
2505                 ($filename ? ($filename) : ())
2506                 or die_error(500, "Open git-rev-list failed");
2507         while (my $line = <$fd>) {
2508                 my %co = parse_commit_text($line);
2509                 push @cos, \%co;
2510         }
2511         close $fd;
2513         return wantarray ? @cos : \@cos;
2516 # parse line of git-diff-tree "raw" output
2517 sub parse_difftree_raw_line {
2518         my $line = shift;
2519         my %res;
2521         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2522         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2523         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2524                 $res{'from_mode'} = $1;
2525                 $res{'to_mode'} = $2;
2526                 $res{'from_id'} = $3;
2527                 $res{'to_id'} = $4;
2528                 $res{'status'} = $5;
2529                 $res{'similarity'} = $6;
2530                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2531                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2532                 } else {
2533                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2534                 }
2535         }
2536         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2537         # combined diff (for merge commit)
2538         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2539                 $res{'nparents'}  = length($1);
2540                 $res{'from_mode'} = [ split(' ', $2) ];
2541                 $res{'to_mode'} = pop @{$res{'from_mode'}};
2542                 $res{'from_id'} = [ split(' ', $3) ];
2543                 $res{'to_id'} = pop @{$res{'from_id'}};
2544                 $res{'status'} = [ split('', $4) ];
2545                 $res{'to_file'} = unquote($5);
2546         }
2547         # 'c512b523472485aef4fff9e57b229d9d243c967f'
2548         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2549                 $res{'commit'} = $1;
2550         }
2552         return wantarray ? %res : \%res;
2555 # wrapper: return parsed line of git-diff-tree "raw" output
2556 # (the argument might be raw line, or parsed info)
2557 sub parsed_difftree_line {
2558         my $line_or_ref = shift;
2560         if (ref($line_or_ref) eq "HASH") {
2561                 # pre-parsed (or generated by hand)
2562                 return $line_or_ref;
2563         } else {
2564                 return parse_difftree_raw_line($line_or_ref);
2565         }
2568 # parse line of git-ls-tree output
2569 sub parse_ls_tree_line ($;%) {
2570         my $line = shift;
2571         my %opts = @_;
2572         my %res;
2574         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2575         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2577         $res{'mode'} = $1;
2578         $res{'type'} = $2;
2579         $res{'hash'} = $3;
2580         if ($opts{'-z'}) {
2581                 $res{'name'} = $4;
2582         } else {
2583                 $res{'name'} = unquote($4);
2584         }
2586         return wantarray ? %res : \%res;
2589 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2590 sub parse_from_to_diffinfo {
2591         my ($diffinfo, $from, $to, @parents) = @_;
2593         if ($diffinfo->{'nparents'}) {
2594                 # combined diff
2595                 $from->{'file'} = [];
2596                 $from->{'href'} = [];
2597                 fill_from_file_info($diffinfo, @parents)
2598                         unless exists $diffinfo->{'from_file'};
2599                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2600                         $from->{'file'}[$i] =
2601                                 defined $diffinfo->{'from_file'}[$i] ?
2602                                         $diffinfo->{'from_file'}[$i] :
2603                                         $diffinfo->{'to_file'};
2604                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2605                                 $from->{'href'}[$i] = href(action=>"blob",
2606                                                            hash_base=>$parents[$i],
2607                                                            hash=>$diffinfo->{'from_id'}[$i],
2608                                                            file_name=>$from->{'file'}[$i]);
2609                         } else {
2610                                 $from->{'href'}[$i] = undef;
2611                         }
2612                 }
2613         } else {
2614                 # ordinary (not combined) diff
2615                 $from->{'file'} = $diffinfo->{'from_file'};
2616                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2617                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2618                                                hash=>$diffinfo->{'from_id'},
2619                                                file_name=>$from->{'file'});
2620                 } else {
2621                         delete $from->{'href'};
2622                 }
2623         }
2625         $to->{'file'} = $diffinfo->{'to_file'};
2626         if (!is_deleted($diffinfo)) { # file exists in result
2627                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2628                                      hash=>$diffinfo->{'to_id'},
2629                                      file_name=>$to->{'file'});
2630         } else {
2631                 delete $to->{'href'};
2632         }
2635 ## ......................................................................
2636 ## parse to array of hashes functions
2638 sub git_get_heads_list {
2639         my $limit = shift;
2640         my @headslist;
2642         open my $fd, '-|', git_cmd(), 'for-each-ref',
2643                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2644                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2645                 'refs/heads'
2646                 or return;
2647         while (my $line = <$fd>) {
2648                 my %ref_item;
2650                 chomp $line;
2651                 my ($refinfo, $committerinfo) = split(/\0/, $line);
2652                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2653                 my ($committer, $epoch, $tz) =
2654                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2655                 $ref_item{'fullname'}  = $name;
2656                 $name =~ s!^refs/heads/!!;
2658                 $ref_item{'name'}  = $name;
2659                 $ref_item{'id'}    = $hash;
2660                 $ref_item{'title'} = $title || '(no commit message)';
2661                 $ref_item{'epoch'} = $epoch;
2662                 if ($epoch) {
2663                         $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2664                 } else {
2665                         $ref_item{'age'} = "unknown";
2666                 }
2668                 push @headslist, \%ref_item;
2669         }
2670         close $fd;
2672         return wantarray ? @headslist : \@headslist;
2675 sub git_get_tags_list {
2676         my $limit = shift;
2677         my @tagslist;
2679         open my $fd, '-|', git_cmd(), 'for-each-ref',
2680                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2681                 '--format=%(objectname) %(objecttype) %(refname) '.
2682                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2683                 'refs/tags'
2684                 or return;
2685         while (my $line = <$fd>) {
2686                 my %ref_item;
2688                 chomp $line;
2689                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2690                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2691                 my ($creator, $epoch, $tz) =
2692                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2693                 $ref_item{'fullname'} = $name;
2694                 $name =~ s!^refs/tags/!!;
2696                 $ref_item{'type'} = $type;
2697                 $ref_item{'id'} = $id;
2698                 $ref_item{'name'} = $name;
2699                 if ($type eq "tag") {
2700                         $ref_item{'subject'} = $title;
2701                         $ref_item{'reftype'} = $reftype;
2702                         $ref_item{'refid'}   = $refid;
2703                 } else {
2704                         $ref_item{'reftype'} = $type;
2705                         $ref_item{'refid'}   = $id;
2706                 }
2708                 if ($type eq "tag" || $type eq "commit") {
2709                         $ref_item{'epoch'} = $epoch;
2710                         if ($epoch) {
2711                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2712                         } else {
2713                                 $ref_item{'age'} = "unknown";
2714                         }
2715                 }
2717                 push @tagslist, \%ref_item;
2718         }
2719         close $fd;
2721         return wantarray ? @tagslist : \@tagslist;
2724 ## ----------------------------------------------------------------------
2725 ## filesystem-related functions
2727 sub get_file_owner {
2728         my $path = shift;
2730         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2731         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2732         if (!defined $gcos) {
2733                 return undef;
2734         }
2735         my $owner = $gcos;
2736         $owner =~ s/[,;].*$//;
2737         return to_utf8($owner);
2740 # assume that file exists
2741 sub insert_file {
2742         my $filename = shift;
2744         open my $fd, '<', $filename;
2745         print map { to_utf8($_) } <$fd>;
2746         close $fd;
2749 ## ......................................................................
2750 ## mimetype related functions
2752 sub mimetype_guess_file {
2753         my $filename = shift;
2754         my $mimemap = shift;
2755         -r $mimemap or return undef;
2757         my %mimemap;
2758         open(MIME, $mimemap) or return undef;
2759         while (<MIME>) {
2760                 next if m/^#/; # skip comments
2761                 my ($mime, $exts) = split(/\t+/);
2762                 if (defined $exts) {
2763                         my @exts = split(/\s+/, $exts);
2764                         foreach my $ext (@exts) {
2765                                 $mimemap{$ext} = $mime;
2766                         }
2767                 }
2768         }
2769         close(MIME);
2771         $filename =~ /\.([^.]*)$/;
2772         return $mimemap{$1};
2775 sub mimetype_guess {
2776         my $filename = shift;
2777         my $mime;
2778         $filename =~ /\./ or return undef;
2780         if ($mimetypes_file) {
2781                 my $file = $mimetypes_file;
2782                 if ($file !~ m!^/!) { # if it is relative path
2783                         # it is relative to project
2784                         $file = "$projectroot/$project/$file";
2785                 }
2786                 $mime = mimetype_guess_file($filename, $file);
2787         }
2788         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2789         return $mime;
2792 sub blob_mimetype {
2793         my $fd = shift;
2794         my $filename = shift;
2796         if ($filename) {
2797                 my $mime = mimetype_guess($filename);
2798                 $mime and return $mime;
2799         }
2801         # just in case
2802         return $default_blob_plain_mimetype unless $fd;
2804         if (-T $fd) {
2805                 return 'text/plain';
2806         } elsif (! $filename) {
2807                 return 'application/octet-stream';
2808         } elsif ($filename =~ m/\.png$/i) {
2809                 return 'image/png';
2810         } elsif ($filename =~ m/\.gif$/i) {
2811                 return 'image/gif';
2812         } elsif ($filename =~ m/\.jpe?g$/i) {
2813                 return 'image/jpeg';
2814         } else {
2815                 return 'application/octet-stream';
2816         }
2819 sub blob_contenttype {
2820         my ($fd, $file_name, $type) = @_;
2822         $type ||= blob_mimetype($fd, $file_name);
2823         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2824                 $type .= "; charset=$default_text_plain_charset";
2825         }
2827         return $type;
2830 ## ======================================================================
2831 ## functions printing HTML: header, footer, error page
2833 sub git_header_html {
2834         my $status = shift || "200 OK";
2835         my $expires = shift;
2837         my $title = "$site_name";
2838         if (defined $project) {
2839                 $title .= " - " . to_utf8($project);
2840                 if (defined $action) {
2841                         $title .= "/$action";
2842                         if (defined $file_name) {
2843                                 $title .= " - " . esc_path($file_name);
2844                                 if ($action eq "tree" && $file_name !~ m|/$|) {
2845                                         $title .= "/";
2846                                 }
2847                         }
2848                 }
2849         }
2850         my $content_type;
2851         # require explicit support from the UA if we are to send the page as
2852         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2853         # we have to do this because MSIE sometimes globs '*/*', pretending to
2854         # support xhtml+xml but choking when it gets what it asked for.
2855         if (defined $cgi->http('HTTP_ACCEPT') &&
2856             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2857             $cgi->Accept('application/xhtml+xml') != 0) {
2858                 $content_type = 'application/xhtml+xml';
2859         } else {
2860                 $content_type = 'text/html';
2861         }
2862         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2863                            -status=> $status, -expires => $expires);
2864         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2865         print <<EOF;
2866 <?xml version="1.0" encoding="utf-8"?>
2867 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2868 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2869 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2870 <!-- git core binaries version $git_version -->
2871 <head>
2872 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2873 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2874 <meta name="robots" content="index, nofollow"/>
2875 <title>$title</title>
2876 EOF
2877 # print out each stylesheet that exist
2878         if (defined $stylesheet) {
2879 #provides backwards capability for those people who define style sheet in a config file
2880                 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2881         } else {
2882                 foreach my $stylesheet (@stylesheets) {
2883                         next unless $stylesheet;
2884                         print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2885                 }
2886         }
2887         if (defined $project) {
2888                 my %href_params = get_feed_info();
2889                 if (!exists $href_params{'-title'}) {
2890                         $href_params{'-title'} = 'log';
2891                 }
2893                 foreach my $format qw(RSS Atom) {
2894                         my $type = lc($format);
2895                         my %link_attr = (
2896                                 '-rel' => 'alternate',
2897                                 '-title' => "$project - $href_params{'-title'} - $format feed",
2898                                 '-type' => "application/$type+xml"
2899                         );
2901                         $href_params{'action'} = $type;
2902                         $link_attr{'-href'} = href(%href_params);
2903                         print "<link ".
2904                               "rel=\"$link_attr{'-rel'}\" ".
2905                               "title=\"$link_attr{'-title'}\" ".
2906                               "href=\"$link_attr{'-href'}\" ".
2907                               "type=\"$link_attr{'-type'}\" ".
2908                               "/>\n";
2910                         $href_params{'extra_options'} = '--no-merges';
2911                         $link_attr{'-href'} = href(%href_params);
2912                         $link_attr{'-title'} .= ' (no merges)';
2913                         print "<link ".
2914                               "rel=\"$link_attr{'-rel'}\" ".
2915                               "title=\"$link_attr{'-title'}\" ".
2916                               "href=\"$link_attr{'-href'}\" ".
2917                               "type=\"$link_attr{'-type'}\" ".
2918                               "/>\n";
2919                 }
2921         } else {
2922                 printf('<link rel="alternate" title="%s projects list" '.
2923                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
2924                        $site_name, href(project=>undef, action=>"project_index"));
2925                 printf('<link rel="alternate" title="%s projects feeds" '.
2926                        'href="%s" type="text/x-opml" />'."\n",
2927                        $site_name, href(project=>undef, action=>"opml"));
2928         }
2929         if (defined $favicon) {
2930                 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2931         }
2933         print "</head>\n" .
2934               "<body>\n";
2936         if (-f $site_header) {
2937                 insert_file($site_header);
2938         }
2940         print "<div class=\"page_header\">\n" .
2941               $cgi->a({-href => esc_url($logo_url),
2942                        -title => $logo_label},
2943                       qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2944         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2945         if (defined $project) {
2946                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2947                 if (defined $action) {
2948                         print " / $action";
2949                 }
2950                 print "\n";
2951         }
2952         print "</div>\n";
2954         my $have_search = gitweb_check_feature('search');
2955         if (defined $project && $have_search) {
2956                 if (!defined $searchtext) {
2957                         $searchtext = "";
2958                 }
2959                 my $search_hash;
2960                 if (defined $hash_base) {
2961                         $search_hash = $hash_base;
2962                 } elsif (defined $hash) {
2963                         $search_hash = $hash;
2964                 } else {
2965                         $search_hash = "HEAD";
2966                 }
2967                 my $action = $my_uri;
2968                 my $use_pathinfo = gitweb_check_feature('pathinfo');
2969                 if ($use_pathinfo) {
2970                         $action .= "/".esc_url($project);
2971                 }
2972                 print $cgi->startform(-method => "get", -action => $action) .
2973                       "<div class=\"search\">\n" .
2974                       (!$use_pathinfo &&
2975                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2976                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2977                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2978                       $cgi->popup_menu(-name => 'st', -default => 'commit',
2979                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2980                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2981                       " search:\n",
2982                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2983                       "<span title=\"Extended regular expression\">" .
2984                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2985                                      -checked => $search_use_regexp) .
2986                       "</span>" .
2987                       "</div>" .
2988                       $cgi->end_form() . "\n";
2989         }
2992 sub git_footer_html {
2993         my $feed_class = 'rss_logo';
2995         print "<div class=\"page_footer\">\n";
2996         if (defined $project) {
2997                 my $descr = git_get_project_description($project);
2998                 if (defined $descr) {
2999                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3000                 }
3002                 my %href_params = get_feed_info();
3003                 if (!%href_params) {
3004                         $feed_class .= ' generic';
3005                 }
3006                 $href_params{'-title'} ||= 'log';
3008                 foreach my $format qw(RSS Atom) {
3009                         $href_params{'action'} = lc($format);
3010                         print $cgi->a({-href => href(%href_params),
3011                                       -title => "$href_params{'-title'} $format feed",
3012                                       -class => $feed_class}, $format)."\n";
3013                 }
3015         } else {
3016                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3017                               -class => $feed_class}, "OPML") . " ";
3018                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3019                               -class => $feed_class}, "TXT") . "\n";
3020         }
3021         print "</div>\n"; # class="page_footer"
3023         if (-f $site_footer) {
3024                 insert_file($site_footer);
3025         }
3027         print "</body>\n" .
3028               "</html>";
3031 # die_error(<http_status_code>, <error_message>)
3032 # Example: die_error(404, 'Hash not found')
3033 # By convention, use the following status codes (as defined in RFC 2616):
3034 # 400: Invalid or missing CGI parameters, or
3035 #      requested object exists but has wrong type.
3036 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3037 #      this server or project.
3038 # 404: Requested object/revision/project doesn't exist.
3039 # 500: The server isn't configured properly, or
3040 #      an internal error occurred (e.g. failed assertions caused by bugs), or
3041 #      an unknown error occurred (e.g. the git binary died unexpectedly).
3042 sub die_error {
3043         my $status = shift || 500;
3044         my $error = shift || "Internal server error";
3046         my %http_responses = (400 => '400 Bad Request',
3047                               403 => '403 Forbidden',
3048                               404 => '404 Not Found',
3049                               500 => '500 Internal Server Error');
3050         git_header_html($http_responses{$status});
3051         print <<EOF;
3052 <div class="page_body">
3053 <br /><br />
3054 $status - $error
3055 <br />
3056 </div>
3057 EOF
3058         git_footer_html();
3059         exit;
3062 ## ----------------------------------------------------------------------
3063 ## functions printing or outputting HTML: navigation
3065 sub git_print_page_nav {
3066         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3067         $extra = '' if !defined $extra; # pager or formats
3069         my @navs = qw(summary shortlog log commit commitdiff tree);
3070         if ($suppress) {
3071                 @navs = grep { $_ ne $suppress } @navs;
3072         }
3074         my %arg = map { $_ => {action=>$_} } @navs;
3075         if (defined $head) {
3076                 for (qw(commit commitdiff)) {
3077                         $arg{$_}{'hash'} = $head;
3078                 }
3079                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3080                         for (qw(shortlog log)) {
3081                                 $arg{$_}{'hash'} = $head;
3082                         }
3083                 }
3084         }
3086         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3087         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3089         my @actions = gitweb_get_feature('actions');
3090         my %repl = (
3091                 '%' => '%',
3092                 'n' => $project,         # project name
3093                 'f' => $git_dir,         # project path within filesystem
3094                 'h' => $treehead || '',  # current hash ('h' parameter)
3095                 'b' => $treebase || '',  # hash base ('hb' parameter)
3096         );
3097         while (@actions) {
3098                 my ($label, $link, $pos) = splice(@actions,0,3);
3099                 # insert
3100                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3101                 # munch munch
3102                 $link =~ s/%([%nfhb])/$repl{$1}/g;
3103                 $arg{$label}{'_href'} = $link;
3104         }
3106         print "<div class=\"page_nav\">\n" .
3107                 (join " | ",
3108                  map { $_ eq $current ?
3109                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3110                  } @navs);
3111         print "<br/>\n$extra<br/>\n" .
3112               "</div>\n";
3115 sub format_paging_nav {
3116         my ($action, $hash, $head, $page, $has_next_link) = @_;
3117         my $paging_nav;
3120         if ($hash ne $head || $page) {
3121                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3122         } else {
3123                 $paging_nav .= "HEAD";
3124         }
3126         if ($page > 0) {
3127                 $paging_nav .= " &sdot; " .
3128                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
3129                                  -accesskey => "p", -title => "Alt-p"}, "prev");
3130         } else {
3131                 $paging_nav .= " &sdot; prev";
3132         }
3134         if ($has_next_link) {
3135                 $paging_nav .= " &sdot; " .
3136                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
3137                                  -accesskey => "n", -title => "Alt-n"}, "next");
3138         } else {
3139                 $paging_nav .= " &sdot; next";
3140         }
3142         return $paging_nav;
3145 ## ......................................................................
3146 ## functions printing or outputting HTML: div
3148 sub git_print_header_div {
3149         my ($action, $title, $hash, $hash_base) = @_;
3150         my %args = ();
3152         $args{'action'} = $action;
3153         $args{'hash'} = $hash if $hash;
3154         $args{'hash_base'} = $hash_base if $hash_base;
3156         print "<div class=\"header\">\n" .
3157               $cgi->a({-href => href(%args), -class => "title"},
3158               $title ? $title : $action) .
3159               "\n</div>\n";
3162 #sub git_print_authorship (\%) {
3163 sub git_print_authorship {
3164         my $co = shift;
3166         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3167         print "<div class=\"author_date\">" .
3168               esc_html($co->{'author_name'}) .
3169               " [$ad{'rfc2822'}";
3170         if ($ad{'hour_local'} < 6) {
3171                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3172                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3173         } else {
3174                 printf(" (%02d:%02d %s)",
3175                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3176         }
3177         print "]</div>\n";
3180 sub git_print_page_path {
3181         my $name = shift;
3182         my $type = shift;
3183         my $hb = shift;
3186         print "<div class=\"page_path\">";
3187         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3188                       -title => 'tree root'}, to_utf8("[$project]"));
3189         print " / ";
3190         if (defined $name) {
3191                 my @dirname = split '/', $name;
3192                 my $basename = pop @dirname;
3193                 my $fullname = '';
3195                 foreach my $dir (@dirname) {
3196                         $fullname .= ($fullname ? '/' : '') . $dir;
3197                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3198                                                      hash_base=>$hb),
3199                                       -title => $fullname}, esc_path($dir));
3200                         print " / ";
3201                 }
3202                 if (defined $type && $type eq 'blob') {
3203                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3204                                                      hash_base=>$hb),
3205                                       -title => $name}, esc_path($basename));
3206                 } elsif (defined $type && $type eq 'tree') {
3207                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3208                                                      hash_base=>$hb),
3209                                       -title => $name}, esc_path($basename));
3210                         print " / ";
3211                 } else {
3212                         print esc_path($basename);
3213                 }
3214         }
3215         print "<br/></div>\n";
3218 # sub git_print_log (\@;%) {
3219 sub git_print_log ($;%) {
3220         my $log = shift;
3221         my %opts = @_;
3223         if ($opts{'-remove_title'}) {
3224                 # remove title, i.e. first line of log
3225                 shift @$log;
3226         }
3227         # remove leading empty lines
3228         while (defined $log->[0] && $log->[0] eq "") {
3229                 shift @$log;
3230         }
3232         # print log
3233         my $signoff = 0;
3234         my $empty = 0;
3235         foreach my $line (@$log) {
3236                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3237                         $signoff = 1;
3238                         $empty = 0;
3239                         if (! $opts{'-remove_signoff'}) {
3240                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3241                                 next;
3242                         } else {
3243                                 # remove signoff lines
3244                                 next;
3245                         }
3246                 } else {
3247                         $signoff = 0;
3248                 }
3250                 # print only one empty line
3251                 # do not print empty line after signoff
3252                 if ($line eq "") {
3253                         next if ($empty || $signoff);
3254                         $empty = 1;
3255                 } else {
3256                         $empty = 0;
3257                 }
3259                 print format_log_line_html($line) . "<br/>\n";
3260         }
3262         if ($opts{'-final_empty_line'}) {
3263                 # end with single empty line
3264                 print "<br/>\n" unless $empty;
3265         }
3268 # return link target (what link points to)
3269 sub git_get_link_target {
3270         my $hash = shift;
3271         my $link_target;
3273         # read link
3274         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3275                 or return;
3276         {
3277                 local $/;
3278                 $link_target = <$fd>;
3279         }
3280         close $fd
3281                 or return;
3283         return $link_target;
3286 # given link target, and the directory (basedir) the link is in,
3287 # return target of link relative to top directory (top tree);
3288 # return undef if it is not possible (including absolute links).
3289 sub normalize_link_target {
3290         my ($link_target, $basedir, $hash_base) = @_;
3292         # we can normalize symlink target only if $hash_base is provided
3293         return unless $hash_base;
3295         # absolute symlinks (beginning with '/') cannot be normalized
3296         return if (substr($link_target, 0, 1) eq '/');
3298         # normalize link target to path from top (root) tree (dir)
3299         my $path;
3300         if ($basedir) {
3301                 $path = $basedir . '/' . $link_target;
3302         } else {
3303                 # we are in top (root) tree (dir)
3304                 $path = $link_target;
3305         }
3307         # remove //, /./, and /../
3308         my @path_parts;
3309         foreach my $part (split('/', $path)) {
3310                 # discard '.' and ''
3311                 next if (!$part || $part eq '.');
3312                 # handle '..'
3313                 if ($part eq '..') {
3314                         if (@path_parts) {
3315                                 pop @path_parts;
3316                         } else {
3317                                 # link leads outside repository (outside top dir)
3318                                 return;
3319                         }
3320                 } else {
3321                         push @path_parts, $part;
3322                 }
3323         }
3324         $path = join('/', @path_parts);
3326         return $path;
3329 # print tree entry (row of git_tree), but without encompassing <tr> element
3330 sub git_print_tree_entry {
3331         my ($t, $basedir, $hash_base, $have_blame) = @_;
3333         my %base_key = ();
3334         $base_key{'hash_base'} = $hash_base if defined $hash_base;
3336         # The format of a table row is: mode list link.  Where mode is
3337         # the mode of the entry, list is the name of the entry, an href,
3338         # and link is the action links of the entry.
3340         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3341         if ($t->{'type'} eq "blob") {
3342                 print "<td class=\"list\">" .
3343                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3344                                                file_name=>"$basedir$t->{'name'}", %base_key),
3345                                 -class => "list"}, esc_path($t->{'name'}));
3346                 if (S_ISLNK(oct $t->{'mode'})) {
3347                         my $link_target = git_get_link_target($t->{'hash'});
3348                         if ($link_target) {
3349                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3350                                 if (defined $norm_target) {
3351                                         print " -> " .
3352                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3353                                                                      file_name=>$norm_target),
3354                                                        -title => $norm_target}, esc_path($link_target));
3355                                 } else {
3356                                         print " -> " . esc_path($link_target);
3357                                 }
3358                         }
3359                 }
3360                 print "</td>\n";
3361                 print "<td class=\"link\">";
3362                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3363                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3364                               "blob");
3365                 if ($have_blame) {
3366                         print " | " .
3367                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3368                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3369                                       "blame");
3370                 }
3371                 if (defined $hash_base) {
3372                         print " | " .
3373                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3374                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3375                                       "history");
3376                 }
3377                 print " | " .
3378                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3379                                                file_name=>"$basedir$t->{'name'}")},
3380                                 "raw");
3381                 print "</td>\n";
3383         } elsif ($t->{'type'} eq "tree") {
3384                 print "<td class=\"list\">";
3385                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3386                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3387                               esc_path($t->{'name'}));
3388                 print "</td>\n";
3389                 print "<td class=\"link\">";
3390                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3391                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3392                               "tree");
3393                 if (defined $hash_base) {
3394                         print " | " .
3395                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3396                                                      file_name=>"$basedir$t->{'name'}")},
3397                                       "history");
3398                 }
3399                 print "</td>\n";
3400         } else {
3401                 # unknown object: we can only present history for it
3402                 # (this includes 'commit' object, i.e. submodule support)
3403                 print "<td class=\"list\">" .
3404                       esc_path($t->{'name'}) .
3405                       "</td>\n";
3406                 print "<td class=\"link\">";
3407                 if (defined $hash_base) {
3408                         print $cgi->a({-href => href(action=>"history",
3409                                                      hash_base=>$hash_base,
3410                                                      file_name=>"$basedir$t->{'name'}")},
3411                                       "history");
3412                 }
3413                 print "</td>\n";
3414         }
3417 ## ......................................................................
3418 ## functions printing large fragments of HTML
3420 # get pre-image filenames for merge (combined) diff
3421 sub fill_from_file_info {
3422         my ($diff, @parents) = @_;
3424         $diff->{'from_file'} = [ ];
3425         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3426         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3427                 if ($diff->{'status'}[$i] eq 'R' ||
3428                     $diff->{'status'}[$i] eq 'C') {
3429                         $diff->{'from_file'}[$i] =
3430                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3431                 }
3432         }
3434         return $diff;
3437 # is current raw difftree line of file deletion
3438 sub is_deleted {
3439         my $diffinfo = shift;
3441         return $diffinfo->{'to_id'} eq ('0' x 40);
3444 # does patch correspond to [previous] difftree raw line
3445 # $diffinfo  - hashref of parsed raw diff format
3446 # $patchinfo - hashref of parsed patch diff format
3447 #              (the same keys as in $diffinfo)
3448 sub is_patch_split {
3449         my ($diffinfo, $patchinfo) = @_;
3451         return defined $diffinfo && defined $patchinfo
3452                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3456 sub git_difftree_body {
3457         my ($difftree, $hash, @parents) = @_;
3458         my ($parent) = $parents[0];
3459         my $have_blame = gitweb_check_feature('blame');
3460         print "<div class=\"list_head\">\n";
3461         if ($#{$difftree} > 10) {
3462                 print(($#{$difftree} + 1) . " files changed:\n");
3463         }
3464         print "</div>\n";
3466         print "<table class=\"" .
3467               (@parents > 1 ? "combined " : "") .
3468               "diff_tree\">\n";
3470         # header only for combined diff in 'commitdiff' view
3471         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3472         if ($has_header) {
3473                 # table header
3474                 print "<thead><tr>\n" .
3475                        "<th></th><th></th>\n"; # filename, patchN link
3476                 for (my $i = 0; $i < @parents; $i++) {
3477                         my $par = $parents[$i];
3478                         print "<th>" .
3479                               $cgi->a({-href => href(action=>"commitdiff",
3480                                                      hash=>$hash, hash_parent=>$par),
3481                                        -title => 'commitdiff to parent number ' .
3482                                                   ($i+1) . ': ' . substr($par,0,7)},
3483                                       $i+1) .
3484                               "&nbsp;</th>\n";
3485                 }
3486                 print "</tr></thead>\n<tbody>\n";
3487         }
3489         my $alternate = 1;
3490         my $patchno = 0;
3491         foreach my $line (@{$difftree}) {
3492                 my $diff = parsed_difftree_line($line);
3494                 if ($alternate) {
3495                         print "<tr class=\"dark\">\n";
3496                 } else {
3497                         print "<tr class=\"light\">\n";
3498                 }
3499                 $alternate ^= 1;
3501                 if (exists $diff->{'nparents'}) { # combined diff
3503                         fill_from_file_info($diff, @parents)
3504                                 unless exists $diff->{'from_file'};
3506                         if (!is_deleted($diff)) {
3507                                 # file exists in the result (child) commit
3508                                 print "<td>" .
3509                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3510                                                              file_name=>$diff->{'to_file'},
3511                                                              hash_base=>$hash),
3512                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3513                                       "</td>\n";
3514                         } else {
3515                                 print "<td>" .
3516                                       esc_path($diff->{'to_file'}) .
3517                                       "</td>\n";
3518                         }
3520                         if ($action eq 'commitdiff') {
3521                                 # link to patch
3522                                 $patchno++;
3523                                 print "<td class=\"link\">" .
3524                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3525                                       " | " .
3526                                       "</td>\n";
3527                         }
3529                         my $has_history = 0;
3530                         my $not_deleted = 0;
3531                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3532                                 my $hash_parent = $parents[$i];
3533                                 my $from_hash = $diff->{'from_id'}[$i];
3534                                 my $from_path = $diff->{'from_file'}[$i];
3535                                 my $status = $diff->{'status'}[$i];
3537                                 $has_history ||= ($status ne 'A');
3538                                 $not_deleted ||= ($status ne 'D');
3540                                 if ($status eq 'A') {
3541                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3542                                 } elsif ($status eq 'D') {
3543                                         print "<td class=\"link\">" .
3544                                               $cgi->a({-href => href(action=>"blob",
3545                                                                      hash_base=>$hash,
3546                                                                      hash=>$from_hash,
3547                                                                      file_name=>$from_path)},
3548                                                       "blob" . ($i+1)) .
3549                                               " | </td>\n";
3550                                 } else {
3551                                         if ($diff->{'to_id'} eq $from_hash) {
3552                                                 print "<td class=\"link nochange\">";
3553                                         } else {
3554                                                 print "<td class=\"link\">";
3555                                         }
3556                                         print $cgi->a({-href => href(action=>"blobdiff",
3557                                                                      hash=>$diff->{'to_id'},
3558                                                                      hash_parent=>$from_hash,
3559                                                                      hash_base=>$hash,
3560                                                                      hash_parent_base=>$hash_parent,
3561                                                                      file_name=>$diff->{'to_file'},
3562                                                                      file_parent=>$from_path)},
3563                                                       "diff" . ($i+1)) .
3564                                               " | </td>\n";
3565                                 }
3566                         }
3568                         print "<td class=\"link\">";
3569                         if ($not_deleted) {
3570                                 print $cgi->a({-href => href(action=>"blob",
3571                                                              hash=>$diff->{'to_id'},
3572                                                              file_name=>$diff->{'to_file'},
3573                                                              hash_base=>$hash)},
3574                                               "blob");
3575                                 print " | " if ($has_history);
3576                         }
3577                         if ($has_history) {
3578                                 print $cgi->a({-href => href(action=>"history",
3579                                                              file_name=>$diff->{'to_file'},
3580                                                              hash_base=>$hash)},
3581                                               "history");
3582                         }
3583                         print "</td>\n";
3585                         print "</tr>\n";
3586                         next; # instead of 'else' clause, to avoid extra indent
3587                 }
3588                 # else ordinary diff
3590                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3591                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3592                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3593                         $to_mode_oct = oct $diff->{'to_mode'};
3594                         if (S_ISREG($to_mode_oct)) { # only for regular file
3595                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3596                         }
3597                         $to_file_type = file_type($diff->{'to_mode'});
3598                 }
3599                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3600                         $from_mode_oct = oct $diff->{'from_mode'};
3601                         if (S_ISREG($to_mode_oct)) { # only for regular file
3602                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3603                         }
3604                         $from_file_type = file_type($diff->{'from_mode'});
3605                 }
3607                 if ($diff->{'status'} eq "A") { # created
3608                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3609                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3610                         $mode_chng   .= "]</span>";
3611                         print "<td>";
3612                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3613                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3614                                       -class => "list"}, esc_path($diff->{'file'}));
3615                         print "</td>\n";
3616                         print "<td>$mode_chng</td>\n";
3617                         print "<td class=\"link\">";
3618                         if ($action eq 'commitdiff') {
3619                                 # link to patch
3620                                 $patchno++;
3621                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3622                                 print " | ";
3623                         }
3624                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3625                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3626                                       "blob");
3627                         print "</td>\n";
3629                 } elsif ($diff->{'status'} eq "D") { # deleted
3630                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3631                         print "<td>";
3632                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3633                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3634                                        -class => "list"}, esc_path($diff->{'file'}));
3635                         print "</td>\n";
3636                         print "<td>$mode_chng</td>\n";
3637                         print "<td class=\"link\">";
3638                         if ($action eq 'commitdiff') {
3639                                 # link to patch
3640                                 $patchno++;
3641                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3642                                 print " | ";
3643                         }
3644                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3645                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3646                                       "blob") . " | ";
3647                         if ($have_blame) {
3648                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3649                                                              file_name=>$diff->{'file'})},
3650                                               "blame") . " | ";
3651                         }
3652                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3653                                                      file_name=>$diff->{'file'})},
3654                                       "history");
3655                         print "</td>\n";
3657                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3658                         my $mode_chnge = "";
3659                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3660                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3661                                 if ($from_file_type ne $to_file_type) {
3662                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3663                                 }
3664                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3665                                         if ($from_mode_str && $to_mode_str) {
3666                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3667                                         } elsif ($to_mode_str) {
3668                                                 $mode_chnge .= " mode: $to_mode_str";
3669                                         }
3670                                 }
3671                                 $mode_chnge .= "]</span>\n";
3672                         }
3673                         print "<td>";
3674                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3675                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3676                                       -class => "list"}, esc_path($diff->{'file'}));
3677                         print "</td>\n";
3678                         print "<td>$mode_chnge</td>\n";
3679                         print "<td class=\"link\">";
3680                         if ($action eq 'commitdiff') {
3681                                 # link to patch
3682                                 $patchno++;
3683                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3684                                       " | ";
3685                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3686                                 # "commit" view and modified file (not onlu mode changed)
3687                                 print $cgi->a({-href => href(action=>"blobdiff",
3688                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3689                                                              hash_base=>$hash, hash_parent_base=>$parent,
3690                                                              file_name=>$diff->{'file'})},
3691                                               "diff") .
3692                                       " | ";
3693                         }
3694                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3695                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3696                                        "blob") . " | ";
3697                         if ($have_blame) {
3698                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3699                                                              file_name=>$diff->{'file'})},
3700                                               "blame") . " | ";
3701                         }
3702                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3703                                                      file_name=>$diff->{'file'})},
3704                                       "history");
3705                         print "</td>\n";
3707                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3708                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3709                         my $nstatus = $status_name{$diff->{'status'}};
3710                         my $mode_chng = "";
3711                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3712                                 # mode also for directories, so we cannot use $to_mode_str
3713                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3714                         }
3715                         print "<td>" .
3716                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3717                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3718                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3719                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3720                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3721                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3722                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3723                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3724                               "<td class=\"link\">";
3725                         if ($action eq 'commitdiff') {
3726                                 # link to patch
3727                                 $patchno++;
3728                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3729                                       " | ";
3730                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3731                                 # "commit" view and modified file (not only pure rename or copy)
3732                                 print $cgi->a({-href => href(action=>"blobdiff",
3733                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3734                                                              hash_base=>$hash, hash_parent_base=>$parent,
3735                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3736                                               "diff") .
3737                                       " | ";
3738                         }
3739                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3740                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3741                                       "blob") . " | ";
3742                         if ($have_blame) {
3743                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3744                                                              file_name=>$diff->{'to_file'})},
3745                                               "blame") . " | ";
3746                         }
3747                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3748                                                     file_name=>$diff->{'to_file'})},
3749                                       "history");
3750                         print "</td>\n";
3752                 } # we should not encounter Unmerged (U) or Unknown (X) status
3753                 print "</tr>\n";
3754         }
3755         print "</tbody>" if $has_header;
3756         print "</table>\n";
3759 sub git_patchset_body {
3760         my ($fd, $difftree, $hash, @hash_parents) = @_;
3761         my ($hash_parent) = $hash_parents[0];
3763         my $is_combined = (@hash_parents > 1);
3764         my $patch_idx = 0;
3765         my $patch_number = 0;
3766         my $patch_line;
3767         my $diffinfo;
3768         my $to_name;
3769         my (%from, %to);
3771         print "<div class=\"patchset\">\n";
3773         # skip to first patch
3774         while ($patch_line = <$fd>) {
3775                 chomp $patch_line;
3777                 last if ($patch_line =~ m/^diff /);
3778         }
3780  PATCH:
3781         while ($patch_line) {
3783                 # parse "git diff" header line
3784                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3785                         # $1 is from_name, which we do not use
3786                         $to_name = unquote($2);
3787                         $to_name =~ s!^b/!!;
3788                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3789                         # $1 is 'cc' or 'combined', which we do not use
3790                         $to_name = unquote($2);
3791                 } else {
3792                         $to_name = undef;
3793                 }
3795                 # check if current patch belong to current raw line
3796                 # and parse raw git-diff line if needed
3797                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3798                         # this is continuation of a split patch
3799                         print "<div class=\"patch cont\">\n";
3800                 } else {
3801                         # advance raw git-diff output if needed
3802                         $patch_idx++ if defined $diffinfo;
3804                         # read and prepare patch information
3805                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3807                         # compact combined diff output can have some patches skipped
3808                         # find which patch (using pathname of result) we are at now;
3809                         if ($is_combined) {
3810                                 while ($to_name ne $diffinfo->{'to_file'}) {
3811                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3812                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3813                                               "</div>\n";  # class="patch"
3815                                         $patch_idx++;
3816                                         $patch_number++;
3818                                         last if $patch_idx > $#$difftree;
3819                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3820                                 }
3821                         }
3823                         # modifies %from, %to hashes
3824                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3826                         # this is first patch for raw difftree line with $patch_idx index
3827                         # we index @$difftree array from 0, but number patches from 1
3828                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3829                 }
3831                 # git diff header
3832                 #assert($patch_line =~ m/^diff /) if DEBUG;
3833                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3834                 $patch_number++;
3835                 # print "git diff" header
3836                 print format_git_diff_header_line($patch_line, $diffinfo,
3837                                                   \%from, \%to);
3839                 # print extended diff header
3840                 print "<div class=\"diff extended_header\">\n";
3841         EXTENDED_HEADER:
3842                 while ($patch_line = <$fd>) {
3843                         chomp $patch_line;
3845                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3847                         print format_extended_diff_header_line($patch_line, $diffinfo,
3848                                                                \%from, \%to);
3849                 }
3850                 print "</div>\n"; # class="diff extended_header"
3852                 # from-file/to-file diff header
3853                 if (! $patch_line) {
3854                         print "</div>\n"; # class="patch"
3855                         last PATCH;
3856                 }
3857                 next PATCH if ($patch_line =~ m/^diff /);
3858                 #assert($patch_line =~ m/^---/) if DEBUG;
3860                 my $last_patch_line = $patch_line;
3861                 $patch_line = <$fd>;
3862                 chomp $patch_line;
3863                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3865                 print format_diff_from_to_header($last_patch_line, $patch_line,
3866                                                  $diffinfo, \%from, \%to,
3867                                                  @hash_parents);
3869                 # the patch itself
3870         LINE:
3871                 while ($patch_line = <$fd>) {
3872                         chomp $patch_line;
3874                         next PATCH if ($patch_line =~ m/^diff /);
3876                         print format_diff_line($patch_line, \%from, \%to);
3877                 }
3879         } continue {
3880                 print "</div>\n"; # class="patch"
3881         }
3883         # for compact combined (--cc) format, with chunk and patch simpliciaction
3884         # patchset might be empty, but there might be unprocessed raw lines
3885         for (++$patch_idx if $patch_number > 0;
3886              $patch_idx < @$difftree;
3887              ++$patch_idx) {
3888                 # read and prepare patch information
3889                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3891                 # generate anchor for "patch" links in difftree / whatchanged part
3892                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3893                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3894                       "</div>\n";  # class="patch"
3896                 $patch_number++;
3897         }
3899         if ($patch_number == 0) {
3900                 if (@hash_parents > 1) {
3901                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3902                 } else {
3903                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3904                 }
3905         }
3907         print "</div>\n"; # class="patchset"
3910 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3912 # fills project list info (age, description, owner, forks) for each
3913 # project in the list, removing invalid projects from returned list
3914 # NOTE: modifies $projlist, but does not remove entries from it
3915 sub fill_project_list_info {
3916         my ($projlist, $check_forks) = @_;
3917         my @projects;
3919         my $show_ctags = gitweb_check_feature('ctags');
3920  PROJECT:
3921         foreach my $pr (@$projlist) {
3922                 my (@activity) = git_get_last_activity($pr->{'path'});
3923                 unless (@activity) {
3924                         next PROJECT;
3925                 }
3926                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3927                 if (!defined $pr->{'descr'}) {
3928                         my $descr = git_get_project_description($pr->{'path'}) || "";
3929                         $descr = to_utf8($descr);
3930                         $pr->{'descr_long'} = $descr;
3931                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3932                 }
3933                 if (!defined $pr->{'owner'}) {
3934                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3935                 }
3936                 if ($check_forks) {
3937                         my $pname = $pr->{'path'};
3938                         if (($pname =~ s/\.git$//) &&
3939                             ($pname !~ /\/$/) &&
3940                             (-d "$projectroot/$pname")) {
3941                                 $pr->{'forks'} = "-d $projectroot/$pname";
3942                         }       else {
3943                                 $pr->{'forks'} = 0;
3944                         }
3945                 }
3946                 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3947                 push @projects, $pr;
3948         }
3950         return @projects;
3953 # print 'sort by' <th> element, generating 'sort by $name' replay link
3954 # if that order is not selected
3955 sub print_sort_th {
3956         my ($name, $order, $header) = @_;
3957         $header ||= ucfirst($name);
3959         if ($order eq $name) {
3960                 print "<th>$header</th>\n";
3961         } else {
3962                 print "<th>" .
3963                       $cgi->a({-href => href(-replay=>1, order=>$name),
3964                                -class => "header"}, $header) .
3965                       "</th>\n";
3966         }
3969 sub git_project_list_body {
3970         # actually uses global variable $project
3971         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3973         my $check_forks = gitweb_check_feature('forks');
3974         my @projects = fill_project_list_info($projlist, $check_forks);
3976         $order ||= $default_projects_order;
3977         $from = 0 unless defined $from;
3978         $to = $#projects if (!defined $to || $#projects < $to);
3980         my %order_info = (
3981                 project => { key => 'path', type => 'str' },
3982                 descr => { key => 'descr_long', type => 'str' },
3983                 owner => { key => 'owner', type => 'str' },
3984                 age => { key => 'age', type => 'num' }
3985         );
3986         my $oi = $order_info{$order};
3987         if ($oi->{'type'} eq 'str') {
3988                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3989         } else {
3990                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3991         }
3993         my $show_ctags = gitweb_check_feature('ctags');
3994         if ($show_ctags) {
3995                 my %ctags;
3996                 foreach my $p (@projects) {
3997                         foreach my $ct (keys %{$p->{'ctags'}}) {
3998                                 $ctags{$ct} += $p->{'ctags'}->{$ct};
3999                         }
4000                 }
4001                 my $cloud = git_populate_project_tagcloud(\%ctags);
4002                 print git_show_project_tagcloud($cloud, 64);
4003         }
4005         print "<table class=\"project_list\">\n";
4006         unless ($no_header) {
4007                 print "<tr>\n";
4008                 if ($check_forks) {
4009                         print "<th></th>\n";
4010                 }
4011                 print_sort_th('project', $order, 'Project');
4012                 print_sort_th('descr', $order, 'Description');
4013                 print_sort_th('owner', $order, 'Owner');
4014                 print_sort_th('age', $order, 'Last Change');
4015                 print "<th></th>\n" . # for links
4016                       "</tr>\n";
4017         }
4018         my $alternate = 1;
4019         my $tagfilter = $cgi->param('by_tag');
4020         for (my $i = $from; $i <= $to; $i++) {
4021                 my $pr = $projects[$i];
4023                 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4024                 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4025                         and not $pr->{'descr_long'} =~ /$searchtext/;
4026                 # Weed out forks or non-matching entries of search
4027                 if ($check_forks) {
4028                         my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4029                         $forkbase="^$forkbase" if $forkbase;
4030                         next if not $searchtext and not $tagfilter and $show_ctags
4031                                 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4032                 }
4034                 if ($alternate) {
4035                         print "<tr class=\"dark\">\n";
4036                 } else {
4037                         print "<tr class=\"light\">\n";
4038                 }
4039                 $alternate ^= 1;
4040                 if ($check_forks) {
4041                         print "<td>";
4042                         if ($pr->{'forks'}) {
4043                                 print "<!-- $pr->{'forks'} -->\n";
4044                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4045                         }
4046                         print "</td>\n";
4047                 }
4048                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4049                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4050                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4051                                         -class => "list", -title => $pr->{'descr_long'}},
4052                                         esc_html($pr->{'descr'})) . "</td>\n" .
4053                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4054                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4055                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4056                       "<td class=\"link\">" .
4057                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4058                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4059                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4060                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4061                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4062                       "</td>\n" .
4063                       "</tr>\n";
4064         }
4065         if (defined $extra) {
4066                 print "<tr>\n";
4067                 if ($check_forks) {
4068                         print "<td></td>\n";
4069                 }
4070                 print "<td colspan=\"5\">$extra</td>\n" .
4071                       "</tr>\n";
4072         }
4073         print "</table>\n";
4076 sub git_shortlog_body {
4077         # uses global variable $project
4078         my ($commitlist, $from, $to, $refs, $extra) = @_;
4080         $from = 0 unless defined $from;
4081         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4083         print "<table class=\"shortlog\">\n";
4084         my $alternate = 1;
4085         for (my $i = $from; $i <= $to; $i++) {
4086                 my %co = %{$commitlist->[$i]};
4087                 my $commit = $co{'id'};
4088                 my $ref = format_ref_marker($refs, $commit);
4089                 if ($alternate) {
4090                         print "<tr class=\"dark\">\n";
4091                 } else {
4092                         print "<tr class=\"light\">\n";
4093                 }
4094                 $alternate ^= 1;
4095                 my $author = chop_and_escape_str($co{'author_name'}, 10);
4096                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4097                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4098                       "<td><i>" . $author . "</i></td>\n" .
4099                       "<td>";
4100                 print format_subject_html($co{'title'}, $co{'title_short'},
4101                                           href(action=>"commit", hash=>$commit), $ref);
4102                 print "</td>\n" .
4103                       "<td class=\"link\">" .
4104                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4105                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4106                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4107                 my $snapshot_links = format_snapshot_links($commit);
4108                 if (defined $snapshot_links) {
4109                         print " | " . $snapshot_links;
4110                 }
4111                 print "</td>\n" .
4112                       "</tr>\n";
4113         }
4114         if (defined $extra) {
4115                 print "<tr>\n" .
4116                       "<td colspan=\"4\">$extra</td>\n" .
4117                       "</tr>\n";
4118         }
4119         print "</table>\n";
4122 sub git_history_body {
4123         # Warning: assumes constant type (blob or tree) during history
4124         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4126         $from = 0 unless defined $from;
4127         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4129         print "<table class=\"history\">\n";
4130         my $alternate = 1;
4131         for (my $i = $from; $i <= $to; $i++) {
4132                 my %co = %{$commitlist->[$i]};
4133                 if (!%co) {
4134                         next;
4135                 }
4136                 my $commit = $co{'id'};
4138                 my $ref = format_ref_marker($refs, $commit);
4140                 if ($alternate) {
4141                         print "<tr class=\"dark\">\n";
4142                 } else {
4143                         print "<tr class=\"light\">\n";
4144                 }
4145                 $alternate ^= 1;
4146         # shortlog uses      chop_str($co{'author_name'}, 10)
4147                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4148                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4149                       "<td><i>" . $author . "</i></td>\n" .
4150                       "<td>";
4151                 # originally git_history used chop_str($co{'title'}, 50)
4152                 print format_subject_html($co{'title'}, $co{'title_short'},
4153                                           href(action=>"commit", hash=>$commit), $ref);
4154                 print "</td>\n" .
4155                       "<td class=\"link\">" .
4156                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4157                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4159                 if ($ftype eq 'blob') {
4160                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4161                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4162                         if (defined $blob_current && defined $blob_parent &&
4163                                         $blob_current ne $blob_parent) {
4164                                 print " | " .
4165                                         $cgi->a({-href => href(action=>"blobdiff",
4166                                                                hash=>$blob_current, hash_parent=>$blob_parent,
4167                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
4168                                                                file_name=>$file_name)},
4169                                                 "diff to current");
4170                         }
4171                 }
4172                 print "</td>\n" .
4173                       "</tr>\n";
4174         }
4175         if (defined $extra) {
4176                 print "<tr>\n" .
4177                       "<td colspan=\"4\">$extra</td>\n" .
4178                       "</tr>\n";
4179         }
4180         print "</table>\n";
4183 sub git_tags_body {
4184         # uses global variable $project
4185         my ($taglist, $from, $to, $extra) = @_;
4186         $from = 0 unless defined $from;
4187         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4189         print "<table class=\"tags\">\n";
4190         my $alternate = 1;
4191         for (my $i = $from; $i <= $to; $i++) {
4192                 my $entry = $taglist->[$i];
4193                 my %tag = %$entry;
4194                 my $comment = $tag{'subject'};
4195                 my $comment_short;
4196                 if (defined $comment) {
4197                         $comment_short = chop_str($comment, 30, 5);
4198                 }
4199                 if ($alternate) {
4200                         print "<tr class=\"dark\">\n";
4201                 } else {
4202                         print "<tr class=\"light\">\n";
4203                 }
4204                 $alternate ^= 1;
4205                 if (defined $tag{'age'}) {
4206                         print "<td><i>$tag{'age'}</i></td>\n";
4207                 } else {
4208                         print "<td></td>\n";
4209                 }
4210                 print "<td>" .
4211                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4212                                -class => "list name"}, esc_html($tag{'name'})) .
4213                       "</td>\n" .
4214                       "<td>";
4215                 if (defined $comment) {
4216                         print format_subject_html($comment, $comment_short,
4217                                                   href(action=>"tag", hash=>$tag{'id'}));
4218                 }
4219                 print "</td>\n" .
4220                       "<td class=\"selflink\">";
4221                 if ($tag{'type'} eq "tag") {
4222                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4223                 } else {
4224                         print "&nbsp;";
4225                 }
4226                 print "</td>\n" .
4227                       "<td class=\"link\">" . " | " .
4228                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4229                 if ($tag{'reftype'} eq "commit") {
4230                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4231                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4232                 } elsif ($tag{'reftype'} eq "blob") {
4233                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4234                 }
4235                 print "</td>\n" .
4236                       "</tr>";
4237         }
4238         if (defined $extra) {
4239                 print "<tr>\n" .
4240                       "<td colspan=\"5\">$extra</td>\n" .
4241                       "</tr>\n";
4242         }
4243         print "</table>\n";
4246 sub git_heads_body {
4247         # uses global variable $project
4248         my ($headlist, $head, $from, $to, $extra) = @_;
4249         $from = 0 unless defined $from;
4250         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4252         print "<table class=\"heads\">\n";
4253         my $alternate = 1;
4254         for (my $i = $from; $i <= $to; $i++) {
4255                 my $entry = $headlist->[$i];
4256                 my %ref = %$entry;
4257                 my $curr = $ref{'id'} eq $head;
4258                 if ($alternate) {
4259                         print "<tr class=\"dark\">\n";
4260                 } else {
4261                         print "<tr class=\"light\">\n";
4262                 }
4263                 $alternate ^= 1;
4264                 print "<td><i>$ref{'age'}</i></td>\n" .
4265                       ($curr ? "<td class=\"current_head\">" : "<td>") .
4266                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4267                                -class => "list name"},esc_html($ref{'name'})) .
4268                       "</td>\n" .
4269                       "<td class=\"link\">" .
4270                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4271                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4272                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4273                       "</td>\n" .
4274                       "</tr>";
4275         }
4276         if (defined $extra) {
4277                 print "<tr>\n" .
4278                       "<td colspan=\"3\">$extra</td>\n" .
4279                       "</tr>\n";
4280         }
4281         print "</table>\n";
4284 sub git_search_grep_body {
4285         my ($commitlist, $from, $to, $extra) = @_;
4286         $from = 0 unless defined $from;
4287         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4289         print "<table class=\"commit_search\">\n";
4290         my $alternate = 1;
4291         for (my $i = $from; $i <= $to; $i++) {
4292                 my %co = %{$commitlist->[$i]};
4293                 if (!%co) {
4294                         next;
4295                 }
4296                 my $commit = $co{'id'};
4297                 if ($alternate) {
4298                         print "<tr class=\"dark\">\n";
4299                 } else {
4300                         print "<tr class=\"light\">\n";
4301                 }
4302                 $alternate ^= 1;
4303                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4304                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4305                       "<td><i>" . $author . "</i></td>\n" .
4306                       "<td>" .
4307                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4308                                -class => "list subject"},
4309                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
4310                 my $comment = $co{'comment'};
4311                 foreach my $line (@$comment) {
4312                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4313                                 my ($lead, $match, $trail) = ($1, $2, $3);
4314                                 $match = chop_str($match, 70, 5, 'center');
4315                                 my $contextlen = int((80 - length($match))/2);
4316                                 $contextlen = 30 if ($contextlen > 30);
4317                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
4318                                 $trail = chop_str($trail, $contextlen, 10, 'right');
4320                                 $lead  = esc_html($lead);
4321                                 $match = esc_html($match);
4322                                 $trail = esc_html($trail);
4324                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
4325                         }
4326                 }
4327                 print "</td>\n" .
4328                       "<td class=\"link\">" .
4329                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4330                       " | " .
4331                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4332                       " | " .
4333                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4334                 print "</td>\n" .
4335                       "</tr>\n";
4336         }
4337         if (defined $extra) {
4338                 print "<tr>\n" .
4339                       "<td colspan=\"3\">$extra</td>\n" .
4340                       "</tr>\n";
4341         }
4342         print "</table>\n";
4345 ## ======================================================================
4346 ## ======================================================================
4347 ## actions
4349 sub git_project_list {
4350         my $order = $input_params{'order'};
4351         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4352                 die_error(400, "Unknown order parameter");
4353         }
4355         my @list = git_get_projects_list();
4356         if (!@list) {
4357                 die_error(404, "No projects found");
4358         }
4360         git_header_html();
4361         if (-f $home_text) {
4362                 print "<div class=\"index_include\">\n";
4363                 insert_file($home_text);
4364                 print "</div>\n";
4365         }
4366         print $cgi->startform(-method => "get") .
4367               "<p class=\"projsearch\">Search:\n" .
4368               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4369               "</p>" .
4370               $cgi->end_form() . "\n";
4371         git_project_list_body(\@list, $order);
4372         git_footer_html();
4375 sub git_forks {
4376         my $order = $input_params{'order'};
4377         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4378                 die_error(400, "Unknown order parameter");
4379         }
4381         my @list = git_get_projects_list($project);
4382         if (!@list) {
4383                 die_error(404, "No forks found");
4384         }
4386         git_header_html();
4387         git_print_page_nav('','');
4388         git_print_header_div('summary', "$project forks");
4389         git_project_list_body(\@list, $order);
4390         git_footer_html();
4393 sub git_project_index {
4394         my @projects = git_get_projects_list($project);
4396         print $cgi->header(
4397                 -type => 'text/plain',
4398                 -charset => 'utf-8',
4399                 -content_disposition => 'inline; filename="index.aux"');
4401         foreach my $pr (@projects) {
4402                 if (!exists $pr->{'owner'}) {
4403                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4404                 }
4406                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4407                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4408                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4409                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4410                 $path  =~ s/ /\+/g;
4411                 $owner =~ s/ /\+/g;
4413                 print "$path $owner\n";
4414         }
4417 sub git_summary {
4418         my $descr = git_get_project_description($project) || "none";
4419         my %co = parse_commit("HEAD");
4420         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4421         my $head = $co{'id'};
4423         my $owner = git_get_project_owner($project);
4425         my $refs = git_get_references();
4426         # These get_*_list functions return one more to allow us to see if
4427         # there are more ...
4428         my @taglist  = git_get_tags_list(16);
4429         my @headlist = git_get_heads_list(16);
4430         my @forklist;
4431         my $check_forks = gitweb_check_feature('forks');
4433         if ($check_forks) {
4434                 @forklist = git_get_projects_list($project);
4435         }
4437         git_header_html();
4438         git_print_page_nav('summary','', $head);
4440         print "<div class=\"title\">&nbsp;</div>\n";
4441         print "<table class=\"projects_list\">\n" .
4442               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4443               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4444         if (defined $cd{'rfc2822'}) {
4445                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4446         }
4448         # use per project git URL list in $projectroot/$project/cloneurl
4449         # or make project git URL from git base URL and project name
4450         my $url_tag = "URL";
4451         my @url_list = git_get_project_url_list($project);
4452         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4453         foreach my $git_url (@url_list) {
4454                 next unless $git_url;
4455                 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4456                 $url_tag = "";
4457         }
4459         # Tag cloud
4460         my $show_ctags = gitweb_check_feature('ctags');
4461         if ($show_ctags) {
4462                 my $ctags = git_get_project_ctags($project);
4463                 my $cloud = git_populate_project_tagcloud($ctags);
4464                 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4465                 print "</td>\n<td>" unless %$ctags;
4466                 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4467                 print "</td>\n<td>" if %$ctags;
4468                 print git_show_project_tagcloud($cloud, 48);
4469                 print "</td></tr>";
4470         }
4472         print "</table>\n";
4474         if (-s "$projectroot/$project/README.html") {
4475                 print "<div class=\"title\">readme</div>\n" .
4476                       "<div class=\"readme\">\n";
4477                 insert_file("$projectroot/$project/README.html");
4478                 print "\n</div>\n"; # class="readme"
4479         }
4481         # we need to request one more than 16 (0..15) to check if
4482         # those 16 are all
4483         my @commitlist = $head ? parse_commits($head, 17) : ();
4484         if (@commitlist) {
4485                 git_print_header_div('shortlog');
4486                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4487                                   $#commitlist <=  15 ? undef :
4488                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4489         }
4491         if (@taglist) {
4492                 git_print_header_div('tags');
4493                 git_tags_body(\@taglist, 0, 15,
4494                               $#taglist <=  15 ? undef :
4495                               $cgi->a({-href => href(action=>"tags")}, "..."));
4496         }
4498         if (@headlist) {
4499                 git_print_header_div('heads');
4500                 git_heads_body(\@headlist, $head, 0, 15,
4501                                $#headlist <= 15 ? undef :
4502                                $cgi->a({-href => href(action=>"heads")}, "..."));
4503         }
4505         if (@forklist) {
4506                 git_print_header_div('forks');
4507                 git_project_list_body(\@forklist, 'age', 0, 15,
4508                                       $#forklist <= 15 ? undef :
4509                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4510                                       'no_header');
4511         }
4513         git_footer_html();
4516 sub git_tag {
4517         my $head = git_get_head_hash($project);
4518         git_header_html();
4519         git_print_page_nav('','', $head,undef,$head);
4520         my %tag = parse_tag($hash);
4522         if (! %tag) {
4523                 die_error(404, "Unknown tag object");
4524         }
4526         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4527         print "<div class=\"title_text\">\n" .
4528               "<table class=\"object_header\">\n" .
4529               "<tr>\n" .
4530               "<td>object</td>\n" .
4531               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4532                                $tag{'object'}) . "</td>\n" .
4533               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4534                                               $tag{'type'}) . "</td>\n" .
4535               "</tr>\n";
4536         if (defined($tag{'author'})) {
4537                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4538                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4539                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4540                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4541                         "</td></tr>\n";
4542         }
4543         print "</table>\n\n" .
4544               "</div>\n";
4545         print "<div class=\"page_body\">";
4546         my $comment = $tag{'comment'};
4547         foreach my $line (@$comment) {
4548                 chomp $line;
4549                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4550         }
4551         print "</div>\n";
4552         git_footer_html();
4555 sub git_blame {
4556         # permissions
4557         gitweb_check_feature('blame')
4558                 or die_error(403, "Blame view not allowed");
4560         # error checking
4561         die_error(400, "No file name given") unless $file_name;
4562         $hash_base ||= git_get_head_hash($project);
4563         die_error(404, "Couldn't find base commit") unless $hash_base;
4564         my %co = parse_commit($hash_base)
4565                 or die_error(404, "Commit not found");
4566         my $ftype = "blob";
4567         if (!defined $hash) {
4568                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4569                         or die_error(404, "Error looking up file");
4570         } else {
4571                 $ftype = git_get_type($hash);
4572                 if ($ftype !~ "blob") {
4573                         die_error(400, "Object is not a blob");
4574                 }
4575         }
4577         # run git-blame --porcelain
4578         open my $fd, "-|", git_cmd(), "blame", '-p',
4579                 $hash_base, '--', $file_name
4580                 or die_error(500, "Open git-blame failed");
4582         # page header
4583         git_header_html();
4584         my $formats_nav =
4585                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4586                         "blob") .
4587                 " | " .
4588                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4589                         "history") .
4590                 " | " .
4591                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4592                         "HEAD");
4593         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4594         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4595         git_print_page_path($file_name, $ftype, $hash_base);
4597         # page body
4598         my @rev_color = qw(light2 dark2);
4599         my $num_colors = scalar(@rev_color);
4600         my $current_color = 0;
4601         my %metainfo = ();
4603         print <<HTML;
4604 <div class="page_body">
4605 <table class="blame">
4606 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4607 HTML
4608  LINE:
4609         while (my $line = <$fd>) {
4610                 chomp $line;
4611                 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4612                 # no <lines in group> for subsequent lines in group of lines
4613                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4614                    ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4615                 if (!exists $metainfo{$full_rev}) {
4616                         $metainfo{$full_rev} = {};
4617                 }
4618                 my $meta = $metainfo{$full_rev};
4619                 my $data;
4620                 while ($data = <$fd>) {
4621                         chomp $data;
4622                         last if ($data =~ s/^\t//); # contents of line
4623                         if ($data =~ /^(\S+) (.*)$/) {
4624                                 $meta->{$1} = $2;
4625                         }
4626                 }
4627                 my $short_rev = substr($full_rev, 0, 8);
4628                 my $author = $meta->{'author'};
4629                 my %date =
4630                         parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4631                 my $date = $date{'iso-tz'};
4632                 if ($group_size) {
4633                         $current_color = ($current_color + 1) % $num_colors;
4634                 }
4635                 print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
4636                 if ($group_size) {
4637                         print "<td class=\"sha1\"";
4638                         print " title=\"". esc_html($author) . ", $date\"";
4639                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4640                         print ">";
4641                         print $cgi->a({-href => href(action=>"commit",
4642                                                      hash=>$full_rev,
4643                                                      file_name=>$file_name)},
4644                                       esc_html($short_rev));
4645                         print "</td>\n";
4646                 }
4647                 my $parent_commit;
4648                 if (!exists $meta->{'parent'}) {
4649                         open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4650                                 or die_error(500, "Open git-rev-parse failed");
4651                         $parent_commit = <$dd>;
4652                         close $dd;
4653                         chomp($parent_commit);
4654                         $meta->{'parent'} = $parent_commit;
4655                 } else {
4656                         $parent_commit = $meta->{'parent'};
4657                 }
4658                 my $blamed = href(action => 'blame',
4659                                   file_name => $meta->{'filename'},
4660                                   hash_base => $parent_commit);
4661                 print "<td class=\"linenr\">";
4662                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4663                                 -class => "linenr" },
4664                               esc_html($lineno));
4665                 print "</td>";
4666                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4667                 print "</tr>\n";
4668         }
4669         print "</table>\n";
4670         print "</div>";
4671         close $fd
4672                 or print "Reading blob failed\n";
4674         # page footer
4675         git_footer_html();
4678 sub git_tags {
4679         my $head = git_get_head_hash($project);
4680         git_header_html();
4681         git_print_page_nav('','', $head,undef,$head);
4682         git_print_header_div('summary', $project);
4684         my @tagslist = git_get_tags_list();
4685         if (@tagslist) {
4686                 git_tags_body(\@tagslist);
4687         }
4688         git_footer_html();
4691 sub git_heads {
4692         my $head = git_get_head_hash($project);
4693         git_header_html();
4694         git_print_page_nav('','', $head,undef,$head);
4695         git_print_header_div('summary', $project);
4697         my @headslist = git_get_heads_list();
4698         if (@headslist) {
4699                 git_heads_body(\@headslist, $head);
4700         }
4701         git_footer_html();
4704 sub git_blob_plain {
4705         my $type = shift;
4706         my $expires;
4708         if (!defined $hash) {
4709                 if (defined $file_name) {
4710                         my $base = $hash_base || git_get_head_hash($project);
4711                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4712                                 or die_error(404, "Cannot find file");
4713                 } else {
4714                         die_error(400, "No file name defined");
4715                 }
4716         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4717                 # blobs defined by non-textual hash id's can be cached
4718                 $expires = "+1d";
4719         }
4721         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4722                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4724         # content-type (can include charset)
4725         $type = blob_contenttype($fd, $file_name, $type);
4727         # "save as" filename, even when no $file_name is given
4728         my $save_as = "$hash";
4729         if (defined $file_name) {
4730                 $save_as = $file_name;
4731         } elsif ($type =~ m/^text\//) {
4732                 $save_as .= '.txt';
4733         }
4735         print $cgi->header(
4736                 -type => $type,
4737                 -expires => $expires,
4738                 -content_disposition => 'inline; filename="' . $save_as . '"');
4739         undef $/;
4740         binmode STDOUT, ':raw';
4741         print <$fd>;
4742         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4743         $/ = "\n";
4744         close $fd;
4747 sub git_blob {
4748         my $expires;
4750         if (!defined $hash) {
4751                 if (defined $file_name) {
4752                         my $base = $hash_base || git_get_head_hash($project);
4753                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4754                                 or die_error(404, "Cannot find file");
4755                 } else {
4756                         die_error(400, "No file name defined");
4757                 }
4758         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4759                 # blobs defined by non-textual hash id's can be cached
4760                 $expires = "+1d";
4761         }
4763         my $have_blame = gitweb_check_feature('blame');
4764         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4765                 or die_error(500, "Couldn't cat $file_name, $hash");
4766         my $mimetype = blob_mimetype($fd, $file_name);
4767         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4768                 close $fd;
4769                 return git_blob_plain($mimetype);
4770         }
4771         # we can have blame only for text/* mimetype
4772         $have_blame &&= ($mimetype =~ m!^text/!);
4774         git_header_html(undef, $expires);
4775         my $formats_nav = '';
4776         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4777                 if (defined $file_name) {
4778                         if ($have_blame) {
4779                                 $formats_nav .=
4780                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
4781                                                 "blame") .
4782                                         " | ";
4783                         }
4784                         $formats_nav .=
4785                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4786                                         "history") .
4787                                 " | " .
4788                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4789                                         "raw") .
4790                                 " | " .
4791                                 $cgi->a({-href => href(action=>"blob",
4792                                                        hash_base=>"HEAD", file_name=>$file_name)},
4793                                         "HEAD");
4794                 } else {
4795                         $formats_nav .=
4796                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4797                                         "raw");
4798                 }
4799                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4800                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4801         } else {
4802                 print "<div class=\"page_nav\">\n" .
4803                       "<br/><br/></div>\n" .
4804                       "<div class=\"title\">$hash</div>\n";
4805         }
4806         git_print_page_path($file_name, "blob", $hash_base);
4807         print "<div class=\"page_body\">\n";
4808         if ($mimetype =~ m!^image/!) {
4809                 print qq!<img type="$mimetype"!;
4810                 if ($file_name) {
4811                         print qq! alt="$file_name" title="$file_name"!;
4812                 }
4813                 print qq! src="! .
4814                       href(action=>"blob_plain", hash=>$hash,
4815                            hash_base=>$hash_base, file_name=>$file_name) .
4816                       qq!" />\n!;
4817         } else {
4818                 my $nr;
4819                 while (my $line = <$fd>) {
4820                         chomp $line;
4821                         $nr++;
4822                         $line = untabify($line);
4823                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4824                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4825                 }
4826         }
4827         close $fd
4828                 or print "Reading blob failed.\n";
4829         print "</div>";
4830         git_footer_html();
4833 sub git_tree {
4834         if (!defined $hash_base) {
4835                 $hash_base = "HEAD";
4836         }
4837         if (!defined $hash) {
4838                 if (defined $file_name) {
4839                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4840                 } else {
4841                         $hash = $hash_base;
4842                 }
4843         }
4844         die_error(404, "No such tree") unless defined($hash);
4845         $/ = "\0";
4846         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4847                 or die_error(500, "Open git-ls-tree failed");
4848         my @entries = map { chomp; $_ } <$fd>;
4849         close $fd or die_error(404, "Reading tree failed");
4850         $/ = "\n";
4852         my $refs = git_get_references();
4853         my $ref = format_ref_marker($refs, $hash_base);
4854         git_header_html();
4855         my $basedir = '';
4856         my $have_blame = gitweb_check_feature('blame');
4857         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4858                 my @views_nav = ();
4859                 if (defined $file_name) {
4860                         push @views_nav,
4861                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4862                                         "history"),
4863                                 $cgi->a({-href => href(action=>"tree",
4864                                                        hash_base=>"HEAD", file_name=>$file_name)},
4865                                         "HEAD"),
4866                 }
4867                 my $snapshot_links = format_snapshot_links($hash);
4868                 if (defined $snapshot_links) {
4869                         # FIXME: Should be available when we have no hash base as well.
4870                         push @views_nav, $snapshot_links;
4871                 }
4872                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4873                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4874         } else {
4875                 undef $hash_base;
4876                 print "<div class=\"page_nav\">\n";
4877                 print "<br/><br/></div>\n";
4878                 print "<div class=\"title\">$hash</div>\n";
4879         }
4880         if (defined $file_name) {
4881                 $basedir = $file_name;
4882                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4883                         $basedir .= '/';
4884                 }
4885                 git_print_page_path($file_name, 'tree', $hash_base);
4886         }
4887         print "<div class=\"page_body\">\n";
4888         print "<table class=\"tree\">\n";
4889         my $alternate = 1;
4890         # '..' (top directory) link if possible
4891         if (defined $hash_base &&
4892             defined $file_name && $file_name =~ m![^/]+$!) {
4893                 if ($alternate) {
4894                         print "<tr class=\"dark\">\n";
4895                 } else {
4896                         print "<tr class=\"light\">\n";
4897                 }
4898                 $alternate ^= 1;
4900                 my $up = $file_name;
4901                 $up =~ s!/?[^/]+$!!;
4902                 undef $up unless $up;
4903                 # based on git_print_tree_entry
4904                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4905                 print '<td class="list">';
4906                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4907                                              file_name=>$up)},
4908                               "..");
4909                 print "</td>\n";
4910                 print "<td class=\"link\"></td>\n";
4912                 print "</tr>\n";
4913         }
4914         foreach my $line (@entries) {
4915                 my %t = parse_ls_tree_line($line, -z => 1);
4917                 if ($alternate) {
4918                         print "<tr class=\"dark\">\n";
4919                 } else {
4920                         print "<tr class=\"light\">\n";
4921                 }
4922                 $alternate ^= 1;
4924                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4926                 print "</tr>\n";
4927         }
4928         print "</table>\n" .
4929               "</div>";
4930         git_footer_html();
4933 sub git_snapshot {
4934         my $format = $input_params{'snapshot_format'};
4935         if (!@snapshot_fmts) {
4936                 die_error(403, "Snapshots not allowed");
4937         }
4938         # default to first supported snapshot format
4939         $format ||= $snapshot_fmts[0];
4940         if ($format !~ m/^[a-z0-9]+$/) {
4941                 die_error(400, "Invalid snapshot format parameter");
4942         } elsif (!exists($known_snapshot_formats{$format})) {
4943                 die_error(400, "Unknown snapshot format");
4944         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
4945                 die_error(403, "Unsupported snapshot format");
4946         }
4948         if (!defined $hash) {
4949                 $hash = git_get_head_hash($project);
4950         }
4952         my $name = $project;
4953         $name =~ s,([^/])/*\.git$,$1,;
4954         $name = basename($name);
4955         my $filename = to_utf8($name);
4956         $name =~ s/\047/\047\\\047\047/g;
4957         my $cmd;
4958         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4959         $cmd = quote_command(
4960                 git_cmd(), 'archive',
4961                 "--format=$known_snapshot_formats{$format}{'format'}",
4962                 "--prefix=$name/", $hash);
4963         if (exists $known_snapshot_formats{$format}{'compressor'}) {
4964                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4965         }
4967         print $cgi->header(
4968                 -type => $known_snapshot_formats{$format}{'type'},
4969                 -content_disposition => 'inline; filename="' . "$filename" . '"',
4970                 -status => '200 OK');
4972         open my $fd, "-|", $cmd
4973                 or die_error(500, "Execute git-archive failed");
4974         binmode STDOUT, ':raw';
4975         print <$fd>;
4976         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4977         close $fd;
4980 sub git_log {
4981         my $head = git_get_head_hash($project);
4982         if (!defined $hash) {
4983                 $hash = $head;
4984         }
4985         if (!defined $page) {
4986                 $page = 0;
4987         }
4988         my $refs = git_get_references();
4990         my @commitlist = parse_commits($hash, 101, (100 * $page));
4992         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4994         git_header_html();
4995         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4997         if (!@commitlist) {
4998                 my %co = parse_commit($hash);
5000                 git_print_header_div('summary', $project);
5001                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5002         }
5003         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5004         for (my $i = 0; $i <= $to; $i++) {
5005                 my %co = %{$commitlist[$i]};
5006                 next if !%co;
5007                 my $commit = $co{'id'};
5008                 my $ref = format_ref_marker($refs, $commit);
5009                 my %ad = parse_date($co{'author_epoch'});
5010                 git_print_header_div('commit',
5011                                "<span class=\"age\">$co{'age_string'}</span>" .
5012                                esc_html($co{'title'}) . $ref,
5013                                $commit);
5014                 print "<div class=\"title_text\">\n" .
5015                       "<div class=\"log_link\">\n" .
5016                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5017                       " | " .
5018                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5019                       " | " .
5020                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5021                       "<br/>\n" .
5022                       "</div>\n" .
5023                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
5024                       "</div>\n";
5026                 print "<div class=\"log_body\">\n";
5027                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5028                 print "</div>\n";
5029         }
5030         if ($#commitlist >= 100) {
5031                 print "<div class=\"page_nav\">\n";
5032                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5033                                -accesskey => "n", -title => "Alt-n"}, "next");
5034                 print "</div>\n";
5035         }
5036         git_footer_html();
5039 sub git_commit {
5040         $hash ||= $hash_base || "HEAD";
5041         my %co = parse_commit($hash)
5042             or die_error(404, "Unknown commit object");
5043         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5044         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
5046         my $parent  = $co{'parent'};
5047         my $parents = $co{'parents'}; # listref
5049         # we need to prepare $formats_nav before any parameter munging
5050         my $formats_nav;
5051         if (!defined $parent) {
5052                 # --root commitdiff
5053                 $formats_nav .= '(initial)';
5054         } elsif (@$parents == 1) {
5055                 # single parent commit
5056                 $formats_nav .=
5057                         '(parent: ' .
5058                         $cgi->a({-href => href(action=>"commit",
5059                                                hash=>$parent)},
5060                                 esc_html(substr($parent, 0, 7))) .
5061                         ')';
5062         } else {
5063                 # merge commit
5064                 $formats_nav .=
5065                         '(merge: ' .
5066                         join(' ', map {
5067                                 $cgi->a({-href => href(action=>"commit",
5068                                                        hash=>$_)},
5069                                         esc_html(substr($_, 0, 7)));
5070                         } @$parents ) .
5071                         ')';
5072         }
5074         if (!defined $parent) {
5075                 $parent = "--root";
5076         }
5077         my @difftree;
5078         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5079                 @diff_opts,
5080                 (@$parents <= 1 ? $parent : '-c'),
5081                 $hash, "--"
5082                 or die_error(500, "Open git-diff-tree failed");
5083         @difftree = map { chomp; $_ } <$fd>;
5084         close $fd or die_error(404, "Reading git-diff-tree failed");
5086         # non-textual hash id's can be cached
5087         my $expires;
5088         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5089                 $expires = "+1d";
5090         }
5091         my $refs = git_get_references();
5092         my $ref = format_ref_marker($refs, $co{'id'});
5094         git_header_html(undef, $expires);
5095         git_print_page_nav('commit', '',
5096                            $hash, $co{'tree'}, $hash,
5097                            $formats_nav);
5099         if (defined $co{'parent'}) {
5100                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5101         } else {
5102                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5103         }
5104         print "<div class=\"title_text\">\n" .
5105               "<table class=\"object_header\">\n";
5106         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5107               "<tr>" .
5108               "<td></td><td> $ad{'rfc2822'}";
5109         if ($ad{'hour_local'} < 6) {
5110                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5111                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5112         } else {
5113                 printf(" (%02d:%02d %s)",
5114                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5115         }
5116         print "</td>" .
5117               "</tr>\n";
5118         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5119         print "<tr><td></td><td> $cd{'rfc2822'}" .
5120               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5121               "</td></tr>\n";
5122         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5123         print "<tr>" .
5124               "<td>tree</td>" .
5125               "<td class=\"sha1\">" .
5126               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5127                        class => "list"}, $co{'tree'}) .
5128               "</td>" .
5129               "<td class=\"link\">" .
5130               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5131                       "tree");
5132         my $snapshot_links = format_snapshot_links($hash);
5133         if (defined $snapshot_links) {
5134                 print " | " . $snapshot_links;
5135         }
5136         print "</td>" .
5137               "</tr>\n";
5139         foreach my $par (@$parents) {
5140                 print "<tr>" .
5141                       "<td>parent</td>" .
5142                       "<td class=\"sha1\">" .
5143                       $cgi->a({-href => href(action=>"commit", hash=>$par),
5144                                class => "list"}, $par) .
5145                       "</td>" .
5146                       "<td class=\"link\">" .
5147                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5148                       " | " .
5149                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5150                       "</td>" .
5151                       "</tr>\n";
5152         }
5153         print "</table>".
5154               "</div>\n";
5156         print "<div class=\"page_body\">\n";
5157         git_print_log($co{'comment'});
5158         print "</div>\n";
5160         git_difftree_body(\@difftree, $hash, @$parents);
5162         git_footer_html();
5165 sub git_object {
5166         # object is defined by:
5167         # - hash or hash_base alone
5168         # - hash_base and file_name
5169         my $type;
5171         # - hash or hash_base alone
5172         if ($hash || ($hash_base && !defined $file_name)) {
5173                 my $object_id = $hash || $hash_base;
5175                 open my $fd, "-|", quote_command(
5176                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5177                         or die_error(404, "Object does not exist");
5178                 $type = <$fd>;
5179                 chomp $type;
5180                 close $fd
5181                         or die_error(404, "Object does not exist");
5183         # - hash_base and file_name
5184         } elsif ($hash_base && defined $file_name) {
5185                 $file_name =~ s,/+$,,;
5187                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5188                         or die_error(404, "Base object does not exist");
5190                 # here errors should not hapen
5191                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5192                         or die_error(500, "Open git-ls-tree failed");
5193                 my $line = <$fd>;
5194                 close $fd;
5196                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5197                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5198                         die_error(404, "File or directory for given base does not exist");
5199                 }
5200                 $type = $2;
5201                 $hash = $3;
5202         } else {
5203                 die_error(400, "Not enough information to find object");
5204         }
5206         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5207                                           hash=>$hash, hash_base=>$hash_base,
5208                                           file_name=>$file_name),
5209                              -status => '302 Found');
5212 sub git_blobdiff {
5213         my $format = shift || 'html';
5215         my $fd;
5216         my @difftree;
5217         my %diffinfo;
5218         my $expires;
5220         # preparing $fd and %diffinfo for git_patchset_body
5221         # new style URI
5222         if (defined $hash_base && defined $hash_parent_base) {
5223                 if (defined $file_name) {
5224                         # read raw output
5225                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5226                                 $hash_parent_base, $hash_base,
5227                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
5228                                 or die_error(500, "Open git-diff-tree failed");
5229                         @difftree = map { chomp; $_ } <$fd>;
5230                         close $fd
5231                                 or die_error(404, "Reading git-diff-tree failed");
5232                         @difftree
5233                                 or die_error(404, "Blob diff not found");
5235                 } elsif (defined $hash &&
5236                          $hash =~ /[0-9a-fA-F]{40}/) {
5237                         # try to find filename from $hash
5239                         # read filtered raw output
5240                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5241                                 $hash_parent_base, $hash_base, "--"
5242                                 or die_error(500, "Open git-diff-tree failed");
5243                         @difftree =
5244                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5245                                 # $hash == to_id
5246                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5247                                 map { chomp; $_ } <$fd>;
5248                         close $fd
5249                                 or die_error(404, "Reading git-diff-tree failed");
5250                         @difftree
5251                                 or die_error(404, "Blob diff not found");
5253                 } else {
5254                         die_error(400, "Missing one of the blob diff parameters");
5255                 }
5257                 if (@difftree > 1) {
5258                         die_error(400, "Ambiguous blob diff specification");
5259                 }
5261                 %diffinfo = parse_difftree_raw_line($difftree[0]);
5262                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5263                 $file_name   ||= $diffinfo{'to_file'};
5265                 $hash_parent ||= $diffinfo{'from_id'};
5266                 $hash        ||= $diffinfo{'to_id'};
5268                 # non-textual hash id's can be cached
5269                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5270                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5271                         $expires = '+1d';
5272                 }
5274                 # open patch output
5275                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5276                         '-p', ($format eq 'html' ? "--full-index" : ()),
5277                         $hash_parent_base, $hash_base,
5278                         "--", (defined $file_parent ? $file_parent : ()), $file_name
5279                         or die_error(500, "Open git-diff-tree failed");
5280         }
5282         # old/legacy style URI -- not generated anymore since 1.4.3.
5283         if (!%diffinfo) {
5284                 die_error('404 Not Found', "Missing one of the blob diff parameters")
5285         }
5287         # header
5288         if ($format eq 'html') {
5289                 my $formats_nav =
5290                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5291                                 "raw");
5292                 git_header_html(undef, $expires);
5293                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5294                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5295                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5296                 } else {
5297                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5298                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5299                 }
5300                 if (defined $file_name) {
5301                         git_print_page_path($file_name, "blob", $hash_base);
5302                 } else {
5303                         print "<div class=\"page_path\"></div>\n";
5304                 }
5306         } elsif ($format eq 'plain') {
5307                 print $cgi->header(
5308                         -type => 'text/plain',
5309                         -charset => 'utf-8',
5310                         -expires => $expires,
5311                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5313                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5315         } else {
5316                 die_error(400, "Unknown blobdiff format");
5317         }
5319         # patch
5320         if ($format eq 'html') {
5321                 print "<div class=\"page_body\">\n";
5323                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5324                 close $fd;
5326                 print "</div>\n"; # class="page_body"
5327                 git_footer_html();
5329         } else {
5330                 while (my $line = <$fd>) {
5331                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5332                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5334                         print $line;
5336                         last if $line =~ m!^\+\+\+!;
5337                 }
5338                 local $/ = undef;
5339                 print <$fd>;
5340                 close $fd;
5341         }
5344 sub git_blobdiff_plain {
5345         git_blobdiff('plain');
5348 sub git_commitdiff {
5349         my $format = shift || 'html';
5350         $hash ||= $hash_base || "HEAD";
5351         my %co = parse_commit($hash)
5352             or die_error(404, "Unknown commit object");
5354         # choose format for commitdiff for merge
5355         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5356                 $hash_parent = '--cc';
5357         }
5358         # we need to prepare $formats_nav before almost any parameter munging
5359         my $formats_nav;
5360         if ($format eq 'html') {
5361                 $formats_nav =
5362                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5363                                 "raw");
5365                 if (defined $hash_parent &&
5366                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
5367                         # commitdiff with two commits given
5368                         my $hash_parent_short = $hash_parent;
5369                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5370                                 $hash_parent_short = substr($hash_parent, 0, 7);
5371                         }
5372                         $formats_nav .=
5373                                 ' (from';
5374                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5375                                 if ($co{'parents'}[$i] eq $hash_parent) {
5376                                         $formats_nav .= ' parent ' . ($i+1);
5377                                         last;
5378                                 }
5379                         }
5380                         $formats_nav .= ': ' .
5381                                 $cgi->a({-href => href(action=>"commitdiff",
5382                                                        hash=>$hash_parent)},
5383                                         esc_html($hash_parent_short)) .
5384                                 ')';
5385                 } elsif (!$co{'parent'}) {
5386                         # --root commitdiff
5387                         $formats_nav .= ' (initial)';
5388                 } elsif (scalar @{$co{'parents'}} == 1) {
5389                         # single parent commit
5390                         $formats_nav .=
5391                                 ' (parent: ' .
5392                                 $cgi->a({-href => href(action=>"commitdiff",
5393                                                        hash=>$co{'parent'})},
5394                                         esc_html(substr($co{'parent'}, 0, 7))) .
5395                                 ')';
5396                 } else {
5397                         # merge commit
5398                         if ($hash_parent eq '--cc') {
5399                                 $formats_nav .= ' | ' .
5400                                         $cgi->a({-href => href(action=>"commitdiff",
5401                                                                hash=>$hash, hash_parent=>'-c')},
5402                                                 'combined');
5403                         } else { # $hash_parent eq '-c'
5404                                 $formats_nav .= ' | ' .
5405                                         $cgi->a({-href => href(action=>"commitdiff",
5406                                                                hash=>$hash, hash_parent=>'--cc')},
5407                                                 'compact');
5408                         }
5409                         $formats_nav .=
5410                                 ' (merge: ' .
5411                                 join(' ', map {
5412                                         $cgi->a({-href => href(action=>"commitdiff",
5413                                                                hash=>$_)},
5414                                                 esc_html(substr($_, 0, 7)));
5415                                 } @{$co{'parents'}} ) .
5416                                 ')';
5417                 }
5418         }
5420         my $hash_parent_param = $hash_parent;
5421         if (!defined $hash_parent_param) {
5422                 # --cc for multiple parents, --root for parentless
5423                 $hash_parent_param =
5424                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5425         }
5427         # read commitdiff
5428         my $fd;
5429         my @difftree;
5430         if ($format eq 'html') {
5431                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5432                         "--no-commit-id", "--patch-with-raw", "--full-index",
5433                         $hash_parent_param, $hash, "--"
5434                         or die_error(500, "Open git-diff-tree failed");
5436                 while (my $line = <$fd>) {
5437                         chomp $line;
5438                         # empty line ends raw part of diff-tree output
5439                         last unless $line;
5440                         push @difftree, scalar parse_difftree_raw_line($line);
5441                 }
5443         } elsif ($format eq 'plain') {
5444                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5445                         '-p', $hash_parent_param, $hash, "--"
5446                         or die_error(500, "Open git-diff-tree failed");
5448         } else {
5449                 die_error(400, "Unknown commitdiff format");
5450         }
5452         # non-textual hash id's can be cached
5453         my $expires;
5454         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5455                 $expires = "+1d";
5456         }
5458         # write commit message
5459         if ($format eq 'html') {
5460                 my $refs = git_get_references();
5461                 my $ref = format_ref_marker($refs, $co{'id'});
5463                 git_header_html(undef, $expires);
5464                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5465                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5466                 git_print_authorship(\%co);
5467                 print "<div class=\"page_body\">\n";
5468                 if (@{$co{'comment'}} > 1) {
5469                         print "<div class=\"log\">\n";
5470                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5471                         print "</div>\n"; # class="log"
5472                 }
5474         } elsif ($format eq 'plain') {
5475                 my $refs = git_get_references("tags");
5476                 my $tagname = git_get_rev_name_tags($hash);
5477                 my $filename = basename($project) . "-$hash.patch";
5479                 print $cgi->header(
5480                         -type => 'text/plain',
5481                         -charset => 'utf-8',
5482                         -expires => $expires,
5483                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5484                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5485                 print "From: " . to_utf8($co{'author'}) . "\n";
5486                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5487                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5489                 print "X-Git-Tag: $tagname\n" if $tagname;
5490                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5492                 foreach my $line (@{$co{'comment'}}) {
5493                         print to_utf8($line) . "\n";
5494                 }
5495                 print "---\n\n";
5496         }
5498         # write patch
5499         if ($format eq 'html') {
5500                 my $use_parents = !defined $hash_parent ||
5501                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5502                 git_difftree_body(\@difftree, $hash,
5503                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5504                 print "<br/>\n";
5506                 git_patchset_body($fd, \@difftree, $hash,
5507                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5508                 close $fd;
5509                 print "</div>\n"; # class="page_body"
5510                 git_footer_html();
5512         } elsif ($format eq 'plain') {
5513                 local $/ = undef;
5514                 print <$fd>;
5515                 close $fd
5516                         or print "Reading git-diff-tree failed\n";
5517         }
5520 sub git_commitdiff_plain {
5521         git_commitdiff('plain');
5524 sub git_history {
5525         if (!defined $hash_base) {
5526                 $hash_base = git_get_head_hash($project);
5527         }
5528         if (!defined $page) {
5529                 $page = 0;
5530         }
5531         my $ftype;
5532         my %co = parse_commit($hash_base)
5533             or die_error(404, "Unknown commit object");
5535         my $refs = git_get_references();
5536         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5538         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5539                                        $file_name, "--full-history")
5540             or die_error(404, "No such file or directory on given branch");
5542         if (!defined $hash && defined $file_name) {
5543                 # some commits could have deleted file in question,
5544                 # and not have it in tree, but one of them has to have it
5545                 for (my $i = 0; $i <= @commitlist; $i++) {
5546                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5547                         last if defined $hash;
5548                 }
5549         }
5550         if (defined $hash) {
5551                 $ftype = git_get_type($hash);
5552         }
5553         if (!defined $ftype) {
5554                 die_error(500, "Unknown type of object");
5555         }
5557         my $paging_nav = '';
5558         if ($page > 0) {
5559                 $paging_nav .=
5560                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5561                                                file_name=>$file_name)},
5562                                 "first");
5563                 $paging_nav .= " &sdot; " .
5564                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5565                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5566         } else {
5567                 $paging_nav .= "first";
5568                 $paging_nav .= " &sdot; prev";
5569         }
5570         my $next_link = '';
5571         if ($#commitlist >= 100) {
5572                 $next_link =
5573                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5574                                  -accesskey => "n", -title => "Alt-n"}, "next");
5575                 $paging_nav .= " &sdot; $next_link";
5576         } else {
5577                 $paging_nav .= " &sdot; next";
5578         }
5580         git_header_html();
5581         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5582         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5583         git_print_page_path($file_name, $ftype, $hash_base);
5585         git_history_body(\@commitlist, 0, 99,
5586                          $refs, $hash_base, $ftype, $next_link);
5588         git_footer_html();
5591 sub git_search {
5592         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5593         if (!defined $searchtext) {
5594                 die_error(400, "Text field is empty");
5595         }
5596         if (!defined $hash) {
5597                 $hash = git_get_head_hash($project);
5598         }
5599         my %co = parse_commit($hash);
5600         if (!%co) {
5601                 die_error(404, "Unknown commit object");
5602         }
5603         if (!defined $page) {
5604                 $page = 0;
5605         }
5607         $searchtype ||= 'commit';
5608         if ($searchtype eq 'pickaxe') {
5609                 # pickaxe may take all resources of your box and run for several minutes
5610                 # with every query - so decide by yourself how public you make this feature
5611                 gitweb_check_feature('pickaxe')
5612                     or die_error(403, "Pickaxe is disabled");
5613         }
5614         if ($searchtype eq 'grep') {
5615                 gitweb_check_feature('grep')
5616                     or die_error(403, "Grep is disabled");
5617         }
5619         git_header_html();
5621         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5622                 my $greptype;
5623                 if ($searchtype eq 'commit') {
5624                         $greptype = "--grep=";
5625                 } elsif ($searchtype eq 'author') {
5626                         $greptype = "--author=";
5627                 } elsif ($searchtype eq 'committer') {
5628                         $greptype = "--committer=";
5629                 }
5630                 $greptype .= $searchtext;
5631                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5632                                                $greptype, '--regexp-ignore-case',
5633                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5635                 my $paging_nav = '';
5636                 if ($page > 0) {
5637                         $paging_nav .=
5638                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5639                                                        searchtext=>$searchtext,
5640                                                        searchtype=>$searchtype)},
5641                                         "first");
5642                         $paging_nav .= " &sdot; " .
5643                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5644                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5645                 } else {
5646                         $paging_nav .= "first";
5647                         $paging_nav .= " &sdot; prev";
5648                 }
5649                 my $next_link = '';
5650                 if ($#commitlist >= 100) {
5651                         $next_link =
5652                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5653                                          -accesskey => "n", -title => "Alt-n"}, "next");
5654                         $paging_nav .= " &sdot; $next_link";
5655                 } else {
5656                         $paging_nav .= " &sdot; next";
5657                 }
5659                 if ($#commitlist >= 100) {
5660                 }
5662                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5663                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5664                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5665         }
5667         if ($searchtype eq 'pickaxe') {
5668                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5669                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5671                 print "<table class=\"pickaxe search\">\n";
5672                 my $alternate = 1;
5673                 $/ = "\n";
5674                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5675                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5676                         ($search_use_regexp ? '--pickaxe-regex' : ());
5677                 undef %co;
5678                 my @files;
5679                 while (my $line = <$fd>) {
5680                         chomp $line;
5681                         next unless $line;
5683                         my %set = parse_difftree_raw_line($line);
5684                         if (defined $set{'commit'}) {
5685                                 # finish previous commit
5686                                 if (%co) {
5687                                         print "</td>\n" .
5688                                               "<td class=\"link\">" .
5689                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5690                                               " | " .
5691                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5692                                         print "</td>\n" .
5693                                               "</tr>\n";
5694                                 }
5696                                 if ($alternate) {
5697                                         print "<tr class=\"dark\">\n";
5698                                 } else {
5699                                         print "<tr class=\"light\">\n";
5700                                 }
5701                                 $alternate ^= 1;
5702                                 %co = parse_commit($set{'commit'});
5703                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5704                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5705                                       "<td><i>$author</i></td>\n" .
5706                                       "<td>" .
5707                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5708                                               -class => "list subject"},
5709                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5710                         } elsif (defined $set{'to_id'}) {
5711                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5713                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5714                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5715                                               -class => "list"},
5716                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5717                                       "<br/>\n";
5718                         }
5719                 }
5720                 close $fd;
5722                 # finish last commit (warning: repetition!)
5723                 if (%co) {
5724                         print "</td>\n" .
5725                               "<td class=\"link\">" .
5726                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5727                               " | " .
5728                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5729                         print "</td>\n" .
5730                               "</tr>\n";
5731                 }
5733                 print "</table>\n";
5734         }
5736         if ($searchtype eq 'grep') {
5737                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5738                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5740                 print "<table class=\"grep_search\">\n";
5741                 my $alternate = 1;
5742                 my $matches = 0;
5743                 $/ = "\n";
5744                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5745                         $search_use_regexp ? ('-E', '-i') : '-F',
5746                         $searchtext, $co{'tree'};
5747                 my $lastfile = '';
5748                 while (my $line = <$fd>) {
5749                         chomp $line;
5750                         my ($file, $lno, $ltext, $binary);
5751                         last if ($matches++ > 1000);
5752                         if ($line =~ /^Binary file (.+) matches$/) {
5753                                 $file = $1;
5754                                 $binary = 1;
5755                         } else {
5756                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5757                         }
5758                         if ($file ne $lastfile) {
5759                                 $lastfile and print "</td></tr>\n";
5760                                 if ($alternate++) {
5761                                         print "<tr class=\"dark\">\n";
5762                                 } else {
5763                                         print "<tr class=\"light\">\n";
5764                                 }
5765                                 print "<td class=\"list\">".
5766                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5767                                                                file_name=>"$file"),
5768                                                 -class => "list"}, esc_path($file));
5769                                 print "</td><td>\n";
5770                                 $lastfile = $file;
5771                         }
5772                         if ($binary) {
5773                                 print "<div class=\"binary\">Binary file</div>\n";
5774                         } else {
5775                                 $ltext = untabify($ltext);
5776                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5777                                         $ltext = esc_html($1, -nbsp=>1);
5778                                         $ltext .= '<span class="match">';
5779                                         $ltext .= esc_html($2, -nbsp=>1);
5780                                         $ltext .= '</span>';
5781                                         $ltext .= esc_html($3, -nbsp=>1);
5782                                 } else {
5783                                         $ltext = esc_html($ltext, -nbsp=>1);
5784                                 }
5785                                 print "<div class=\"pre\">" .
5786                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5787                                                                file_name=>"$file").'#l'.$lno,
5788                                                 -class => "linenr"}, sprintf('%4i', $lno))
5789                                         . ' ' .  $ltext . "</div>\n";
5790                         }
5791                 }
5792                 if ($lastfile) {
5793                         print "</td></tr>\n";
5794                         if ($matches > 1000) {
5795                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5796                         }
5797                 } else {
5798                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5799                 }
5800                 close $fd;
5802                 print "</table>\n";
5803         }
5804         git_footer_html();
5807 sub git_search_help {
5808         git_header_html();
5809         git_print_page_nav('','', $hash,$hash,$hash);
5810         print <<EOT;
5811 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5812 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5813 the pattern entered is recognized as the POSIX extended
5814 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5815 insensitive).</p>
5816 <dl>
5817 <dt><b>commit</b></dt>
5818 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5819 EOT
5820         my $have_grep = gitweb_check_feature('grep');
5821         if ($have_grep) {
5822                 print <<EOT;
5823 <dt><b>grep</b></dt>
5824 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5825     a different one) are searched for the given pattern. On large trees, this search can take
5826 a while and put some strain on the server, so please use it with some consideration. Note that
5827 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5828 case-sensitive.</dd>
5829 EOT
5830         }
5831         print <<EOT;
5832 <dt><b>author</b></dt>
5833 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5834 <dt><b>committer</b></dt>
5835 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5836 EOT
5837         my $have_pickaxe = gitweb_check_feature('pickaxe');
5838         if ($have_pickaxe) {
5839                 print <<EOT;
5840 <dt><b>pickaxe</b></dt>
5841 <dd>All commits that caused the string to appear or disappear from any file (changes that
5842 added, removed or "modified" the string) will be listed. This search can take a while and
5843 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5844 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5845 EOT
5846         }
5847         print "</dl>\n";
5848         git_footer_html();
5851 sub git_shortlog {
5852         my $head = git_get_head_hash($project);
5853         if (!defined $hash) {
5854                 $hash = $head;
5855         }
5856         if (!defined $page) {
5857                 $page = 0;
5858         }
5859         my $refs = git_get_references();
5861         my $commit_hash = $hash;
5862         if (defined $hash_parent) {
5863                 $commit_hash = "$hash_parent..$hash";
5864         }
5865         my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5867         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5868         my $next_link = '';
5869         if ($#commitlist >= 100) {
5870                 $next_link =
5871                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5872                                  -accesskey => "n", -title => "Alt-n"}, "next");
5873         }
5875         git_header_html();
5876         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5877         git_print_header_div('summary', $project);
5879         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5881         git_footer_html();
5884 ## ......................................................................
5885 ## feeds (RSS, Atom; OPML)
5887 sub git_feed {
5888         my $format = shift || 'atom';
5889         my $have_blame = gitweb_check_feature('blame');
5891         # Atom: http://www.atomenabled.org/developers/syndication/
5892         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5893         if ($format ne 'rss' && $format ne 'atom') {
5894                 die_error(400, "Unknown web feed format");
5895         }
5897         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5898         my $head = $hash || 'HEAD';
5899         my @commitlist = parse_commits($head, 150, 0, $file_name);
5901         my %latest_commit;
5902         my %latest_date;
5903         my $content_type = "application/$format+xml";
5904         if (defined $cgi->http('HTTP_ACCEPT') &&
5905                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5906                 # browser (feed reader) prefers text/xml
5907                 $content_type = 'text/xml';
5908         }
5909         if (defined($commitlist[0])) {
5910                 %latest_commit = %{$commitlist[0]};
5911                 %latest_date   = parse_date($latest_commit{'author_epoch'});
5912                 print $cgi->header(
5913                         -type => $content_type,
5914                         -charset => 'utf-8',
5915                         -last_modified => $latest_date{'rfc2822'});
5916         } else {
5917                 print $cgi->header(
5918                         -type => $content_type,
5919                         -charset => 'utf-8');
5920         }
5922         # Optimization: skip generating the body if client asks only
5923         # for Last-Modified date.
5924         return if ($cgi->request_method() eq 'HEAD');
5926         # header variables
5927         my $title = "$site_name - $project/$action";
5928         my $feed_type = 'log';
5929         if (defined $hash) {
5930                 $title .= " - '$hash'";
5931                 $feed_type = 'branch log';
5932                 if (defined $file_name) {
5933                         $title .= " :: $file_name";
5934                         $feed_type = 'history';
5935                 }
5936         } elsif (defined $file_name) {
5937                 $title .= " - $file_name";
5938                 $feed_type = 'history';
5939         }
5940         $title .= " $feed_type";
5941         my $descr = git_get_project_description($project);
5942         if (defined $descr) {
5943                 $descr = esc_html($descr);
5944         } else {
5945                 $descr = "$project " .
5946                          ($format eq 'rss' ? 'RSS' : 'Atom') .
5947                          " feed";
5948         }
5949         my $owner = git_get_project_owner($project);
5950         $owner = esc_html($owner);
5952         #header
5953         my $alt_url;
5954         if (defined $file_name) {
5955                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5956         } elsif (defined $hash) {
5957                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5958         } else {
5959                 $alt_url = href(-full=>1, action=>"summary");
5960         }
5961         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5962         if ($format eq 'rss') {
5963                 print <<XML;
5964 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5965 <channel>
5966 XML
5967                 print "<title>$title</title>\n" .
5968                       "<link>$alt_url</link>\n" .
5969                       "<description>$descr</description>\n" .
5970                       "<language>en</language>\n";
5971         } elsif ($format eq 'atom') {
5972                 print <<XML;
5973 <feed xmlns="http://www.w3.org/2005/Atom">
5974 XML
5975                 print "<title>$title</title>\n" .
5976                       "<subtitle>$descr</subtitle>\n" .
5977                       '<link rel="alternate" type="text/html" href="' .
5978                       $alt_url . '" />' . "\n" .
5979                       '<link rel="self" type="' . $content_type . '" href="' .
5980                       $cgi->self_url() . '" />' . "\n" .
5981                       "<id>" . href(-full=>1) . "</id>\n" .
5982                       # use project owner for feed author
5983                       "<author><name>$owner</name></author>\n";
5984                 if (defined $favicon) {
5985                         print "<icon>" . esc_url($favicon) . "</icon>\n";
5986                 }
5987                 if (defined $logo_url) {
5988                         # not twice as wide as tall: 72 x 27 pixels
5989                         print "<logo>" . esc_url($logo) . "</logo>\n";
5990                 }
5991                 if (! %latest_date) {
5992                         # dummy date to keep the feed valid until commits trickle in:
5993                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
5994                 } else {
5995                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
5996                 }
5997         }
5999         # contents
6000         for (my $i = 0; $i <= $#commitlist; $i++) {
6001                 my %co = %{$commitlist[$i]};
6002                 my $commit = $co{'id'};
6003                 # we read 150, we always show 30 and the ones more recent than 48 hours
6004                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6005                         last;
6006                 }
6007                 my %cd = parse_date($co{'author_epoch'});
6009                 # get list of changed files
6010                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6011                         $co{'parent'} || "--root",
6012                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
6013                         or next;
6014                 my @difftree = map { chomp; $_ } <$fd>;
6015                 close $fd
6016                         or next;
6018                 # print element (entry, item)
6019                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6020                 if ($format eq 'rss') {
6021                         print "<item>\n" .
6022                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
6023                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
6024                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6025                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6026                               "<link>$co_url</link>\n" .
6027                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
6028                               "<content:encoded>" .
6029                               "<![CDATA[\n";
6030                 } elsif ($format eq 'atom') {
6031                         print "<entry>\n" .
6032                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6033                               "<updated>$cd{'iso-8601'}</updated>\n" .
6034                               "<author>\n" .
6035                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6036                         if ($co{'author_email'}) {
6037                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6038                         }
6039                         print "</author>\n" .
6040                               # use committer for contributor
6041                               "<contributor>\n" .
6042                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6043                         if ($co{'committer_email'}) {
6044                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6045                         }
6046                         print "</contributor>\n" .
6047                               "<published>$cd{'iso-8601'}</published>\n" .
6048                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6049                               "<id>$co_url</id>\n" .
6050                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6051                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6052                 }
6053                 my $comment = $co{'comment'};
6054                 print "<pre>\n";
6055                 foreach my $line (@$comment) {
6056                         $line = esc_html($line);
6057                         print "$line\n";
6058                 }
6059                 print "</pre><ul>\n";
6060                 foreach my $difftree_line (@difftree) {
6061                         my %difftree = parse_difftree_raw_line($difftree_line);
6062                         next if !$difftree{'from_id'};
6064                         my $file = $difftree{'file'} || $difftree{'to_file'};
6066                         print "<li>" .
6067                               "[" .
6068                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6069                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6070                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6071                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
6072                                       -title => "diff"}, 'D');
6073                         if ($have_blame) {
6074                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
6075                                                              file_name=>$file, hash_base=>$commit),
6076                                               -title => "blame"}, 'B');
6077                         }
6078                         # if this is not a feed of a file history
6079                         if (!defined $file_name || $file_name ne $file) {
6080                                 print $cgi->a({-href => href(-full=>1, action=>"history",
6081                                                              file_name=>$file, hash=>$commit),
6082                                               -title => "history"}, 'H');
6083                         }
6084                         $file = esc_path($file);
6085                         print "] ".
6086                               "$file</li>\n";
6087                 }
6088                 if ($format eq 'rss') {
6089                         print "</ul>]]>\n" .
6090                               "</content:encoded>\n" .
6091                               "</item>\n";
6092                 } elsif ($format eq 'atom') {
6093                         print "</ul>\n</div>\n" .
6094                               "</content>\n" .
6095                               "</entry>\n";
6096                 }
6097         }
6099         # end of feed
6100         if ($format eq 'rss') {
6101                 print "</channel>\n</rss>\n";
6102         }       elsif ($format eq 'atom') {
6103                 print "</feed>\n";
6104         }
6107 sub git_rss {
6108         git_feed('rss');
6111 sub git_atom {
6112         git_feed('atom');
6115 sub git_opml {
6116         my @list = git_get_projects_list();
6118         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
6119         print <<XML;
6120 <?xml version="1.0" encoding="utf-8"?>
6121 <opml version="1.0">
6122 <head>
6123   <title>$site_name OPML Export</title>
6124 </head>
6125 <body>
6126 <outline text="git RSS feeds">
6127 XML
6129         foreach my $pr (@list) {
6130                 my %proj = %$pr;
6131                 my $head = git_get_head_hash($proj{'path'});
6132                 if (!defined $head) {
6133                         next;
6134                 }
6135                 $git_dir = "$projectroot/$proj{'path'}";
6136                 my %co = parse_commit($head);
6137                 if (!%co) {
6138                         next;
6139                 }
6141                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6142                 my $rss  = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6143                 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6144                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6145         }
6146         print <<XML;
6147 </outline>
6148 </body>
6149 </opml>
6150 XML