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