Code

4b28136ba7dc2c46779c79879b6181fba13a03cc
[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' => \&feature_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                 'override' => 0,
245                 'default' => [1]},
247         # Enable the pickaxe search, which will list the commits that modified
248         # a given string in a file. This can be practical and quite faster
249         # alternative to 'blame', but still potentially CPU-intensive.
251         # To enable system wide have in $GITWEB_CONFIG
252         # $feature{'pickaxe'}{'default'} = [1];
253         # To have project specific config enable override in $GITWEB_CONFIG
254         # $feature{'pickaxe'}{'override'} = 1;
255         # and in project config gitweb.pickaxe = 0|1;
256         'pickaxe' => {
257                 'sub' => \&feature_pickaxe,
258                 'override' => 0,
259                 'default' => [1]},
261         # Make gitweb use an alternative format of the URLs which can be
262         # more readable and natural-looking: project name is embedded
263         # directly in the path and the query string contains other
264         # auxiliary information. All gitweb installations recognize
265         # URL in either format; this configures in which formats gitweb
266         # generates links.
268         # To enable system wide have in $GITWEB_CONFIG
269         # $feature{'pathinfo'}{'default'} = [1];
270         # Project specific override is not supported.
272         # Note that you will need to change the default location of CSS,
273         # favicon, logo and possibly other files to an absolute URL. Also,
274         # if gitweb.cgi serves as your indexfile, you will need to force
275         # $my_uri to contain the script name in your $GITWEB_CONFIG.
276         'pathinfo' => {
277                 'override' => 0,
278                 'default' => [0]},
280         # Make gitweb consider projects in project root subdirectories
281         # to be forks of existing projects. Given project $projname.git,
282         # projects matching $projname/*.git will not be shown in the main
283         # projects list, instead a '+' mark will be added to $projname
284         # there and a 'forks' view will be enabled for the project, listing
285         # all the forks. If project list is taken from a file, forks have
286         # to be listed after the main project.
288         # To enable system wide have in $GITWEB_CONFIG
289         # $feature{'forks'}{'default'} = [1];
290         # Project specific override is not supported.
291         'forks' => {
292                 'override' => 0,
293                 'default' => [0]},
295         # Insert custom links to the action bar of all project pages.
296         # This enables you mainly to link to third-party scripts integrating
297         # into gitweb; e.g. git-browser for graphical history representation
298         # or custom web-based repository administration interface.
300         # The 'default' value consists of a list of triplets in the form
301         # (label, link, position) where position is the label after which
302         # to insert the link and link is a format string where %n expands
303         # to the project name, %f to the project path within the filesystem,
304         # %h to the current hash (h gitweb parameter) and %b to the current
305         # hash base (hb gitweb parameter); %% expands to %.
307         # To enable system wide have in $GITWEB_CONFIG e.g.
308         # $feature{'actions'}{'default'} = [('graphiclog',
309         #       '/git-browser/by-commit.html?r=%n', 'summary')];
310         # Project specific override is not supported.
311         'actions' => {
312                 'override' => 0,
313                 'default' => []},
315         # Allow gitweb scan project content tags described in ctags/
316         # of project repository, and display the popular Web 2.0-ish
317         # "tag cloud" near the project list. Note that this is something
318         # COMPLETELY different from the normal Git tags.
320         # gitweb by itself can show existing tags, but it does not handle
321         # tagging itself; you need an external application for that.
322         # For an example script, check Girocco's cgi/tagproj.cgi.
323         # You may want to install the HTML::TagCloud Perl module to get
324         # a pretty tag cloud instead of just a list of tags.
326         # To enable system wide have in $GITWEB_CONFIG
327         # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
328         # Project specific override is not supported.
329         'ctags' => {
330                 'override' => 0,
331                 'default' => [0]},
333         # The maximum number of patches in a patchset generated in patch
334         # view. Set this to 0 or undef to disable patch view, or to a
335         # negative number to remove any limit.
337         # To disable system wide have in $GITWEB_CONFIG
338         # $feature{'patches'}{'default'} = [0];
339         # To have project specific config enable override in $GITWEB_CONFIG
340         # $feature{'patches'}{'override'} = 1;
341         # and in project config gitweb.patches = 0|n;
342         # where n is the maximum number of patches allowed in a patchset.
343         'patches' => {
344                 'sub' => \&feature_patches,
345                 'override' => 0,
346                 'default' => [16]},
347 );
349 sub gitweb_get_feature {
350         my ($name) = @_;
351         return unless exists $feature{$name};
352         my ($sub, $override, @defaults) = (
353                 $feature{$name}{'sub'},
354                 $feature{$name}{'override'},
355                 @{$feature{$name}{'default'}});
356         if (!$override) { return @defaults; }
357         if (!defined $sub) {
358                 warn "feature $name is not overrideable";
359                 return @defaults;
360         }
361         return $sub->(@defaults);
364 # A wrapper to check if a given feature is enabled.
365 # With this, you can say
367 #   my $bool_feat = gitweb_check_feature('bool_feat');
368 #   gitweb_check_feature('bool_feat') or somecode;
370 # instead of
372 #   my ($bool_feat) = gitweb_get_feature('bool_feat');
373 #   (gitweb_get_feature('bool_feat'))[0] or somecode;
375 sub gitweb_check_feature {
376         return (gitweb_get_feature(@_))[0];
380 sub feature_blame {
381         my ($val) = git_get_project_config('blame', '--bool');
383         if ($val eq 'true') {
384                 return 1;
385         } elsif ($val eq 'false') {
386                 return 0;
387         }
389         return $_[0];
392 sub feature_snapshot {
393         my (@fmts) = @_;
395         my ($val) = git_get_project_config('snapshot');
397         if ($val) {
398                 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
399         }
401         return @fmts;
404 sub feature_grep {
405         my ($val) = git_get_project_config('grep', '--bool');
407         if ($val eq 'true') {
408                 return (1);
409         } elsif ($val eq 'false') {
410                 return (0);
411         }
413         return ($_[0]);
416 sub feature_pickaxe {
417         my ($val) = git_get_project_config('pickaxe', '--bool');
419         if ($val eq 'true') {
420                 return (1);
421         } elsif ($val eq 'false') {
422                 return (0);
423         }
425         return ($_[0]);
428 sub feature_patches {
429         my @val = (git_get_project_config('patches', '--int'));
431         if (@val) {
432                 return @val;
433         }
435         return ($_[0]);
438 # checking HEAD file with -e is fragile if the repository was
439 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
440 # and then pruned.
441 sub check_head_link {
442         my ($dir) = @_;
443         my $headfile = "$dir/HEAD";
444         return ((-e $headfile) ||
445                 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
448 sub check_export_ok {
449         my ($dir) = @_;
450         return (check_head_link($dir) &&
451                 (!$export_ok || -e "$dir/$export_ok") &&
452                 (!$export_auth_hook || $export_auth_hook->($dir)));
455 # process alternate names for backward compatibility
456 # filter out unsupported (unknown) snapshot formats
457 sub filter_snapshot_fmts {
458         my @fmts = @_;
460         @fmts = map {
461                 exists $known_snapshot_format_aliases{$_} ?
462                        $known_snapshot_format_aliases{$_} : $_} @fmts;
463         @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
467 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
468 if (-e $GITWEB_CONFIG) {
469         do $GITWEB_CONFIG;
470 } else {
471         our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
472         do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
475 # version of the core git binary
476 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
478 $projects_list ||= $projectroot;
480 # ======================================================================
481 # input validation and dispatch
483 # input parameters can be collected from a variety of sources (presently, CGI
484 # and PATH_INFO), so we define an %input_params hash that collects them all
485 # together during validation: this allows subsequent uses (e.g. href()) to be
486 # agnostic of the parameter origin
488 our %input_params = ();
490 # input parameters are stored with the long parameter name as key. This will
491 # also be used in the href subroutine to convert parameters to their CGI
492 # equivalent, and since the href() usage is the most frequent one, we store
493 # the name -> CGI key mapping here, instead of the reverse.
495 # XXX: Warning: If you touch this, check the search form for updating,
496 # too.
498 our @cgi_param_mapping = (
499         project => "p",
500         action => "a",
501         file_name => "f",
502         file_parent => "fp",
503         hash => "h",
504         hash_parent => "hp",
505         hash_base => "hb",
506         hash_parent_base => "hpb",
507         page => "pg",
508         order => "o",
509         searchtext => "s",
510         searchtype => "st",
511         snapshot_format => "sf",
512         extra_options => "opt",
513         search_use_regexp => "sr",
514 );
515 our %cgi_param_mapping = @cgi_param_mapping;
517 # we will also need to know the possible actions, for validation
518 our %actions = (
519         "blame" => \&git_blame,
520         "blobdiff" => \&git_blobdiff,
521         "blobdiff_plain" => \&git_blobdiff_plain,
522         "blob" => \&git_blob,
523         "blob_plain" => \&git_blob_plain,
524         "commitdiff" => \&git_commitdiff,
525         "commitdiff_plain" => \&git_commitdiff_plain,
526         "commit" => \&git_commit,
527         "forks" => \&git_forks,
528         "heads" => \&git_heads,
529         "history" => \&git_history,
530         "log" => \&git_log,
531         "patch" => \&git_patch,
532         "patches" => \&git_patches,
533         "rss" => \&git_rss,
534         "atom" => \&git_atom,
535         "search" => \&git_search,
536         "search_help" => \&git_search_help,
537         "shortlog" => \&git_shortlog,
538         "summary" => \&git_summary,
539         "tag" => \&git_tag,
540         "tags" => \&git_tags,
541         "tree" => \&git_tree,
542         "snapshot" => \&git_snapshot,
543         "object" => \&git_object,
544         # those below don't need $project
545         "opml" => \&git_opml,
546         "project_list" => \&git_project_list,
547         "project_index" => \&git_project_index,
548 );
550 # finally, we have the hash of allowed extra_options for the commands that
551 # allow them
552 our %allowed_options = (
553         "--no-merges" => [ qw(rss atom log shortlog history) ],
554 );
556 # fill %input_params with the CGI parameters. All values except for 'opt'
557 # should be single values, but opt can be an array. We should probably
558 # build an array of parameters that can be multi-valued, but since for the time
559 # being it's only this one, we just single it out
560 while (my ($name, $symbol) = each %cgi_param_mapping) {
561         if ($symbol eq 'opt') {
562                 $input_params{$name} = [ $cgi->param($symbol) ];
563         } else {
564                 $input_params{$name} = $cgi->param($symbol);
565         }
568 # now read PATH_INFO and update the parameter list for missing parameters
569 sub evaluate_path_info {
570         return if defined $input_params{'project'};
571         return if !$path_info;
572         $path_info =~ s,^/+,,;
573         return if !$path_info;
575         # find which part of PATH_INFO is project
576         my $project = $path_info;
577         $project =~ s,/+$,,;
578         while ($project && !check_head_link("$projectroot/$project")) {
579                 $project =~ s,/*[^/]*$,,;
580         }
581         return unless $project;
582         $input_params{'project'} = $project;
584         # do not change any parameters if an action is given using the query string
585         return if $input_params{'action'};
586         $path_info =~ s,^\Q$project\E/*,,;
588         # next, check if we have an action
589         my $action = $path_info;
590         $action =~ s,/.*$,,;
591         if (exists $actions{$action}) {
592                 $path_info =~ s,^$action/*,,;
593                 $input_params{'action'} = $action;
594         }
596         # list of actions that want hash_base instead of hash, but can have no
597         # pathname (f) parameter
598         my @wants_base = (
599                 'tree',
600                 'history',
601         );
603         # we want to catch
604         # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
605         my ($parentrefname, $parentpathname, $refname, $pathname) =
606                 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
608         # first, analyze the 'current' part
609         if (defined $pathname) {
610                 # we got "branch:filename" or "branch:dir/"
611                 # we could use git_get_type(branch:pathname), but:
612                 # - it needs $git_dir
613                 # - it does a git() call
614                 # - the convention of terminating directories with a slash
615                 #   makes it superfluous
616                 # - embedding the action in the PATH_INFO would make it even
617                 #   more superfluous
618                 $pathname =~ s,^/+,,;
619                 if (!$pathname || substr($pathname, -1) eq "/") {
620                         $input_params{'action'} ||= "tree";
621                         $pathname =~ s,/$,,;
622                 } else {
623                         # the default action depends on whether we had parent info
624                         # or not
625                         if ($parentrefname) {
626                                 $input_params{'action'} ||= "blobdiff_plain";
627                         } else {
628                                 $input_params{'action'} ||= "blob_plain";
629                         }
630                 }
631                 $input_params{'hash_base'} ||= $refname;
632                 $input_params{'file_name'} ||= $pathname;
633         } elsif (defined $refname) {
634                 # we got "branch". In this case we have to choose if we have to
635                 # set hash or hash_base.
636                 #
637                 # Most of the actions without a pathname only want hash to be
638                 # set, except for the ones specified in @wants_base that want
639                 # hash_base instead. It should also be noted that hand-crafted
640                 # links having 'history' as an action and no pathname or hash
641                 # set will fail, but that happens regardless of PATH_INFO.
642                 $input_params{'action'} ||= "shortlog";
643                 if (grep { $_ eq $input_params{'action'} } @wants_base) {
644                         $input_params{'hash_base'} ||= $refname;
645                 } else {
646                         $input_params{'hash'} ||= $refname;
647                 }
648         }
650         # next, handle the 'parent' part, if present
651         if (defined $parentrefname) {
652                 # a missing pathspec defaults to the 'current' filename, allowing e.g.
653                 # someproject/blobdiff/oldrev..newrev:/filename
654                 if ($parentpathname) {
655                         $parentpathname =~ s,^/+,,;
656                         $parentpathname =~ s,/$,,;
657                         $input_params{'file_parent'} ||= $parentpathname;
658                 } else {
659                         $input_params{'file_parent'} ||= $input_params{'file_name'};
660                 }
661                 # we assume that hash_parent_base is wanted if a path was specified,
662                 # or if the action wants hash_base instead of hash
663                 if (defined $input_params{'file_parent'} ||
664                         grep { $_ eq $input_params{'action'} } @wants_base) {
665                         $input_params{'hash_parent_base'} ||= $parentrefname;
666                 } else {
667                         $input_params{'hash_parent'} ||= $parentrefname;
668                 }
669         }
671         # for the snapshot action, we allow URLs in the form
672         # $project/snapshot/$hash.ext
673         # where .ext determines the snapshot and gets removed from the
674         # passed $refname to provide the $hash.
675         #
676         # To be able to tell that $refname includes the format extension, we
677         # require the following two conditions to be satisfied:
678         # - the hash input parameter MUST have been set from the $refname part
679         #   of the URL (i.e. they must be equal)
680         # - the snapshot format MUST NOT have been defined already (e.g. from
681         #   CGI parameter sf)
682         # It's also useless to try any matching unless $refname has a dot,
683         # so we check for that too
684         if (defined $input_params{'action'} &&
685                 $input_params{'action'} eq 'snapshot' &&
686                 defined $refname && index($refname, '.') != -1 &&
687                 $refname eq $input_params{'hash'} &&
688                 !defined $input_params{'snapshot_format'}) {
689                 # We loop over the known snapshot formats, checking for
690                 # extensions. Allowed extensions are both the defined suffix
691                 # (which includes the initial dot already) and the snapshot
692                 # format key itself, with a prepended dot
693                 while (my ($fmt, %opt) = each %known_snapshot_formats) {
694                         my $hash = $refname;
695                         my $sfx;
696                         $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//;
697                         next unless $sfx = $1;
698                         # a valid suffix was found, so set the snapshot format
699                         # and reset the hash parameter
700                         $input_params{'snapshot_format'} = $fmt;
701                         $input_params{'hash'} = $hash;
702                         # we also set the format suffix to the one requested
703                         # in the URL: this way a request for e.g. .tgz returns
704                         # a .tgz instead of a .tar.gz
705                         $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
706                         last;
707                 }
708         }
710 evaluate_path_info();
712 our $action = $input_params{'action'};
713 if (defined $action) {
714         if (!validate_action($action)) {
715                 die_error(400, "Invalid action parameter");
716         }
719 # parameters which are pathnames
720 our $project = $input_params{'project'};
721 if (defined $project) {
722         if (!validate_project($project)) {
723                 undef $project;
724                 die_error(404, "No such project");
725         }
728 our $file_name = $input_params{'file_name'};
729 if (defined $file_name) {
730         if (!validate_pathname($file_name)) {
731                 die_error(400, "Invalid file parameter");
732         }
735 our $file_parent = $input_params{'file_parent'};
736 if (defined $file_parent) {
737         if (!validate_pathname($file_parent)) {
738                 die_error(400, "Invalid file parent parameter");
739         }
742 # parameters which are refnames
743 our $hash = $input_params{'hash'};
744 if (defined $hash) {
745         if (!validate_refname($hash)) {
746                 die_error(400, "Invalid hash parameter");
747         }
750 our $hash_parent = $input_params{'hash_parent'};
751 if (defined $hash_parent) {
752         if (!validate_refname($hash_parent)) {
753                 die_error(400, "Invalid hash parent parameter");
754         }
757 our $hash_base = $input_params{'hash_base'};
758 if (defined $hash_base) {
759         if (!validate_refname($hash_base)) {
760                 die_error(400, "Invalid hash base parameter");
761         }
764 our @extra_options = @{$input_params{'extra_options'}};
765 # @extra_options is always defined, since it can only be (currently) set from
766 # CGI, and $cgi->param() returns the empty array in array context if the param
767 # is not set
768 foreach my $opt (@extra_options) {
769         if (not exists $allowed_options{$opt}) {
770                 die_error(400, "Invalid option parameter");
771         }
772         if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
773                 die_error(400, "Invalid option parameter for this action");
774         }
777 our $hash_parent_base = $input_params{'hash_parent_base'};
778 if (defined $hash_parent_base) {
779         if (!validate_refname($hash_parent_base)) {
780                 die_error(400, "Invalid hash parent base parameter");
781         }
784 # other parameters
785 our $page = $input_params{'page'};
786 if (defined $page) {
787         if ($page =~ m/[^0-9]/) {
788                 die_error(400, "Invalid page parameter");
789         }
792 our $searchtype = $input_params{'searchtype'};
793 if (defined $searchtype) {
794         if ($searchtype =~ m/[^a-z]/) {
795                 die_error(400, "Invalid searchtype parameter");
796         }
799 our $search_use_regexp = $input_params{'search_use_regexp'};
801 our $searchtext = $input_params{'searchtext'};
802 our $search_regexp;
803 if (defined $searchtext) {
804         if (length($searchtext) < 2) {
805                 die_error(403, "At least two characters are required for search parameter");
806         }
807         $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
810 # path to the current git repository
811 our $git_dir;
812 $git_dir = "$projectroot/$project" if $project;
814 # list of supported snapshot formats
815 our @snapshot_fmts = gitweb_get_feature('snapshot');
816 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
818 # dispatch
819 if (!defined $action) {
820         if (defined $hash) {
821                 $action = git_get_type($hash);
822         } elsif (defined $hash_base && defined $file_name) {
823                 $action = git_get_type("$hash_base:$file_name");
824         } elsif (defined $project) {
825                 $action = 'summary';
826         } else {
827                 $action = 'project_list';
828         }
830 if (!defined($actions{$action})) {
831         die_error(400, "Unknown action");
833 if ($action !~ m/^(opml|project_list|project_index)$/ &&
834     !$project) {
835         die_error(400, "Project needed");
837 $actions{$action}->();
838 exit;
840 ## ======================================================================
841 ## action links
843 sub href (%) {
844         my %params = @_;
845         # default is to use -absolute url() i.e. $my_uri
846         my $href = $params{-full} ? $my_url : $my_uri;
848         $params{'project'} = $project unless exists $params{'project'};
850         if ($params{-replay}) {
851                 while (my ($name, $symbol) = each %cgi_param_mapping) {
852                         if (!exists $params{$name}) {
853                                 $params{$name} = $input_params{$name};
854                         }
855                 }
856         }
858         my $use_pathinfo = gitweb_check_feature('pathinfo');
859         if ($use_pathinfo) {
860                 # try to put as many parameters as possible in PATH_INFO:
861                 #   - project name
862                 #   - action
863                 #   - hash_parent or hash_parent_base:/file_parent
864                 #   - hash or hash_base:/filename
865                 #   - the snapshot_format as an appropriate suffix
867                 # When the script is the root DirectoryIndex for the domain,
868                 # $href here would be something like http://gitweb.example.com/
869                 # Thus, we strip any trailing / from $href, to spare us double
870                 # slashes in the final URL
871                 $href =~ s,/$,,;
873                 # Then add the project name, if present
874                 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
875                 delete $params{'project'};
877                 # since we destructively absorb parameters, we keep this
878                 # boolean that remembers if we're handling a snapshot
879                 my $is_snapshot = $params{'action'} eq 'snapshot';
881                 # Summary just uses the project path URL, any other action is
882                 # added to the URL
883                 if (defined $params{'action'}) {
884                         $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
885                         delete $params{'action'};
886                 }
888                 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
889                 # stripping nonexistent or useless pieces
890                 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
891                         || $params{'hash_parent'} || $params{'hash'});
892                 if (defined $params{'hash_base'}) {
893                         if (defined $params{'hash_parent_base'}) {
894                                 $href .= esc_url($params{'hash_parent_base'});
895                                 # skip the file_parent if it's the same as the file_name
896                                 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
897                                 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
898                                         $href .= ":/".esc_url($params{'file_parent'});
899                                         delete $params{'file_parent'};
900                                 }
901                                 $href .= "..";
902                                 delete $params{'hash_parent'};
903                                 delete $params{'hash_parent_base'};
904                         } elsif (defined $params{'hash_parent'}) {
905                                 $href .= esc_url($params{'hash_parent'}). "..";
906                                 delete $params{'hash_parent'};
907                         }
909                         $href .= esc_url($params{'hash_base'});
910                         if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
911                                 $href .= ":/".esc_url($params{'file_name'});
912                                 delete $params{'file_name'};
913                         }
914                         delete $params{'hash'};
915                         delete $params{'hash_base'};
916                 } elsif (defined $params{'hash'}) {
917                         $href .= esc_url($params{'hash'});
918                         delete $params{'hash'};
919                 }
921                 # If the action was a snapshot, we can absorb the
922                 # snapshot_format parameter too
923                 if ($is_snapshot) {
924                         my $fmt = $params{'snapshot_format'};
925                         # snapshot_format should always be defined when href()
926                         # is called, but just in case some code forgets, we
927                         # fall back to the default
928                         $fmt ||= $snapshot_fmts[0];
929                         $href .= $known_snapshot_formats{$fmt}{'suffix'};
930                         delete $params{'snapshot_format'};
931                 }
932         }
934         # now encode the parameters explicitly
935         my @result = ();
936         for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
937                 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
938                 if (defined $params{$name}) {
939                         if (ref($params{$name}) eq "ARRAY") {
940                                 foreach my $par (@{$params{$name}}) {
941                                         push @result, $symbol . "=" . esc_param($par);
942                                 }
943                         } else {
944                                 push @result, $symbol . "=" . esc_param($params{$name});
945                         }
946                 }
947         }
948         $href .= "?" . join(';', @result) if scalar @result;
950         return $href;
954 ## ======================================================================
955 ## validation, quoting/unquoting and escaping
957 sub validate_action {
958         my $input = shift || return undef;
959         return undef unless exists $actions{$input};
960         return $input;
963 sub validate_project {
964         my $input = shift || return undef;
965         if (!validate_pathname($input) ||
966                 !(-d "$projectroot/$input") ||
967                 !check_export_ok("$projectroot/$input") ||
968                 ($strict_export && !project_in_list($input))) {
969                 return undef;
970         } else {
971                 return $input;
972         }
975 sub validate_pathname {
976         my $input = shift || return undef;
978         # no '.' or '..' as elements of path, i.e. no '.' nor '..'
979         # at the beginning, at the end, and between slashes.
980         # also this catches doubled slashes
981         if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
982                 return undef;
983         }
984         # no null characters
985         if ($input =~ m!\0!) {
986                 return undef;
987         }
988         return $input;
991 sub validate_refname {
992         my $input = shift || return undef;
994         # textual hashes are O.K.
995         if ($input =~ m/^[0-9a-fA-F]{40}$/) {
996                 return $input;
997         }
998         # it must be correct pathname
999         $input = validate_pathname($input)
1000                 or return undef;
1001         # restrictions on ref name according to git-check-ref-format
1002         if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1003                 return undef;
1004         }
1005         return $input;
1008 # decode sequences of octets in utf8 into Perl's internal form,
1009 # which is utf-8 with utf8 flag set if needed.  gitweb writes out
1010 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1011 sub to_utf8 {
1012         my $str = shift;
1013         if (utf8::valid($str)) {
1014                 utf8::decode($str);
1015                 return $str;
1016         } else {
1017                 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1018         }
1021 # quote unsafe chars, but keep the slash, even when it's not
1022 # correct, but quoted slashes look too horrible in bookmarks
1023 sub esc_param {
1024         my $str = shift;
1025         $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
1026         $str =~ s/\+/%2B/g;
1027         $str =~ s/ /\+/g;
1028         return $str;
1031 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1032 sub esc_url {
1033         my $str = shift;
1034         $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1035         $str =~ s/\+/%2B/g;
1036         $str =~ s/ /\+/g;
1037         return $str;
1040 # replace invalid utf8 character with SUBSTITUTION sequence
1041 sub esc_html ($;%) {
1042         my $str = shift;
1043         my %opts = @_;
1045         $str = to_utf8($str);
1046         $str = $cgi->escapeHTML($str);
1047         if ($opts{'-nbsp'}) {
1048                 $str =~ s/ /&nbsp;/g;
1049         }
1050         $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1051         return $str;
1054 # quote control characters and escape filename to HTML
1055 sub esc_path {
1056         my $str = shift;
1057         my %opts = @_;
1059         $str = to_utf8($str);
1060         $str = $cgi->escapeHTML($str);
1061         if ($opts{'-nbsp'}) {
1062                 $str =~ s/ /&nbsp;/g;
1063         }
1064         $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1065         return $str;
1068 # Make control characters "printable", using character escape codes (CEC)
1069 sub quot_cec {
1070         my $cntrl = shift;
1071         my %opts = @_;
1072         my %es = ( # character escape codes, aka escape sequences
1073                 "\t" => '\t',   # tab            (HT)
1074                 "\n" => '\n',   # line feed      (LF)
1075                 "\r" => '\r',   # carrige return (CR)
1076                 "\f" => '\f',   # form feed      (FF)
1077                 "\b" => '\b',   # backspace      (BS)
1078                 "\a" => '\a',   # alarm (bell)   (BEL)
1079                 "\e" => '\e',   # escape         (ESC)
1080                 "\013" => '\v', # vertical tab   (VT)
1081                 "\000" => '\0', # nul character  (NUL)
1082         );
1083         my $chr = ( (exists $es{$cntrl})
1084                     ? $es{$cntrl}
1085                     : sprintf('\%2x', ord($cntrl)) );
1086         if ($opts{-nohtml}) {
1087                 return $chr;
1088         } else {
1089                 return "<span class=\"cntrl\">$chr</span>";
1090         }
1093 # Alternatively use unicode control pictures codepoints,
1094 # Unicode "printable representation" (PR)
1095 sub quot_upr {
1096         my $cntrl = shift;
1097         my %opts = @_;
1099         my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1100         if ($opts{-nohtml}) {
1101                 return $chr;
1102         } else {
1103                 return "<span class=\"cntrl\">$chr</span>";
1104         }
1107 # git may return quoted and escaped filenames
1108 sub unquote {
1109         my $str = shift;
1111         sub unq {
1112                 my $seq = shift;
1113                 my %es = ( # character escape codes, aka escape sequences
1114                         't' => "\t",   # tab            (HT, TAB)
1115                         'n' => "\n",   # newline        (NL)
1116                         'r' => "\r",   # return         (CR)
1117                         'f' => "\f",   # form feed      (FF)
1118                         'b' => "\b",   # backspace      (BS)
1119                         'a' => "\a",   # alarm (bell)   (BEL)
1120                         'e' => "\e",   # escape         (ESC)
1121                         'v' => "\013", # vertical tab   (VT)
1122                 );
1124                 if ($seq =~ m/^[0-7]{1,3}$/) {
1125                         # octal char sequence
1126                         return chr(oct($seq));
1127                 } elsif (exists $es{$seq}) {
1128                         # C escape sequence, aka character escape code
1129                         return $es{$seq};
1130                 }
1131                 # quoted ordinary character
1132                 return $seq;
1133         }
1135         if ($str =~ m/^"(.*)"$/) {
1136                 # needs unquoting
1137                 $str = $1;
1138                 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1139         }
1140         return $str;
1143 # escape tabs (convert tabs to spaces)
1144 sub untabify {
1145         my $line = shift;
1147         while ((my $pos = index($line, "\t")) != -1) {
1148                 if (my $count = (8 - ($pos % 8))) {
1149                         my $spaces = ' ' x $count;
1150                         $line =~ s/\t/$spaces/;
1151                 }
1152         }
1154         return $line;
1157 sub project_in_list {
1158         my $project = shift;
1159         my @list = git_get_projects_list();
1160         return @list && scalar(grep { $_->{'path'} eq $project } @list);
1163 ## ----------------------------------------------------------------------
1164 ## HTML aware string manipulation
1166 # Try to chop given string on a word boundary between position
1167 # $len and $len+$add_len. If there is no word boundary there,
1168 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1169 # (marking chopped part) would be longer than given string.
1170 sub chop_str {
1171         my $str = shift;
1172         my $len = shift;
1173         my $add_len = shift || 10;
1174         my $where = shift || 'right'; # 'left' | 'center' | 'right'
1176         # Make sure perl knows it is utf8 encoded so we don't
1177         # cut in the middle of a utf8 multibyte char.
1178         $str = to_utf8($str);
1180         # allow only $len chars, but don't cut a word if it would fit in $add_len
1181         # if it doesn't fit, cut it if it's still longer than the dots we would add
1182         # remove chopped character entities entirely
1184         # when chopping in the middle, distribute $len into left and right part
1185         # return early if chopping wouldn't make string shorter
1186         if ($where eq 'center') {
1187                 return $str if ($len + 5 >= length($str)); # filler is length 5
1188                 $len = int($len/2);
1189         } else {
1190                 return $str if ($len + 4 >= length($str)); # filler is length 4
1191         }
1193         # regexps: ending and beginning with word part up to $add_len
1194         my $endre = qr/.{$len}\w{0,$add_len}/;
1195         my $begre = qr/\w{0,$add_len}.{$len}/;
1197         if ($where eq 'left') {
1198                 $str =~ m/^(.*?)($begre)$/;
1199                 my ($lead, $body) = ($1, $2);
1200                 if (length($lead) > 4) {
1201                         $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1202                         $lead = " ...";
1203                 }
1204                 return "$lead$body";
1206         } elsif ($where eq 'center') {
1207                 $str =~ m/^($endre)(.*)$/;
1208                 my ($left, $str)  = ($1, $2);
1209                 $str =~ m/^(.*?)($begre)$/;
1210                 my ($mid, $right) = ($1, $2);
1211                 if (length($mid) > 5) {
1212                         $left  =~ s/&[^;]*$//;
1213                         $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1214                         $mid = " ... ";
1215                 }
1216                 return "$left$mid$right";
1218         } else {
1219                 $str =~ m/^($endre)(.*)$/;
1220                 my $body = $1;
1221                 my $tail = $2;
1222                 if (length($tail) > 4) {
1223                         $body =~ s/&[^;]*$//;
1224                         $tail = "... ";
1225                 }
1226                 return "$body$tail";
1227         }
1230 # takes the same arguments as chop_str, but also wraps a <span> around the
1231 # result with a title attribute if it does get chopped. Additionally, the
1232 # string is HTML-escaped.
1233 sub chop_and_escape_str {
1234         my ($str) = @_;
1236         my $chopped = chop_str(@_);
1237         if ($chopped eq $str) {
1238                 return esc_html($chopped);
1239         } else {
1240                 $str =~ s/([[:cntrl:]])/?/g;
1241                 return $cgi->span({-title=>$str}, esc_html($chopped));
1242         }
1245 ## ----------------------------------------------------------------------
1246 ## functions returning short strings
1248 # CSS class for given age value (in seconds)
1249 sub age_class {
1250         my $age = shift;
1252         if (!defined $age) {
1253                 return "noage";
1254         } elsif ($age < 60*60*2) {
1255                 return "age0";
1256         } elsif ($age < 60*60*24*2) {
1257                 return "age1";
1258         } else {
1259                 return "age2";
1260         }
1263 # convert age in seconds to "nn units ago" string
1264 sub age_string {
1265         my $age = shift;
1266         my $age_str;
1268         if ($age > 60*60*24*365*2) {
1269                 $age_str = (int $age/60/60/24/365);
1270                 $age_str .= " years ago";
1271         } elsif ($age > 60*60*24*(365/12)*2) {
1272                 $age_str = int $age/60/60/24/(365/12);
1273                 $age_str .= " months ago";
1274         } elsif ($age > 60*60*24*7*2) {
1275                 $age_str = int $age/60/60/24/7;
1276                 $age_str .= " weeks ago";
1277         } elsif ($age > 60*60*24*2) {
1278                 $age_str = int $age/60/60/24;
1279                 $age_str .= " days ago";
1280         } elsif ($age > 60*60*2) {
1281                 $age_str = int $age/60/60;
1282                 $age_str .= " hours ago";
1283         } elsif ($age > 60*2) {
1284                 $age_str = int $age/60;
1285                 $age_str .= " min ago";
1286         } elsif ($age > 2) {
1287                 $age_str = int $age;
1288                 $age_str .= " sec ago";
1289         } else {
1290                 $age_str .= " right now";
1291         }
1292         return $age_str;
1295 use constant {
1296         S_IFINVALID => 0030000,
1297         S_IFGITLINK => 0160000,
1298 };
1300 # submodule/subproject, a commit object reference
1301 sub S_ISGITLINK($) {
1302         my $mode = shift;
1304         return (($mode & S_IFMT) == S_IFGITLINK)
1307 # convert file mode in octal to symbolic file mode string
1308 sub mode_str {
1309         my $mode = oct shift;
1311         if (S_ISGITLINK($mode)) {
1312                 return 'm---------';
1313         } elsif (S_ISDIR($mode & S_IFMT)) {
1314                 return 'drwxr-xr-x';
1315         } elsif (S_ISLNK($mode)) {
1316                 return 'lrwxrwxrwx';
1317         } elsif (S_ISREG($mode)) {
1318                 # git cares only about the executable bit
1319                 if ($mode & S_IXUSR) {
1320                         return '-rwxr-xr-x';
1321                 } else {
1322                         return '-rw-r--r--';
1323                 };
1324         } else {
1325                 return '----------';
1326         }
1329 # convert file mode in octal to file type string
1330 sub file_type {
1331         my $mode = shift;
1333         if ($mode !~ m/^[0-7]+$/) {
1334                 return $mode;
1335         } else {
1336                 $mode = oct $mode;
1337         }
1339         if (S_ISGITLINK($mode)) {
1340                 return "submodule";
1341         } elsif (S_ISDIR($mode & S_IFMT)) {
1342                 return "directory";
1343         } elsif (S_ISLNK($mode)) {
1344                 return "symlink";
1345         } elsif (S_ISREG($mode)) {
1346                 return "file";
1347         } else {
1348                 return "unknown";
1349         }
1352 # convert file mode in octal to file type description string
1353 sub file_type_long {
1354         my $mode = shift;
1356         if ($mode !~ m/^[0-7]+$/) {
1357                 return $mode;
1358         } else {
1359                 $mode = oct $mode;
1360         }
1362         if (S_ISGITLINK($mode)) {
1363                 return "submodule";
1364         } elsif (S_ISDIR($mode & S_IFMT)) {
1365                 return "directory";
1366         } elsif (S_ISLNK($mode)) {
1367                 return "symlink";
1368         } elsif (S_ISREG($mode)) {
1369                 if ($mode & S_IXUSR) {
1370                         return "executable";
1371                 } else {
1372                         return "file";
1373                 };
1374         } else {
1375                 return "unknown";
1376         }
1380 ## ----------------------------------------------------------------------
1381 ## functions returning short HTML fragments, or transforming HTML fragments
1382 ## which don't belong to other sections
1384 # format line of commit message.
1385 sub format_log_line_html {
1386         my $line = shift;
1388         $line = esc_html($line, -nbsp=>1);
1389         if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1390                 my $hash_text = $1;
1391                 my $link =
1392                         $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1393                                 -class => "text"}, $hash_text);
1394                 $line =~ s/$hash_text/$link/;
1395         }
1396         return $line;
1399 # format marker of refs pointing to given object
1401 # the destination action is chosen based on object type and current context:
1402 # - for annotated tags, we choose the tag view unless it's the current view
1403 #   already, in which case we go to shortlog view
1404 # - for other refs, we keep the current view if we're in history, shortlog or
1405 #   log view, and select shortlog otherwise
1406 sub format_ref_marker {
1407         my ($refs, $id) = @_;
1408         my $markers = '';
1410         if (defined $refs->{$id}) {
1411                 foreach my $ref (@{$refs->{$id}}) {
1412                         # this code exploits the fact that non-lightweight tags are the
1413                         # only indirect objects, and that they are the only objects for which
1414                         # we want to use tag instead of shortlog as action
1415                         my ($type, $name) = qw();
1416                         my $indirect = ($ref =~ s/\^\{\}$//);
1417                         # e.g. tags/v2.6.11 or heads/next
1418                         if ($ref =~ m!^(.*?)s?/(.*)$!) {
1419                                 $type = $1;
1420                                 $name = $2;
1421                         } else {
1422                                 $type = "ref";
1423                                 $name = $ref;
1424                         }
1426                         my $class = $type;
1427                         $class .= " indirect" if $indirect;
1429                         my $dest_action = "shortlog";
1431                         if ($indirect) {
1432                                 $dest_action = "tag" unless $action eq "tag";
1433                         } elsif ($action =~ /^(history|(short)?log)$/) {
1434                                 $dest_action = $action;
1435                         }
1437                         my $dest = "";
1438                         $dest .= "refs/" unless $ref =~ m!^refs/!;
1439                         $dest .= $ref;
1441                         my $link = $cgi->a({
1442                                 -href => href(
1443                                         action=>$dest_action,
1444                                         hash=>$dest
1445                                 )}, $name);
1447                         $markers .= " <span class=\"$class\" title=\"$ref\">" .
1448                                 $link . "</span>";
1449                 }
1450         }
1452         if ($markers) {
1453                 return ' <span class="refs">'. $markers . '</span>';
1454         } else {
1455                 return "";
1456         }
1459 # format, perhaps shortened and with markers, title line
1460 sub format_subject_html {
1461         my ($long, $short, $href, $extra) = @_;
1462         $extra = '' unless defined($extra);
1464         if (length($short) < length($long)) {
1465                 return $cgi->a({-href => $href, -class => "list subject",
1466                                 -title => to_utf8($long)},
1467                        esc_html($short) . $extra);
1468         } else {
1469                 return $cgi->a({-href => $href, -class => "list subject"},
1470                        esc_html($long)  . $extra);
1471         }
1474 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1475 sub format_git_diff_header_line {
1476         my $line = shift;
1477         my $diffinfo = shift;
1478         my ($from, $to) = @_;
1480         if ($diffinfo->{'nparents'}) {
1481                 # combined diff
1482                 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1483                 if ($to->{'href'}) {
1484                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1485                                          esc_path($to->{'file'}));
1486                 } else { # file was deleted (no href)
1487                         $line .= esc_path($to->{'file'});
1488                 }
1489         } else {
1490                 # "ordinary" diff
1491                 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1492                 if ($from->{'href'}) {
1493                         $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1494                                          'a/' . esc_path($from->{'file'}));
1495                 } else { # file was added (no href)
1496                         $line .= 'a/' . esc_path($from->{'file'});
1497                 }
1498                 $line .= ' ';
1499                 if ($to->{'href'}) {
1500                         $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1501                                          'b/' . esc_path($to->{'file'}));
1502                 } else { # file was deleted
1503                         $line .= 'b/' . esc_path($to->{'file'});
1504                 }
1505         }
1507         return "<div class=\"diff header\">$line</div>\n";
1510 # format extended diff header line, before patch itself
1511 sub format_extended_diff_header_line {
1512         my $line = shift;
1513         my $diffinfo = shift;
1514         my ($from, $to) = @_;
1516         # match <path>
1517         if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1518                 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1519                                        esc_path($from->{'file'}));
1520         }
1521         if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1522                 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1523                                  esc_path($to->{'file'}));
1524         }
1525         # match single <mode>
1526         if ($line =~ m/\s(\d{6})$/) {
1527                 $line .= '<span class="info"> (' .
1528                          file_type_long($1) .
1529                          ')</span>';
1530         }
1531         # match <hash>
1532         if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1533                 # can match only for combined diff
1534                 $line = 'index ';
1535                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1536                         if ($from->{'href'}[$i]) {
1537                                 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1538                                                   -class=>"hash"},
1539                                                  substr($diffinfo->{'from_id'}[$i],0,7));
1540                         } else {
1541                                 $line .= '0' x 7;
1542                         }
1543                         # separator
1544                         $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1545                 }
1546                 $line .= '..';
1547                 if ($to->{'href'}) {
1548                         $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1549                                          substr($diffinfo->{'to_id'},0,7));
1550                 } else {
1551                         $line .= '0' x 7;
1552                 }
1554         } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1555                 # can match only for ordinary diff
1556                 my ($from_link, $to_link);
1557                 if ($from->{'href'}) {
1558                         $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1559                                              substr($diffinfo->{'from_id'},0,7));
1560                 } else {
1561                         $from_link = '0' x 7;
1562                 }
1563                 if ($to->{'href'}) {
1564                         $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1565                                            substr($diffinfo->{'to_id'},0,7));
1566                 } else {
1567                         $to_link = '0' x 7;
1568                 }
1569                 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1570                 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1571         }
1573         return $line . "<br/>\n";
1576 # format from-file/to-file diff header
1577 sub format_diff_from_to_header {
1578         my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1579         my $line;
1580         my $result = '';
1582         $line = $from_line;
1583         #assert($line =~ m/^---/) if DEBUG;
1584         # no extra formatting for "^--- /dev/null"
1585         if (! $diffinfo->{'nparents'}) {
1586                 # ordinary (single parent) diff
1587                 if ($line =~ m!^--- "?a/!) {
1588                         if ($from->{'href'}) {
1589                                 $line = '--- a/' .
1590                                         $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1591                                                 esc_path($from->{'file'}));
1592                         } else {
1593                                 $line = '--- a/' .
1594                                         esc_path($from->{'file'});
1595                         }
1596                 }
1597                 $result .= qq!<div class="diff from_file">$line</div>\n!;
1599         } else {
1600                 # combined diff (merge commit)
1601                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1602                         if ($from->{'href'}[$i]) {
1603                                 $line = '--- ' .
1604                                         $cgi->a({-href=>href(action=>"blobdiff",
1605                                                              hash_parent=>$diffinfo->{'from_id'}[$i],
1606                                                              hash_parent_base=>$parents[$i],
1607                                                              file_parent=>$from->{'file'}[$i],
1608                                                              hash=>$diffinfo->{'to_id'},
1609                                                              hash_base=>$hash,
1610                                                              file_name=>$to->{'file'}),
1611                                                  -class=>"path",
1612                                                  -title=>"diff" . ($i+1)},
1613                                                 $i+1) .
1614                                         '/' .
1615                                         $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1616                                                 esc_path($from->{'file'}[$i]));
1617                         } else {
1618                                 $line = '--- /dev/null';
1619                         }
1620                         $result .= qq!<div class="diff from_file">$line</div>\n!;
1621                 }
1622         }
1624         $line = $to_line;
1625         #assert($line =~ m/^\+\+\+/) if DEBUG;
1626         # no extra formatting for "^+++ /dev/null"
1627         if ($line =~ m!^\+\+\+ "?b/!) {
1628                 if ($to->{'href'}) {
1629                         $line = '+++ b/' .
1630                                 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1631                                         esc_path($to->{'file'}));
1632                 } else {
1633                         $line = '+++ b/' .
1634                                 esc_path($to->{'file'});
1635                 }
1636         }
1637         $result .= qq!<div class="diff to_file">$line</div>\n!;
1639         return $result;
1642 # create note for patch simplified by combined diff
1643 sub format_diff_cc_simplified {
1644         my ($diffinfo, @parents) = @_;
1645         my $result = '';
1647         $result .= "<div class=\"diff header\">" .
1648                    "diff --cc ";
1649         if (!is_deleted($diffinfo)) {
1650                 $result .= $cgi->a({-href => href(action=>"blob",
1651                                                   hash_base=>$hash,
1652                                                   hash=>$diffinfo->{'to_id'},
1653                                                   file_name=>$diffinfo->{'to_file'}),
1654                                     -class => "path"},
1655                                    esc_path($diffinfo->{'to_file'}));
1656         } else {
1657                 $result .= esc_path($diffinfo->{'to_file'});
1658         }
1659         $result .= "</div>\n" . # class="diff header"
1660                    "<div class=\"diff nodifferences\">" .
1661                    "Simple merge" .
1662                    "</div>\n"; # class="diff nodifferences"
1664         return $result;
1667 # format patch (diff) line (not to be used for diff headers)
1668 sub format_diff_line {
1669         my $line = shift;
1670         my ($from, $to) = @_;
1671         my $diff_class = "";
1673         chomp $line;
1675         if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1676                 # combined diff
1677                 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1678                 if ($line =~ m/^\@{3}/) {
1679                         $diff_class = " chunk_header";
1680                 } elsif ($line =~ m/^\\/) {
1681                         $diff_class = " incomplete";
1682                 } elsif ($prefix =~ tr/+/+/) {
1683                         $diff_class = " add";
1684                 } elsif ($prefix =~ tr/-/-/) {
1685                         $diff_class = " rem";
1686                 }
1687         } else {
1688                 # assume ordinary diff
1689                 my $char = substr($line, 0, 1);
1690                 if ($char eq '+') {
1691                         $diff_class = " add";
1692                 } elsif ($char eq '-') {
1693                         $diff_class = " rem";
1694                 } elsif ($char eq '@') {
1695                         $diff_class = " chunk_header";
1696                 } elsif ($char eq "\\") {
1697                         $diff_class = " incomplete";
1698                 }
1699         }
1700         $line = untabify($line);
1701         if ($from && $to && $line =~ m/^\@{2} /) {
1702                 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1703                         $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1705                 $from_lines = 0 unless defined $from_lines;
1706                 $to_lines   = 0 unless defined $to_lines;
1708                 if ($from->{'href'}) {
1709                         $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1710                                              -class=>"list"}, $from_text);
1711                 }
1712                 if ($to->{'href'}) {
1713                         $to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1714                                              -class=>"list"}, $to_text);
1715                 }
1716                 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1717                         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1718                 return "<div class=\"diff$diff_class\">$line</div>\n";
1719         } elsif ($from && $to && $line =~ m/^\@{3}/) {
1720                 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1721                 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1723                 @from_text = split(' ', $ranges);
1724                 for (my $i = 0; $i < @from_text; ++$i) {
1725                         ($from_start[$i], $from_nlines[$i]) =
1726                                 (split(',', substr($from_text[$i], 1)), 0);
1727                 }
1729                 $to_text   = pop @from_text;
1730                 $to_start  = pop @from_start;
1731                 $to_nlines = pop @from_nlines;
1733                 $line = "<span class=\"chunk_info\">$prefix ";
1734                 for (my $i = 0; $i < @from_text; ++$i) {
1735                         if ($from->{'href'}[$i]) {
1736                                 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1737                                                   -class=>"list"}, $from_text[$i]);
1738                         } else {
1739                                 $line .= $from_text[$i];
1740                         }
1741                         $line .= " ";
1742                 }
1743                 if ($to->{'href'}) {
1744                         $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1745                                           -class=>"list"}, $to_text);
1746                 } else {
1747                         $line .= $to_text;
1748                 }
1749                 $line .= " $prefix</span>" .
1750                          "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1751                 return "<div class=\"diff$diff_class\">$line</div>\n";
1752         }
1753         return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1756 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1757 # linked.  Pass the hash of the tree/commit to snapshot.
1758 sub format_snapshot_links {
1759         my ($hash) = @_;
1760         my $num_fmts = @snapshot_fmts;
1761         if ($num_fmts > 1) {
1762                 # A parenthesized list of links bearing format names.
1763                 # e.g. "snapshot (_tar.gz_ _zip_)"
1764                 return "snapshot (" . join(' ', map
1765                         $cgi->a({
1766                                 -href => href(
1767                                         action=>"snapshot",
1768                                         hash=>$hash,
1769                                         snapshot_format=>$_
1770                                 )
1771                         }, $known_snapshot_formats{$_}{'display'})
1772                 , @snapshot_fmts) . ")";
1773         } elsif ($num_fmts == 1) {
1774                 # A single "snapshot" link whose tooltip bears the format name.
1775                 # i.e. "_snapshot_"
1776                 my ($fmt) = @snapshot_fmts;
1777                 return
1778                         $cgi->a({
1779                                 -href => href(
1780                                         action=>"snapshot",
1781                                         hash=>$hash,
1782                                         snapshot_format=>$fmt
1783                                 ),
1784                                 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1785                         }, "snapshot");
1786         } else { # $num_fmts == 0
1787                 return undef;
1788         }
1791 ## ......................................................................
1792 ## functions returning values to be passed, perhaps after some
1793 ## transformation, to other functions; e.g. returning arguments to href()
1795 # returns hash to be passed to href to generate gitweb URL
1796 # in -title key it returns description of link
1797 sub get_feed_info {
1798         my $format = shift || 'Atom';
1799         my %res = (action => lc($format));
1801         # feed links are possible only for project views
1802         return unless (defined $project);
1803         # some views should link to OPML, or to generic project feed,
1804         # or don't have specific feed yet (so they should use generic)
1805         return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1807         my $branch;
1808         # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1809         # from tag links; this also makes possible to detect branch links
1810         if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1811             (defined $hash      && $hash      =~ m!^refs/heads/(.*)$!)) {
1812                 $branch = $1;
1813         }
1814         # find log type for feed description (title)
1815         my $type = 'log';
1816         if (defined $file_name) {
1817                 $type  = "history of $file_name";
1818                 $type .= "/" if ($action eq 'tree');
1819                 $type .= " on '$branch'" if (defined $branch);
1820         } else {
1821                 $type = "log of $branch" if (defined $branch);
1822         }
1824         $res{-title} = $type;
1825         $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1826         $res{'file_name'} = $file_name;
1828         return %res;
1831 ## ----------------------------------------------------------------------
1832 ## git utility subroutines, invoking git commands
1834 # returns path to the core git executable and the --git-dir parameter as list
1835 sub git_cmd {
1836         return $GIT, '--git-dir='.$git_dir;
1839 # quote the given arguments for passing them to the shell
1840 # quote_command("command", "arg 1", "arg with ' and ! characters")
1841 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1842 # Try to avoid using this function wherever possible.
1843 sub quote_command {
1844         return join(' ',
1845                     map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1848 # get HEAD ref of given project as hash
1849 sub git_get_head_hash {
1850         my $project = shift;
1851         my $o_git_dir = $git_dir;
1852         my $retval = undef;
1853         $git_dir = "$projectroot/$project";
1854         if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1855                 my $head = <$fd>;
1856                 close $fd;
1857                 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1858                         $retval = $1;
1859                 }
1860         }
1861         if (defined $o_git_dir) {
1862                 $git_dir = $o_git_dir;
1863         }
1864         return $retval;
1867 # get type of given object
1868 sub git_get_type {
1869         my $hash = shift;
1871         open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1872         my $type = <$fd>;
1873         close $fd or return;
1874         chomp $type;
1875         return $type;
1878 # repository configuration
1879 our $config_file = '';
1880 our %config;
1882 # store multiple values for single key as anonymous array reference
1883 # single values stored directly in the hash, not as [ <value> ]
1884 sub hash_set_multi {
1885         my ($hash, $key, $value) = @_;
1887         if (!exists $hash->{$key}) {
1888                 $hash->{$key} = $value;
1889         } elsif (!ref $hash->{$key}) {
1890                 $hash->{$key} = [ $hash->{$key}, $value ];
1891         } else {
1892                 push @{$hash->{$key}}, $value;
1893         }
1896 # return hash of git project configuration
1897 # optionally limited to some section, e.g. 'gitweb'
1898 sub git_parse_project_config {
1899         my $section_regexp = shift;
1900         my %config;
1902         local $/ = "\0";
1904         open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1905                 or return;
1907         while (my $keyval = <$fh>) {
1908                 chomp $keyval;
1909                 my ($key, $value) = split(/\n/, $keyval, 2);
1911                 hash_set_multi(\%config, $key, $value)
1912                         if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1913         }
1914         close $fh;
1916         return %config;
1919 # convert config value to boolean, 'true' or 'false'
1920 # no value, number > 0, 'true' and 'yes' values are true
1921 # rest of values are treated as false (never as error)
1922 sub config_to_bool {
1923         my $val = shift;
1925         # strip leading and trailing whitespace
1926         $val =~ s/^\s+//;
1927         $val =~ s/\s+$//;
1929         return (!defined $val ||               # section.key
1930                 ($val =~ /^\d+$/ && $val) ||   # section.key = 1
1931                 ($val =~ /^(?:true|yes)$/i));  # section.key = true
1934 # convert config value to simple decimal number
1935 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1936 # to be multiplied by 1024, 1048576, or 1073741824
1937 sub config_to_int {
1938         my $val = shift;
1940         # strip leading and trailing whitespace
1941         $val =~ s/^\s+//;
1942         $val =~ s/\s+$//;
1944         if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1945                 $unit = lc($unit);
1946                 # unknown unit is treated as 1
1947                 return $num * ($unit eq 'g' ? 1073741824 :
1948                                $unit eq 'm' ?    1048576 :
1949                                $unit eq 'k' ?       1024 : 1);
1950         }
1951         return $val;
1954 # convert config value to array reference, if needed
1955 sub config_to_multi {
1956         my $val = shift;
1958         return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1961 sub git_get_project_config {
1962         my ($key, $type) = @_;
1964         # key sanity check
1965         return unless ($key);
1966         $key =~ s/^gitweb\.//;
1967         return if ($key =~ m/\W/);
1969         # type sanity check
1970         if (defined $type) {
1971                 $type =~ s/^--//;
1972                 $type = undef
1973                         unless ($type eq 'bool' || $type eq 'int');
1974         }
1976         # get config
1977         if (!defined $config_file ||
1978             $config_file ne "$git_dir/config") {
1979                 %config = git_parse_project_config('gitweb');
1980                 $config_file = "$git_dir/config";
1981         }
1983         # ensure given type
1984         if (!defined $type) {
1985                 return $config{"gitweb.$key"};
1986         } elsif ($type eq 'bool') {
1987                 # backward compatibility: 'git config --bool' returns true/false
1988                 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1989         } elsif ($type eq 'int') {
1990                 return config_to_int($config{"gitweb.$key"});
1991         }
1992         return $config{"gitweb.$key"};
1995 # get hash of given path at given ref
1996 sub git_get_hash_by_path {
1997         my $base = shift;
1998         my $path = shift || return undef;
1999         my $type = shift;
2001         $path =~ s,/+$,,;
2003         open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2004                 or die_error(500, "Open git-ls-tree failed");
2005         my $line = <$fd>;
2006         close $fd or return undef;
2008         if (!defined $line) {
2009                 # there is no tree or hash given by $path at $base
2010                 return undef;
2011         }
2013         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2014         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2015         if (defined $type && $type ne $2) {
2016                 # type doesn't match
2017                 return undef;
2018         }
2019         return $3;
2022 # get path of entry with given hash at given tree-ish (ref)
2023 # used to get 'from' filename for combined diff (merge commit) for renames
2024 sub git_get_path_by_hash {
2025         my $base = shift || return;
2026         my $hash = shift || return;
2028         local $/ = "\0";
2030         open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2031                 or return undef;
2032         while (my $line = <$fd>) {
2033                 chomp $line;
2035                 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423  gitweb'
2036                 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f  gitweb/README'
2037                 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2038                         close $fd;
2039                         return $1;
2040                 }
2041         }
2042         close $fd;
2043         return undef;
2046 ## ......................................................................
2047 ## git utility functions, directly accessing git repository
2049 sub git_get_project_description {
2050         my $path = shift;
2052         $git_dir = "$projectroot/$path";
2053         open my $fd, "$git_dir/description"
2054                 or return git_get_project_config('description');
2055         my $descr = <$fd>;
2056         close $fd;
2057         if (defined $descr) {
2058                 chomp $descr;
2059         }
2060         return $descr;
2063 sub git_get_project_ctags {
2064         my $path = shift;
2065         my $ctags = {};
2067         $git_dir = "$projectroot/$path";
2068         unless (opendir D, "$git_dir/ctags") {
2069                 return $ctags;
2070         }
2071         foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
2072                 open CT, $_ or next;
2073                 my $val = <CT>;
2074                 chomp $val;
2075                 close CT;
2076                 my $ctag = $_; $ctag =~ s#.*/##;
2077                 $ctags->{$ctag} = $val;
2078         }
2079         closedir D;
2080         $ctags;
2083 sub git_populate_project_tagcloud {
2084         my $ctags = shift;
2086         # First, merge different-cased tags; tags vote on casing
2087         my %ctags_lc;
2088         foreach (keys %$ctags) {
2089                 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2090                 if (not $ctags_lc{lc $_}->{topcount}
2091                     or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2092                         $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2093                         $ctags_lc{lc $_}->{topname} = $_;
2094                 }
2095         }
2097         my $cloud;
2098         if (eval { require HTML::TagCloud; 1; }) {
2099                 $cloud = HTML::TagCloud->new;
2100                 foreach (sort keys %ctags_lc) {
2101                         # Pad the title with spaces so that the cloud looks
2102                         # less crammed.
2103                         my $title = $ctags_lc{$_}->{topname};
2104                         $title =~ s/ /&nbsp;/g;
2105                         $title =~ s/^/&nbsp;/g;
2106                         $title =~ s/$/&nbsp;/g;
2107                         $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2108                 }
2109         } else {
2110                 $cloud = \%ctags_lc;
2111         }
2112         $cloud;
2115 sub git_show_project_tagcloud {
2116         my ($cloud, $count) = @_;
2117         print STDERR ref($cloud)."..\n";
2118         if (ref $cloud eq 'HTML::TagCloud') {
2119                 return $cloud->html_and_css($count);
2120         } else {
2121                 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2122                 return '<p align="center">' . join (', ', map {
2123                         "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2124                 } splice(@tags, 0, $count)) . '</p>';
2125         }
2128 sub git_get_project_url_list {
2129         my $path = shift;
2131         $git_dir = "$projectroot/$path";
2132         open my $fd, "$git_dir/cloneurl"
2133                 or return wantarray ?
2134                 @{ config_to_multi(git_get_project_config('url')) } :
2135                    config_to_multi(git_get_project_config('url'));
2136         my @git_project_url_list = map { chomp; $_ } <$fd>;
2137         close $fd;
2139         return wantarray ? @git_project_url_list : \@git_project_url_list;
2142 sub git_get_projects_list {
2143         my ($filter) = @_;
2144         my @list;
2146         $filter ||= '';
2147         $filter =~ s/\.git$//;
2149         my $check_forks = gitweb_check_feature('forks');
2151         if (-d $projects_list) {
2152                 # search in directory
2153                 my $dir = $projects_list . ($filter ? "/$filter" : '');
2154                 # remove the trailing "/"
2155                 $dir =~ s!/+$!!;
2156                 my $pfxlen = length("$dir");
2157                 my $pfxdepth = ($dir =~ tr!/!!);
2159                 File::Find::find({
2160                         follow_fast => 1, # follow symbolic links
2161                         follow_skip => 2, # ignore duplicates
2162                         dangling_symlinks => 0, # ignore dangling symlinks, silently
2163                         wanted => sub {
2164                                 # skip project-list toplevel, if we get it.
2165                                 return if (m!^[/.]$!);
2166                                 # only directories can be git repositories
2167                                 return unless (-d $_);
2168                                 # don't traverse too deep (Find is super slow on os x)
2169                                 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2170                                         $File::Find::prune = 1;
2171                                         return;
2172                                 }
2174                                 my $subdir = substr($File::Find::name, $pfxlen + 1);
2175                                 # we check related file in $projectroot
2176                                 if (check_export_ok("$projectroot/$filter/$subdir")) {
2177                                         push @list, { path => ($filter ? "$filter/" : '') . $subdir };
2178                                         $File::Find::prune = 1;
2179                                 }
2180                         },
2181                 }, "$dir");
2183         } elsif (-f $projects_list) {
2184                 # read from file(url-encoded):
2185                 # 'git%2Fgit.git Linus+Torvalds'
2186                 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2187                 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2188                 my %paths;
2189                 open my ($fd), $projects_list or return;
2190         PROJECT:
2191                 while (my $line = <$fd>) {
2192                         chomp $line;
2193                         my ($path, $owner) = split ' ', $line;
2194                         $path = unescape($path);
2195                         $owner = unescape($owner);
2196                         if (!defined $path) {
2197                                 next;
2198                         }
2199                         if ($filter ne '') {
2200                                 # looking for forks;
2201                                 my $pfx = substr($path, 0, length($filter));
2202                                 if ($pfx ne $filter) {
2203                                         next PROJECT;
2204                                 }
2205                                 my $sfx = substr($path, length($filter));
2206                                 if ($sfx !~ /^\/.*\.git$/) {
2207                                         next PROJECT;
2208                                 }
2209                         } elsif ($check_forks) {
2210                         PATH:
2211                                 foreach my $filter (keys %paths) {
2212                                         # looking for forks;
2213                                         my $pfx = substr($path, 0, length($filter));
2214                                         if ($pfx ne $filter) {
2215                                                 next PATH;
2216                                         }
2217                                         my $sfx = substr($path, length($filter));
2218                                         if ($sfx !~ /^\/.*\.git$/) {
2219                                                 next PATH;
2220                                         }
2221                                         # is a fork, don't include it in
2222                                         # the list
2223                                         next PROJECT;
2224                                 }
2225                         }
2226                         if (check_export_ok("$projectroot/$path")) {
2227                                 my $pr = {
2228                                         path => $path,
2229                                         owner => to_utf8($owner),
2230                                 };
2231                                 push @list, $pr;
2232                                 (my $forks_path = $path) =~ s/\.git$//;
2233                                 $paths{$forks_path}++;
2234                         }
2235                 }
2236                 close $fd;
2237         }
2238         return @list;
2241 our $gitweb_project_owner = undef;
2242 sub git_get_project_list_from_file {
2244         return if (defined $gitweb_project_owner);
2246         $gitweb_project_owner = {};
2247         # read from file (url-encoded):
2248         # 'git%2Fgit.git Linus+Torvalds'
2249         # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2250         # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2251         if (-f $projects_list) {
2252                 open (my $fd , $projects_list);
2253                 while (my $line = <$fd>) {
2254                         chomp $line;
2255                         my ($pr, $ow) = split ' ', $line;
2256                         $pr = unescape($pr);
2257                         $ow = unescape($ow);
2258                         $gitweb_project_owner->{$pr} = to_utf8($ow);
2259                 }
2260                 close $fd;
2261         }
2264 sub git_get_project_owner {
2265         my $project = shift;
2266         my $owner;
2268         return undef unless $project;
2269         $git_dir = "$projectroot/$project";
2271         if (!defined $gitweb_project_owner) {
2272                 git_get_project_list_from_file();
2273         }
2275         if (exists $gitweb_project_owner->{$project}) {
2276                 $owner = $gitweb_project_owner->{$project};
2277         }
2278         if (!defined $owner){
2279                 $owner = git_get_project_config('owner');
2280         }
2281         if (!defined $owner) {
2282                 $owner = get_file_owner("$git_dir");
2283         }
2285         return $owner;
2288 sub git_get_last_activity {
2289         my ($path) = @_;
2290         my $fd;
2292         $git_dir = "$projectroot/$path";
2293         open($fd, "-|", git_cmd(), 'for-each-ref',
2294              '--format=%(committer)',
2295              '--sort=-committerdate',
2296              '--count=1',
2297              'refs/heads') or return;
2298         my $most_recent = <$fd>;
2299         close $fd or return;
2300         if (defined $most_recent &&
2301             $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2302                 my $timestamp = $1;
2303                 my $age = time - $timestamp;
2304                 return ($age, age_string($age));
2305         }
2306         return (undef, undef);
2309 sub git_get_references {
2310         my $type = shift || "";
2311         my %refs;
2312         # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2313         # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2314         open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2315                 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2316                 or return;
2318         while (my $line = <$fd>) {
2319                 chomp $line;
2320                 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2321                         if (defined $refs{$1}) {
2322                                 push @{$refs{$1}}, $2;
2323                         } else {
2324                                 $refs{$1} = [ $2 ];
2325                         }
2326                 }
2327         }
2328         close $fd or return;
2329         return \%refs;
2332 sub git_get_rev_name_tags {
2333         my $hash = shift || return undef;
2335         open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2336                 or return;
2337         my $name_rev = <$fd>;
2338         close $fd;
2340         if ($name_rev =~ m|^$hash tags/(.*)$|) {
2341                 return $1;
2342         } else {
2343                 # catches also '$hash undefined' output
2344                 return undef;
2345         }
2348 ## ----------------------------------------------------------------------
2349 ## parse to hash functions
2351 sub parse_date {
2352         my $epoch = shift;
2353         my $tz = shift || "-0000";
2355         my %date;
2356         my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2357         my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2358         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2359         $date{'hour'} = $hour;
2360         $date{'minute'} = $min;
2361         $date{'mday'} = $mday;
2362         $date{'day'} = $days[$wday];
2363         $date{'month'} = $months[$mon];
2364         $date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2365                              $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2366         $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2367                              $mday, $months[$mon], $hour ,$min;
2368         $date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2369                              1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2371         $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2372         my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2373         ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2374         $date{'hour_local'} = $hour;
2375         $date{'minute_local'} = $min;
2376         $date{'tz_local'} = $tz;
2377         $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2378                                   1900+$year, $mon+1, $mday,
2379                                   $hour, $min, $sec, $tz);
2380         return %date;
2383 sub parse_tag {
2384         my $tag_id = shift;
2385         my %tag;
2386         my @comment;
2388         open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2389         $tag{'id'} = $tag_id;
2390         while (my $line = <$fd>) {
2391                 chomp $line;
2392                 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2393                         $tag{'object'} = $1;
2394                 } elsif ($line =~ m/^type (.+)$/) {
2395                         $tag{'type'} = $1;
2396                 } elsif ($line =~ m/^tag (.+)$/) {
2397                         $tag{'name'} = $1;
2398                 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2399                         $tag{'author'} = $1;
2400                         $tag{'epoch'} = $2;
2401                         $tag{'tz'} = $3;
2402                 } elsif ($line =~ m/--BEGIN/) {
2403                         push @comment, $line;
2404                         last;
2405                 } elsif ($line eq "") {
2406                         last;
2407                 }
2408         }
2409         push @comment, <$fd>;
2410         $tag{'comment'} = \@comment;
2411         close $fd or return;
2412         if (!defined $tag{'name'}) {
2413                 return
2414         };
2415         return %tag
2418 sub parse_commit_text {
2419         my ($commit_text, $withparents) = @_;
2420         my @commit_lines = split '\n', $commit_text;
2421         my %co;
2423         pop @commit_lines; # Remove '\0'
2425         if (! @commit_lines) {
2426                 return;
2427         }
2429         my $header = shift @commit_lines;
2430         if ($header !~ m/^[0-9a-fA-F]{40}/) {
2431                 return;
2432         }
2433         ($co{'id'}, my @parents) = split ' ', $header;
2434         while (my $line = shift @commit_lines) {
2435                 last if $line eq "\n";
2436                 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2437                         $co{'tree'} = $1;
2438                 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2439                         push @parents, $1;
2440                 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2441                         $co{'author'} = $1;
2442                         $co{'author_epoch'} = $2;
2443                         $co{'author_tz'} = $3;
2444                         if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2445                                 $co{'author_name'}  = $1;
2446                                 $co{'author_email'} = $2;
2447                         } else {
2448                                 $co{'author_name'} = $co{'author'};
2449                         }
2450                 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2451                         $co{'committer'} = $1;
2452                         $co{'committer_epoch'} = $2;
2453                         $co{'committer_tz'} = $3;
2454                         $co{'committer_name'} = $co{'committer'};
2455                         if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2456                                 $co{'committer_name'}  = $1;
2457                                 $co{'committer_email'} = $2;
2458                         } else {
2459                                 $co{'committer_name'} = $co{'committer'};
2460                         }
2461                 }
2462         }
2463         if (!defined $co{'tree'}) {
2464                 return;
2465         };
2466         $co{'parents'} = \@parents;
2467         $co{'parent'} = $parents[0];
2469         foreach my $title (@commit_lines) {
2470                 $title =~ s/^    //;
2471                 if ($title ne "") {
2472                         $co{'title'} = chop_str($title, 80, 5);
2473                         # remove leading stuff of merges to make the interesting part visible
2474                         if (length($title) > 50) {
2475                                 $title =~ s/^Automatic //;
2476                                 $title =~ s/^merge (of|with) /Merge ... /i;
2477                                 if (length($title) > 50) {
2478                                         $title =~ s/(http|rsync):\/\///;
2479                                 }
2480                                 if (length($title) > 50) {
2481                                         $title =~ s/(master|www|rsync)\.//;
2482                                 }
2483                                 if (length($title) > 50) {
2484                                         $title =~ s/kernel.org:?//;
2485                                 }
2486                                 if (length($title) > 50) {
2487                                         $title =~ s/\/pub\/scm//;
2488                                 }
2489                         }
2490                         $co{'title_short'} = chop_str($title, 50, 5);
2491                         last;
2492                 }
2493         }
2494         if (! defined $co{'title'} || $co{'title'} eq "") {
2495                 $co{'title'} = $co{'title_short'} = '(no commit message)';
2496         }
2497         # remove added spaces
2498         foreach my $line (@commit_lines) {
2499                 $line =~ s/^    //;
2500         }
2501         $co{'comment'} = \@commit_lines;
2503         my $age = time - $co{'committer_epoch'};
2504         $co{'age'} = $age;
2505         $co{'age_string'} = age_string($age);
2506         my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2507         if ($age > 60*60*24*7*2) {
2508                 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2509                 $co{'age_string_age'} = $co{'age_string'};
2510         } else {
2511                 $co{'age_string_date'} = $co{'age_string'};
2512                 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2513         }
2514         return %co;
2517 sub parse_commit {
2518         my ($commit_id) = @_;
2519         my %co;
2521         local $/ = "\0";
2523         open my $fd, "-|", git_cmd(), "rev-list",
2524                 "--parents",
2525                 "--header",
2526                 "--max-count=1",
2527                 $commit_id,
2528                 "--",
2529                 or die_error(500, "Open git-rev-list failed");
2530         %co = parse_commit_text(<$fd>, 1);
2531         close $fd;
2533         return %co;
2536 sub parse_commits {
2537         my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2538         my @cos;
2540         $maxcount ||= 1;
2541         $skip ||= 0;
2543         local $/ = "\0";
2545         open my $fd, "-|", git_cmd(), "rev-list",
2546                 "--header",
2547                 @args,
2548                 ("--max-count=" . $maxcount),
2549                 ("--skip=" . $skip),
2550                 @extra_options,
2551                 $commit_id,
2552                 "--",
2553                 ($filename ? ($filename) : ())
2554                 or die_error(500, "Open git-rev-list failed");
2555         while (my $line = <$fd>) {
2556                 my %co = parse_commit_text($line);
2557                 push @cos, \%co;
2558         }
2559         close $fd;
2561         return wantarray ? @cos : \@cos;
2564 # parse line of git-diff-tree "raw" output
2565 sub parse_difftree_raw_line {
2566         my $line = shift;
2567         my %res;
2569         # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M   ls-files.c'
2570         # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M   rev-tree.c'
2571         if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2572                 $res{'from_mode'} = $1;
2573                 $res{'to_mode'} = $2;
2574                 $res{'from_id'} = $3;
2575                 $res{'to_id'} = $4;
2576                 $res{'status'} = $5;
2577                 $res{'similarity'} = $6;
2578                 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2579                         ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2580                 } else {
2581                         $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2582                 }
2583         }
2584         # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2585         # combined diff (for merge commit)
2586         elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2587                 $res{'nparents'}  = length($1);
2588                 $res{'from_mode'} = [ split(' ', $2) ];
2589                 $res{'to_mode'} = pop @{$res{'from_mode'}};
2590                 $res{'from_id'} = [ split(' ', $3) ];
2591                 $res{'to_id'} = pop @{$res{'from_id'}};
2592                 $res{'status'} = [ split('', $4) ];
2593                 $res{'to_file'} = unquote($5);
2594         }
2595         # 'c512b523472485aef4fff9e57b229d9d243c967f'
2596         elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2597                 $res{'commit'} = $1;
2598         }
2600         return wantarray ? %res : \%res;
2603 # wrapper: return parsed line of git-diff-tree "raw" output
2604 # (the argument might be raw line, or parsed info)
2605 sub parsed_difftree_line {
2606         my $line_or_ref = shift;
2608         if (ref($line_or_ref) eq "HASH") {
2609                 # pre-parsed (or generated by hand)
2610                 return $line_or_ref;
2611         } else {
2612                 return parse_difftree_raw_line($line_or_ref);
2613         }
2616 # parse line of git-ls-tree output
2617 sub parse_ls_tree_line ($;%) {
2618         my $line = shift;
2619         my %opts = @_;
2620         my %res;
2622         #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
2623         $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2625         $res{'mode'} = $1;
2626         $res{'type'} = $2;
2627         $res{'hash'} = $3;
2628         if ($opts{'-z'}) {
2629                 $res{'name'} = $4;
2630         } else {
2631                 $res{'name'} = unquote($4);
2632         }
2634         return wantarray ? %res : \%res;
2637 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2638 sub parse_from_to_diffinfo {
2639         my ($diffinfo, $from, $to, @parents) = @_;
2641         if ($diffinfo->{'nparents'}) {
2642                 # combined diff
2643                 $from->{'file'} = [];
2644                 $from->{'href'} = [];
2645                 fill_from_file_info($diffinfo, @parents)
2646                         unless exists $diffinfo->{'from_file'};
2647                 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2648                         $from->{'file'}[$i] =
2649                                 defined $diffinfo->{'from_file'}[$i] ?
2650                                         $diffinfo->{'from_file'}[$i] :
2651                                         $diffinfo->{'to_file'};
2652                         if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2653                                 $from->{'href'}[$i] = href(action=>"blob",
2654                                                            hash_base=>$parents[$i],
2655                                                            hash=>$diffinfo->{'from_id'}[$i],
2656                                                            file_name=>$from->{'file'}[$i]);
2657                         } else {
2658                                 $from->{'href'}[$i] = undef;
2659                         }
2660                 }
2661         } else {
2662                 # ordinary (not combined) diff
2663                 $from->{'file'} = $diffinfo->{'from_file'};
2664                 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2665                         $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2666                                                hash=>$diffinfo->{'from_id'},
2667                                                file_name=>$from->{'file'});
2668                 } else {
2669                         delete $from->{'href'};
2670                 }
2671         }
2673         $to->{'file'} = $diffinfo->{'to_file'};
2674         if (!is_deleted($diffinfo)) { # file exists in result
2675                 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2676                                      hash=>$diffinfo->{'to_id'},
2677                                      file_name=>$to->{'file'});
2678         } else {
2679                 delete $to->{'href'};
2680         }
2683 ## ......................................................................
2684 ## parse to array of hashes functions
2686 sub git_get_heads_list {
2687         my $limit = shift;
2688         my @headslist;
2690         open my $fd, '-|', git_cmd(), 'for-each-ref',
2691                 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2692                 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2693                 'refs/heads'
2694                 or return;
2695         while (my $line = <$fd>) {
2696                 my %ref_item;
2698                 chomp $line;
2699                 my ($refinfo, $committerinfo) = split(/\0/, $line);
2700                 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2701                 my ($committer, $epoch, $tz) =
2702                         ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2703                 $ref_item{'fullname'}  = $name;
2704                 $name =~ s!^refs/heads/!!;
2706                 $ref_item{'name'}  = $name;
2707                 $ref_item{'id'}    = $hash;
2708                 $ref_item{'title'} = $title || '(no commit message)';
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                 }
2716                 push @headslist, \%ref_item;
2717         }
2718         close $fd;
2720         return wantarray ? @headslist : \@headslist;
2723 sub git_get_tags_list {
2724         my $limit = shift;
2725         my @tagslist;
2727         open my $fd, '-|', git_cmd(), 'for-each-ref',
2728                 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2729                 '--format=%(objectname) %(objecttype) %(refname) '.
2730                 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2731                 'refs/tags'
2732                 or return;
2733         while (my $line = <$fd>) {
2734                 my %ref_item;
2736                 chomp $line;
2737                 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2738                 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2739                 my ($creator, $epoch, $tz) =
2740                         ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2741                 $ref_item{'fullname'} = $name;
2742                 $name =~ s!^refs/tags/!!;
2744                 $ref_item{'type'} = $type;
2745                 $ref_item{'id'} = $id;
2746                 $ref_item{'name'} = $name;
2747                 if ($type eq "tag") {
2748                         $ref_item{'subject'} = $title;
2749                         $ref_item{'reftype'} = $reftype;
2750                         $ref_item{'refid'}   = $refid;
2751                 } else {
2752                         $ref_item{'reftype'} = $type;
2753                         $ref_item{'refid'}   = $id;
2754                 }
2756                 if ($type eq "tag" || $type eq "commit") {
2757                         $ref_item{'epoch'} = $epoch;
2758                         if ($epoch) {
2759                                 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2760                         } else {
2761                                 $ref_item{'age'} = "unknown";
2762                         }
2763                 }
2765                 push @tagslist, \%ref_item;
2766         }
2767         close $fd;
2769         return wantarray ? @tagslist : \@tagslist;
2772 ## ----------------------------------------------------------------------
2773 ## filesystem-related functions
2775 sub get_file_owner {
2776         my $path = shift;
2778         my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2779         my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2780         if (!defined $gcos) {
2781                 return undef;
2782         }
2783         my $owner = $gcos;
2784         $owner =~ s/[,;].*$//;
2785         return to_utf8($owner);
2788 # assume that file exists
2789 sub insert_file {
2790         my $filename = shift;
2792         open my $fd, '<', $filename;
2793         print map(to_utf8, <$fd>);
2794         close $fd;
2797 ## ......................................................................
2798 ## mimetype related functions
2800 sub mimetype_guess_file {
2801         my $filename = shift;
2802         my $mimemap = shift;
2803         -r $mimemap or return undef;
2805         my %mimemap;
2806         open(MIME, $mimemap) or return undef;
2807         while (<MIME>) {
2808                 next if m/^#/; # skip comments
2809                 my ($mime, $exts) = split(/\t+/);
2810                 if (defined $exts) {
2811                         my @exts = split(/\s+/, $exts);
2812                         foreach my $ext (@exts) {
2813                                 $mimemap{$ext} = $mime;
2814                         }
2815                 }
2816         }
2817         close(MIME);
2819         $filename =~ /\.([^.]*)$/;
2820         return $mimemap{$1};
2823 sub mimetype_guess {
2824         my $filename = shift;
2825         my $mime;
2826         $filename =~ /\./ or return undef;
2828         if ($mimetypes_file) {
2829                 my $file = $mimetypes_file;
2830                 if ($file !~ m!^/!) { # if it is relative path
2831                         # it is relative to project
2832                         $file = "$projectroot/$project/$file";
2833                 }
2834                 $mime = mimetype_guess_file($filename, $file);
2835         }
2836         $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2837         return $mime;
2840 sub blob_mimetype {
2841         my $fd = shift;
2842         my $filename = shift;
2844         if ($filename) {
2845                 my $mime = mimetype_guess($filename);
2846                 $mime and return $mime;
2847         }
2849         # just in case
2850         return $default_blob_plain_mimetype unless $fd;
2852         if (-T $fd) {
2853                 return 'text/plain';
2854         } elsif (! $filename) {
2855                 return 'application/octet-stream';
2856         } elsif ($filename =~ m/\.png$/i) {
2857                 return 'image/png';
2858         } elsif ($filename =~ m/\.gif$/i) {
2859                 return 'image/gif';
2860         } elsif ($filename =~ m/\.jpe?g$/i) {
2861                 return 'image/jpeg';
2862         } else {
2863                 return 'application/octet-stream';
2864         }
2867 sub blob_contenttype {
2868         my ($fd, $file_name, $type) = @_;
2870         $type ||= blob_mimetype($fd, $file_name);
2871         if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2872                 $type .= "; charset=$default_text_plain_charset";
2873         }
2875         return $type;
2878 ## ======================================================================
2879 ## functions printing HTML: header, footer, error page
2881 sub git_header_html {
2882         my $status = shift || "200 OK";
2883         my $expires = shift;
2885         my $title = "$site_name";
2886         if (defined $project) {
2887                 $title .= " - " . to_utf8($project);
2888                 if (defined $action) {
2889                         $title .= "/$action";
2890                         if (defined $file_name) {
2891                                 $title .= " - " . esc_path($file_name);
2892                                 if ($action eq "tree" && $file_name !~ m|/$|) {
2893                                         $title .= "/";
2894                                 }
2895                         }
2896                 }
2897         }
2898         my $content_type;
2899         # require explicit support from the UA if we are to send the page as
2900         # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2901         # we have to do this because MSIE sometimes globs '*/*', pretending to
2902         # support xhtml+xml but choking when it gets what it asked for.
2903         if (defined $cgi->http('HTTP_ACCEPT') &&
2904             $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2905             $cgi->Accept('application/xhtml+xml') != 0) {
2906                 $content_type = 'application/xhtml+xml';
2907         } else {
2908                 $content_type = 'text/html';
2909         }
2910         print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2911                            -status=> $status, -expires => $expires);
2912         my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2913         print <<EOF;
2914 <?xml version="1.0" encoding="utf-8"?>
2915 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2916 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2917 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2918 <!-- git core binaries version $git_version -->
2919 <head>
2920 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2921 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2922 <meta name="robots" content="index, nofollow"/>
2923 <title>$title</title>
2924 EOF
2925 # print out each stylesheet that exist
2926         if (defined $stylesheet) {
2927 #provides backwards capability for those people who define style sheet in a config file
2928                 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2929         } else {
2930                 foreach my $stylesheet (@stylesheets) {
2931                         next unless $stylesheet;
2932                         print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2933                 }
2934         }
2935         if (defined $project) {
2936                 my %href_params = get_feed_info();
2937                 if (!exists $href_params{'-title'}) {
2938                         $href_params{'-title'} = 'log';
2939                 }
2941                 foreach my $format qw(RSS Atom) {
2942                         my $type = lc($format);
2943                         my %link_attr = (
2944                                 '-rel' => 'alternate',
2945                                 '-title' => "$project - $href_params{'-title'} - $format feed",
2946                                 '-type' => "application/$type+xml"
2947                         );
2949                         $href_params{'action'} = $type;
2950                         $link_attr{'-href'} = href(%href_params);
2951                         print "<link ".
2952                               "rel=\"$link_attr{'-rel'}\" ".
2953                               "title=\"$link_attr{'-title'}\" ".
2954                               "href=\"$link_attr{'-href'}\" ".
2955                               "type=\"$link_attr{'-type'}\" ".
2956                               "/>\n";
2958                         $href_params{'extra_options'} = '--no-merges';
2959                         $link_attr{'-href'} = href(%href_params);
2960                         $link_attr{'-title'} .= ' (no merges)';
2961                         print "<link ".
2962                               "rel=\"$link_attr{'-rel'}\" ".
2963                               "title=\"$link_attr{'-title'}\" ".
2964                               "href=\"$link_attr{'-href'}\" ".
2965                               "type=\"$link_attr{'-type'}\" ".
2966                               "/>\n";
2967                 }
2969         } else {
2970                 printf('<link rel="alternate" title="%s projects list" '.
2971                        'href="%s" type="text/plain; charset=utf-8" />'."\n",
2972                        $site_name, href(project=>undef, action=>"project_index"));
2973                 printf('<link rel="alternate" title="%s projects feeds" '.
2974                        'href="%s" type="text/x-opml" />'."\n",
2975                        $site_name, href(project=>undef, action=>"opml"));
2976         }
2977         if (defined $favicon) {
2978                 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2979         }
2981         print "</head>\n" .
2982               "<body>\n";
2984         if (-f $site_header) {
2985                 insert_file($site_header);
2986         }
2988         print "<div class=\"page_header\">\n" .
2989               $cgi->a({-href => esc_url($logo_url),
2990                        -title => $logo_label},
2991                       qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2992         print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2993         if (defined $project) {
2994                 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2995                 if (defined $action) {
2996                         print " / $action";
2997                 }
2998                 print "\n";
2999         }
3000         print "</div>\n";
3002         my $have_search = gitweb_check_feature('search');
3003         if (defined $project && $have_search) {
3004                 if (!defined $searchtext) {
3005                         $searchtext = "";
3006                 }
3007                 my $search_hash;
3008                 if (defined $hash_base) {
3009                         $search_hash = $hash_base;
3010                 } elsif (defined $hash) {
3011                         $search_hash = $hash;
3012                 } else {
3013                         $search_hash = "HEAD";
3014                 }
3015                 my $action = $my_uri;
3016                 my $use_pathinfo = gitweb_check_feature('pathinfo');
3017                 if ($use_pathinfo) {
3018                         $action .= "/".esc_url($project);
3019                 }
3020                 print $cgi->startform(-method => "get", -action => $action) .
3021                       "<div class=\"search\">\n" .
3022                       (!$use_pathinfo &&
3023                       $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3024                       $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3025                       $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3026                       $cgi->popup_menu(-name => 'st', -default => 'commit',
3027                                        -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3028                       $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3029                       " search:\n",
3030                       $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3031                       "<span title=\"Extended regular expression\">" .
3032                       $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3033                                      -checked => $search_use_regexp) .
3034                       "</span>" .
3035                       "</div>" .
3036                       $cgi->end_form() . "\n";
3037         }
3040 sub git_footer_html {
3041         my $feed_class = 'rss_logo';
3043         print "<div class=\"page_footer\">\n";
3044         if (defined $project) {
3045                 my $descr = git_get_project_description($project);
3046                 if (defined $descr) {
3047                         print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3048                 }
3050                 my %href_params = get_feed_info();
3051                 if (!%href_params) {
3052                         $feed_class .= ' generic';
3053                 }
3054                 $href_params{'-title'} ||= 'log';
3056                 foreach my $format qw(RSS Atom) {
3057                         $href_params{'action'} = lc($format);
3058                         print $cgi->a({-href => href(%href_params),
3059                                       -title => "$href_params{'-title'} $format feed",
3060                                       -class => $feed_class}, $format)."\n";
3061                 }
3063         } else {
3064                 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3065                               -class => $feed_class}, "OPML") . " ";
3066                 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3067                               -class => $feed_class}, "TXT") . "\n";
3068         }
3069         print "</div>\n"; # class="page_footer"
3071         if (-f $site_footer) {
3072                 insert_file($site_footer);
3073         }
3075         print "</body>\n" .
3076               "</html>";
3079 # die_error(<http_status_code>, <error_message>)
3080 # Example: die_error(404, 'Hash not found')
3081 # By convention, use the following status codes (as defined in RFC 2616):
3082 # 400: Invalid or missing CGI parameters, or
3083 #      requested object exists but has wrong type.
3084 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3085 #      this server or project.
3086 # 404: Requested object/revision/project doesn't exist.
3087 # 500: The server isn't configured properly, or
3088 #      an internal error occurred (e.g. failed assertions caused by bugs), or
3089 #      an unknown error occurred (e.g. the git binary died unexpectedly).
3090 sub die_error {
3091         my $status = shift || 500;
3092         my $error = shift || "Internal server error";
3094         my %http_responses = (400 => '400 Bad Request',
3095                               403 => '403 Forbidden',
3096                               404 => '404 Not Found',
3097                               500 => '500 Internal Server Error');
3098         git_header_html($http_responses{$status});
3099         print <<EOF;
3100 <div class="page_body">
3101 <br /><br />
3102 $status - $error
3103 <br />
3104 </div>
3105 EOF
3106         git_footer_html();
3107         exit;
3110 ## ----------------------------------------------------------------------
3111 ## functions printing or outputting HTML: navigation
3113 sub git_print_page_nav {
3114         my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3115         $extra = '' if !defined $extra; # pager or formats
3117         my @navs = qw(summary shortlog log commit commitdiff tree);
3118         if ($suppress) {
3119                 @navs = grep { $_ ne $suppress } @navs;
3120         }
3122         my %arg = map { $_ => {action=>$_} } @navs;
3123         if (defined $head) {
3124                 for (qw(commit commitdiff)) {
3125                         $arg{$_}{'hash'} = $head;
3126                 }
3127                 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3128                         for (qw(shortlog log)) {
3129                                 $arg{$_}{'hash'} = $head;
3130                         }
3131                 }
3132         }
3134         $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3135         $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3137         my @actions = gitweb_get_feature('actions');
3138         my %repl = (
3139                 '%' => '%',
3140                 'n' => $project,         # project name
3141                 'f' => $git_dir,         # project path within filesystem
3142                 'h' => $treehead || '',  # current hash ('h' parameter)
3143                 'b' => $treebase || '',  # hash base ('hb' parameter)
3144         );
3145         while (@actions) {
3146                 my ($label, $link, $pos) = splice(@actions,0,3);
3147                 # insert
3148                 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3149                 # munch munch
3150                 $link =~ s/%([%nfhb])/$repl{$1}/g;
3151                 $arg{$label}{'_href'} = $link;
3152         }
3154         print "<div class=\"page_nav\">\n" .
3155                 (join " | ",
3156                  map { $_ eq $current ?
3157                        $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3158                  } @navs);
3159         print "<br/>\n$extra<br/>\n" .
3160               "</div>\n";
3163 sub format_paging_nav {
3164         my ($action, $hash, $head, $page, $has_next_link) = @_;
3165         my $paging_nav;
3168         if ($hash ne $head || $page) {
3169                 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3170         } else {
3171                 $paging_nav .= "HEAD";
3172         }
3174         if ($page > 0) {
3175                 $paging_nav .= " &sdot; " .
3176                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
3177                                  -accesskey => "p", -title => "Alt-p"}, "prev");
3178         } else {
3179                 $paging_nav .= " &sdot; prev";
3180         }
3182         if ($has_next_link) {
3183                 $paging_nav .= " &sdot; " .
3184                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
3185                                  -accesskey => "n", -title => "Alt-n"}, "next");
3186         } else {
3187                 $paging_nav .= " &sdot; next";
3188         }
3190         return $paging_nav;
3193 ## ......................................................................
3194 ## functions printing or outputting HTML: div
3196 sub git_print_header_div {
3197         my ($action, $title, $hash, $hash_base) = @_;
3198         my %args = ();
3200         $args{'action'} = $action;
3201         $args{'hash'} = $hash if $hash;
3202         $args{'hash_base'} = $hash_base if $hash_base;
3204         print "<div class=\"header\">\n" .
3205               $cgi->a({-href => href(%args), -class => "title"},
3206               $title ? $title : $action) .
3207               "\n</div>\n";
3210 #sub git_print_authorship (\%) {
3211 sub git_print_authorship {
3212         my $co = shift;
3214         my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3215         print "<div class=\"author_date\">" .
3216               esc_html($co->{'author_name'}) .
3217               " [$ad{'rfc2822'}";
3218         if ($ad{'hour_local'} < 6) {
3219                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3220                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3221         } else {
3222                 printf(" (%02d:%02d %s)",
3223                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3224         }
3225         print "]</div>\n";
3228 sub git_print_page_path {
3229         my $name = shift;
3230         my $type = shift;
3231         my $hb = shift;
3234         print "<div class=\"page_path\">";
3235         print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3236                       -title => 'tree root'}, to_utf8("[$project]"));
3237         print " / ";
3238         if (defined $name) {
3239                 my @dirname = split '/', $name;
3240                 my $basename = pop @dirname;
3241                 my $fullname = '';
3243                 foreach my $dir (@dirname) {
3244                         $fullname .= ($fullname ? '/' : '') . $dir;
3245                         print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3246                                                      hash_base=>$hb),
3247                                       -title => $fullname}, esc_path($dir));
3248                         print " / ";
3249                 }
3250                 if (defined $type && $type eq 'blob') {
3251                         print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3252                                                      hash_base=>$hb),
3253                                       -title => $name}, esc_path($basename));
3254                 } elsif (defined $type && $type eq 'tree') {
3255                         print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3256                                                      hash_base=>$hb),
3257                                       -title => $name}, esc_path($basename));
3258                         print " / ";
3259                 } else {
3260                         print esc_path($basename);
3261                 }
3262         }
3263         print "<br/></div>\n";
3266 # sub git_print_log (\@;%) {
3267 sub git_print_log ($;%) {
3268         my $log = shift;
3269         my %opts = @_;
3271         if ($opts{'-remove_title'}) {
3272                 # remove title, i.e. first line of log
3273                 shift @$log;
3274         }
3275         # remove leading empty lines
3276         while (defined $log->[0] && $log->[0] eq "") {
3277                 shift @$log;
3278         }
3280         # print log
3281         my $signoff = 0;
3282         my $empty = 0;
3283         foreach my $line (@$log) {
3284                 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3285                         $signoff = 1;
3286                         $empty = 0;
3287                         if (! $opts{'-remove_signoff'}) {
3288                                 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3289                                 next;
3290                         } else {
3291                                 # remove signoff lines
3292                                 next;
3293                         }
3294                 } else {
3295                         $signoff = 0;
3296                 }
3298                 # print only one empty line
3299                 # do not print empty line after signoff
3300                 if ($line eq "") {
3301                         next if ($empty || $signoff);
3302                         $empty = 1;
3303                 } else {
3304                         $empty = 0;
3305                 }
3307                 print format_log_line_html($line) . "<br/>\n";
3308         }
3310         if ($opts{'-final_empty_line'}) {
3311                 # end with single empty line
3312                 print "<br/>\n" unless $empty;
3313         }
3316 # return link target (what link points to)
3317 sub git_get_link_target {
3318         my $hash = shift;
3319         my $link_target;
3321         # read link
3322         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3323                 or return;
3324         {
3325                 local $/;
3326                 $link_target = <$fd>;
3327         }
3328         close $fd
3329                 or return;
3331         return $link_target;
3334 # given link target, and the directory (basedir) the link is in,
3335 # return target of link relative to top directory (top tree);
3336 # return undef if it is not possible (including absolute links).
3337 sub normalize_link_target {
3338         my ($link_target, $basedir, $hash_base) = @_;
3340         # we can normalize symlink target only if $hash_base is provided
3341         return unless $hash_base;
3343         # absolute symlinks (beginning with '/') cannot be normalized
3344         return if (substr($link_target, 0, 1) eq '/');
3346         # normalize link target to path from top (root) tree (dir)
3347         my $path;
3348         if ($basedir) {
3349                 $path = $basedir . '/' . $link_target;
3350         } else {
3351                 # we are in top (root) tree (dir)
3352                 $path = $link_target;
3353         }
3355         # remove //, /./, and /../
3356         my @path_parts;
3357         foreach my $part (split('/', $path)) {
3358                 # discard '.' and ''
3359                 next if (!$part || $part eq '.');
3360                 # handle '..'
3361                 if ($part eq '..') {
3362                         if (@path_parts) {
3363                                 pop @path_parts;
3364                         } else {
3365                                 # link leads outside repository (outside top dir)
3366                                 return;
3367                         }
3368                 } else {
3369                         push @path_parts, $part;
3370                 }
3371         }
3372         $path = join('/', @path_parts);
3374         return $path;
3377 # print tree entry (row of git_tree), but without encompassing <tr> element
3378 sub git_print_tree_entry {
3379         my ($t, $basedir, $hash_base, $have_blame) = @_;
3381         my %base_key = ();
3382         $base_key{'hash_base'} = $hash_base if defined $hash_base;
3384         # The format of a table row is: mode list link.  Where mode is
3385         # the mode of the entry, list is the name of the entry, an href,
3386         # and link is the action links of the entry.
3388         print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3389         if ($t->{'type'} eq "blob") {
3390                 print "<td class=\"list\">" .
3391                         $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3392                                                file_name=>"$basedir$t->{'name'}", %base_key),
3393                                 -class => "list"}, esc_path($t->{'name'}));
3394                 if (S_ISLNK(oct $t->{'mode'})) {
3395                         my $link_target = git_get_link_target($t->{'hash'});
3396                         if ($link_target) {
3397                                 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3398                                 if (defined $norm_target) {
3399                                         print " -> " .
3400                                               $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3401                                                                      file_name=>$norm_target),
3402                                                        -title => $norm_target}, esc_path($link_target));
3403                                 } else {
3404                                         print " -> " . esc_path($link_target);
3405                                 }
3406                         }
3407                 }
3408                 print "</td>\n";
3409                 print "<td class=\"link\">";
3410                 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3411                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3412                               "blob");
3413                 if ($have_blame) {
3414                         print " | " .
3415                               $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3416                                                      file_name=>"$basedir$t->{'name'}", %base_key)},
3417                                       "blame");
3418                 }
3419                 if (defined $hash_base) {
3420                         print " | " .
3421                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3422                                                      hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3423                                       "history");
3424                 }
3425                 print " | " .
3426                         $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3427                                                file_name=>"$basedir$t->{'name'}")},
3428                                 "raw");
3429                 print "</td>\n";
3431         } elsif ($t->{'type'} eq "tree") {
3432                 print "<td class=\"list\">";
3433                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3434                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3435                               esc_path($t->{'name'}));
3436                 print "</td>\n";
3437                 print "<td class=\"link\">";
3438                 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3439                                              file_name=>"$basedir$t->{'name'}", %base_key)},
3440                               "tree");
3441                 if (defined $hash_base) {
3442                         print " | " .
3443                               $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3444                                                      file_name=>"$basedir$t->{'name'}")},
3445                                       "history");
3446                 }
3447                 print "</td>\n";
3448         } else {
3449                 # unknown object: we can only present history for it
3450                 # (this includes 'commit' object, i.e. submodule support)
3451                 print "<td class=\"list\">" .
3452                       esc_path($t->{'name'}) .
3453                       "</td>\n";
3454                 print "<td class=\"link\">";
3455                 if (defined $hash_base) {
3456                         print $cgi->a({-href => href(action=>"history",
3457                                                      hash_base=>$hash_base,
3458                                                      file_name=>"$basedir$t->{'name'}")},
3459                                       "history");
3460                 }
3461                 print "</td>\n";
3462         }
3465 ## ......................................................................
3466 ## functions printing large fragments of HTML
3468 # get pre-image filenames for merge (combined) diff
3469 sub fill_from_file_info {
3470         my ($diff, @parents) = @_;
3472         $diff->{'from_file'} = [ ];
3473         $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3474         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3475                 if ($diff->{'status'}[$i] eq 'R' ||
3476                     $diff->{'status'}[$i] eq 'C') {
3477                         $diff->{'from_file'}[$i] =
3478                                 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3479                 }
3480         }
3482         return $diff;
3485 # is current raw difftree line of file deletion
3486 sub is_deleted {
3487         my $diffinfo = shift;
3489         return $diffinfo->{'to_id'} eq ('0' x 40);
3492 # does patch correspond to [previous] difftree raw line
3493 # $diffinfo  - hashref of parsed raw diff format
3494 # $patchinfo - hashref of parsed patch diff format
3495 #              (the same keys as in $diffinfo)
3496 sub is_patch_split {
3497         my ($diffinfo, $patchinfo) = @_;
3499         return defined $diffinfo && defined $patchinfo
3500                 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3504 sub git_difftree_body {
3505         my ($difftree, $hash, @parents) = @_;
3506         my ($parent) = $parents[0];
3507         my $have_blame = gitweb_check_feature('blame');
3508         print "<div class=\"list_head\">\n";
3509         if ($#{$difftree} > 10) {
3510                 print(($#{$difftree} + 1) . " files changed:\n");
3511         }
3512         print "</div>\n";
3514         print "<table class=\"" .
3515               (@parents > 1 ? "combined " : "") .
3516               "diff_tree\">\n";
3518         # header only for combined diff in 'commitdiff' view
3519         my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3520         if ($has_header) {
3521                 # table header
3522                 print "<thead><tr>\n" .
3523                        "<th></th><th></th>\n"; # filename, patchN link
3524                 for (my $i = 0; $i < @parents; $i++) {
3525                         my $par = $parents[$i];
3526                         print "<th>" .
3527                               $cgi->a({-href => href(action=>"commitdiff",
3528                                                      hash=>$hash, hash_parent=>$par),
3529                                        -title => 'commitdiff to parent number ' .
3530                                                   ($i+1) . ': ' . substr($par,0,7)},
3531                                       $i+1) .
3532                               "&nbsp;</th>\n";
3533                 }
3534                 print "</tr></thead>\n<tbody>\n";
3535         }
3537         my $alternate = 1;
3538         my $patchno = 0;
3539         foreach my $line (@{$difftree}) {
3540                 my $diff = parsed_difftree_line($line);
3542                 if ($alternate) {
3543                         print "<tr class=\"dark\">\n";
3544                 } else {
3545                         print "<tr class=\"light\">\n";
3546                 }
3547                 $alternate ^= 1;
3549                 if (exists $diff->{'nparents'}) { # combined diff
3551                         fill_from_file_info($diff, @parents)
3552                                 unless exists $diff->{'from_file'};
3554                         if (!is_deleted($diff)) {
3555                                 # file exists in the result (child) commit
3556                                 print "<td>" .
3557                                       $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3558                                                              file_name=>$diff->{'to_file'},
3559                                                              hash_base=>$hash),
3560                                               -class => "list"}, esc_path($diff->{'to_file'})) .
3561                                       "</td>\n";
3562                         } else {
3563                                 print "<td>" .
3564                                       esc_path($diff->{'to_file'}) .
3565                                       "</td>\n";
3566                         }
3568                         if ($action eq 'commitdiff') {
3569                                 # link to patch
3570                                 $patchno++;
3571                                 print "<td class=\"link\">" .
3572                                       $cgi->a({-href => "#patch$patchno"}, "patch") .
3573                                       " | " .
3574                                       "</td>\n";
3575                         }
3577                         my $has_history = 0;
3578                         my $not_deleted = 0;
3579                         for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3580                                 my $hash_parent = $parents[$i];
3581                                 my $from_hash = $diff->{'from_id'}[$i];
3582                                 my $from_path = $diff->{'from_file'}[$i];
3583                                 my $status = $diff->{'status'}[$i];
3585                                 $has_history ||= ($status ne 'A');
3586                                 $not_deleted ||= ($status ne 'D');
3588                                 if ($status eq 'A') {
3589                                         print "<td  class=\"link\" align=\"right\"> | </td>\n";
3590                                 } elsif ($status eq 'D') {
3591                                         print "<td class=\"link\">" .
3592                                               $cgi->a({-href => href(action=>"blob",
3593                                                                      hash_base=>$hash,
3594                                                                      hash=>$from_hash,
3595                                                                      file_name=>$from_path)},
3596                                                       "blob" . ($i+1)) .
3597                                               " | </td>\n";
3598                                 } else {
3599                                         if ($diff->{'to_id'} eq $from_hash) {
3600                                                 print "<td class=\"link nochange\">";
3601                                         } else {
3602                                                 print "<td class=\"link\">";
3603                                         }
3604                                         print $cgi->a({-href => href(action=>"blobdiff",
3605                                                                      hash=>$diff->{'to_id'},
3606                                                                      hash_parent=>$from_hash,
3607                                                                      hash_base=>$hash,
3608                                                                      hash_parent_base=>$hash_parent,
3609                                                                      file_name=>$diff->{'to_file'},
3610                                                                      file_parent=>$from_path)},
3611                                                       "diff" . ($i+1)) .
3612                                               " | </td>\n";
3613                                 }
3614                         }
3616                         print "<td class=\"link\">";
3617                         if ($not_deleted) {
3618                                 print $cgi->a({-href => href(action=>"blob",
3619                                                              hash=>$diff->{'to_id'},
3620                                                              file_name=>$diff->{'to_file'},
3621                                                              hash_base=>$hash)},
3622                                               "blob");
3623                                 print " | " if ($has_history);
3624                         }
3625                         if ($has_history) {
3626                                 print $cgi->a({-href => href(action=>"history",
3627                                                              file_name=>$diff->{'to_file'},
3628                                                              hash_base=>$hash)},
3629                                               "history");
3630                         }
3631                         print "</td>\n";
3633                         print "</tr>\n";
3634                         next; # instead of 'else' clause, to avoid extra indent
3635                 }
3636                 # else ordinary diff
3638                 my ($to_mode_oct, $to_mode_str, $to_file_type);
3639                 my ($from_mode_oct, $from_mode_str, $from_file_type);
3640                 if ($diff->{'to_mode'} ne ('0' x 6)) {
3641                         $to_mode_oct = oct $diff->{'to_mode'};
3642                         if (S_ISREG($to_mode_oct)) { # only for regular file
3643                                 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3644                         }
3645                         $to_file_type = file_type($diff->{'to_mode'});
3646                 }
3647                 if ($diff->{'from_mode'} ne ('0' x 6)) {
3648                         $from_mode_oct = oct $diff->{'from_mode'};
3649                         if (S_ISREG($to_mode_oct)) { # only for regular file
3650                                 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3651                         }
3652                         $from_file_type = file_type($diff->{'from_mode'});
3653                 }
3655                 if ($diff->{'status'} eq "A") { # created
3656                         my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3657                         $mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
3658                         $mode_chng   .= "]</span>";
3659                         print "<td>";
3660                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3661                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3662                                       -class => "list"}, esc_path($diff->{'file'}));
3663                         print "</td>\n";
3664                         print "<td>$mode_chng</td>\n";
3665                         print "<td class=\"link\">";
3666                         if ($action eq 'commitdiff') {
3667                                 # link to patch
3668                                 $patchno++;
3669                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3670                                 print " | ";
3671                         }
3672                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3673                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3674                                       "blob");
3675                         print "</td>\n";
3677                 } elsif ($diff->{'status'} eq "D") { # deleted
3678                         my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3679                         print "<td>";
3680                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3681                                                      hash_base=>$parent, file_name=>$diff->{'file'}),
3682                                        -class => "list"}, esc_path($diff->{'file'}));
3683                         print "</td>\n";
3684                         print "<td>$mode_chng</td>\n";
3685                         print "<td class=\"link\">";
3686                         if ($action eq 'commitdiff') {
3687                                 # link to patch
3688                                 $patchno++;
3689                                 print $cgi->a({-href => "#patch$patchno"}, "patch");
3690                                 print " | ";
3691                         }
3692                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3693                                                      hash_base=>$parent, file_name=>$diff->{'file'})},
3694                                       "blob") . " | ";
3695                         if ($have_blame) {
3696                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3697                                                              file_name=>$diff->{'file'})},
3698                                               "blame") . " | ";
3699                         }
3700                         print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3701                                                      file_name=>$diff->{'file'})},
3702                                       "history");
3703                         print "</td>\n";
3705                 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3706                         my $mode_chnge = "";
3707                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3708                                 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3709                                 if ($from_file_type ne $to_file_type) {
3710                                         $mode_chnge .= " from $from_file_type to $to_file_type";
3711                                 }
3712                                 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3713                                         if ($from_mode_str && $to_mode_str) {
3714                                                 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3715                                         } elsif ($to_mode_str) {
3716                                                 $mode_chnge .= " mode: $to_mode_str";
3717                                         }
3718                                 }
3719                                 $mode_chnge .= "]</span>\n";
3720                         }
3721                         print "<td>";
3722                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3723                                                      hash_base=>$hash, file_name=>$diff->{'file'}),
3724                                       -class => "list"}, esc_path($diff->{'file'}));
3725                         print "</td>\n";
3726                         print "<td>$mode_chnge</td>\n";
3727                         print "<td class=\"link\">";
3728                         if ($action eq 'commitdiff') {
3729                                 # link to patch
3730                                 $patchno++;
3731                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3732                                       " | ";
3733                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3734                                 # "commit" view and modified file (not onlu mode changed)
3735                                 print $cgi->a({-href => href(action=>"blobdiff",
3736                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3737                                                              hash_base=>$hash, hash_parent_base=>$parent,
3738                                                              file_name=>$diff->{'file'})},
3739                                               "diff") .
3740                                       " | ";
3741                         }
3742                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3743                                                      hash_base=>$hash, file_name=>$diff->{'file'})},
3744                                        "blob") . " | ";
3745                         if ($have_blame) {
3746                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3747                                                              file_name=>$diff->{'file'})},
3748                                               "blame") . " | ";
3749                         }
3750                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3751                                                      file_name=>$diff->{'file'})},
3752                                       "history");
3753                         print "</td>\n";
3755                 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3756                         my %status_name = ('R' => 'moved', 'C' => 'copied');
3757                         my $nstatus = $status_name{$diff->{'status'}};
3758                         my $mode_chng = "";
3759                         if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3760                                 # mode also for directories, so we cannot use $to_mode_str
3761                                 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3762                         }
3763                         print "<td>" .
3764                               $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3765                                                      hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3766                                       -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3767                               "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3768                               $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3769                                                      hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3770                                       -class => "list"}, esc_path($diff->{'from_file'})) .
3771                               " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3772                               "<td class=\"link\">";
3773                         if ($action eq 'commitdiff') {
3774                                 # link to patch
3775                                 $patchno++;
3776                                 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3777                                       " | ";
3778                         } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3779                                 # "commit" view and modified file (not only pure rename or copy)
3780                                 print $cgi->a({-href => href(action=>"blobdiff",
3781                                                              hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3782                                                              hash_base=>$hash, hash_parent_base=>$parent,
3783                                                              file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3784                                               "diff") .
3785                                       " | ";
3786                         }
3787                         print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3788                                                      hash_base=>$parent, file_name=>$diff->{'to_file'})},
3789                                       "blob") . " | ";
3790                         if ($have_blame) {
3791                                 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3792                                                              file_name=>$diff->{'to_file'})},
3793                                               "blame") . " | ";
3794                         }
3795                         print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3796                                                     file_name=>$diff->{'to_file'})},
3797                                       "history");
3798                         print "</td>\n";
3800                 } # we should not encounter Unmerged (U) or Unknown (X) status
3801                 print "</tr>\n";
3802         }
3803         print "</tbody>" if $has_header;
3804         print "</table>\n";
3807 sub git_patchset_body {
3808         my ($fd, $difftree, $hash, @hash_parents) = @_;
3809         my ($hash_parent) = $hash_parents[0];
3811         my $is_combined = (@hash_parents > 1);
3812         my $patch_idx = 0;
3813         my $patch_number = 0;
3814         my $patch_line;
3815         my $diffinfo;
3816         my $to_name;
3817         my (%from, %to);
3819         print "<div class=\"patchset\">\n";
3821         # skip to first patch
3822         while ($patch_line = <$fd>) {
3823                 chomp $patch_line;
3825                 last if ($patch_line =~ m/^diff /);
3826         }
3828  PATCH:
3829         while ($patch_line) {
3831                 # parse "git diff" header line
3832                 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3833                         # $1 is from_name, which we do not use
3834                         $to_name = unquote($2);
3835                         $to_name =~ s!^b/!!;
3836                 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3837                         # $1 is 'cc' or 'combined', which we do not use
3838                         $to_name = unquote($2);
3839                 } else {
3840                         $to_name = undef;
3841                 }
3843                 # check if current patch belong to current raw line
3844                 # and parse raw git-diff line if needed
3845                 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3846                         # this is continuation of a split patch
3847                         print "<div class=\"patch cont\">\n";
3848                 } else {
3849                         # advance raw git-diff output if needed
3850                         $patch_idx++ if defined $diffinfo;
3852                         # read and prepare patch information
3853                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3855                         # compact combined diff output can have some patches skipped
3856                         # find which patch (using pathname of result) we are at now;
3857                         if ($is_combined) {
3858                                 while ($to_name ne $diffinfo->{'to_file'}) {
3859                                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3860                                               format_diff_cc_simplified($diffinfo, @hash_parents) .
3861                                               "</div>\n";  # class="patch"
3863                                         $patch_idx++;
3864                                         $patch_number++;
3866                                         last if $patch_idx > $#$difftree;
3867                                         $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3868                                 }
3869                         }
3871                         # modifies %from, %to hashes
3872                         parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3874                         # this is first patch for raw difftree line with $patch_idx index
3875                         # we index @$difftree array from 0, but number patches from 1
3876                         print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3877                 }
3879                 # git diff header
3880                 #assert($patch_line =~ m/^diff /) if DEBUG;
3881                 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3882                 $patch_number++;
3883                 # print "git diff" header
3884                 print format_git_diff_header_line($patch_line, $diffinfo,
3885                                                   \%from, \%to);
3887                 # print extended diff header
3888                 print "<div class=\"diff extended_header\">\n";
3889         EXTENDED_HEADER:
3890                 while ($patch_line = <$fd>) {
3891                         chomp $patch_line;
3893                         last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3895                         print format_extended_diff_header_line($patch_line, $diffinfo,
3896                                                                \%from, \%to);
3897                 }
3898                 print "</div>\n"; # class="diff extended_header"
3900                 # from-file/to-file diff header
3901                 if (! $patch_line) {
3902                         print "</div>\n"; # class="patch"
3903                         last PATCH;
3904                 }
3905                 next PATCH if ($patch_line =~ m/^diff /);
3906                 #assert($patch_line =~ m/^---/) if DEBUG;
3908                 my $last_patch_line = $patch_line;
3909                 $patch_line = <$fd>;
3910                 chomp $patch_line;
3911                 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3913                 print format_diff_from_to_header($last_patch_line, $patch_line,
3914                                                  $diffinfo, \%from, \%to,
3915                                                  @hash_parents);
3917                 # the patch itself
3918         LINE:
3919                 while ($patch_line = <$fd>) {
3920                         chomp $patch_line;
3922                         next PATCH if ($patch_line =~ m/^diff /);
3924                         print format_diff_line($patch_line, \%from, \%to);
3925                 }
3927         } continue {
3928                 print "</div>\n"; # class="patch"
3929         }
3931         # for compact combined (--cc) format, with chunk and patch simpliciaction
3932         # patchset might be empty, but there might be unprocessed raw lines
3933         for (++$patch_idx if $patch_number > 0;
3934              $patch_idx < @$difftree;
3935              ++$patch_idx) {
3936                 # read and prepare patch information
3937                 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3939                 # generate anchor for "patch" links in difftree / whatchanged part
3940                 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3941                       format_diff_cc_simplified($diffinfo, @hash_parents) .
3942                       "</div>\n";  # class="patch"
3944                 $patch_number++;
3945         }
3947         if ($patch_number == 0) {
3948                 if (@hash_parents > 1) {
3949                         print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3950                 } else {
3951                         print "<div class=\"diff nodifferences\">No differences found</div>\n";
3952                 }
3953         }
3955         print "</div>\n"; # class="patchset"
3958 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3960 # fills project list info (age, description, owner, forks) for each
3961 # project in the list, removing invalid projects from returned list
3962 # NOTE: modifies $projlist, but does not remove entries from it
3963 sub fill_project_list_info {
3964         my ($projlist, $check_forks) = @_;
3965         my @projects;
3967         my $show_ctags = gitweb_check_feature('ctags');
3968  PROJECT:
3969         foreach my $pr (@$projlist) {
3970                 my (@activity) = git_get_last_activity($pr->{'path'});
3971                 unless (@activity) {
3972                         next PROJECT;
3973                 }
3974                 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3975                 if (!defined $pr->{'descr'}) {
3976                         my $descr = git_get_project_description($pr->{'path'}) || "";
3977                         $descr = to_utf8($descr);
3978                         $pr->{'descr_long'} = $descr;
3979                         $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3980                 }
3981                 if (!defined $pr->{'owner'}) {
3982                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3983                 }
3984                 if ($check_forks) {
3985                         my $pname = $pr->{'path'};
3986                         if (($pname =~ s/\.git$//) &&
3987                             ($pname !~ /\/$/) &&
3988                             (-d "$projectroot/$pname")) {
3989                                 $pr->{'forks'} = "-d $projectroot/$pname";
3990                         }       else {
3991                                 $pr->{'forks'} = 0;
3992                         }
3993                 }
3994                 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3995                 push @projects, $pr;
3996         }
3998         return @projects;
4001 # print 'sort by' <th> element, generating 'sort by $name' replay link
4002 # if that order is not selected
4003 sub print_sort_th {
4004         my ($name, $order, $header) = @_;
4005         $header ||= ucfirst($name);
4007         if ($order eq $name) {
4008                 print "<th>$header</th>\n";
4009         } else {
4010                 print "<th>" .
4011                       $cgi->a({-href => href(-replay=>1, order=>$name),
4012                                -class => "header"}, $header) .
4013                       "</th>\n";
4014         }
4017 sub git_project_list_body {
4018         # actually uses global variable $project
4019         my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4021         my $check_forks = gitweb_check_feature('forks');
4022         my @projects = fill_project_list_info($projlist, $check_forks);
4024         $order ||= $default_projects_order;
4025         $from = 0 unless defined $from;
4026         $to = $#projects if (!defined $to || $#projects < $to);
4028         my %order_info = (
4029                 project => { key => 'path', type => 'str' },
4030                 descr => { key => 'descr_long', type => 'str' },
4031                 owner => { key => 'owner', type => 'str' },
4032                 age => { key => 'age', type => 'num' }
4033         );
4034         my $oi = $order_info{$order};
4035         if ($oi->{'type'} eq 'str') {
4036                 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4037         } else {
4038                 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4039         }
4041         my $show_ctags = gitweb_check_feature('ctags');
4042         if ($show_ctags) {
4043                 my %ctags;
4044                 foreach my $p (@projects) {
4045                         foreach my $ct (keys %{$p->{'ctags'}}) {
4046                                 $ctags{$ct} += $p->{'ctags'}->{$ct};
4047                         }
4048                 }
4049                 my $cloud = git_populate_project_tagcloud(\%ctags);
4050                 print git_show_project_tagcloud($cloud, 64);
4051         }
4053         print "<table class=\"project_list\">\n";
4054         unless ($no_header) {
4055                 print "<tr>\n";
4056                 if ($check_forks) {
4057                         print "<th></th>\n";
4058                 }
4059                 print_sort_th('project', $order, 'Project');
4060                 print_sort_th('descr', $order, 'Description');
4061                 print_sort_th('owner', $order, 'Owner');
4062                 print_sort_th('age', $order, 'Last Change');
4063                 print "<th></th>\n" . # for links
4064                       "</tr>\n";
4065         }
4066         my $alternate = 1;
4067         my $tagfilter = $cgi->param('by_tag');
4068         for (my $i = $from; $i <= $to; $i++) {
4069                 my $pr = $projects[$i];
4071                 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4072                 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4073                         and not $pr->{'descr_long'} =~ /$searchtext/;
4074                 # Weed out forks or non-matching entries of search
4075                 if ($check_forks) {
4076                         my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4077                         $forkbase="^$forkbase" if $forkbase;
4078                         next if not $searchtext and not $tagfilter and $show_ctags
4079                                 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4080                 }
4082                 if ($alternate) {
4083                         print "<tr class=\"dark\">\n";
4084                 } else {
4085                         print "<tr class=\"light\">\n";
4086                 }
4087                 $alternate ^= 1;
4088                 if ($check_forks) {
4089                         print "<td>";
4090                         if ($pr->{'forks'}) {
4091                                 print "<!-- $pr->{'forks'} -->\n";
4092                                 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4093                         }
4094                         print "</td>\n";
4095                 }
4096                 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4097                                         -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4098                       "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4099                                         -class => "list", -title => $pr->{'descr_long'}},
4100                                         esc_html($pr->{'descr'})) . "</td>\n" .
4101                       "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4102                 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4103                       (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4104                       "<td class=\"link\">" .
4105                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
4106                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4107                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4108                       $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4109                       ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4110                       "</td>\n" .
4111                       "</tr>\n";
4112         }
4113         if (defined $extra) {
4114                 print "<tr>\n";
4115                 if ($check_forks) {
4116                         print "<td></td>\n";
4117                 }
4118                 print "<td colspan=\"5\">$extra</td>\n" .
4119                       "</tr>\n";
4120         }
4121         print "</table>\n";
4124 sub git_shortlog_body {
4125         # uses global variable $project
4126         my ($commitlist, $from, $to, $refs, $extra) = @_;
4128         $from = 0 unless defined $from;
4129         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4131         print "<table class=\"shortlog\">\n";
4132         my $alternate = 1;
4133         for (my $i = $from; $i <= $to; $i++) {
4134                 my %co = %{$commitlist->[$i]};
4135                 my $commit = $co{'id'};
4136                 my $ref = format_ref_marker($refs, $commit);
4137                 if ($alternate) {
4138                         print "<tr class=\"dark\">\n";
4139                 } else {
4140                         print "<tr class=\"light\">\n";
4141                 }
4142                 $alternate ^= 1;
4143                 my $author = chop_and_escape_str($co{'author_name'}, 10);
4144                 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4145                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4146                       "<td><i>" . $author . "</i></td>\n" .
4147                       "<td>";
4148                 print format_subject_html($co{'title'}, $co{'title_short'},
4149                                           href(action=>"commit", hash=>$commit), $ref);
4150                 print "</td>\n" .
4151                       "<td class=\"link\">" .
4152                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4153                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4154                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4155                 my $snapshot_links = format_snapshot_links($commit);
4156                 if (defined $snapshot_links) {
4157                         print " | " . $snapshot_links;
4158                 }
4159                 print "</td>\n" .
4160                       "</tr>\n";
4161         }
4162         if (defined $extra) {
4163                 print "<tr>\n" .
4164                       "<td colspan=\"4\">$extra</td>\n" .
4165                       "</tr>\n";
4166         }
4167         print "</table>\n";
4170 sub git_history_body {
4171         # Warning: assumes constant type (blob or tree) during history
4172         my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4174         $from = 0 unless defined $from;
4175         $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4177         print "<table class=\"history\">\n";
4178         my $alternate = 1;
4179         for (my $i = $from; $i <= $to; $i++) {
4180                 my %co = %{$commitlist->[$i]};
4181                 if (!%co) {
4182                         next;
4183                 }
4184                 my $commit = $co{'id'};
4186                 my $ref = format_ref_marker($refs, $commit);
4188                 if ($alternate) {
4189                         print "<tr class=\"dark\">\n";
4190                 } else {
4191                         print "<tr class=\"light\">\n";
4192                 }
4193                 $alternate ^= 1;
4194         # shortlog uses      chop_str($co{'author_name'}, 10)
4195                 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4196                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4197                       "<td><i>" . $author . "</i></td>\n" .
4198                       "<td>";
4199                 # originally git_history used chop_str($co{'title'}, 50)
4200                 print format_subject_html($co{'title'}, $co{'title_short'},
4201                                           href(action=>"commit", hash=>$commit), $ref);
4202                 print "</td>\n" .
4203                       "<td class=\"link\">" .
4204                       $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4205                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4207                 if ($ftype eq 'blob') {
4208                         my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4209                         my $blob_parent  = git_get_hash_by_path($commit, $file_name);
4210                         if (defined $blob_current && defined $blob_parent &&
4211                                         $blob_current ne $blob_parent) {
4212                                 print " | " .
4213                                         $cgi->a({-href => href(action=>"blobdiff",
4214                                                                hash=>$blob_current, hash_parent=>$blob_parent,
4215                                                                hash_base=>$hash_base, hash_parent_base=>$commit,
4216                                                                file_name=>$file_name)},
4217                                                 "diff to current");
4218                         }
4219                 }
4220                 print "</td>\n" .
4221                       "</tr>\n";
4222         }
4223         if (defined $extra) {
4224                 print "<tr>\n" .
4225                       "<td colspan=\"4\">$extra</td>\n" .
4226                       "</tr>\n";
4227         }
4228         print "</table>\n";
4231 sub git_tags_body {
4232         # uses global variable $project
4233         my ($taglist, $from, $to, $extra) = @_;
4234         $from = 0 unless defined $from;
4235         $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4237         print "<table class=\"tags\">\n";
4238         my $alternate = 1;
4239         for (my $i = $from; $i <= $to; $i++) {
4240                 my $entry = $taglist->[$i];
4241                 my %tag = %$entry;
4242                 my $comment = $tag{'subject'};
4243                 my $comment_short;
4244                 if (defined $comment) {
4245                         $comment_short = chop_str($comment, 30, 5);
4246                 }
4247                 if ($alternate) {
4248                         print "<tr class=\"dark\">\n";
4249                 } else {
4250                         print "<tr class=\"light\">\n";
4251                 }
4252                 $alternate ^= 1;
4253                 if (defined $tag{'age'}) {
4254                         print "<td><i>$tag{'age'}</i></td>\n";
4255                 } else {
4256                         print "<td></td>\n";
4257                 }
4258                 print "<td>" .
4259                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4260                                -class => "list name"}, esc_html($tag{'name'})) .
4261                       "</td>\n" .
4262                       "<td>";
4263                 if (defined $comment) {
4264                         print format_subject_html($comment, $comment_short,
4265                                                   href(action=>"tag", hash=>$tag{'id'}));
4266                 }
4267                 print "</td>\n" .
4268                       "<td class=\"selflink\">";
4269                 if ($tag{'type'} eq "tag") {
4270                         print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4271                 } else {
4272                         print "&nbsp;";
4273                 }
4274                 print "</td>\n" .
4275                       "<td class=\"link\">" . " | " .
4276                       $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4277                 if ($tag{'reftype'} eq "commit") {
4278                         print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4279                               " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4280                 } elsif ($tag{'reftype'} eq "blob") {
4281                         print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4282                 }
4283                 print "</td>\n" .
4284                       "</tr>";
4285         }
4286         if (defined $extra) {
4287                 print "<tr>\n" .
4288                       "<td colspan=\"5\">$extra</td>\n" .
4289                       "</tr>\n";
4290         }
4291         print "</table>\n";
4294 sub git_heads_body {
4295         # uses global variable $project
4296         my ($headlist, $head, $from, $to, $extra) = @_;
4297         $from = 0 unless defined $from;
4298         $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4300         print "<table class=\"heads\">\n";
4301         my $alternate = 1;
4302         for (my $i = $from; $i <= $to; $i++) {
4303                 my $entry = $headlist->[$i];
4304                 my %ref = %$entry;
4305                 my $curr = $ref{'id'} eq $head;
4306                 if ($alternate) {
4307                         print "<tr class=\"dark\">\n";
4308                 } else {
4309                         print "<tr class=\"light\">\n";
4310                 }
4311                 $alternate ^= 1;
4312                 print "<td><i>$ref{'age'}</i></td>\n" .
4313                       ($curr ? "<td class=\"current_head\">" : "<td>") .
4314                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4315                                -class => "list name"},esc_html($ref{'name'})) .
4316                       "</td>\n" .
4317                       "<td class=\"link\">" .
4318                       $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4319                       $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4320                       $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4321                       "</td>\n" .
4322                       "</tr>";
4323         }
4324         if (defined $extra) {
4325                 print "<tr>\n" .
4326                       "<td colspan=\"3\">$extra</td>\n" .
4327                       "</tr>\n";
4328         }
4329         print "</table>\n";
4332 sub git_search_grep_body {
4333         my ($commitlist, $from, $to, $extra) = @_;
4334         $from = 0 unless defined $from;
4335         $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4337         print "<table class=\"commit_search\">\n";
4338         my $alternate = 1;
4339         for (my $i = $from; $i <= $to; $i++) {
4340                 my %co = %{$commitlist->[$i]};
4341                 if (!%co) {
4342                         next;
4343                 }
4344                 my $commit = $co{'id'};
4345                 if ($alternate) {
4346                         print "<tr class=\"dark\">\n";
4347                 } else {
4348                         print "<tr class=\"light\">\n";
4349                 }
4350                 $alternate ^= 1;
4351                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4352                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4353                       "<td><i>" . $author . "</i></td>\n" .
4354                       "<td>" .
4355                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4356                                -class => "list subject"},
4357                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
4358                 my $comment = $co{'comment'};
4359                 foreach my $line (@$comment) {
4360                         if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4361                                 my ($lead, $match, $trail) = ($1, $2, $3);
4362                                 $match = chop_str($match, 70, 5, 'center');
4363                                 my $contextlen = int((80 - length($match))/2);
4364                                 $contextlen = 30 if ($contextlen > 30);
4365                                 $lead  = chop_str($lead,  $contextlen, 10, 'left');
4366                                 $trail = chop_str($trail, $contextlen, 10, 'right');
4368                                 $lead  = esc_html($lead);
4369                                 $match = esc_html($match);
4370                                 $trail = esc_html($trail);
4372                                 print "$lead<span class=\"match\">$match</span>$trail<br />";
4373                         }
4374                 }
4375                 print "</td>\n" .
4376                       "<td class=\"link\">" .
4377                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4378                       " | " .
4379                       $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4380                       " | " .
4381                       $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4382                 print "</td>\n" .
4383                       "</tr>\n";
4384         }
4385         if (defined $extra) {
4386                 print "<tr>\n" .
4387                       "<td colspan=\"3\">$extra</td>\n" .
4388                       "</tr>\n";
4389         }
4390         print "</table>\n";
4393 ## ======================================================================
4394 ## ======================================================================
4395 ## actions
4397 sub git_project_list {
4398         my $order = $input_params{'order'};
4399         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4400                 die_error(400, "Unknown order parameter");
4401         }
4403         my @list = git_get_projects_list();
4404         if (!@list) {
4405                 die_error(404, "No projects found");
4406         }
4408         git_header_html();
4409         if (-f $home_text) {
4410                 print "<div class=\"index_include\">\n";
4411                 insert_file($home_text);
4412                 print "</div>\n";
4413         }
4414         print $cgi->startform(-method => "get") .
4415               "<p class=\"projsearch\">Search:\n" .
4416               $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4417               "</p>" .
4418               $cgi->end_form() . "\n";
4419         git_project_list_body(\@list, $order);
4420         git_footer_html();
4423 sub git_forks {
4424         my $order = $input_params{'order'};
4425         if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4426                 die_error(400, "Unknown order parameter");
4427         }
4429         my @list = git_get_projects_list($project);
4430         if (!@list) {
4431                 die_error(404, "No forks found");
4432         }
4434         git_header_html();
4435         git_print_page_nav('','');
4436         git_print_header_div('summary', "$project forks");
4437         git_project_list_body(\@list, $order);
4438         git_footer_html();
4441 sub git_project_index {
4442         my @projects = git_get_projects_list($project);
4444         print $cgi->header(
4445                 -type => 'text/plain',
4446                 -charset => 'utf-8',
4447                 -content_disposition => 'inline; filename="index.aux"');
4449         foreach my $pr (@projects) {
4450                 if (!exists $pr->{'owner'}) {
4451                         $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4452                 }
4454                 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4455                 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4456                 $path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4457                 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4458                 $path  =~ s/ /\+/g;
4459                 $owner =~ s/ /\+/g;
4461                 print "$path $owner\n";
4462         }
4465 sub git_summary {
4466         my $descr = git_get_project_description($project) || "none";
4467         my %co = parse_commit("HEAD");
4468         my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4469         my $head = $co{'id'};
4471         my $owner = git_get_project_owner($project);
4473         my $refs = git_get_references();
4474         # These get_*_list functions return one more to allow us to see if
4475         # there are more ...
4476         my @taglist  = git_get_tags_list(16);
4477         my @headlist = git_get_heads_list(16);
4478         my @forklist;
4479         my $check_forks = gitweb_check_feature('forks');
4481         if ($check_forks) {
4482                 @forklist = git_get_projects_list($project);
4483         }
4485         git_header_html();
4486         git_print_page_nav('summary','', $head);
4488         print "<div class=\"title\">&nbsp;</div>\n";
4489         print "<table class=\"projects_list\">\n" .
4490               "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4491               "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4492         if (defined $cd{'rfc2822'}) {
4493                 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4494         }
4496         # use per project git URL list in $projectroot/$project/cloneurl
4497         # or make project git URL from git base URL and project name
4498         my $url_tag = "URL";
4499         my @url_list = git_get_project_url_list($project);
4500         @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4501         foreach my $git_url (@url_list) {
4502                 next unless $git_url;
4503                 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4504                 $url_tag = "";
4505         }
4507         # Tag cloud
4508         my $show_ctags = gitweb_check_feature('ctags');
4509         if ($show_ctags) {
4510                 my $ctags = git_get_project_ctags($project);
4511                 my $cloud = git_populate_project_tagcloud($ctags);
4512                 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4513                 print "</td>\n<td>" unless %$ctags;
4514                 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4515                 print "</td>\n<td>" if %$ctags;
4516                 print git_show_project_tagcloud($cloud, 48);
4517                 print "</td></tr>";
4518         }
4520         print "</table>\n";
4522         if (-s "$projectroot/$project/README.html") {
4523                 print "<div class=\"title\">readme</div>\n" .
4524                       "<div class=\"readme\">\n";
4525                 insert_file("$projectroot/$project/README.html");
4526                 print "\n</div>\n"; # class="readme"
4527         }
4529         # we need to request one more than 16 (0..15) to check if
4530         # those 16 are all
4531         my @commitlist = $head ? parse_commits($head, 17) : ();
4532         if (@commitlist) {
4533                 git_print_header_div('shortlog');
4534                 git_shortlog_body(\@commitlist, 0, 15, $refs,
4535                                   $#commitlist <=  15 ? undef :
4536                                   $cgi->a({-href => href(action=>"shortlog")}, "..."));
4537         }
4539         if (@taglist) {
4540                 git_print_header_div('tags');
4541                 git_tags_body(\@taglist, 0, 15,
4542                               $#taglist <=  15 ? undef :
4543                               $cgi->a({-href => href(action=>"tags")}, "..."));
4544         }
4546         if (@headlist) {
4547                 git_print_header_div('heads');
4548                 git_heads_body(\@headlist, $head, 0, 15,
4549                                $#headlist <= 15 ? undef :
4550                                $cgi->a({-href => href(action=>"heads")}, "..."));
4551         }
4553         if (@forklist) {
4554                 git_print_header_div('forks');
4555                 git_project_list_body(\@forklist, 'age', 0, 15,
4556                                       $#forklist <= 15 ? undef :
4557                                       $cgi->a({-href => href(action=>"forks")}, "..."),
4558                                       'no_header');
4559         }
4561         git_footer_html();
4564 sub git_tag {
4565         my $head = git_get_head_hash($project);
4566         git_header_html();
4567         git_print_page_nav('','', $head,undef,$head);
4568         my %tag = parse_tag($hash);
4570         if (! %tag) {
4571                 die_error(404, "Unknown tag object");
4572         }
4574         git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4575         print "<div class=\"title_text\">\n" .
4576               "<table class=\"object_header\">\n" .
4577               "<tr>\n" .
4578               "<td>object</td>\n" .
4579               "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4580                                $tag{'object'}) . "</td>\n" .
4581               "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4582                                               $tag{'type'}) . "</td>\n" .
4583               "</tr>\n";
4584         if (defined($tag{'author'})) {
4585                 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4586                 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4587                 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4588                         sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4589                         "</td></tr>\n";
4590         }
4591         print "</table>\n\n" .
4592               "</div>\n";
4593         print "<div class=\"page_body\">";
4594         my $comment = $tag{'comment'};
4595         foreach my $line (@$comment) {
4596                 chomp $line;
4597                 print esc_html($line, -nbsp=>1) . "<br/>\n";
4598         }
4599         print "</div>\n";
4600         git_footer_html();
4603 sub git_blame {
4604         my $fd;
4605         my $ftype;
4607         gitweb_check_feature('blame')
4608             or die_error(403, "Blame view not allowed");
4610         die_error(400, "No file name given") unless $file_name;
4611         $hash_base ||= git_get_head_hash($project);
4612         die_error(404, "Couldn't find base commit") unless ($hash_base);
4613         my %co = parse_commit($hash_base)
4614                 or die_error(404, "Commit not found");
4615         if (!defined $hash) {
4616                 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4617                         or die_error(404, "Error looking up file");
4618         }
4619         $ftype = git_get_type($hash);
4620         if ($ftype !~ "blob") {
4621                 die_error(400, "Object is not a blob");
4622         }
4623         open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4624               $file_name, $hash_base)
4625                 or die_error(500, "Open git-blame failed");
4626         git_header_html();
4627         my $formats_nav =
4628                 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4629                         "blob") .
4630                 " | " .
4631                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4632                         "history") .
4633                 " | " .
4634                 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4635                         "HEAD");
4636         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4637         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4638         git_print_page_path($file_name, $ftype, $hash_base);
4639         my @rev_color = (qw(light2 dark2));
4640         my $num_colors = scalar(@rev_color);
4641         my $current_color = 0;
4642         my $last_rev;
4643         print <<HTML;
4644 <div class="page_body">
4645 <table class="blame">
4646 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4647 HTML
4648         my %metainfo = ();
4649         while (1) {
4650                 $_ = <$fd>;
4651                 last unless defined $_;
4652                 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4653                     /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4654                 if (!exists $metainfo{$full_rev}) {
4655                         $metainfo{$full_rev} = {};
4656                 }
4657                 my $meta = $metainfo{$full_rev};
4658                 while (<$fd>) {
4659                         last if (s/^\t//);
4660                         if (/^(\S+) (.*)$/) {
4661                                 $meta->{$1} = $2;
4662                         }
4663                 }
4664                 my $data = $_;
4665                 chomp $data;
4666                 my $rev = substr($full_rev, 0, 8);
4667                 my $author = $meta->{'author'};
4668                 my %date = parse_date($meta->{'author-time'},
4669                                       $meta->{'author-tz'});
4670                 my $date = $date{'iso-tz'};
4671                 if ($group_size) {
4672                         $current_color = ++$current_color % $num_colors;
4673                 }
4674                 print "<tr class=\"$rev_color[$current_color]\">\n";
4675                 if ($group_size) {
4676                         print "<td class=\"sha1\"";
4677                         print " title=\"". esc_html($author) . ", $date\"";
4678                         print " rowspan=\"$group_size\"" if ($group_size > 1);
4679                         print ">";
4680                         print $cgi->a({-href => href(action=>"commit",
4681                                                      hash=>$full_rev,
4682                                                      file_name=>$file_name)},
4683                                       esc_html($rev));
4684                         print "</td>\n";
4685                 }
4686                 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4687                         or die_error(500, "Open git-rev-parse failed");
4688                 my $parent_commit = <$dd>;
4689                 close $dd;
4690                 chomp($parent_commit);
4691                 my $blamed = href(action => 'blame',
4692                                   file_name => $meta->{'filename'},
4693                                   hash_base => $parent_commit);
4694                 print "<td class=\"linenr\">";
4695                 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4696                                 -id => "l$lineno",
4697                                 -class => "linenr" },
4698                               esc_html($lineno));
4699                 print "</td>";
4700                 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4701                 print "</tr>\n";
4702         }
4703         print "</table>\n";
4704         print "</div>";
4705         close $fd
4706                 or print "Reading blob failed\n";
4707         git_footer_html();
4710 sub git_tags {
4711         my $head = git_get_head_hash($project);
4712         git_header_html();
4713         git_print_page_nav('','', $head,undef,$head);
4714         git_print_header_div('summary', $project);
4716         my @tagslist = git_get_tags_list();
4717         if (@tagslist) {
4718                 git_tags_body(\@tagslist);
4719         }
4720         git_footer_html();
4723 sub git_heads {
4724         my $head = git_get_head_hash($project);
4725         git_header_html();
4726         git_print_page_nav('','', $head,undef,$head);
4727         git_print_header_div('summary', $project);
4729         my @headslist = git_get_heads_list();
4730         if (@headslist) {
4731                 git_heads_body(\@headslist, $head);
4732         }
4733         git_footer_html();
4736 sub git_blob_plain {
4737         my $type = shift;
4738         my $expires;
4740         if (!defined $hash) {
4741                 if (defined $file_name) {
4742                         my $base = $hash_base || git_get_head_hash($project);
4743                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4744                                 or die_error(404, "Cannot find file");
4745                 } else {
4746                         die_error(400, "No file name defined");
4747                 }
4748         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4749                 # blobs defined by non-textual hash id's can be cached
4750                 $expires = "+1d";
4751         }
4753         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4754                 or die_error(500, "Open git-cat-file blob '$hash' failed");
4756         # content-type (can include charset)
4757         $type = blob_contenttype($fd, $file_name, $type);
4759         # "save as" filename, even when no $file_name is given
4760         my $save_as = "$hash";
4761         if (defined $file_name) {
4762                 $save_as = $file_name;
4763         } elsif ($type =~ m/^text\//) {
4764                 $save_as .= '.txt';
4765         }
4767         print $cgi->header(
4768                 -type => $type,
4769                 -expires => $expires,
4770                 -content_disposition => 'inline; filename="' . $save_as . '"');
4771         undef $/;
4772         binmode STDOUT, ':raw';
4773         print <$fd>;
4774         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4775         $/ = "\n";
4776         close $fd;
4779 sub git_blob {
4780         my $expires;
4782         if (!defined $hash) {
4783                 if (defined $file_name) {
4784                         my $base = $hash_base || git_get_head_hash($project);
4785                         $hash = git_get_hash_by_path($base, $file_name, "blob")
4786                                 or die_error(404, "Cannot find file");
4787                 } else {
4788                         die_error(400, "No file name defined");
4789                 }
4790         } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4791                 # blobs defined by non-textual hash id's can be cached
4792                 $expires = "+1d";
4793         }
4795         my $have_blame = gitweb_check_feature('blame');
4796         open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4797                 or die_error(500, "Couldn't cat $file_name, $hash");
4798         my $mimetype = blob_mimetype($fd, $file_name);
4799         if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4800                 close $fd;
4801                 return git_blob_plain($mimetype);
4802         }
4803         # we can have blame only for text/* mimetype
4804         $have_blame &&= ($mimetype =~ m!^text/!);
4806         git_header_html(undef, $expires);
4807         my $formats_nav = '';
4808         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4809                 if (defined $file_name) {
4810                         if ($have_blame) {
4811                                 $formats_nav .=
4812                                         $cgi->a({-href => href(action=>"blame", -replay=>1)},
4813                                                 "blame") .
4814                                         " | ";
4815                         }
4816                         $formats_nav .=
4817                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4818                                         "history") .
4819                                 " | " .
4820                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4821                                         "raw") .
4822                                 " | " .
4823                                 $cgi->a({-href => href(action=>"blob",
4824                                                        hash_base=>"HEAD", file_name=>$file_name)},
4825                                         "HEAD");
4826                 } else {
4827                         $formats_nav .=
4828                                 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4829                                         "raw");
4830                 }
4831                 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4832                 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4833         } else {
4834                 print "<div class=\"page_nav\">\n" .
4835                       "<br/><br/></div>\n" .
4836                       "<div class=\"title\">$hash</div>\n";
4837         }
4838         git_print_page_path($file_name, "blob", $hash_base);
4839         print "<div class=\"page_body\">\n";
4840         if ($mimetype =~ m!^image/!) {
4841                 print qq!<img type="$mimetype"!;
4842                 if ($file_name) {
4843                         print qq! alt="$file_name" title="$file_name"!;
4844                 }
4845                 print qq! src="! .
4846                       href(action=>"blob_plain", hash=>$hash,
4847                            hash_base=>$hash_base, file_name=>$file_name) .
4848                       qq!" />\n!;
4849         } else {
4850                 my $nr;
4851                 while (my $line = <$fd>) {
4852                         chomp $line;
4853                         $nr++;
4854                         $line = untabify($line);
4855                         printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4856                                $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4857                 }
4858         }
4859         close $fd
4860                 or print "Reading blob failed.\n";
4861         print "</div>";
4862         git_footer_html();
4865 sub git_tree {
4866         if (!defined $hash_base) {
4867                 $hash_base = "HEAD";
4868         }
4869         if (!defined $hash) {
4870                 if (defined $file_name) {
4871                         $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4872                 } else {
4873                         $hash = $hash_base;
4874                 }
4875         }
4876         die_error(404, "No such tree") unless defined($hash);
4877         $/ = "\0";
4878         open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4879                 or die_error(500, "Open git-ls-tree failed");
4880         my @entries = map { chomp; $_ } <$fd>;
4881         close $fd or die_error(404, "Reading tree failed");
4882         $/ = "\n";
4884         my $refs = git_get_references();
4885         my $ref = format_ref_marker($refs, $hash_base);
4886         git_header_html();
4887         my $basedir = '';
4888         my $have_blame = gitweb_check_feature('blame');
4889         if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4890                 my @views_nav = ();
4891                 if (defined $file_name) {
4892                         push @views_nav,
4893                                 $cgi->a({-href => href(action=>"history", -replay=>1)},
4894                                         "history"),
4895                                 $cgi->a({-href => href(action=>"tree",
4896                                                        hash_base=>"HEAD", file_name=>$file_name)},
4897                                         "HEAD"),
4898                 }
4899                 my $snapshot_links = format_snapshot_links($hash);
4900                 if (defined $snapshot_links) {
4901                         # FIXME: Should be available when we have no hash base as well.
4902                         push @views_nav, $snapshot_links;
4903                 }
4904                 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4905                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4906         } else {
4907                 undef $hash_base;
4908                 print "<div class=\"page_nav\">\n";
4909                 print "<br/><br/></div>\n";
4910                 print "<div class=\"title\">$hash</div>\n";
4911         }
4912         if (defined $file_name) {
4913                 $basedir = $file_name;
4914                 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4915                         $basedir .= '/';
4916                 }
4917                 git_print_page_path($file_name, 'tree', $hash_base);
4918         }
4919         print "<div class=\"page_body\">\n";
4920         print "<table class=\"tree\">\n";
4921         my $alternate = 1;
4922         # '..' (top directory) link if possible
4923         if (defined $hash_base &&
4924             defined $file_name && $file_name =~ m![^/]+$!) {
4925                 if ($alternate) {
4926                         print "<tr class=\"dark\">\n";
4927                 } else {
4928                         print "<tr class=\"light\">\n";
4929                 }
4930                 $alternate ^= 1;
4932                 my $up = $file_name;
4933                 $up =~ s!/?[^/]+$!!;
4934                 undef $up unless $up;
4935                 # based on git_print_tree_entry
4936                 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4937                 print '<td class="list">';
4938                 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4939                                              file_name=>$up)},
4940                               "..");
4941                 print "</td>\n";
4942                 print "<td class=\"link\"></td>\n";
4944                 print "</tr>\n";
4945         }
4946         foreach my $line (@entries) {
4947                 my %t = parse_ls_tree_line($line, -z => 1);
4949                 if ($alternate) {
4950                         print "<tr class=\"dark\">\n";
4951                 } else {
4952                         print "<tr class=\"light\">\n";
4953                 }
4954                 $alternate ^= 1;
4956                 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4958                 print "</tr>\n";
4959         }
4960         print "</table>\n" .
4961               "</div>";
4962         git_footer_html();
4965 sub git_snapshot {
4966         my $format = $input_params{'snapshot_format'};
4967         if (!@snapshot_fmts) {
4968                 die_error(403, "Snapshots not allowed");
4969         }
4970         # default to first supported snapshot format
4971         $format ||= $snapshot_fmts[0];
4972         if ($format !~ m/^[a-z0-9]+$/) {
4973                 die_error(400, "Invalid snapshot format parameter");
4974         } elsif (!exists($known_snapshot_formats{$format})) {
4975                 die_error(400, "Unknown snapshot format");
4976         } elsif (!grep($_ eq $format, @snapshot_fmts)) {
4977                 die_error(403, "Unsupported snapshot format");
4978         }
4980         if (!defined $hash) {
4981                 $hash = git_get_head_hash($project);
4982         }
4984         my $name = $project;
4985         $name =~ s,([^/])/*\.git$,$1,;
4986         $name = basename($name);
4987         my $filename = to_utf8($name);
4988         $name =~ s/\047/\047\\\047\047/g;
4989         my $cmd;
4990         $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4991         $cmd = quote_command(
4992                 git_cmd(), 'archive',
4993                 "--format=$known_snapshot_formats{$format}{'format'}",
4994                 "--prefix=$name/", $hash);
4995         if (exists $known_snapshot_formats{$format}{'compressor'}) {
4996                 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4997         }
4999         print $cgi->header(
5000                 -type => $known_snapshot_formats{$format}{'type'},
5001                 -content_disposition => 'inline; filename="' . "$filename" . '"',
5002                 -status => '200 OK');
5004         open my $fd, "-|", $cmd
5005                 or die_error(500, "Execute git-archive failed");
5006         binmode STDOUT, ':raw';
5007         print <$fd>;
5008         binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5009         close $fd;
5012 sub git_log {
5013         my $head = git_get_head_hash($project);
5014         if (!defined $hash) {
5015                 $hash = $head;
5016         }
5017         if (!defined $page) {
5018                 $page = 0;
5019         }
5020         my $refs = git_get_references();
5022         my @commitlist = parse_commits($hash, 101, (100 * $page));
5024         my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5026         git_header_html();
5027         git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5029         if (!@commitlist) {
5030                 my %co = parse_commit($hash);
5032                 git_print_header_div('summary', $project);
5033                 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5034         }
5035         my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5036         for (my $i = 0; $i <= $to; $i++) {
5037                 my %co = %{$commitlist[$i]};
5038                 next if !%co;
5039                 my $commit = $co{'id'};
5040                 my $ref = format_ref_marker($refs, $commit);
5041                 my %ad = parse_date($co{'author_epoch'});
5042                 git_print_header_div('commit',
5043                                "<span class=\"age\">$co{'age_string'}</span>" .
5044                                esc_html($co{'title'}) . $ref,
5045                                $commit);
5046                 print "<div class=\"title_text\">\n" .
5047                       "<div class=\"log_link\">\n" .
5048                       $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5049                       " | " .
5050                       $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5051                       " | " .
5052                       $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5053                       "<br/>\n" .
5054                       "</div>\n" .
5055                       "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
5056                       "</div>\n";
5058                 print "<div class=\"log_body\">\n";
5059                 git_print_log($co{'comment'}, -final_empty_line=> 1);
5060                 print "</div>\n";
5061         }
5062         if ($#commitlist >= 100) {
5063                 print "<div class=\"page_nav\">\n";
5064                 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5065                                -accesskey => "n", -title => "Alt-n"}, "next");
5066                 print "</div>\n";
5067         }
5068         git_footer_html();
5071 sub git_commit {
5072         $hash ||= $hash_base || "HEAD";
5073         my %co = parse_commit($hash)
5074             or die_error(404, "Unknown commit object");
5075         my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5076         my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
5078         my $parent  = $co{'parent'};
5079         my $parents = $co{'parents'}; # listref
5081         # we need to prepare $formats_nav before any parameter munging
5082         my $formats_nav;
5083         if (!defined $parent) {
5084                 # --root commitdiff
5085                 $formats_nav .= '(initial)';
5086         } elsif (@$parents == 1) {
5087                 # single parent commit
5088                 $formats_nav .=
5089                         '(parent: ' .
5090                         $cgi->a({-href => href(action=>"commit",
5091                                                hash=>$parent)},
5092                                 esc_html(substr($parent, 0, 7))) .
5093                         ')';
5094         } else {
5095                 # merge commit
5096                 $formats_nav .=
5097                         '(merge: ' .
5098                         join(' ', map {
5099                                 $cgi->a({-href => href(action=>"commit",
5100                                                        hash=>$_)},
5101                                         esc_html(substr($_, 0, 7)));
5102                         } @$parents ) .
5103                         ')';
5104         }
5106         if (!defined $parent) {
5107                 $parent = "--root";
5108         }
5109         my @difftree;
5110         open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5111                 @diff_opts,
5112                 (@$parents <= 1 ? $parent : '-c'),
5113                 $hash, "--"
5114                 or die_error(500, "Open git-diff-tree failed");
5115         @difftree = map { chomp; $_ } <$fd>;
5116         close $fd or die_error(404, "Reading git-diff-tree failed");
5118         # non-textual hash id's can be cached
5119         my $expires;
5120         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5121                 $expires = "+1d";
5122         }
5123         my $refs = git_get_references();
5124         my $ref = format_ref_marker($refs, $co{'id'});
5126         git_header_html(undef, $expires);
5127         git_print_page_nav('commit', '',
5128                            $hash, $co{'tree'}, $hash,
5129                            $formats_nav);
5131         if (defined $co{'parent'}) {
5132                 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5133         } else {
5134                 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5135         }
5136         print "<div class=\"title_text\">\n" .
5137               "<table class=\"object_header\">\n";
5138         print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5139               "<tr>" .
5140               "<td></td><td> $ad{'rfc2822'}";
5141         if ($ad{'hour_local'} < 6) {
5142                 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5143                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5144         } else {
5145                 printf(" (%02d:%02d %s)",
5146                        $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5147         }
5148         print "</td>" .
5149               "</tr>\n";
5150         print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5151         print "<tr><td></td><td> $cd{'rfc2822'}" .
5152               sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5153               "</td></tr>\n";
5154         print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5155         print "<tr>" .
5156               "<td>tree</td>" .
5157               "<td class=\"sha1\">" .
5158               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5159                        class => "list"}, $co{'tree'}) .
5160               "</td>" .
5161               "<td class=\"link\">" .
5162               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5163                       "tree");
5164         my $snapshot_links = format_snapshot_links($hash);
5165         if (defined $snapshot_links) {
5166                 print " | " . $snapshot_links;
5167         }
5168         print "</td>" .
5169               "</tr>\n";
5171         foreach my $par (@$parents) {
5172                 print "<tr>" .
5173                       "<td>parent</td>" .
5174                       "<td class=\"sha1\">" .
5175                       $cgi->a({-href => href(action=>"commit", hash=>$par),
5176                                class => "list"}, $par) .
5177                       "</td>" .
5178                       "<td class=\"link\">" .
5179                       $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5180                       " | " .
5181                       $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5182                       "</td>" .
5183                       "</tr>\n";
5184         }
5185         print "</table>".
5186               "</div>\n";
5188         print "<div class=\"page_body\">\n";
5189         git_print_log($co{'comment'});
5190         print "</div>\n";
5192         git_difftree_body(\@difftree, $hash, @$parents);
5194         git_footer_html();
5197 sub git_object {
5198         # object is defined by:
5199         # - hash or hash_base alone
5200         # - hash_base and file_name
5201         my $type;
5203         # - hash or hash_base alone
5204         if ($hash || ($hash_base && !defined $file_name)) {
5205                 my $object_id = $hash || $hash_base;
5207                 open my $fd, "-|", quote_command(
5208                         git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5209                         or die_error(404, "Object does not exist");
5210                 $type = <$fd>;
5211                 chomp $type;
5212                 close $fd
5213                         or die_error(404, "Object does not exist");
5215         # - hash_base and file_name
5216         } elsif ($hash_base && defined $file_name) {
5217                 $file_name =~ s,/+$,,;
5219                 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5220                         or die_error(404, "Base object does not exist");
5222                 # here errors should not hapen
5223                 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5224                         or die_error(500, "Open git-ls-tree failed");
5225                 my $line = <$fd>;
5226                 close $fd;
5228                 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa  panic.c'
5229                 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5230                         die_error(404, "File or directory for given base does not exist");
5231                 }
5232                 $type = $2;
5233                 $hash = $3;
5234         } else {
5235                 die_error(400, "Not enough information to find object");
5236         }
5238         print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5239                                           hash=>$hash, hash_base=>$hash_base,
5240                                           file_name=>$file_name),
5241                              -status => '302 Found');
5244 sub git_blobdiff {
5245         my $format = shift || 'html';
5247         my $fd;
5248         my @difftree;
5249         my %diffinfo;
5250         my $expires;
5252         # preparing $fd and %diffinfo for git_patchset_body
5253         # new style URI
5254         if (defined $hash_base && defined $hash_parent_base) {
5255                 if (defined $file_name) {
5256                         # read raw output
5257                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5258                                 $hash_parent_base, $hash_base,
5259                                 "--", (defined $file_parent ? $file_parent : ()), $file_name
5260                                 or die_error(500, "Open git-diff-tree failed");
5261                         @difftree = map { chomp; $_ } <$fd>;
5262                         close $fd
5263                                 or die_error(404, "Reading git-diff-tree failed");
5264                         @difftree
5265                                 or die_error(404, "Blob diff not found");
5267                 } elsif (defined $hash &&
5268                          $hash =~ /[0-9a-fA-F]{40}/) {
5269                         # try to find filename from $hash
5271                         # read filtered raw output
5272                         open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5273                                 $hash_parent_base, $hash_base, "--"
5274                                 or die_error(500, "Open git-diff-tree failed");
5275                         @difftree =
5276                                 # ':100644 100644 03b21826... 3b93d5e7... M     ls-files.c'
5277                                 # $hash == to_id
5278                                 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5279                                 map { chomp; $_ } <$fd>;
5280                         close $fd
5281                                 or die_error(404, "Reading git-diff-tree failed");
5282                         @difftree
5283                                 or die_error(404, "Blob diff not found");
5285                 } else {
5286                         die_error(400, "Missing one of the blob diff parameters");
5287                 }
5289                 if (@difftree > 1) {
5290                         die_error(400, "Ambiguous blob diff specification");
5291                 }
5293                 %diffinfo = parse_difftree_raw_line($difftree[0]);
5294                 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5295                 $file_name   ||= $diffinfo{'to_file'};
5297                 $hash_parent ||= $diffinfo{'from_id'};
5298                 $hash        ||= $diffinfo{'to_id'};
5300                 # non-textual hash id's can be cached
5301                 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5302                     $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5303                         $expires = '+1d';
5304                 }
5306                 # open patch output
5307                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5308                         '-p', ($format eq 'html' ? "--full-index" : ()),
5309                         $hash_parent_base, $hash_base,
5310                         "--", (defined $file_parent ? $file_parent : ()), $file_name
5311                         or die_error(500, "Open git-diff-tree failed");
5312         }
5314         # old/legacy style URI
5315         if (!%diffinfo && # if new style URI failed
5316             defined $hash && defined $hash_parent) {
5317                 # fake git-diff-tree raw output
5318                 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5319                 $diffinfo{'from_id'} = $hash_parent;
5320                 $diffinfo{'to_id'}   = $hash;
5321                 if (defined $file_name) {
5322                         if (defined $file_parent) {
5323                                 $diffinfo{'status'} = '2';
5324                                 $diffinfo{'from_file'} = $file_parent;
5325                                 $diffinfo{'to_file'}   = $file_name;
5326                         } else { # assume not renamed
5327                                 $diffinfo{'status'} = '1';
5328                                 $diffinfo{'from_file'} = $file_name;
5329                                 $diffinfo{'to_file'}   = $file_name;
5330                         }
5331                 } else { # no filename given
5332                         $diffinfo{'status'} = '2';
5333                         $diffinfo{'from_file'} = $hash_parent;
5334                         $diffinfo{'to_file'}   = $hash;
5335                 }
5337                 # non-textual hash id's can be cached
5338                 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5339                     $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5340                         $expires = '+1d';
5341                 }
5343                 # open patch output
5344                 open $fd, "-|", git_cmd(), "diff", @diff_opts,
5345                         '-p', ($format eq 'html' ? "--full-index" : ()),
5346                         $hash_parent, $hash, "--"
5347                         or die_error(500, "Open git-diff failed");
5348         } else  {
5349                 die_error(400, "Missing one of the blob diff parameters")
5350                         unless %diffinfo;
5351         }
5353         # header
5354         if ($format eq 'html') {
5355                 my $formats_nav =
5356                         $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5357                                 "raw");
5358                 git_header_html(undef, $expires);
5359                 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5360                         git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5361                         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5362                 } else {
5363                         print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5364                         print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5365                 }
5366                 if (defined $file_name) {
5367                         git_print_page_path($file_name, "blob", $hash_base);
5368                 } else {
5369                         print "<div class=\"page_path\"></div>\n";
5370                 }
5372         } elsif ($format eq 'plain') {
5373                 print $cgi->header(
5374                         -type => 'text/plain',
5375                         -charset => 'utf-8',
5376                         -expires => $expires,
5377                         -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5379                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5381         } else {
5382                 die_error(400, "Unknown blobdiff format");
5383         }
5385         # patch
5386         if ($format eq 'html') {
5387                 print "<div class=\"page_body\">\n";
5389                 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5390                 close $fd;
5392                 print "</div>\n"; # class="page_body"
5393                 git_footer_html();
5395         } else {
5396                 while (my $line = <$fd>) {
5397                         $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5398                         $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5400                         print $line;
5402                         last if $line =~ m!^\+\+\+!;
5403                 }
5404                 local $/ = undef;
5405                 print <$fd>;
5406                 close $fd;
5407         }
5410 sub git_blobdiff_plain {
5411         git_blobdiff('plain');
5414 sub git_commitdiff {
5415         my %params = @_;
5416         my $format = $params{-format} || 'html';
5418         my $patch_max;
5419         if ($format eq 'patch') {
5420                 ($patch_max) = gitweb_get_feature('patches');
5421                 die_error(403, "Patch view not allowed") unless $patch_max;
5422         }
5424         $hash ||= $hash_base || "HEAD";
5425         my %co = parse_commit($hash)
5426             or die_error(404, "Unknown commit object");
5428         # choose format for commitdiff for merge
5429         if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5430                 $hash_parent = '--cc';
5431         }
5432         # we need to prepare $formats_nav before almost any parameter munging
5433         my $formats_nav;
5434         if ($format eq 'html') {
5435                 $formats_nav =
5436                         $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5437                                 "raw");
5439                 if (defined $hash_parent &&
5440                     $hash_parent ne '-c' && $hash_parent ne '--cc') {
5441                         # commitdiff with two commits given
5442                         my $hash_parent_short = $hash_parent;
5443                         if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5444                                 $hash_parent_short = substr($hash_parent, 0, 7);
5445                         }
5446                         $formats_nav .=
5447                                 ' (from';
5448                         for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5449                                 if ($co{'parents'}[$i] eq $hash_parent) {
5450                                         $formats_nav .= ' parent ' . ($i+1);
5451                                         last;
5452                                 }
5453                         }
5454                         $formats_nav .= ': ' .
5455                                 $cgi->a({-href => href(action=>"commitdiff",
5456                                                        hash=>$hash_parent)},
5457                                         esc_html($hash_parent_short)) .
5458                                 ')';
5459                 } elsif (!$co{'parent'}) {
5460                         # --root commitdiff
5461                         $formats_nav .= ' (initial)';
5462                 } elsif (scalar @{$co{'parents'}} == 1) {
5463                         # single parent commit
5464                         $formats_nav .=
5465                                 ' (parent: ' .
5466                                 $cgi->a({-href => href(action=>"commitdiff",
5467                                                        hash=>$co{'parent'})},
5468                                         esc_html(substr($co{'parent'}, 0, 7))) .
5469                                 ')';
5470                 } else {
5471                         # merge commit
5472                         if ($hash_parent eq '--cc') {
5473                                 $formats_nav .= ' | ' .
5474                                         $cgi->a({-href => href(action=>"commitdiff",
5475                                                                hash=>$hash, hash_parent=>'-c')},
5476                                                 'combined');
5477                         } else { # $hash_parent eq '-c'
5478                                 $formats_nav .= ' | ' .
5479                                         $cgi->a({-href => href(action=>"commitdiff",
5480                                                                hash=>$hash, hash_parent=>'--cc')},
5481                                                 'compact');
5482                         }
5483                         $formats_nav .=
5484                                 ' (merge: ' .
5485                                 join(' ', map {
5486                                         $cgi->a({-href => href(action=>"commitdiff",
5487                                                                hash=>$_)},
5488                                                 esc_html(substr($_, 0, 7)));
5489                                 } @{$co{'parents'}} ) .
5490                                 ')';
5491                 }
5492         }
5494         my $hash_parent_param = $hash_parent;
5495         if (!defined $hash_parent_param) {
5496                 # --cc for multiple parents, --root for parentless
5497                 $hash_parent_param =
5498                         @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5499         }
5501         # read commitdiff
5502         my $fd;
5503         my @difftree;
5504         if ($format eq 'html') {
5505                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5506                         "--no-commit-id", "--patch-with-raw", "--full-index",
5507                         $hash_parent_param, $hash, "--"
5508                         or die_error(500, "Open git-diff-tree failed");
5510                 while (my $line = <$fd>) {
5511                         chomp $line;
5512                         # empty line ends raw part of diff-tree output
5513                         last unless $line;
5514                         push @difftree, scalar parse_difftree_raw_line($line);
5515                 }
5517         } elsif ($format eq 'plain') {
5518                 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5519                         '-p', $hash_parent_param, $hash, "--"
5520                         or die_error(500, "Open git-diff-tree failed");
5521         } elsif ($format eq 'patch') {
5522                 # For commit ranges, we limit the output to the number of
5523                 # patches specified in the 'patches' feature.
5524                 # For single commits, we limit the output to a single patch,
5525                 # diverging from the git-format-patch default.
5526                 my @commit_spec = ();
5527                 if ($hash_parent) {
5528                         if ($patch_max > 0) {
5529                                 push @commit_spec, "-$patch_max";
5530                         }
5531                         push @commit_spec, '-n', "$hash_parent..$hash";
5532                 } else {
5533                         if ($params{-single}) {
5534                                 push @commit_spec, '-1';
5535                         } else {
5536                                 if ($patch_max > 0) {
5537                                         push @commit_spec, "-$patch_max";
5538                                 }
5539                                 push @commit_spec, "-n";
5540                         }
5541                         push @commit_spec, '--root', $hash;
5542                 }
5543                 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5544                         '--stdout', @commit_spec
5545                         or die_error(500, "Open git-format-patch failed");
5546         } else {
5547                 die_error(400, "Unknown commitdiff format");
5548         }
5550         # non-textual hash id's can be cached
5551         my $expires;
5552         if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5553                 $expires = "+1d";
5554         }
5556         # write commit message
5557         if ($format eq 'html') {
5558                 my $refs = git_get_references();
5559                 my $ref = format_ref_marker($refs, $co{'id'});
5561                 git_header_html(undef, $expires);
5562                 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5563                 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5564                 git_print_authorship(\%co);
5565                 print "<div class=\"page_body\">\n";
5566                 if (@{$co{'comment'}} > 1) {
5567                         print "<div class=\"log\">\n";
5568                         git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5569                         print "</div>\n"; # class="log"
5570                 }
5572         } elsif ($format eq 'plain') {
5573                 my $refs = git_get_references("tags");
5574                 my $tagname = git_get_rev_name_tags($hash);
5575                 my $filename = basename($project) . "-$hash.patch";
5577                 print $cgi->header(
5578                         -type => 'text/plain',
5579                         -charset => 'utf-8',
5580                         -expires => $expires,
5581                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5582                 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5583                 print "From: " . to_utf8($co{'author'}) . "\n";
5584                 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5585                 print "Subject: " . to_utf8($co{'title'}) . "\n";
5587                 print "X-Git-Tag: $tagname\n" if $tagname;
5588                 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5590                 foreach my $line (@{$co{'comment'}}) {
5591                         print to_utf8($line) . "\n";
5592                 }
5593                 print "---\n\n";
5594         } elsif ($format eq 'patch') {
5595                 my $filename = basename($project) . "-$hash.patch";
5597                 print $cgi->header(
5598                         -type => 'text/plain',
5599                         -charset => 'utf-8',
5600                         -expires => $expires,
5601                         -content_disposition => 'inline; filename="' . "$filename" . '"');
5602         }
5604         # write patch
5605         if ($format eq 'html') {
5606                 my $use_parents = !defined $hash_parent ||
5607                         $hash_parent eq '-c' || $hash_parent eq '--cc';
5608                 git_difftree_body(\@difftree, $hash,
5609                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5610                 print "<br/>\n";
5612                 git_patchset_body($fd, \@difftree, $hash,
5613                                   $use_parents ? @{$co{'parents'}} : $hash_parent);
5614                 close $fd;
5615                 print "</div>\n"; # class="page_body"
5616                 git_footer_html();
5618         } elsif ($format eq 'plain') {
5619                 local $/ = undef;
5620                 print <$fd>;
5621                 close $fd
5622                         or print "Reading git-diff-tree failed\n";
5623         } elsif ($format eq 'patch') {
5624                 local $/ = undef;
5625                 print <$fd>;
5626                 close $fd
5627                         or print "Reading git-format-patch failed\n";
5628         }
5631 sub git_commitdiff_plain {
5632         git_commitdiff(-format => 'plain');
5635 # format-patch-style patches
5636 sub git_patch {
5637         git_commitdiff(-format => 'patch', -single=> 1);
5640 sub git_patches {
5641         git_commitdiff(-format => 'patch');
5644 sub git_history {
5645         if (!defined $hash_base) {
5646                 $hash_base = git_get_head_hash($project);
5647         }
5648         if (!defined $page) {
5649                 $page = 0;
5650         }
5651         my $ftype;
5652         my %co = parse_commit($hash_base)
5653             or die_error(404, "Unknown commit object");
5655         my $refs = git_get_references();
5656         my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5658         my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5659                                        $file_name, "--full-history")
5660             or die_error(404, "No such file or directory on given branch");
5662         if (!defined $hash && defined $file_name) {
5663                 # some commits could have deleted file in question,
5664                 # and not have it in tree, but one of them has to have it
5665                 for (my $i = 0; $i <= @commitlist; $i++) {
5666                         $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5667                         last if defined $hash;
5668                 }
5669         }
5670         if (defined $hash) {
5671                 $ftype = git_get_type($hash);
5672         }
5673         if (!defined $ftype) {
5674                 die_error(500, "Unknown type of object");
5675         }
5677         my $paging_nav = '';
5678         if ($page > 0) {
5679                 $paging_nav .=
5680                         $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5681                                                file_name=>$file_name)},
5682                                 "first");
5683                 $paging_nav .= " &sdot; " .
5684                         $cgi->a({-href => href(-replay=>1, page=>$page-1),
5685                                  -accesskey => "p", -title => "Alt-p"}, "prev");
5686         } else {
5687                 $paging_nav .= "first";
5688                 $paging_nav .= " &sdot; prev";
5689         }
5690         my $next_link = '';
5691         if ($#commitlist >= 100) {
5692                 $next_link =
5693                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5694                                  -accesskey => "n", -title => "Alt-n"}, "next");
5695                 $paging_nav .= " &sdot; $next_link";
5696         } else {
5697                 $paging_nav .= " &sdot; next";
5698         }
5700         git_header_html();
5701         git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5702         git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5703         git_print_page_path($file_name, $ftype, $hash_base);
5705         git_history_body(\@commitlist, 0, 99,
5706                          $refs, $hash_base, $ftype, $next_link);
5708         git_footer_html();
5711 sub git_search {
5712         gitweb_check_feature('search') or die_error(403, "Search is disabled");
5713         if (!defined $searchtext) {
5714                 die_error(400, "Text field is empty");
5715         }
5716         if (!defined $hash) {
5717                 $hash = git_get_head_hash($project);
5718         }
5719         my %co = parse_commit($hash);
5720         if (!%co) {
5721                 die_error(404, "Unknown commit object");
5722         }
5723         if (!defined $page) {
5724                 $page = 0;
5725         }
5727         $searchtype ||= 'commit';
5728         if ($searchtype eq 'pickaxe') {
5729                 # pickaxe may take all resources of your box and run for several minutes
5730                 # with every query - so decide by yourself how public you make this feature
5731                 gitweb_check_feature('pickaxe')
5732                     or die_error(403, "Pickaxe is disabled");
5733         }
5734         if ($searchtype eq 'grep') {
5735                 gitweb_check_feature('grep')
5736                     or die_error(403, "Grep is disabled");
5737         }
5739         git_header_html();
5741         if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5742                 my $greptype;
5743                 if ($searchtype eq 'commit') {
5744                         $greptype = "--grep=";
5745                 } elsif ($searchtype eq 'author') {
5746                         $greptype = "--author=";
5747                 } elsif ($searchtype eq 'committer') {
5748                         $greptype = "--committer=";
5749                 }
5750                 $greptype .= $searchtext;
5751                 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5752                                                $greptype, '--regexp-ignore-case',
5753                                                $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5755                 my $paging_nav = '';
5756                 if ($page > 0) {
5757                         $paging_nav .=
5758                                 $cgi->a({-href => href(action=>"search", hash=>$hash,
5759                                                        searchtext=>$searchtext,
5760                                                        searchtype=>$searchtype)},
5761                                         "first");
5762                         $paging_nav .= " &sdot; " .
5763                                 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5764                                          -accesskey => "p", -title => "Alt-p"}, "prev");
5765                 } else {
5766                         $paging_nav .= "first";
5767                         $paging_nav .= " &sdot; prev";
5768                 }
5769                 my $next_link = '';
5770                 if ($#commitlist >= 100) {
5771                         $next_link =
5772                                 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5773                                          -accesskey => "n", -title => "Alt-n"}, "next");
5774                         $paging_nav .= " &sdot; $next_link";
5775                 } else {
5776                         $paging_nav .= " &sdot; next";
5777                 }
5779                 if ($#commitlist >= 100) {
5780                 }
5782                 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5783                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5784                 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5785         }
5787         if ($searchtype eq 'pickaxe') {
5788                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5789                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5791                 print "<table class=\"pickaxe search\">\n";
5792                 my $alternate = 1;
5793                 $/ = "\n";
5794                 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5795                         '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5796                         ($search_use_regexp ? '--pickaxe-regex' : ());
5797                 undef %co;
5798                 my @files;
5799                 while (my $line = <$fd>) {
5800                         chomp $line;
5801                         next unless $line;
5803                         my %set = parse_difftree_raw_line($line);
5804                         if (defined $set{'commit'}) {
5805                                 # finish previous commit
5806                                 if (%co) {
5807                                         print "</td>\n" .
5808                                               "<td class=\"link\">" .
5809                                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5810                                               " | " .
5811                                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5812                                         print "</td>\n" .
5813                                               "</tr>\n";
5814                                 }
5816                                 if ($alternate) {
5817                                         print "<tr class=\"dark\">\n";
5818                                 } else {
5819                                         print "<tr class=\"light\">\n";
5820                                 }
5821                                 $alternate ^= 1;
5822                                 %co = parse_commit($set{'commit'});
5823                                 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5824                                 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5825                                       "<td><i>$author</i></td>\n" .
5826                                       "<td>" .
5827                                       $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5828                                               -class => "list subject"},
5829                                               chop_and_escape_str($co{'title'}, 50) . "<br/>");
5830                         } elsif (defined $set{'to_id'}) {
5831                                 next if ($set{'to_id'} =~ m/^0{40}$/);
5833                                 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5834                                                              hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5835                                               -class => "list"},
5836                                               "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5837                                       "<br/>\n";
5838                         }
5839                 }
5840                 close $fd;
5842                 # finish last commit (warning: repetition!)
5843                 if (%co) {
5844                         print "</td>\n" .
5845                               "<td class=\"link\">" .
5846                               $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5847                               " | " .
5848                               $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5849                         print "</td>\n" .
5850                               "</tr>\n";
5851                 }
5853                 print "</table>\n";
5854         }
5856         if ($searchtype eq 'grep') {
5857                 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5858                 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5860                 print "<table class=\"grep_search\">\n";
5861                 my $alternate = 1;
5862                 my $matches = 0;
5863                 $/ = "\n";
5864                 open my $fd, "-|", git_cmd(), 'grep', '-n',
5865                         $search_use_regexp ? ('-E', '-i') : '-F',
5866                         $searchtext, $co{'tree'};
5867                 my $lastfile = '';
5868                 while (my $line = <$fd>) {
5869                         chomp $line;
5870                         my ($file, $lno, $ltext, $binary);
5871                         last if ($matches++ > 1000);
5872                         if ($line =~ /^Binary file (.+) matches$/) {
5873                                 $file = $1;
5874                                 $binary = 1;
5875                         } else {
5876                                 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5877                         }
5878                         if ($file ne $lastfile) {
5879                                 $lastfile and print "</td></tr>\n";
5880                                 if ($alternate++) {
5881                                         print "<tr class=\"dark\">\n";
5882                                 } else {
5883                                         print "<tr class=\"light\">\n";
5884                                 }
5885                                 print "<td class=\"list\">".
5886                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5887                                                                file_name=>"$file"),
5888                                                 -class => "list"}, esc_path($file));
5889                                 print "</td><td>\n";
5890                                 $lastfile = $file;
5891                         }
5892                         if ($binary) {
5893                                 print "<div class=\"binary\">Binary file</div>\n";
5894                         } else {
5895                                 $ltext = untabify($ltext);
5896                                 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5897                                         $ltext = esc_html($1, -nbsp=>1);
5898                                         $ltext .= '<span class="match">';
5899                                         $ltext .= esc_html($2, -nbsp=>1);
5900                                         $ltext .= '</span>';
5901                                         $ltext .= esc_html($3, -nbsp=>1);
5902                                 } else {
5903                                         $ltext = esc_html($ltext, -nbsp=>1);
5904                                 }
5905                                 print "<div class=\"pre\">" .
5906                                         $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5907                                                                file_name=>"$file").'#l'.$lno,
5908                                                 -class => "linenr"}, sprintf('%4i', $lno))
5909                                         . ' ' .  $ltext . "</div>\n";
5910                         }
5911                 }
5912                 if ($lastfile) {
5913                         print "</td></tr>\n";
5914                         if ($matches > 1000) {
5915                                 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5916                         }
5917                 } else {
5918                         print "<div class=\"diff nodifferences\">No matches found</div>\n";
5919                 }
5920                 close $fd;
5922                 print "</table>\n";
5923         }
5924         git_footer_html();
5927 sub git_search_help {
5928         git_header_html();
5929         git_print_page_nav('','', $hash,$hash,$hash);
5930         print <<EOT;
5931 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5932 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5933 the pattern entered is recognized as the POSIX extended
5934 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5935 insensitive).</p>
5936 <dl>
5937 <dt><b>commit</b></dt>
5938 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5939 EOT
5940         my $have_grep = gitweb_check_feature('grep');
5941         if ($have_grep) {
5942                 print <<EOT;
5943 <dt><b>grep</b></dt>
5944 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5945     a different one) are searched for the given pattern. On large trees, this search can take
5946 a while and put some strain on the server, so please use it with some consideration. Note that
5947 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5948 case-sensitive.</dd>
5949 EOT
5950         }
5951         print <<EOT;
5952 <dt><b>author</b></dt>
5953 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5954 <dt><b>committer</b></dt>
5955 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5956 EOT
5957         my $have_pickaxe = gitweb_check_feature('pickaxe');
5958         if ($have_pickaxe) {
5959                 print <<EOT;
5960 <dt><b>pickaxe</b></dt>
5961 <dd>All commits that caused the string to appear or disappear from any file (changes that
5962 added, removed or "modified" the string) will be listed. This search can take a while and
5963 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5964 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5965 EOT
5966         }
5967         print "</dl>\n";
5968         git_footer_html();
5971 sub git_shortlog {
5972         my $head = git_get_head_hash($project);
5973         if (!defined $hash) {
5974                 $hash = $head;
5975         }
5976         if (!defined $page) {
5977                 $page = 0;
5978         }
5979         my $refs = git_get_references();
5981         my $commit_hash = $hash;
5982         if (defined $hash_parent) {
5983                 $commit_hash = "$hash_parent..$hash";
5984         }
5985         my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5987         my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5988         my $next_link = '';
5989         if ($#commitlist >= 100) {
5990                 $next_link =
5991                         $cgi->a({-href => href(-replay=>1, page=>$page+1),
5992                                  -accesskey => "n", -title => "Alt-n"}, "next");
5993         }
5995         git_header_html();
5996         git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5997         git_print_header_div('summary', $project);
5999         git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6001         git_footer_html();
6004 ## ......................................................................
6005 ## feeds (RSS, Atom; OPML)
6007 sub git_feed {
6008         my $format = shift || 'atom';
6009         my $have_blame = gitweb_check_feature('blame');
6011         # Atom: http://www.atomenabled.org/developers/syndication/
6012         # RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6013         if ($format ne 'rss' && $format ne 'atom') {
6014                 die_error(400, "Unknown web feed format");
6015         }
6017         # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6018         my $head = $hash || 'HEAD';
6019         my @commitlist = parse_commits($head, 150, 0, $file_name);
6021         my %latest_commit;
6022         my %latest_date;
6023         my $content_type = "application/$format+xml";
6024         if (defined $cgi->http('HTTP_ACCEPT') &&
6025                  $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6026                 # browser (feed reader) prefers text/xml
6027                 $content_type = 'text/xml';
6028         }
6029         if (defined($commitlist[0])) {
6030                 %latest_commit = %{$commitlist[0]};
6031                 %latest_date   = parse_date($latest_commit{'author_epoch'});
6032                 print $cgi->header(
6033                         -type => $content_type,
6034                         -charset => 'utf-8',
6035                         -last_modified => $latest_date{'rfc2822'});
6036         } else {
6037                 print $cgi->header(
6038                         -type => $content_type,
6039                         -charset => 'utf-8');
6040         }
6042         # Optimization: skip generating the body if client asks only
6043         # for Last-Modified date.
6044         return if ($cgi->request_method() eq 'HEAD');
6046         # header variables
6047         my $title = "$site_name - $project/$action";
6048         my $feed_type = 'log';
6049         if (defined $hash) {
6050                 $title .= " - '$hash'";
6051                 $feed_type = 'branch log';
6052                 if (defined $file_name) {
6053                         $title .= " :: $file_name";
6054                         $feed_type = 'history';
6055                 }
6056         } elsif (defined $file_name) {
6057                 $title .= " - $file_name";
6058                 $feed_type = 'history';
6059         }
6060         $title .= " $feed_type";
6061         my $descr = git_get_project_description($project);
6062         if (defined $descr) {
6063                 $descr = esc_html($descr);
6064         } else {
6065                 $descr = "$project " .
6066                          ($format eq 'rss' ? 'RSS' : 'Atom') .
6067                          " feed";
6068         }
6069         my $owner = git_get_project_owner($project);
6070         $owner = esc_html($owner);
6072         #header
6073         my $alt_url;
6074         if (defined $file_name) {
6075                 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6076         } elsif (defined $hash) {
6077                 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6078         } else {
6079                 $alt_url = href(-full=>1, action=>"summary");
6080         }
6081         print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6082         if ($format eq 'rss') {
6083                 print <<XML;
6084 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6085 <channel>
6086 XML
6087                 print "<title>$title</title>\n" .
6088                       "<link>$alt_url</link>\n" .
6089                       "<description>$descr</description>\n" .
6090                       "<language>en</language>\n";
6091         } elsif ($format eq 'atom') {
6092                 print <<XML;
6093 <feed xmlns="http://www.w3.org/2005/Atom">
6094 XML
6095                 print "<title>$title</title>\n" .
6096                       "<subtitle>$descr</subtitle>\n" .
6097                       '<link rel="alternate" type="text/html" href="' .
6098                       $alt_url . '" />' . "\n" .
6099                       '<link rel="self" type="' . $content_type . '" href="' .
6100                       $cgi->self_url() . '" />' . "\n" .
6101                       "<id>" . href(-full=>1) . "</id>\n" .
6102                       # use project owner for feed author
6103                       "<author><name>$owner</name></author>\n";
6104                 if (defined $favicon) {
6105                         print "<icon>" . esc_url($favicon) . "</icon>\n";
6106                 }
6107                 if (defined $logo_url) {
6108                         # not twice as wide as tall: 72 x 27 pixels
6109                         print "<logo>" . esc_url($logo) . "</logo>\n";
6110                 }
6111                 if (! %latest_date) {
6112                         # dummy date to keep the feed valid until commits trickle in:
6113                         print "<updated>1970-01-01T00:00:00Z</updated>\n";
6114                 } else {
6115                         print "<updated>$latest_date{'iso-8601'}</updated>\n";
6116                 }
6117         }
6119         # contents
6120         for (my $i = 0; $i <= $#commitlist; $i++) {
6121                 my %co = %{$commitlist[$i]};
6122                 my $commit = $co{'id'};
6123                 # we read 150, we always show 30 and the ones more recent than 48 hours
6124                 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6125                         last;
6126                 }
6127                 my %cd = parse_date($co{'author_epoch'});
6129                 # get list of changed files
6130                 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6131                         $co{'parent'} || "--root",
6132                         $co{'id'}, "--", (defined $file_name ? $file_name : ())
6133                         or next;
6134                 my @difftree = map { chomp; $_ } <$fd>;
6135                 close $fd
6136                         or next;
6138                 # print element (entry, item)
6139                 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6140                 if ($format eq 'rss') {
6141                         print "<item>\n" .
6142                               "<title>" . esc_html($co{'title'}) . "</title>\n" .
6143                               "<author>" . esc_html($co{'author'}) . "</author>\n" .
6144                               "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6145                               "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6146                               "<link>$co_url</link>\n" .
6147                               "<description>" . esc_html($co{'title'}) . "</description>\n" .
6148                               "<content:encoded>" .
6149                               "<![CDATA[\n";
6150                 } elsif ($format eq 'atom') {
6151                         print "<entry>\n" .
6152                               "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6153                               "<updated>$cd{'iso-8601'}</updated>\n" .
6154                               "<author>\n" .
6155                               "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
6156                         if ($co{'author_email'}) {
6157                                 print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
6158                         }
6159                         print "</author>\n" .
6160                               # use committer for contributor
6161                               "<contributor>\n" .
6162                               "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6163                         if ($co{'committer_email'}) {
6164                                 print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6165                         }
6166                         print "</contributor>\n" .
6167                               "<published>$cd{'iso-8601'}</published>\n" .
6168                               "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6169                               "<id>$co_url</id>\n" .
6170                               "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6171                               "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6172                 }
6173                 my $comment = $co{'comment'};
6174                 print "<pre>\n";
6175                 foreach my $line (@$comment) {
6176                         $line = esc_html($line);
6177                         print "$line\n";
6178                 }
6179                 print "</pre><ul>\n";
6180                 foreach my $difftree_line (@difftree) {
6181                         my %difftree = parse_difftree_raw_line($difftree_line);
6182                         next if !$difftree{'from_id'};
6184                         my $file = $difftree{'file'} || $difftree{'to_file'};
6186                         print "<li>" .
6187                               "[" .
6188                               $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6189                                                      hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6190                                                      hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6191                                                      file_name=>$file, file_parent=>$difftree{'from_file'}),
6192                                       -title => "diff"}, 'D');
6193                         if ($have_blame) {
6194                                 print $cgi->a({-href => href(-full=>1, action=>"blame",
6195                                                              file_name=>$file, hash_base=>$commit),
6196                                               -title => "blame"}, 'B');
6197                         }
6198                         # if this is not a feed of a file history
6199                         if (!defined $file_name || $file_name ne $file) {
6200                                 print $cgi->a({-href => href(-full=>1, action=>"history",
6201                                                              file_name=>$file, hash=>$commit),
6202                                               -title => "history"}, 'H');
6203                         }
6204                         $file = esc_path($file);
6205                         print "] ".
6206                               "$file</li>\n";
6207                 }
6208                 if ($format eq 'rss') {
6209                         print "</ul>]]>\n" .
6210                               "</content:encoded>\n" .
6211                               "</item>\n";
6212                 } elsif ($format eq 'atom') {
6213                         print "</ul>\n</div>\n" .
6214                               "</content>\n" .
6215                               "</entry>\n";
6216                 }
6217         }
6219         # end of feed
6220         if ($format eq 'rss') {
6221                 print "</channel>\n</rss>\n";
6222         }       elsif ($format eq 'atom') {
6223                 print "</feed>\n";
6224         }
6227 sub git_rss {
6228         git_feed('rss');
6231 sub git_atom {
6232         git_feed('atom');
6235 sub git_opml {
6236         my @list = git_get_projects_list();
6238         print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
6239         print <<XML;
6240 <?xml version="1.0" encoding="utf-8"?>
6241 <opml version="1.0">
6242 <head>
6243   <title>$site_name OPML Export</title>
6244 </head>
6245 <body>
6246 <outline text="git RSS feeds">
6247 XML
6249         foreach my $pr (@list) {
6250                 my %proj = %$pr;
6251                 my $head = git_get_head_hash($proj{'path'});
6252                 if (!defined $head) {
6253                         next;
6254                 }
6255                 $git_dir = "$projectroot/$proj{'path'}";
6256                 my %co = parse_commit($head);
6257                 if (!%co) {
6258                         next;
6259                 }
6261                 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6262                 my $rss  = "$my_url?p=$proj{'path'};a=rss";
6263                 my $html = "$my_url?p=$proj{'path'};a=summary";
6264                 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6265         }
6266         print <<XML;
6267 </outline>
6268 </body>
6269 </opml>
6270 XML