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