Merge branch 'next' into refs/top-bases/pu
[git/gitweb.git] / gitweb / gitweb.perl
blob28b9e6c915ac7f83ce26d380a569cfc75fc81d72
1 #!/usr/bin/perl
3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
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'};
25 our $cgi = new CGI;
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute => 1);
30 # core git executable to use
31 # this can just be "git" if your webserver has a sensible PATH
32 our $GIT = "++GIT_BINDIR++/git";
34 # absolute fs-path which will be prepended to the project path
35 #our $projectroot = "/pub/scm";
36 our $projectroot = "++GITWEB_PROJECTROOT++";
38 # fs traversing limit for getting project list
39 # the number is relative to the projectroot
40 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
42 # target of the home link on top of all pages
43 our $home_link = $my_uri || "/";
45 # string of the home link on top of all pages
46 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
48 # name of your site or organization to appear in page titles
49 # replace this with something more descriptive for clearer bookmarks
50 our $site_name = "++GITWEB_SITENAME++"
51 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
53 # filename of html text to include at top of each page
54 our $site_header = "++GITWEB_SITE_HEADER++";
55 # html text to include at home page
56 our $home_text = "++GITWEB_HOMETEXT++";
57 # filename of html text to include at bottom of each page
58 our $site_footer = "++GITWEB_SITE_FOOTER++";
60 # URI of stylesheets
61 our @stylesheets = ("++GITWEB_CSS++");
62 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
63 our $stylesheet = undef;
64 # URI of GIT logo (72x27 size)
65 our $logo = "++GITWEB_LOGO++";
66 # URI of GIT favicon, assumed to be image/png type
67 our $favicon = "++GITWEB_FAVICON++";
68 # URI of gitweb.js
69 our $gitwebjs = "++GITWEB_GITWEBJS++";
71 # URI and label (title) of GIT logo link
72 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
73 #our $logo_label = "git documentation";
74 our $logo_url = "http://git.or.cz/";
75 our $logo_label = "git homepage";
77 # source of projects list
78 our $projects_list = "++GITWEB_LIST++";
80 # the width (in characters) of the projects list "Description" column
81 our $projects_list_description_width = 25;
83 # default order of projects list
84 # valid values are none, project, descr, owner, and age
85 our $default_projects_order = "project";
87 # show repository only if this file exists
88 # (only effective if this variable evaluates to true)
89 our $export_ok = "++GITWEB_EXPORT_OK++";
91 # only allow viewing of repositories also shown on the overview page
92 our $strict_export = "++GITWEB_STRICT_EXPORT++";
94 # list of git base URLs used for URL to where fetch project from,
95 # i.e. full URL is "$git_base_url/$project"
96 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
98 # default blob_plain mimetype and default charset for text/plain blob
99 our $default_blob_plain_mimetype = 'text/plain';
100 our $default_text_plain_charset = undef;
102 # file to use for guessing MIME types before trying /etc/mime.types
103 # (relative to the current git repository)
104 our $mimetypes_file = undef;
106 # assume this charset if line contains non-UTF-8 characters;
107 # it should be valid encoding (see Encoding::Supported(3pm) for list),
108 # for which encoding all byte sequences are valid, for example
109 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
110 # could be even 'utf-8' for the old behavior)
111 our $fallback_encoding = 'latin1';
113 # rename detection options for git-diff and git-diff-tree
114 # - default is '-M', with the cost proportional to
115 # (number of removed files) * (number of new files).
116 # - more costly is '-C' (which implies '-M'), with the cost proportional to
117 # (number of changed files + number of removed files) * (number of new files)
118 # - even more costly is '-C', '--find-copies-harder' with cost
119 # (number of files in the original tree) * (number of new files)
120 # - one might want to include '-B' option, e.g. '-B', '-M'
121 our @diff_opts = ('-M'); # taken from git_commit
123 # projects list cache for busy sites with many projects;
124 # if you set this to non-zero, it will be used as the cached
125 # index lifetime in minutes
127 # the cached list version is stored in $cache_dir/$cache_name and can
128 # be tweaked by other scripts running with the same uid as gitweb -
129 # use this ONLY at secure installations; only single gitweb project
130 # root per system is supported, unless you tweak configuration!
131 our $projlist_cache_lifetime = 0; # in minutes
132 # FHS compliant $cache_dir would be "/var/cache/gitweb"
133 our $cache_dir =
134 (defined $ENV{'TMPDIR'} ? $ENV{'TMPDIR'} : '/tmp').'/gitweb';
135 our $projlist_cache_name = 'gitweb.index.cache';
137 # information about snapshot formats that gitweb is capable of serving
138 our %known_snapshot_formats = (
139 # name => {
140 # 'display' => display name,
141 # 'type' => mime type,
142 # 'suffix' => filename suffix,
143 # 'format' => --format for git-archive,
144 # 'compressor' => [compressor command and arguments]
145 # (array reference, optional)}
147 'tgz' => {
148 'display' => 'tar.gz',
149 'type' => 'application/x-gzip',
150 'suffix' => '.tar.gz',
151 'format' => 'tar',
152 'compressor' => ['gzip']},
154 'tbz2' => {
155 'display' => 'tar.bz2',
156 'type' => 'application/x-bzip2',
157 'suffix' => '.tar.bz2',
158 'format' => 'tar',
159 'compressor' => ['bzip2']},
161 'zip' => {
162 'display' => 'zip',
163 'type' => 'application/x-zip',
164 'suffix' => '.zip',
165 'format' => 'zip'},
168 # Aliases so we understand old gitweb.snapshot values in repository
169 # configuration.
170 our %known_snapshot_format_aliases = (
171 'gzip' => 'tgz',
172 'bzip2' => 'tbz2',
174 # backward compatibility: legacy gitweb config support
175 'x-gzip' => undef, 'gz' => undef,
176 'x-bzip2' => undef, 'bz2' => undef,
177 'x-zip' => undef, '' => undef,
180 # You define site-wide feature defaults here; override them with
181 # $GITWEB_CONFIG as necessary.
182 our %feature = (
183 # feature => {
184 # 'sub' => feature-sub (subroutine),
185 # 'override' => allow-override (boolean),
186 # 'default' => [ default options...] (array reference)}
188 # if feature is overridable (it means that allow-override has true value),
189 # then feature-sub will be called with default options as parameters;
190 # return value of feature-sub indicates if to enable specified feature
192 # if there is no 'sub' key (no feature-sub), then feature cannot be
193 # overriden
195 # use gitweb_check_feature(<feature>) to check if <feature> 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 inster 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).
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.
324 # To enable system wide have in $GITWEB_CONFIG
325 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
326 # Project specific override is not supported.
327 'ctags' => {
328 'override' => 0,
329 'default' => [0]},
332 sub gitweb_check_feature {
333 my ($name) = @_;
334 return unless exists $feature{$name};
335 my ($sub, $override, @defaults) = (
336 $feature{$name}{'sub'},
337 $feature{$name}{'override'},
338 @{$feature{$name}{'default'}});
339 if (!$override) { return @defaults; }
340 if (!defined $sub) {
341 warn "feature $name is not overrideable";
342 return @defaults;
344 return $sub->(@defaults);
347 sub feature_blame {
348 my ($val) = git_get_project_config('blame', '--bool');
350 if ($val eq 'true') {
351 return 1;
352 } elsif ($val eq 'false') {
353 return 0;
356 return $_[0];
359 sub feature_snapshot {
360 my (@fmts) = @_;
362 my ($val) = git_get_project_config('snapshot');
364 if ($val) {
365 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
368 return @fmts;
371 sub feature_grep {
372 my ($val) = git_get_project_config('grep', '--bool');
374 if ($val eq 'true') {
375 return (1);
376 } elsif ($val eq 'false') {
377 return (0);
380 return ($_[0]);
383 sub feature_pickaxe {
384 my ($val) = git_get_project_config('pickaxe', '--bool');
386 if ($val eq 'true') {
387 return (1);
388 } elsif ($val eq 'false') {
389 return (0);
392 return ($_[0]);
395 # checking HEAD file with -e is fragile if the repository was
396 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
397 # and then pruned.
398 sub check_head_link {
399 my ($dir) = @_;
400 my $headfile = "$dir/HEAD";
401 return ((-e $headfile) ||
402 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
405 sub check_export_ok {
406 my ($dir) = @_;
407 return (check_head_link($dir) &&
408 (!$export_ok || -e "$dir/$export_ok"));
411 # process alternate names for backward compatibility
412 # filter out unsupported (unknown) snapshot formats
413 sub filter_snapshot_fmts {
414 my @fmts = @_;
416 @fmts = map {
417 exists $known_snapshot_format_aliases{$_} ?
418 $known_snapshot_format_aliases{$_} : $_} @fmts;
419 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
423 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
424 if (-e $GITWEB_CONFIG) {
425 do $GITWEB_CONFIG;
426 } else {
427 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
428 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
431 # version of the core git binary
432 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
434 $projects_list ||= $projectroot;
436 # ======================================================================
437 # input validation and dispatch
438 our $action = $cgi->param('a');
439 if (defined $action) {
440 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
441 die_error(400, "Invalid action parameter");
445 # parameters which are pathnames
446 our $project = $cgi->param('p');
447 if (defined $project) {
448 if (!validate_pathname($project) ||
449 !(-d "$projectroot/$project") ||
450 !check_head_link("$projectroot/$project") ||
451 ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
452 ($strict_export && !project_in_list($project))) {
453 undef $project;
454 die_error(404, "No such project");
458 our $file_name = $cgi->param('f');
459 if (defined $file_name) {
460 if (!validate_pathname($file_name)) {
461 die_error(400, "Invalid file parameter");
465 our $file_parent = $cgi->param('fp');
466 if (defined $file_parent) {
467 if (!validate_pathname($file_parent)) {
468 die_error(400, "Invalid file parent parameter");
472 # parameters which are refnames
473 our $hash = $cgi->param('h');
474 if (defined $hash) {
475 if (!validate_refname($hash)) {
476 die_error(400, "Invalid hash parameter");
480 our $hash_parent = $cgi->param('hp');
481 if (defined $hash_parent) {
482 if (!validate_refname($hash_parent)) {
483 die_error(400, "Invalid hash parent parameter");
487 our $hash_base = $cgi->param('hb');
488 if (defined $hash_base) {
489 if (!validate_refname($hash_base)) {
490 die_error(400, "Invalid hash base parameter");
494 my %allowed_options = (
495 "--no-merges" => [ qw(rss atom log shortlog history) ],
498 our @extra_options = $cgi->param('opt');
499 if (defined @extra_options) {
500 foreach my $opt (@extra_options) {
501 if (not exists $allowed_options{$opt}) {
502 die_error(400, "Invalid option parameter");
504 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
505 die_error(400, "Invalid option parameter for this action");
510 our $hash_parent_base = $cgi->param('hpb');
511 if (defined $hash_parent_base) {
512 if (!validate_refname($hash_parent_base)) {
513 die_error(400, "Invalid hash parent base parameter");
517 # other parameters
518 our $page = $cgi->param('pg');
519 if (defined $page) {
520 if ($page =~ m/[^0-9]/) {
521 die_error(400, "Invalid page parameter");
525 our $searchtype = $cgi->param('st');
526 if (defined $searchtype) {
527 if ($searchtype =~ m/[^a-z]/) {
528 die_error(400, "Invalid searchtype parameter");
532 our $search_use_regexp = $cgi->param('sr');
534 our $searchtext = $cgi->param('s');
535 our $search_regexp;
536 if (defined $searchtext) {
537 if (length($searchtext) < 2) {
538 die_error(403, "At least two characters are required for search parameter");
540 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
543 # now read PATH_INFO and use it as alternative to parameters
544 sub evaluate_path_info {
545 return if defined $project;
546 my $path_info = $ENV{"PATH_INFO"};
547 return if !$path_info;
548 $path_info =~ s,^/+,,;
549 return if !$path_info;
550 # find which part of PATH_INFO is project
551 $project = $path_info;
552 $project =~ s,/+$,,;
553 while ($project && !check_head_link("$projectroot/$project")) {
554 $project =~ s,/*[^/]*$,,;
556 # validate project
557 $project = validate_pathname($project);
558 if (!$project ||
559 ($export_ok && !-e "$projectroot/$project/$export_ok") ||
560 ($strict_export && !project_in_list($project))) {
561 undef $project;
562 return;
564 # do not change any parameters if an action is given using the query string
565 return if $action;
566 $path_info =~ s,^\Q$project\E/*,,;
567 my ($refname, $pathname) = split(/:/, $path_info, 2);
568 if (defined $pathname) {
569 # we got "project.git/branch:filename" or "project.git/branch:dir/"
570 # we could use git_get_type(branch:pathname), but it needs $git_dir
571 $pathname =~ s,^/+,,;
572 if (!$pathname || substr($pathname, -1) eq "/") {
573 $action ||= "tree";
574 $pathname =~ s,/$,,;
575 } else {
576 $action ||= "blob_plain";
578 $hash_base ||= validate_refname($refname);
579 $file_name ||= validate_pathname($pathname);
580 } elsif (defined $refname) {
581 # we got "project.git/branch"
582 $action ||= "shortlog";
583 $hash ||= validate_refname($refname);
586 evaluate_path_info();
588 # path to the current git repository
589 our $git_dir;
590 $git_dir = "$projectroot/$project" if $project;
592 # dispatch
593 my %actions = (
594 "blame" => \&git_blame,
595 "blame_incremental" => \&git_blame_incremental,
596 "blame_data" => \&git_blame_data,
597 "blobdiff" => \&git_blobdiff,
598 "blobdiff_plain" => \&git_blobdiff_plain,
599 "blob" => \&git_blob,
600 "blob_plain" => \&git_blob_plain,
601 "commitdiff" => \&git_commitdiff,
602 "commitdiff_plain" => \&git_commitdiff_plain,
603 "commit" => \&git_commit,
604 "forks" => \&git_forks,
605 "heads" => \&git_heads,
606 "history" => \&git_history,
607 "log" => \&git_log,
608 "rss" => \&git_rss,
609 "atom" => \&git_atom,
610 "search" => \&git_search,
611 "search_help" => \&git_search_help,
612 "shortlog" => \&git_shortlog,
613 "summary" => \&git_summary,
614 "tag" => \&git_tag,
615 "tags" => \&git_tags,
616 "tree" => \&git_tree,
617 "snapshot" => \&git_snapshot,
618 "object" => \&git_object,
619 # those below don't need $project
620 "opml" => \&git_opml,
621 "project_list" => \&git_project_list,
622 "project_index" => \&git_project_index,
625 if (!defined $action) {
626 if (defined $hash) {
627 $action = git_get_type($hash);
628 } elsif (defined $hash_base && defined $file_name) {
629 $action = git_get_type("$hash_base:$file_name");
630 } elsif (defined $project) {
631 $action = 'summary';
632 } else {
633 $action = 'project_list';
636 if (!defined($actions{$action})) {
637 die_error(400, "Unknown action");
639 if ($action !~ m/^(opml|project_list|project_index)$/ &&
640 !$project) {
641 die_error(400, "Project needed");
643 $actions{$action}->();
644 exit;
646 ## ======================================================================
647 ## action links
649 sub href (%) {
650 my %params = @_;
651 # default is to use -absolute url() i.e. $my_uri
652 my $href = $params{-full} ? $my_url : $my_uri;
654 # XXX: Warning: If you touch this, check the search form for updating,
655 # too.
657 my @mapping = (
658 project => "p",
659 action => "a",
660 file_name => "f",
661 file_parent => "fp",
662 hash => "h",
663 hash_parent => "hp",
664 hash_base => "hb",
665 hash_parent_base => "hpb",
666 page => "pg",
667 order => "o",
668 searchtext => "s",
669 searchtype => "st",
670 snapshot_format => "sf",
671 extra_options => "opt",
672 search_use_regexp => "sr",
674 my %mapping = @mapping;
676 $params{'project'} = $project unless exists $params{'project'};
678 if ($params{-replay}) {
679 while (my ($name, $symbol) = each %mapping) {
680 if (!exists $params{$name}) {
681 # to allow for multivalued params we use arrayref form
682 $params{$name} = [ $cgi->param($symbol) ];
687 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
688 if ($use_pathinfo) {
689 # use PATH_INFO for project name
690 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
691 delete $params{'project'};
693 # Summary just uses the project path URL
694 if (defined $params{'action'} && $params{'action'} eq 'summary') {
695 delete $params{'action'};
699 # now encode the parameters explicitly
700 my @result = ();
701 for (my $i = 0; $i < @mapping; $i += 2) {
702 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
703 if (defined $params{$name}) {
704 if (ref($params{$name}) eq "ARRAY") {
705 foreach my $par (@{$params{$name}}) {
706 push @result, $symbol . "=" . esc_param($par);
708 } else {
709 push @result, $symbol . "=" . esc_param($params{$name});
713 $href .= "?" . join(';', @result) if $params{-partial_query} or scalar @result;
715 return $href;
719 ## ======================================================================
720 ## validation, quoting/unquoting and escaping
722 sub validate_pathname {
723 my $input = shift || return undef;
725 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
726 # at the beginning, at the end, and between slashes.
727 # also this catches doubled slashes
728 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
729 return undef;
731 # no null characters
732 if ($input =~ m!\0!) {
733 return undef;
735 return $input;
738 sub validate_refname {
739 my $input = shift || return undef;
741 # textual hashes are O.K.
742 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
743 return $input;
745 # it must be correct pathname
746 $input = validate_pathname($input)
747 or return undef;
748 # restrictions on ref name according to git-check-ref-format
749 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
750 return undef;
752 return $input;
755 # decode sequences of octets in utf8 into Perl's internal form,
756 # which is utf-8 with utf8 flag set if needed. gitweb writes out
757 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
758 sub to_utf8 {
759 my $str = shift;
760 if (utf8::valid($str)) {
761 utf8::decode($str);
762 return $str;
763 } else {
764 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
768 # quote unsafe chars, but keep the slash, even when it's not
769 # correct, but quoted slashes look too horrible in bookmarks
770 sub esc_param {
771 my $str = shift;
772 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
773 $str =~ s/\+/%2B/g;
774 $str =~ s/ /\+/g;
775 return $str;
778 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
779 sub esc_url {
780 my $str = shift;
781 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
782 $str =~ s/\+/%2B/g;
783 $str =~ s/ /\+/g;
784 return $str;
787 # replace invalid utf8 character with SUBSTITUTION sequence
788 sub esc_html ($;%) {
789 my $str = shift;
790 my %opts = @_;
792 $str = to_utf8($str);
793 $str = $cgi->escapeHTML($str);
794 if ($opts{'-nbsp'}) {
795 $str =~ s/ /&nbsp;/g;
797 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
798 return $str;
801 # quote control characters and escape filename to HTML
802 sub esc_path {
803 my $str = shift;
804 my %opts = @_;
806 $str = to_utf8($str);
807 $str = $cgi->escapeHTML($str);
808 if ($opts{'-nbsp'}) {
809 $str =~ s/ /&nbsp;/g;
811 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
812 return $str;
815 # Make control characters "printable", using character escape codes (CEC)
816 sub quot_cec {
817 my $cntrl = shift;
818 my %opts = @_;
819 my %es = ( # character escape codes, aka escape sequences
820 "\t" => '\t', # tab (HT)
821 "\n" => '\n', # line feed (LF)
822 "\r" => '\r', # carrige return (CR)
823 "\f" => '\f', # form feed (FF)
824 "\b" => '\b', # backspace (BS)
825 "\a" => '\a', # alarm (bell) (BEL)
826 "\e" => '\e', # escape (ESC)
827 "\013" => '\v', # vertical tab (VT)
828 "\000" => '\0', # nul character (NUL)
830 my $chr = ( (exists $es{$cntrl})
831 ? $es{$cntrl}
832 : sprintf('\%2x', ord($cntrl)) );
833 if ($opts{-nohtml}) {
834 return $chr;
835 } else {
836 return "<span class=\"cntrl\">$chr</span>";
840 # Alternatively use unicode control pictures codepoints,
841 # Unicode "printable representation" (PR)
842 sub quot_upr {
843 my $cntrl = shift;
844 my %opts = @_;
846 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
847 if ($opts{-nohtml}) {
848 return $chr;
849 } else {
850 return "<span class=\"cntrl\">$chr</span>";
854 # git may return quoted and escaped filenames
855 sub unquote {
856 my $str = shift;
858 sub unq {
859 my $seq = shift;
860 my %es = ( # character escape codes, aka escape sequences
861 't' => "\t", # tab (HT, TAB)
862 'n' => "\n", # newline (NL)
863 'r' => "\r", # return (CR)
864 'f' => "\f", # form feed (FF)
865 'b' => "\b", # backspace (BS)
866 'a' => "\a", # alarm (bell) (BEL)
867 'e' => "\e", # escape (ESC)
868 'v' => "\013", # vertical tab (VT)
871 if ($seq =~ m/^[0-7]{1,3}$/) {
872 # octal char sequence
873 return chr(oct($seq));
874 } elsif (exists $es{$seq}) {
875 # C escape sequence, aka character escape code
876 return $es{$seq};
878 # quoted ordinary character
879 return $seq;
882 if ($str =~ m/^"(.*)"$/) {
883 # needs unquoting
884 $str = $1;
885 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
887 return $str;
890 # escape tabs (convert tabs to spaces)
891 sub untabify {
892 my $line = shift;
894 while ((my $pos = index($line, "\t")) != -1) {
895 if (my $count = (8 - ($pos % 8))) {
896 my $spaces = ' ' x $count;
897 $line =~ s/\t/$spaces/;
901 return $line;
904 sub project_in_list {
905 my $project = shift;
906 my @list = git_get_projects_list();
907 return @list && scalar(grep { $_->{'path'} eq $project } @list);
910 ## ----------------------------------------------------------------------
911 ## HTML aware string manipulation
913 # Try to chop given string on a word boundary between position
914 # $len and $len+$add_len. If there is no word boundary there,
915 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
916 # (marking chopped part) would be longer than given string.
917 sub chop_str {
918 my $str = shift;
919 my $len = shift;
920 my $add_len = shift || 10;
921 my $where = shift || 'right'; # 'left' | 'center' | 'right'
923 # Make sure perl knows it is utf8 encoded so we don't
924 # cut in the middle of a utf8 multibyte char.
925 $str = to_utf8($str);
927 # allow only $len chars, but don't cut a word if it would fit in $add_len
928 # if it doesn't fit, cut it if it's still longer than the dots we would add
929 # remove chopped character entities entirely
931 # when chopping in the middle, distribute $len into left and right part
932 # return early if chopping wouldn't make string shorter
933 if ($where eq 'center') {
934 return $str if ($len + 5 >= length($str)); # filler is length 5
935 $len = int($len/2);
936 } else {
937 return $str if ($len + 4 >= length($str)); # filler is length 4
940 # regexps: ending and beginning with word part up to $add_len
941 my $endre = qr/.{$len}\w{0,$add_len}/;
942 my $begre = qr/\w{0,$add_len}.{$len}/;
944 if ($where eq 'left') {
945 $str =~ m/^(.*?)($begre)$/;
946 my ($lead, $body) = ($1, $2);
947 if (length($lead) > 4) {
948 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
949 $lead = " ...";
951 return "$lead$body";
953 } elsif ($where eq 'center') {
954 $str =~ m/^($endre)(.*)$/;
955 my ($left, $str) = ($1, $2);
956 $str =~ m/^(.*?)($begre)$/;
957 my ($mid, $right) = ($1, $2);
958 if (length($mid) > 5) {
959 $left =~ s/&[^;]*$//;
960 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
961 $mid = " ... ";
963 return "$left$mid$right";
965 } else {
966 $str =~ m/^($endre)(.*)$/;
967 my $body = $1;
968 my $tail = $2;
969 if (length($tail) > 4) {
970 $body =~ s/&[^;]*$//;
971 $tail = "... ";
973 return "$body$tail";
977 # takes the same arguments as chop_str, but also wraps a <span> around the
978 # result with a title attribute if it does get chopped. Additionally, the
979 # string is HTML-escaped.
980 sub chop_and_escape_str {
981 my ($str) = @_;
983 my $chopped = chop_str(@_);
984 if ($chopped eq $str) {
985 return esc_html($chopped);
986 } else {
987 $str =~ s/([[:cntrl:]])/?/g;
988 return $cgi->span({-title=>$str}, esc_html($chopped));
992 ## ----------------------------------------------------------------------
993 ## functions returning short strings
995 # CSS class for given age value (in seconds)
996 sub age_class {
997 my $age = shift;
999 if (!defined $age) {
1000 return "noage";
1001 } elsif ($age < 60*60*2) {
1002 return "age0";
1003 } elsif ($age < 60*60*24*2) {
1004 return "age1";
1005 } else {
1006 return "age2";
1010 # convert age in seconds to "nn units ago" string
1011 sub age_string {
1012 my $age = shift;
1013 my $age_str;
1015 if ($age > 60*60*24*365*2) {
1016 $age_str = (int $age/60/60/24/365);
1017 $age_str .= " years ago";
1018 } elsif ($age > 60*60*24*(365/12)*2) {
1019 $age_str = int $age/60/60/24/(365/12);
1020 $age_str .= " months ago";
1021 } elsif ($age > 60*60*24*7*2) {
1022 $age_str = int $age/60/60/24/7;
1023 $age_str .= " weeks ago";
1024 } elsif ($age > 60*60*24*2) {
1025 $age_str = int $age/60/60/24;
1026 $age_str .= " days ago";
1027 } elsif ($age > 60*60*2) {
1028 $age_str = int $age/60/60;
1029 $age_str .= " hours ago";
1030 } elsif ($age > 60*2) {
1031 $age_str = int $age/60;
1032 $age_str .= " min ago";
1033 } elsif ($age > 2) {
1034 $age_str = int $age;
1035 $age_str .= " sec ago";
1036 } else {
1037 $age_str .= " right now";
1039 return $age_str;
1042 use constant {
1043 S_IFINVALID => 0030000,
1044 S_IFGITLINK => 0160000,
1047 # submodule/subproject, a commit object reference
1048 sub S_ISGITLINK($) {
1049 my $mode = shift;
1051 return (($mode & S_IFMT) == S_IFGITLINK)
1054 # convert file mode in octal to symbolic file mode string
1055 sub mode_str {
1056 my $mode = oct shift;
1058 if (S_ISGITLINK($mode)) {
1059 return 'm---------';
1060 } elsif (S_ISDIR($mode & S_IFMT)) {
1061 return 'drwxr-xr-x';
1062 } elsif (S_ISLNK($mode)) {
1063 return 'lrwxrwxrwx';
1064 } elsif (S_ISREG($mode)) {
1065 # git cares only about the executable bit
1066 if ($mode & S_IXUSR) {
1067 return '-rwxr-xr-x';
1068 } else {
1069 return '-rw-r--r--';
1071 } else {
1072 return '----------';
1076 # convert file mode in octal to file type string
1077 sub file_type {
1078 my $mode = shift;
1080 if ($mode !~ m/^[0-7]+$/) {
1081 return $mode;
1082 } else {
1083 $mode = oct $mode;
1086 if (S_ISGITLINK($mode)) {
1087 return "submodule";
1088 } elsif (S_ISDIR($mode & S_IFMT)) {
1089 return "directory";
1090 } elsif (S_ISLNK($mode)) {
1091 return "symlink";
1092 } elsif (S_ISREG($mode)) {
1093 return "file";
1094 } else {
1095 return "unknown";
1099 # convert file mode in octal to file type description string
1100 sub file_type_long {
1101 my $mode = shift;
1103 if ($mode !~ m/^[0-7]+$/) {
1104 return $mode;
1105 } else {
1106 $mode = oct $mode;
1109 if (S_ISGITLINK($mode)) {
1110 return "submodule";
1111 } elsif (S_ISDIR($mode & S_IFMT)) {
1112 return "directory";
1113 } elsif (S_ISLNK($mode)) {
1114 return "symlink";
1115 } elsif (S_ISREG($mode)) {
1116 if ($mode & S_IXUSR) {
1117 return "executable";
1118 } else {
1119 return "file";
1121 } else {
1122 return "unknown";
1127 ## ----------------------------------------------------------------------
1128 ## functions returning short HTML fragments, or transforming HTML fragments
1129 ## which don't belong to other sections
1131 # format line of commit message.
1132 sub format_log_line_html {
1133 my $line = shift;
1135 $line = esc_html($line, -nbsp=>1);
1136 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1137 my $hash_text = $1;
1138 my $link =
1139 $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1140 -class => "text"}, $hash_text);
1141 $line =~ s/$hash_text/$link/;
1143 return $line;
1146 # format marker of refs pointing to given object
1148 # the destination action is chosen based on object type and current context:
1149 # - for annotated tags, we choose the tag view unless it's the current view
1150 # already, in which case we go to shortlog view
1151 # - for other refs, we keep the current view if we're in history, shortlog or
1152 # log view, and select shortlog otherwise
1153 sub format_ref_marker {
1154 my ($refs, $id) = @_;
1155 my $markers = '';
1157 if (defined $refs->{$id}) {
1158 foreach my $ref (@{$refs->{$id}}) {
1159 # this code exploits the fact that non-lightweight tags are the
1160 # only indirect objects, and that they are the only objects for which
1161 # we want to use tag instead of shortlog as action
1162 my ($type, $name) = qw();
1163 my $indirect = ($ref =~ s/\^\{\}$//);
1164 # e.g. tags/v2.6.11 or heads/next
1165 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1166 $type = $1;
1167 $name = $2;
1168 } else {
1169 $type = "ref";
1170 $name = $ref;
1173 my $class = $type;
1174 $class .= " indirect" if $indirect;
1176 my $dest_action = "shortlog";
1178 if ($indirect) {
1179 $dest_action = "tag" unless $action eq "tag";
1180 } elsif ($action =~ /^(history|(short)?log)$/) {
1181 $dest_action = $action;
1184 my $dest = "";
1185 $dest .= "refs/" unless $ref =~ m!^refs/!;
1186 $dest .= $ref;
1188 my $link = $cgi->a({
1189 -href => href(
1190 action=>$dest_action,
1191 hash=>$dest
1192 )}, $name);
1194 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1195 $link . "</span>";
1199 if ($markers) {
1200 return ' <span class="refs">'. $markers . '</span>';
1201 } else {
1202 return "";
1206 # format, perhaps shortened and with markers, title line
1207 sub format_subject_html {
1208 my ($long, $short, $href, $extra) = @_;
1209 $extra = '' unless defined($extra);
1211 if (length($short) < length($long)) {
1212 return $cgi->a({-href => $href, -class => "list subject",
1213 -title => to_utf8($long)},
1214 esc_html($short) . $extra);
1215 } else {
1216 return $cgi->a({-href => $href, -class => "list subject"},
1217 esc_html($long) . $extra);
1221 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1222 sub format_git_diff_header_line {
1223 my $line = shift;
1224 my $diffinfo = shift;
1225 my ($from, $to) = @_;
1227 if ($diffinfo->{'nparents'}) {
1228 # combined diff
1229 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1230 if ($to->{'href'}) {
1231 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1232 esc_path($to->{'file'}));
1233 } else { # file was deleted (no href)
1234 $line .= esc_path($to->{'file'});
1236 } else {
1237 # "ordinary" diff
1238 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1239 if ($from->{'href'}) {
1240 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1241 'a/' . esc_path($from->{'file'}));
1242 } else { # file was added (no href)
1243 $line .= 'a/' . esc_path($from->{'file'});
1245 $line .= ' ';
1246 if ($to->{'href'}) {
1247 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1248 'b/' . esc_path($to->{'file'}));
1249 } else { # file was deleted
1250 $line .= 'b/' . esc_path($to->{'file'});
1254 return "<div class=\"diff header\">$line</div>\n";
1257 # format extended diff header line, before patch itself
1258 sub format_extended_diff_header_line {
1259 my $line = shift;
1260 my $diffinfo = shift;
1261 my ($from, $to) = @_;
1263 # match <path>
1264 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1265 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1266 esc_path($from->{'file'}));
1268 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1269 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1270 esc_path($to->{'file'}));
1272 # match single <mode>
1273 if ($line =~ m/\s(\d{6})$/) {
1274 $line .= '<span class="info"> (' .
1275 file_type_long($1) .
1276 ')</span>';
1278 # match <hash>
1279 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1280 # can match only for combined diff
1281 $line = 'index ';
1282 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1283 if ($from->{'href'}[$i]) {
1284 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1285 -class=>"hash"},
1286 substr($diffinfo->{'from_id'}[$i],0,7));
1287 } else {
1288 $line .= '0' x 7;
1290 # separator
1291 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1293 $line .= '..';
1294 if ($to->{'href'}) {
1295 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1296 substr($diffinfo->{'to_id'},0,7));
1297 } else {
1298 $line .= '0' x 7;
1301 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1302 # can match only for ordinary diff
1303 my ($from_link, $to_link);
1304 if ($from->{'href'}) {
1305 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1306 substr($diffinfo->{'from_id'},0,7));
1307 } else {
1308 $from_link = '0' x 7;
1310 if ($to->{'href'}) {
1311 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1312 substr($diffinfo->{'to_id'},0,7));
1313 } else {
1314 $to_link = '0' x 7;
1316 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1317 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1320 return $line . "<br/>\n";
1323 # format from-file/to-file diff header
1324 sub format_diff_from_to_header {
1325 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1326 my $line;
1327 my $result = '';
1329 $line = $from_line;
1330 #assert($line =~ m/^---/) if DEBUG;
1331 # no extra formatting for "^--- /dev/null"
1332 if (! $diffinfo->{'nparents'}) {
1333 # ordinary (single parent) diff
1334 if ($line =~ m!^--- "?a/!) {
1335 if ($from->{'href'}) {
1336 $line = '--- a/' .
1337 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1338 esc_path($from->{'file'}));
1339 } else {
1340 $line = '--- a/' .
1341 esc_path($from->{'file'});
1344 $result .= qq!<div class="diff from_file">$line</div>\n!;
1346 } else {
1347 # combined diff (merge commit)
1348 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1349 if ($from->{'href'}[$i]) {
1350 $line = '--- ' .
1351 $cgi->a({-href=>href(action=>"blobdiff",
1352 hash_parent=>$diffinfo->{'from_id'}[$i],
1353 hash_parent_base=>$parents[$i],
1354 file_parent=>$diffinfo->{'from_prefix'}.$from->{'file'}[$i],
1355 hash=>$diffinfo->{'to_id'},
1356 hash_base=>$hash,
1357 file_name=>$diffinfo->{'to_prefix'}.$to->{'file'}),
1358 -class=>"path",
1359 -title=>"diff" . ($i+1)},
1360 $i+1) .
1361 '/' .
1362 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1363 esc_path($from->{'file'}[$i]));
1364 } else {
1365 $line = '--- /dev/null';
1367 $result .= qq!<div class="diff from_file">$line</div>\n!;
1371 $line = $to_line;
1372 #assert($line =~ m/^\+\+\+/) if DEBUG;
1373 # no extra formatting for "^+++ /dev/null"
1374 if ($line =~ m!^\+\+\+ "?b/!) {
1375 if ($to->{'href'}) {
1376 $line = '+++ b/' .
1377 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1378 esc_path($to->{'file'}));
1379 } else {
1380 $line = '+++ b/' .
1381 esc_path($to->{'file'});
1384 $result .= qq!<div class="diff to_file">$line</div>\n!;
1386 return $result;
1389 # create note for patch simplified by combined diff
1390 sub format_diff_cc_simplified {
1391 my ($diffinfo, @parents) = @_;
1392 my $result = '';
1394 $result .= "<div class=\"diff header\">" .
1395 "diff --cc ";
1396 if (!is_deleted($diffinfo)) {
1397 $result .= $cgi->a({-href => href(action=>"blob",
1398 hash_base=>$hash,
1399 hash=>$diffinfo->{'to_id'},
1400 file_name=>$diffinfo->{'to_prefix'}.$diffinfo->{'to_file'}),
1401 -class => "path"},
1402 esc_path($diffinfo->{'to_file'}));
1403 } else {
1404 $result .= esc_path($diffinfo->{'to_file'});
1406 $result .= "</div>\n" . # class="diff header"
1407 "<div class=\"diff nodifferences\">" .
1408 "Simple merge" .
1409 "</div>\n"; # class="diff nodifferences"
1411 return $result;
1414 # format patch (diff) line (not to be used for diff headers)
1415 sub format_diff_line {
1416 my $line = shift;
1417 my ($from, $to) = @_;
1418 my $diff_class = "";
1420 chomp $line;
1422 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1423 # combined diff
1424 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1425 if ($line =~ m/^\@{3}/) {
1426 $diff_class = " chunk_header";
1427 } elsif ($line =~ m/^\\/) {
1428 $diff_class = " incomplete";
1429 } elsif ($prefix =~ tr/+/+/) {
1430 $diff_class = " add";
1431 } elsif ($prefix =~ tr/-/-/) {
1432 $diff_class = " rem";
1434 } else {
1435 # assume ordinary diff
1436 my $char = substr($line, 0, 1);
1437 if ($char eq '+') {
1438 $diff_class = " add";
1439 } elsif ($char eq '-') {
1440 $diff_class = " rem";
1441 } elsif ($char eq '@') {
1442 $diff_class = " chunk_header";
1443 } elsif ($char eq "\\") {
1444 $diff_class = " incomplete";
1447 $line = untabify($line);
1448 if ($from && $to && $line =~ m/^\@{2} /) {
1449 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1450 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1452 $from_lines = 0 unless defined $from_lines;
1453 $to_lines = 0 unless defined $to_lines;
1455 if ($from->{'href'}) {
1456 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1457 -class=>"list"}, $from_text);
1459 if ($to->{'href'}) {
1460 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1461 -class=>"list"}, $to_text);
1463 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1464 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1465 return "<div class=\"diff$diff_class\">$line</div>\n";
1466 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1467 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1468 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1470 @from_text = split(' ', $ranges);
1471 for (my $i = 0; $i < @from_text; ++$i) {
1472 ($from_start[$i], $from_nlines[$i]) =
1473 (split(',', substr($from_text[$i], 1)), 0);
1476 $to_text = pop @from_text;
1477 $to_start = pop @from_start;
1478 $to_nlines = pop @from_nlines;
1480 $line = "<span class=\"chunk_info\">$prefix ";
1481 for (my $i = 0; $i < @from_text; ++$i) {
1482 if ($from->{'href'}[$i]) {
1483 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1484 -class=>"list"}, $from_text[$i]);
1485 } else {
1486 $line .= $from_text[$i];
1488 $line .= " ";
1490 if ($to->{'href'}) {
1491 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1492 -class=>"list"}, $to_text);
1493 } else {
1494 $line .= $to_text;
1496 $line .= " $prefix</span>" .
1497 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1498 return "<div class=\"diff$diff_class\">$line</div>\n";
1500 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1503 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1504 # linked. Pass the hash of the tree/commit to snapshot.
1505 sub format_snapshot_links {
1506 my ($hash) = @_;
1507 my @snapshot_fmts = gitweb_check_feature('snapshot');
1508 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1509 my $num_fmts = @snapshot_fmts;
1510 if ($num_fmts > 1) {
1511 # A parenthesized list of links bearing format names.
1512 # e.g. "snapshot (_tar.gz_ _zip_)"
1513 return "snapshot (" . join(' ', map
1514 $cgi->a({
1515 -href => href(
1516 action=>"snapshot",
1517 hash=>$hash,
1518 snapshot_format=>$_
1520 }, $known_snapshot_formats{$_}{'display'})
1521 , @snapshot_fmts) . ")";
1522 } elsif ($num_fmts == 1) {
1523 # A single "snapshot" link whose tooltip bears the format name.
1524 # i.e. "_snapshot_"
1525 my ($fmt) = @snapshot_fmts;
1526 return
1527 $cgi->a({
1528 -href => href(
1529 action=>"snapshot",
1530 hash=>$hash,
1531 snapshot_format=>$fmt
1533 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1534 }, "snapshot");
1535 } else { # $num_fmts == 0
1536 return undef;
1540 ## ......................................................................
1541 ## functions returning values to be passed, perhaps after some
1542 ## transformation, to other functions; e.g. returning arguments to href()
1544 # returns hash to be passed to href to generate gitweb URL
1545 # in -title key it returns description of link
1546 sub get_feed_info {
1547 my $format = shift || 'Atom';
1548 my %res = (action => lc($format));
1550 # feed links are possible only for project views
1551 return unless (defined $project);
1552 # some views should link to OPML, or to generic project feed,
1553 # or don't have specific feed yet (so they should use generic)
1554 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1556 my $branch;
1557 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1558 # from tag links; this also makes possible to detect branch links
1559 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1560 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1561 $branch = $1;
1563 # find log type for feed description (title)
1564 my $type = 'log';
1565 if (defined $file_name) {
1566 $type = "history of $file_name";
1567 $type .= "/" if ($action eq 'tree');
1568 $type .= " on '$branch'" if (defined $branch);
1569 } else {
1570 $type = "log of $branch" if (defined $branch);
1573 $res{-title} = $type;
1574 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1575 $res{'file_name'} = $file_name;
1577 return %res;
1580 ## ----------------------------------------------------------------------
1581 ## git utility subroutines, invoking git commands
1583 # returns path to the core git executable and the --git-dir parameter as list
1584 sub git_cmd {
1585 return $GIT, '--git-dir='.$git_dir;
1588 # quote the given arguments for passing them to the shell
1589 # quote_command("command", "arg 1", "arg with ' and ! characters")
1590 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1591 # Try to avoid using this function wherever possible.
1592 sub quote_command {
1593 return join(' ',
1594 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1597 # get HEAD ref of given project as hash
1598 sub git_get_head_hash {
1599 my $project = shift;
1600 my $o_git_dir = $git_dir;
1601 my $retval = undef;
1602 $git_dir = "$projectroot/$project";
1603 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1604 my $head = <$fd>;
1605 close $fd;
1606 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1607 $retval = $1;
1610 if (defined $o_git_dir) {
1611 $git_dir = $o_git_dir;
1613 return $retval;
1616 # get type of given object
1617 sub git_get_type {
1618 my $hash = shift;
1620 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1621 my $type = <$fd>;
1622 close $fd or return;
1623 chomp $type;
1624 return $type;
1627 # repository configuration
1628 our $config_file = '';
1629 our %config;
1631 # store multiple values for single key as anonymous array reference
1632 # single values stored directly in the hash, not as [ <value> ]
1633 sub hash_set_multi {
1634 my ($hash, $key, $value) = @_;
1636 if (!exists $hash->{$key}) {
1637 $hash->{$key} = $value;
1638 } elsif (!ref $hash->{$key}) {
1639 $hash->{$key} = [ $hash->{$key}, $value ];
1640 } else {
1641 push @{$hash->{$key}}, $value;
1645 # return hash of git project configuration
1646 # optionally limited to some section, e.g. 'gitweb'
1647 sub git_parse_project_config {
1648 my $section_regexp = shift;
1649 my %config;
1651 local $/ = "\0";
1653 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1654 or return;
1656 while (my $keyval = <$fh>) {
1657 chomp $keyval;
1658 my ($key, $value) = split(/\n/, $keyval, 2);
1660 hash_set_multi(\%config, $key, $value)
1661 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1663 close $fh;
1665 return %config;
1668 # convert config value to boolean, 'true' or 'false'
1669 # no value, number > 0, 'true' and 'yes' values are true
1670 # rest of values are treated as false (never as error)
1671 sub config_to_bool {
1672 my $val = shift;
1674 # strip leading and trailing whitespace
1675 $val =~ s/^\s+//;
1676 $val =~ s/\s+$//;
1678 return (!defined $val || # section.key
1679 ($val =~ /^\d+$/ && $val) || # section.key = 1
1680 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1683 # convert config value to simple decimal number
1684 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1685 # to be multiplied by 1024, 1048576, or 1073741824
1686 sub config_to_int {
1687 my $val = shift;
1689 # strip leading and trailing whitespace
1690 $val =~ s/^\s+//;
1691 $val =~ s/\s+$//;
1693 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1694 $unit = lc($unit);
1695 # unknown unit is treated as 1
1696 return $num * ($unit eq 'g' ? 1073741824 :
1697 $unit eq 'm' ? 1048576 :
1698 $unit eq 'k' ? 1024 : 1);
1700 return $val;
1703 # convert config value to array reference, if needed
1704 sub config_to_multi {
1705 my $val = shift;
1707 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1710 sub git_get_project_config {
1711 my ($key, $type) = @_;
1713 # key sanity check
1714 return unless ($key);
1715 $key =~ s/^gitweb\.//;
1716 return if ($key =~ m/\W/);
1718 # type sanity check
1719 if (defined $type) {
1720 $type =~ s/^--//;
1721 $type = undef
1722 unless ($type eq 'bool' || $type eq 'int');
1725 # get config
1726 if (!defined $config_file ||
1727 $config_file ne "$git_dir/config") {
1728 %config = git_parse_project_config('gitweb');
1729 $config_file = "$git_dir/config";
1732 # ensure given type
1733 if (!defined $type) {
1734 return $config{"gitweb.$key"};
1735 } elsif ($type eq 'bool') {
1736 # backward compatibility: 'git config --bool' returns true/false
1737 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1738 } elsif ($type eq 'int') {
1739 return config_to_int($config{"gitweb.$key"});
1741 return $config{"gitweb.$key"};
1744 # get hash of given path at given ref
1745 sub git_get_hash_by_path {
1746 my $base = shift;
1747 my $path = shift || return undef;
1748 my $type = shift;
1750 $path =~ s,/+$,,;
1752 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1753 or die_error(500, "Open git-ls-tree failed");
1754 my $line = <$fd>;
1755 close $fd or return undef;
1757 if (!defined $line) {
1758 # there is no tree or hash given by $path at $base
1759 return undef;
1762 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1763 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1764 if (defined $type && $type ne $2) {
1765 # type doesn't match
1766 return undef;
1768 return $3;
1771 # get path of entry with given hash at given tree-ish (ref)
1772 # used to get 'from' filename for combined diff (merge commit) for renames
1773 sub git_get_path_by_hash {
1774 my $base = shift || return;
1775 my $hash = shift || return;
1777 local $/ = "\0";
1779 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1780 or return undef;
1781 while (my $line = <$fd>) {
1782 chomp $line;
1784 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1785 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1786 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1787 close $fd;
1788 return $1;
1791 close $fd;
1792 return undef;
1795 ## ......................................................................
1796 ## git utility functions, directly accessing git repository
1798 sub git_get_project_description {
1799 my $path = shift;
1801 $git_dir = "$projectroot/$path";
1802 open my $fd, "$git_dir/description"
1803 or return git_get_project_config('description');
1804 my $descr = <$fd>;
1805 close $fd;
1806 if (defined $descr) {
1807 chomp $descr;
1809 return $descr;
1812 sub git_get_project_ctags {
1813 my $path = shift;
1814 my $ctags = {};
1816 $git_dir = "$projectroot/$path";
1817 foreach (<$git_dir/ctags/*>) {
1818 open CT, $_ or next;
1819 my $val = <CT>;
1820 chomp $val;
1821 close CT;
1822 my $ctag = $_; $ctag =~ s#.*/##;
1823 $ctags->{$ctag} = $val;
1825 $ctags;
1828 sub git_populate_project_tagcloud {
1829 my $ctags = shift;
1831 # First, merge different-cased tags; tags vote on casing
1832 my %ctags_lc;
1833 foreach (keys %$ctags) {
1834 $ctags_lc{lc $_}->{count} += $ctags->{$_};
1835 if (not $ctags_lc{lc $_}->{topcount}
1836 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
1837 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
1838 $ctags_lc{lc $_}->{topname} = $_;
1842 my $cloud;
1843 if (eval { require HTML::TagCloud; 1; }) {
1844 $cloud = HTML::TagCloud->new;
1845 foreach (sort keys %ctags_lc) {
1846 # Pad the title with spaces so that the cloud looks
1847 # less crammed.
1848 my $title = $ctags_lc{$_}->{topname};
1849 $title =~ s/ /&nbsp;/g;
1850 $title =~ s/^/&nbsp;/g;
1851 $title =~ s/$/&nbsp;/g;
1852 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
1854 } else {
1855 $cloud = \%ctags_lc;
1857 $cloud;
1860 sub git_show_project_tagcloud {
1861 my ($cloud, $count) = @_;
1862 print STDERR ref($cloud)."..\n";
1863 if (ref $cloud eq 'HTML::TagCloud') {
1864 return $cloud->html_and_css($count);
1865 } else {
1866 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
1867 return '<p align="center">' . join (', ', map {
1868 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1869 } splice(@tags, 0, $count)) . '</p>';
1873 sub git_get_project_url_list {
1874 my $path = shift;
1876 $git_dir = "$projectroot/$path";
1877 open my $fd, "$git_dir/cloneurl"
1878 or return wantarray ?
1879 @{ config_to_multi(git_get_project_config('url')) } :
1880 config_to_multi(git_get_project_config('url'));
1881 my @git_project_url_list = map { chomp; $_ } <$fd>;
1882 close $fd;
1884 return wantarray ? @git_project_url_list : \@git_project_url_list;
1887 sub git_get_projects_list {
1888 my ($filter) = @_;
1889 my @list;
1891 $filter ||= '';
1892 $filter =~ s/\.git$//;
1894 my ($check_forks) = gitweb_check_feature('forks');
1896 if (-d $projects_list) {
1897 # search in directory
1898 my $dir = $projects_list . ($filter ? "/$filter" : '');
1899 # remove the trailing "/"
1900 $dir =~ s!/+$!!;
1901 my $pfxlen = length("$dir");
1902 my $pfxdepth = ($dir =~ tr!/!!);
1904 File::Find::find({
1905 follow_fast => 1, # follow symbolic links
1906 follow_skip => 2, # ignore duplicates
1907 dangling_symlinks => 0, # ignore dangling symlinks, silently
1908 wanted => sub {
1909 # skip project-list toplevel, if we get it.
1910 return if (m!^[/.]$!);
1911 # only directories can be git repositories
1912 return unless (-d $_);
1913 # don't traverse too deep (Find is super slow on os x)
1914 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1915 $File::Find::prune = 1;
1916 return;
1919 my $subdir = substr($File::Find::name, $pfxlen + 1);
1920 # we check related file in $projectroot
1921 if (check_export_ok("$projectroot/$filter/$subdir")) {
1922 push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1923 $File::Find::prune = 1;
1926 }, "$dir");
1928 } elsif (-f $projects_list) {
1929 # read from file(url-encoded):
1930 # 'git%2Fgit.git Linus+Torvalds'
1931 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1932 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1933 my %paths;
1934 open my ($fd), $projects_list or return;
1935 PROJECT:
1936 while (my $line = <$fd>) {
1937 chomp $line;
1938 my ($path, $owner) = split ' ', $line;
1939 $path = unescape($path);
1940 $owner = unescape($owner);
1941 if (!defined $path) {
1942 next;
1944 if ($filter ne '') {
1945 # looking for forks;
1946 my $pfx = substr($path, 0, length($filter));
1947 if ($pfx ne $filter) {
1948 next PROJECT;
1950 my $sfx = substr($path, length($filter));
1951 if ($sfx !~ /^\/.*\.git$/) {
1952 next PROJECT;
1954 } elsif ($check_forks) {
1955 PATH:
1956 foreach my $filter (keys %paths) {
1957 # looking for forks;
1958 my $pfx = substr($path, 0, length($filter));
1959 if ($pfx ne $filter) {
1960 next PATH;
1962 my $sfx = substr($path, length($filter));
1963 if ($sfx !~ /^\/.*\.git$/) {
1964 next PATH;
1966 # is a fork, don't include it in
1967 # the list
1968 next PROJECT;
1971 if (check_export_ok("$projectroot/$path")) {
1972 my $pr = {
1973 path => $path,
1974 owner => to_utf8($owner),
1976 push @list, $pr;
1977 (my $forks_path = $path) =~ s/\.git$//;
1978 $paths{$forks_path}++;
1981 close $fd;
1983 return @list;
1986 our $gitweb_project_owner = undef;
1987 sub git_get_project_list_from_file {
1989 return if (defined $gitweb_project_owner);
1991 $gitweb_project_owner = {};
1992 # read from file (url-encoded):
1993 # 'git%2Fgit.git Linus+Torvalds'
1994 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1995 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1996 if (-f $projects_list) {
1997 open (my $fd , $projects_list);
1998 while (my $line = <$fd>) {
1999 chomp $line;
2000 my ($pr, $ow) = split ' ', $line;
2001 $pr = unescape($pr);
2002 $ow = unescape($ow);
2003 $gitweb_project_owner->{$pr} = to_utf8($ow);
2005 close $fd;
2009 sub git_get_project_owner {
2010 my $project = shift;
2011 my $owner;
2013 return undef unless $project;
2014 $git_dir = "$projectroot/$project";
2016 if (!defined $gitweb_project_owner) {
2017 git_get_project_list_from_file();
2020 if (exists $gitweb_project_owner->{$project}) {
2021 $owner = $gitweb_project_owner->{$project};
2023 if (!defined $owner){
2024 $owner = git_get_project_config('owner');
2026 if (!defined $owner) {
2027 $owner = get_file_owner("$git_dir");
2030 return $owner;
2033 sub git_get_last_activity {
2034 my ($path) = @_;
2035 my $fd;
2037 $git_dir = "$projectroot/$path";
2038 open($fd, "-|", git_cmd(), 'for-each-ref',
2039 '--format=%(committer)',
2040 '--sort=-committerdate',
2041 '--count=1',
2042 'refs/heads') or return;
2043 my $most_recent = <$fd>;
2044 close $fd or return;
2045 if (defined $most_recent &&
2046 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2047 my $timestamp = $1;
2048 my $age = time - $timestamp;
2049 return ($age, age_string($age));
2051 return (undef, undef);
2054 sub git_get_references {
2055 my $type = shift || "";
2056 my %refs;
2057 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2058 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2059 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2060 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2061 or return;
2063 while (my $line = <$fd>) {
2064 chomp $line;
2065 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2066 if (defined $refs{$1}) {
2067 push @{$refs{$1}}, $2;
2068 } else {
2069 $refs{$1} = [ $2 ];
2073 close $fd or return;
2074 return \%refs;
2077 sub git_get_rev_name_tags {
2078 my $hash = shift || return undef;
2080 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2081 or return;
2082 my $name_rev = <$fd>;
2083 close $fd;
2085 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2086 return $1;
2087 } else {
2088 # catches also '$hash undefined' output
2089 return undef;
2093 ## ----------------------------------------------------------------------
2094 ## parse to hash functions
2096 sub parse_date {
2097 my $epoch = shift;
2098 my $tz = shift || "-0000";
2100 my %date;
2101 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2102 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2103 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2104 $date{'hour'} = $hour;
2105 $date{'minute'} = $min;
2106 $date{'mday'} = $mday;
2107 $date{'day'} = $days[$wday];
2108 $date{'month'} = $months[$mon];
2109 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2110 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2111 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2112 $mday, $months[$mon], $hour ,$min;
2113 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2114 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2116 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2117 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2118 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2119 $date{'hour_local'} = $hour;
2120 $date{'minute_local'} = $min;
2121 $date{'tz_local'} = $tz;
2122 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2123 1900+$year, $mon+1, $mday,
2124 $hour, $min, $sec, $tz);
2125 return %date;
2128 sub parse_tag {
2129 my $tag_id = shift;
2130 my %tag;
2131 my @comment;
2133 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2134 $tag{'id'} = $tag_id;
2135 while (my $line = <$fd>) {
2136 chomp $line;
2137 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2138 $tag{'object'} = $1;
2139 } elsif ($line =~ m/^type (.+)$/) {
2140 $tag{'type'} = $1;
2141 } elsif ($line =~ m/^tag (.+)$/) {
2142 $tag{'name'} = $1;
2143 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2144 $tag{'author'} = $1;
2145 $tag{'epoch'} = $2;
2146 $tag{'tz'} = $3;
2147 } elsif ($line =~ m/--BEGIN/) {
2148 push @comment, $line;
2149 last;
2150 } elsif ($line eq "") {
2151 last;
2154 push @comment, <$fd>;
2155 $tag{'comment'} = \@comment;
2156 close $fd or return;
2157 if (!defined $tag{'name'}) {
2158 return
2160 return %tag
2163 sub parse_commit_text {
2164 my ($commit_text, $withparents) = @_;
2165 my @commit_lines = split '\n', $commit_text;
2166 my %co;
2168 pop @commit_lines; # Remove '\0'
2170 if (! @commit_lines) {
2171 return;
2174 my $header = shift @commit_lines;
2175 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2176 return;
2178 ($co{'id'}, my @parents) = split ' ', $header;
2179 while (my $line = shift @commit_lines) {
2180 last if $line eq "\n";
2181 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2182 $co{'tree'} = $1;
2183 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2184 push @parents, $1;
2185 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2186 $co{'author'} = $1;
2187 $co{'author_epoch'} = $2;
2188 $co{'author_tz'} = $3;
2189 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2190 $co{'author_name'} = $1;
2191 $co{'author_email'} = $2;
2192 } else {
2193 $co{'author_name'} = $co{'author'};
2195 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2196 $co{'committer'} = $1;
2197 $co{'committer_epoch'} = $2;
2198 $co{'committer_tz'} = $3;
2199 $co{'committer_name'} = $co{'committer'};
2200 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2201 $co{'committer_name'} = $1;
2202 $co{'committer_email'} = $2;
2203 } else {
2204 $co{'committer_name'} = $co{'committer'};
2208 if (!defined $co{'tree'}) {
2209 return;
2211 $co{'parents'} = \@parents;
2212 $co{'parent'} = $parents[0];
2214 foreach my $title (@commit_lines) {
2215 $title =~ s/^ //;
2216 if ($title ne "") {
2217 $co{'title'} = chop_str($title, 80, 5);
2218 # remove leading stuff of merges to make the interesting part visible
2219 if (length($title) > 50) {
2220 $title =~ s/^Automatic //;
2221 $title =~ s/^merge (of|with) /Merge ... /i;
2222 if (length($title) > 50) {
2223 $title =~ s/(http|rsync):\/\///;
2225 if (length($title) > 50) {
2226 $title =~ s/(master|www|rsync)\.//;
2228 if (length($title) > 50) {
2229 $title =~ s/kernel.org:?//;
2231 if (length($title) > 50) {
2232 $title =~ s/\/pub\/scm//;
2235 $co{'title_short'} = chop_str($title, 50, 5);
2236 last;
2239 if (! defined $co{'title'} || $co{'title'} eq "") {
2240 $co{'title'} = $co{'title_short'} = '(no commit message)';
2242 # remove added spaces
2243 foreach my $line (@commit_lines) {
2244 $line =~ s/^ //;
2246 $co{'comment'} = \@commit_lines;
2248 my $age = time - $co{'committer_epoch'};
2249 $co{'age'} = $age;
2250 $co{'age_string'} = age_string($age);
2251 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2252 if ($age > 60*60*24*7*2) {
2253 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2254 $co{'age_string_age'} = $co{'age_string'};
2255 } else {
2256 $co{'age_string_date'} = $co{'age_string'};
2257 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2259 return %co;
2262 sub parse_commit {
2263 my ($commit_id) = @_;
2264 my %co;
2266 local $/ = "\0";
2268 open my $fd, "-|", git_cmd(), "rev-list",
2269 "--parents",
2270 "--header",
2271 "--max-count=1",
2272 $commit_id,
2273 "--",
2274 or die_error(500, "Open git-rev-list failed");
2275 %co = parse_commit_text(<$fd>, 1);
2276 close $fd;
2278 return %co;
2281 sub parse_commits {
2282 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2283 my @cos;
2285 $maxcount ||= 1;
2286 $skip ||= 0;
2288 local $/ = "\0";
2290 open my $fd, "-|", git_cmd(), "rev-list",
2291 "--header",
2292 @args,
2293 ("--max-count=" . $maxcount),
2294 ("--skip=" . $skip),
2295 @extra_options,
2296 $commit_id,
2297 "--",
2298 ($filename ? ($filename) : ())
2299 or die_error(500, "Open git-rev-list failed");
2300 while (my $line = <$fd>) {
2301 my %co = parse_commit_text($line);
2302 push @cos, \%co;
2304 close $fd;
2306 return wantarray ? @cos : \@cos;
2309 # parse line of git-diff-tree "raw" output
2310 sub parse_difftree_raw_line {
2311 my $line = shift;
2312 my %res;
2314 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2315 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2316 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2317 $res{'from_mode'} = $1;
2318 $res{'to_mode'} = $2;
2319 $res{'from_id'} = $3;
2320 $res{'to_id'} = $4;
2321 $res{'status'} = $5;
2322 $res{'similarity'} = $6;
2323 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2324 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2325 } else {
2326 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2329 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2330 # combined diff (for merge commit)
2331 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2332 $res{'nparents'} = length($1);
2333 $res{'from_mode'} = [ split(' ', $2) ];
2334 $res{'to_mode'} = pop @{$res{'from_mode'}};
2335 $res{'from_id'} = [ split(' ', $3) ];
2336 $res{'to_id'} = pop @{$res{'from_id'}};
2337 $res{'status'} = [ split('', $4) ];
2338 $res{'to_file'} = unquote($5);
2340 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2341 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2342 $res{'commit'} = $1;
2345 return wantarray ? %res : \%res;
2348 # wrapper: return parsed line of git-diff-tree "raw" output
2349 # (the argument might be raw line, or parsed info)
2350 sub parsed_difftree_line {
2351 my $line_or_ref = shift;
2353 if (ref($line_or_ref) eq "HASH") {
2354 # pre-parsed (or generated by hand)
2355 return $line_or_ref;
2356 } else {
2357 return parse_difftree_raw_line($line_or_ref);
2361 # parse line of git-ls-tree output
2362 sub parse_ls_tree_line ($;%) {
2363 my $line = shift;
2364 my %opts = @_;
2365 my %res;
2367 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2368 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2370 $res{'mode'} = $1;
2371 $res{'type'} = $2;
2372 $res{'hash'} = $3;
2373 if ($opts{'-z'}) {
2374 $res{'name'} = $4;
2375 } else {
2376 $res{'name'} = unquote($4);
2379 return wantarray ? %res : \%res;
2382 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2383 sub parse_from_to_diffinfo {
2384 my ($diffinfo, $from, $to, @parents) = @_;
2386 if ($diffinfo->{'nparents'}) {
2387 # combined diff
2388 $from->{'file'} = [];
2389 $from->{'href'} = [];
2390 fill_from_file_info($diffinfo, @parents)
2391 unless exists $diffinfo->{'from_file'};
2392 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2393 $from->{'file'}[$i] =
2394 defined $diffinfo->{'from_file'}[$i] ?
2395 $diffinfo->{'from_file'}[$i] :
2396 $diffinfo->{'to_file'};
2397 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2398 $from->{'href'}[$i] = href(action=>"blob",
2399 hash_base=>$parents[$i],
2400 hash=>$diffinfo->{'from_id'}[$i],
2401 file_name=>$diffinfo->{'from_prefix'}.$from->{'file'}[$i]);
2402 } else {
2403 $from->{'href'}[$i] = undef;
2406 } else {
2407 # ordinary (not combined) diff
2408 $from->{'file'} = $diffinfo->{'from_file'};
2409 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2410 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2411 hash=>$diffinfo->{'from_id'},
2412 file_name=>$diffinfo->{'from_prefix'}.$from->{'file'});
2413 } else {
2414 delete $from->{'href'};
2418 $to->{'file'} = $diffinfo->{'to_file'};
2419 if (!is_deleted($diffinfo)) { # file exists in result
2420 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2421 hash=>$diffinfo->{'to_id'},
2422 file_name=>$diffinfo->{'to_prefix'}.$to->{'file'});
2423 } else {
2424 delete $to->{'href'};
2428 ## ......................................................................
2429 ## parse to array of hashes functions
2431 sub git_get_heads_list {
2432 my $limit = shift;
2433 my @headslist;
2435 open my $fd, '-|', git_cmd(), 'for-each-ref',
2436 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2437 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2438 'refs/heads'
2439 or return;
2440 while (my $line = <$fd>) {
2441 my %ref_item;
2443 chomp $line;
2444 my ($refinfo, $committerinfo) = split(/\0/, $line);
2445 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2446 my ($committer, $epoch, $tz) =
2447 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2448 $ref_item{'fullname'} = $name;
2449 $name =~ s!^refs/heads/!!;
2451 $ref_item{'name'} = $name;
2452 $ref_item{'id'} = $hash;
2453 $ref_item{'title'} = $title || '(no commit message)';
2454 $ref_item{'epoch'} = $epoch;
2455 if ($epoch) {
2456 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2457 } else {
2458 $ref_item{'age'} = "unknown";
2461 push @headslist, \%ref_item;
2463 close $fd;
2465 return wantarray ? @headslist : \@headslist;
2468 sub git_get_tags_list {
2469 my $limit = shift;
2470 my @tagslist;
2472 open my $fd, '-|', git_cmd(), 'for-each-ref',
2473 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2474 '--format=%(objectname) %(objecttype) %(refname) '.
2475 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2476 'refs/tags'
2477 or return;
2478 while (my $line = <$fd>) {
2479 my %ref_item;
2481 chomp $line;
2482 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2483 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2484 my ($creator, $epoch, $tz) =
2485 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2486 $ref_item{'fullname'} = $name;
2487 $name =~ s!^refs/tags/!!;
2489 $ref_item{'type'} = $type;
2490 $ref_item{'id'} = $id;
2491 $ref_item{'name'} = $name;
2492 if ($type eq "tag") {
2493 $ref_item{'subject'} = $title;
2494 $ref_item{'reftype'} = $reftype;
2495 $ref_item{'refid'} = $refid;
2496 } else {
2497 $ref_item{'reftype'} = $type;
2498 $ref_item{'refid'} = $id;
2501 if ($type eq "tag" || $type eq "commit") {
2502 $ref_item{'epoch'} = $epoch;
2503 if ($epoch) {
2504 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2505 } else {
2506 $ref_item{'age'} = "unknown";
2510 push @tagslist, \%ref_item;
2512 close $fd;
2514 return wantarray ? @tagslist : \@tagslist;
2517 ## ----------------------------------------------------------------------
2518 ## filesystem-related functions
2520 sub get_file_owner {
2521 my $path = shift;
2523 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2524 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2525 if (!defined $gcos) {
2526 return undef;
2528 my $owner = $gcos;
2529 $owner =~ s/[,;].*$//;
2530 return to_utf8($owner);
2533 ## ......................................................................
2534 ## mimetype related functions
2536 sub mimetype_guess_file {
2537 my $filename = shift;
2538 my $mimemap = shift;
2539 -r $mimemap or return undef;
2541 my %mimemap;
2542 open(MIME, $mimemap) or return undef;
2543 while (<MIME>) {
2544 next if m/^#/; # skip comments
2545 my ($mime, $exts) = split(/\t+/);
2546 if (defined $exts) {
2547 my @exts = split(/\s+/, $exts);
2548 foreach my $ext (@exts) {
2549 $mimemap{$ext} = $mime;
2553 close(MIME);
2555 $filename =~ /\.([^.]*)$/;
2556 return $mimemap{$1};
2559 sub mimetype_guess {
2560 my $filename = shift;
2561 my $mime;
2562 $filename =~ /\./ or return undef;
2564 if ($mimetypes_file) {
2565 my $file = $mimetypes_file;
2566 if ($file !~ m!^/!) { # if it is relative path
2567 # it is relative to project
2568 $file = "$projectroot/$project/$file";
2570 $mime = mimetype_guess_file($filename, $file);
2572 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2573 return $mime;
2576 sub blob_mimetype {
2577 my $fd = shift;
2578 my $filename = shift;
2580 if ($filename) {
2581 my $mime = mimetype_guess($filename);
2582 $mime and return $mime;
2585 # just in case
2586 return $default_blob_plain_mimetype unless $fd;
2588 if (-T $fd) {
2589 return 'text/plain';
2590 } elsif (! $filename) {
2591 return 'application/octet-stream';
2592 } elsif ($filename =~ m/\.png$/i) {
2593 return 'image/png';
2594 } elsif ($filename =~ m/\.gif$/i) {
2595 return 'image/gif';
2596 } elsif ($filename =~ m/\.jpe?g$/i) {
2597 return 'image/jpeg';
2598 } else {
2599 return 'application/octet-stream';
2603 sub blob_contenttype {
2604 my ($fd, $file_name, $type) = @_;
2606 $type ||= blob_mimetype($fd, $file_name);
2607 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2608 $type .= "; charset=$default_text_plain_charset";
2611 return $type;
2614 ## ======================================================================
2615 ## functions printing HTML: header, footer, error page
2617 sub git_header_html {
2618 my $status = shift || "200 OK";
2619 my $expires = shift;
2621 my $title = "$site_name";
2622 if (defined $project) {
2623 $title .= " - " . to_utf8($project);
2624 if (defined $action) {
2625 $title .= "/$action";
2626 if (defined $file_name) {
2627 $title .= " - " . esc_path($file_name);
2628 if ($action eq "tree" && $file_name !~ m|/$|) {
2629 $title .= "/";
2634 my $content_type;
2635 # require explicit support from the UA if we are to send the page as
2636 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2637 # we have to do this because MSIE sometimes globs '*/*', pretending to
2638 # support xhtml+xml but choking when it gets what it asked for.
2639 if (defined $cgi->http('HTTP_ACCEPT') &&
2640 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2641 $cgi->Accept('application/xhtml+xml') != 0) {
2642 $content_type = 'application/xhtml+xml';
2643 } else {
2644 $content_type = 'text/html';
2646 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2647 -status=> $status, -expires => $expires);
2648 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2649 print <<EOF;
2650 <?xml version="1.0" encoding="utf-8"?>
2651 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2652 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2653 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2654 <!-- git core binaries version $git_version -->
2655 <head>
2656 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2657 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2658 <meta name="robots" content="index, nofollow"/>
2659 <title>$title</title>
2660 <script type="text/javascript">/* <![CDATA[ */
2661 function fixBlameLinks() {
2662 var allLinks = document.getElementsByTagName("a");
2663 for (var i = 0; i < allLinks.length; i++) {
2664 var link = allLinks.item(i);
2665 if (link.className == 'blamelink')
2666 link.href = link.href.replace("a=blame", "a=blame_incremental");
2669 /* ]]> */</script>
2671 # print out each stylesheet that exist
2672 if (defined $stylesheet) {
2673 #provides backwards capability for those people who define style sheet in a config file
2674 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2675 } else {
2676 foreach my $stylesheet (@stylesheets) {
2677 next unless $stylesheet;
2678 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2681 if (defined $project) {
2682 my %href_params = get_feed_info();
2683 if (!exists $href_params{'-title'}) {
2684 $href_params{'-title'} = 'log';
2687 foreach my $format qw(RSS Atom) {
2688 my $type = lc($format);
2689 my %link_attr = (
2690 '-rel' => 'alternate',
2691 '-title' => "$project - $href_params{'-title'} - $format feed",
2692 '-type' => "application/$type+xml"
2695 $href_params{'action'} = $type;
2696 $link_attr{'-href'} = href(%href_params);
2697 print "<link ".
2698 "rel=\"$link_attr{'-rel'}\" ".
2699 "title=\"$link_attr{'-title'}\" ".
2700 "href=\"$link_attr{'-href'}\" ".
2701 "type=\"$link_attr{'-type'}\" ".
2702 "/>\n";
2704 $href_params{'extra_options'} = '--no-merges';
2705 $link_attr{'-href'} = href(%href_params);
2706 $link_attr{'-title'} .= ' (no merges)';
2707 print "<link ".
2708 "rel=\"$link_attr{'-rel'}\" ".
2709 "title=\"$link_attr{'-title'}\" ".
2710 "href=\"$link_attr{'-href'}\" ".
2711 "type=\"$link_attr{'-type'}\" ".
2712 "/>\n";
2715 } else {
2716 printf('<link rel="alternate" title="%s projects list" '.
2717 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2718 $site_name, href(project=>undef, action=>"project_index"));
2719 printf('<link rel="alternate" title="%s projects feeds" '.
2720 'href="%s" type="text/x-opml" />'."\n",
2721 $site_name, href(project=>undef, action=>"opml"));
2723 if (defined $favicon) {
2724 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2727 if (defined $gitwebjs) {
2728 print qq(<script src="$gitwebjs" type="text/javascript"></script>\n);
2731 print "</head>\n" .
2732 "<body onload=\"GitAddLinks(); fixBlameLinks();\">\n";
2734 if (-f $site_header) {
2735 open (my $fd, $site_header);
2736 print <$fd>;
2737 close $fd;
2740 print "<div class=\"page_header\">\n" .
2741 $cgi->a({-href => esc_url($logo_url),
2742 -title => $logo_label},
2743 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2744 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2745 if (defined $project) {
2746 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2747 if (defined $action) {
2748 print " / $action";
2750 print "\n";
2752 print "</div>\n";
2754 my ($have_search) = gitweb_check_feature('search');
2755 if (defined $project && $have_search) {
2756 if (!defined $searchtext) {
2757 $searchtext = "";
2759 my $search_hash;
2760 if (defined $hash_base) {
2761 $search_hash = $hash_base;
2762 } elsif (defined $hash) {
2763 $search_hash = $hash;
2764 } else {
2765 $search_hash = "HEAD";
2767 my $action = $my_uri;
2768 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2769 if ($use_pathinfo) {
2770 $action .= "/".esc_url($project);
2772 print $cgi->startform(-method => "get", -action => $action) .
2773 "<div class=\"search\">\n" .
2774 (!$use_pathinfo &&
2775 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2776 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2777 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2778 $cgi->popup_menu(-name => 'st', -default => 'commit',
2779 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2780 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2781 " search:\n",
2782 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2783 "<span title=\"Extended regular expression\">" .
2784 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2785 -checked => $search_use_regexp) .
2786 "</span>" .
2787 "</div>" .
2788 $cgi->end_form() . "\n";
2792 sub git_footer_html {
2793 my $feed_class = 'rss_logo';
2795 print "<div class=\"page_footer\">\n";
2796 if (defined $project) {
2797 my $descr = git_get_project_description($project);
2798 if (defined $descr) {
2799 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2802 my %href_params = get_feed_info();
2803 if (!%href_params) {
2804 $feed_class .= ' generic';
2806 $href_params{'-title'} ||= 'log';
2808 foreach my $format qw(RSS Atom) {
2809 $href_params{'action'} = lc($format);
2810 print $cgi->a({-href => href(%href_params),
2811 -title => "$href_params{'-title'} $format feed",
2812 -class => $feed_class}, $format)."\n";
2815 } else {
2816 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2817 -class => $feed_class}, "OPML") . " ";
2818 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2819 -class => $feed_class}, "TXT") . "\n";
2821 print "</div>\n"; # class="page_footer"
2823 if (-f $site_footer) {
2824 open (my $fd, $site_footer);
2825 print <$fd>;
2826 close $fd;
2829 print "</body>\n" .
2830 "</html>";
2833 # die_error(<http_status_code>, <error_message>)
2834 # Example: die_error(404, 'Hash not found')
2835 # By convention, use the following status codes (as defined in RFC 2616):
2836 # 400: Invalid or missing CGI parameters, or
2837 # requested object exists but has wrong type.
2838 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2839 # this server or project.
2840 # 404: Requested object/revision/project doesn't exist.
2841 # 500: The server isn't configured properly, or
2842 # an internal error occurred (e.g. failed assertions caused by bugs), or
2843 # an unknown error occurred (e.g. the git binary died unexpectedly).
2844 sub die_error {
2845 my $status = shift || 500;
2846 my $error = shift || "Internal server error";
2848 my %http_responses = (400 => '400 Bad Request',
2849 403 => '403 Forbidden',
2850 404 => '404 Not Found',
2851 500 => '500 Internal Server Error');
2852 git_header_html($http_responses{$status});
2853 print <<EOF;
2854 <div class="page_body">
2855 <br /><br />
2856 $status - $error
2857 <br />
2858 </div>
2860 git_footer_html();
2861 exit;
2864 ## ----------------------------------------------------------------------
2865 ## functions printing or outputting HTML: navigation
2867 sub git_print_page_nav {
2868 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2869 $extra = '' if !defined $extra; # pager or formats
2871 my @navs = qw(summary shortlog log commit commitdiff tree);
2872 if ($suppress) {
2873 @navs = grep { $_ ne $suppress } @navs;
2876 my %arg = map { $_ => {action=>$_} } @navs;
2877 if (defined $head) {
2878 for (qw(commit commitdiff)) {
2879 $arg{$_}{'hash'} = $head;
2881 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2882 for (qw(shortlog log)) {
2883 $arg{$_}{'hash'} = $head;
2888 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2889 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2891 my @actions = gitweb_check_feature('actions');
2892 while (@actions) {
2893 my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
2894 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2895 # munch munch
2896 $link =~ s#%n#$project#g;
2897 $link =~ s#%f#$git_dir#g;
2898 $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
2899 $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
2900 $arg{$label}{'_href'} = $link;
2903 print "<div class=\"page_nav\">\n" .
2904 (join " | ",
2905 map { $_ eq $current ?
2906 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
2907 } @navs);
2908 print "<br/>\n$extra<br/>\n" .
2909 "</div>\n";
2912 sub format_paging_nav {
2913 my ($action, $hash, $head, $page, $has_next_link) = @_;
2914 my $paging_nav;
2917 if ($hash ne $head || $page) {
2918 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2919 } else {
2920 $paging_nav .= "HEAD";
2923 if ($page > 0) {
2924 $paging_nav .= " &sdot; " .
2925 $cgi->a({-href => href(-replay=>1, page=>$page-1),
2926 -accesskey => "p", -title => "Alt-p"}, "prev");
2927 } else {
2928 $paging_nav .= " &sdot; prev";
2931 if ($has_next_link) {
2932 $paging_nav .= " &sdot; " .
2933 $cgi->a({-href => href(-replay=>1, page=>$page+1),
2934 -accesskey => "n", -title => "Alt-n"}, "next");
2935 } else {
2936 $paging_nav .= " &sdot; next";
2939 return $paging_nav;
2942 ## ......................................................................
2943 ## functions printing or outputting HTML: div
2945 sub git_print_header_div {
2946 my ($action, $title, $hash, $hash_base) = @_;
2947 my %args = ();
2949 $args{'action'} = $action;
2950 $args{'hash'} = $hash if $hash;
2951 $args{'hash_base'} = $hash_base if $hash_base;
2953 print "<div class=\"header\">\n" .
2954 $cgi->a({-href => href(%args), -class => "title"},
2955 $title ? $title : $action) .
2956 "\n</div>\n";
2959 #sub git_print_authorship (\%) {
2960 sub git_print_authorship {
2961 my $co = shift;
2963 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2964 print "<div class=\"author_date\">" .
2965 esc_html($co->{'author_name'}) .
2966 " [$ad{'rfc2822'}";
2967 if ($ad{'hour_local'} < 6) {
2968 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2969 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2970 } else {
2971 printf(" (%02d:%02d %s)",
2972 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2974 print "]</div>\n";
2977 sub git_print_page_path {
2978 my $name = shift;
2979 my $type = shift;
2980 my $hb = shift;
2983 print "<div class=\"page_path\">";
2984 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2985 -title => 'tree root'}, to_utf8("[$project]"));
2986 print " / ";
2987 if (defined $name) {
2988 my @dirname = split '/', $name;
2989 my $basename = pop @dirname;
2990 my $fullname = '';
2992 foreach my $dir (@dirname) {
2993 $fullname .= ($fullname ? '/' : '') . $dir;
2994 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2995 hash_base=>$hb),
2996 -title => $fullname}, esc_path($dir));
2997 print " / ";
2999 if (defined $type && $type eq 'blob') {
3000 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3001 hash_base=>$hb),
3002 -title => $name}, esc_path($basename));
3003 } elsif (defined $type && $type eq 'tree') {
3004 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3005 hash_base=>$hb),
3006 -title => $name}, esc_path($basename));
3007 print " / ";
3008 } else {
3009 print esc_path($basename);
3012 print "<br/></div>\n";
3015 # sub git_print_log (\@;%) {
3016 sub git_print_log ($;%) {
3017 my $log = shift;
3018 my %opts = @_;
3020 if ($opts{'-remove_title'}) {
3021 # remove title, i.e. first line of log
3022 shift @$log;
3024 # remove leading empty lines
3025 while (defined $log->[0] && $log->[0] eq "") {
3026 shift @$log;
3029 # print log
3030 my $signoff = 0;
3031 my $empty = 0;
3032 foreach my $line (@$log) {
3033 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3034 $signoff = 1;
3035 $empty = 0;
3036 if (! $opts{'-remove_signoff'}) {
3037 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3038 next;
3039 } else {
3040 # remove signoff lines
3041 next;
3043 } else {
3044 $signoff = 0;
3047 # print only one empty line
3048 # do not print empty line after signoff
3049 if ($line eq "") {
3050 next if ($empty || $signoff);
3051 $empty = 1;
3052 } else {
3053 $empty = 0;
3056 print format_log_line_html($line) . "<br/>\n";
3059 if ($opts{'-final_empty_line'}) {
3060 # end with single empty line
3061 print "<br/>\n" unless $empty;
3065 # return link target (what link points to)
3066 sub git_get_link_target {
3067 my $hash = shift;
3068 my $link_target;
3070 # read link
3071 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3072 or return;
3074 local $/;
3075 $link_target = <$fd>;
3077 close $fd
3078 or return;
3080 return $link_target;
3083 # given link target, and the directory (basedir) the link is in,
3084 # return target of link relative to top directory (top tree);
3085 # return undef if it is not possible (including absolute links).
3086 sub normalize_link_target {
3087 my ($link_target, $basedir, $hash_base) = @_;
3089 # we can normalize symlink target only if $hash_base is provided
3090 return unless $hash_base;
3092 # absolute symlinks (beginning with '/') cannot be normalized
3093 return if (substr($link_target, 0, 1) eq '/');
3095 # normalize link target to path from top (root) tree (dir)
3096 my $path;
3097 if ($basedir) {
3098 $path = $basedir . '/' . $link_target;
3099 } else {
3100 # we are in top (root) tree (dir)
3101 $path = $link_target;
3104 # remove //, /./, and /../
3105 my @path_parts;
3106 foreach my $part (split('/', $path)) {
3107 # discard '.' and ''
3108 next if (!$part || $part eq '.');
3109 # handle '..'
3110 if ($part eq '..') {
3111 if (@path_parts) {
3112 pop @path_parts;
3113 } else {
3114 # link leads outside repository (outside top dir)
3115 return;
3117 } else {
3118 push @path_parts, $part;
3121 $path = join('/', @path_parts);
3123 return $path;
3126 # print tree entry (row of git_tree), but without encompassing <tr> element
3127 sub git_print_tree_entry {
3128 my ($t, $basedir, $hash_base, $have_blame) = @_;
3130 my %base_key = ();
3131 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3133 # The format of a table row is: mode list link. Where mode is
3134 # the mode of the entry, list is the name of the entry, an href,
3135 # and link is the action links of the entry.
3137 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3138 if ($t->{'type'} eq "blob") {
3139 print "<td class=\"list\">" .
3140 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3141 file_name=>"$basedir$t->{'name'}", %base_key),
3142 -class => "list"}, esc_path($t->{'name'}));
3143 if (S_ISLNK(oct $t->{'mode'})) {
3144 my $link_target = git_get_link_target($t->{'hash'});
3145 if ($link_target) {
3146 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3147 if (defined $norm_target) {
3148 print " -> " .
3149 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3150 file_name=>$norm_target),
3151 -title => $norm_target}, esc_path($link_target));
3152 } else {
3153 print " -> " . esc_path($link_target);
3157 print "</td>\n";
3158 print "<td class=\"link\">";
3159 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3160 file_name=>"$basedir$t->{'name'}", %base_key)},
3161 "blob");
3162 if ($have_blame) {
3163 print " | " .
3164 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3165 file_name=>"$basedir$t->{'name'}", %base_key), -class => "blamelink"},
3166 "blame");
3168 if (defined $hash_base) {
3169 print " | " .
3170 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3171 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3172 "history");
3174 print " | " .
3175 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3176 file_name=>"$basedir$t->{'name'}")},
3177 "raw");
3178 print "</td>\n";
3180 } elsif ($t->{'type'} eq "tree") {
3181 print "<td class=\"list\">";
3182 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3183 file_name=>"$basedir$t->{'name'}", %base_key)},
3184 esc_path($t->{'name'}));
3185 print "</td>\n";
3186 print "<td class=\"link\">";
3187 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3188 file_name=>"$basedir$t->{'name'}", %base_key)},
3189 "tree");
3190 if (defined $hash_base) {
3191 print " | " .
3192 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3193 file_name=>"$basedir$t->{'name'}")},
3194 "history");
3196 print "</td>\n";
3197 } else {
3198 # unknown object: we can only present history for it
3199 # (this includes 'commit' object, i.e. submodule support)
3200 print "<td class=\"list\">" .
3201 esc_path($t->{'name'}) .
3202 "</td>\n";
3203 print "<td class=\"link\">";
3204 if (defined $hash_base) {
3205 print $cgi->a({-href => href(action=>"history",
3206 hash_base=>$hash_base,
3207 file_name=>"$basedir$t->{'name'}")},
3208 "history");
3210 print "</td>\n";
3214 ## ......................................................................
3215 ## functions printing large fragments of HTML
3217 # get pre-image filenames for merge (combined) diff
3218 sub fill_from_file_info {
3219 my ($diff, @parents) = @_;
3221 $diff->{'from_file'} = [ ];
3222 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3223 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3224 if ($diff->{'status'}[$i] eq 'R' ||
3225 $diff->{'status'}[$i] eq 'C') {
3226 $diff->{'from_file'}[$i] =
3227 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3231 return $diff;
3234 # is current raw difftree line of file deletion
3235 sub is_deleted {
3236 my $diffinfo = shift;
3238 return $diffinfo->{'to_id'} eq ('0' x 40);
3241 # does patch correspond to [previous] difftree raw line
3242 # $diffinfo - hashref of parsed raw diff format
3243 # $patchinfo - hashref of parsed patch diff format
3244 # (the same keys as in $diffinfo)
3245 sub is_patch_split {
3246 my ($diffinfo, $patchinfo) = @_;
3248 return defined $diffinfo && defined $patchinfo
3249 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3253 sub git_difftree_body {
3254 my ($difftree, $from_prefix, $to_prefix, $hash, @parents) = @_;
3255 my ($parent) = $parents[0];
3256 my ($have_blame) = gitweb_check_feature('blame');
3258 $from_prefix = !defined $from_prefix ? '' : $from_prefix.'/';
3259 $to_prefix = !defined $to_prefix ? '' : $to_prefix . '/';
3261 print "<div class=\"list_head\">\n";
3262 if ($#{$difftree} > 10) {
3263 print(($#{$difftree} + 1) . " files changed:\n");
3265 print "</div>\n";
3267 print "<table class=\"" .
3268 (@parents > 1 ? "combined " : "") .
3269 "diff_tree\">\n";
3271 # header only for combined diff in 'commitdiff' view
3272 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3273 if ($has_header) {
3274 # table header
3275 print "<thead><tr>\n" .
3276 "<th></th><th></th>\n"; # filename, patchN link
3277 for (my $i = 0; $i < @parents; $i++) {
3278 my $par = $parents[$i];
3279 print "<th>" .
3280 $cgi->a({-href => href(action=>"commitdiff",
3281 hash=>$hash, hash_parent=>$par),
3282 -title => 'commitdiff to parent number ' .
3283 ($i+1) . ': ' . substr($par,0,7)},
3284 $i+1) .
3285 "&nbsp;</th>\n";
3287 print "</tr></thead>\n<tbody>\n";
3290 my $alternate = 1;
3291 my $patchno = 0;
3292 foreach my $line (@{$difftree}) {
3293 my $diff = parsed_difftree_line($line);
3295 if ($alternate) {
3296 print "<tr class=\"dark\">\n";
3297 } else {
3298 print "<tr class=\"light\">\n";
3300 $alternate ^= 1;
3302 if (exists $diff->{'nparents'}) { # combined diff
3304 fill_from_file_info($diff, @parents)
3305 unless exists $diff->{'from_file'};
3307 if (!is_deleted($diff)) {
3308 # file exists in the result (child) commit
3309 print "<td>" .
3310 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3311 file_name=>$to_prefix.$diff->{'to_file'},
3312 hash_base=>$hash),
3313 -class => "list"}, esc_path($diff->{'to_file'})) .
3314 "</td>\n";
3315 } else {
3316 print "<td>" .
3317 esc_path($diff->{'to_file'}) .
3318 "</td>\n";
3321 if ($action eq 'commitdiff') {
3322 # link to patch
3323 $patchno++;
3324 print "<td class=\"link\">" .
3325 $cgi->a({-href => "#patch$patchno"}, "patch") .
3326 " | " .
3327 "</td>\n";
3330 my $has_history = 0;
3331 my $not_deleted = 0;
3332 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3333 my $hash_parent = $parents[$i];
3334 my $from_hash = $diff->{'from_id'}[$i];
3335 my $from_path = $diff->{'from_file'}[$i];
3336 my $status = $diff->{'status'}[$i];
3338 $has_history ||= ($status ne 'A');
3339 $not_deleted ||= ($status ne 'D');
3341 if ($status eq 'A') {
3342 print "<td class=\"link\" align=\"right\"> | </td>\n";
3343 } elsif ($status eq 'D') {
3344 print "<td class=\"link\">" .
3345 $cgi->a({-href => href(action=>"blob",
3346 hash_base=>$hash,
3347 hash=>$from_hash,
3348 file_name=>$from_prefix.$from_path)},
3349 "blob" . ($i+1)) .
3350 " | </td>\n";
3351 } else {
3352 if ($diff->{'to_id'} eq $from_hash) {
3353 print "<td class=\"link nochange\">";
3354 } else {
3355 print "<td class=\"link\">";
3357 print $cgi->a({-href => href(action=>"blobdiff",
3358 hash=>$diff->{'to_id'},
3359 hash_parent=>$from_hash,
3360 hash_base=>$hash,
3361 hash_parent_base=>$hash_parent,
3362 file_name=>$to_prefix.$diff->{'to_file'},
3363 file_parent=>$from_prefix.$from_path)},
3364 "diff" . ($i+1)) .
3365 " | </td>\n";
3369 print "<td class=\"link\">";
3370 if ($not_deleted) {
3371 print $cgi->a({-href => href(action=>"blob",
3372 hash=>$diff->{'to_id'},
3373 file_name=>$to_prefix.$diff->{'to_file'},
3374 hash_base=>$hash)},
3375 "blob");
3376 print " | " if ($has_history);
3378 if ($has_history) {
3379 print $cgi->a({-href => href(action=>"history",
3380 file_name=>$to_prefix.$diff->{'to_file'},
3381 hash_base=>$hash)},
3382 "history");
3384 print "</td>\n";
3386 print "</tr>\n";
3387 next; # instead of 'else' clause, to avoid extra indent
3389 # else ordinary diff
3391 my ($to_mode_oct, $to_mode_str, $to_file_type);
3392 my ($from_mode_oct, $from_mode_str, $from_file_type);
3393 if ($diff->{'to_mode'} ne ('0' x 6)) {
3394 $to_mode_oct = oct $diff->{'to_mode'};
3395 if (S_ISREG($to_mode_oct)) { # only for regular file
3396 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3398 $to_file_type = file_type($diff->{'to_mode'});
3400 if ($diff->{'from_mode'} ne ('0' x 6)) {
3401 $from_mode_oct = oct $diff->{'from_mode'};
3402 if (S_ISREG($to_mode_oct)) { # only for regular file
3403 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3405 $from_file_type = file_type($diff->{'from_mode'});
3408 if ($diff->{'status'} eq "A") { # created
3409 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3410 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3411 $mode_chng .= "]</span>";
3412 print "<td>";
3413 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3414 hash_base=>$hash, file_name=>$to_prefix.$diff->{'file'}),
3415 -class => "list"}, esc_path($diff->{'file'}));
3416 print "</td>\n";
3417 print "<td>$mode_chng</td>\n";
3418 print "<td class=\"link\">";
3419 if ($action eq 'commitdiff') {
3420 # link to patch
3421 $patchno++;
3422 print $cgi->a({-href => "#patch$patchno"}, "patch");
3423 print " | ";
3425 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3426 hash_base=>$hash, file_name=>$to_prefix.$diff->{'file'})},
3427 "blob");
3428 print "</td>\n";
3430 } elsif ($diff->{'status'} eq "D") { # deleted
3431 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3432 print "<td>";
3433 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3434 hash_base=>$parent, file_name=>$from_prefix.$diff->{'file'}),
3435 -class => "list"}, esc_path($diff->{'file'}));
3436 print "</td>\n";
3437 print "<td>$mode_chng</td>\n";
3438 print "<td class=\"link\">";
3439 if ($action eq 'commitdiff') {
3440 # link to patch
3441 $patchno++;
3442 print $cgi->a({-href => "#patch$patchno"}, "patch");
3443 print " | ";
3445 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3446 hash_base=>$parent, file_name=>$from_prefix.$diff->{'file'})},
3447 "blob") . " | ";
3448 if ($have_blame) {
3449 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3450 file_name=>$from_prefix.$diff->{'file'}), -class => "blamelink"},
3451 "blame") . " | ";
3453 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3454 file_name=>$from_prefix.$diff->{'file'})},
3455 "history");
3456 print "</td>\n";
3458 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3459 my $mode_chnge = "";
3460 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3461 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3462 if ($from_file_type ne $to_file_type) {
3463 $mode_chnge .= " from $from_file_type to $to_file_type";
3465 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3466 if ($from_mode_str && $to_mode_str) {
3467 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3468 } elsif ($to_mode_str) {
3469 $mode_chnge .= " mode: $to_mode_str";
3472 $mode_chnge .= "]</span>\n";
3474 print "<td>";
3475 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3476 hash_base=>$hash, file_name=>$to_prefix.$diff->{'file'}),
3477 -class => "list"}, esc_path($diff->{'file'}));
3478 print "</td>\n";
3479 print "<td>$mode_chnge</td>\n";
3480 print "<td class=\"link\">";
3481 if ($action eq 'commitdiff') {
3482 # link to patch
3483 $patchno++;
3484 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3485 " | ";
3486 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3487 # "commit" view and modified file (not onlu mode changed)
3488 print $cgi->a({-href => href(action=>"blobdiff",
3489 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3490 hash_base=>$hash, hash_parent_base=>$parent,
3491 file_name=>$to_prefix.$diff->{'file'},
3492 file_parent=>$from_prefix.$diff->{'file'})},
3493 "diff") .
3494 " | ";
3496 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3497 hash_base=>$hash, file_name=>$to_prefix.$diff->{'file'})},
3498 "blob") . " | ";
3499 if ($have_blame) {
3500 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3501 file_name=>$diff->{'file'}), -class => "blamelink"},
3502 "blame") . " | ";
3504 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3505 file_name=>$to_prefix.$diff->{'file'})},
3506 "history");
3507 print "</td>\n";
3509 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3510 my %status_name = ('R' => 'moved', 'C' => 'copied');
3511 my $nstatus = $status_name{$diff->{'status'}};
3512 my $mode_chng = "";
3513 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3514 # mode also for directories, so we cannot use $to_mode_str
3515 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3517 print "<td>" .
3518 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3519 hash=>$diff->{'to_id'}, file_name=>$to_prefix.$diff->{'to_file'}),
3520 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3521 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3522 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3523 hash=>$diff->{'from_id'}, file_name=>$from_prefix.$diff->{'from_file'}),
3524 -class => "list"}, esc_path($diff->{'from_file'})) .
3525 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3526 "<td class=\"link\">";
3527 if ($action eq 'commitdiff') {
3528 # link to patch
3529 $patchno++;
3530 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3531 " | ";
3532 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3533 # "commit" view and modified file (not only pure rename or copy)
3534 print $cgi->a({-href => href(action=>"blobdiff",
3535 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3536 hash_base=>$hash, hash_parent_base=>$parent,
3537 file_name=>$to_prefix.$diff->{'to_file'}, file_parent=>$from_prefix.$diff->{'from_file'})},
3538 "diff") .
3539 " | ";
3541 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3542 hash_base=>$parent, file_name=>$to_prefix.$diff->{'to_file'})},
3543 "blob") . " | ";
3544 if ($have_blame) {
3545 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3546 file_name=>$diff->{'to_file'}), -class => "blamelink"},
3547 "blame") . " | ";
3549 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3550 file_name=>$to_prefix.$diff->{'to_file'})},
3551 "history");
3552 print "</td>\n";
3554 } # we should not encounter Unmerged (U) or Unknown (X) status
3555 print "</tr>\n";
3557 print "</tbody>" if $has_header;
3558 print "</table>\n";
3561 sub git_patchset_body {
3562 my ($fd, $difftree, $from_prefix, $to_prefix, $hash, @hash_parents) = @_;
3563 my ($hash_parent) = $hash_parents[0];
3565 my $is_combined = (@hash_parents > 1);
3566 my $patch_idx = 0;
3567 my $patch_number = 0;
3568 my $patch_line;
3569 my $diffinfo;
3570 my $to_name;
3571 my (%from, %to);
3573 $from_prefix = !defined $from_prefix ? '' : $from_prefix.'/';
3574 $to_prefix = !defined $to_prefix ? '' : $to_prefix . '/';
3576 print "<div class=\"patchset\">\n";
3578 # skip to first patch
3579 while ($patch_line = <$fd>) {
3580 chomp $patch_line;
3582 last if ($patch_line =~ m/^diff /);
3585 PATCH:
3586 while ($patch_line) {
3588 # parse "git diff" header line
3589 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3590 # $1 is from_name, which we do not use
3591 $to_name = unquote($2);
3592 $to_name =~ s!^b/!!;
3593 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3594 # $1 is 'cc' or 'combined', which we do not use
3595 $to_name = unquote($2);
3596 } else {
3597 $to_name = undef;
3600 # check if current patch belong to current raw line
3601 # and parse raw git-diff line if needed
3602 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3603 # this is continuation of a split patch
3604 print "<div class=\"patch cont\">\n";
3605 $diffinfo->{'from_prefix'} = $from_prefix;
3606 $diffinfo->{'to_prefix'} = $to_prefix;
3607 } else {
3608 # advance raw git-diff output if needed
3609 $patch_idx++ if defined $diffinfo;
3611 # read and prepare patch information
3612 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3614 # compact combined diff output can have some patches skipped
3615 # find which patch (using pathname of result) we are at now;
3616 if ($is_combined) {
3617 while ($to_name ne $diffinfo->{'to_file'}) {
3618 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3619 format_diff_cc_simplified($diffinfo, @hash_parents) .
3620 "</div>\n"; # class="patch"
3622 $patch_idx++;
3623 $patch_number++;
3625 last if $patch_idx > $#$difftree;
3626 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3630 $diffinfo->{'from_prefix'} = $from_prefix;
3631 $diffinfo->{'to_prefix'} = $to_prefix;
3633 # modifies %from, %to hashes
3634 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3635 # this is first patch for raw difftree line with $patch_idx index
3636 # we index @$difftree array from 0, but number patches from 1
3637 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3640 # git diff header
3641 #assert($patch_line =~ m/^diff /) if DEBUG;
3642 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3643 $patch_number++;
3644 # print "git diff" header
3645 print format_git_diff_header_line($patch_line, $diffinfo,
3646 \%from, \%to);
3648 # print extended diff header
3649 print "<div class=\"diff extended_header\">\n";
3650 EXTENDED_HEADER:
3651 while ($patch_line = <$fd>) {
3652 chomp $patch_line;
3654 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3656 print format_extended_diff_header_line($patch_line, $diffinfo,
3657 \%from, \%to);
3659 print "</div>\n"; # class="diff extended_header"
3661 # from-file/to-file diff header
3662 if (! $patch_line) {
3663 print "</div>\n"; # class="patch"
3664 last PATCH;
3666 next PATCH if ($patch_line =~ m/^diff /);
3667 #assert($patch_line =~ m/^---/) if DEBUG;
3669 my $last_patch_line = $patch_line;
3670 $patch_line = <$fd>;
3671 chomp $patch_line;
3672 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3674 print format_diff_from_to_header($last_patch_line, $patch_line,
3675 $diffinfo, \%from, \%to,
3676 @hash_parents);
3678 # the patch itself
3679 LINE:
3680 while ($patch_line = <$fd>) {
3681 chomp $patch_line;
3683 next PATCH if ($patch_line =~ m/^diff /);
3685 print format_diff_line($patch_line, \%from, \%to);
3688 } continue {
3689 print "</div>\n"; # class="patch"
3692 # for compact combined (--cc) format, with chunk and patch simpliciaction
3693 # patchset might be empty, but there might be unprocessed raw lines
3694 for (++$patch_idx if $patch_number > 0;
3695 $patch_idx < @$difftree;
3696 ++$patch_idx) {
3697 # read and prepare patch information
3698 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3700 # generate anchor for "patch" links in difftree / whatchanged part
3701 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3702 format_diff_cc_simplified($diffinfo, @hash_parents) .
3703 "</div>\n"; # class="patch"
3705 $patch_number++;
3708 if ($patch_number == 0) {
3709 if (@hash_parents > 1) {
3710 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3711 } else {
3712 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3716 print "</div>\n"; # class="patchset"
3719 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3721 # fills project list info (age, description, owner, forks) for each
3722 # project in the list, removing invalid projects from returned list
3723 # NOTE: modifies $projlist, but does not remove entries from it
3724 sub fill_project_list_info {
3725 my ($projlist, $check_forks) = @_;
3726 my @projects;
3728 my $show_ctags = gitweb_check_feature('ctags');
3729 PROJECT:
3730 foreach my $pr (@$projlist) {
3731 my (@activity) = git_get_last_activity($pr->{'path'});
3732 unless (@activity) {
3733 next PROJECT;
3735 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3736 if (!defined $pr->{'descr'}) {
3737 my $descr = git_get_project_description($pr->{'path'}) || "";
3738 $descr = to_utf8($descr);
3739 $pr->{'descr_long'} = $descr;
3740 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3742 if (!defined $pr->{'owner'}) {
3743 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3745 if ($check_forks) {
3746 my $pname = $pr->{'path'};
3747 if (($pname =~ s/\.git$//) &&
3748 ($pname !~ /\/$/) &&
3749 (-d "$projectroot/$pname")) {
3750 $pr->{'forks'} = "-d $projectroot/$pname";
3751 } else {
3752 $pr->{'forks'} = 0;
3755 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3756 push @projects, $pr;
3759 return @projects;
3762 # print 'sort by' <th> element, generating 'sort by $name' replay link
3763 # if that order is not selected
3764 sub print_sort_th {
3765 my ($name, $order, $header) = @_;
3766 $header ||= ucfirst($name);
3768 if ($order eq $name) {
3769 print "<th>$header</th>\n";
3770 } else {
3771 print "<th>" .
3772 $cgi->a({-href => href(-replay=>1, order=>$name),
3773 -class => "header"}, $header) .
3774 "</th>\n";
3778 sub git_project_list_body {
3779 # actually uses global variable $project
3780 my ($projlist, $order, $from, $to, $extra, $no_header, $cache_lifetime) = @_;
3782 my ($check_forks) = gitweb_check_feature('forks');
3784 use File::stat;
3785 use POSIX qw(:fcntl_h);
3786 use Storable qw(store_fd retrieve);
3788 my $cache_file = "$cache_dir/$projlist_cache_name";
3790 my @projects;
3791 my $stale = 0;
3792 my $now = time();
3793 my $cache_mtime;
3794 if ($cache_lifetime && -f $cache_file) {
3795 $cache_mtime = stat($cache_file)->mtime;
3797 if (defined $cache_mtime && # caching is on and $cache_file exists
3798 $cache_mtime + $cache_lifetime*60 > $now &&
3799 (my $dump = retrieve($cache_file))) {
3800 $stale = $now - $cache_mtime;
3801 @projects = @$dump;
3802 } else {
3803 if (defined $cache_mtime) {
3804 # Postpone timeout by two minutes so that we get
3805 # enough time to do our job, or to be more exact
3806 # make cache expire after two minutes from now.
3807 my $time = $now - $cache_lifetime*60 + 120;
3808 utime $time, $time, $cache_file;
3810 @projects = fill_project_list_info($projlist, $check_forks);
3811 if ($cache_lifetime &&
3812 (-d $cache_dir || mkdir($cache_dir, 0700)) &&
3813 sysopen(my $fd, "$cache_file.lock", O_WRONLY|O_CREAT|O_EXCL, 0600)) {
3814 store_fd(\@projects, $fd);
3815 close $fd;
3816 rename "$cache_file.lock", $cache_file;
3820 $order ||= $default_projects_order;
3821 $from = 0 unless defined $from;
3822 $to = $#projects if (!defined $to || $#projects < $to);
3824 my %order_info = (
3825 project => { key => 'path', type => 'str' },
3826 descr => { key => 'descr_long', type => 'str' },
3827 owner => { key => 'owner', type => 'str' },
3828 age => { key => 'age', type => 'num' }
3830 my $oi = $order_info{$order};
3831 if ($oi->{'type'} eq 'str') {
3832 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3833 } else {
3834 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3837 if ($cache_lifetime && $stale > 0) {
3838 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n";
3841 my $show_ctags = gitweb_check_feature('ctags');
3842 if ($show_ctags) {
3843 my %ctags;
3844 foreach my $p (@projects) {
3845 foreach my $ct (keys %{$p->{'ctags'}}) {
3846 $ctags{$ct} += $p->{'ctags'}->{$ct};
3849 my $cloud = git_populate_project_tagcloud(\%ctags);
3850 print git_show_project_tagcloud($cloud, 64);
3853 print "<table class=\"project_list\">\n";
3854 unless ($no_header) {
3855 print "<tr>\n";
3856 if ($check_forks) {
3857 print "<th></th>\n";
3859 print_sort_th('project', $order, 'Project');
3860 print_sort_th('descr', $order, 'Description');
3861 print_sort_th('owner', $order, 'Owner');
3862 print_sort_th('age', $order, 'Last Change');
3863 print "<th></th>\n" . # for links
3864 "</tr>\n";
3866 my $alternate = 1;
3867 my $tagfilter = $cgi->param('by_tag');
3868 for (my $i = $from; $i <= $to; $i++) {
3869 my $pr = $projects[$i];
3871 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3872 # Weed out forks
3873 if ($check_forks) {
3874 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3875 $forkbase="^$forkbase" if $forkbase;
3876 next if not $tagfilter and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3879 if ($alternate) {
3880 print "<tr class=\"dark\">\n";
3881 } else {
3882 print "<tr class=\"light\">\n";
3884 $alternate ^= 1;
3885 if ($check_forks) {
3886 print "<td>";
3887 if ($pr->{'forks'}) {
3888 print "<!-- $pr->{'forks'} -->\n";
3889 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3891 print "</td>\n";
3893 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3894 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3895 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3896 -class => "list", -title => $pr->{'descr_long'}},
3897 esc_html($pr->{'descr'})) . "</td>\n" .
3898 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3899 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3900 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3901 "<td class=\"link\">" .
3902 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
3903 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3904 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3905 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3906 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3907 "</td>\n" .
3908 "</tr>\n";
3910 if (defined $extra) {
3911 print "<tr>\n";
3912 if ($check_forks) {
3913 print "<td></td>\n";
3915 print "<td colspan=\"5\">$extra</td>\n" .
3916 "</tr>\n";
3918 print "</table>\n";
3921 sub git_shortlog_body {
3922 # uses global variable $project
3923 my ($commitlist, $from, $to, $refs, $extra) = @_;
3925 $from = 0 unless defined $from;
3926 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3928 print "<table class=\"shortlog\">\n";
3929 my $alternate = 1;
3930 for (my $i = $from; $i <= $to; $i++) {
3931 my %co = %{$commitlist->[$i]};
3932 my $commit = $co{'id'};
3933 my $ref = format_ref_marker($refs, $commit);
3934 if ($alternate) {
3935 print "<tr class=\"dark\">\n";
3936 } else {
3937 print "<tr class=\"light\">\n";
3939 $alternate ^= 1;
3940 my $author = chop_and_escape_str($co{'author_name'}, 10);
3941 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3942 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3943 "<td><i>" . $author . "</i></td>\n" .
3944 "<td>";
3945 print format_subject_html($co{'title'}, $co{'title_short'},
3946 href(action=>"commit", hash=>$commit), $ref);
3947 print "</td>\n" .
3948 "<td class=\"link\">" .
3949 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3950 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3951 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3952 my $snapshot_links = format_snapshot_links($commit);
3953 if (defined $snapshot_links) {
3954 print " | " . $snapshot_links;
3956 print "</td>\n" .
3957 "</tr>\n";
3959 if (defined $extra) {
3960 print "<tr>\n" .
3961 "<td colspan=\"4\">$extra</td>\n" .
3962 "</tr>\n";
3964 print "</table>\n";
3967 sub git_history_body {
3968 # Warning: assumes constant type (blob or tree) during history
3969 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3971 $from = 0 unless defined $from;
3972 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3974 print "<table class=\"history\">\n";
3975 my $alternate = 1;
3976 for (my $i = $from; $i <= $to; $i++) {
3977 my %co = %{$commitlist->[$i]};
3978 if (!%co) {
3979 next;
3981 my $commit = $co{'id'};
3983 my $ref = format_ref_marker($refs, $commit);
3985 if ($alternate) {
3986 print "<tr class=\"dark\">\n";
3987 } else {
3988 print "<tr class=\"light\">\n";
3990 $alternate ^= 1;
3991 # shortlog uses chop_str($co{'author_name'}, 10)
3992 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3993 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3994 "<td><i>" . $author . "</i></td>\n" .
3995 "<td>";
3996 # originally git_history used chop_str($co{'title'}, 50)
3997 print format_subject_html($co{'title'}, $co{'title_short'},
3998 href(action=>"commit", hash=>$commit), $ref);
3999 print "</td>\n" .
4000 "<td class=\"link\">" .
4001 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4002 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4004 if ($ftype eq 'blob') {
4005 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4006 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4007 if (defined $blob_current && defined $blob_parent &&
4008 $blob_current ne $blob_parent) {
4009 print " | " .
4010 $cgi->a({-href => href(action=>"blobdiff",
4011 hash=>$blob_current, hash_parent=>$blob_parent,
4012 hash_base=>$hash_base, hash_parent_base=>$commit,
4013 file_name=>$file_name)},
4014 "diff to current");
4017 print "</td>\n" .
4018 "</tr>\n";
4020 if (defined $extra) {
4021 print "<tr>\n" .
4022 "<td colspan=\"4\">$extra</td>\n" .
4023 "</tr>\n";
4025 print "</table>\n";
4028 sub git_tags_body {
4029 # uses global variable $project
4030 my ($taglist, $from, $to, $extra) = @_;
4031 $from = 0 unless defined $from;
4032 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4034 print "<table class=\"tags\">\n";
4035 my $alternate = 1;
4036 for (my $i = $from; $i <= $to; $i++) {
4037 my $entry = $taglist->[$i];
4038 my %tag = %$entry;
4039 my $comment = $tag{'subject'};
4040 my $comment_short;
4041 if (defined $comment) {
4042 $comment_short = chop_str($comment, 30, 5);
4044 if ($alternate) {
4045 print "<tr class=\"dark\">\n";
4046 } else {
4047 print "<tr class=\"light\">\n";
4049 $alternate ^= 1;
4050 if (defined $tag{'age'}) {
4051 print "<td><i>$tag{'age'}</i></td>\n";
4052 } else {
4053 print "<td></td>\n";
4055 print "<td>" .
4056 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4057 -class => "list name"}, esc_html($tag{'name'})) .
4058 "</td>\n" .
4059 "<td>";
4060 if (defined $comment) {
4061 print format_subject_html($comment, $comment_short,
4062 href(action=>"tag", hash=>$tag{'id'}));
4064 print "</td>\n" .
4065 "<td class=\"selflink\">";
4066 if ($tag{'type'} eq "tag") {
4067 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4068 } else {
4069 print "&nbsp;";
4071 print "</td>\n" .
4072 "<td class=\"link\">" . " | " .
4073 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4074 if ($tag{'reftype'} eq "commit") {
4075 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4076 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4077 } elsif ($tag{'reftype'} eq "blob") {
4078 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4080 print "</td>\n" .
4081 "</tr>";
4083 if (defined $extra) {
4084 print "<tr>\n" .
4085 "<td colspan=\"5\">$extra</td>\n" .
4086 "</tr>\n";
4088 print "</table>\n";
4091 sub git_heads_body {
4092 # uses global variable $project
4093 my ($headlist, $head, $from, $to, $extra) = @_;
4094 $from = 0 unless defined $from;
4095 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4097 print "<table class=\"heads\">\n";
4098 my $alternate = 1;
4099 for (my $i = $from; $i <= $to; $i++) {
4100 my $entry = $headlist->[$i];
4101 my %ref = %$entry;
4102 my $curr = $ref{'id'} eq $head;
4103 if ($alternate) {
4104 print "<tr class=\"dark\">\n";
4105 } else {
4106 print "<tr class=\"light\">\n";
4108 $alternate ^= 1;
4109 print "<td><i>$ref{'age'}</i></td>\n" .
4110 ($curr ? "<td class=\"current_head\">" : "<td>") .
4111 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4112 -class => "list name"},esc_html($ref{'name'})) .
4113 "</td>\n" .
4114 "<td class=\"link\">" .
4115 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4116 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4117 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4118 "</td>\n" .
4119 "</tr>";
4121 if (defined $extra) {
4122 print "<tr>\n" .
4123 "<td colspan=\"3\">$extra</td>\n" .
4124 "</tr>\n";
4126 print "</table>\n";
4129 sub git_search_grep_body {
4130 my ($commitlist, $from, $to, $extra) = @_;
4131 $from = 0 unless defined $from;
4132 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4134 print "<table class=\"commit_search\">\n";
4135 my $alternate = 1;
4136 for (my $i = $from; $i <= $to; $i++) {
4137 my %co = %{$commitlist->[$i]};
4138 if (!%co) {
4139 next;
4141 my $commit = $co{'id'};
4142 if ($alternate) {
4143 print "<tr class=\"dark\">\n";
4144 } else {
4145 print "<tr class=\"light\">\n";
4147 $alternate ^= 1;
4148 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4149 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4150 "<td><i>" . $author . "</i></td>\n" .
4151 "<td>" .
4152 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4153 -class => "list subject"},
4154 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4155 my $comment = $co{'comment'};
4156 foreach my $line (@$comment) {
4157 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4158 my ($lead, $match, $trail) = ($1, $2, $3);
4159 $match = chop_str($match, 70, 5, 'center');
4160 my $contextlen = int((80 - length($match))/2);
4161 $contextlen = 30 if ($contextlen > 30);
4162 $lead = chop_str($lead, $contextlen, 10, 'left');
4163 $trail = chop_str($trail, $contextlen, 10, 'right');
4165 $lead = esc_html($lead);
4166 $match = esc_html($match);
4167 $trail = esc_html($trail);
4169 print "$lead<span class=\"match\">$match</span>$trail<br />";
4172 print "</td>\n" .
4173 "<td class=\"link\">" .
4174 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4175 " | " .
4176 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4177 " | " .
4178 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4179 print "</td>\n" .
4180 "</tr>\n";
4182 if (defined $extra) {
4183 print "<tr>\n" .
4184 "<td colspan=\"3\">$extra</td>\n" .
4185 "</tr>\n";
4187 print "</table>\n";
4190 ## ======================================================================
4191 ## ======================================================================
4192 ## actions
4194 sub git_project_list {
4195 my $order = $cgi->param('o');
4196 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4197 die_error(400, "Unknown order parameter");
4200 my @list = git_get_projects_list();
4201 if (!@list) {
4202 die_error(404, "No projects found");
4205 git_header_html();
4206 if (-f $home_text) {
4207 print "<div class=\"index_include\">\n";
4208 open (my $fd, $home_text);
4209 print <$fd>;
4210 close $fd;
4211 print "</div>\n";
4213 git_project_list_body(\@list, $order, undef, undef, undef, undef, $projlist_cache_lifetime);
4214 git_footer_html();
4217 sub git_forks {
4218 my $order = $cgi->param('o');
4219 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4220 die_error(400, "Unknown order parameter");
4223 my @list = git_get_projects_list($project);
4224 if (!@list) {
4225 die_error(404, "No forks found");
4228 git_header_html();
4229 git_print_page_nav('','');
4230 git_print_header_div('summary', "$project forks");
4231 git_project_list_body(\@list, $order);
4232 git_footer_html();
4235 sub git_project_index {
4236 my @projects = git_get_projects_list($project);
4238 print $cgi->header(
4239 -type => 'text/plain',
4240 -charset => 'utf-8',
4241 -content_disposition => 'inline; filename="index.aux"');
4243 foreach my $pr (@projects) {
4244 if (!exists $pr->{'owner'}) {
4245 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4248 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4249 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4250 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4251 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4252 $path =~ s/ /\+/g;
4253 $owner =~ s/ /\+/g;
4255 print "$path $owner\n";
4259 sub git_summary {
4260 my $descr = git_get_project_description($project) || "none";
4261 my %co = parse_commit("HEAD");
4262 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4263 my $head = $co{'id'};
4265 my $owner = git_get_project_owner($project);
4267 my $refs = git_get_references();
4268 # These get_*_list functions return one more to allow us to see if
4269 # there are more ...
4270 my @taglist = git_get_tags_list(16);
4271 my @headlist = git_get_heads_list(16);
4272 my @forklist;
4273 my ($check_forks) = gitweb_check_feature('forks');
4275 if ($check_forks) {
4276 @forklist = git_get_projects_list($project);
4279 git_header_html();
4280 git_print_page_nav('summary','', $head);
4282 print "<div class=\"title\">&nbsp;</div>\n";
4283 print "<table class=\"projects_list\">\n" .
4284 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4285 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4286 if (defined $cd{'rfc2822'}) {
4287 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4290 # use per project git URL list in $projectroot/$project/cloneurl
4291 # or make project git URL from git base URL and project name
4292 my $url_tag = "URL";
4293 my @url_list = git_get_project_url_list($project);
4294 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4295 foreach my $git_url (@url_list) {
4296 next unless $git_url;
4297 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4298 $url_tag = "";
4301 # Tag cloud
4302 my $show_ctags = (gitweb_check_feature('ctags'))[0];
4303 if ($show_ctags) {
4304 my $ctags = git_get_project_ctags($project);
4305 my $cloud = git_populate_project_tagcloud($ctags);
4306 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4307 print "</td>\n<td>" unless %$ctags;
4308 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4309 print "</td>\n<td>" if %$ctags;
4310 print git_show_project_tagcloud($cloud, 48);
4311 print "</td></tr>";
4314 print "</table>\n";
4316 if (-s "$projectroot/$project/README.html") {
4317 if (open my $fd, "$projectroot/$project/README.html") {
4318 print "<div class=\"title\">readme</div>\n" .
4319 "<div class=\"readme\">\n";
4320 print $_ while (<$fd>);
4321 print "\n</div>\n"; # class="readme"
4322 close $fd;
4326 # we need to request one more than 16 (0..15) to check if
4327 # those 16 are all
4328 my @commitlist = $head ? parse_commits($head, 17) : ();
4329 if (@commitlist) {
4330 git_print_header_div('shortlog');
4331 git_shortlog_body(\@commitlist, 0, 15, $refs,
4332 $#commitlist <= 15 ? undef :
4333 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4336 if (@taglist) {
4337 git_print_header_div('tags');
4338 git_tags_body(\@taglist, 0, 15,
4339 $#taglist <= 15 ? undef :
4340 $cgi->a({-href => href(action=>"tags")}, "..."));
4343 if (@headlist) {
4344 git_print_header_div('heads');
4345 git_heads_body(\@headlist, $head, 0, 15,
4346 $#headlist <= 15 ? undef :
4347 $cgi->a({-href => href(action=>"heads")}, "..."));
4350 if (@forklist) {
4351 git_print_header_div('forks');
4352 git_project_list_body(\@forklist, 'age', 0, 15,
4353 $#forklist <= 15 ? undef :
4354 $cgi->a({-href => href(action=>"forks")}, "..."),
4355 'no_header');
4358 git_footer_html();
4361 sub git_tag {
4362 my $head = git_get_head_hash($project);
4363 git_header_html();
4364 git_print_page_nav('','', $head,undef,$head);
4365 my %tag = parse_tag($hash);
4367 if (! %tag) {
4368 die_error(404, "Unknown tag object");
4371 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4372 print "<div class=\"title_text\">\n" .
4373 "<table class=\"object_header\">\n" .
4374 "<tr>\n" .
4375 "<td>object</td>\n" .
4376 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4377 $tag{'object'}) . "</td>\n" .
4378 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4379 $tag{'type'}) . "</td>\n" .
4380 "</tr>\n";
4381 if (defined($tag{'author'})) {
4382 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4383 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4384 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4385 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4386 "</td></tr>\n";
4388 print "</table>\n\n" .
4389 "</div>\n";
4390 print "<div class=\"page_body\">";
4391 my $comment = $tag{'comment'};
4392 foreach my $line (@$comment) {
4393 chomp $line;
4394 print esc_html($line, -nbsp=>1) . "<br/>\n";
4396 print "</div>\n";
4397 git_footer_html();
4400 sub git_blame_data {
4401 my $fd;
4402 my $ftype;
4404 my ($have_blame) = gitweb_check_feature('blame');
4405 if (!$have_blame) {
4406 die_error('403 Permission denied', "Permission denied");
4408 die_error('404 Not Found', "File name not defined") if (!$file_name);
4409 $hash_base ||= git_get_head_hash($project);
4410 die_error(undef, "Couldn't find base commit") unless ($hash_base);
4411 my %co = parse_commit($hash_base)
4412 or die_error(undef, "Reading commit failed");
4413 if (!defined $hash) {
4414 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4415 or die_error(undef, "Error looking up file");
4417 $ftype = git_get_type($hash);
4418 if ($ftype !~ "blob") {
4419 die_error("400 Bad Request", "Object is not a blob");
4421 open ($fd, "-|", git_cmd(), "blame", '--incremental', $hash_base, '--',
4422 $file_name)
4423 or die_error(undef, "Open git-blame --incremental failed");
4425 print $cgi->header(-type=>"text/plain", -charset => 'utf-8',
4426 -status=> "200 OK");
4428 while(<$fd>) {
4429 if (/^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/ or
4430 /^author-time |^author |^filename /) {
4431 print;
4435 close $fd or print "Reading blame data failed\n";
4438 sub git_blame_common {
4439 my ($type) = @_;
4441 my $fd;
4442 my $ftype;
4444 gitweb_check_feature('blame')
4445 or die_error(403, "Blame view not allowed");
4447 die_error(400, "No file name given") unless $file_name;
4448 $hash_base ||= git_get_head_hash($project);
4449 die_error(404, "Couldn't find base commit") unless ($hash_base);
4450 my %co = parse_commit($hash_base)
4451 or die_error(404, "Commit not found");
4452 if (!defined $hash) {
4453 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4454 or die_error(404, "Error looking up file");
4456 $ftype = git_get_type($hash);
4457 if ($ftype !~ "blob") {
4458 die_error(400, "Object is not a blob");
4460 if ($type eq 'incremental') {
4461 open ($fd, "-|", git_cmd(), 'cat-file', 'blob', $hash)
4462 or die_error(undef, "Open git-cat-file failed");
4463 } else {
4464 open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4465 $file_name, $hash_base)
4466 or die_error(500, "Open git-blame failed");
4468 git_header_html();
4469 my $formats_nav =
4470 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4471 "blob") .
4472 " | " .
4473 $cgi->a({-href => href(action=>"history", -replay=>1)},
4474 "history") .
4475 " | " .
4476 $cgi->a({-href => href(action=>"blame", file_name=>$file_name), -class => "blamelink"},
4477 "HEAD");
4478 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4479 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4480 git_print_page_path($file_name, $ftype, $hash_base);
4481 my @rev_color = (qw(light2 dark2));
4482 my $num_colors = scalar(@rev_color);
4483 my $current_color = 0;
4484 my $last_rev;
4485 print <<HTML;
4487 <div class="page_body">
4488 <table class="blame">
4489 <tr><th>Commit&nbsp;<a href="javascript:extra_blame_columns()" id="columns_expander">[+]</a></th>
4490 <th class="extra_column">Author</th>
4491 <th class="extra_column">Date</th>
4492 <th>Line</th>
4493 <th>Data</th></tr>
4494 HTML
4495 my %metainfo = ();
4496 my $linenr = 0;
4497 while (<$fd>) {
4498 chomp;
4499 if ($type eq 'incremental') {
4500 # Empty stage with just the file contents
4501 $linenr += 1;
4502 print "<tr id=\"l$linenr\" class=\"light2\">";
4503 print '<td class="sha1"><a href=""></a></td>';
4504 print "<td class=\"extra_column\"></td>";
4505 print "<td class=\"extra_column\"></td>";
4506 print "<td class=\"linenr\"><a class=\"linenr\" href=\"\">$linenr</a></td><td class=\"pre\">" . esc_html($_) . "</td>\n";
4507 print "</tr>\n";
4508 next;
4511 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4512 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4513 if (!exists $metainfo{$full_rev}) {
4514 $metainfo{$full_rev} = {};
4516 my $meta = $metainfo{$full_rev};
4517 while (<$fd>) {
4518 last if (s/^\t//);
4519 if (/^(\S+) (.*)$/) {
4520 $meta->{$1} = $2;
4523 my $data = $_;
4524 chomp $data;
4525 my $rev = substr($full_rev, 0, 8);
4526 my $author = $meta->{'author'};
4527 my %date = parse_date($meta->{'author-time'},
4528 $meta->{'author-tz'});
4529 my $date = $date{'iso-tz'};
4530 if ($group_size) {
4531 $current_color = ++$current_color % $num_colors;
4533 print "<tr class=\"$rev_color[$current_color]\">\n";
4534 if ($group_size) {
4535 my $rowspan = $group_size > 1 ? " rowspan=\"$group_size\"" : "";
4536 print "<td class=\"sha1\"";
4537 print " title=\"". esc_html($author) . ", $date\"";
4538 print "$rowspan>";
4539 print $cgi->a({-href => href(action=>"commit",
4540 hash=>$full_rev,
4541 file_name=>$file_name)},
4542 esc_html($rev));
4543 print "</td>\n";
4544 print "<td class=\"extra_column\" $rowspan>". esc_html($author) . "</td>";
4545 print "<td class=\"extra_column\" $rowspan>". $date . "</td>";
4547 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4548 or die_error(500, "Open git-rev-parse failed");
4549 my $parent_commit = <$dd>;
4550 close $dd;
4551 chomp($parent_commit);
4552 my $blamed = href(action => 'blame',
4553 file_name => $meta->{'filename'},
4554 hash_base => $parent_commit);
4555 print "<td class=\"linenr\">";
4556 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4557 -id => "l$lineno",
4558 -class => "linenr" },
4559 esc_html($lineno));
4560 print "</td>";
4561 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4562 print "</tr>\n";
4565 print "</table>\n";
4566 print "</div>";
4567 close $fd
4568 or print "Reading blob failed\n";
4570 if ($type eq 'incremental') {
4571 print "<script type=\"text/javascript\">\n";
4572 print "startBlame(\"" . href(action=>"blame_data", hash_base=>$hash_base, file_name=>$file_name) . "\", \"" .
4573 href(-partial_query=>1) . "\");\n";
4574 print "</script>\n";
4577 git_footer_html();
4580 sub git_blame_incremental {
4581 git_blame_common('incremental');
4584 sub git_blame {
4585 git_blame_common('oneshot');
4588 sub git_tags {
4589 my $head = git_get_head_hash($project);
4590 git_header_html();
4591 git_print_page_nav('','', $head,undef,$head);
4592 git_print_header_div('summary', $project);
4594 my @tagslist = git_get_tags_list();
4595 if (@tagslist) {
4596 git_tags_body(\@tagslist);
4598 git_footer_html();
4601 sub git_heads {
4602 my $head = git_get_head_hash($project);
4603 git_header_html();
4604 git_print_page_nav('','', $head,undef,$head);
4605 git_print_header_div('summary', $project);
4607 my @headslist = git_get_heads_list();
4608 if (@headslist) {
4609 git_heads_body(\@headslist, $head);
4611 git_footer_html();
4614 sub git_blob_plain {
4615 my $type = shift;
4616 my $expires;
4618 if (!defined $hash) {
4619 if (defined $file_name) {
4620 my $base = $hash_base || git_get_head_hash($project);
4621 $hash = git_get_hash_by_path($base, $file_name, "blob")
4622 or die_error(404, "Cannot find file");
4623 } else {
4624 die_error(400, "No file name defined");
4626 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4627 # blobs defined by non-textual hash id's can be cached
4628 $expires = "+1d";
4631 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4632 or die_error(500, "Open git-cat-file blob '$hash' failed");
4634 # content-type (can include charset)
4635 $type = blob_contenttype($fd, $file_name, $type);
4637 # "save as" filename, even when no $file_name is given
4638 my $save_as = "$hash";
4639 if (defined $file_name) {
4640 $save_as = $file_name;
4641 } elsif ($type =~ m/^text\//) {
4642 $save_as .= '.txt';
4645 print $cgi->header(
4646 -type => $type,
4647 -expires => $expires,
4648 -content_disposition => 'inline; filename="' . $save_as . '"');
4649 undef $/;
4650 binmode STDOUT, ':raw';
4651 print <$fd>;
4652 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4653 $/ = "\n";
4654 close $fd;
4657 sub git_blob {
4658 my $expires;
4660 if (!defined $hash) {
4661 if (defined $file_name) {
4662 my $base = $hash_base || git_get_head_hash($project);
4663 $hash = git_get_hash_by_path($base, $file_name, "blob")
4664 or die_error(404, "Cannot find file");
4665 } else {
4666 die_error(400, "No file name defined");
4668 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4669 # blobs defined by non-textual hash id's can be cached
4670 $expires = "+1d";
4673 my ($have_blame) = gitweb_check_feature('blame');
4674 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4675 or die_error(500, "Couldn't cat $file_name, $hash");
4676 my $mimetype = blob_mimetype($fd, $file_name);
4677 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4678 close $fd;
4679 return git_blob_plain($mimetype);
4681 # we can have blame only for text/* mimetype
4682 $have_blame &&= ($mimetype =~ m!^text/!);
4684 git_header_html(undef, $expires);
4685 my $formats_nav = '';
4686 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4687 if (defined $file_name) {
4688 if ($have_blame) {
4689 $formats_nav .=
4690 $cgi->a({-href => href(action=>"blame", -replay=>1,
4691 -class => "blamelink")},
4692 "blame") .
4693 " | ";
4695 $formats_nav .=
4696 $cgi->a({-href => href(action=>"history", -replay=>1)},
4697 "history") .
4698 " | " .
4699 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4700 "raw") .
4701 " | " .
4702 $cgi->a({-href => href(action=>"blob",
4703 hash_base=>"HEAD", file_name=>$file_name)},
4704 "HEAD");
4705 } else {
4706 $formats_nav .=
4707 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4708 "raw");
4710 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4711 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4712 } else {
4713 print "<div class=\"page_nav\">\n" .
4714 "<br/><br/></div>\n" .
4715 "<div class=\"title\">$hash</div>\n";
4717 git_print_page_path($file_name, "blob", $hash_base);
4718 print "<div class=\"page_body\">\n";
4719 if ($mimetype =~ m!^image/!) {
4720 print qq!<img type="$mimetype"!;
4721 if ($file_name) {
4722 print qq! alt="$file_name" title="$file_name"!;
4724 print qq! src="! .
4725 href(action=>"blob_plain", hash=>$hash,
4726 hash_base=>$hash_base, file_name=>$file_name) .
4727 qq!" />\n!;
4728 } else {
4729 my $nr;
4730 while (my $line = <$fd>) {
4731 chomp $line;
4732 $nr++;
4733 $line = untabify($line);
4734 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4735 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4738 close $fd
4739 or print "Reading blob failed.\n";
4740 print "</div>";
4741 git_footer_html();
4744 sub git_tree {
4745 if (!defined $hash_base) {
4746 $hash_base = "HEAD";
4748 if (!defined $hash) {
4749 if (defined $file_name) {
4750 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4751 } else {
4752 $hash = $hash_base;
4755 $/ = "\0";
4756 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4757 or die_error(500, "Open git-ls-tree failed");
4758 my @entries = map { chomp; $_ } <$fd>;
4759 close $fd or die_error(404, "Reading tree failed");
4760 $/ = "\n";
4762 my $refs = git_get_references();
4763 my $ref = format_ref_marker($refs, $hash_base);
4764 git_header_html();
4765 my $basedir = '';
4766 my ($have_blame) = gitweb_check_feature('blame');
4767 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4768 my @views_nav = ();
4769 if (defined $file_name) {
4770 push @views_nav,
4771 $cgi->a({-href => href(action=>"history", -replay=>1)},
4772 "history"),
4773 $cgi->a({-href => href(action=>"tree",
4774 hash_base=>"HEAD", file_name=>$file_name)},
4775 "HEAD"),
4777 my $snapshot_links = format_snapshot_links($hash);
4778 if (defined $snapshot_links) {
4779 # FIXME: Should be available when we have no hash base as well.
4780 push @views_nav, $snapshot_links;
4782 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4783 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4784 } else {
4785 undef $hash_base;
4786 print "<div class=\"page_nav\">\n";
4787 print "<br/><br/></div>\n";
4788 print "<div class=\"title\">$hash</div>\n";
4790 if (defined $file_name) {
4791 $basedir = $file_name;
4792 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4793 $basedir .= '/';
4796 git_print_page_path($file_name, 'tree', $hash_base);
4797 print "<div class=\"page_body\">\n";
4798 print "<table class=\"tree\">\n";
4799 my $alternate = 1;
4800 # '..' (top directory) link if possible
4801 if (defined $hash_base &&
4802 defined $file_name && $file_name =~ m![^/]+$!) {
4803 if ($alternate) {
4804 print "<tr class=\"dark\">\n";
4805 } else {
4806 print "<tr class=\"light\">\n";
4808 $alternate ^= 1;
4810 my $up = $file_name;
4811 $up =~ s!/?[^/]+$!!;
4812 undef $up unless $up;
4813 # based on git_print_tree_entry
4814 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4815 print '<td class="list">';
4816 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4817 file_name=>$up)},
4818 "..");
4819 print "</td>\n";
4820 print "<td class=\"link\"></td>\n";
4822 print "</tr>\n";
4824 foreach my $line (@entries) {
4825 my %t = parse_ls_tree_line($line, -z => 1);
4827 if ($alternate) {
4828 print "<tr class=\"dark\">\n";
4829 } else {
4830 print "<tr class=\"light\">\n";
4832 $alternate ^= 1;
4834 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4836 print "</tr>\n";
4838 print "</table>\n" .
4839 "</div>";
4840 git_footer_html();
4843 sub git_snapshot {
4844 my @supported_fmts = gitweb_check_feature('snapshot');
4845 @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4847 my $format = $cgi->param('sf');
4848 if (!@supported_fmts) {
4849 die_error(403, "Snapshots not allowed");
4851 # default to first supported snapshot format
4852 $format ||= $supported_fmts[0];
4853 if ($format !~ m/^[a-z0-9]+$/) {
4854 die_error(400, "Invalid snapshot format parameter");
4855 } elsif (!exists($known_snapshot_formats{$format})) {
4856 die_error(400, "Unknown snapshot format");
4857 } elsif (!grep($_ eq $format, @supported_fmts)) {
4858 die_error(403, "Unsupported snapshot format");
4861 if (!defined $hash) {
4862 $hash = git_get_head_hash($project);
4865 my $name = $project;
4866 $name =~ s,([^/])/*\.git$,$1,;
4867 $name = basename($name);
4868 my $filename = to_utf8($name);
4869 $name =~ s/\047/\047\\\047\047/g;
4870 my $cmd;
4871 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4872 $cmd = quote_command(
4873 git_cmd(), 'archive',
4874 "--format=$known_snapshot_formats{$format}{'format'}",
4875 "--prefix=$name/", $hash);
4876 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4877 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4880 print $cgi->header(
4881 -type => $known_snapshot_formats{$format}{'type'},
4882 -content_disposition => 'inline; filename="' . "$filename" . '"',
4883 -status => '200 OK');
4885 open my $fd, "-|", $cmd
4886 or die_error(500, "Execute git-archive failed");
4887 binmode STDOUT, ':raw';
4888 print <$fd>;
4889 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4890 close $fd;
4893 sub git_log {
4894 my $head = git_get_head_hash($project);
4895 if (!defined $hash) {
4896 $hash = $head;
4898 if (!defined $page) {
4899 $page = 0;
4901 my $refs = git_get_references();
4903 my @commitlist = parse_commits($hash, 101, (100 * $page));
4905 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4907 git_header_html();
4908 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4910 if (!@commitlist) {
4911 my %co = parse_commit($hash);
4913 git_print_header_div('summary', $project);
4914 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4916 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4917 for (my $i = 0; $i <= $to; $i++) {
4918 my %co = %{$commitlist[$i]};
4919 next if !%co;
4920 my $commit = $co{'id'};
4921 my $ref = format_ref_marker($refs, $commit);
4922 my %ad = parse_date($co{'author_epoch'});
4923 git_print_header_div('commit',
4924 "<span class=\"age\">$co{'age_string'}</span>" .
4925 esc_html($co{'title'}) . $ref,
4926 $commit);
4927 print "<div class=\"title_text\">\n" .
4928 "<div class=\"log_link\">\n" .
4929 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4930 " | " .
4931 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4932 " | " .
4933 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4934 "<br/>\n" .
4935 "</div>\n" .
4936 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4937 "</div>\n";
4939 print "<div class=\"log_body\">\n";
4940 git_print_log($co{'comment'}, -final_empty_line=> 1);
4941 print "</div>\n";
4943 if ($#commitlist >= 100) {
4944 print "<div class=\"page_nav\">\n";
4945 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4946 -accesskey => "n", -title => "Alt-n"}, "next");
4947 print "</div>\n";
4949 git_footer_html();
4952 sub git_commit {
4953 $hash ||= $hash_base || "HEAD";
4954 my %co = parse_commit($hash)
4955 or die_error(404, "Unknown commit object");
4956 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4957 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4959 my $parent = $co{'parent'};
4960 my $parents = $co{'parents'}; # listref
4962 # we need to prepare $formats_nav before any parameter munging
4963 my $formats_nav;
4964 if (!defined $parent) {
4965 # --root commitdiff
4966 $formats_nav .= '(initial)';
4967 } elsif (@$parents == 1) {
4968 # single parent commit
4969 $formats_nav .=
4970 '(parent: ' .
4971 $cgi->a({-href => href(action=>"commit",
4972 hash=>$parent)},
4973 esc_html(substr($parent, 0, 7))) .
4974 ')';
4975 } else {
4976 # merge commit
4977 $formats_nav .=
4978 '(merge: ' .
4979 join(' ', map {
4980 $cgi->a({-href => href(action=>"commit",
4981 hash=>$_)},
4982 esc_html(substr($_, 0, 7)));
4983 } @$parents ) .
4984 ')';
4987 if (!defined $parent) {
4988 $parent = "--root";
4990 my @difftree;
4991 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4992 @diff_opts,
4993 (@$parents <= 1 ? $parent : '-c'),
4994 $hash, "--"
4995 or die_error(500, "Open git-diff-tree failed");
4996 @difftree = map { chomp; $_ } <$fd>;
4997 close $fd or die_error(404, "Reading git-diff-tree failed");
4999 # non-textual hash id's can be cached
5000 my $expires;
5001 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5002 $expires = "+1d";
5004 my $refs = git_get_references();
5005 my $ref = format_ref_marker($refs, $co{'id'});
5007 git_header_html(undef, $expires);
5008 git_print_page_nav('commit', '',
5009 $hash, $co{'tree'}, $hash,
5010 $formats_nav);
5012 if (defined $co{'parent'}) {
5013 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5014 } else {
5015 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5017 print "<div class=\"title_text\">\n" .
5018 "<table class=\"object_header\">\n";
5019 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5020 "<tr>" .
5021 "<td></td><td> $ad{'rfc2822'}";
5022 if ($ad{'hour_local'} < 6) {
5023 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5024 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5025 } else {
5026 printf(" (%02d:%02d %s)",
5027 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5029 print "</td>" .
5030 "</tr>\n";
5031 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5032 print "<tr><td></td><td> $cd{'rfc2822'}" .
5033 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5034 "</td></tr>\n";
5035 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5036 print "<tr>" .
5037 "<td>tree</td>" .
5038 "<td class=\"sha1\">" .
5039 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5040 class => "list"}, $co{'tree'}) .
5041 "</td>" .
5042 "<td class=\"link\">" .
5043 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5044 "tree");
5045 my $snapshot_links = format_snapshot_links($hash);
5046 if (defined $snapshot_links) {
5047 print " | " . $snapshot_links;
5049 print "</td>" .
5050 "</tr>\n";
5052 foreach my $par (@$parents) {
5053 print "<tr>" .
5054 "<td>parent</td>" .
5055 "<td class=\"sha1\">" .
5056 $cgi->a({-href => href(action=>"commit", hash=>$par),
5057 class => "list"}, $par) .
5058 "</td>" .
5059 "<td class=\"link\">" .
5060 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5061 " | " .
5062 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5063 "</td>" .
5064 "</tr>\n";
5066 print "</table>".
5067 "</div>\n";
5069 print "<div class=\"page_body\">\n";
5070 git_print_log($co{'comment'});
5071 print "</div>\n";
5073 git_difftree_body(\@difftree, undef, undef, $hash, @$parents);
5075 git_footer_html();
5078 sub git_object {
5079 # object is defined by:
5080 # - hash or hash_base alone
5081 # - hash_base and file_name
5082 my $type;
5084 # - hash or hash_base alone
5085 if ($hash || ($hash_base && !defined $file_name)) {
5086 my $object_id = $hash || $hash_base;
5088 open my $fd, "-|", quote_command(
5089 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5090 or die_error(404, "Object does not exist");
5091 $type = <$fd>;
5092 chomp $type;
5093 close $fd
5094 or die_error(404, "Object does not exist");
5096 # - hash_base and file_name
5097 } elsif ($hash_base && defined $file_name) {
5098 $file_name =~ s,/+$,,;
5100 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5101 or die_error(404, "Base object does not exist");
5103 # here errors should not hapen
5104 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5105 or die_error(500, "Open git-ls-tree failed");
5106 my $line = <$fd>;
5107 close $fd;
5109 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5110 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5111 die_error(404, "File or directory for given base does not exist");
5113 $type = $2;
5114 $hash = $3;
5115 } else {
5116 die_error(400, "Not enough information to find object");
5119 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5120 hash=>$hash, hash_base=>$hash_base,
5121 file_name=>$file_name),
5122 -status => '302 Found');
5125 sub git_blobdiff {
5126 my $format = shift || 'html';
5128 my $fd;
5129 my @difftree;
5130 my %diffinfo;
5131 my $expires = '+1d';
5132 my ($from, $to);
5134 $file_parent ||= $file_name;
5136 # non-textual hash id's can be cached
5137 if (defined $hash && $hash !~ m/^[0-9a-fA-F]{40}$/) {
5138 $expires = undef;
5139 } elsif (defined $hash_parent && $hash_parent !~ m/^[0-9a-fA-F]{40}$/) {
5140 $expires = undef;
5141 } elsif (defined $hash_base && $hash_base !~ m/^[0-9a-fA-F]{40}$/) {
5142 $expires = undef;
5143 } elsif (defined $hash_parent_base && $hash_parent_base !~ m/^[0-9a-fA-F]{40}$/) {
5144 $expires = undef;
5147 # if hash parameter is missing, read it from the commit.
5148 if (defined $hash_base && defined $file_name && !defined $hash) {
5149 $hash = git_get_hash_by_path($hash_base, $file_name);
5152 if (defined $hash_parent_base && defined $file_parent && !defined $hash_parent) {
5153 $hash_parent = git_get_hash_by_path($hash_parent_base, $file_parent);
5156 if (!defined $hash || ! defined $hash_parent) {
5157 die_error(404, "Missing one of the blob diff parameters");
5160 if (defined $hash_base && defined $file_name) {
5161 $to = $hash_base . ':' . $file_name;
5162 } else {
5163 $to = $hash;
5166 if (defined $hash_parent_base && defined $file_parent) {
5167 $from = $hash_parent_base . ':' . $file_parent;
5168 } else {
5169 $from = $hash_parent;
5172 # fake git-diff-tree raw output
5173 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5174 $diffinfo{'from_id'} = $hash_parent;
5175 $diffinfo{'to_id'} = $hash;
5176 if (defined $file_name) {
5177 $diffinfo{'status'} = '2';
5178 $diffinfo{'from_file'} = $file_parent;
5179 $diffinfo{'to_file'} = $file_name;
5180 } else { # no filename given
5181 $diffinfo{'status'} = '2';
5182 $diffinfo{'from_file'} = $hash_parent;
5183 $diffinfo{'to_file'} = $hash;
5186 # open patch output
5187 open $fd, "-|", git_cmd(), "diff", @diff_opts, '-p', "--full-index",
5188 ($format eq 'html' ? "--raw" : ()), $from, $to, "--"
5189 or die_error(500, "Open git-diff failed");
5191 # header
5192 if ($format eq 'html') {
5193 my $formats_nav =
5194 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5195 "raw");
5196 git_header_html(undef, $expires);
5197 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5198 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5199 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5200 } else {
5201 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5202 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5204 if (defined $file_name) {
5205 git_print_page_path($file_name, "blob", $hash_base);
5206 } else {
5207 print "<div class=\"page_path\"></div>\n";
5210 } elsif ($format eq 'plain') {
5211 my $patch_file_name = $file_name || $hash;
5212 print $cgi->header(
5213 -type => 'text/plain',
5214 -charset => 'utf-8',
5215 -expires => $expires,
5216 -content_disposition => 'inline; filename="' . "$patch_file_name" . '.patch"');
5218 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5220 } else {
5221 die_error(400, "Unknown blobdiff format");
5224 # patch
5225 if ($format eq 'html') {
5226 print "<div class=\"page_body\">\n";
5228 git_patchset_body($fd, [ \%diffinfo ], undef, undef, $hash_base, $hash_parent_base);
5229 close $fd;
5231 print "</div>\n"; # class="page_body"
5232 git_footer_html();
5234 } else {
5235 while (my $line = <$fd>) {
5236 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5237 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5239 print $line;
5241 last if $line =~ m!^\+\+\+!;
5243 local $/ = undef;
5244 print <$fd>;
5245 close $fd;
5249 sub git_blobdiff_plain {
5250 git_blobdiff('plain');
5253 sub git_commitdiff {
5254 my $format = shift || 'html';
5255 $hash ||= $hash_base || "HEAD";
5256 my %co = parse_commit($hash)
5257 or die_error(404, "Unknown commit object");
5259 # choose format for commitdiff for merge
5260 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5261 $hash_parent = '--cc';
5263 # we need to prepare $formats_nav before almost any parameter munging
5264 my $formats_nav;
5265 if ($format eq 'html') {
5266 $formats_nav =
5267 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5268 "raw");
5270 if (defined $hash_parent &&
5271 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5272 # commitdiff with two commits given
5273 my $hash_parent_short = $hash_parent;
5274 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5275 $hash_parent_short = substr($hash_parent, 0, 7);
5277 $formats_nav .=
5278 ' (from';
5279 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5280 if ($co{'parents'}[$i] eq $hash_parent) {
5281 $formats_nav .= ' parent ' . ($i+1);
5282 last;
5285 $formats_nav .= ': ' .
5286 $cgi->a({-href => href(action=>"commitdiff",
5287 hash=>$hash_parent)},
5288 esc_html($hash_parent_short)) .
5289 ')';
5290 } elsif (!$co{'parent'}) {
5291 # --root commitdiff
5292 $formats_nav .= ' (initial)';
5293 } elsif (scalar @{$co{'parents'}} == 1) {
5294 # single parent commit
5295 $formats_nav .=
5296 ' (parent: ' .
5297 $cgi->a({-href => href(action=>"commitdiff",
5298 hash=>$co{'parent'})},
5299 esc_html(substr($co{'parent'}, 0, 7))) .
5300 ')';
5301 } else {
5302 # merge commit
5303 if ($hash_parent eq '--cc') {
5304 $formats_nav .= ' | ' .
5305 $cgi->a({-href => href(action=>"commitdiff",
5306 hash=>$hash, hash_parent=>'-c')},
5307 'combined');
5308 } else { # $hash_parent eq '-c'
5309 $formats_nav .= ' | ' .
5310 $cgi->a({-href => href(action=>"commitdiff",
5311 hash=>$hash, hash_parent=>'--cc')},
5312 'compact');
5314 $formats_nav .=
5315 ' (merge: ' .
5316 join(' ', map {
5317 $cgi->a({-href => href(action=>"commitdiff",
5318 hash=>$_)},
5319 esc_html(substr($_, 0, 7)));
5320 } @{$co{'parents'}} ) .
5321 ')';
5325 my $hash_parent_param = $hash_parent;
5326 if (!defined $hash_parent_param) {
5327 # --cc for multiple parents, --root for parentless
5328 $hash_parent_param =
5329 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5332 # read commitdiff
5333 my $fd;
5334 my @difftree;
5335 if ($format eq 'html') {
5336 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5337 "--no-commit-id", "--patch-with-raw", "--full-index",
5338 $hash_parent_param, $hash, "--"
5339 or die_error(500, "Open git-diff-tree failed");
5341 while (my $line = <$fd>) {
5342 chomp $line;
5343 # empty line ends raw part of diff-tree output
5344 last unless $line;
5345 push @difftree, scalar parse_difftree_raw_line($line);
5348 } elsif ($format eq 'plain') {
5349 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5350 '-p', $hash_parent_param, $hash, "--"
5351 or die_error(500, "Open git-diff-tree failed");
5353 } else {
5354 die_error(400, "Unknown commitdiff format");
5357 # non-textual hash id's can be cached
5358 my $expires;
5359 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5360 $expires = "+1d";
5363 # write commit message
5364 if ($format eq 'html') {
5365 my $refs = git_get_references();
5366 my $ref = format_ref_marker($refs, $co{'id'});
5368 git_header_html(undef, $expires);
5369 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5370 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5371 git_print_authorship(\%co);
5372 print "<div class=\"page_body\">\n";
5373 if (@{$co{'comment'}} > 1) {
5374 print "<div class=\"log\">\n";
5375 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5376 print "</div>\n"; # class="log"
5379 } elsif ($format eq 'plain') {
5380 my $refs = git_get_references("tags");
5381 my $tagname = git_get_rev_name_tags($hash);
5382 my $filename = basename($project) . "-$hash.patch";
5384 print $cgi->header(
5385 -type => 'text/plain',
5386 -charset => 'utf-8',
5387 -expires => $expires,
5388 -content_disposition => 'inline; filename="' . "$filename" . '"');
5389 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5390 print "From: " . to_utf8($co{'author'}) . "\n";
5391 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5392 print "Subject: " . to_utf8($co{'title'}) . "\n";
5394 print "X-Git-Tag: $tagname\n" if $tagname;
5395 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5397 foreach my $line (@{$co{'comment'}}) {
5398 print to_utf8($line) . "\n";
5400 print "---\n\n";
5403 # write patch
5404 if ($format eq 'html') {
5405 my $use_parents = !defined $hash_parent ||
5406 $hash_parent eq '-c' || $hash_parent eq '--cc';
5407 git_difftree_body(\@difftree, undef, undef, $hash,
5408 $use_parents ? @{$co{'parents'}} : $hash_parent);
5409 print "<br/>\n";
5411 git_patchset_body($fd, \@difftree, undef, undef, $hash,
5412 $use_parents ? @{$co{'parents'}} : $hash_parent);
5413 close $fd;
5414 print "</div>\n"; # class="page_body"
5415 git_footer_html();
5417 } elsif ($format eq 'plain') {
5418 local $/ = undef;
5419 print <$fd>;
5420 close $fd
5421 or print "Reading git-diff-tree failed\n";
5425 sub git_commitdiff_plain {
5426 git_commitdiff('plain');
5429 sub git_history {
5430 if (!defined $hash_base) {
5431 $hash_base = git_get_head_hash($project);
5433 if (!defined $page) {
5434 $page = 0;
5436 my $ftype;
5437 my %co = parse_commit($hash_base)
5438 or die_error(404, "Unknown commit object");
5440 my $refs = git_get_references();
5441 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5443 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5444 $file_name, "--full-history")
5445 or die_error(404, "No such file or directory on given branch");
5447 if (!defined $hash && defined $file_name) {
5448 # some commits could have deleted file in question,
5449 # and not have it in tree, but one of them has to have it
5450 for (my $i = 0; $i <= @commitlist; $i++) {
5451 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5452 last if defined $hash;
5455 if (defined $hash) {
5456 $ftype = git_get_type($hash);
5458 if (!defined $ftype) {
5459 die_error(500, "Unknown type of object");
5462 my $paging_nav = '';
5463 if ($page > 0) {
5464 $paging_nav .=
5465 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5466 file_name=>$file_name)},
5467 "first");
5468 $paging_nav .= " &sdot; " .
5469 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5470 -accesskey => "p", -title => "Alt-p"}, "prev");
5471 } else {
5472 $paging_nav .= "first";
5473 $paging_nav .= " &sdot; prev";
5475 my $next_link = '';
5476 if ($#commitlist >= 100) {
5477 $next_link =
5478 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5479 -accesskey => "n", -title => "Alt-n"}, "next");
5480 $paging_nav .= " &sdot; $next_link";
5481 } else {
5482 $paging_nav .= " &sdot; next";
5485 git_header_html();
5486 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5487 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5488 git_print_page_path($file_name, $ftype, $hash_base);
5490 git_history_body(\@commitlist, 0, 99,
5491 $refs, $hash_base, $ftype, $next_link);
5493 git_footer_html();
5496 sub git_search {
5497 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5498 if (!defined $searchtext) {
5499 die_error(400, "Text field is empty");
5501 if (!defined $hash) {
5502 $hash = git_get_head_hash($project);
5504 my %co = parse_commit($hash);
5505 if (!%co) {
5506 die_error(404, "Unknown commit object");
5508 if (!defined $page) {
5509 $page = 0;
5512 $searchtype ||= 'commit';
5513 if ($searchtype eq 'pickaxe') {
5514 # pickaxe may take all resources of your box and run for several minutes
5515 # with every query - so decide by yourself how public you make this feature
5516 gitweb_check_feature('pickaxe')
5517 or die_error(403, "Pickaxe is disabled");
5519 if ($searchtype eq 'grep') {
5520 gitweb_check_feature('grep')
5521 or die_error(403, "Grep is disabled");
5524 git_header_html();
5526 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5527 my $greptype;
5528 if ($searchtype eq 'commit') {
5529 $greptype = "--grep=";
5530 } elsif ($searchtype eq 'author') {
5531 $greptype = "--author=";
5532 } elsif ($searchtype eq 'committer') {
5533 $greptype = "--committer=";
5535 $greptype .= $searchtext;
5536 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5537 $greptype, '--regexp-ignore-case',
5538 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5540 my $paging_nav = '';
5541 if ($page > 0) {
5542 $paging_nav .=
5543 $cgi->a({-href => href(action=>"search", hash=>$hash,
5544 searchtext=>$searchtext,
5545 searchtype=>$searchtype)},
5546 "first");
5547 $paging_nav .= " &sdot; " .
5548 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5549 -accesskey => "p", -title => "Alt-p"}, "prev");
5550 } else {
5551 $paging_nav .= "first";
5552 $paging_nav .= " &sdot; prev";
5554 my $next_link = '';
5555 if ($#commitlist >= 100) {
5556 $next_link =
5557 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5558 -accesskey => "n", -title => "Alt-n"}, "next");
5559 $paging_nav .= " &sdot; $next_link";
5560 } else {
5561 $paging_nav .= " &sdot; next";
5564 if ($#commitlist >= 100) {
5567 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5568 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5569 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5572 if ($searchtype eq 'pickaxe') {
5573 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5574 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5576 print "<table class=\"pickaxe search\">\n";
5577 my $alternate = 1;
5578 $/ = "\n";
5579 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5580 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5581 ($search_use_regexp ? '--pickaxe-regex' : ());
5582 undef %co;
5583 my @files;
5584 while (my $line = <$fd>) {
5585 chomp $line;
5586 next unless $line;
5588 my %set = parse_difftree_raw_line($line);
5589 if (defined $set{'commit'}) {
5590 # finish previous commit
5591 if (%co) {
5592 print "</td>\n" .
5593 "<td class=\"link\">" .
5594 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5595 " | " .
5596 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5597 print "</td>\n" .
5598 "</tr>\n";
5601 if ($alternate) {
5602 print "<tr class=\"dark\">\n";
5603 } else {
5604 print "<tr class=\"light\">\n";
5606 $alternate ^= 1;
5607 %co = parse_commit($set{'commit'});
5608 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5609 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5610 "<td><i>$author</i></td>\n" .
5611 "<td>" .
5612 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5613 -class => "list subject"},
5614 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5615 } elsif (defined $set{'to_id'}) {
5616 next if ($set{'to_id'} =~ m/^0{40}$/);
5618 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5619 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5620 -class => "list"},
5621 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5622 "<br/>\n";
5625 close $fd;
5627 # finish last commit (warning: repetition!)
5628 if (%co) {
5629 print "</td>\n" .
5630 "<td class=\"link\">" .
5631 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5632 " | " .
5633 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5634 print "</td>\n" .
5635 "</tr>\n";
5638 print "</table>\n";
5641 if ($searchtype eq 'grep') {
5642 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5643 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5645 print "<table class=\"grep_search\">\n";
5646 my $alternate = 1;
5647 my $matches = 0;
5648 $/ = "\n";
5649 open my $fd, "-|", git_cmd(), 'grep', '-n',
5650 $search_use_regexp ? ('-E', '-i') : '-F',
5651 $searchtext, $co{'tree'};
5652 my $lastfile = '';
5653 while (my $line = <$fd>) {
5654 chomp $line;
5655 my ($file, $lno, $ltext, $binary);
5656 last if ($matches++ > 1000);
5657 if ($line =~ /^Binary file (.+) matches$/) {
5658 $file = $1;
5659 $binary = 1;
5660 } else {
5661 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5663 if ($file ne $lastfile) {
5664 $lastfile and print "</td></tr>\n";
5665 if ($alternate++) {
5666 print "<tr class=\"dark\">\n";
5667 } else {
5668 print "<tr class=\"light\">\n";
5670 print "<td class=\"list\">".
5671 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5672 file_name=>"$file"),
5673 -class => "list"}, esc_path($file));
5674 print "</td><td>\n";
5675 $lastfile = $file;
5677 if ($binary) {
5678 print "<div class=\"binary\">Binary file</div>\n";
5679 } else {
5680 $ltext = untabify($ltext);
5681 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5682 $ltext = esc_html($1, -nbsp=>1);
5683 $ltext .= '<span class="match">';
5684 $ltext .= esc_html($2, -nbsp=>1);
5685 $ltext .= '</span>';
5686 $ltext .= esc_html($3, -nbsp=>1);
5687 } else {
5688 $ltext = esc_html($ltext, -nbsp=>1);
5690 print "<div class=\"pre\">" .
5691 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5692 file_name=>"$file").'#l'.$lno,
5693 -class => "linenr"}, sprintf('%4i', $lno))
5694 . ' ' . $ltext . "</div>\n";
5697 if ($lastfile) {
5698 print "</td></tr>\n";
5699 if ($matches > 1000) {
5700 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5702 } else {
5703 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5705 close $fd;
5707 print "</table>\n";
5709 git_footer_html();
5712 sub git_search_help {
5713 git_header_html();
5714 git_print_page_nav('','', $hash,$hash,$hash);
5715 print <<EOT;
5716 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5717 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5718 the pattern entered is recognized as the POSIX extended
5719 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5720 insensitive).</p>
5721 <dl>
5722 <dt><b>commit</b></dt>
5723 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5725 my ($have_grep) = gitweb_check_feature('grep');
5726 if ($have_grep) {
5727 print <<EOT;
5728 <dt><b>grep</b></dt>
5729 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5730 a different one) are searched for the given pattern. On large trees, this search can take
5731 a while and put some strain on the server, so please use it with some consideration. Note that
5732 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5733 case-sensitive.</dd>
5736 print <<EOT;
5737 <dt><b>author</b></dt>
5738 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5739 <dt><b>committer</b></dt>
5740 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5742 my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5743 if ($have_pickaxe) {
5744 print <<EOT;
5745 <dt><b>pickaxe</b></dt>
5746 <dd>All commits that caused the string to appear or disappear from any file (changes that
5747 added, removed or "modified" the string) will be listed. This search can take a while and
5748 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5749 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5752 print "</dl>\n";
5753 git_footer_html();
5756 sub git_shortlog {
5757 my $head = git_get_head_hash($project);
5758 if (!defined $hash) {
5759 $hash = $head;
5761 if (!defined $page) {
5762 $page = 0;
5764 my $refs = git_get_references();
5766 my @commitlist = parse_commits($hash, 101, (100 * $page));
5768 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
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");
5776 git_header_html();
5777 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5778 git_print_header_div('summary', $project);
5780 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5782 git_footer_html();
5785 ## ......................................................................
5786 ## feeds (RSS, Atom; OPML)
5788 sub git_feed {
5789 my $format = shift || 'atom';
5790 my ($have_blame) = gitweb_check_feature('blame');
5792 # Atom: http://www.atomenabled.org/developers/syndication/
5793 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5794 if ($format ne 'rss' && $format ne 'atom') {
5795 die_error(400, "Unknown web feed format");
5798 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5799 my $head = $hash || 'HEAD';
5800 my @commitlist = parse_commits($head, 150, 0, $file_name);
5802 my %latest_commit;
5803 my %latest_date;
5804 my $content_type = "application/$format+xml";
5805 if (defined $cgi->http('HTTP_ACCEPT') &&
5806 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5807 # browser (feed reader) prefers text/xml
5808 $content_type = 'text/xml';
5810 if (defined($commitlist[0])) {
5811 %latest_commit = %{$commitlist[0]};
5812 %latest_date = parse_date($latest_commit{'author_epoch'});
5813 print $cgi->header(
5814 -type => $content_type,
5815 -charset => 'utf-8',
5816 -last_modified => $latest_date{'rfc2822'});
5817 } else {
5818 print $cgi->header(
5819 -type => $content_type,
5820 -charset => 'utf-8');
5823 # Optimization: skip generating the body if client asks only
5824 # for Last-Modified date.
5825 return if ($cgi->request_method() eq 'HEAD');
5827 # header variables
5828 my $title = "$site_name - $project/$action";
5829 my $feed_type = 'log';
5830 if (defined $hash) {
5831 $title .= " - '$hash'";
5832 $feed_type = 'branch log';
5833 if (defined $file_name) {
5834 $title .= " :: $file_name";
5835 $feed_type = 'history';
5837 } elsif (defined $file_name) {
5838 $title .= " - $file_name";
5839 $feed_type = 'history';
5841 $title .= " $feed_type";
5842 my $descr = git_get_project_description($project);
5843 if (defined $descr) {
5844 $descr = esc_html($descr);
5845 } else {
5846 $descr = "$project " .
5847 ($format eq 'rss' ? 'RSS' : 'Atom') .
5848 " feed";
5850 my $owner = git_get_project_owner($project);
5851 $owner = esc_html($owner);
5853 #header
5854 my $alt_url;
5855 if (defined $file_name) {
5856 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5857 } elsif (defined $hash) {
5858 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5859 } else {
5860 $alt_url = href(-full=>1, action=>"summary");
5862 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5863 if ($format eq 'rss') {
5864 print <<XML;
5865 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5866 <channel>
5868 print "<title>$title</title>\n" .
5869 "<link>$alt_url</link>\n" .
5870 "<description>$descr</description>\n" .
5871 "<language>en</language>\n";
5872 } elsif ($format eq 'atom') {
5873 print <<XML;
5874 <feed xmlns="http://www.w3.org/2005/Atom">
5876 print "<title>$title</title>\n" .
5877 "<subtitle>$descr</subtitle>\n" .
5878 '<link rel="alternate" type="text/html" href="' .
5879 $alt_url . '" />' . "\n" .
5880 '<link rel="self" type="' . $content_type . '" href="' .
5881 $cgi->self_url() . '" />' . "\n" .
5882 "<id>" . href(-full=>1) . "</id>\n" .
5883 # use project owner for feed author
5884 "<author><name>$owner</name></author>\n";
5885 if (defined $favicon) {
5886 print "<icon>" . esc_url($favicon) . "</icon>\n";
5888 if (defined $logo_url) {
5889 # not twice as wide as tall: 72 x 27 pixels
5890 print "<logo>" . esc_url($logo) . "</logo>\n";
5892 if (! %latest_date) {
5893 # dummy date to keep the feed valid until commits trickle in:
5894 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5895 } else {
5896 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5900 # contents
5901 for (my $i = 0; $i <= $#commitlist; $i++) {
5902 my %co = %{$commitlist[$i]};
5903 my $commit = $co{'id'};
5904 # we read 150, we always show 30 and the ones more recent than 48 hours
5905 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5906 last;
5908 my %cd = parse_date($co{'author_epoch'});
5910 # get list of changed files
5911 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5912 $co{'parent'} || "--root",
5913 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5914 or next;
5915 my @difftree = map { chomp; $_ } <$fd>;
5916 close $fd
5917 or next;
5919 # print element (entry, item)
5920 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5921 if ($format eq 'rss') {
5922 print "<item>\n" .
5923 "<title>" . esc_html($co{'title'}) . "</title>\n" .
5924 "<author>" . esc_html($co{'author'}) . "</author>\n" .
5925 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5926 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5927 "<link>$co_url</link>\n" .
5928 "<description>" . esc_html($co{'title'}) . "</description>\n" .
5929 "<content:encoded>" .
5930 "<![CDATA[\n";
5931 } elsif ($format eq 'atom') {
5932 print "<entry>\n" .
5933 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5934 "<updated>$cd{'iso-8601'}</updated>\n" .
5935 "<author>\n" .
5936 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
5937 if ($co{'author_email'}) {
5938 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
5940 print "</author>\n" .
5941 # use committer for contributor
5942 "<contributor>\n" .
5943 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5944 if ($co{'committer_email'}) {
5945 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5947 print "</contributor>\n" .
5948 "<published>$cd{'iso-8601'}</published>\n" .
5949 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5950 "<id>$co_url</id>\n" .
5951 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5952 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5954 my $comment = $co{'comment'};
5955 print "<pre>\n";
5956 foreach my $line (@$comment) {
5957 $line = esc_html($line);
5958 print "$line\n";
5960 print "</pre><ul>\n";
5961 foreach my $difftree_line (@difftree) {
5962 my %difftree = parse_difftree_raw_line($difftree_line);
5963 next if !$difftree{'from_id'};
5965 my $file = $difftree{'file'} || $difftree{'to_file'};
5967 print "<li>" .
5968 "[" .
5969 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5970 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5971 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5972 file_name=>$file, file_parent=>$difftree{'from_file'}),
5973 -title => "diff"}, 'D');
5974 if ($have_blame) {
5975 print $cgi->a({-href => href(-full=>1, action=>"blame",
5976 file_name=>$file, hash_base=>$commit), -class => "blamelink",
5977 -title => "blame"}, 'B');
5979 # if this is not a feed of a file history
5980 if (!defined $file_name || $file_name ne $file) {
5981 print $cgi->a({-href => href(-full=>1, action=>"history",
5982 file_name=>$file, hash=>$commit),
5983 -title => "history"}, 'H');
5985 $file = esc_path($file);
5986 print "] ".
5987 "$file</li>\n";
5989 if ($format eq 'rss') {
5990 print "</ul>]]>\n" .
5991 "</content:encoded>\n" .
5992 "</item>\n";
5993 } elsif ($format eq 'atom') {
5994 print "</ul>\n</div>\n" .
5995 "</content>\n" .
5996 "</entry>\n";
6000 # end of feed
6001 if ($format eq 'rss') {
6002 print "</channel>\n</rss>\n";
6003 } elsif ($format eq 'atom') {
6004 print "</feed>\n";
6008 sub git_rss {
6009 git_feed('rss');
6012 sub git_atom {
6013 git_feed('atom');
6016 sub git_opml {
6017 my @list = git_get_projects_list();
6019 print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
6020 print <<XML;
6021 <?xml version="1.0" encoding="utf-8"?>
6022 <opml version="1.0">
6023 <head>
6024 <title>$site_name OPML Export</title>
6025 </head>
6026 <body>
6027 <outline text="git RSS feeds">
6030 foreach my $pr (@list) {
6031 my %proj = %$pr;
6032 my $head = git_get_head_hash($proj{'path'});
6033 if (!defined $head) {
6034 next;
6036 $git_dir = "$projectroot/$proj{'path'}";
6037 my %co = parse_commit($head);
6038 if (!%co) {
6039 next;
6042 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6043 my $rss = "$my_url?p=$proj{'path'};a=rss";
6044 my $html = "$my_url?p=$proj{'path'};a=summary";
6045 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6047 print <<XML;
6048 </outline>
6049 </body>
6050 </opml>