Merge branch 'gitmaster'
[git/gitweb-warthdog9.git] / gitweb / gitweb.perl
blob90f43c575ed39493df6b3dc534e531fb4e2d2767
1 #!/usr/bin/perl
3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
10 use strict;
11 use warnings;
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser set_message);
15 use Encode;
16 use Fcntl ':mode';
17 use File::Find qw();
18 use File::Path qw(mkpath rmtree);
19 use File::Basename qw(basename);
20 use Digest::MD5 qw(md5 md5_hex md5_base64);
21 use Fcntl ':flock';
22 use IPC::Open2;
23 binmode STDOUT, ':utf8';
25 our $t0;
26 if (eval { require Time::HiRes; 1; }) {
27 $t0 = [Time::HiRes::gettimeofday()];
29 our $number_of_git_cmds = 0;
31 BEGIN {
32 CGI->compile() if $ENV{'MOD_PERL'};
35 our $version = "++GIT_VERSION++";
38 # Define and than setup our configuration
40 our(
41 $VERSION,
42 $path_info,
43 $GIT,
44 $projectroot,
45 $project_maxdepth,
46 $home_link,
47 $home_link_str,
48 $site_name,
49 $site_header,
50 $home_text,
51 $site_footer,
52 @stylesheets,
53 $stylesheet,
54 $logo,
55 $favicon,
56 $javascript,
57 $logo_url,
58 $logo_label,
59 $projects_list,
60 $projects_list_description_width,
61 $default_projects_order,
62 $export_ok,
63 $export_auth_hook,
64 $strict_export,
65 @git_base_url_list,
66 $default_blob_plain_mimetype,
67 $default_text_plain_charset,
68 $mimetypes_file,
69 $missmatch_git,
70 $gitlinkurl,
71 $maxload,
72 $cache_enable,
73 $minCacheTime,
74 $maxCacheTime,
75 $cachedir,
76 $backgroundCache,
77 $nocachedata,
78 $nocachedatabin,
79 $fullhashpath,
80 $fullhashbinpath,
81 %known_snapshot_format_aliases,
82 %known_snapshot_formats,
83 $fallback_encoding,
84 %avatar_size,
85 $headerRefresh,
86 $base_url,
87 $prevent_xss,
88 @diff_opts,
89 %feature,
90 $my_url,
91 $my_uri,
92 %highlight_basename,
93 %highlight_ext,
94 $highlight_bin
97 do 'gitweb_defaults.pl';
99 sub evaluate_uri {
100 our $cgi;
102 our $my_url = $cgi->url();
103 our $my_uri = $cgi->url(-absolute => 1);
105 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
106 # needed and used only for URLs with nonempty PATH_INFO
107 our $base_url = $my_url;
109 # When the script is used as DirectoryIndex, the URL does not contain the name
110 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
111 # have to do it ourselves. We make $path_info global because it's also used
112 # later on.
114 # Another issue with the script being the DirectoryIndex is that the resulting
115 # $my_url data is not the full script URL: this is good, because we want
116 # generated links to keep implying the script name if it wasn't explicitly
117 # indicated in the URL we're handling, but it means that $my_url cannot be used
118 # as base URL.
119 # Therefore, if we needed to strip PATH_INFO, then we know that we have
120 # to build the base URL ourselves:
121 our $path_info = $ENV{"PATH_INFO"};
122 if ($path_info) {
123 if ($my_url =~ s,\Q$path_info\E$,, &&
124 $my_uri =~ s,\Q$path_info\E$,, &&
125 defined $ENV{'SCRIPT_NAME'}) {
126 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
130 # target of the home link on top of all pages
131 our $home_link = $my_uri || "/";
134 sub gitweb_get_feature {
135 my ($name) = @_;
136 return unless exists $feature{$name};
137 my ($sub, $override, @defaults) = (
138 $feature{$name}{'sub'},
139 $feature{$name}{'override'},
140 @{$feature{$name}{'default'}});
141 # project specific override is possible only if we have project
142 our $git_dir; # global variable, declared later
143 if (!$override || !defined $git_dir) {
144 return @defaults;
146 if (!defined $sub) {
147 warn "feature $name is not overridable";
148 return @defaults;
150 return $sub->(@defaults);
153 # A wrapper to check if a given feature is enabled.
154 # With this, you can say
156 # my $bool_feat = gitweb_check_feature('bool_feat');
157 # gitweb_check_feature('bool_feat') or somecode;
159 # instead of
161 # my ($bool_feat) = gitweb_get_feature('bool_feat');
162 # (gitweb_get_feature('bool_feat'))[0] or somecode;
164 sub gitweb_check_feature {
165 return (gitweb_get_feature(@_))[0];
169 sub feature_bool {
170 my $key = shift;
171 my ($val) = git_get_project_config($key, '--bool');
173 if (!defined $val) {
174 return ($_[0]);
175 } elsif ($val eq 'true') {
176 return (1);
177 } elsif ($val eq 'false') {
178 return (0);
182 sub feature_snapshot {
183 my (@fmts) = @_;
185 my ($val) = git_get_project_config('snapshot');
187 if ($val) {
188 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
191 return @fmts;
194 sub feature_patches {
195 my @val = (git_get_project_config('patches', '--int'));
197 if (@val) {
198 return @val;
201 return ($_[0]);
204 sub feature_avatar {
205 my @val = (git_get_project_config('avatar'));
207 return @val ? @val : @_;
210 # checking HEAD file with -e is fragile if the repository was
211 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
212 # and then pruned.
213 sub check_head_link {
214 my ($dir) = @_;
215 my $headfile = "$dir/HEAD";
216 return ((-e $headfile) ||
217 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
220 sub check_export_ok {
221 my ($dir) = @_;
222 return (check_head_link($dir) &&
223 (!$export_ok || -e "$dir/$export_ok") &&
224 (!$export_auth_hook || $export_auth_hook->($dir)));
227 # process alternate names for backward compatibility
228 # filter out unsupported (unknown) snapshot formats
229 sub filter_snapshot_fmts {
230 my @fmts = @_;
232 @fmts = map {
233 exists $known_snapshot_format_aliases{$_} ?
234 $known_snapshot_format_aliases{$_} : $_} @fmts;
235 @fmts = grep {
236 exists $known_snapshot_formats{$_} &&
237 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
240 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
241 sub evaluate_gitweb_config {
242 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
243 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
244 # die if there are errors parsing config file
245 if (-e $GITWEB_CONFIG) {
246 do $GITWEB_CONFIG;
247 die $@ if $@;
248 } elsif (-e $GITWEB_CONFIG_SYSTEM) {
249 do $GITWEB_CONFIG_SYSTEM;
250 die $@ if $@;
254 # Get loadavg of system, to compare against $maxload.
255 # Currently it requires '/proc/loadavg' present to get loadavg;
256 # if it is not present it returns 0, which means no load checking.
257 sub get_loadavg {
258 if( -e '/proc/loadavg' ){
259 open my $fd, '<', '/proc/loadavg'
260 or return 0;
261 my @load = split(/\s+/, scalar <$fd>);
262 close $fd;
264 # The first three columns measure CPU and IO utilization of the last one,
265 # five, and 10 minute periods. The fourth column shows the number of
266 # currently running processes and the total number of processes in the m/n
267 # format. The last column displays the last process ID used.
268 return $load[0] || 0;
270 # additional checks for load average should go here for things that don't export
271 # /proc/loadavg
273 return 0;
277 # Includes
279 do 'cache.pm';
281 # version of the core git binary
282 our $git_version;
283 sub evaluate_git_version {
284 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
285 $number_of_git_cmds++;
288 sub check_loadavg {
289 if (defined $maxload && get_loadavg() > $maxload) {
290 die_error(503, "The load average on the server is too high");
294 # ======================================================================
295 # input validation and dispatch
297 # input parameters can be collected from a variety of sources (presently, CGI
298 # and PATH_INFO), so we define an %input_params hash that collects them all
299 # together during validation: this allows subsequent uses (e.g. href()) to be
300 # agnostic of the parameter origin
302 our %input_params = ();
304 # input parameters are stored with the long parameter name as key. This will
305 # also be used in the href subroutine to convert parameters to their CGI
306 # equivalent, and since the href() usage is the most frequent one, we store
307 # the name -> CGI key mapping here, instead of the reverse.
309 # XXX: Warning: If you touch this, check the search form for updating,
310 # too.
312 our @cgi_param_mapping = (
313 project => "p",
314 action => "a",
315 file_name => "f",
316 file_parent => "fp",
317 hash => "h",
318 hash_parent => "hp",
319 hash_base => "hb",
320 hash_parent_base => "hpb",
321 page => "pg",
322 order => "o",
323 searchtext => "s",
324 searchtype => "st",
325 snapshot_format => "sf",
326 extra_options => "opt",
327 search_use_regexp => "sr",
328 # this must be last entry (for manipulation from JavaScript)
329 javascript => "js"
331 our %cgi_param_mapping = @cgi_param_mapping;
333 # we will also need to know the possible actions, for validation
334 our %actions = (
335 "blame" => \&git_blame,
336 "blame_incremental" => \&git_blame_incremental,
337 "blame_data" => \&git_blame_data,
338 "blobdiff" => \&git_blobdiff,
339 "blobdiff_plain" => \&git_blobdiff_plain,
340 "blob" => \&git_blob,
341 "blob_plain" => \&git_blob_plain,
342 "commitdiff" => \&git_commitdiff,
343 "commitdiff_plain" => \&git_commitdiff_plain,
344 "commit" => \&git_commit,
345 "forks" => \&git_forks,
346 "heads" => \&git_heads,
347 "history" => \&git_history,
348 "log" => \&git_log,
349 "patch" => \&git_patch,
350 "patches" => \&git_patches,
351 "rss" => \&git_rss,
352 "atom" => \&git_atom,
353 "search" => \&git_search,
354 "search_help" => \&git_search_help,
355 "shortlog" => \&git_shortlog,
356 "summary" => \&git_summary,
357 "tag" => \&git_tag,
358 "tags" => \&git_tags,
359 "tree" => \&git_tree,
360 "snapshot" => \&git_snapshot,
361 "object" => \&git_object,
362 # those below don't need $project
363 "opml" => \&git_opml,
364 "project_list" => \&git_project_list,
365 "project_index" => \&git_project_index,
368 # finally, we have the hash of allowed extra_options for the commands that
369 # allow them
370 our %allowed_options = (
371 "--no-merges" => [ qw(rss atom log shortlog history) ],
374 # fill %input_params with the CGI parameters. All values except for 'opt'
375 # should be single values, but opt can be an array. We should probably
376 # build an array of parameters that can be multi-valued, but since for the time
377 # being it's only this one, we just single it out
378 sub evaluate_query_params {
379 our $cgi;
381 while (my ($name, $symbol) = each %cgi_param_mapping) {
382 if ($symbol eq 'opt') {
383 $input_params{$name} = [ $cgi->param($symbol) ];
384 } else {
385 $input_params{$name} = $cgi->param($symbol);
390 # now read PATH_INFO and update the parameter list for missing parameters
391 sub evaluate_path_info {
392 return if defined $input_params{'project'};
393 return if !$path_info;
394 $path_info =~ s,^/+,,;
395 return if !$path_info;
397 # find which part of PATH_INFO is project
398 my $project = $path_info;
399 $project =~ s,/+$,,;
400 while ($project && !check_head_link("$projectroot/$project")) {
401 $project =~ s,/*[^/]*$,,;
403 return unless $project;
404 $input_params{'project'} = $project;
406 # do not change any parameters if an action is given using the query string
407 return if $input_params{'action'};
408 $path_info =~ s,^\Q$project\E/*,,;
410 # next, check if we have an action
411 my $action = $path_info;
412 $action =~ s,/.*$,,;
413 if (exists $actions{$action}) {
414 $path_info =~ s,^$action/*,,;
415 $input_params{'action'} = $action;
418 # list of actions that want hash_base instead of hash, but can have no
419 # pathname (f) parameter
420 my @wants_base = (
421 'tree',
422 'history',
425 # we want to catch, among others
426 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
427 my ($parentrefname, $parentpathname, $refname, $pathname) =
428 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
430 # first, analyze the 'current' part
431 if (defined $pathname) {
432 # we got "branch:filename" or "branch:dir/"
433 # we could use git_get_type(branch:pathname), but:
434 # - it needs $git_dir
435 # - it does a git() call
436 # - the convention of terminating directories with a slash
437 # makes it superfluous
438 # - embedding the action in the PATH_INFO would make it even
439 # more superfluous
440 $pathname =~ s,^/+,,;
441 if (!$pathname || substr($pathname, -1) eq "/") {
442 $input_params{'action'} ||= "tree";
443 $pathname =~ s,/$,,;
444 } else {
445 # the default action depends on whether we had parent info
446 # or not
447 if ($parentrefname) {
448 $input_params{'action'} ||= "blobdiff_plain";
449 } else {
450 $input_params{'action'} ||= "blob_plain";
453 $input_params{'hash_base'} ||= $refname;
454 $input_params{'file_name'} ||= $pathname;
455 } elsif (defined $refname) {
456 # we got "branch". In this case we have to choose if we have to
457 # set hash or hash_base.
459 # Most of the actions without a pathname only want hash to be
460 # set, except for the ones specified in @wants_base that want
461 # hash_base instead. It should also be noted that hand-crafted
462 # links having 'history' as an action and no pathname or hash
463 # set will fail, but that happens regardless of PATH_INFO.
464 if (defined $parentrefname) {
465 # if there is parent let the default be 'shortlog' action
466 # (for http://git.example.com/repo.git/A..B links); if there
467 # is no parent, dispatch will detect type of object and set
468 # action appropriately if required (if action is not set)
469 $input_params{'action'} ||= "shortlog";
471 if ($input_params{'action'} &&
472 grep { $_ eq $input_params{'action'} } @wants_base) {
473 $input_params{'hash_base'} ||= $refname;
474 } else {
475 $input_params{'hash'} ||= $refname;
479 # next, handle the 'parent' part, if present
480 if (defined $parentrefname) {
481 # a missing pathspec defaults to the 'current' filename, allowing e.g.
482 # someproject/blobdiff/oldrev..newrev:/filename
483 if ($parentpathname) {
484 $parentpathname =~ s,^/+,,;
485 $parentpathname =~ s,/$,,;
486 $input_params{'file_parent'} ||= $parentpathname;
487 } else {
488 $input_params{'file_parent'} ||= $input_params{'file_name'};
490 # we assume that hash_parent_base is wanted if a path was specified,
491 # or if the action wants hash_base instead of hash
492 if (defined $input_params{'file_parent'} ||
493 grep { $_ eq $input_params{'action'} } @wants_base) {
494 $input_params{'hash_parent_base'} ||= $parentrefname;
495 } else {
496 $input_params{'hash_parent'} ||= $parentrefname;
500 # for the snapshot action, we allow URLs in the form
501 # $project/snapshot/$hash.ext
502 # where .ext determines the snapshot and gets removed from the
503 # passed $refname to provide the $hash.
505 # To be able to tell that $refname includes the format extension, we
506 # require the following two conditions to be satisfied:
507 # - the hash input parameter MUST have been set from the $refname part
508 # of the URL (i.e. they must be equal)
509 # - the snapshot format MUST NOT have been defined already (e.g. from
510 # CGI parameter sf)
511 # It's also useless to try any matching unless $refname has a dot,
512 # so we check for that too
513 if (defined $input_params{'action'} &&
514 $input_params{'action'} eq 'snapshot' &&
515 defined $refname && index($refname, '.') != -1 &&
516 $refname eq $input_params{'hash'} &&
517 !defined $input_params{'snapshot_format'}) {
518 # We loop over the known snapshot formats, checking for
519 # extensions. Allowed extensions are both the defined suffix
520 # (which includes the initial dot already) and the snapshot
521 # format key itself, with a prepended dot
522 while (my ($fmt, $opt) = each %known_snapshot_formats) {
523 my $hash = $refname;
524 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
525 next;
527 my $sfx = $1;
528 # a valid suffix was found, so set the snapshot format
529 # and reset the hash parameter
530 $input_params{'snapshot_format'} = $fmt;
531 $input_params{'hash'} = $hash;
532 # we also set the format suffix to the one requested
533 # in the URL: this way a request for e.g. .tgz returns
534 # a .tgz instead of a .tar.gz
535 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
536 last;
541 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
542 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
543 $searchtext, $search_regexp);
544 sub evaluate_and_validate_params {
545 our $action = $input_params{'action'};
546 if (defined $action) {
547 if (!validate_action($action)) {
548 die_error(400, "Invalid action parameter");
552 # parameters which are pathnames
553 our $project = $input_params{'project'};
554 if (defined $project) {
555 if (!validate_project($project)) {
556 undef $project;
557 die_error(404, "No such project");
561 our $file_name = $input_params{'file_name'};
562 if (defined $file_name) {
563 if (!validate_pathname($file_name)) {
564 die_error(400, "Invalid file parameter");
568 our $file_parent = $input_params{'file_parent'};
569 if (defined $file_parent) {
570 if (!validate_pathname($file_parent)) {
571 die_error(400, "Invalid file parent parameter");
575 # parameters which are refnames
576 our $hash = $input_params{'hash'};
577 if (defined $hash) {
578 if (!validate_refname($hash)) {
579 die_error(400, "Invalid hash parameter");
583 our $hash_parent = $input_params{'hash_parent'};
584 if (defined $hash_parent) {
585 if (!validate_refname($hash_parent)) {
586 die_error(400, "Invalid hash parent parameter");
590 our $hash_base = $input_params{'hash_base'};
591 if (defined $hash_base) {
592 if (!validate_refname($hash_base)) {
593 die_error(400, "Invalid hash base parameter");
597 our @extra_options = @{$input_params{'extra_options'}};
598 # @extra_options is always defined, since it can only be (currently) set from
599 # CGI, and $cgi->param() returns the empty array in array context if the param
600 # is not set
601 foreach my $opt (@extra_options) {
602 if (not exists $allowed_options{$opt}) {
603 die_error(400, "Invalid option parameter");
605 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
606 die_error(400, "Invalid option parameter for this action");
610 our $hash_parent_base = $input_params{'hash_parent_base'};
611 if (defined $hash_parent_base) {
612 if (!validate_refname($hash_parent_base)) {
613 die_error(400, "Invalid hash parent base parameter");
617 # other parameters
618 our $page = $input_params{'page'};
619 if (defined $page) {
620 if ($page =~ m/[^0-9]/) {
621 die_error(400, "Invalid page parameter");
625 our $searchtype = $input_params{'searchtype'};
626 if (defined $searchtype) {
627 if ($searchtype =~ m/[^a-z]/) {
628 die_error(400, "Invalid searchtype parameter");
632 our $search_use_regexp = $input_params{'search_use_regexp'};
634 our $searchtext = $input_params{'searchtext'};
635 our $search_regexp;
636 if (defined $searchtext) {
637 if (length($searchtext) < 2) {
638 die_error(403, "At least two characters are required for search parameter");
640 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
644 # path to the current git repository
645 our $git_dir;
646 sub evaluate_git_dir {
647 our $git_dir = "$projectroot/$project" if $project;
650 our (@snapshot_fmts, $git_avatar);
651 sub configure_gitweb_features {
652 # list of supported snapshot formats
653 our @snapshot_fmts = gitweb_get_feature('snapshot');
654 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
656 # check that the avatar feature is set to a known provider name,
657 # and for each provider check if the dependencies are satisfied.
658 # if the provider name is invalid or the dependencies are not met,
659 # reset $git_avatar to the empty string.
660 our ($git_avatar) = gitweb_get_feature('avatar');
661 if ($git_avatar eq 'gravatar') {
662 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
663 } elsif ($git_avatar eq 'picon') {
664 # no dependencies
665 } else {
666 $git_avatar = '';
670 # custom error handler: 'die <message>' is Internal Server Error
671 sub handle_errors_html {
672 my $msg = shift; # it is already HTML escaped
674 # to avoid infinite loop where error occurs in die_error,
675 # change handler to default handler, disabling handle_errors_html
676 set_message("Error occured when inside die_error:\n$msg");
678 # you cannot jump out of die_error when called as error handler;
679 # the subroutine set via CGI::Carp::set_message is called _after_
680 # HTTP headers are already written, so it cannot write them itself
681 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
683 set_message(\&handle_errors_html);
685 # dispatch
686 sub dispatch {
687 if (!defined $action) {
688 if (defined $hash) {
689 $action = git_get_type($hash);
690 } elsif (defined $hash_base && defined $file_name) {
691 $action = git_get_type("$hash_base:$file_name");
692 } elsif (defined $project) {
693 $action = 'summary';
694 } else {
695 $action = 'project_list';
698 if (!defined($actions{$action})) {
699 die_error(400, "Unknown action");
701 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
702 !$project) {
703 die_error(400, "Project needed");
705 #$actions{$action}->();
706 cache_fetch($action);
709 sub reset_timer {
710 our $t0 = [Time::HiRes::gettimeofday()]
711 if defined $t0;
712 our $number_of_git_cmds = 0;
715 sub run_request {
716 reset_timer();
718 evaluate_uri();
719 evaluate_gitweb_config();
720 check_loadavg();
722 # $projectroot and $projects_list might be set in gitweb config file
723 $projects_list ||= $projectroot;
725 evaluate_query_params();
726 evaluate_path_info();
727 evaluate_and_validate_params();
728 evaluate_git_dir();
730 configure_gitweb_features();
732 dispatch();
735 our $is_last_request = sub { 1 };
736 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
737 our $CGI = 'CGI';
738 our $cgi;
739 sub configure_as_fcgi {
740 require CGI::Fast;
741 our $CGI = 'CGI::Fast';
743 my $request_number = 0;
744 # let each child service 100 requests
745 our $is_last_request = sub { ++$request_number > 100 };
747 sub evaluate_argv {
748 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
749 configure_as_fcgi()
750 if $script_name =~ /\.fcgi$/;
752 return unless (@ARGV);
754 require Getopt::Long;
755 Getopt::Long::GetOptions(
756 'fastcgi|fcgi|f' => \&configure_as_fcgi,
757 'nproc|n=i' => sub {
758 my ($arg, $val) = @_;
759 return unless eval { require FCGI::ProcManager; 1; };
760 my $proc_manager = FCGI::ProcManager->new({
761 n_processes => $val,
763 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
764 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
765 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
770 sub run {
771 evaluate_argv();
772 evaluate_git_version();
774 # There's a pretty serious flaw that we silently fail if git doesn't find something it needs
775 # a quick and simple check is to have gitweb do a simple check - are we running on the same
776 # version of git that we shipped with - if not, throw up an error so that people doing
777 # first installs don't have to debug perl to figure out whats going on
778 if (
779 $git_version ne $version
781 $missmatch_git eq ''
783 git_header_html();
784 print "<p><b>*** Warning ***</b></p>\n";
785 print "<p>\n";
786 print "This version of gitweb was compiled for <b>$version</b> however git version <b>$git_version</b> was found<br/>\n";
787 print "If you are sure this version of git works with this version of gitweb - please define <b>\$missmatch_git</b> to a non empty string in your git config file.\n";
788 print "</p>\n";
789 git_footer_html();
790 exit;
794 $pre_listen_hook->()
795 if $pre_listen_hook;
797 REQUEST:
798 while ($cgi = $CGI->new()) {
799 $pre_dispatch_hook->()
800 if $pre_dispatch_hook;
802 run_request();
804 $post_dispatch_hook->()
805 if $post_dispatch_hook;
807 last REQUEST if ($is_last_request->());
810 DONE_GITWEB:
814 run();
816 if (defined caller) {
817 # wrapped in a subroutine processing requests,
818 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
819 return;
820 } else {
821 # pure CGI script, serving single request
822 exit;
825 ## ======================================================================
826 ## action links
828 # possible values of extra options
829 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
830 # -replay => 1 - start from a current view (replay with modifications)
831 # -path_info => 0|1 - don't use/use path_info URL (if possible)
832 sub href {
833 my %params = @_;
834 # default is to use -absolute url() i.e. $my_uri
835 my $href = $params{-full} ? $my_url : $my_uri;
837 $params{'project'} = $project unless exists $params{'project'};
839 if ($params{-replay}) {
840 while (my ($name, $symbol) = each %cgi_param_mapping) {
841 if (!exists $params{$name}) {
842 $params{$name} = $input_params{$name};
847 my $use_pathinfo = gitweb_check_feature('pathinfo');
848 if (defined $params{'project'} &&
849 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
850 # try to put as many parameters as possible in PATH_INFO:
851 # - project name
852 # - action
853 # - hash_parent or hash_parent_base:/file_parent
854 # - hash or hash_base:/filename
855 # - the snapshot_format as an appropriate suffix
857 # When the script is the root DirectoryIndex for the domain,
858 # $href here would be something like http://gitweb.example.com/
859 # Thus, we strip any trailing / from $href, to spare us double
860 # slashes in the final URL
861 $href =~ s,/$,,;
863 # Then add the project name, if present
864 $href .= "/".esc_url($params{'project'});
865 delete $params{'project'};
867 # since we destructively absorb parameters, we keep this
868 # boolean that remembers if we're handling a snapshot
869 my $is_snapshot = $params{'action'} eq 'snapshot';
871 # Summary just uses the project path URL, any other action is
872 # added to the URL
873 if (defined $params{'action'}) {
874 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
875 delete $params{'action'};
878 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
879 # stripping nonexistent or useless pieces
880 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
881 || $params{'hash_parent'} || $params{'hash'});
882 if (defined $params{'hash_base'}) {
883 if (defined $params{'hash_parent_base'}) {
884 $href .= esc_url($params{'hash_parent_base'});
885 # skip the file_parent if it's the same as the file_name
886 if (defined $params{'file_parent'}) {
887 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
888 delete $params{'file_parent'};
889 } elsif ($params{'file_parent'} !~ /\.\./) {
890 $href .= ":/".esc_url($params{'file_parent'});
891 delete $params{'file_parent'};
894 $href .= "..";
895 delete $params{'hash_parent'};
896 delete $params{'hash_parent_base'};
897 } elsif (defined $params{'hash_parent'}) {
898 $href .= esc_url($params{'hash_parent'}). "..";
899 delete $params{'hash_parent'};
902 $href .= esc_url($params{'hash_base'});
903 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
904 $href .= ":/".esc_url($params{'file_name'});
905 delete $params{'file_name'};
907 delete $params{'hash'};
908 delete $params{'hash_base'};
909 } elsif (defined $params{'hash'}) {
910 $href .= esc_url($params{'hash'});
911 delete $params{'hash'};
914 # If the action was a snapshot, we can absorb the
915 # snapshot_format parameter too
916 if ($is_snapshot) {
917 my $fmt = $params{'snapshot_format'};
918 # snapshot_format should always be defined when href()
919 # is called, but just in case some code forgets, we
920 # fall back to the default
921 $fmt ||= $snapshot_fmts[0];
922 $href .= $known_snapshot_formats{$fmt}{'suffix'};
923 delete $params{'snapshot_format'};
927 # now encode the parameters explicitly
928 my @result = ();
929 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
930 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
931 if (defined $params{$name}) {
932 if (ref($params{$name}) eq "ARRAY") {
933 foreach my $par (@{$params{$name}}) {
934 push @result, $symbol . "=" . esc_param($par);
936 } else {
937 push @result, $symbol . "=" . esc_param($params{$name});
941 $href .= "?" . join(';', @result) if scalar @result;
943 return $href;
947 ## ======================================================================
948 ## validation, quoting/unquoting and escaping
950 sub validate_action {
951 my $input = shift || return undef;
952 return undef unless exists $actions{$input};
953 return $input;
956 sub validate_project {
957 my $input = shift || return undef;
958 if (!validate_pathname($input) ||
959 !(-d "$projectroot/$input") ||
960 !check_export_ok("$projectroot/$input") ||
961 ($strict_export && !project_in_list($input))) {
962 return undef;
963 } else {
964 return $input;
968 sub validate_pathname {
969 my $input = shift || return undef;
971 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
972 # at the beginning, at the end, and between slashes.
973 # also this catches doubled slashes
974 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
975 return undef;
977 # no null characters
978 if ($input =~ m!\0!) {
979 return undef;
981 return $input;
984 sub validate_refname {
985 my $input = shift || return undef;
987 # textual hashes are O.K.
988 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
989 return $input;
991 # it must be correct pathname
992 $input = validate_pathname($input)
993 or return undef;
994 # restrictions on ref name according to git-check-ref-format
995 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
996 return undef;
998 return $input;
1001 # decode sequences of octets in utf8 into Perl's internal form,
1002 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1003 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1004 sub to_utf8 {
1005 my $str = shift;
1006 return undef unless defined $str;
1007 if (utf8::valid($str)) {
1008 utf8::decode($str);
1009 return $str;
1010 } else {
1011 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1015 # quote unsafe chars, but keep the slash, even when it's not
1016 # correct, but quoted slashes look too horrible in bookmarks
1017 sub esc_param {
1018 my $str = shift;
1019 return undef unless defined $str;
1020 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1021 $str =~ s/ /\+/g;
1022 return $str;
1025 # quote unsafe chars in whole URL, so some characters cannot be quoted
1026 sub esc_url {
1027 my $str = shift;
1028 return undef unless defined $str;
1029 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1030 $str =~ s/ /\+/g;
1031 return $str;
1034 # replace invalid utf8 character with SUBSTITUTION sequence
1035 sub esc_html {
1036 my $str = shift;
1037 my %opts = @_;
1039 return undef unless defined $str;
1041 $str = to_utf8($str);
1042 $str = $cgi->escapeHTML($str);
1043 if ($opts{'-nbsp'}) {
1044 $str =~ s/ /&nbsp;/g;
1046 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1047 return $str;
1050 # quote control characters and escape filename to HTML
1051 sub esc_path {
1052 my $str = shift;
1053 my %opts = @_;
1055 return undef unless defined $str;
1057 $str = to_utf8($str);
1058 $str = $cgi->escapeHTML($str);
1059 if ($opts{'-nbsp'}) {
1060 $str =~ s/ /&nbsp;/g;
1062 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1063 return $str;
1066 # Make control characters "printable", using character escape codes (CEC)
1067 sub quot_cec {
1068 my $cntrl = shift;
1069 my %opts = @_;
1070 my %es = ( # character escape codes, aka escape sequences
1071 "\t" => '\t', # tab (HT)
1072 "\n" => '\n', # line feed (LF)
1073 "\r" => '\r', # carrige return (CR)
1074 "\f" => '\f', # form feed (FF)
1075 "\b" => '\b', # backspace (BS)
1076 "\a" => '\a', # alarm (bell) (BEL)
1077 "\e" => '\e', # escape (ESC)
1078 "\013" => '\v', # vertical tab (VT)
1079 "\000" => '\0', # nul character (NUL)
1081 my $chr = ( (exists $es{$cntrl})
1082 ? $es{$cntrl}
1083 : sprintf('\%2x', ord($cntrl)) );
1084 if ($opts{-nohtml}) {
1085 return $chr;
1086 } else {
1087 return "<span class=\"cntrl\">$chr</span>";
1091 # Alternatively use unicode control pictures codepoints,
1092 # Unicode "printable representation" (PR)
1093 sub quot_upr {
1094 my $cntrl = shift;
1095 my %opts = @_;
1097 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1098 if ($opts{-nohtml}) {
1099 return $chr;
1100 } else {
1101 return "<span class=\"cntrl\">$chr</span>";
1105 # git may return quoted and escaped filenames
1106 sub unquote {
1107 my $str = shift;
1109 sub unq {
1110 my $seq = shift;
1111 my %es = ( # character escape codes, aka escape sequences
1112 't' => "\t", # tab (HT, TAB)
1113 'n' => "\n", # newline (NL)
1114 'r' => "\r", # return (CR)
1115 'f' => "\f", # form feed (FF)
1116 'b' => "\b", # backspace (BS)
1117 'a' => "\a", # alarm (bell) (BEL)
1118 'e' => "\e", # escape (ESC)
1119 'v' => "\013", # vertical tab (VT)
1122 if ($seq =~ m/^[0-7]{1,3}$/) {
1123 # octal char sequence
1124 return chr(oct($seq));
1125 } elsif (exists $es{$seq}) {
1126 # C escape sequence, aka character escape code
1127 return $es{$seq};
1129 # quoted ordinary character
1130 return $seq;
1133 if ($str =~ m/^"(.*)"$/) {
1134 # needs unquoting
1135 $str = $1;
1136 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1138 return $str;
1141 # escape tabs (convert tabs to spaces)
1142 sub untabify {
1143 my $line = shift;
1145 while ((my $pos = index($line, "\t")) != -1) {
1146 if (my $count = (8 - ($pos % 8))) {
1147 my $spaces = ' ' x $count;
1148 $line =~ s/\t/$spaces/;
1152 return $line;
1155 sub project_in_list {
1156 my $project = shift;
1157 my @list = git_get_projects_list();
1158 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1161 ## ----------------------------------------------------------------------
1162 ## HTML aware string manipulation
1164 # Try to chop given string on a word boundary between position
1165 # $len and $len+$add_len. If there is no word boundary there,
1166 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1167 # (marking chopped part) would be longer than given string.
1168 sub chop_str {
1169 my $str = shift;
1170 my $len = shift;
1171 my $add_len = shift || 10;
1172 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1174 # Make sure perl knows it is utf8 encoded so we don't
1175 # cut in the middle of a utf8 multibyte char.
1176 $str = to_utf8($str);
1178 # allow only $len chars, but don't cut a word if it would fit in $add_len
1179 # if it doesn't fit, cut it if it's still longer than the dots we would add
1180 # remove chopped character entities entirely
1182 # when chopping in the middle, distribute $len into left and right part
1183 # return early if chopping wouldn't make string shorter
1184 if ($where eq 'center') {
1185 return $str if ($len + 5 >= length($str)); # filler is length 5
1186 $len = int($len/2);
1187 } else {
1188 return $str if ($len + 4 >= length($str)); # filler is length 4
1191 # regexps: ending and beginning with word part up to $add_len
1192 my $endre = qr/.{$len}\w{0,$add_len}/;
1193 my $begre = qr/\w{0,$add_len}.{$len}/;
1195 if ($where eq 'left') {
1196 $str =~ m/^(.*?)($begre)$/;
1197 my ($lead, $body) = ($1, $2);
1198 if (length($lead) > 4) {
1199 $lead = " ...";
1201 return "$lead$body";
1203 } elsif ($where eq 'center') {
1204 $str =~ m/^($endre)(.*)$/;
1205 my ($left, $str) = ($1, $2);
1206 $str =~ m/^(.*?)($begre)$/;
1207 my ($mid, $right) = ($1, $2);
1208 if (length($mid) > 5) {
1209 $mid = " ... ";
1211 return "$left$mid$right";
1213 } else {
1214 $str =~ m/^($endre)(.*)$/;
1215 my $body = $1;
1216 my $tail = $2;
1217 if (length($tail) > 4) {
1218 $tail = "... ";
1220 return "$body$tail";
1224 # takes the same arguments as chop_str, but also wraps a <span> around the
1225 # result with a title attribute if it does get chopped. Additionally, the
1226 # string is HTML-escaped.
1227 sub chop_and_escape_str {
1228 my ($str) = @_;
1230 my $chopped = chop_str(@_);
1231 if ($chopped eq $str) {
1232 return esc_html($chopped);
1233 } else {
1234 $str =~ s/[[:cntrl:]]/?/g;
1235 return $cgi->span({-title=>$str}, esc_html($chopped));
1239 ## ----------------------------------------------------------------------
1240 ## functions returning short strings
1242 # CSS class for given age value (in seconds)
1243 sub age_class {
1244 my $age = shift;
1246 if (!defined $age) {
1247 return "noage";
1248 } elsif ($age < 60*60*2) {
1249 return "age0";
1250 } elsif ($age < 60*60*24*2) {
1251 return "age1";
1252 } else {
1253 return "age2";
1257 # convert age in seconds to "nn units ago" string
1258 sub age_string {
1259 my $age = shift;
1260 my $age_str;
1262 if ($age > 60*60*24*365*2) {
1263 $age_str = (int $age/60/60/24/365);
1264 $age_str .= " years ago";
1265 } elsif ($age > 60*60*24*(365/12)*2) {
1266 $age_str = int $age/60/60/24/(365/12);
1267 $age_str .= " months ago";
1268 } elsif ($age > 60*60*24*7*2) {
1269 $age_str = int $age/60/60/24/7;
1270 $age_str .= " weeks ago";
1271 } elsif ($age > 60*60*24*2) {
1272 $age_str = int $age/60/60/24;
1273 $age_str .= " days ago";
1274 } elsif ($age > 60*60*2) {
1275 $age_str = int $age/60/60;
1276 $age_str .= " hours ago";
1277 } elsif ($age > 60*2) {
1278 $age_str = int $age/60;
1279 $age_str .= " min ago";
1280 } elsif ($age > 2) {
1281 $age_str = int $age;
1282 $age_str .= " sec ago";
1283 } else {
1284 $age_str .= " right now";
1286 return $age_str;
1289 use constant {
1290 S_IFINVALID => 0030000,
1291 S_IFGITLINK => 0160000,
1294 # submodule/subproject, a commit object reference
1295 sub S_ISGITLINK {
1296 my $mode = shift;
1298 return (($mode & S_IFMT) == S_IFGITLINK)
1301 # convert file mode in octal to symbolic file mode string
1302 sub mode_str {
1303 my $mode = oct shift;
1305 if (S_ISGITLINK($mode)) {
1306 return 'm---------';
1307 } elsif (S_ISDIR($mode & S_IFMT)) {
1308 return 'drwxr-xr-x';
1309 } elsif (S_ISLNK($mode)) {
1310 return 'lrwxrwxrwx';
1311 } elsif (S_ISREG($mode)) {
1312 # git cares only about the executable bit
1313 if ($mode & S_IXUSR) {
1314 return '-rwxr-xr-x';
1315 } else {
1316 return '-rw-r--r--';
1318 } else {
1319 return '----------';
1323 # convert file mode in octal to file type string
1324 sub file_type {
1325 my $mode = shift;
1327 if ($mode !~ m/^[0-7]+$/) {
1328 return $mode;
1329 } else {
1330 $mode = oct $mode;
1333 if (S_ISGITLINK($mode)) {
1334 return "submodule";
1335 } elsif (S_ISDIR($mode & S_IFMT)) {
1336 return "directory";
1337 } elsif (S_ISLNK($mode)) {
1338 return "symlink";
1339 } elsif (S_ISREG($mode)) {
1340 return "file";
1341 } else {
1342 return "unknown";
1346 # convert file mode in octal to file type description string
1347 sub file_type_long {
1348 my $mode = shift;
1350 if ($mode !~ m/^[0-7]+$/) {
1351 return $mode;
1352 } else {
1353 $mode = oct $mode;
1356 if (S_ISGITLINK($mode)) {
1357 return "submodule";
1358 } elsif (S_ISDIR($mode & S_IFMT)) {
1359 return "directory";
1360 } elsif (S_ISLNK($mode)) {
1361 return "symlink";
1362 } elsif (S_ISREG($mode)) {
1363 if ($mode & S_IXUSR) {
1364 return "executable";
1365 } else {
1366 return "file";
1368 } else {
1369 return "unknown";
1374 ## ----------------------------------------------------------------------
1375 ## functions returning short HTML fragments, or transforming HTML fragments
1376 ## which don't belong to other sections
1378 # format line of commit message.
1379 sub format_log_line_html {
1380 my $line = shift;
1382 $line = esc_html($line, -nbsp=>1);
1383 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1384 $cgi->a({-href => href(action=>"object", hash=>$1),
1385 -class => "text"}, $1);
1386 }eg;
1388 return $line;
1391 # format marker of refs pointing to given object
1393 # the destination action is chosen based on object type and current context:
1394 # - for annotated tags, we choose the tag view unless it's the current view
1395 # already, in which case we go to shortlog view
1396 # - for other refs, we keep the current view if we're in history, shortlog or
1397 # log view, and select shortlog otherwise
1398 sub format_ref_marker {
1399 my ($refs, $id) = @_;
1400 my $markers = '';
1402 if (defined $refs->{$id}) {
1403 foreach my $ref (@{$refs->{$id}}) {
1404 # this code exploits the fact that non-lightweight tags are the
1405 # only indirect objects, and that they are the only objects for which
1406 # we want to use tag instead of shortlog as action
1407 my ($type, $name) = qw();
1408 my $indirect = ($ref =~ s/\^\{\}$//);
1409 # e.g. tags/v2.6.11 or heads/next
1410 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1411 $type = $1;
1412 $name = $2;
1413 } else {
1414 $type = "ref";
1415 $name = $ref;
1418 my $class = $type;
1419 $class .= " indirect" if $indirect;
1421 my $dest_action = "shortlog";
1423 if ($indirect) {
1424 $dest_action = "tag" unless $action eq "tag";
1425 } elsif ($action =~ /^(history|(short)?log)$/) {
1426 $dest_action = $action;
1429 my $dest = "";
1430 $dest .= "refs/" unless $ref =~ m!^refs/!;
1431 $dest .= $ref;
1433 my $link = $cgi->a({
1434 -href => href(
1435 action=>$dest_action,
1436 hash=>$dest
1437 )}, $name);
1439 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1440 $link . "</span>";
1444 if ($markers) {
1445 return ' <span class="refs">'. $markers . '</span>';
1446 } else {
1447 return "";
1451 # format, perhaps shortened and with markers, title line
1452 sub format_subject_html {
1453 my ($long, $short, $href, $extra) = @_;
1454 $extra = '' unless defined($extra);
1456 if (length($short) < length($long)) {
1457 $long =~ s/[[:cntrl:]]/?/g;
1458 return $cgi->a({-href => $href, -class => "list subject",
1459 -title => to_utf8($long)},
1460 esc_html($short)) . $extra;
1461 } else {
1462 return $cgi->a({-href => $href, -class => "list subject"},
1463 esc_html($long)) . $extra;
1467 # Rather than recomputing the url for an email multiple times, we cache it
1468 # after the first hit. This gives a visible benefit in views where the avatar
1469 # for the same email is used repeatedly (e.g. shortlog).
1470 # The cache is shared by all avatar engines (currently gravatar only), which
1471 # are free to use it as preferred. Since only one avatar engine is used for any
1472 # given page, there's no risk for cache conflicts.
1473 our %avatar_cache = ();
1475 # Compute the picon url for a given email, by using the picon search service over at
1476 # http://www.cs.indiana.edu/picons/search.html
1477 sub picon_url {
1478 my $email = lc shift;
1479 if (!$avatar_cache{$email}) {
1480 my ($user, $domain) = split('@', $email);
1481 $avatar_cache{$email} =
1482 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1483 "$domain/$user/" .
1484 "users+domains+unknown/up/single";
1486 return $avatar_cache{$email};
1489 # Compute the gravatar url for a given email, if it's not in the cache already.
1490 # Gravatar stores only the part of the URL before the size, since that's the
1491 # one computationally more expensive. This also allows reuse of the cache for
1492 # different sizes (for this particular engine).
1493 sub gravatar_url {
1494 my $email = lc shift;
1495 my $size = shift;
1496 $avatar_cache{$email} ||=
1497 "http://www.gravatar.com/avatar/" .
1498 Digest::MD5::md5_hex($email) . "?s=";
1499 return $avatar_cache{$email} . $size;
1502 # Insert an avatar for the given $email at the given $size if the feature
1503 # is enabled.
1504 sub git_get_avatar {
1505 my ($email, %opts) = @_;
1506 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
1507 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
1508 $opts{-size} ||= 'default';
1509 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1510 my $url = "";
1511 if ($git_avatar eq 'gravatar') {
1512 $url = gravatar_url($email, $size);
1513 } elsif ($git_avatar eq 'picon') {
1514 $url = picon_url($email);
1516 # Other providers can be added by extending the if chain, defining $url
1517 # as needed. If no variant puts something in $url, we assume avatars
1518 # are completely disabled/unavailable.
1519 if ($url) {
1520 return $pre_white .
1521 "<img width=\"$size\" " .
1522 "class=\"avatar\" " .
1523 "src=\"$url\" " .
1524 "alt=\"\" " .
1525 "/>" . $post_white;
1526 } else {
1527 return "";
1531 sub format_search_author {
1532 my ($author, $searchtype, $displaytext) = @_;
1533 my $have_search = gitweb_check_feature('search');
1535 if ($have_search) {
1536 my $performed = "";
1537 if ($searchtype eq 'author') {
1538 $performed = "authored";
1539 } elsif ($searchtype eq 'committer') {
1540 $performed = "committed";
1543 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1544 searchtext=>$author,
1545 searchtype=>$searchtype), class=>"list",
1546 title=>"Search for commits $performed by $author"},
1547 $displaytext);
1549 } else {
1550 return $displaytext;
1554 # format the author name of the given commit with the given tag
1555 # the author name is chopped and escaped according to the other
1556 # optional parameters (see chop_str).
1557 sub format_author_html {
1558 my $tag = shift;
1559 my $co = shift;
1560 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1561 return "<$tag class=\"author\">" .
1562 format_search_author($co->{'author_name'}, "author",
1563 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1564 $author) .
1565 "</$tag>";
1568 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1569 sub format_git_diff_header_line {
1570 my $line = shift;
1571 my $diffinfo = shift;
1572 my ($from, $to) = @_;
1574 if ($diffinfo->{'nparents'}) {
1575 # combined diff
1576 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1577 if ($to->{'href'}) {
1578 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1579 esc_path($to->{'file'}));
1580 } else { # file was deleted (no href)
1581 $line .= esc_path($to->{'file'});
1583 } else {
1584 # "ordinary" diff
1585 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1586 if ($from->{'href'}) {
1587 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1588 'a/' . esc_path($from->{'file'}));
1589 } else { # file was added (no href)
1590 $line .= 'a/' . esc_path($from->{'file'});
1592 $line .= ' ';
1593 if ($to->{'href'}) {
1594 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1595 'b/' . esc_path($to->{'file'}));
1596 } else { # file was deleted
1597 $line .= 'b/' . esc_path($to->{'file'});
1601 return "<div class=\"diff header\">$line</div>\n";
1604 # format extended diff header line, before patch itself
1605 sub format_extended_diff_header_line {
1606 my $line = shift;
1607 my $diffinfo = shift;
1608 my ($from, $to) = @_;
1610 # match <path>
1611 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1612 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1613 esc_path($from->{'file'}));
1615 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1616 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1617 esc_path($to->{'file'}));
1619 # match single <mode>
1620 if ($line =~ m/\s(\d{6})$/) {
1621 $line .= '<span class="info"> (' .
1622 file_type_long($1) .
1623 ')</span>';
1625 # match <hash>
1626 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1627 # can match only for combined diff
1628 $line = 'index ';
1629 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1630 if ($from->{'href'}[$i]) {
1631 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1632 -class=>"hash"},
1633 substr($diffinfo->{'from_id'}[$i],0,7));
1634 } else {
1635 $line .= '0' x 7;
1637 # separator
1638 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1640 $line .= '..';
1641 if ($to->{'href'}) {
1642 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1643 substr($diffinfo->{'to_id'},0,7));
1644 } else {
1645 $line .= '0' x 7;
1648 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1649 # can match only for ordinary diff
1650 my ($from_link, $to_link);
1651 if ($from->{'href'}) {
1652 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1653 substr($diffinfo->{'from_id'},0,7));
1654 } else {
1655 $from_link = '0' x 7;
1657 if ($to->{'href'}) {
1658 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1659 substr($diffinfo->{'to_id'},0,7));
1660 } else {
1661 $to_link = '0' x 7;
1663 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1664 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1667 return $line . "<br/>\n";
1670 # format from-file/to-file diff header
1671 sub format_diff_from_to_header {
1672 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1673 my $line;
1674 my $result = '';
1676 $line = $from_line;
1677 #assert($line =~ m/^---/) if DEBUG;
1678 # no extra formatting for "^--- /dev/null"
1679 if (! $diffinfo->{'nparents'}) {
1680 # ordinary (single parent) diff
1681 if ($line =~ m!^--- "?a/!) {
1682 if ($from->{'href'}) {
1683 $line = '--- a/' .
1684 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1685 esc_path($from->{'file'}));
1686 } else {
1687 $line = '--- a/' .
1688 esc_path($from->{'file'});
1691 $result .= qq!<div class="diff from_file">$line</div>\n!;
1693 } else {
1694 # combined diff (merge commit)
1695 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1696 if ($from->{'href'}[$i]) {
1697 $line = '--- ' .
1698 $cgi->a({-href=>href(action=>"blobdiff",
1699 hash_parent=>$diffinfo->{'from_id'}[$i],
1700 hash_parent_base=>$parents[$i],
1701 file_parent=>$from->{'file'}[$i],
1702 hash=>$diffinfo->{'to_id'},
1703 hash_base=>$hash,
1704 file_name=>$to->{'file'}),
1705 -class=>"path",
1706 -title=>"diff" . ($i+1)},
1707 $i+1) .
1708 '/' .
1709 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1710 esc_path($from->{'file'}[$i]));
1711 } else {
1712 $line = '--- /dev/null';
1714 $result .= qq!<div class="diff from_file">$line</div>\n!;
1718 $line = $to_line;
1719 #assert($line =~ m/^\+\+\+/) if DEBUG;
1720 # no extra formatting for "^+++ /dev/null"
1721 if ($line =~ m!^\+\+\+ "?b/!) {
1722 if ($to->{'href'}) {
1723 $line = '+++ b/' .
1724 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1725 esc_path($to->{'file'}));
1726 } else {
1727 $line = '+++ b/' .
1728 esc_path($to->{'file'});
1731 $result .= qq!<div class="diff to_file">$line</div>\n!;
1733 return $result;
1736 # create note for patch simplified by combined diff
1737 sub format_diff_cc_simplified {
1738 my ($diffinfo, @parents) = @_;
1739 my $result = '';
1741 $result .= "<div class=\"diff header\">" .
1742 "diff --cc ";
1743 if (!is_deleted($diffinfo)) {
1744 $result .= $cgi->a({-href => href(action=>"blob",
1745 hash_base=>$hash,
1746 hash=>$diffinfo->{'to_id'},
1747 file_name=>$diffinfo->{'to_file'}),
1748 -class => "path"},
1749 esc_path($diffinfo->{'to_file'}));
1750 } else {
1751 $result .= esc_path($diffinfo->{'to_file'});
1753 $result .= "</div>\n" . # class="diff header"
1754 "<div class=\"diff nodifferences\">" .
1755 "Simple merge" .
1756 "</div>\n"; # class="diff nodifferences"
1758 return $result;
1761 # format patch (diff) line (not to be used for diff headers)
1762 sub format_diff_line {
1763 my $line = shift;
1764 my ($from, $to) = @_;
1765 my $diff_class = "";
1767 chomp $line;
1769 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1770 # combined diff
1771 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1772 if ($line =~ m/^\@{3}/) {
1773 $diff_class = " chunk_header";
1774 } elsif ($line =~ m/^\\/) {
1775 $diff_class = " incomplete";
1776 } elsif ($prefix =~ tr/+/+/) {
1777 $diff_class = " add";
1778 } elsif ($prefix =~ tr/-/-/) {
1779 $diff_class = " rem";
1781 } else {
1782 # assume ordinary diff
1783 my $char = substr($line, 0, 1);
1784 if ($char eq '+') {
1785 $diff_class = " add";
1786 } elsif ($char eq '-') {
1787 $diff_class = " rem";
1788 } elsif ($char eq '@') {
1789 $diff_class = " chunk_header";
1790 } elsif ($char eq "\\") {
1791 $diff_class = " incomplete";
1794 $line = untabify($line);
1795 if ($from && $to && $line =~ m/^\@{2} /) {
1796 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1797 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1799 $from_lines = 0 unless defined $from_lines;
1800 $to_lines = 0 unless defined $to_lines;
1802 if ($from->{'href'}) {
1803 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1804 -class=>"list"}, $from_text);
1806 if ($to->{'href'}) {
1807 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1808 -class=>"list"}, $to_text);
1810 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1811 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1812 return "<div class=\"diff$diff_class\">$line</div>\n";
1813 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1814 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1815 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1817 @from_text = split(' ', $ranges);
1818 for (my $i = 0; $i < @from_text; ++$i) {
1819 ($from_start[$i], $from_nlines[$i]) =
1820 (split(',', substr($from_text[$i], 1)), 0);
1823 $to_text = pop @from_text;
1824 $to_start = pop @from_start;
1825 $to_nlines = pop @from_nlines;
1827 $line = "<span class=\"chunk_info\">$prefix ";
1828 for (my $i = 0; $i < @from_text; ++$i) {
1829 if ($from->{'href'}[$i]) {
1830 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1831 -class=>"list"}, $from_text[$i]);
1832 } else {
1833 $line .= $from_text[$i];
1835 $line .= " ";
1837 if ($to->{'href'}) {
1838 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1839 -class=>"list"}, $to_text);
1840 } else {
1841 $line .= $to_text;
1843 $line .= " $prefix</span>" .
1844 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1845 return "<div class=\"diff$diff_class\">$line</div>\n";
1847 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1850 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1851 # linked. Pass the hash of the tree/commit to snapshot.
1852 sub format_snapshot_links {
1853 my ($hash) = @_;
1854 my $num_fmts = @snapshot_fmts;
1855 if ($num_fmts > 1) {
1856 # A parenthesized list of links bearing format names.
1857 # e.g. "snapshot (_tar.gz_ _zip_)"
1858 return "snapshot (" . join(' ', map
1859 $cgi->a({
1860 -href => href(
1861 action=>"snapshot",
1862 hash=>$hash,
1863 snapshot_format=>$_
1865 }, $known_snapshot_formats{$_}{'display'})
1866 , @snapshot_fmts) . ")";
1867 } elsif ($num_fmts == 1) {
1868 # A single "snapshot" link whose tooltip bears the format name.
1869 # i.e. "_snapshot_"
1870 my ($fmt) = @snapshot_fmts;
1871 return
1872 $cgi->a({
1873 -href => href(
1874 action=>"snapshot",
1875 hash=>$hash,
1876 snapshot_format=>$fmt
1878 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1879 }, "snapshot");
1880 } else { # $num_fmts == 0
1881 return undef;
1885 ## ......................................................................
1886 ## functions returning values to be passed, perhaps after some
1887 ## transformation, to other functions; e.g. returning arguments to href()
1889 # returns hash to be passed to href to generate gitweb URL
1890 # in -title key it returns description of link
1891 sub get_feed_info {
1892 my $format = shift || 'Atom';
1893 my %res = (action => lc($format));
1895 # feed links are possible only for project views
1896 return unless (defined $project);
1897 # some views should link to OPML, or to generic project feed,
1898 # or don't have specific feed yet (so they should use generic)
1899 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1901 my $branch;
1902 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1903 # from tag links; this also makes possible to detect branch links
1904 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1905 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1906 $branch = $1;
1908 # find log type for feed description (title)
1909 my $type = 'log';
1910 if (defined $file_name) {
1911 $type = "history of $file_name";
1912 $type .= "/" if ($action eq 'tree');
1913 $type .= " on '$branch'" if (defined $branch);
1914 } else {
1915 $type = "log of $branch" if (defined $branch);
1918 $res{-title} = $type;
1919 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1920 $res{'file_name'} = $file_name;
1922 return %res;
1925 ## ----------------------------------------------------------------------
1926 ## git utility subroutines, invoking git commands
1928 # returns path to the core git executable and the --git-dir parameter as list
1929 sub git_cmd {
1930 $number_of_git_cmds++;
1931 return $GIT, '--git-dir='.$git_dir;
1934 # quote the given arguments for passing them to the shell
1935 # quote_command("command", "arg 1", "arg with ' and ! characters")
1936 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1937 # Try to avoid using this function wherever possible.
1938 sub quote_command {
1939 return join(' ',
1940 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
1943 # get HEAD ref of given project as hash
1944 sub git_get_head_hash {
1945 return git_get_full_hash(shift, 'HEAD');
1948 sub git_get_full_hash {
1949 return git_get_hash(@_);
1952 sub git_get_short_hash {
1953 return git_get_hash(@_, '--short=7');
1956 sub git_get_hash {
1957 my ($project, $hash, @options) = @_;
1958 my $o_git_dir = $git_dir;
1959 my $retval = undef;
1960 $git_dir = "$projectroot/$project";
1961 if (open my $fd, '-|', git_cmd(), 'rev-parse',
1962 '--verify', '-q', @options, $hash) {
1963 $retval = <$fd>;
1964 chomp $retval if defined $retval;
1965 close $fd;
1967 if (defined $o_git_dir) {
1968 $git_dir = $o_git_dir;
1970 return $retval;
1973 # get type of given object
1974 sub git_get_type {
1975 my $hash = shift;
1977 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1978 my $type = <$fd>;
1979 close $fd or return;
1980 chomp $type;
1981 return $type;
1984 # repository configuration
1985 our $config_file = '';
1986 our %config;
1988 # store multiple values for single key as anonymous array reference
1989 # single values stored directly in the hash, not as [ <value> ]
1990 sub hash_set_multi {
1991 my ($hash, $key, $value) = @_;
1993 if (!exists $hash->{$key}) {
1994 $hash->{$key} = $value;
1995 } elsif (!ref $hash->{$key}) {
1996 $hash->{$key} = [ $hash->{$key}, $value ];
1997 } else {
1998 push @{$hash->{$key}}, $value;
2002 # return hash of git project configuration
2003 # optionally limited to some section, e.g. 'gitweb'
2004 sub git_parse_project_config {
2005 my $section_regexp = shift;
2006 my %config;
2008 local $/ = "\0";
2010 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2011 or return;
2013 while (my $keyval = <$fh>) {
2014 chomp $keyval;
2015 my ($key, $value) = split(/\n/, $keyval, 2);
2017 hash_set_multi(\%config, $key, $value)
2018 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2020 close $fh;
2022 return %config;
2025 # convert config value to boolean: 'true' or 'false'
2026 # no value, number > 0, 'true' and 'yes' values are true
2027 # rest of values are treated as false (never as error)
2028 sub config_to_bool {
2029 my $val = shift;
2031 return 1 if !defined $val; # section.key
2033 # strip leading and trailing whitespace
2034 $val =~ s/^\s+//;
2035 $val =~ s/\s+$//;
2037 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2038 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2041 # convert config value to simple decimal number
2042 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2043 # to be multiplied by 1024, 1048576, or 1073741824
2044 sub config_to_int {
2045 my $val = shift;
2047 # strip leading and trailing whitespace
2048 $val =~ s/^\s+//;
2049 $val =~ s/\s+$//;
2051 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2052 $unit = lc($unit);
2053 # unknown unit is treated as 1
2054 return $num * ($unit eq 'g' ? 1073741824 :
2055 $unit eq 'm' ? 1048576 :
2056 $unit eq 'k' ? 1024 : 1);
2058 return $val;
2061 # convert config value to array reference, if needed
2062 sub config_to_multi {
2063 my $val = shift;
2065 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2068 sub git_get_project_config {
2069 my ($key, $type) = @_;
2071 return unless defined $git_dir;
2073 # key sanity check
2074 return unless ($key);
2075 $key =~ s/^gitweb\.//;
2076 return if ($key =~ m/\W/);
2078 # type sanity check
2079 if (defined $type) {
2080 $type =~ s/^--//;
2081 $type = undef
2082 unless ($type eq 'bool' || $type eq 'int');
2085 # get config
2086 if (!defined $config_file ||
2087 $config_file ne "$git_dir/config") {
2088 %config = git_parse_project_config('gitweb');
2089 $config_file = "$git_dir/config";
2092 # check if config variable (key) exists
2093 return unless exists $config{"gitweb.$key"};
2095 # ensure given type
2096 if (!defined $type) {
2097 return $config{"gitweb.$key"};
2098 } elsif ($type eq 'bool') {
2099 # backward compatibility: 'git config --bool' returns true/false
2100 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2101 } elsif ($type eq 'int') {
2102 return config_to_int($config{"gitweb.$key"});
2104 return $config{"gitweb.$key"};
2107 # get hash of given path at given ref
2108 sub git_get_hash_by_path {
2109 my $base = shift;
2110 my $path = shift || return undef;
2111 my $type = shift;
2113 $path =~ s,/+$,,;
2115 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2116 or die_error(500, "Open git-ls-tree failed");
2117 my $line = <$fd>;
2118 close $fd or return undef;
2120 if (!defined $line) {
2121 # there is no tree or hash given by $path at $base
2122 return undef;
2125 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2126 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2127 if (defined $type && $type ne $2) {
2128 # type doesn't match
2129 return undef;
2131 return $3;
2134 # get path of entry with given hash at given tree-ish (ref)
2135 # used to get 'from' filename for combined diff (merge commit) for renames
2136 sub git_get_path_by_hash {
2137 my $base = shift || return;
2138 my $hash = shift || return;
2140 local $/ = "\0";
2142 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2143 or return undef;
2144 while (my $line = <$fd>) {
2145 chomp $line;
2147 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2148 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2149 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2150 close $fd;
2151 return $1;
2154 close $fd;
2155 return undef;
2158 ## ......................................................................
2159 ## git utility functions, directly accessing git repository
2161 sub git_get_project_description {
2162 my $path = shift;
2164 $git_dir = "$projectroot/$path";
2165 open my $fd, '<', "$git_dir/description"
2166 or return git_get_project_config('description');
2167 my $descr = <$fd>;
2168 close $fd;
2169 if (defined $descr) {
2170 chomp $descr;
2172 return $descr;
2175 sub git_get_project_ctags {
2176 my $path = shift;
2177 my $ctags = {};
2179 $git_dir = "$projectroot/$path";
2180 opendir my $dh, "$git_dir/ctags"
2181 or return $ctags;
2182 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2183 open my $ct, '<', $_ or next;
2184 my $val = <$ct>;
2185 chomp $val;
2186 close $ct;
2187 my $ctag = $_; $ctag =~ s#.*/##;
2188 $ctags->{$ctag} = $val;
2190 closedir $dh;
2191 $ctags;
2194 sub git_populate_project_tagcloud {
2195 my $ctags = shift;
2197 # First, merge different-cased tags; tags vote on casing
2198 my %ctags_lc;
2199 foreach (keys %$ctags) {
2200 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2201 if (not $ctags_lc{lc $_}->{topcount}
2202 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2203 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2204 $ctags_lc{lc $_}->{topname} = $_;
2208 my $cloud;
2209 if (eval { require HTML::TagCloud; 1; }) {
2210 $cloud = HTML::TagCloud->new;
2211 foreach (sort keys %ctags_lc) {
2212 # Pad the title with spaces so that the cloud looks
2213 # less crammed.
2214 my $title = $ctags_lc{$_}->{topname};
2215 $title =~ s/ /&nbsp;/g;
2216 $title =~ s/^/&nbsp;/g;
2217 $title =~ s/$/&nbsp;/g;
2218 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2220 } else {
2221 $cloud = \%ctags_lc;
2223 $cloud;
2226 sub git_show_project_tagcloud {
2227 my ($cloud, $count) = @_;
2228 print STDERR ref($cloud)."..\n";
2229 if (ref $cloud eq 'HTML::TagCloud') {
2230 return $cloud->html_and_css($count);
2231 } else {
2232 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2233 return '<p align="center">' . join (', ', map {
2234 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2235 } splice(@tags, 0, $count)) . '</p>';
2239 sub git_get_project_url_list {
2240 my $path = shift;
2242 $git_dir = "$projectroot/$path";
2243 open my $fd, '<', "$git_dir/cloneurl"
2244 or return wantarray ?
2245 @{ config_to_multi(git_get_project_config('url')) } :
2246 config_to_multi(git_get_project_config('url'));
2247 my @git_project_url_list = map { chomp; $_ } <$fd>;
2248 close $fd;
2250 return wantarray ? @git_project_url_list : \@git_project_url_list;
2253 sub git_get_projects_list {
2254 my ($filter) = @_;
2255 my @list;
2257 $filter ||= '';
2258 $filter =~ s/\.git$//;
2260 my $check_forks = gitweb_check_feature('forks');
2262 if (-d $projects_list) {
2263 # search in directory
2264 my $dir = $projects_list . ($filter ? "/$filter" : '');
2265 # remove the trailing "/"
2266 $dir =~ s!/+$!!;
2267 my $pfxlen = length("$dir");
2268 my $pfxdepth = ($dir =~ tr!/!!);
2270 File::Find::find({
2271 follow_fast => 1, # follow symbolic links
2272 follow_skip => 2, # ignore duplicates
2273 dangling_symlinks => 0, # ignore dangling symlinks, silently
2274 wanted => sub {
2275 # global variables
2276 our $project_maxdepth;
2277 our $projectroot;
2278 # skip project-list toplevel, if we get it.
2279 return if (m!^[/.]$!);
2280 # only directories can be git repositories
2281 return unless (-d $_);
2282 # don't traverse too deep (Find is super slow on os x)
2283 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2284 $File::Find::prune = 1;
2285 return;
2288 my $subdir = substr($File::Find::name, $pfxlen + 1);
2289 # we check related file in $projectroot
2290 my $path = ($filter ? "$filter/" : '') . $subdir;
2291 if (check_export_ok("$projectroot/$path")) {
2292 push @list, { path => $path };
2293 $File::Find::prune = 1;
2296 }, "$dir");
2298 } elsif (-f $projects_list) {
2299 # read from file(url-encoded):
2300 # 'git%2Fgit.git Linus+Torvalds'
2301 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2302 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2303 my %paths;
2304 open my $fd, '<', $projects_list or return;
2305 PROJECT:
2306 while (my $line = <$fd>) {
2307 chomp $line;
2308 my ($path, $owner) = split ' ', $line;
2309 $path = unescape($path);
2310 $owner = unescape($owner);
2311 if (!defined $path) {
2312 next;
2314 if ($filter ne '') {
2315 # looking for forks;
2316 my $pfx = substr($path, 0, length($filter));
2317 if ($pfx ne $filter) {
2318 next PROJECT;
2320 my $sfx = substr($path, length($filter));
2321 if ($sfx !~ /^\/.*\.git$/) {
2322 next PROJECT;
2324 } elsif ($check_forks) {
2325 PATH:
2326 foreach my $filter (keys %paths) {
2327 # looking for forks;
2328 my $pfx = substr($path, 0, length($filter));
2329 if ($pfx ne $filter) {
2330 next PATH;
2332 my $sfx = substr($path, length($filter));
2333 if ($sfx !~ /^\/.*\.git$/) {
2334 next PATH;
2336 # is a fork, don't include it in
2337 # the list
2338 next PROJECT;
2341 if (check_export_ok("$projectroot/$path")) {
2342 my $pr = {
2343 path => $path,
2344 owner => to_utf8($owner),
2346 push @list, $pr;
2347 (my $forks_path = $path) =~ s/\.git$//;
2348 $paths{$forks_path}++;
2351 close $fd;
2353 return @list;
2356 our $gitweb_project_owner = undef;
2357 sub git_get_project_list_from_file {
2359 return if (defined $gitweb_project_owner);
2361 $gitweb_project_owner = {};
2362 # read from file (url-encoded):
2363 # 'git%2Fgit.git Linus+Torvalds'
2364 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2365 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2366 if (-f $projects_list) {
2367 open(my $fd, '<', $projects_list);
2368 while (my $line = <$fd>) {
2369 chomp $line;
2370 my ($pr, $ow) = split ' ', $line;
2371 $pr = unescape($pr);
2372 $ow = unescape($ow);
2373 $gitweb_project_owner->{$pr} = to_utf8($ow);
2375 close $fd;
2379 sub git_get_project_owner {
2380 my $project = shift;
2381 my $owner;
2383 return undef unless $project;
2384 $git_dir = "$projectroot/$project";
2386 if (!defined $gitweb_project_owner) {
2387 git_get_project_list_from_file();
2390 if (exists $gitweb_project_owner->{$project}) {
2391 $owner = $gitweb_project_owner->{$project};
2393 if (!defined $owner){
2394 $owner = git_get_project_config('owner');
2396 if (!defined $owner) {
2397 $owner = get_file_owner("$git_dir");
2400 return $owner;
2403 sub git_get_last_activity {
2404 my ($path) = @_;
2405 my $fd;
2407 $git_dir = "$projectroot/$path";
2408 open($fd, "-|", git_cmd(), 'for-each-ref',
2409 '--format=%(committer)',
2410 '--sort=-committerdate',
2411 '--count=1',
2412 'refs/heads') or return;
2413 my $most_recent = <$fd>;
2414 close $fd or return;
2415 if (defined $most_recent &&
2416 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2417 my $timestamp = $1;
2418 my $age = time - $timestamp;
2419 return ($age, age_string($age));
2421 return (undef, undef);
2424 sub git_get_references {
2425 my $type = shift || "";
2426 my %refs;
2427 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2428 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2429 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2430 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2431 or return;
2433 while (my $line = <$fd>) {
2434 chomp $line;
2435 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2436 if (defined $refs{$1}) {
2437 push @{$refs{$1}}, $2;
2438 } else {
2439 $refs{$1} = [ $2 ];
2443 close $fd or return;
2444 return \%refs;
2447 sub git_get_rev_name_tags {
2448 my $hash = shift || return undef;
2450 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2451 or return;
2452 my $name_rev = <$fd>;
2453 close $fd;
2455 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2456 return $1;
2457 } else {
2458 # catches also '$hash undefined' output
2459 return undef;
2463 ## ----------------------------------------------------------------------
2464 ## parse to hash functions
2466 sub parse_date {
2467 my $epoch = shift;
2468 my $tz = shift || "-0000";
2470 my %date;
2471 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2472 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2473 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2474 $date{'hour'} = $hour;
2475 $date{'minute'} = $min;
2476 $date{'mday'} = $mday;
2477 $date{'day'} = $days[$wday];
2478 $date{'month'} = $months[$mon];
2479 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2480 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2481 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2482 $mday, $months[$mon], $hour ,$min;
2483 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2484 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2486 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2487 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2488 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2489 $date{'hour_local'} = $hour;
2490 $date{'minute_local'} = $min;
2491 $date{'tz_local'} = $tz;
2492 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2493 1900+$year, $mon+1, $mday,
2494 $hour, $min, $sec, $tz);
2495 return %date;
2498 sub parse_tag {
2499 my $tag_id = shift;
2500 my %tag;
2501 my @comment;
2503 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2504 $tag{'id'} = $tag_id;
2505 while (my $line = <$fd>) {
2506 chomp $line;
2507 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2508 $tag{'object'} = $1;
2509 } elsif ($line =~ m/^type (.+)$/) {
2510 $tag{'type'} = $1;
2511 } elsif ($line =~ m/^tag (.+)$/) {
2512 $tag{'name'} = $1;
2513 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2514 $tag{'author'} = $1;
2515 $tag{'author_epoch'} = $2;
2516 $tag{'author_tz'} = $3;
2517 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2518 $tag{'author_name'} = $1;
2519 $tag{'author_email'} = $2;
2520 } else {
2521 $tag{'author_name'} = $tag{'author'};
2523 } elsif ($line =~ m/--BEGIN/) {
2524 push @comment, $line;
2525 last;
2526 } elsif ($line eq "") {
2527 last;
2530 push @comment, <$fd>;
2531 $tag{'comment'} = \@comment;
2532 close $fd or return;
2533 if (!defined $tag{'name'}) {
2534 return
2536 return %tag
2539 sub parse_commit_text {
2540 my ($commit_text, $withparents) = @_;
2541 my @commit_lines = split '\n', $commit_text;
2542 my %co;
2544 pop @commit_lines; # Remove '\0'
2546 if (! @commit_lines) {
2547 return;
2550 my $header = shift @commit_lines;
2551 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2552 return;
2554 ($co{'id'}, my @parents) = split ' ', $header;
2555 while (my $line = shift @commit_lines) {
2556 last if $line eq "\n";
2557 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2558 $co{'tree'} = $1;
2559 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2560 push @parents, $1;
2561 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2562 $co{'author'} = to_utf8($1);
2563 $co{'author_epoch'} = $2;
2564 $co{'author_tz'} = $3;
2565 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2566 $co{'author_name'} = $1;
2567 $co{'author_email'} = $2;
2568 } else {
2569 $co{'author_name'} = $co{'author'};
2571 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2572 $co{'committer'} = to_utf8($1);
2573 $co{'committer_epoch'} = $2;
2574 $co{'committer_tz'} = $3;
2575 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2576 $co{'committer_name'} = $1;
2577 $co{'committer_email'} = $2;
2578 } else {
2579 $co{'committer_name'} = $co{'committer'};
2583 if (!defined $co{'tree'}) {
2584 return;
2586 $co{'parents'} = \@parents;
2587 $co{'parent'} = $parents[0];
2589 foreach my $title (@commit_lines) {
2590 $title =~ s/^ //;
2591 if ($title ne "") {
2592 $co{'title'} = chop_str($title, 80, 5);
2593 # remove leading stuff of merges to make the interesting part visible
2594 if (length($title) > 50) {
2595 $title =~ s/^Automatic //;
2596 $title =~ s/^merge (of|with) /Merge ... /i;
2597 if (length($title) > 50) {
2598 $title =~ s/(http|rsync):\/\///;
2600 if (length($title) > 50) {
2601 $title =~ s/(master|www|rsync)\.//;
2603 if (length($title) > 50) {
2604 $title =~ s/kernel.org:?//;
2606 if (length($title) > 50) {
2607 $title =~ s/\/pub\/scm//;
2610 $co{'title_short'} = chop_str($title, 50, 5);
2611 last;
2614 if (! defined $co{'title'} || $co{'title'} eq "") {
2615 $co{'title'} = $co{'title_short'} = '(no commit message)';
2617 # remove added spaces
2618 foreach my $line (@commit_lines) {
2619 $line =~ s/^ //;
2621 $co{'comment'} = \@commit_lines;
2623 my $age = time - $co{'committer_epoch'};
2624 $co{'age'} = $age;
2625 $co{'age_string'} = age_string($age);
2626 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2627 if ($age > 60*60*24*7*2) {
2628 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2629 $co{'age_string_age'} = $co{'age_string'};
2630 } else {
2631 $co{'age_string_date'} = $co{'age_string'};
2632 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2634 return %co;
2637 sub parse_commit {
2638 my ($commit_id) = @_;
2639 my %co;
2641 local $/ = "\0";
2643 open my $fd, "-|", git_cmd(), "rev-list",
2644 "--parents",
2645 "--header",
2646 "--max-count=1",
2647 $commit_id,
2648 "--",
2649 or die_error(500, "Open git-rev-list failed");
2650 %co = parse_commit_text(<$fd>, 1);
2651 close $fd;
2653 return %co;
2656 sub parse_commits {
2657 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2658 my @cos;
2660 $maxcount ||= 1;
2661 $skip ||= 0;
2663 local $/ = "\0";
2665 open my $fd, "-|", git_cmd(), "rev-list",
2666 "--header",
2667 @args,
2668 ("--max-count=" . $maxcount),
2669 ("--skip=" . $skip),
2670 @extra_options,
2671 $commit_id,
2672 "--",
2673 ($filename ? ($filename) : ())
2674 or die_error(500, "Open git-rev-list failed");
2675 while (my $line = <$fd>) {
2676 my %co = parse_commit_text($line);
2677 push @cos, \%co;
2679 close $fd;
2681 return wantarray ? @cos : \@cos;
2684 # parse line of git-diff-tree "raw" output
2685 sub parse_difftree_raw_line {
2686 my $line = shift;
2687 my %res;
2689 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2690 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2691 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2692 $res{'from_mode'} = $1;
2693 $res{'to_mode'} = $2;
2694 $res{'from_id'} = $3;
2695 $res{'to_id'} = $4;
2696 $res{'status'} = $5;
2697 $res{'similarity'} = $6;
2698 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2699 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2700 } else {
2701 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2704 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2705 # combined diff (for merge commit)
2706 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2707 $res{'nparents'} = length($1);
2708 $res{'from_mode'} = [ split(' ', $2) ];
2709 $res{'to_mode'} = pop @{$res{'from_mode'}};
2710 $res{'from_id'} = [ split(' ', $3) ];
2711 $res{'to_id'} = pop @{$res{'from_id'}};
2712 $res{'status'} = [ split('', $4) ];
2713 $res{'to_file'} = unquote($5);
2715 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2716 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2717 $res{'commit'} = $1;
2720 return wantarray ? %res : \%res;
2723 # wrapper: return parsed line of git-diff-tree "raw" output
2724 # (the argument might be raw line, or parsed info)
2725 sub parsed_difftree_line {
2726 my $line_or_ref = shift;
2728 if (ref($line_or_ref) eq "HASH") {
2729 # pre-parsed (or generated by hand)
2730 return $line_or_ref;
2731 } else {
2732 return parse_difftree_raw_line($line_or_ref);
2736 # parse line of git-ls-tree output
2737 sub parse_ls_tree_line {
2738 my $line = shift;
2739 my %opts = @_;
2740 my %res;
2742 if ($opts{'-l'}) {
2743 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
2744 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
2746 $res{'mode'} = $1;
2747 $res{'type'} = $2;
2748 $res{'hash'} = $3;
2749 $res{'size'} = $4;
2750 if ($opts{'-z'}) {
2751 $res{'name'} = $5;
2752 } else {
2753 $res{'name'} = unquote($5);
2755 } else {
2756 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2757 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2759 $res{'mode'} = $1;
2760 $res{'type'} = $2;
2761 $res{'hash'} = $3;
2762 if ($opts{'-z'}) {
2763 $res{'name'} = $4;
2764 } else {
2765 $res{'name'} = unquote($4);
2769 return wantarray ? %res : \%res;
2772 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2773 sub parse_from_to_diffinfo {
2774 my ($diffinfo, $from, $to, @parents) = @_;
2776 if ($diffinfo->{'nparents'}) {
2777 # combined diff
2778 $from->{'file'} = [];
2779 $from->{'href'} = [];
2780 fill_from_file_info($diffinfo, @parents)
2781 unless exists $diffinfo->{'from_file'};
2782 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2783 $from->{'file'}[$i] =
2784 defined $diffinfo->{'from_file'}[$i] ?
2785 $diffinfo->{'from_file'}[$i] :
2786 $diffinfo->{'to_file'};
2787 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2788 $from->{'href'}[$i] = href(action=>"blob",
2789 hash_base=>$parents[$i],
2790 hash=>$diffinfo->{'from_id'}[$i],
2791 file_name=>$from->{'file'}[$i]);
2792 } else {
2793 $from->{'href'}[$i] = undef;
2796 } else {
2797 # ordinary (not combined) diff
2798 $from->{'file'} = $diffinfo->{'from_file'};
2799 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2800 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2801 hash=>$diffinfo->{'from_id'},
2802 file_name=>$from->{'file'});
2803 } else {
2804 delete $from->{'href'};
2808 $to->{'file'} = $diffinfo->{'to_file'};
2809 if (!is_deleted($diffinfo)) { # file exists in result
2810 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2811 hash=>$diffinfo->{'to_id'},
2812 file_name=>$to->{'file'});
2813 } else {
2814 delete $to->{'href'};
2818 ## ......................................................................
2819 ## parse to array of hashes functions
2821 sub git_get_heads_list {
2822 my $limit = shift;
2823 my @headslist;
2825 open my $fd, '-|', git_cmd(), 'for-each-ref',
2826 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2827 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2828 'refs/heads'
2829 or return;
2830 while (my $line = <$fd>) {
2831 my %ref_item;
2833 chomp $line;
2834 my ($refinfo, $committerinfo) = split(/\0/, $line);
2835 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2836 my ($committer, $epoch, $tz) =
2837 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2838 $ref_item{'fullname'} = $name;
2839 $name =~ s!^refs/heads/!!;
2841 $ref_item{'name'} = $name;
2842 $ref_item{'id'} = $hash;
2843 $ref_item{'title'} = $title || '(no commit message)';
2844 $ref_item{'epoch'} = $epoch;
2845 if ($epoch) {
2846 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2847 } else {
2848 $ref_item{'age'} = "unknown";
2851 push @headslist, \%ref_item;
2853 close $fd;
2855 return wantarray ? @headslist : \@headslist;
2858 sub git_get_tags_list {
2859 my $limit = shift;
2860 my @tagslist;
2862 open my $fd, '-|', git_cmd(), 'for-each-ref',
2863 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2864 '--format=%(objectname) %(objecttype) %(refname) '.
2865 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2866 'refs/tags'
2867 or return;
2868 while (my $line = <$fd>) {
2869 my %ref_item;
2871 chomp $line;
2872 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2873 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2874 my ($creator, $epoch, $tz) =
2875 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2876 $ref_item{'fullname'} = $name;
2877 $name =~ s!^refs/tags/!!;
2879 $ref_item{'type'} = $type;
2880 $ref_item{'id'} = $id;
2881 $ref_item{'name'} = $name;
2882 if ($type eq "tag") {
2883 $ref_item{'subject'} = $title;
2884 $ref_item{'reftype'} = $reftype;
2885 $ref_item{'refid'} = $refid;
2886 } else {
2887 $ref_item{'reftype'} = $type;
2888 $ref_item{'refid'} = $id;
2891 if ($type eq "tag" || $type eq "commit") {
2892 $ref_item{'epoch'} = $epoch;
2893 if ($epoch) {
2894 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2895 } else {
2896 $ref_item{'age'} = "unknown";
2900 push @tagslist, \%ref_item;
2902 close $fd;
2904 return wantarray ? @tagslist : \@tagslist;
2907 ## ----------------------------------------------------------------------
2908 ## filesystem-related functions
2910 sub get_file_owner {
2911 my $path = shift;
2913 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2914 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2915 if (!defined $gcos) {
2916 return undef;
2918 my $owner = $gcos;
2919 $owner =~ s/[,;].*$//;
2920 return to_utf8($owner);
2923 # assume that file exists
2924 sub insert_file {
2925 my $filename = shift;
2927 my $output = "";
2929 open my $fd, '<', $filename;
2930 my @toutf8 = map { to_utf8($_) } <$fd>;
2931 foreach( @toutf8 ){
2932 $output .= $_;
2934 close $fd;
2936 return $output;
2939 ## ......................................................................
2940 ## mimetype related functions
2942 sub mimetype_guess_file {
2943 my $filename = shift;
2944 my $mimemap = shift;
2945 -r $mimemap or return undef;
2947 my %mimemap;
2948 open(my $mh, '<', $mimemap) or return undef;
2949 while (<$mh>) {
2950 next if m/^#/; # skip comments
2951 my ($mimetype, $exts) = split(/\t+/);
2952 if (defined $exts) {
2953 my @exts = split(/\s+/, $exts);
2954 foreach my $ext (@exts) {
2955 $mimemap{$ext} = $mimetype;
2959 close($mh);
2961 $filename =~ /\.([^.]*)$/;
2962 return $mimemap{$1};
2965 sub mimetype_guess {
2966 my $filename = shift;
2967 my $mime;
2968 $filename =~ /\./ or return undef;
2970 if ($mimetypes_file) {
2971 my $file = $mimetypes_file;
2972 if ($file !~ m!^/!) { # if it is relative path
2973 # it is relative to project
2974 $file = "$projectroot/$project/$file";
2976 $mime = mimetype_guess_file($filename, $file);
2978 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2979 return $mime;
2982 sub blob_mimetype {
2983 my $fd = shift;
2984 my $filename = shift;
2986 if ($filename) {
2987 my $mime = mimetype_guess($filename);
2988 $mime and return $mime;
2991 # just in case
2992 return $default_blob_plain_mimetype unless $fd;
2994 if (-T $fd) {
2995 return 'text/plain';
2996 } elsif (! $filename) {
2997 return 'application/octet-stream';
2998 } elsif ($filename =~ m/\.png$/i) {
2999 return 'image/png';
3000 } elsif ($filename =~ m/\.gif$/i) {
3001 return 'image/gif';
3002 } elsif ($filename =~ m/\.jpe?g$/i) {
3003 return 'image/jpeg';
3004 } else {
3005 return 'application/octet-stream';
3009 sub blob_contenttype {
3010 my ($fd, $file_name, $type) = @_;
3012 $type ||= blob_mimetype($fd, $file_name);
3013 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3014 $type .= "; charset=$default_text_plain_charset";
3017 return $type;
3020 # guess file syntax for syntax highlighting; return undef if no highlighting
3021 # the name of syntax can (in the future) depend on syntax highlighter used
3022 sub guess_file_syntax {
3023 my ($highlight, $mimetype, $file_name) = @_;
3024 return undef unless ($highlight && defined $file_name);
3025 my $basename = basename($file_name, '.in');
3026 return $highlight_basename{$basename}
3027 if exists $highlight_basename{$basename};
3029 $basename =~ /\.([^.]*)$/;
3030 my $ext = $1 or return undef;
3031 return $highlight_ext{$ext}
3032 if exists $highlight_ext{$ext};
3034 return undef;
3037 # run highlighter and return FD of its output,
3038 # or return original FD if no highlighting
3039 sub run_highlighter {
3040 my ($fd, $highlight, $syntax) = @_;
3041 return $fd unless ($highlight && defined $syntax);
3043 close $fd
3044 or die_error(404, "Reading blob failed");
3045 open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3046 quote_command($highlight_bin).
3047 " --xhtml --fragment --syntax $syntax |"
3048 or die_error(500, "Couldn't open file or run syntax highlighter");
3049 return $fd;
3052 ## ======================================================================
3053 ## functions printing HTML: header, footer, error page
3055 sub get_page_title {
3056 my $title = to_utf8($site_name);
3058 return $title unless (defined $project);
3059 $title .= " - " . to_utf8($project);
3061 return $title unless (defined $action);
3062 $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3064 return $title unless (defined $file_name);
3065 $title .= " - " . esc_path($file_name);
3066 if ($action eq "tree" && $file_name !~ m|/$|) {
3067 $title .= "/";
3070 return $title;
3073 sub git_header_html {
3074 my $status = shift || "200 OK";
3075 my $expires = shift;
3077 my $output = "";
3078 my %opts = @_;
3080 my $title = get_page_title();
3081 my $content_type;
3082 # require explicit support from the UA if we are to send the page as
3083 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3084 # we have to do this because MSIE sometimes globs '*/*', pretending to
3085 # support xhtml+xml but choking when it gets what it asked for.
3086 #if (defined $::cgi->http('HTTP_ACCEPT') &&
3087 # $::cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3088 # $::cgi->Accept('application/xhtml+xml') != 0) {
3089 # $content_type = 'application/xhtml+xml';
3090 #} else {
3091 $content_type = 'text/html';
3093 $output .= $cgi->header(-type=>$content_type, -charset => 'utf-8',
3094 -status=> $status, -expires => $expires)
3095 unless ($opts{'-no_http_header'});
3096 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3097 $output .= <<EOF;
3098 <?xml version="1.0" encoding="utf-8"?>
3099 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3100 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3101 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3102 <!-- git core binaries version $git_version -->
3103 <head>
3104 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3105 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3106 <meta name="robots" content="index, nofollow"/>
3107 <title>$title</title>
3109 if($headerRefresh){
3110 $output .= "<meta http-equiv=\"refresh\" content=\"1\"/>\n";
3113 # the stylesheet, favicon etc urls won't work correctly with path_info
3114 # unless we set the appropriate base URL
3115 if ($ENV{'PATH_INFO'}) {
3116 $output .= "<base href=\"".esc_url($base_url)."\" />\n";
3118 # print out each stylesheet that exist, providing backwards capability
3119 # for those people who defined $stylesheet in a config file
3120 if (defined $stylesheet) {
3121 $output .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3122 } else {
3123 foreach my $stylesheet (@stylesheets) {
3124 next unless $stylesheet;
3125 $output .= '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3128 if (defined $project) {
3129 my %href_params = get_feed_info();
3130 if (!exists $href_params{'-title'}) {
3131 $href_params{'-title'} = 'log';
3134 foreach my $format qw(RSS Atom) {
3135 my $type = lc($format);
3136 my %link_attr = (
3137 '-rel' => 'alternate',
3138 '-title' => "$project - $href_params{'-title'} - $format feed",
3139 '-type' => "application/$type+xml"
3142 $href_params{'action'} = $type;
3143 $link_attr{'-href'} = href(%href_params);
3144 $output .= "<link ".
3145 "rel=\"$link_attr{'-rel'}\" ".
3146 "title=\"$link_attr{'-title'}\" ".
3147 "href=\"$link_attr{'-href'}\" ".
3148 "type=\"$link_attr{'-type'}\" ".
3149 "/>\n";
3151 $href_params{'extra_options'} = '--no-merges';
3152 $link_attr{'-href'} = href(%href_params);
3153 $link_attr{'-title'} .= ' (no merges)';
3154 $output .= "<link ".
3155 "rel=\"$link_attr{'-rel'}\" ".
3156 "title=\"$link_attr{'-title'}\" ".
3157 "href=\"$link_attr{'-href'}\" ".
3158 "type=\"$link_attr{'-type'}\" ".
3159 "/>\n";
3162 } else {
3163 $output .= sprintf('<link rel="alternate" title="%s projects list" '.
3164 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3165 $site_name, href(project=>undef, action=>"project_index"));
3166 $output .= sprintf('<link rel="alternate" title="%s projects feeds" '.
3167 'href="%s" type="text/x-opml" />'."\n",
3168 $site_name, href(project=>undef, action=>"opml"));
3170 if (defined $favicon) {
3171 $output .= qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3174 $output .= "</head>\n" .
3175 "<body>\n";
3177 if (defined $site_header && -f $site_header) {
3178 $output .= insert_file($site_header);
3181 $output .= "<div class=\"page_header\">\n" .
3182 $cgi->a({-href => esc_url($logo_url),
3183 -title => $logo_label},
3184 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3185 $output .= $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3186 if (defined $project) {
3187 $output .= $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3188 if (defined $action) {
3189 $output .= " / $action";
3191 $output .= "\n";
3193 $output .= "</div>\n";
3195 my $have_search = gitweb_check_feature('search');
3196 if (defined $project && $have_search) {
3197 if (!defined $searchtext) {
3198 $searchtext = "";
3200 my $search_hash;
3201 if (defined $hash_base) {
3202 $search_hash = $hash_base;
3203 } elsif (defined $hash) {
3204 $search_hash = $hash;
3205 } else {
3206 $search_hash = "HEAD";
3208 my $action = $my_uri;
3209 my $use_pathinfo = gitweb_check_feature('pathinfo');
3210 if ($use_pathinfo) {
3211 $action .= "/".esc_url($project);
3213 $output .= $cgi->startform(-method => "get", -action => $action) .
3214 "<div class=\"search\">\n" .
3215 (!$use_pathinfo &&
3216 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3217 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3218 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3219 $cgi->popup_menu(-name => 'st', -default => 'commit',
3220 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3221 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3222 " search:\n".
3223 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3224 "<span title=\"Extended regular expression\">" .
3225 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3226 -checked => $search_use_regexp) .
3227 "</span>" .
3228 "</div>" .
3229 $cgi->end_form() . "\n";
3231 $output .= "</div>\n";
3233 return $output;
3236 sub git_footer_html {
3237 my $output;
3239 my $feed_class = 'rss_logo';
3240 $output .= "<div class=\"page_footer\">\n";
3241 $output .= "<div class=\"cachetime\">Cache Last Updated: ". gmtime( time ) ." GMT</div>\n";
3242 if (defined $project) {
3243 my $descr = git_get_project_description($project);
3244 if (defined $descr) {
3245 $output .= "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3248 my %href_params = get_feed_info();
3249 if (!%href_params) {
3250 $feed_class .= ' generic';
3252 $href_params{'-title'} ||= 'log';
3254 foreach my $format qw(RSS Atom) {
3255 $href_params{'action'} = lc($format);
3256 $output .= $cgi->a({-href => href(%href_params),
3257 -title => "$href_params{'-title'} $format feed",
3258 -class => $feed_class}, $format)."\n";
3261 } else {
3262 $output .= $cgi->a({-href => href(project=>undef, action=>"opml"),
3263 -class => $feed_class}, "OPML") . " ";
3264 $output .= $cgi->a({-href => href(project=>undef, action=>"project_index"),
3265 -class => $feed_class}, "TXT") . "\n";
3267 $output .= "</div>\n"; # class="page_footer"
3269 if (defined $t0 && gitweb_check_feature('timed')) {
3270 $output .= "<div id=\"generating_info\">\n";
3271 $output .= 'This page took '.
3272 '<span id="generating_time" class="time_span">'.
3273 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
3274 ' seconds </span>'.
3275 ' and '.
3276 '<span id="generating_cmd">'.
3277 $number_of_git_cmds.
3278 '</span> git commands '.
3279 " to generate.\n";
3280 $output .= "</div>\n"; # class="page_footer"
3283 if (defined $site_footer && -f $site_footer) {
3284 $output .= insert_file($site_footer);
3287 $output .= qq!<script type="text/javascript" src="$javascript"></script>\n!;
3288 if (defined $action &&
3289 $action eq 'blame_incremental') {
3290 $output .= qq!<script type="text/javascript">\n!.
3291 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3292 qq! "!. href() .qq!");\n!.
3293 qq!</script>\n!;
3294 } elsif (gitweb_check_feature('javascript-actions')) {
3295 $output .= qq!<script type="text/javascript">\n!.
3296 qq!window.onload = fixLinks;\n!.
3297 qq!</script>\n!;
3300 $output .= "</body>\n" .
3301 "</html>";
3304 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3305 # Example: die_error(404, 'Hash not found')
3306 # By convention, use the following status codes (as defined in RFC 2616):
3307 # 400: Invalid or missing CGI parameters, or
3308 # requested object exists but has wrong type.
3309 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3310 # this server or project.
3311 # 404: Requested object/revision/project doesn't exist.
3312 # 500: The server isn't configured properly, or
3313 # an internal error occurred (e.g. failed assertions caused by bugs), or
3314 # an unknown error occurred (e.g. the git binary died unexpectedly).
3315 # 503: The server is currently unavailable (because it is overloaded,
3316 # or down for maintenance). Generally, this is a temporary state.
3317 sub die_error {
3318 my $status = shift || 500;
3319 my $error = esc_html(shift) || "Internal Server Error";
3320 my $extra = shift;
3321 my %opts = @_;
3323 my %http_responses = (
3324 400 => '400 Bad Request',
3325 403 => '403 Forbidden',
3326 404 => '404 Not Found',
3327 500 => '500 Internal Server Error',
3328 503 => '503 Service Unavailable',
3330 print git_header_html($http_responses{$status}, undef, %opts);
3331 print <<EOF;
3332 <div class="page_body">
3333 <br /><br />
3334 $status - $error
3335 <br />
3337 if (defined $extra) {
3338 print "<hr />\n" .
3339 "$extra\n";
3341 print "</div>\n";
3343 print git_footer_html();
3344 goto DONE_GITWEB
3345 unless ($opts{'-error_handler'});
3348 ## ----------------------------------------------------------------------
3349 ## functions printing or outputting HTML: navigation
3351 sub git_print_page_nav {
3352 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3353 my $output = "";
3354 $extra = '' if !defined $extra; # pager or formats
3356 my @navs = qw(summary shortlog log commit commitdiff tree);
3357 if ($suppress) {
3358 @navs = grep { $_ ne $suppress } @navs;
3361 my %arg = map { $_ => {action=>$_} } @navs;
3362 if (defined $head) {
3363 for (qw(commit commitdiff)) {
3364 $arg{$_}{'hash'} = $head;
3366 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3367 for (qw(shortlog log)) {
3368 $arg{$_}{'hash'} = $head;
3373 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3374 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3376 my @actions = gitweb_get_feature('actions');
3377 my %repl = (
3378 '%' => '%',
3379 'n' => $project, # project name
3380 'f' => $git_dir, # project path within filesystem
3381 'h' => $treehead || '', # current hash ('h' parameter)
3382 'b' => $treebase || '', # hash base ('hb' parameter)
3384 while (@actions) {
3385 my ($label, $link, $pos) = splice(@actions,0,3);
3386 # insert
3387 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3388 # munch munch
3389 $link =~ s/%([%nfhb])/$repl{$1}/g;
3390 $arg{$label}{'_href'} = $link;
3393 $output .= "<div class=\"page_nav\">\n" .
3394 (join " | ",
3395 map { $_ eq $current ?
3396 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3397 } @navs);
3398 $output .= "<br/>\n$extra<br/>\n" .
3399 "</div>\n";
3402 sub format_paging_nav {
3403 my ($action, $page, $has_next_link) = @_;
3404 my $paging_nav;
3407 if ($page > 0) {
3408 $paging_nav .=
3409 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
3410 " &sdot; " .
3411 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3412 -accesskey => "p", -title => "Alt-p"}, "prev");
3413 } else {
3414 $paging_nav .= "first &sdot; prev";
3417 if ($has_next_link) {
3418 $paging_nav .= " &sdot; " .
3419 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3420 -accesskey => "n", -title => "Alt-n"}, "next");
3421 } else {
3422 $paging_nav .= " &sdot; next";
3425 return $paging_nav;
3428 ## ......................................................................
3429 ## functions printing or outputting HTML: div
3431 sub git_print_header_div {
3432 my ($action, $title, $hash, $hash_base) = @_;
3433 my %args = ();
3434 my $output = "";
3436 $args{'action'} = $action;
3437 $args{'hash'} = $hash if $hash;
3438 $args{'hash_base'} = $hash_base if $hash_base;
3440 $output .= "<div class=\"header\">\n" .
3441 $cgi->a({-href => href(%args), -class => "title"},
3442 $title ? $title : $action) .
3443 "\n</div>\n";
3444 return $output;
3447 sub print_local_time {
3448 print format_local_time(@_);
3451 sub format_local_time {
3452 my $localtime = '';
3453 my %date = @_;
3454 if ($date{'hour_local'} < 6) {
3455 $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3456 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3457 } else {
3458 $localtime .= sprintf(" (%02d:%02d %s)",
3459 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3462 return $localtime;
3465 # Outputs the author name and date in long form
3466 sub git_print_authorship {
3467 my $co = shift;
3468 my $output = "";
3470 my %opts = @_;
3471 my $tag = $opts{-tag} || 'div';
3472 my $author = $co->{'author_name'};
3474 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3475 $output .= "<$tag class=\"author_date\">" .
3476 format_search_author($author, "author", esc_html($author)) .
3477 " [$ad{'rfc2822'}";
3478 $output .= format_local_time(%ad) if ($opts{-localtime});
3479 $output .= "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3480 . "</$tag>\n";
3482 return $output;
3485 # Outputs table rows containing the full author or committer information,
3486 # in the format expected for 'commit' view (& similar).
3487 # Parameters are a commit hash reference, followed by the list of people
3488 # to output information for. If the list is empty it defaults to both
3489 # author and committer.
3490 sub git_print_authorship_rows {
3491 my $output = "";
3493 my $co = shift;
3494 # too bad we can't use @people = @_ || ('author', 'committer')
3495 my @people = @_;
3496 @people = ('author', 'committer') unless @people;
3497 foreach my $who (@people) {
3498 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3499 $output .= "<tr><td>$who</td><td>" .
3500 format_search_author($co->{"${who}_name"}, $who,
3501 esc_html($co->{"${who}_name"})) . " " .
3502 format_search_author($co->{"${who}_email"}, $who,
3503 esc_html("<" . $co->{"${who}_email"} . ">")) .
3504 "</td><td rowspan=\"2\">" .
3505 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3506 "</td></tr>\n" .
3507 "<tr>" .
3508 "<td></td><td> $wd{'rfc2822'}";
3509 $output .= format_local_time(%wd);
3510 $output .= "</td>" .
3511 "</tr>\n";
3514 return $output;
3517 sub git_print_page_path {
3518 my $name = shift;
3519 my $type = shift;
3520 my $hb = shift;
3522 my $output = "";
3524 $output .= "<div class=\"page_path\">";
3525 $output .= $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3526 -title => 'tree root'}, to_utf8("[$project]"));
3527 $output .= " / ";
3528 if (defined $name) {
3529 my @dirname = split '/', $name;
3530 my $basename = pop @dirname;
3531 my $fullname = '';
3533 foreach my $dir (@dirname) {
3534 $fullname .= ($fullname ? '/' : '') . $dir;
3535 $output .= $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3536 hash_base=>$hb),
3537 -title => $fullname}, esc_path($dir));
3538 $output .= " / ";
3540 if (defined $type && $type eq 'blob') {
3541 $output .= $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3542 hash_base=>$hb),
3543 -title => $name}, esc_path($basename));
3544 } elsif (defined $type && $type eq 'tree') {
3545 $output .= $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3546 hash_base=>$hb),
3547 -title => $name}, esc_path($basename));
3548 $output .= " / ";
3549 } else {
3550 $output .= esc_path($basename);
3553 $output .= "<br/></div>\n";
3555 return $output;
3558 sub git_print_log {
3559 my $log = shift;
3560 my %opts = @_;
3562 my $output = "";
3564 if ($opts{'-remove_title'}) {
3565 # remove title, i.e. first line of log
3566 shift @$log;
3568 # remove leading empty lines
3569 while (defined $log->[0] && $log->[0] eq "") {
3570 shift @$log;
3573 # print log
3574 my $signoff = 0;
3575 my $empty = 0;
3576 foreach my $line (@$log) {
3577 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3578 $signoff = 1;
3579 $empty = 0;
3580 if (! $opts{'-remove_signoff'}) {
3581 $output .= "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3582 next;
3583 } else {
3584 # remove signoff lines
3585 next;
3587 } else {
3588 $signoff = 0;
3591 # print only one empty line
3592 # do not print empty line after signoff
3593 if ($line eq "") {
3594 next if ($empty || $signoff);
3595 $empty = 1;
3596 } else {
3597 $empty = 0;
3600 $output .= format_log_line_html($line) . "<br/>\n";
3603 if ($opts{'-final_empty_line'}) {
3604 # end with single empty line
3605 $output .= "<br/>\n" unless $empty;
3608 return $output;
3611 # return link target (what link points to)
3612 sub git_get_link_target {
3613 my $hash = shift;
3614 my $link_target;
3616 # read link
3617 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3618 or return;
3620 local $/ = undef;
3621 $link_target = <$fd>;
3623 close $fd
3624 or return;
3626 return $link_target;
3629 # given link target, and the directory (basedir) the link is in,
3630 # return target of link relative to top directory (top tree);
3631 # return undef if it is not possible (including absolute links).
3632 sub normalize_link_target {
3633 my ($link_target, $basedir) = @_;
3635 # absolute symlinks (beginning with '/') cannot be normalized
3636 return if (substr($link_target, 0, 1) eq '/');
3638 # normalize link target to path from top (root) tree (dir)
3639 my $path;
3640 if ($basedir) {
3641 $path = $basedir . '/' . $link_target;
3642 } else {
3643 # we are in top (root) tree (dir)
3644 $path = $link_target;
3647 # remove //, /./, and /../
3648 my @path_parts;
3649 foreach my $part (split('/', $path)) {
3650 # discard '.' and ''
3651 next if (!$part || $part eq '.');
3652 # handle '..'
3653 if ($part eq '..') {
3654 if (@path_parts) {
3655 pop @path_parts;
3656 } else {
3657 # link leads outside repository (outside top dir)
3658 return;
3660 } else {
3661 push @path_parts, $part;
3664 $path = join('/', @path_parts);
3666 return $path;
3669 # print tree entry (row of git_tree), but without encompassing <tr> element
3670 sub git_print_tree_entry {
3671 my ($t, $basedir, $hash_base, $have_blame) = @_;
3673 my $output = "";
3675 my %base_key = ();
3676 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3678 # The format of a table row is: mode list link. Where mode is
3679 # the mode of the entry, list is the name of the entry, an href,
3680 # and link is the action links of the entry.
3682 $output .= "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3683 if (exists $t->{'size'}) {
3684 $output .= "<td class=\"size\">$t->{'size'}</td>\n";
3686 if ($t->{'type'} eq "blob") {
3687 $output .= "<td class=\"list\">" .
3688 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3689 file_name=>"$basedir$t->{'name'}", %base_key),
3690 -class => "list"}, esc_path($t->{'name'}));
3691 if (S_ISLNK(oct $t->{'mode'})) {
3692 my $link_target = git_get_link_target($t->{'hash'});
3693 if ($link_target) {
3694 my $norm_target = normalize_link_target($link_target, $basedir);
3695 if (defined $norm_target) {
3696 $output .= " -> " .
3697 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3698 file_name=>$norm_target),
3699 -title => $norm_target}, esc_path($link_target));
3700 } else {
3701 $output .= " -> " . esc_path($link_target);
3705 $output .= "</td>\n";
3706 $output .= "<td class=\"link\">";
3707 $output .= $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3708 file_name=>"$basedir$t->{'name'}", %base_key)},
3709 "blob");
3710 if ($have_blame) {
3711 $output .= " | " .
3712 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3713 file_name=>"$basedir$t->{'name'}", %base_key)},
3714 "blame");
3716 if (defined $hash_base) {
3717 $output .= " | " .
3718 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3719 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3720 "history");
3722 $output .= " | " .
3723 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3724 file_name=>"$basedir$t->{'name'}")},
3725 "raw");
3726 $output .= "</td>\n";
3728 } elsif ($t->{'type'} eq "tree") {
3729 $output .= "<td class=\"list\">";
3730 $output .= $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3731 file_name=>"$basedir$t->{'name'}",
3732 %base_key)},
3733 esc_path($t->{'name'}));
3734 $output .= "</td>\n";
3735 $output .= "<td class=\"link\">";
3736 $output .= $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3737 file_name=>"$basedir$t->{'name'}",
3738 %base_key)},
3739 "tree");
3740 if (defined $hash_base) {
3741 $output .= " | " .
3742 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3743 file_name=>"$basedir$t->{'name'}")},
3744 "history");
3746 $output .= "</td>\n";
3747 } else {
3748 # unknown object: we can only present history for it
3749 # (this includes 'commit' object, i.e. submodule support)
3750 $output .= "<td class=\"list\">" .
3751 esc_path($t->{'name'}) .
3752 "</td>\n";
3753 $output .= "<td class=\"link\">";
3754 if (defined $hash_base) {
3755 $output .= $cgi->a({-href => href(action=>"history",
3756 hash_base=>$hash_base,
3757 file_name=>"$basedir$t->{'name'}")},
3758 "history");
3760 $output .= "</td>\n";
3763 return $output;
3766 ## ......................................................................
3767 ## functions printing large fragments of HTML
3769 # get pre-image filenames for merge (combined) diff
3770 sub fill_from_file_info {
3771 my ($diff, @parents) = @_;
3773 $diff->{'from_file'} = [ ];
3774 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3775 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3776 if ($diff->{'status'}[$i] eq 'R' ||
3777 $diff->{'status'}[$i] eq 'C') {
3778 $diff->{'from_file'}[$i] =
3779 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3783 return $diff;
3786 # is current raw difftree line of file deletion
3787 sub is_deleted {
3788 my $diffinfo = shift;
3790 return $diffinfo->{'to_id'} eq ('0' x 40);
3793 # does patch correspond to [previous] difftree raw line
3794 # $diffinfo - hashref of parsed raw diff format
3795 # $patchinfo - hashref of parsed patch diff format
3796 # (the same keys as in $diffinfo)
3797 sub is_patch_split {
3798 my ($diffinfo, $patchinfo) = @_;
3800 return defined $diffinfo && defined $patchinfo
3801 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3805 sub git_difftree_body {
3806 my $output = "";
3808 my ($difftree, $hash, @parents) = @_;
3809 my ($parent) = $parents[0];
3810 my $have_blame = gitweb_check_feature('blame');
3811 $output .= "<div class=\"list_head\">\n";
3812 if ($#{$difftree} > 10) {
3813 $output .= (($#{$difftree} + 1) . " files changed:\n");
3815 $output .= "</div>\n";
3817 $output .= "<table class=\"" .
3818 (@parents > 1 ? "combined " : "") .
3819 "diff_tree\">\n";
3821 # header only for combined diff in 'commitdiff' view
3822 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3823 if ($has_header) {
3824 # table header
3825 $output .= "<thead><tr>\n" .
3826 "<th></th><th></th>\n"; # filename, patchN link
3827 for (my $i = 0; $i < @parents; $i++) {
3828 my $par = $parents[$i];
3829 $output .= "<th>" .
3830 $cgi->a({-href => href(action=>"commitdiff",
3831 hash=>$hash, hash_parent=>$par),
3832 -title => 'commitdiff to parent number ' .
3833 ($i+1) . ': ' . substr($par,0,7)},
3834 $i+1) .
3835 "&nbsp;</th>\n";
3837 $output .= "</tr></thead>\n<tbody>\n";
3840 my $alternate = 1;
3841 my $patchno = 0;
3842 foreach my $line (@{$difftree}) {
3843 my $diff = parsed_difftree_line($line);
3845 if ($alternate) {
3846 $output .= "<tr class=\"dark\">\n";
3847 } else {
3848 $output .= "<tr class=\"light\">\n";
3850 $alternate ^= 1;
3852 if (exists $diff->{'nparents'}) { # combined diff
3854 fill_from_file_info($diff, @parents)
3855 unless exists $diff->{'from_file'};
3857 if (!is_deleted($diff)) {
3858 # file exists in the result (child) commit
3859 $output .= "<td>" .
3860 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3861 file_name=>$diff->{'to_file'},
3862 hash_base=>$hash),
3863 -class => "list"}, esc_path($diff->{'to_file'})) .
3864 "</td>\n";
3865 } else {
3866 $output .= "<td>" .
3867 esc_path($diff->{'to_file'}) .
3868 "</td>\n";
3871 if ($action eq 'commitdiff') {
3872 # link to patch
3873 $patchno++;
3874 $output .= "<td class=\"link\">" .
3875 $cgi->a({-href => "#patch$patchno"}, "patch") .
3876 " | " .
3877 "</td>\n";
3880 my $has_history = 0;
3881 my $not_deleted = 0;
3882 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3883 my $hash_parent = $parents[$i];
3884 my $from_hash = $diff->{'from_id'}[$i];
3885 my $from_path = $diff->{'from_file'}[$i];
3886 my $status = $diff->{'status'}[$i];
3888 $has_history ||= ($status ne 'A');
3889 $not_deleted ||= ($status ne 'D');
3891 if ($status eq 'A') {
3892 $output .= "<td class=\"link\" align=\"right\"> | </td>\n";
3893 } elsif ($status eq 'D') {
3894 $output .= "<td class=\"link\">" .
3895 $cgi->a({-href => href(action=>"blob",
3896 hash_base=>$hash,
3897 hash=>$from_hash,
3898 file_name=>$from_path)},
3899 "blob" . ($i+1)) .
3900 " | </td>\n";
3901 } else {
3902 if ($diff->{'to_id'} eq $from_hash) {
3903 $output .= "<td class=\"link nochange\">";
3904 } else {
3905 $output .= "<td class=\"link\">";
3907 $output .= $cgi->a({-href => href(action=>"blobdiff",
3908 hash=>$diff->{'to_id'},
3909 hash_parent=>$from_hash,
3910 hash_base=>$hash,
3911 hash_parent_base=>$hash_parent,
3912 file_name=>$diff->{'to_file'},
3913 file_parent=>$from_path)},
3914 "diff" . ($i+1)) .
3915 " | </td>\n";
3919 $output .= "<td class=\"link\">";
3920 if ($not_deleted) {
3921 $output .= $cgi->a({-href => href(action=>"blob",
3922 hash=>$diff->{'to_id'},
3923 file_name=>$diff->{'to_file'},
3924 hash_base=>$hash)},
3925 "blob");
3926 $output .= " | " if ($has_history);
3928 if ($has_history) {
3929 $output .= $cgi->a({-href => href(action=>"history",
3930 file_name=>$diff->{'to_file'},
3931 hash_base=>$hash)},
3932 "history");
3934 $output .= "</td>\n";
3936 $output .= "</tr>\n";
3937 next; # instead of 'else' clause, to avoid extra indent
3939 # else ordinary diff
3941 my ($to_mode_oct, $to_mode_str, $to_file_type);
3942 my ($from_mode_oct, $from_mode_str, $from_file_type);
3943 if ($diff->{'to_mode'} ne ('0' x 6)) {
3944 $to_mode_oct = oct $diff->{'to_mode'};
3945 if (S_ISREG($to_mode_oct)) { # only for regular file
3946 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3948 $to_file_type = file_type($diff->{'to_mode'});
3950 if ($diff->{'from_mode'} ne ('0' x 6)) {
3951 $from_mode_oct = oct $diff->{'from_mode'};
3952 if (S_ISREG($to_mode_oct)) { # only for regular file
3953 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3955 $from_file_type = file_type($diff->{'from_mode'});
3958 if ($diff->{'status'} eq "A") { # created
3959 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3960 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3961 $mode_chng .= "]</span>";
3962 $output .= "<td>";
3963 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3964 hash_base=>$hash, file_name=>$diff->{'file'}),
3965 -class => "list"}, esc_path($diff->{'file'}));
3966 $output .= "</td>\n";
3967 $output .= "<td>$mode_chng</td>\n";
3968 $output .= "<td class=\"link\">";
3969 if ($action eq 'commitdiff') {
3970 # link to patch
3971 $patchno++;
3972 $output .= $cgi->a({-href => "#patch$patchno"}, "patch");
3973 $output .= " | ";
3975 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3976 hash_base=>$hash, file_name=>$diff->{'file'})},
3977 "blob");
3978 $output .= "</td>\n";
3980 } elsif ($diff->{'status'} eq "D") { # deleted
3981 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3982 $output .= "<td>";
3983 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3984 hash_base=>$parent, file_name=>$diff->{'file'}),
3985 -class => "list"}, esc_path($diff->{'file'}));
3986 $output .= "</td>\n";
3987 $output .= "<td>$mode_chng</td>\n";
3988 $output .= "<td class=\"link\">";
3989 if ($action eq 'commitdiff') {
3990 # link to patch
3991 $patchno++;
3992 $output .= $cgi->a({-href => "#patch$patchno"}, "patch");
3993 $output .= " | ";
3995 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3996 hash_base=>$parent, file_name=>$diff->{'file'})},
3997 "blob") . " | ";
3998 if ($have_blame) {
3999 $output .= $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4000 file_name=>$diff->{'file'})},
4001 "blame") . " | ";
4003 $output .= $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4004 file_name=>$diff->{'file'})},
4005 "history");
4006 $output .= "</td>\n";
4008 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4009 my $mode_chnge = "";
4010 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4011 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4012 if ($from_file_type ne $to_file_type) {
4013 $mode_chnge .= " from $from_file_type to $to_file_type";
4015 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4016 if ($from_mode_str && $to_mode_str) {
4017 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4018 } elsif ($to_mode_str) {
4019 $mode_chnge .= " mode: $to_mode_str";
4022 $mode_chnge .= "]</span>\n";
4024 $output .= "<td>";
4025 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4026 hash_base=>$hash, file_name=>$diff->{'file'}),
4027 -class => "list"}, esc_path($diff->{'file'}));
4028 $output .= "</td>\n";
4029 $output .= "<td>$mode_chnge</td>\n";
4030 $output .= "<td class=\"link\">";
4031 if ($action eq 'commitdiff') {
4032 # link to patch
4033 $patchno++;
4034 $output .= $cgi->a({-href => "#patch$patchno"}, "patch") .
4035 " | ";
4036 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4037 # "commit" view and modified file (not onlu mode changed)
4038 $output .= $cgi->a({-href => href(action=>"blobdiff",
4039 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4040 hash_base=>$hash, hash_parent_base=>$parent,
4041 file_name=>$diff->{'file'})},
4042 "diff") .
4043 " | ";
4045 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4046 hash_base=>$hash, file_name=>$diff->{'file'})},
4047 "blob") . " | ";
4048 if ($have_blame) {
4049 $output .= $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4050 file_name=>$diff->{'file'})},
4051 "blame") . " | ";
4053 $output .= $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4054 file_name=>$diff->{'file'})},
4055 "history");
4056 $output .= "</td>\n";
4058 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4059 my %status_name = ('R' => 'moved', 'C' => 'copied');
4060 my $nstatus = $status_name{$diff->{'status'}};
4061 my $mode_chng = "";
4062 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4063 # mode also for directories, so we cannot use $to_mode_str
4064 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4066 $output .= "<td>" .
4067 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4068 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4069 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4070 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4071 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4072 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4073 -class => "list"}, esc_path($diff->{'from_file'})) .
4074 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4075 "<td class=\"link\">";
4076 if ($action eq 'commitdiff') {
4077 # link to patch
4078 $patchno++;
4079 $output .= $cgi->a({-href => "#patch$patchno"}, "patch") .
4080 " | ";
4081 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4082 # "commit" view and modified file (not only pure rename or copy)
4083 $output .= $cgi->a({-href => href(action=>"blobdiff",
4084 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4085 hash_base=>$hash, hash_parent_base=>$parent,
4086 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4087 "diff") .
4088 " | ";
4090 $output .= $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4091 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4092 "blob") . " | ";
4093 if ($have_blame) {
4094 $output .= $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4095 file_name=>$diff->{'to_file'})},
4096 "blame") . " | ";
4098 $output .= $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4099 file_name=>$diff->{'to_file'})},
4100 "history");
4101 $output .= "</td>\n";
4103 } # we should not encounter Unmerged (U) or Unknown (X) status
4104 $output .= "</tr>\n";
4106 $output .= "</tbody>" if $has_header;
4107 $output .= "</table>\n";
4109 return $output;
4112 sub git_patchset_body {
4113 my ($fd, $difftree, $hash, @hash_parents) = @_;
4114 my ($hash_parent) = $hash_parents[0];
4116 my $is_combined = (@hash_parents > 1);
4117 my $patch_idx = 0;
4118 my $patch_number = 0;
4119 my $patch_line;
4120 my $diffinfo;
4121 my $to_name;
4122 my (%from, %to);
4124 my $output = "";
4126 $output .= "<div class=\"patchset\">\n";
4128 # skip to first patch
4129 while ($patch_line = <$fd>) {
4130 chomp $patch_line;
4132 last if ($patch_line =~ m/^diff /);
4135 PATCH:
4136 while ($patch_line) {
4138 # parse "git diff" header line
4139 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4140 # $1 is from_name, which we do not use
4141 $to_name = unquote($2);
4142 $to_name =~ s!^b/!!;
4143 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4144 # $1 is 'cc' or 'combined', which we do not use
4145 $to_name = unquote($2);
4146 } else {
4147 $to_name = undef;
4150 # check if current patch belong to current raw line
4151 # and parse raw git-diff line if needed
4152 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4153 # this is continuation of a split patch
4154 $output .= "<div class=\"patch cont\">\n";
4155 } else {
4156 # advance raw git-diff output if needed
4157 $patch_idx++ if defined $diffinfo;
4159 # read and prepare patch information
4160 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4162 # compact combined diff output can have some patches skipped
4163 # find which patch (using pathname of result) we are at now;
4164 if ($is_combined) {
4165 while ($to_name ne $diffinfo->{'to_file'}) {
4166 $output .= "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4167 format_diff_cc_simplified($diffinfo, @hash_parents) .
4168 "</div>\n"; # class="patch"
4170 $patch_idx++;
4171 $patch_number++;
4173 last if $patch_idx > $#$difftree;
4174 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4178 # modifies %from, %to hashes
4179 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4181 # this is first patch for raw difftree line with $patch_idx index
4182 # we index @$difftree array from 0, but number patches from 1
4183 $output .= "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4186 # git diff header
4187 #assert($patch_line =~ m/^diff /) if DEBUG;
4188 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4189 $patch_number++;
4190 # print "git diff" header
4191 $output .= format_git_diff_header_line($patch_line, $diffinfo,
4192 \%from, \%to);
4194 # print extended diff header
4195 $output .= "<div class=\"diff extended_header\">\n";
4196 EXTENDED_HEADER:
4197 while ($patch_line = <$fd>) {
4198 chomp $patch_line;
4200 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4202 $output .= format_extended_diff_header_line($patch_line, $diffinfo,
4203 \%from, \%to);
4205 $output .= "</div>\n"; # class="diff extended_header"
4207 # from-file/to-file diff header
4208 if (! $patch_line) {
4209 $output .= "</div>\n"; # class="patch"
4210 last PATCH;
4212 next PATCH if ($patch_line =~ m/^diff /);
4213 #assert($patch_line =~ m/^---/) if DEBUG;
4215 my $last_patch_line = $patch_line;
4216 $patch_line = <$fd>;
4217 chomp $patch_line;
4218 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4220 $output .= format_diff_from_to_header($last_patch_line, $patch_line,
4221 $diffinfo, \%from, \%to,
4222 @hash_parents);
4224 # the patch itself
4225 LINE:
4226 while ($patch_line = <$fd>) {
4227 chomp $patch_line;
4229 next PATCH if ($patch_line =~ m/^diff /);
4231 $output .= format_diff_line($patch_line, \%from, \%to);
4234 } continue {
4235 $output .= "</div>\n"; # class="patch"
4238 # for compact combined (--cc) format, with chunk and patch simplification
4239 # the patchset might be empty, but there might be unprocessed raw lines
4240 for (++$patch_idx if $patch_number > 0;
4241 $patch_idx < @$difftree;
4242 ++$patch_idx) {
4243 # read and prepare patch information
4244 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4246 # generate anchor for "patch" links in difftree / whatchanged part
4247 $output .= "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4248 format_diff_cc_simplified($diffinfo, @hash_parents) .
4249 "</div>\n"; # class="patch"
4251 $patch_number++;
4254 if ($patch_number == 0) {
4255 if (@hash_parents > 1) {
4256 $output .= "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4257 } else {
4258 $output .= "<div class=\"diff nodifferences\">No differences found</div>\n";
4262 $output .= "</div>\n"; # class="patchset"
4265 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4267 # fills project list info (age, description, owner, forks) for each
4268 # project in the list, removing invalid projects from returned list
4269 # NOTE: modifies $projlist, but does not remove entries from it
4270 sub fill_project_list_info {
4271 my ($projlist, $check_forks) = @_;
4272 my @projects;
4274 my $output = "";
4276 my $show_ctags = gitweb_check_feature('ctags');
4277 PROJECT:
4278 foreach my $pr (@$projlist) {
4279 my (@activity) = git_get_last_activity($pr->{'path'});
4280 unless (@activity) {
4281 next PROJECT;
4283 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4284 if (!defined $pr->{'descr'}) {
4285 my $descr = git_get_project_description($pr->{'path'}) || "";
4286 $descr = to_utf8($descr);
4287 $pr->{'descr_long'} = $descr;
4288 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4290 if (!defined $pr->{'owner'}) {
4291 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4293 if ($check_forks) {
4294 my $pname = $pr->{'path'};
4295 if (($pname =~ s/\.git$//) &&
4296 ($pname !~ /\/$/) &&
4297 (-d "$projectroot/$pname")) {
4298 $pr->{'forks'} = "-d $projectroot/$pname";
4299 } else {
4300 $pr->{'forks'} = 0;
4303 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4304 push @projects, $pr;
4307 return @projects;
4310 # print 'sort by' <th> element, generating 'sort by $name' replay link
4311 # if that order is not selected
4312 sub print_sort_th {
4313 print format_sort_th(@_);
4316 sub format_sort_th {
4317 my ($name, $order, $header) = @_;
4318 my $sort_th = "";
4319 $header ||= ucfirst($name);
4321 if ($order eq $name) {
4322 $sort_th .= "<th>$header</th>\n";
4323 } else {
4324 $sort_th .= "<th>" .
4325 $cgi->a({-href => href(-replay=>1, order=>$name),
4326 -class => "header"}, $header) .
4327 "</th>\n";
4330 return $sort_th;
4333 sub git_project_list_body {
4334 # actually uses global variable $project
4335 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4337 my $output = "";
4339 my $check_forks = gitweb_check_feature('forks');
4340 my @projects = fill_project_list_info($projlist, $check_forks);
4342 $order ||= $default_projects_order;
4343 $from = 0 unless defined $from;
4344 $to = $#projects if (!defined $to || $#projects < $to);
4346 my %order_info = (
4347 project => { key => 'path', type => 'str' },
4348 descr => { key => 'descr_long', type => 'str' },
4349 owner => { key => 'owner', type => 'str' },
4350 age => { key => 'age', type => 'num' }
4352 my $oi = $order_info{$order};
4353 if ($oi->{'type'} eq 'str') {
4354 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4355 } else {
4356 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4359 my $show_ctags = gitweb_check_feature('ctags');
4360 if ($show_ctags) {
4361 my %ctags;
4362 foreach my $p (@projects) {
4363 foreach my $ct (keys %{$p->{'ctags'}}) {
4364 $ctags{$ct} += $p->{'ctags'}->{$ct};
4367 my $cloud = git_populate_project_tagcloud(\%ctags);
4368 $output .= git_show_project_tagcloud($cloud, 64);
4371 $output .= "<table class=\"project_list\">\n";
4372 unless ($no_header) {
4373 $output .= "<tr>\n";
4374 if ($check_forks) {
4375 $output .= "<th></th>\n";
4377 $output .= format_sort_th('project', $order, 'Project');
4378 $output .= format_sort_th('descr', $order, 'Description');
4379 $output .= format_sort_th('owner', $order, 'Owner');
4380 $output .= format_sort_th('age', $order, 'Last Change');
4381 $output .= "<th></th>\n" . # for links
4382 "</tr>\n";
4384 my $alternate = 1;
4385 my $tagfilter = $cgi->param('by_tag');
4386 for (my $i = $from; $i <= $to; $i++) {
4387 my $pr = $projects[$i];
4389 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4390 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4391 and not $pr->{'descr_long'} =~ /$searchtext/;
4392 # Weed out forks or non-matching entries of search
4393 if ($check_forks) {
4394 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4395 $forkbase="^$forkbase" if $forkbase;
4396 next if not $searchtext and not $tagfilter and $show_ctags
4397 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4400 if ($alternate) {
4401 $output .= "<tr class=\"dark\">\n";
4402 } else {
4403 $output .= "<tr class=\"light\">\n";
4405 #$alternate ^= 1;
4406 if ($check_forks) {
4407 $output .= "<td>";
4408 if ($pr->{'forks'}) {
4409 $output .= "<!-- $pr->{'forks'} -->\n";
4410 $output .= $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4412 $output .= "</td>\n";
4414 $output .= "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4415 -class => "list"}, esc_html($pr->{'path'})) ."</td>\n".
4416 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4417 -class => "list", -title => $pr->{'descr_long'}},
4418 esc_html($pr->{'descr'})) . "</td>\n" .
4419 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4420 $output .= "<td class=\"". age_class($pr->{'age'}) . "\">" .
4421 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4422 "<td class=\"link\">" .
4423 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4424 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4425 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4426 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4427 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '');
4428 if( $gitlinkurl ne '' ){
4429 $output .= " | ". $cgi->a({-href => "git://$gitlinkurl/".esc_html($pr->{'path'})}, "git");
4431 $output .= "".
4432 "</td>\n" .
4433 "</tr>\n";
4434 $alternate ^= 1;
4436 if (defined $extra) {
4437 $output .= "<tr>\n";
4438 if ($check_forks) {
4439 $output .= "<td></td>\n";
4441 $output .= "<td colspan=\"5\">$extra</td>\n" .
4442 "</tr>\n";
4444 $output .= "</table>\n";
4446 return $output;
4449 sub git_log_body {
4450 my $output = "";
4451 # uses global variable $project
4452 my ($commitlist, $from, $to, $refs, $extra) = @_;
4454 $from = 0 unless defined $from;
4455 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4457 for (my $i = 0; $i <= $to; $i++) {
4458 my %co = %{$commitlist->[$i]};
4459 next if !%co;
4460 my $commit = $co{'id'};
4461 my $ref = format_ref_marker($refs, $commit);
4462 my %ad = parse_date($co{'author_epoch'});
4463 $output .= git_print_header_div('commit',
4464 "<span class=\"age\">$co{'age_string'}</span>" .
4465 esc_html($co{'title'}) . $ref,
4466 $commit);
4467 $output .="<div class=\"title_text\">\n" .
4468 "<div class=\"log_link\">\n" .
4469 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4470 " | " .
4471 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4472 " | " .
4473 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4474 "<br/>\n" .
4475 "</div>\n";
4476 $output .= git_print_authorship(\%co, -tag => 'span');
4477 $output .= "<br/>\n</div>\n";
4479 $output .= "<div class=\"log_body\">\n";
4480 $output .= git_print_log($co{'comment'}, -final_empty_line=> 1);
4481 $output .= "</div>\n";
4483 if ($extra) {
4484 $output .= "<div class=\"page_nav\">\n";
4485 $output .= "$extra\n";
4486 $output .= "</div>\n";
4489 return $output;
4492 sub git_shortlog_body {
4493 # uses global variable $project
4494 my ($commitlist, $from, $to, $refs, $extra) = @_;
4496 $from = 0 unless defined $from;
4497 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4499 my $output = "";
4501 $output .= "<table class=\"shortlog\">\n";
4502 my $alternate = 1;
4503 for (my $i = $from; $i <= $to; $i++) {
4504 my %co = %{$commitlist->[$i]};
4505 my $commit = $co{'id'};
4506 my $ref = format_ref_marker($refs, $commit);
4507 if ($alternate) {
4508 $output .= "<tr class=\"dark\">\n";
4509 } else {
4510 $output .= "<tr class=\"light\">\n";
4512 $alternate ^= 1;
4513 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4514 $output .= "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4515 format_author_html('td', \%co, 10) . "<td>";
4516 $output .= format_subject_html($co{'title'}, $co{'title_short'},
4517 href(action=>"commit", hash=>$commit), $ref);
4518 $output .= "</td>\n" .
4519 "<td class=\"link\">" .
4520 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4521 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4522 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4523 my $snapshot_links = format_snapshot_links($commit);
4524 if (defined $snapshot_links) {
4525 $output .= " | " . $snapshot_links;
4527 $output .= "</td>\n" .
4528 "</tr>\n";
4530 if (defined $extra) {
4531 $output .= "<tr>\n" .
4532 "<td colspan=\"4\">$extra</td>\n" .
4533 "</tr>\n";
4535 $output .= "</table>\n";
4537 return $output;
4540 sub git_history_body {
4541 # Warning: assumes constant type (blob or tree) during history
4542 my ($commitlist, $from, $to, $refs, $extra,
4543 $file_name, $file_hash, $ftype) = @_;
4545 my $output = "";
4547 $from = 0 unless defined $from;
4548 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4550 $output .= "<table class=\"history\">\n";
4551 my $alternate = 1;
4552 for (my $i = $from; $i <= $to; $i++) {
4553 my %co = %{$commitlist->[$i]};
4554 if (!%co) {
4555 next;
4557 my $commit = $co{'id'};
4559 my $ref = format_ref_marker($refs, $commit);
4561 if ($alternate) {
4562 $output .= "<tr class=\"dark\">\n";
4563 } else {
4564 $output .= "<tr class=\"light\">\n";
4566 $alternate ^= 1;
4567 $output .= "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4568 # shortlog: format_author_html('td', \%co, 10)
4569 format_author_html('td', \%co, 15, 3) . "<td>";
4570 # originally git_history used chop_str($co{'title'}, 50)
4571 $output .= format_subject_html($co{'title'}, $co{'title_short'},
4572 href(action=>"commit", hash=>$commit), $ref);
4573 $output .= "</td>\n" .
4574 "<td class=\"link\">" .
4575 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4576 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4578 if ($ftype eq 'blob') {
4579 my $blob_current = $file_hash;
4580 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4581 if (defined $blob_current && defined $blob_parent &&
4582 $blob_current ne $blob_parent) {
4583 $output .= " | " .
4584 $cgi->a({-href => href(action=>"blobdiff",
4585 hash=>$blob_current, hash_parent=>$blob_parent,
4586 hash_base=>$hash_base, hash_parent_base=>$commit,
4587 file_name=>$file_name)},
4588 "diff to current");
4591 $output .= "</td>\n" .
4592 "</tr>\n";
4594 if (defined $extra) {
4595 $output .= "<tr>\n" .
4596 "<td colspan=\"4\">$extra</td>\n" .
4597 "</tr>\n";
4599 $output .= "</table>\n";
4601 return $output;
4604 sub git_tags_body {
4605 # uses global variable $project
4606 my ($taglist, $from, $to, $extra) = @_;
4607 $from = 0 unless defined $from;
4608 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4610 my $output = "";
4612 $output .= "<table class=\"tags\">\n";
4613 my $alternate = 1;
4614 for (my $i = $from; $i <= $to; $i++) {
4615 my $entry = $taglist->[$i];
4616 my %tag = %$entry;
4617 my $comment = $tag{'subject'};
4618 my $comment_short;
4619 if (defined $comment) {
4620 $comment_short = chop_str($comment, 30, 5);
4622 if ($alternate) {
4623 $output .= "<tr class=\"dark\">\n";
4624 } else {
4625 $output .= "<tr class=\"light\">\n";
4627 $alternate ^= 1;
4628 if (defined $tag{'age'}) {
4629 $output .= "<td><i>$tag{'age'}</i></td>\n";
4630 } else {
4631 $output .= "<td></td>\n";
4633 $output .= "<td>" .
4634 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4635 -class => "list name"}, esc_html($tag{'name'})) .
4636 "</td>\n" .
4637 "<td>";
4638 if (defined $comment) {
4639 $output .= format_subject_html($comment, $comment_short,
4640 href(action=>"tag", hash=>$tag{'id'}));
4642 $output .= "</td>\n" .
4643 "<td class=\"selflink\">";
4644 if ($tag{'type'} eq "tag") {
4645 $output .= $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4646 } else {
4647 $output .= "&nbsp;";
4649 $output .= "</td>\n" .
4650 "<td class=\"link\">" . " | " .
4651 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4652 if ($tag{'reftype'} eq "commit") {
4653 $output .= " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4654 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4655 } elsif ($tag{'reftype'} eq "blob") {
4656 $output .= " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4658 $output .= "</td>\n" .
4659 "</tr>";
4661 if (defined $extra) {
4662 $output .= "<tr>\n" .
4663 "<td colspan=\"5\">$extra</td>\n" .
4664 "</tr>\n";
4666 $output .= "</table>\n";
4668 return $output;
4671 sub git_heads_body {
4672 # uses global variable $project
4673 my ($headlist, $head, $from, $to, $extra) = @_;
4674 $from = 0 unless defined $from;
4675 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4677 my $output = "";
4679 $output .= "<table class=\"heads\">\n";
4680 my $alternate = 1;
4681 for (my $i = $from; $i <= $to; $i++) {
4682 my $entry = $headlist->[$i];
4683 my %ref = %$entry;
4684 my $curr = $ref{'id'} eq $head;
4685 if ($alternate) {
4686 $output .= "<tr class=\"dark\">\n";
4687 } else {
4688 $output .= "<tr class=\"light\">\n";
4690 $alternate ^= 1;
4691 $output .= "<td><i>$ref{'age'}</i></td>\n" .
4692 ($curr ? "<td class=\"current_head\">" : "<td>") .
4693 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4694 -class => "list name"},esc_html($ref{'name'})) .
4695 "</td>\n" .
4696 "<td class=\"link\">" .
4697 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4698 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4699 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4700 "</td>\n" .
4701 "</tr>";
4703 if (defined $extra) {
4704 $output .= "<tr>\n" .
4705 "<td colspan=\"3\">$extra</td>\n" .
4706 "</tr>\n";
4708 $output .= "</table>\n";
4710 return $output;
4713 sub git_search_grep_body {
4714 my ($commitlist, $from, $to, $extra) = @_;
4715 $from = 0 unless defined $from;
4716 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4718 my $output = "";
4720 $output .= "<table class=\"commit_search\">\n";
4721 my $alternate = 1;
4722 for (my $i = $from; $i <= $to; $i++) {
4723 my %co = %{$commitlist->[$i]};
4724 if (!%co) {
4725 next;
4727 my $commit = $co{'id'};
4728 if ($alternate) {
4729 $output .= "<tr class=\"dark\">\n";
4730 } else {
4731 $output .= "<tr class=\"light\">\n";
4733 $alternate ^= 1;
4734 $output .= "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4735 format_author_html('td', \%co, 15, 5) .
4736 "<td>" .
4737 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4738 -class => "list subject"},
4739 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4740 my $comment = $co{'comment'};
4741 foreach my $line (@$comment) {
4742 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4743 my ($lead, $match, $trail) = ($1, $2, $3);
4744 $match = chop_str($match, 70, 5, 'center');
4745 my $contextlen = int((80 - length($match))/2);
4746 $contextlen = 30 if ($contextlen > 30);
4747 $lead = chop_str($lead, $contextlen, 10, 'left');
4748 $trail = chop_str($trail, $contextlen, 10, 'right');
4750 $lead = esc_html($lead);
4751 $match = esc_html($match);
4752 $trail = esc_html($trail);
4754 $output .= "$lead<span class=\"match\">$match</span>$trail<br />";
4757 $output .= "</td>\n" .
4758 "<td class=\"link\">" .
4759 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4760 " | " .
4761 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4762 " | " .
4763 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4764 $output .= "</td>\n" .
4765 "</tr>\n";
4767 if (defined $extra) {
4768 $output .= "<tr>\n" .
4769 "<td colspan=\"3\">$extra</td>\n" .
4770 "</tr>\n";
4772 $output .= "</table>\n";
4774 return $output;
4777 ## ======================================================================
4778 ## ======================================================================
4779 ## actions
4781 sub git_project_list {
4782 my $output = "";
4784 my $order = $input_params{'order'};
4785 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4786 die_error(400, "Unknown order parameter");
4789 my @list = git_get_projects_list();
4790 if (!@list) {
4791 die_error(404, "No projects found");
4794 $output .= git_header_html();
4795 if (defined $home_text && -f $home_text) {
4796 $output .= "<div class=\"index_include\">\n";
4797 $output .= insert_file($home_text);
4798 $output .= "</div>\n";
4800 $output .= $cgi->startform(-method => "get") .
4801 "<p class=\"projsearch\">Search:\n" .
4802 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4803 "</p>" .
4804 $cgi->end_form() . "\n";
4805 $output .= git_project_list_body(\@list, $order);
4806 $output .= git_footer_html();
4808 return $output;
4811 sub git_forks {
4812 my $output = "";
4814 my $order = $input_params{'order'};
4815 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4816 die_error(400, "Unknown order parameter");
4819 my @list = git_get_projects_list($project);
4820 if (!@list) {
4821 die_error(404, "No forks found");
4824 $output .= git_header_html();
4825 $output .= git_print_page_nav('','');
4826 $output .= git_print_header_div('summary', "$project forks");
4827 $output .= git_project_list_body(\@list, $order);
4828 $output .= git_footer_html();
4830 return $output;
4833 sub git_project_index {
4834 my @projects = git_get_projects_list($project);
4836 my $output = "";
4838 $output .= $cgi->header(
4839 -type => 'text/plain',
4840 -charset => 'utf-8',
4841 -content_disposition => 'inline; filename="index.aux"');
4843 foreach my $pr (@projects) {
4844 if (!exists $pr->{'owner'}) {
4845 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4848 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4849 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4850 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4851 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4852 $path =~ s/ /\+/g;
4853 $owner =~ s/ /\+/g;
4855 $output .= "$path $owner\n";
4858 return $output;
4861 sub git_summary {
4862 my $descr = git_get_project_description($project) || "none";
4863 my %co = parse_commit("HEAD");
4864 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4865 my $head = $co{'id'};
4867 my $owner = git_get_project_owner($project);
4869 my $refs = git_get_references();
4870 # These get_*_list functions return one more to allow us to see if
4871 # there are more ...
4872 my @taglist = git_get_tags_list(16);
4873 my @headlist = git_get_heads_list(16);
4874 my @forklist;
4875 my $check_forks = gitweb_check_feature('forks');
4877 my $output = "";
4879 if ($check_forks) {
4880 @forklist = git_get_projects_list($project);
4883 $output .= git_header_html();
4884 $output .= git_print_page_nav('summary','', $head);
4886 $output .= "<div class=\"title\">&nbsp;</div>\n";
4887 $output .= "<table class=\"projects_list\">\n" .
4888 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4889 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4890 if (defined $cd{'rfc2822'}) {
4891 $output .= "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4894 # use per project git URL list in $projectroot/$project/cloneurl
4895 # or make project git URL from git base URL and project name
4896 my $url_tag = "URL";
4897 my @url_list = git_get_project_url_list($project);
4898 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4899 foreach my $git_url (@url_list) {
4900 next unless $git_url;
4901 $output .= "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4902 $url_tag = "";
4905 # Tag cloud
4906 my $show_ctags = gitweb_check_feature('ctags');
4907 if ($show_ctags) {
4908 my $ctags = git_get_project_ctags($project);
4909 my $cloud = git_populate_project_tagcloud($ctags);
4910 $output .= "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4911 $output .= "</td>\n<td>" unless %$ctags;
4912 $output .= "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4913 $output .= "</td>\n<td>" if %$ctags;
4914 $output .= git_show_project_tagcloud($cloud, 48);
4915 $output .= "</td></tr>";
4918 $output .= "</table>\n";
4920 # If XSS prevention is on, we don't include README.html.
4921 # TODO: Allow a readme in some safe format.
4922 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4923 $output .= "<div class=\"title\">readme</div>\n" .
4924 "<div class=\"readme\">\n";
4925 $output .= insert_file("$projectroot/$project/README.html");
4926 $output .= "\n</div>\n"; # class="readme"
4929 # we need to request one more than 16 (0..15) to check if
4930 # those 16 are all
4931 my @commitlist = $head ? parse_commits($head, 17) : ();
4932 if (@commitlist) {
4933 $output .= git_print_header_div('shortlog');
4934 $output .= git_shortlog_body(\@commitlist, 0, 15, $refs,
4935 $#commitlist <= 15 ? undef :
4936 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4939 if (@taglist) {
4940 $output .= git_print_header_div('tags');
4941 $output .= git_tags_body(\@taglist, 0, 15,
4942 $#taglist <= 15 ? undef :
4943 $cgi->a({-href => href(action=>"tags")}, "..."));
4946 if (@headlist) {
4947 $output .= git_print_header_div('heads');
4948 $output .= git_heads_body(\@headlist, $head, 0, 15,
4949 $#headlist <= 15 ? undef :
4950 $cgi->a({-href => href(action=>"heads")}, "..."));
4953 if (@forklist) {
4954 $output .= git_print_header_div('forks');
4955 $output .= git_project_list_body(\@forklist, 'age', 0, 15,
4956 $#forklist <= 15 ? undef :
4957 $cgi->a({-href => href(action=>"forks")}, "..."),
4958 'no_header');
4961 $output .= git_footer_html();
4963 return $output;
4966 sub git_tag {
4967 my $output = "";
4969 my %tag = parse_tag($hash);
4971 if (! %tag) {
4972 die_error(404, "Unknown tag object");
4975 my $head = git_get_head_hash($project);
4976 $output .= git_header_html();
4977 $output .= git_print_page_nav('','', $head,undef,$head);
4978 $output .= git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4979 $output .= "<div class=\"title_text\">\n" .
4980 "<table class=\"object_header\">\n" .
4981 "<tr>\n" .
4982 "<td>object</td>\n" .
4983 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4984 $tag{'object'}) . "</td>\n" .
4985 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4986 $tag{'type'}) . "</td>\n" .
4987 "</tr>\n";
4988 if (defined($tag{'author'})) {
4989 $output .= git_print_authorship_rows(\%tag, 'author');
4991 $output .= "</table>\n\n" .
4992 "</div>\n";
4993 $output .= "<div class=\"page_body\">";
4994 my $comment = $tag{'comment'};
4995 foreach my $line (@$comment) {
4996 chomp $line;
4997 $output .= esc_html($line, -nbsp=>1) . "<br/>\n";
4999 $output .= "</div>\n";
5000 $output .= git_footer_html();
5002 return $output;
5005 sub git_blame_common {
5006 my $output = "";
5007 my $format = shift || 'porcelain';
5008 if ($format eq 'porcelain' && $cgi->param('js')) {
5009 $format = 'incremental';
5010 $action = 'blame_incremental'; # for page title etc
5013 # permissions
5014 gitweb_check_feature('blame')
5015 or die_error(403, "Blame view not allowed");
5017 # error checking
5018 die_error(400, "No file name given") unless $file_name;
5019 $hash_base ||= git_get_head_hash($project);
5020 die_error(404, "Couldn't find base commit") unless $hash_base;
5021 my %co = parse_commit($hash_base)
5022 or die_error(404, "Commit not found");
5023 my $ftype = "blob";
5024 if (!defined $hash) {
5025 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5026 or die_error(404, "Error looking up file");
5027 } else {
5028 $ftype = git_get_type($hash);
5029 if ($ftype !~ "blob") {
5030 die_error(400, "Object is not a blob");
5034 my $fd;
5035 if ($format eq 'incremental') {
5036 # get file contents (as base)
5037 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5038 or die_error(500, "Open git-cat-file failed");
5039 } elsif ($format eq 'data') {
5040 # run git-blame --incremental
5041 open $fd, "-|", git_cmd(), "blame", "--incremental",
5042 $hash_base, "--", $file_name
5043 or die_error(500, "Open git-blame --incremental failed");
5044 } else {
5045 # run git-blame --porcelain
5046 open $fd, "-|", git_cmd(), "blame", '-p',
5047 $hash_base, '--', $file_name
5048 or die_error(500, "Open git-blame --porcelain failed");
5051 # incremental blame data returns early
5052 if ($format eq 'data') {
5053 $output .= $cgi->header(
5054 -type=>"text/plain", -charset => "utf-8",
5055 -status=> "200 OK");
5056 local $| = 1; # output autoflush
5057 while (my $line = <$fd>) {
5058 $output .= $line;
5060 close $fd
5061 or die_error(500, "ERROR $!\n");
5063 $output .= 'END';
5064 if (defined $t0 && gitweb_check_feature('timed')) {
5065 $output .= ' '.
5066 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
5067 ' '.$number_of_git_cmds;
5069 $output .= "\n";
5071 return $output;
5074 # page header
5075 $output .= git_header_html();
5076 my $formats_nav =
5077 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5078 "blob") .
5079 " | ";
5080 if ($format eq 'incremental') {
5081 $formats_nav .=
5082 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5083 "blame") . " (non-incremental)";
5084 } else {
5085 $formats_nav .=
5086 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5087 "blame") . " (incremental)";
5089 $formats_nav .=
5090 " | " .
5091 $cgi->a({-href => href(action=>"history", -replay=>1)},
5092 "history") .
5093 " | " .
5094 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5095 "HEAD");
5096 $output .= git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5097 $output .= git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5098 $output .= git_print_page_path($file_name, $ftype, $hash_base);
5100 # page body
5101 if ($format eq 'incremental') {
5102 $output .= "<noscript>\n<div class=\"error\"><center><b>\n".
5103 "This page requires JavaScript to run.\n Use ".
5104 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5105 'this page').
5106 " instead.\n".
5107 "</b></center></div>\n</noscript>\n";
5109 $output .= qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5112 $output .= qq!<div class="page_body">\n!;
5113 $output .= qq!<div id="progress_info">... / ...</div>\n!
5114 if ($format eq 'incremental');
5115 $output .= qq!<table id="blame_table" class="blame" width="100%">\n!.
5116 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5117 qq!<thead>\n!.
5118 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5119 qq!</thead>\n!.
5120 qq!<tbody>\n!;
5122 my @rev_color = qw(light dark);
5123 my $num_colors = scalar(@rev_color);
5124 my $current_color = 0;
5126 if ($format eq 'incremental') {
5127 my $color_class = $rev_color[$current_color];
5129 #contents of a file
5130 my $linenr = 0;
5131 LINE:
5132 while (my $line = <$fd>) {
5133 chomp $line;
5134 $linenr++;
5136 $output .= qq!<tr id="l$linenr" class="$color_class">!.
5137 qq!<td class="sha1"><a href=""> </a></td>!.
5138 qq!<td class="linenr">!.
5139 qq!<a class="linenr" href="">$linenr</a></td>!;
5140 $output .= qq!<td class="pre">! . esc_html($line) . "</td>\n";
5141 $output .= qq!</tr>\n!;
5144 } else { # porcelain, i.e. ordinary blame
5145 my %metainfo = (); # saves information about commits
5147 # blame data
5148 LINE:
5149 while (my $line = <$fd>) {
5150 chomp $line;
5151 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5152 # no <lines in group> for subsequent lines in group of lines
5153 my ($full_rev, $orig_lineno, $lineno, $group_size) =
5154 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5155 if (!exists $metainfo{$full_rev}) {
5156 $metainfo{$full_rev} = { 'nprevious' => 0 };
5158 my $meta = $metainfo{$full_rev};
5159 my $data;
5160 while ($data = <$fd>) {
5161 chomp $data;
5162 last if ($data =~ s/^\t//); # contents of line
5163 if ($data =~ /^(\S+)(?: (.*))?$/) {
5164 $meta->{$1} = $2 unless exists $meta->{$1};
5166 if ($data =~ /^previous /) {
5167 $meta->{'nprevious'}++;
5170 my $short_rev = substr($full_rev, 0, 8);
5171 my $author = $meta->{'author'};
5172 my %date =
5173 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5174 my $date = $date{'iso-tz'};
5175 if ($group_size) {
5176 $current_color = ($current_color + 1) % $num_colors;
5178 my $tr_class = $rev_color[$current_color];
5179 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5180 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5181 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5182 $output .= "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5183 if ($group_size) {
5184 $output .= "<td class=\"sha1\"";
5185 $output .= " title=\"". esc_html($author) . ", $date\"";
5186 $output .= " rowspan=\"$group_size\"" if ($group_size > 1);
5187 $output .= ">";
5188 $output .= $cgi->a({-href => href(action=>"commit",
5189 hash=>$full_rev,
5190 file_name=>$file_name)},
5191 esc_html($short_rev));
5192 if ($group_size >= 2) {
5193 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5194 if (@author_initials) {
5195 $output .= "<br />" .
5196 esc_html(join('', @author_initials));
5197 # or join('.', ...)
5200 $output .= "</td>\n";
5202 # 'previous' <sha1 of parent commit> <filename at commit>
5203 if (exists $meta->{'previous'} &&
5204 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5205 $meta->{'parent'} = $1;
5206 $meta->{'file_parent'} = unquote($2);
5208 my $linenr_commit =
5209 exists($meta->{'parent'}) ?
5210 $meta->{'parent'} : $full_rev;
5211 my $linenr_filename =
5212 exists($meta->{'file_parent'}) ?
5213 $meta->{'file_parent'} : unquote($meta->{'filename'});
5214 my $blamed = href(action => 'blame',
5215 file_name => $linenr_filename,
5216 hash_base => $linenr_commit);
5217 $output .= "<td class=\"linenr\">";
5218 $output .= $cgi->a({ -href => "$blamed#l$orig_lineno",
5219 -class => "linenr" },
5220 esc_html($lineno));
5221 $output .= "</td>";
5222 $output .= "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5223 $output .= "</tr>\n";
5224 } # end while
5228 # footer
5229 $output .= "</tbody>\n".
5230 "</table>\n"; # class="blame"
5231 $output .= "</div>\n"; # class="blame_body"
5232 close $fd
5233 or $output .= "Reading blob failed\n";
5235 # page footer
5236 $output .= git_footer_html();
5238 return $output;
5241 sub git_blame {
5242 return git_blame_common();
5245 sub git_blame_incremental {
5246 return git_blame_common('incremental');
5249 sub git_blame_data {
5250 return git_blame_common('data');
5253 sub git_tags {
5255 my $output = "";
5257 my $head = git_get_head_hash($project);
5258 $output .= git_header_html();
5259 $output .= git_print_page_nav('','', $head,undef,$head);
5260 $output .= git_print_header_div('summary', $project);
5262 my @tagslist = git_get_tags_list();
5263 if (@tagslist) {
5264 $output .= git_tags_body(\@tagslist);
5266 $output .= git_footer_html();
5268 return $output;
5271 sub git_heads {
5273 my $output = "";
5275 my $head = git_get_head_hash($project);
5276 $output .= git_header_html();
5277 $output .= git_print_page_nav('','', $head,undef,$head);
5278 $output .= git_print_header_div('summary', $project);
5280 my @headslist = git_get_heads_list();
5281 if (@headslist) {
5282 $output .= git_heads_body(\@headslist, $head);
5284 $output .= git_footer_html();
5286 return $output;
5289 sub git_blob_plain {
5290 my $type = shift;
5291 my $expires;
5293 my $output = "";
5295 if (!defined $hash) {
5296 if (defined $file_name) {
5297 my $base = $hash_base || git_get_head_hash($project);
5298 $hash = git_get_hash_by_path($base, $file_name, "blob")
5299 or die_error(404, "Cannot find file");
5300 } else {
5301 die_error(400, "No file name defined");
5303 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5304 # blobs defined by non-textual hash id's can be cached
5305 $expires = "+1d";
5308 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5309 or die_error(500, "Open git-cat-file blob '$hash' failed");
5311 # content-type (can include charset)
5312 $type = blob_contenttype($fd, $file_name, $type);
5314 # "save as" filename, even when no $file_name is given
5315 my $save_as = "$hash";
5316 if (defined $file_name) {
5317 $save_as = $file_name;
5318 } elsif ($type =~ m/^text\//) {
5319 $save_as .= '.txt';
5322 # With XSS prevention on, blobs of all types except a few known safe
5323 # ones are served with "Content-Disposition: attachment" to make sure
5324 # they don't run in our security domain. For certain image types,
5325 # blob view writes an <img> tag referring to blob_plain view, and we
5326 # want to be sure not to break that by serving the image as an
5327 # attachment (though Firefox 3 doesn't seem to care).
5328 my $sandbox = $prevent_xss &&
5329 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5331 $output .= $cgi->header(
5332 -type => $type,
5333 -expires => $expires,
5334 -content_disposition =>
5335 ($sandbox ? 'attachment' : 'inline')
5336 . '; filename="' . $save_as . '"');
5337 local $/ = undef;
5338 if( $cache_enable != 0){
5339 open BINOUT, '>', $fullhashbinpath or die_error(500, "Could not open bin dump file");
5340 }else{
5341 open BINOUT, '>', \$fullhashbinpath or die_error(500, "Could not open bin dump file");
5343 binmode BINOUT, ':raw';
5344 print BINOUT <$fd>;
5345 binmode BINOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5346 close BINOUT;
5347 close $fd;
5349 return $output;
5352 sub git_blob {
5353 my $expires;
5355 my $output = "";
5357 if (!defined $hash) {
5358 if (defined $file_name) {
5359 my $base = $hash_base || git_get_head_hash($project);
5360 $hash = git_get_hash_by_path($base, $file_name, "blob")
5361 or die_error(404, "Cannot find file");
5362 } else {
5363 die_error(400, "No file name defined");
5365 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5366 # blobs defined by non-textual hash id's can be cached
5367 $expires = "+1d";
5370 my $have_blame = gitweb_check_feature('blame');
5371 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5372 or die_error(500, "Couldn't cat $file_name, $hash");
5373 my $mimetype = blob_mimetype($fd, $file_name);
5374 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
5375 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5376 close $fd;
5377 return git_blob_plain($mimetype);
5379 # we can have blame only for text/* mimetype
5380 $have_blame &&= ($mimetype =~ m!^text/!);
5382 my $highlight = gitweb_check_feature('highlight');
5383 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
5384 $fd = run_highlighter($fd, $highlight, $syntax)
5385 if $syntax;
5387 $output .= git_header_html(undef, $expires);
5388 my $formats_nav = '';
5389 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5390 if (defined $file_name) {
5391 if ($have_blame) {
5392 $formats_nav .=
5393 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5394 "blame") .
5395 " | ";
5397 $formats_nav .=
5398 $cgi->a({-href => href(action=>"history", -replay=>1)},
5399 "history") .
5400 " | " .
5401 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5402 "raw") .
5403 " | " .
5404 $cgi->a({-href => href(action=>"blob",
5405 hash_base=>"HEAD", file_name=>$file_name)},
5406 "HEAD");
5407 } else {
5408 $formats_nav .=
5409 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5410 "raw");
5412 $output .= git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5413 $output .= git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5414 } else {
5415 $output .= "<div class=\"page_nav\">\n" .
5416 "<br/><br/></div>\n" .
5417 "<div class=\"title\">$hash</div>\n";
5419 $output .= git_print_page_path($file_name, "blob", $hash_base);
5420 $output .= "<div class=\"page_body\">\n";
5421 if ($mimetype =~ m!^image/!) {
5422 $output .= qq!<img type="$mimetype"!;
5423 if ($file_name) {
5424 $output .= qq! alt="$file_name" title="$file_name"!;
5426 $output .= qq! src="! .
5427 href(action=>"blob_plain", hash=>$hash,
5428 hash_base=>$hash_base, file_name=>$file_name) .
5429 qq!" />\n!;
5430 } else {
5431 my $nr;
5432 while (my $line = <$fd>) {
5433 chomp $line;
5434 $nr++;
5435 $line = untabify($line);
5436 $output .= sprintf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
5437 $nr, href(-replay => 1), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
5440 close $fd
5441 or $output .= "Reading blob failed.\n";
5442 $output .= "</div>";
5443 $output .= git_footer_html();
5445 return $output;
5448 sub git_tree {
5449 my $output = "";
5451 if (!defined $hash_base) {
5452 $hash_base = "HEAD";
5454 if (!defined $hash) {
5455 if (defined $file_name) {
5456 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5457 } else {
5458 $hash = $hash_base;
5461 die_error(404, "No such tree") unless defined($hash);
5463 my $show_sizes = gitweb_check_feature('show-sizes');
5464 my $have_blame = gitweb_check_feature('blame');
5466 my @entries = ();
5468 local $/ = "\0";
5469 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5470 ($show_sizes ? '-l' : ()), @extra_options, $hash
5471 or die_error(500, "Open git-ls-tree failed");
5472 @entries = map { chomp; $_ } <$fd>;
5473 close $fd
5474 or die_error(404, "Reading tree failed");
5477 my $refs = git_get_references();
5478 my $ref = format_ref_marker($refs, $hash_base);
5479 $output .= git_header_html();
5480 my $basedir = '';
5481 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5482 my @views_nav = ();
5483 if (defined $file_name) {
5484 push @views_nav,
5485 $cgi->a({-href => href(action=>"history", -replay=>1)},
5486 "history"),
5487 $cgi->a({-href => href(action=>"tree",
5488 hash_base=>"HEAD", file_name=>$file_name)},
5489 "HEAD"),
5491 my $snapshot_links = format_snapshot_links($hash);
5492 if (defined $snapshot_links) {
5493 # FIXME: Should be available when we have no hash base as well.
5494 push @views_nav, $snapshot_links;
5496 $output .= git_print_page_nav('tree','', $hash_base, undef, undef,
5497 join(' | ', @views_nav));
5498 $output .= git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5499 } else {
5500 undef $hash_base;
5501 $output .= "<div class=\"page_nav\">\n";
5502 $output .= "<br/><br/></div>\n";
5503 $output .= "<div class=\"title\">$hash</div>\n";
5505 if (defined $file_name) {
5506 $basedir = $file_name;
5507 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5508 $basedir .= '/';
5510 $output .= git_print_page_path($file_name, 'tree', $hash_base);
5512 $output .= "<div class=\"page_body\">\n";
5513 $output .= "<table class=\"tree\">\n";
5514 my $alternate = 1;
5515 # '..' (top directory) link if possible
5516 if (defined $hash_base &&
5517 defined $file_name && $file_name =~ m![^/]+$!) {
5518 if ($alternate) {
5519 $output .= "<tr class=\"dark\">\n";
5520 } else {
5521 $output .= "<tr class=\"light\">\n";
5523 $alternate ^= 1;
5525 my $up = $file_name;
5526 $up =~ s!/?[^/]+$!!;
5527 undef $up unless $up;
5528 # based on git_print_tree_entry
5529 $output .= '<td class="mode">' . mode_str('040000') . "</td>\n";
5530 $output .= '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
5531 $output .= '<td class="list">';
5532 $output .= $cgi->a({-href => href(action=>"tree",
5533 hash_base=>$hash_base,
5534 file_name=>$up)},
5535 "..");
5536 $output .= "</td>\n";
5537 $output .= "<td class=\"link\"></td>\n";
5539 $output .= "</tr>\n";
5541 foreach my $line (@entries) {
5542 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5544 if ($alternate) {
5545 $output .= "<tr class=\"dark\">\n";
5546 } else {
5547 $output .= "<tr class=\"light\">\n";
5549 $alternate ^= 1;
5551 $output .= git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5553 $output .= "</tr>\n";
5555 $output .= "</table>\n" .
5556 "</div>";
5557 $output .= git_footer_html();
5559 return $output;
5562 sub snapshot_name {
5563 my ($project, $hash) = @_;
5565 # path/to/project.git -> project
5566 # path/to/project/.git -> project
5567 my $name = to_utf8($project);
5568 $name =~ s,([^/])/*\.git$,$1,;
5569 $name = basename($name);
5570 # sanitize name
5571 $name =~ s/[[:cntrl:]]/?/g;
5573 my $ver = $hash;
5574 if ($hash =~ /^[0-9a-fA-F]+$/) {
5575 # shorten SHA-1 hash
5576 my $full_hash = git_get_full_hash($project, $hash);
5577 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
5578 $ver = git_get_short_hash($project, $hash);
5580 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
5581 # tags don't need shortened SHA-1 hash
5582 $ver = $1;
5583 } else {
5584 # branches and other need shortened SHA-1 hash
5585 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
5586 $ver = $1;
5588 $ver .= '-' . git_get_short_hash($project, $hash);
5590 # in case of hierarchical branch names
5591 $ver =~ s!/!.!g;
5593 # name = project-version_string
5594 $name = "$name-$ver";
5596 return wantarray ? ($name, $name) : $name;
5599 sub git_snapshot {
5600 my $output = "";
5602 my $format = $input_params{'snapshot_format'};
5603 if (!@snapshot_fmts) {
5604 die_error(403, "Snapshots not allowed");
5606 # default to first supported snapshot format
5607 $format ||= $snapshot_fmts[0];
5608 if ($format !~ m/^[a-z0-9]+$/) {
5609 die_error(400, "Invalid snapshot format parameter");
5610 } elsif (!exists($known_snapshot_formats{$format})) {
5611 die_error(400, "Unknown snapshot format");
5612 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5613 die_error(403, "Snapshot format not allowed");
5614 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5615 die_error(403, "Unsupported snapshot format");
5618 my $type = git_get_type("$hash^{}");
5619 if (!$type) {
5620 die_error(404, 'Object does not exist');
5621 } elsif ($type eq 'blob') {
5622 die_error(400, 'Object is not a tree-ish');
5625 my ($name, $prefix) = snapshot_name($project, $hash);
5626 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
5627 my $cmd = quote_command(
5628 git_cmd(), 'archive',
5629 "--format=$known_snapshot_formats{$format}{'format'}",
5630 "--prefix=$prefix/", $hash);
5631 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5632 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5635 $filename =~ s/(["\\])/\\$1/g;
5636 $output .= $cgi->header(
5637 -type => $known_snapshot_formats{$format}{'type'},
5638 -content_disposition => 'inline; filename="' . $filename . '"',
5639 -status => '200 OK');
5641 open my $fd, "-|", $cmd
5642 or die_error(500, "Execute git-archive failed");
5643 if( $cache_enable != 0){
5644 open BINOUT, '>', $fullhashbinpath or die_error(500, "Could not open bin dump file");
5645 }else{
5646 open BINOUT, '>', \$fullhashbinpath or die_error(500, "Could not open bin dump file");
5648 binmode BINOUT, ':raw';
5649 print BINOUT <$fd>;
5650 binmode BINOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5651 close BINOUT;
5652 close $fd;
5654 return $output;
5657 sub git_log_generic {
5658 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
5660 my $head = git_get_head_hash($project);
5662 my $output = "";
5664 if (!defined $base) {
5665 $base = $head;
5667 if (!defined $page) {
5668 $page = 0;
5670 my $refs = git_get_references();
5672 my $commit_hash = $base;
5673 if (defined $parent) {
5674 $commit_hash = "$parent..$base";
5676 my @commitlist =
5677 parse_commits($commit_hash, 101, (100 * $page),
5678 defined $file_name ? ($file_name, "--full-history") : ());
5680 my $ftype;
5681 if (!defined $file_hash && defined $file_name) {
5682 # some commits could have deleted file in question,
5683 # and not have it in tree, but one of them has to have it
5684 for (my $i = 0; $i < @commitlist; $i++) {
5685 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5686 last if defined $file_hash;
5689 if (defined $file_hash) {
5690 $ftype = git_get_type($file_hash);
5692 if (defined $file_name && !defined $ftype) {
5693 die_error(500, "Unknown type of object");
5695 my %co;
5696 if (defined $file_name) {
5697 %co = parse_commit($base)
5698 or die_error(404, "Unknown commit object");
5702 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
5703 my $next_link = '';
5704 if ($#commitlist >= 100) {
5705 $next_link =
5706 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5707 -accesskey => "n", -title => "Alt-n"}, "next");
5709 my $patch_max = gitweb_get_feature('patches');
5710 if ($patch_max && !defined $file_name) {
5711 if ($patch_max < 0 || @commitlist <= $patch_max) {
5712 $paging_nav .= " &sdot; " .
5713 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5714 "patches");
5718 $output .= git_header_html();
5719 $output .= git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
5720 if (defined $file_name) {
5721 $output .= git_print_header_div('commit', esc_html($co{'title'}), $base);
5722 } else {
5723 $output .= git_print_header_div('summary', $project)
5725 $output .= git_print_page_path($file_name, $ftype, $hash_base)
5726 if (defined $file_name);
5728 $output .= $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
5729 $file_name, $file_hash, $ftype);
5731 $output .= git_footer_html();
5733 return $output;
5736 sub git_log {
5737 return git_log_generic('log', \&git_log_body,
5738 $hash, $hash_parent);
5741 sub git_commit {
5742 $hash ||= $hash_base || "HEAD";
5744 my $output = "";
5746 my %co = parse_commit($hash)
5747 or die_error(404, "Unknown commit object");
5749 my $parent = $co{'parent'};
5750 my $parents = $co{'parents'}; # listref
5752 # we need to prepare $formats_nav before any parameter munging
5753 my $formats_nav;
5754 if (!defined $parent) {
5755 # --root commitdiff
5756 $formats_nav .= '(initial)';
5757 } elsif (@$parents == 1) {
5758 # single parent commit
5759 $formats_nav .=
5760 '(parent: ' .
5761 $cgi->a({-href => href(action=>"commit",
5762 hash=>$parent)},
5763 esc_html(substr($parent, 0, 7))) .
5764 ')';
5765 } else {
5766 # merge commit
5767 $formats_nav .=
5768 '(merge: ' .
5769 join(' ', map {
5770 $cgi->a({-href => href(action=>"commit",
5771 hash=>$_)},
5772 esc_html(substr($_, 0, 7)));
5773 } @$parents ) .
5774 ')';
5776 if (gitweb_check_feature('patches') && @$parents <= 1) {
5777 $formats_nav .= " | " .
5778 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5779 "patch");
5782 if (!defined $parent) {
5783 $parent = "--root";
5785 my @difftree;
5786 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5787 @diff_opts,
5788 (@$parents <= 1 ? $parent : '-c'),
5789 $hash, "--"
5790 or die_error(500, "Open git-diff-tree failed");
5791 @difftree = map { chomp; $_ } <$fd>;
5792 close $fd or die_error(404, "Reading git-diff-tree failed");
5794 # non-textual hash id's can be cached
5795 my $expires;
5796 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5797 $expires = "+1d";
5799 my $refs = git_get_references();
5800 my $ref = format_ref_marker($refs, $co{'id'});
5802 $output .= git_header_html(undef, $expires);
5803 $output .= git_print_page_nav('commit', '',
5804 $hash, $co{'tree'}, $hash,
5805 $formats_nav);
5807 if (defined $co{'parent'}) {
5808 $output .= git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5809 } else {
5810 $output .= git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5812 $output .= "<div class=\"title_text\">\n" .
5813 "<table class=\"object_header\">\n";
5814 $output .= git_print_authorship_rows(\%co);
5815 $output .= "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5816 $output .= "<tr>" .
5817 "<td>tree</td>" .
5818 "<td class=\"sha1\">" .
5819 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5820 class => "list"}, $co{'tree'}) .
5821 "</td>" .
5822 "<td class=\"link\">" .
5823 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5824 "tree");
5825 my $snapshot_links = format_snapshot_links($hash);
5826 if (defined $snapshot_links) {
5827 $output .= " | " . $snapshot_links;
5829 $output .= "</td>" .
5830 "</tr>\n";
5832 foreach my $par (@$parents) {
5833 $output .= "<tr>" .
5834 "<td>parent</td>" .
5835 "<td class=\"sha1\">" .
5836 $cgi->a({-href => href(action=>"commit", hash=>$par),
5837 class => "list"}, $par) .
5838 "</td>" .
5839 "<td class=\"link\">" .
5840 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5841 " | " .
5842 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5843 "</td>" .
5844 "</tr>\n";
5846 $output .= "</table>".
5847 "</div>\n";
5849 $output .= "<div class=\"page_body\">\n";
5850 $output .= git_print_log($co{'comment'});
5851 $output .= "</div>\n";
5853 $output .= git_difftree_body(\@difftree, $hash, @$parents);
5855 $output .= git_footer_html();
5857 return $output;
5860 sub git_object {
5861 # object is defined by:
5862 # - hash or hash_base alone
5863 # - hash_base and file_name
5864 my $type;
5866 # - hash or hash_base alone
5867 if ($hash || ($hash_base && !defined $file_name)) {
5868 my $object_id = $hash || $hash_base;
5870 open my $fd, "-|", quote_command(
5871 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5872 or die_error(404, "Object does not exist");
5873 $type = <$fd>;
5874 chomp $type;
5875 close $fd
5876 or die_error(404, "Object does not exist");
5878 # - hash_base and file_name
5879 } elsif ($hash_base && defined $file_name) {
5880 $file_name =~ s,/+$,,;
5882 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5883 or die_error(404, "Base object does not exist");
5885 # here errors should not hapen
5886 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5887 or die_error(500, "Open git-ls-tree failed");
5888 my $line = <$fd>;
5889 close $fd;
5891 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5892 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5893 die_error(404, "File or directory for given base does not exist");
5895 $type = $2;
5896 $hash = $3;
5897 } else {
5898 die_error(400, "Not enough information to find object");
5901 return $cgi->redirect(-uri => href(action=>$type, -full=>1,
5902 hash=>$hash, hash_base=>$hash_base,
5903 file_name=>$file_name),
5904 -status => '302 Found');
5907 sub git_blobdiff {
5908 my $format = shift || 'html';
5910 my $fd;
5911 my @difftree;
5912 my %diffinfo;
5913 my $expires;
5915 my $output = "";
5917 # preparing $fd and %diffinfo for git_patchset_body
5918 # new style URI
5919 if (defined $hash_base && defined $hash_parent_base) {
5920 if (defined $file_name) {
5921 # read raw output
5922 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5923 $hash_parent_base, $hash_base,
5924 "--", (defined $file_parent ? $file_parent : ()), $file_name
5925 or die_error(500, "Open git-diff-tree failed");
5926 @difftree = map { chomp; $_ } <$fd>;
5927 close $fd
5928 or die_error(404, "Reading git-diff-tree failed");
5929 @difftree
5930 or die_error(404, "Blob diff not found");
5932 } elsif (defined $hash &&
5933 $hash =~ /[0-9a-fA-F]{40}/) {
5934 # try to find filename from $hash
5936 # read filtered raw output
5937 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5938 $hash_parent_base, $hash_base, "--"
5939 or die_error(500, "Open git-diff-tree failed");
5940 @difftree =
5941 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5942 # $hash == to_id
5943 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5944 map { chomp; $_ } <$fd>;
5945 close $fd
5946 or die_error(404, "Reading git-diff-tree failed");
5947 @difftree
5948 or die_error(404, "Blob diff not found");
5950 } else {
5951 die_error(400, "Missing one of the blob diff parameters");
5954 if (@difftree > 1) {
5955 die_error(400, "Ambiguous blob diff specification");
5958 %diffinfo = parse_difftree_raw_line($difftree[0]);
5959 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5960 $file_name ||= $diffinfo{'to_file'};
5962 $hash_parent ||= $diffinfo{'from_id'};
5963 $hash ||= $diffinfo{'to_id'};
5965 # non-textual hash id's can be cached
5966 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5967 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5968 $expires = '+1d';
5971 # open patch output
5972 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5973 '-p', ($format eq 'html' ? "--full-index" : ()),
5974 $hash_parent_base, $hash_base,
5975 "--", (defined $file_parent ? $file_parent : ()), $file_name
5976 or die_error(500, "Open git-diff-tree failed");
5979 # old/legacy style URI -- not generated anymore since 1.4.3.
5980 if (!%diffinfo) {
5981 die_error('404 Not Found', "Missing one of the blob diff parameters")
5984 # header
5985 if ($format eq 'html') {
5986 my $formats_nav =
5987 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5988 "raw");
5989 $output .= git_header_html(undef, $expires);
5990 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5991 $output .= git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5992 $output .= git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5993 } else {
5994 $output .= "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5995 $output .= "<div class=\"title\">$hash vs $hash_parent</div>\n";
5997 if (defined $file_name) {
5998 $output .= git_print_page_path($file_name, "blob", $hash_base);
5999 } else {
6000 $output .= "<div class=\"page_path\"></div>\n";
6003 } elsif ($format eq 'plain') {
6004 $output .= $cgi->header(
6005 -type => 'text/plain',
6006 -charset => 'utf-8',
6007 -expires => $expires,
6008 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6010 $output .= "X-Git-Url: " . $cgi->self_url() . "\n\n";
6012 } else {
6013 die_error(400, "Unknown blobdiff format");
6016 # patch
6017 if ($format eq 'html') {
6018 $output .= "<div class=\"page_body\">\n";
6020 $output .= git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6021 close $fd;
6023 $output .= "</div>\n"; # class="page_body"
6024 $output .= git_footer_html();
6026 } else {
6027 while (my $line = <$fd>) {
6028 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6029 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6031 $output .= $line;
6033 last if $line =~ m!^\+\+\+!;
6035 local $/ = undef;
6036 $output .= <$fd>;
6037 close $fd;
6040 return $output;
6043 sub git_blobdiff_plain {
6044 return git_blobdiff('plain');
6047 sub git_commitdiff {
6048 my %params = @_;
6049 my $format = $params{-format} || 'html';
6051 my $output = "";
6053 my ($patch_max) = gitweb_get_feature('patches');
6054 if ($format eq 'patch') {
6055 die_error(403, "Patch view not allowed") unless $patch_max;
6058 $hash ||= $hash_base || "HEAD";
6059 my %co = parse_commit($hash)
6060 or die_error(404, "Unknown commit object");
6062 # choose format for commitdiff for merge
6063 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6064 $hash_parent = '--cc';
6066 # we need to prepare $formats_nav before almost any parameter munging
6067 my $formats_nav;
6068 if ($format eq 'html') {
6069 $formats_nav =
6070 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6071 "raw");
6072 if ($patch_max && @{$co{'parents'}} <= 1) {
6073 $formats_nav .= " | " .
6074 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6075 "patch");
6078 if (defined $hash_parent &&
6079 $hash_parent ne '-c' && $hash_parent ne '--cc') {
6080 # commitdiff with two commits given
6081 my $hash_parent_short = $hash_parent;
6082 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6083 $hash_parent_short = substr($hash_parent, 0, 7);
6085 $formats_nav .=
6086 ' (from';
6087 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6088 if ($co{'parents'}[$i] eq $hash_parent) {
6089 $formats_nav .= ' parent ' . ($i+1);
6090 last;
6093 $formats_nav .= ': ' .
6094 $cgi->a({-href => href(action=>"commitdiff",
6095 hash=>$hash_parent)},
6096 esc_html($hash_parent_short)) .
6097 ')';
6098 } elsif (!$co{'parent'}) {
6099 # --root commitdiff
6100 $formats_nav .= ' (initial)';
6101 } elsif (scalar @{$co{'parents'}} == 1) {
6102 # single parent commit
6103 $formats_nav .=
6104 ' (parent: ' .
6105 $cgi->a({-href => href(action=>"commitdiff",
6106 hash=>$co{'parent'})},
6107 esc_html(substr($co{'parent'}, 0, 7))) .
6108 ')';
6109 } else {
6110 # merge commit
6111 if ($hash_parent eq '--cc') {
6112 $formats_nav .= ' | ' .
6113 $cgi->a({-href => href(action=>"commitdiff",
6114 hash=>$hash, hash_parent=>'-c')},
6115 'combined');
6116 } else { # $hash_parent eq '-c'
6117 $formats_nav .= ' | ' .
6118 $cgi->a({-href => href(action=>"commitdiff",
6119 hash=>$hash, hash_parent=>'--cc')},
6120 'compact');
6122 $formats_nav .=
6123 ' (merge: ' .
6124 join(' ', map {
6125 $cgi->a({-href => href(action=>"commitdiff",
6126 hash=>$_)},
6127 esc_html(substr($_, 0, 7)));
6128 } @{$co{'parents'}} ) .
6129 ')';
6133 my $hash_parent_param = $hash_parent;
6134 if (!defined $hash_parent_param) {
6135 # --cc for multiple parents, --root for parentless
6136 $hash_parent_param =
6137 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6140 # read commitdiff
6141 my $fd;
6142 my @difftree;
6143 if ($format eq 'html') {
6144 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6145 "--no-commit-id", "--patch-with-raw", "--full-index",
6146 $hash_parent_param, $hash, "--"
6147 or die_error(500, "Open git-diff-tree failed");
6149 while (my $line = <$fd>) {
6150 chomp $line;
6151 # empty line ends raw part of diff-tree output
6152 last unless $line;
6153 push @difftree, scalar parse_difftree_raw_line($line);
6156 } elsif ($format eq 'plain') {
6157 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6158 '-p', $hash_parent_param, $hash, "--"
6159 or die_error(500, "Open git-diff-tree failed");
6160 } elsif ($format eq 'patch') {
6161 # For commit ranges, we limit the output to the number of
6162 # patches specified in the 'patches' feature.
6163 # For single commits, we limit the output to a single patch,
6164 # diverging from the git-format-patch default.
6165 my @commit_spec = ();
6166 if ($hash_parent) {
6167 if ($patch_max > 0) {
6168 push @commit_spec, "-$patch_max";
6170 push @commit_spec, '-n', "$hash_parent..$hash";
6171 } else {
6172 if ($params{-single}) {
6173 push @commit_spec, '-1';
6174 } else {
6175 if ($patch_max > 0) {
6176 push @commit_spec, "-$patch_max";
6178 push @commit_spec, "-n";
6180 push @commit_spec, '--root', $hash;
6182 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
6183 '--encoding=utf8', '--stdout', @commit_spec
6184 or die_error(500, "Open git-format-patch failed");
6185 } else {
6186 die_error(400, "Unknown commitdiff format");
6189 # non-textual hash id's can be cached
6190 my $expires;
6191 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6192 $expires = "+1d";
6195 # write commit message
6196 if ($format eq 'html') {
6197 my $refs = git_get_references();
6198 my $ref = format_ref_marker($refs, $co{'id'});
6200 $output .= git_header_html(undef, $expires);
6201 $output .= git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6202 $output .= git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6203 $output .= "<div class=\"title_text\">\n" .
6204 "<table class=\"object_header\">\n";
6205 $output .= git_print_authorship_rows(\%co);
6206 $output .= "</table>".
6207 "</div>\n";
6208 $output .= "<div class=\"page_body\">\n";
6209 if (@{$co{'comment'}} > 1) {
6210 $output .= "<div class=\"log\">\n";
6211 $output .= git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6212 $output .= "</div>\n"; # class="log"
6215 } elsif ($format eq 'plain') {
6216 my $refs = git_get_references("tags");
6217 my $tagname = git_get_rev_name_tags($hash);
6218 my $filename = basename($project) . "-$hash.patch";
6220 $output .= $cgi->header(
6221 -type => 'text/plain',
6222 -charset => 'utf-8',
6223 -expires => $expires,
6224 -content_disposition => 'inline; filename="' . "$filename" . '"');
6225 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6226 $output .= "From: " . to_utf8($co{'author'}) . "\n";
6227 $output .= "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6228 $output .= "Subject: " . to_utf8($co{'title'}) . "\n";
6230 $output .= "X-Git-Tag: $tagname\n" if $tagname;
6231 $output .= "X-Git-Url: " . $cgi->self_url() . "\n\n";
6233 foreach my $line (@{$co{'comment'}}) {
6234 $output .= to_utf8($line) . "\n";
6236 $output .= "---\n\n";
6237 } elsif ($format eq 'patch') {
6238 my $filename = basename($project) . "-$hash.patch";
6240 $output .= $cgi->header(
6241 -type => 'text/plain',
6242 -charset => 'utf-8',
6243 -expires => $expires,
6244 -content_disposition => 'inline; filename="' . "$filename" . '"');
6247 # write patch
6248 if ($format eq 'html') {
6249 my $use_parents = !defined $hash_parent ||
6250 $hash_parent eq '-c' || $hash_parent eq '--cc';
6251 $output .= git_difftree_body(\@difftree, $hash,
6252 $use_parents ? @{$co{'parents'}} : $hash_parent);
6253 $output .= "<br/>\n";
6255 $output .= git_patchset_body($fd, \@difftree, $hash,
6256 $use_parents ? @{$co{'parents'}} : $hash_parent);
6257 close $fd;
6258 $output .= "</div>\n"; # class="page_body"
6259 $output .= git_footer_html();
6261 } elsif ($format eq 'plain') {
6262 local $/ = undef;
6263 $output .= <$fd>;
6264 close $fd
6265 or $output .= "Reading git-diff-tree failed\n";
6266 } elsif ($format eq 'patch') {
6267 local $/ = undef;
6268 $output .= <$fd>;
6269 close $fd
6270 or $output .= "Reading git-format-patch failed\n";
6273 return $output;
6276 sub git_commitdiff_plain {
6277 return git_commitdiff(-format => 'plain');
6280 # format-patch-style patches
6281 sub git_patch {
6282 return git_commitdiff(-format => 'patch', -single => 1);
6285 sub git_patches {
6286 return git_commitdiff(-format => 'patch');
6289 sub git_history {
6290 my $output = "";
6291 $output .= git_log_generic('history', \&git_history_body,
6292 $hash_base, $hash_parent_base,
6293 $file_name, $hash);
6295 return $output;
6298 sub git_search {
6299 my $output = "";
6301 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6302 if (!defined $searchtext) {
6303 die_error(400, "Text field is empty");
6305 if (!defined $hash) {
6306 $hash = git_get_head_hash($project);
6308 my %co = parse_commit($hash);
6309 if (!%co) {
6310 die_error(404, "Unknown commit object");
6312 if (!defined $page) {
6313 $page = 0;
6316 $searchtype ||= 'commit';
6317 if ($searchtype eq 'pickaxe') {
6318 # pickaxe may take all resources of your box and run for several minutes
6319 # with every query - so decide by yourself how public you make this feature
6320 gitweb_check_feature('pickaxe')
6321 or die_error(403, "Pickaxe is disabled");
6323 if ($searchtype eq 'grep') {
6324 gitweb_check_feature('grep')
6325 or die_error(403, "Grep is disabled");
6328 $output .= git_header_html();
6330 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6331 my $greptype;
6332 if ($searchtype eq 'commit') {
6333 $greptype = "--grep=";
6334 } elsif ($searchtype eq 'author') {
6335 $greptype = "--author=";
6336 } elsif ($searchtype eq 'committer') {
6337 $greptype = "--committer=";
6339 $greptype .= $searchtext;
6340 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6341 $greptype, '--regexp-ignore-case',
6342 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6344 my $paging_nav = '';
6345 if ($page > 0) {
6346 $paging_nav .=
6347 $cgi->a({-href => href(action=>"search", hash=>$hash,
6348 searchtext=>$searchtext,
6349 searchtype=>$searchtype)},
6350 "first");
6351 $paging_nav .= " &sdot; " .
6352 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6353 -accesskey => "p", -title => "Alt-p"}, "prev");
6354 } else {
6355 $paging_nav .= "first";
6356 $paging_nav .= " &sdot; prev";
6358 my $next_link = '';
6359 if ($#commitlist >= 100) {
6360 $next_link =
6361 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6362 -accesskey => "n", -title => "Alt-n"}, "next");
6363 $paging_nav .= " &sdot; $next_link";
6364 } else {
6365 $paging_nav .= " &sdot; next";
6368 $output .= git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6369 $output .= git_print_header_div('commit', esc_html($co{'title'}), $hash);
6370 if ($page == 0 && !@commitlist) {
6371 $output .= "<p>No match.</p>\n";
6372 } else {
6373 $output .= git_search_grep_body(\@commitlist, 0, 99, $next_link);
6377 if ($searchtype eq 'pickaxe') {
6378 $output .= git_print_page_nav('','', $hash,$co{'tree'},$hash);
6379 $output .= git_print_header_div('commit', esc_html($co{'title'}), $hash);
6381 $output .= "<table class=\"pickaxe search\">\n";
6382 my $alternate = 1;
6383 local $/ = "\n";
6384 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6385 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6386 ($search_use_regexp ? '--pickaxe-regex' : ());
6387 undef %co;
6388 my @files;
6389 while (my $line = <$fd>) {
6390 chomp $line;
6391 next unless $line;
6393 my %set = parse_difftree_raw_line($line);
6394 if (defined $set{'commit'}) {
6395 # finish previous commit
6396 if (%co) {
6397 $output .= "</td>\n" .
6398 "<td class=\"link\">" .
6399 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6400 " | " .
6401 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6402 $output .= "</td>\n" .
6403 "</tr>\n";
6406 if ($alternate) {
6407 $output .= "<tr class=\"dark\">\n";
6408 } else {
6409 $output .= "<tr class=\"light\">\n";
6411 $alternate ^= 1;
6412 %co = parse_commit($set{'commit'});
6413 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6414 $output .= "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6415 "<td><i>$author</i></td>\n" .
6416 "<td>" .
6417 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6418 -class => "list subject"},
6419 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6420 } elsif (defined $set{'to_id'}) {
6421 next if ($set{'to_id'} =~ m/^0{40}$/);
6423 $output .= $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6424 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6425 -class => "list"},
6426 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6427 "<br/>\n";
6430 close $fd;
6432 # finish last commit (warning: repetition!)
6433 if (%co) {
6434 $output .= "</td>\n" .
6435 "<td class=\"link\">" .
6436 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6437 " | " .
6438 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6439 $output .= "</td>\n" .
6440 "</tr>\n";
6443 $output .= "</table>\n";
6446 if ($searchtype eq 'grep') {
6447 $output .= git_print_page_nav('','', $hash,$co{'tree'},$hash);
6448 $output .= git_print_header_div('commit', esc_html($co{'title'}), $hash);
6450 $output .= "<table class=\"grep_search\">\n";
6451 my $alternate = 1;
6452 my $matches = 0;
6453 local $/ = "\n";
6454 open my $fd, "-|", git_cmd(), 'grep', '-n',
6455 $search_use_regexp ? ('-E', '-i') : '-F',
6456 $searchtext, $co{'tree'};
6457 my $lastfile = '';
6458 while (my $line = <$fd>) {
6459 chomp $line;
6460 my ($file, $lno, $ltext, $binary);
6461 last if ($matches++ > 1000);
6462 if ($line =~ /^Binary file (.+) matches$/) {
6463 $file = $1;
6464 $binary = 1;
6465 } else {
6466 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6468 if ($file ne $lastfile) {
6469 $lastfile and $output .= "</td></tr>\n";
6470 if ($alternate++) {
6471 $output .= "<tr class=\"dark\">\n";
6472 } else {
6473 $output .= "<tr class=\"light\">\n";
6475 $output .= "<td class=\"list\">".
6476 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6477 file_name=>"$file"),
6478 -class => "list"}, esc_path($file));
6479 $output .= "</td><td>\n";
6480 $lastfile = $file;
6482 if ($binary) {
6483 $output .= "<div class=\"binary\">Binary file</div>\n";
6484 } else {
6485 $ltext = untabify($ltext);
6486 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6487 $ltext = esc_html($1, -nbsp=>1);
6488 $ltext .= '<span class="match">';
6489 $ltext .= esc_html($2, -nbsp=>1);
6490 $ltext .= '</span>';
6491 $ltext .= esc_html($3, -nbsp=>1);
6492 } else {
6493 $ltext = esc_html($ltext, -nbsp=>1);
6495 $output .= "<div class=\"pre\">" .
6496 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6497 file_name=>"$file").'#l'.$lno,
6498 -class => "linenr"}, sprintf('%4i', $lno))
6499 . ' ' . $ltext . "</div>\n";
6502 if ($lastfile) {
6503 $output .= "</td></tr>\n";
6504 if ($matches > 1000) {
6505 $output .= "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6507 } else {
6508 $output .= "<div class=\"diff nodifferences\">No matches found</div>\n";
6510 close $fd;
6512 $output .= "</table>\n";
6514 $output .= git_footer_html();
6516 return $output;
6519 sub git_search_help {
6520 my $output = "";
6522 $output .= git_header_html();
6523 $output .= git_print_page_nav('','', $hash,$hash,$hash);
6524 $output .= <<EOT;
6525 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6526 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6527 the pattern entered is recognized as the POSIX extended
6528 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6529 insensitive).</p>
6530 <dl>
6531 <dt><b>commit</b></dt>
6532 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6534 my $have_grep = gitweb_check_feature('grep');
6535 if ($have_grep) {
6536 $output .= <<EOT;
6537 <dt><b>grep</b></dt>
6538 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6539 a different one) are searched for the given pattern. On large trees, this search can take
6540 a while and put some strain on the server, so please use it with some consideration. Note that
6541 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6542 case-sensitive.</dd>
6545 $output .= <<EOT;
6546 <dt><b>author</b></dt>
6547 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6548 <dt><b>committer</b></dt>
6549 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6551 my $have_pickaxe = gitweb_check_feature('pickaxe');
6552 if ($have_pickaxe) {
6553 $output .= <<EOT;
6554 <dt><b>pickaxe</b></dt>
6555 <dd>All commits that caused the string to appear or disappear from any file (changes that
6556 added, removed or "modified" the string) will be listed. This search can take a while and
6557 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6558 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6561 $output .= "</dl>\n";
6562 $output .= git_footer_html();
6564 return $output;
6567 sub git_shortlog {
6568 my $output = "";
6569 $output .= git_log_generic('shortlog', \&git_shortlog_body,
6570 $hash, $hash_parent);
6572 return $output;
6575 ## ......................................................................
6576 ## feeds (RSS, Atom; OPML)
6578 sub git_feed {
6579 my $format = shift || 'atom';
6580 my $have_blame = gitweb_check_feature('blame');
6582 my $output = "";
6584 # Atom: http://www.atomenabled.org/developers/syndication/
6585 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6586 if ($format ne 'rss' && $format ne 'atom') {
6587 die_error(400, "Unknown web feed format");
6590 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6591 my $head = $hash || 'HEAD';
6592 my @commitlist = parse_commits($head, 150, 0, $file_name);
6594 my %latest_commit;
6595 my %latest_date;
6596 my $content_type = "application/$format+xml";
6597 if (defined $cgi->http('HTTP_ACCEPT') &&
6598 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6599 # browser (feed reader) prefers text/xml
6600 $content_type = 'text/xml';
6602 if (defined($commitlist[0])) {
6603 %latest_commit = %{$commitlist[0]};
6604 my $latest_epoch = $latest_commit{'committer_epoch'};
6605 %latest_date = parse_date($latest_epoch);
6606 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6607 if (defined $if_modified) {
6608 my $since;
6609 if (eval { require HTTP::Date; 1; }) {
6610 $since = HTTP::Date::str2time($if_modified);
6611 } elsif (eval { require Time::ParseDate; 1; }) {
6612 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6614 if (defined $since && $latest_epoch <= $since) {
6615 $output .= $cgi->header(
6616 -type => $content_type,
6617 -charset => 'utf-8',
6618 -last_modified => $latest_date{'rfc2822'},
6619 -status => '304 Not Modified');
6620 return $output;
6623 $output .= $cgi->header(
6624 -type => $content_type,
6625 -charset => 'utf-8',
6626 -last_modified => $latest_date{'rfc2822'});
6627 } else {
6628 $output .= $cgi->header(
6629 -type => $content_type,
6630 -charset => 'utf-8');
6633 # Optimization: skip generating the body if client asks only
6634 # for Last-Modified date.
6635 return if ($cgi->request_method() eq 'HEAD');
6637 # header variables
6638 my $title = "$site_name - $project/$action";
6639 my $feed_type = 'log';
6640 if (defined $hash) {
6641 $title .= " - '$hash'";
6642 $feed_type = 'branch log';
6643 if (defined $file_name) {
6644 $title .= " :: $file_name";
6645 $feed_type = 'history';
6647 } elsif (defined $file_name) {
6648 $title .= " - $file_name";
6649 $feed_type = 'history';
6651 $title .= " $feed_type";
6652 my $descr = git_get_project_description($project);
6653 if (defined $descr) {
6654 $descr = esc_html($descr);
6655 } else {
6656 $descr = "$project " .
6657 ($format eq 'rss' ? 'RSS' : 'Atom') .
6658 " feed";
6660 my $owner = git_get_project_owner($project);
6661 $owner = esc_html($owner);
6663 #header
6664 my $alt_url;
6665 if (defined $file_name) {
6666 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6667 } elsif (defined $hash) {
6668 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6669 } else {
6670 $alt_url = href(-full=>1, action=>"summary");
6672 $output .= qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6673 if ($format eq 'rss') {
6674 $output .= <<XML;
6675 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6676 <channel>
6678 $output .= "<title>$title</title>\n" .
6679 "<link>$alt_url</link>\n" .
6680 "<description>$descr</description>\n" .
6681 "<language>en</language>\n" .
6682 # project owner is responsible for 'editorial' content
6683 "<managingEditor>$owner</managingEditor>\n";
6684 if (defined $logo || defined $favicon) {
6685 # prefer the logo to the favicon, since RSS
6686 # doesn't allow both
6687 my $img = esc_url($logo || $favicon);
6688 $output .= "<image>\n" .
6689 "<url>$img</url>\n" .
6690 "<title>$title</title>\n" .
6691 "<link>$alt_url</link>\n" .
6692 "</image>\n";
6694 if (%latest_date) {
6695 $output .= "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6696 $output .= "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6698 $output .= "<generator>gitweb v.$version/$git_version</generator>\n";
6699 } elsif ($format eq 'atom') {
6700 $output .= <<XML;
6701 <feed xmlns="http://www.w3.org/2005/Atom">
6703 $output .= "<title>$title</title>\n" .
6704 "<subtitle>$descr</subtitle>\n" .
6705 '<link rel="alternate" type="text/html" href="' .
6706 $alt_url . '" />' . "\n" .
6707 '<link rel="self" type="' . $content_type . '" href="' .
6708 $cgi->self_url() . '" />' . "\n" .
6709 "<id>" . href(-full=>1) . "</id>\n" .
6710 # use project owner for feed author
6711 "<author><name>$owner</name></author>\n";
6712 if (defined $favicon) {
6713 $output .= "<icon>" . esc_url($favicon) . "</icon>\n";
6715 if (defined $logo_url) {
6716 # not twice as wide as tall: 72 x 27 pixels
6717 $output .= "<logo>" . esc_url($logo) . "</logo>\n";
6719 if (! %latest_date) {
6720 # dummy date to keep the feed valid until commits trickle in:
6721 $output .= "<updated>1970-01-01T00:00:00Z</updated>\n";
6722 } else {
6723 $output .= "<updated>$latest_date{'iso-8601'}</updated>\n";
6725 $output .= "<generator version='$version/$git_version'>gitweb</generator>\n";
6728 # contents
6729 for (my $i = 0; $i <= $#commitlist; $i++) {
6730 my %co = %{$commitlist[$i]};
6731 my $commit = $co{'id'};
6732 # we read 150, we always show 30 and the ones more recent than 48 hours
6733 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6734 last;
6736 my %cd = parse_date($co{'author_epoch'});
6738 # get list of changed files
6739 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6740 $co{'parent'} || "--root",
6741 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6742 or next;
6743 my @difftree = map { chomp; $_ } <$fd>;
6744 close $fd
6745 or next;
6747 # print element (entry, item)
6748 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6749 if ($format eq 'rss') {
6750 $output .= "<item>\n" .
6751 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6752 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6753 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6754 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6755 "<link>$co_url</link>\n" .
6756 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6757 "<content:encoded>" .
6758 "<![CDATA[\n";
6759 } elsif ($format eq 'atom') {
6760 $output .= "<entry>\n" .
6761 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6762 "<updated>$cd{'iso-8601'}</updated>\n" .
6763 "<author>\n" .
6764 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6765 if ($co{'author_email'}) {
6766 $output .= " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6768 $output .= "</author>\n" .
6769 # use committer for contributor
6770 "<contributor>\n" .
6771 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6772 if ($co{'committer_email'}) {
6773 $output .= " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6775 $output .= "</contributor>\n" .
6776 "<published>$cd{'iso-8601'}</published>\n" .
6777 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6778 "<id>$co_url</id>\n" .
6779 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6780 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6782 my $comment = $co{'comment'};
6783 $output .= "<pre>\n";
6784 foreach my $line (@$comment) {
6785 $line = esc_html($line);
6786 $output .= "$line\n";
6788 $output .= "</pre><ul>\n";
6789 foreach my $difftree_line (@difftree) {
6790 my %difftree = parse_difftree_raw_line($difftree_line);
6791 next if !$difftree{'from_id'};
6793 my $file = $difftree{'file'} || $difftree{'to_file'};
6795 $output .= "<li>" .
6796 "[" .
6797 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6798 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6799 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6800 file_name=>$file, file_parent=>$difftree{'from_file'}),
6801 -title => "diff"}, 'D');
6802 if ($have_blame) {
6803 $output .= $cgi->a({-href => href(-full=>1, action=>"blame",
6804 file_name=>$file, hash_base=>$commit),
6805 -title => "blame"}, 'B');
6807 # if this is not a feed of a file history
6808 if (!defined $file_name || $file_name ne $file) {
6809 $output .= $cgi->a({-href => href(-full=>1, action=>"history",
6810 file_name=>$file, hash=>$commit),
6811 -title => "history"}, 'H');
6813 $file = esc_path($file);
6814 $output .= "] ".
6815 "$file</li>\n";
6817 if ($format eq 'rss') {
6818 $output .= "</ul>]]>\n" .
6819 "</content:encoded>\n" .
6820 "</item>\n";
6821 } elsif ($format eq 'atom') {
6822 $output .= "</ul>\n</div>\n" .
6823 "</content>\n" .
6824 "</entry>\n";
6828 # end of feed
6829 if ($format eq 'rss') {
6830 $output .= "</channel>\n</rss>\n";
6831 } elsif ($format eq 'atom') {
6832 $output .= "</feed>\n";
6835 return $output;
6838 sub git_rss {
6839 return git_feed('rss');
6842 sub git_atom {
6843 return git_feed('atom');
6846 sub git_opml {
6847 my @list = git_get_projects_list();
6848 my $output = "";
6850 $output .= $cgi->header(
6851 -type => 'text/xml',
6852 -charset => 'utf-8',
6853 -content_disposition => 'inline; filename="opml.xml"');
6855 $output .= <<XML;
6856 <?xml version="1.0" encoding="utf-8"?>
6857 <opml version="1.0">
6858 <head>
6859 <title>$site_name OPML Export</title>
6860 </head>
6861 <body>
6862 <outline text="git RSS feeds">
6865 foreach my $pr (@list) {
6866 my %proj = %$pr;
6867 my $head = git_get_head_hash($proj{'path'});
6868 if (!defined $head) {
6869 next;
6871 $git_dir = "$projectroot/$proj{'path'}";
6872 my %co = parse_commit($head);
6873 if (!%co) {
6874 next;
6877 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6878 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6879 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6880 $output .= "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6882 $output .= <<XML;
6883 </outline>
6884 </body>
6885 </opml>
6888 return $output;