gitweb: Add git_addrepo sub
[git/gsoc2010-gitweb.git] / gitweb / gitweb.perl
blob1ca32e6644f1a0b3fcb1a97d1f1f0b1c81b2bb11
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;
13 use File::Spec;
14 # __DIR__ is taken from Dir::Self __DIR__ fragment
15 sub __DIR__ () {
16 File::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);
18 use lib __DIR__ . '/lib';
20 use CGI qw(:standard :escapeHTML -nosticky);
21 use CGI::Carp qw(fatalsToBrowser set_message);
22 use Fcntl ':mode';
23 use File::Find qw();
24 use File::Basename qw(basename);
26 binmode STDOUT, ':utf8';
28 use Gitweb::Git;
29 use Gitweb::Config;
30 use Gitweb::Request;
31 use Gitweb::Escape;
32 use Gitweb::RepoConfig;
33 use Gitweb::View;
34 use Gitweb::Util;
35 use Gitweb::Format;
36 use Gitweb::Parse;
38 BEGIN {
39 CGI->compile() if $ENV{'MOD_PERL'};
42 # Only configuration variables with build-time overridable
43 # defaults are listed below. The complete set of variables
44 # with their descriptions is listed in Gitweb::Config.
45 $version = "++GIT_VERSION++";
47 # $GIT is from Gitweb::Git
48 $GIT = "++GIT_BINDIR++/git";
50 $projectroot = "++GITWEB_PROJECTROOT++";
51 $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
53 $home_link_str = "++GITWEB_HOME_LINK_STR++";
54 $site_name = "++GITWEB_SITENAME++"
55 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
56 $site_header = "++GITWEB_SITE_HEADER++";
57 $home_text = "++GITWEB_HOMETEXT++";
58 $site_footer = "++GITWEB_SITE_FOOTER++";
60 @stylesheets = ("++GITWEB_CSS++");
61 $stylesheet = undef;
62 $logo = "++GITWEB_LOGO++";
63 $favicon = "++GITWEB_FAVICON++";
64 $javascript = "++GITWEB_JS++";
66 $projects_list = "++GITWEB_LIST++";
68 $export_ok = "++GITWEB_EXPORT_OK++";
69 $strict_export = "++GITWEB_STRICT_EXPORT++";
71 @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
73 $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
74 $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
76 # Get loadavg of system, to compare against $maxload.
77 # Currently it requires '/proc/loadavg' present to get loadavg;
78 # if it is not present it returns 0, which means no load checking.
79 sub get_loadavg {
80 if( -e '/proc/loadavg' ){
81 open my $fd, '<', '/proc/loadavg'
82 or return 0;
83 my @load = split(/\s+/, scalar <$fd>);
84 close $fd;
86 # The first three columns measure CPU and IO utilization of the last one,
87 # five, and 10 minute periods. The fourth column shows the number of
88 # currently running processes and the total number of processes in the m/n
89 # format. The last column displays the last process ID used.
90 return $load[0] || 0;
92 # additional checks for load average should go here for things that don't export
93 # /proc/loadavg
95 return 0;
98 sub check_loadavg {
99 if (defined $maxload && get_loadavg() > $maxload) {
100 die_error(503, "The load average on the server is too high");
104 # ======================================================================
105 # input validation and dispatch
107 # we will also need to know the possible actions, for validation
108 our %actions = (
109 "blame" => \&git_blame,
110 "blame_incremental" => \&git_blame_incremental,
111 "blame_data" => \&git_blame_data,
112 "blobdiff" => \&git_blobdiff,
113 "blobdiff_plain" => \&git_blobdiff_plain,
114 "blob" => \&git_blob,
115 "blob_plain" => \&git_blob_plain,
116 "commitdiff" => \&git_commitdiff,
117 "commitdiff_plain" => \&git_commitdiff_plain,
118 "commit" => \&git_commit,
119 "forks" => \&git_forks,
120 "heads" => \&git_heads,
121 "history" => \&git_history,
122 "log" => \&git_log,
123 "patch" => \&git_patch,
124 "patches" => \&git_patches,
125 "rss" => \&git_rss,
126 "atom" => \&git_atom,
127 "search" => \&git_search,
128 "search_help" => \&git_search_help,
129 "shortlog" => \&git_shortlog,
130 "summary" => \&git_summary,
131 "tag" => \&git_tag,
132 "tags" => \&git_tags,
133 "tree" => \&git_tree,
134 "snapshot" => \&git_snapshot,
135 "object" => \&git_object,
136 # those below don't need $project
137 "opml" => \&git_opml,
138 "project_list" => \&git_project_list,
139 "project_index" => \&git_project_index,
142 # we will also need to know the possible 'edits', for validation
143 our %edits = (
144 #already existing subroutines
145 "log" => \&git_log,
146 "summary" => \&git_summary,
147 #new subroutines
148 "addrepo" => \&git_addrepo,
151 # now read PATH_INFO and update the parameter list for missing parameters
152 sub evaluate_path_info {
153 return if defined $input_params{'project'};
154 return if !$path_info;
155 $path_info =~ s,^/+,,;
156 return if !$path_info;
158 # find which part of PATH_INFO is project
159 my $project = $path_info;
160 $project =~ s,/+$,,;
161 while ($project && !check_head_link("$projectroot/$project")) {
162 $project =~ s,/*[^/]*$,,;
164 return unless $project;
165 $input_params{'project'} = $project;
167 # do not change any parameters if an action is given using the query string
168 return if $input_params{'action'};
169 $path_info =~ s,^\Q$project\E/*,,;
171 # next, check if we have an action
172 my $action = $path_info;
173 $action =~ s,/.*$,,;
174 if (exists $actions{$action}) {
175 $path_info =~ s,^$action/*,,;
176 $input_params{'action'} = $action;
179 return if $input_params{'edit'};
180 # next, check if we have an edit
181 my $edit = $path_info;
182 $edit =~ s,/.*$,,;
183 if (exists $edits{$edit} && gitweb_check_feature('write')) {
184 $path_info =~ s,^$edit/*,,;
185 $input_params{'edit'} = $edit;
188 # list of actions that want hash_base instead of hash, but can have no
189 # pathname (f) parameter
190 my @wants_base = (
191 'tree',
192 'history',
195 # we want to catch
196 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
197 my ($parentrefname, $parentpathname, $refname, $pathname) =
198 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
200 # first, analyze the 'current' part
201 if (defined $pathname) {
202 # we got "branch:filename" or "branch:dir/"
203 # we could use git_get_type(branch:pathname), but:
204 # - it needs $git_dir
205 # - it does a git() call
206 # - the convention of terminating directories with a slash
207 # makes it superfluous
208 # - embedding the action in the PATH_INFO would make it even
209 # more superfluous
210 $pathname =~ s,^/+,,;
211 if (!$pathname || substr($pathname, -1) eq "/") {
212 $input_params{'action'} ||= "tree";
213 $pathname =~ s,/$,,;
214 } else {
215 # the default action depends on whether we had parent info
216 # or not
217 if ($parentrefname) {
218 $input_params{'action'} ||= "blobdiff_plain";
219 } else {
220 $input_params{'action'} ||= "blob_plain";
223 $input_params{'hash_base'} ||= $refname;
224 $input_params{'file_name'} ||= $pathname;
225 } elsif (defined $refname) {
226 # we got "branch". In this case we have to choose if we have to
227 # set hash or hash_base.
229 # Most of the actions without a pathname only want hash to be
230 # set, except for the ones specified in @wants_base that want
231 # hash_base instead. It should also be noted that hand-crafted
232 # links having 'history' as an action and no pathname or hash
233 # set will fail, but that happens regardless of PATH_INFO.
234 $input_params{'action'} ||= "shortlog";
235 if (grep { $_ eq $input_params{'action'} } @wants_base) {
236 $input_params{'hash_base'} ||= $refname;
237 } else {
238 $input_params{'hash'} ||= $refname;
242 # next, handle the 'parent' part, if present
243 if (defined $parentrefname) {
244 # a missing pathspec defaults to the 'current' filename, allowing e.g.
245 # someproject/blobdiff/oldrev..newrev:/filename
246 if ($parentpathname) {
247 $parentpathname =~ s,^/+,,;
248 $parentpathname =~ s,/$,,;
249 $input_params{'file_parent'} ||= $parentpathname;
250 } else {
251 $input_params{'file_parent'} ||= $input_params{'file_name'};
253 # we assume that hash_parent_base is wanted if a path was specified,
254 # or if the action wants hash_base instead of hash
255 if (defined $input_params{'file_parent'} ||
256 grep { $_ eq $input_params{'action'} } @wants_base) {
257 $input_params{'hash_parent_base'} ||= $parentrefname;
258 } else {
259 $input_params{'hash_parent'} ||= $parentrefname;
263 # for the snapshot action, we allow URLs in the form
264 # $project/snapshot/$hash.ext
265 # where .ext determines the snapshot and gets removed from the
266 # passed $refname to provide the $hash.
268 # To be able to tell that $refname includes the format extension, we
269 # require the following two conditions to be satisfied:
270 # - the hash input parameter MUST have been set from the $refname part
271 # of the URL (i.e. they must be equal)
272 # - the snapshot format MUST NOT have been defined already (e.g. from
273 # CGI parameter sf)
274 # It's also useless to try any matching unless $refname has a dot,
275 # so we check for that too
276 if (defined $input_params{'action'} &&
277 $input_params{'action'} eq 'snapshot' &&
278 defined $refname && index($refname, '.') != -1 &&
279 $refname eq $input_params{'hash'} &&
280 !defined $input_params{'snapshot_format'}) {
281 # We loop over the known snapshot formats, checking for
282 # extensions. Allowed extensions are both the defined suffix
283 # (which includes the initial dot already) and the snapshot
284 # format key itself, with a prepended dot
285 while (my ($fmt, $opt) = each %known_snapshot_formats) {
286 my $hash = $refname;
287 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
288 next;
290 my $sfx = $1;
291 # a valid suffix was found, so set the snapshot format
292 # and reset the hash parameter
293 $input_params{'snapshot_format'} = $fmt;
294 $input_params{'hash'} = $hash;
295 # we also set the format suffix to the one requested
296 # in the URL: this way a request for e.g. .tgz returns
297 # a .tgz instead of a .tar.gz
298 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
299 last;
304 sub evaluate_and_validate_params {
305 $action = $input_params{'action'};
306 if (defined $action) {
307 if (!validate_action($action)) {
308 die_error(400, "Invalid action parameter");
312 $edit = $input_params{'edit'};
313 if(defined $edit && gitweb_check_feature('write')) {
314 if(!validate_edit($edit)) {
315 die_error(400, "Invalid edit parameter");
319 # parameters which are pathnames
320 $project = $input_params{'project'};
321 if (defined $project) {
322 if (!validate_project($project)) {
323 undef $project;
324 die_error(404, "No such project");
328 $file_name = $input_params{'file_name'};
329 if (defined $file_name) {
330 if (!validate_pathname($file_name)) {
331 die_error(400, "Invalid file parameter");
335 $file_parent = $input_params{'file_parent'};
336 if (defined $file_parent) {
337 if (!validate_pathname($file_parent)) {
338 die_error(400, "Invalid file parent parameter");
342 # parameters which are refnames
343 $hash = $input_params{'hash'};
344 if (defined $hash) {
345 if (!validate_refname($hash)) {
346 die_error(400, "Invalid hash parameter");
350 $hash_parent = $input_params{'hash_parent'};
351 if (defined $hash_parent) {
352 if (!validate_refname($hash_parent)) {
353 die_error(400, "Invalid hash parent parameter");
357 $hash_base = $input_params{'hash_base'};
358 if (defined $hash_base) {
359 if (!validate_refname($hash_base)) {
360 die_error(400, "Invalid hash base parameter");
364 @extra_options = @{$input_params{'extra_options'}};
365 # @extra_options is always defined, since it can only be (currently) set from
366 # CGI, and $cgi->param() returns the empty array in array context if the param
367 # is not set
368 foreach my $opt (@extra_options) {
369 if (not exists $allowed_options{$opt}) {
370 die_error(400, "Invalid option parameter");
372 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
373 die_error(400, "Invalid option parameter for this action");
377 $hash_parent_base = $input_params{'hash_parent_base'};
378 if (defined $hash_parent_base) {
379 if (!validate_refname($hash_parent_base)) {
380 die_error(400, "Invalid hash parent base parameter");
384 # other parameters
385 $page = $input_params{'page'};
386 if (defined $page) {
387 if ($page =~ m/[^0-9]/) {
388 die_error(400, "Invalid page parameter");
392 $searchtype = $input_params{'searchtype'};
393 if (defined $searchtype) {
394 if ($searchtype =~ m/[^a-z]/) {
395 die_error(400, "Invalid searchtype parameter");
399 $search_use_regexp = $input_params{'search_use_regexp'};
401 $searchtext = $input_params{'searchtext'};
402 if (defined $searchtext) {
403 if (length($searchtext) < 2) {
404 die_error(403, "At least two characters are required for search parameter");
406 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
410 sub evaluate_git_dir {
411 $git_dir = "$projectroot/$project" if $project;
414 # custom error handler: 'die <message>' is Internal Server Error
415 sub handle_errors_html {
416 my $msg = shift; # it is already HTML escaped
418 # to avoid infinite loop where error occurs in die_error,
419 # change handler to default handler, disabling handle_errors_html
420 set_message("Error occured when inside die_error:\n$msg");
422 # you cannot jump out of die_error when called as error handler;
423 # the subroutine set via CGI::Carp::set_message is called _after_
424 # HTTP headers are already written, so it cannot write them itself
425 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
427 set_message(\&handle_errors_html);
429 # dispatch
430 sub dispatch {
431 if (!defined $action) {
432 if (defined $hash) {
433 $action = git_get_type($hash);
434 } elsif (defined $hash_base && defined $file_name) {
435 $action = git_get_type("$hash_base:$file_name");
436 } elsif (defined $project) {
437 $action = 'summary';
438 } else {
439 $action = 'project_list';
442 if (defined $edit) {
443 $action = $edit;
444 %actions = %edits;
446 if (!defined($actions{$action})) {
447 die_error(400, "Unknown action or edit");
449 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
450 !$project) {
451 die_error(400, "Project needed");
454 $actions{$action}->();
457 sub run_request {
458 our $t0 = [Time::HiRes::gettimeofday()]
459 if defined $t0;
461 evaluate_uri();
462 evaluate_gitweb_config();
463 evaluate_git_version();
464 check_loadavg();
466 # $projectroot and $projects_list might be set in gitweb config file
467 $projects_list ||= $projectroot;
469 evaluate_query_params();
470 evaluate_path_info();
471 evaluate_and_validate_params();
472 evaluate_git_dir();
474 configure_gitweb_features();
476 dispatch();
479 our $is_last_request = sub { 1 };
480 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
481 our $CGI = 'CGI';
482 sub configure_as_fcgi {
483 require CGI::Fast;
484 our $CGI = 'CGI::Fast';
486 my $request_number = 0;
487 # let each child service 100 requests
488 our $is_last_request = sub { ++$request_number > 100 };
490 sub evaluate_argv {
491 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
492 configure_as_fcgi()
493 if $script_name =~ /\.fcgi$/;
495 return unless (@ARGV);
497 require Getopt::Long;
498 Getopt::Long::GetOptions(
499 'fastcgi|fcgi|f' => \&configure_as_fcgi,
500 'nproc|n=i' => sub {
501 my ($arg, $val) = @_;
502 return unless eval { require FCGI::ProcManager; 1; };
503 my $proc_manager = FCGI::ProcManager->new({
504 n_processes => $val,
506 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
507 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
508 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
513 sub run {
514 evaluate_argv();
516 $pre_listen_hook->()
517 if $pre_listen_hook;
519 REQUEST:
520 while ($cgi = $CGI->new()) {
521 $pre_dispatch_hook->()
522 if $pre_dispatch_hook;
524 run_request();
526 $pre_dispatch_hook->()
527 if $post_dispatch_hook;
529 last REQUEST if ($is_last_request->());
532 DONE_GITWEB:
536 run();
538 if (defined caller) {
539 # wrapped in a subroutine processing requests,
540 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
541 return;
542 } else {
543 # pure CGI script, serving single request
544 exit;
547 ## ======================================================================
548 ## validation, quoting/unquoting and escaping
550 sub validate_action {
551 my $input = shift || return undef;
552 return undef unless exists $actions{$input};
553 return $input;
556 sub validate_edit {
557 my $input = shift || return undef;
558 return undef unless exists $edits{$input};
559 return $input;
562 sub validate_project {
563 my $input = shift || return undef;
564 if (!validate_pathname($input) ||
565 !(-d "$projectroot/$input") ||
566 !check_export_ok("$projectroot/$input") ||
567 ($strict_export && !project_in_list($input))) {
568 return undef;
569 } else {
570 return $input;
574 sub validate_pathname {
575 my $input = shift || return undef;
577 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
578 # at the beginning, at the end, and between slashes.
579 # also this catches doubled slashes
580 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
581 return undef;
583 # no null characters
584 if ($input =~ m!\0!) {
585 return undef;
587 return $input;
590 sub validate_refname {
591 my $input = shift || return undef;
593 # textual hashes are O.K.
594 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
595 return $input;
597 # it must be correct pathname
598 $input = validate_pathname($input)
599 or return undef;
600 # restrictions on ref name according to git-check-ref-format
601 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
602 return undef;
604 return $input;
607 ## ......................................................................
608 ## functions printing or outputting HTML: div
610 # Outputs the author name and date in long form
611 sub git_print_authorship {
612 my $co = shift;
613 my %opts = @_;
614 my $tag = $opts{-tag} || 'div';
615 my $author = $co->{'author_name'};
617 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
618 print "<$tag class=\"author_date\">" .
619 format_search_author($author, "author", esc_html($author)) .
620 " [$ad{'rfc2822'}";
621 print_local_time(%ad) if ($opts{-localtime});
622 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
623 . "</$tag>\n";
626 # Outputs table rows containing the full author or committer information,
627 # in the format expected for 'commit' view (& similia).
628 # Parameters are a commit hash reference, followed by the list of people
629 # to output information for. If the list is empty it defalts to both
630 # author and committer.
631 sub git_print_authorship_rows {
632 my $co = shift;
633 # too bad we can't use @people = @_ || ('author', 'committer')
634 my @people = @_;
635 @people = ('author', 'committer') unless @people;
636 foreach my $who (@people) {
637 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
638 print "<tr><td>$who</td><td>" .
639 format_search_author($co->{"${who}_name"}, $who,
640 esc_html($co->{"${who}_name"})) . " " .
641 format_search_author($co->{"${who}_email"}, $who,
642 esc_html("<" . $co->{"${who}_email"} . ">")) .
643 "</td><td rowspan=\"2\">" .
644 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
645 "</td></tr>\n" .
646 "<tr>" .
647 "<td></td><td> $wd{'rfc2822'}";
648 print_local_time(%wd);
649 print "</td>" .
650 "</tr>\n";
654 sub git_print_log {
655 my $log = shift;
656 my %opts = @_;
658 if ($opts{'-remove_title'}) {
659 # remove title, i.e. first line of log
660 shift @$log;
662 # remove leading empty lines
663 while (defined $log->[0] && $log->[0] eq "") {
664 shift @$log;
667 # print log
668 my $signoff = 0;
669 my $empty = 0;
670 foreach my $line (@$log) {
671 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
672 $signoff = 1;
673 $empty = 0;
674 if (! $opts{'-remove_signoff'}) {
675 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
676 next;
677 } else {
678 # remove signoff lines
679 next;
681 } else {
682 $signoff = 0;
685 # print only one empty line
686 # do not print empty line after signoff
687 if ($line eq "") {
688 next if ($empty || $signoff);
689 $empty = 1;
690 } else {
691 $empty = 0;
694 print format_log_line_html($line) . "<br/>\n";
697 if ($opts{'-final_empty_line'}) {
698 # end with single empty line
699 print "<br/>\n" unless $empty;
703 ## ......................................................................
704 ## functions printing large fragments of HTML
706 sub git_difftree_body {
707 my ($difftree, $hash, @parents) = @_;
708 my ($parent) = $parents[0];
709 my $have_blame = gitweb_check_feature('blame');
710 print "<div class=\"list_head\">\n";
711 if ($#{$difftree} > 10) {
712 print(($#{$difftree} + 1) . " files changed:\n");
714 print "</div>\n";
716 print "<table class=\"" .
717 (@parents > 1 ? "combined " : "") .
718 "diff_tree\">\n";
720 # header only for combined diff in 'commitdiff' view
721 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
722 if ($has_header) {
723 # table header
724 print "<thead><tr>\n" .
725 "<th></th><th></th>\n"; # filename, patchN link
726 for (my $i = 0; $i < @parents; $i++) {
727 my $par = $parents[$i];
728 print "<th>" .
729 $cgi->a({-href => href(action=>"commitdiff",
730 hash=>$hash, hash_parent=>$par),
731 -title => 'commitdiff to parent number ' .
732 ($i+1) . ': ' . substr($par,0,7)},
733 $i+1) .
734 "&nbsp;</th>\n";
736 print "</tr></thead>\n<tbody>\n";
739 my $alternate = 1;
740 my $patchno = 0;
741 foreach my $line (@{$difftree}) {
742 my $diff = parsed_difftree_line($line);
744 if ($alternate) {
745 print "<tr class=\"dark\">\n";
746 } else {
747 print "<tr class=\"light\">\n";
749 $alternate ^= 1;
751 if (exists $diff->{'nparents'}) { # combined diff
753 fill_from_file_info($diff, @parents)
754 unless exists $diff->{'from_file'};
756 if (!is_deleted($diff)) {
757 # file exists in the result (child) commit
758 print "<td>" .
759 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
760 file_name=>$diff->{'to_file'},
761 hash_base=>$hash),
762 -class => "list"}, esc_path($diff->{'to_file'})) .
763 "</td>\n";
764 } else {
765 print "<td>" .
766 esc_path($diff->{'to_file'}) .
767 "</td>\n";
770 if ($action eq 'commitdiff') {
771 # link to patch
772 $patchno++;
773 print "<td class=\"link\">" .
774 $cgi->a({-href => "#patch$patchno"}, "patch") .
775 " | " .
776 "</td>\n";
779 my $has_history = 0;
780 my $not_deleted = 0;
781 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
782 my $hash_parent = $parents[$i];
783 my $from_hash = $diff->{'from_id'}[$i];
784 my $from_path = $diff->{'from_file'}[$i];
785 my $status = $diff->{'status'}[$i];
787 $has_history ||= ($status ne 'A');
788 $not_deleted ||= ($status ne 'D');
790 if ($status eq 'A') {
791 print "<td class=\"link\" align=\"right\"> | </td>\n";
792 } elsif ($status eq 'D') {
793 print "<td class=\"link\">" .
794 $cgi->a({-href => href(action=>"blob",
795 hash_base=>$hash,
796 hash=>$from_hash,
797 file_name=>$from_path)},
798 "blob" . ($i+1)) .
799 " | </td>\n";
800 } else {
801 if ($diff->{'to_id'} eq $from_hash) {
802 print "<td class=\"link nochange\">";
803 } else {
804 print "<td class=\"link\">";
806 print $cgi->a({-href => href(action=>"blobdiff",
807 hash=>$diff->{'to_id'},
808 hash_parent=>$from_hash,
809 hash_base=>$hash,
810 hash_parent_base=>$hash_parent,
811 file_name=>$diff->{'to_file'},
812 file_parent=>$from_path)},
813 "diff" . ($i+1)) .
814 " | </td>\n";
818 print "<td class=\"link\">";
819 if ($not_deleted) {
820 print $cgi->a({-href => href(action=>"blob",
821 hash=>$diff->{'to_id'},
822 file_name=>$diff->{'to_file'},
823 hash_base=>$hash)},
824 "blob");
825 print " | " if ($has_history);
827 if ($has_history) {
828 print $cgi->a({-href => href(action=>"history",
829 file_name=>$diff->{'to_file'},
830 hash_base=>$hash)},
831 "history");
833 print "</td>\n";
835 print "</tr>\n";
836 next; # instead of 'else' clause, to avoid extra indent
838 # else ordinary diff
840 my ($to_mode_oct, $to_mode_str, $to_file_type);
841 my ($from_mode_oct, $from_mode_str, $from_file_type);
842 if ($diff->{'to_mode'} ne ('0' x 6)) {
843 $to_mode_oct = oct $diff->{'to_mode'};
844 if (S_ISREG($to_mode_oct)) { # only for regular file
845 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
847 $to_file_type = file_type($diff->{'to_mode'});
849 if ($diff->{'from_mode'} ne ('0' x 6)) {
850 $from_mode_oct = oct $diff->{'from_mode'};
851 if (S_ISREG($to_mode_oct)) { # only for regular file
852 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
854 $from_file_type = file_type($diff->{'from_mode'});
857 if ($diff->{'status'} eq "A") { # created
858 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
859 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
860 $mode_chng .= "]</span>";
861 print "<td>";
862 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
863 hash_base=>$hash, file_name=>$diff->{'file'}),
864 -class => "list"}, esc_path($diff->{'file'}));
865 print "</td>\n";
866 print "<td>$mode_chng</td>\n";
867 print "<td class=\"link\">";
868 if ($action eq 'commitdiff') {
869 # link to patch
870 $patchno++;
871 print $cgi->a({-href => "#patch$patchno"}, "patch");
872 print " | ";
874 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
875 hash_base=>$hash, file_name=>$diff->{'file'})},
876 "blob");
877 print "</td>\n";
879 } elsif ($diff->{'status'} eq "D") { # deleted
880 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
881 print "<td>";
882 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
883 hash_base=>$parent, file_name=>$diff->{'file'}),
884 -class => "list"}, esc_path($diff->{'file'}));
885 print "</td>\n";
886 print "<td>$mode_chng</td>\n";
887 print "<td class=\"link\">";
888 if ($action eq 'commitdiff') {
889 # link to patch
890 $patchno++;
891 print $cgi->a({-href => "#patch$patchno"}, "patch");
892 print " | ";
894 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
895 hash_base=>$parent, file_name=>$diff->{'file'})},
896 "blob") . " | ";
897 if ($have_blame) {
898 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
899 file_name=>$diff->{'file'})},
900 "blame") . " | ";
902 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
903 file_name=>$diff->{'file'})},
904 "history");
905 print "</td>\n";
907 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
908 my $mode_chnge = "";
909 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
910 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
911 if ($from_file_type ne $to_file_type) {
912 $mode_chnge .= " from $from_file_type to $to_file_type";
914 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
915 if ($from_mode_str && $to_mode_str) {
916 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
917 } elsif ($to_mode_str) {
918 $mode_chnge .= " mode: $to_mode_str";
921 $mode_chnge .= "]</span>\n";
923 print "<td>";
924 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
925 hash_base=>$hash, file_name=>$diff->{'file'}),
926 -class => "list"}, esc_path($diff->{'file'}));
927 print "</td>\n";
928 print "<td>$mode_chnge</td>\n";
929 print "<td class=\"link\">";
930 if ($action eq 'commitdiff') {
931 # link to patch
932 $patchno++;
933 print $cgi->a({-href => "#patch$patchno"}, "patch") .
934 " | ";
935 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
936 # "commit" view and modified file (not onlu mode changed)
937 print $cgi->a({-href => href(action=>"blobdiff",
938 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
939 hash_base=>$hash, hash_parent_base=>$parent,
940 file_name=>$diff->{'file'})},
941 "diff") .
942 " | ";
944 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
945 hash_base=>$hash, file_name=>$diff->{'file'})},
946 "blob") . " | ";
947 if ($have_blame) {
948 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
949 file_name=>$diff->{'file'})},
950 "blame") . " | ";
952 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
953 file_name=>$diff->{'file'})},
954 "history");
955 print "</td>\n";
957 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
958 my %status_name = ('R' => 'moved', 'C' => 'copied');
959 my $nstatus = $status_name{$diff->{'status'}};
960 my $mode_chng = "";
961 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
962 # mode also for directories, so we cannot use $to_mode_str
963 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
965 print "<td>" .
966 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
967 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
968 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
969 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
970 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
971 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
972 -class => "list"}, esc_path($diff->{'from_file'})) .
973 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
974 "<td class=\"link\">";
975 if ($action eq 'commitdiff') {
976 # link to patch
977 $patchno++;
978 print $cgi->a({-href => "#patch$patchno"}, "patch") .
979 " | ";
980 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
981 # "commit" view and modified file (not only pure rename or copy)
982 print $cgi->a({-href => href(action=>"blobdiff",
983 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
984 hash_base=>$hash, hash_parent_base=>$parent,
985 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
986 "diff") .
987 " | ";
989 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
990 hash_base=>$parent, file_name=>$diff->{'to_file'})},
991 "blob") . " | ";
992 if ($have_blame) {
993 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
994 file_name=>$diff->{'to_file'})},
995 "blame") . " | ";
997 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
998 file_name=>$diff->{'to_file'})},
999 "history");
1000 print "</td>\n";
1002 } # we should not encounter Unmerged (U) or Unknown (X) status
1003 print "</tr>\n";
1005 print "</tbody>" if $has_header;
1006 print "</table>\n";
1009 sub git_patchset_body {
1010 my ($fd, $difftree, $hash, @hash_parents) = @_;
1011 my ($hash_parent) = $hash_parents[0];
1013 my $is_combined = (@hash_parents > 1);
1014 my $patch_idx = 0;
1015 my $patch_number = 0;
1016 my $patch_line;
1017 my $diffinfo;
1018 my $to_name;
1019 my (%from, %to);
1021 print "<div class=\"patchset\">\n";
1023 # skip to first patch
1024 while ($patch_line = <$fd>) {
1025 chomp $patch_line;
1027 last if ($patch_line =~ m/^diff /);
1030 PATCH:
1031 while ($patch_line) {
1033 # parse "git diff" header line
1034 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
1035 # $1 is from_name, which we do not use
1036 $to_name = unquote($2);
1037 $to_name =~ s!^b/!!;
1038 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
1039 # $1 is 'cc' or 'combined', which we do not use
1040 $to_name = unquote($2);
1041 } else {
1042 $to_name = undef;
1045 # check if current patch belong to current raw line
1046 # and parse raw git-diff line if needed
1047 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
1048 # this is continuation of a split patch
1049 print "<div class=\"patch cont\">\n";
1050 } else {
1051 # advance raw git-diff output if needed
1052 $patch_idx++ if defined $diffinfo;
1054 # read and prepare patch information
1055 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1057 # compact combined diff output can have some patches skipped
1058 # find which patch (using pathname of result) we are at now;
1059 if ($is_combined) {
1060 while ($to_name ne $diffinfo->{'to_file'}) {
1061 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1062 format_diff_cc_simplified($diffinfo, @hash_parents) .
1063 "</div>\n"; # class="patch"
1065 $patch_idx++;
1066 $patch_number++;
1068 last if $patch_idx > $#$difftree;
1069 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1073 # modifies %from, %to hashes
1074 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
1076 # this is first patch for raw difftree line with $patch_idx index
1077 # we index @$difftree array from 0, but number patches from 1
1078 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1081 # git diff header
1082 #assert($patch_line =~ m/^diff /) if DEBUG;
1083 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
1084 $patch_number++;
1085 # print "git diff" header
1086 print format_git_diff_header_line($patch_line, $diffinfo,
1087 \%from, \%to);
1089 # print extended diff header
1090 print "<div class=\"diff extended_header\">\n";
1091 EXTENDED_HEADER:
1092 while ($patch_line = <$fd>) {
1093 chomp $patch_line;
1095 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
1097 print format_extended_diff_header_line($patch_line, $diffinfo,
1098 \%from, \%to);
1100 print "</div>\n"; # class="diff extended_header"
1102 # from-file/to-file diff header
1103 if (! $patch_line) {
1104 print "</div>\n"; # class="patch"
1105 last PATCH;
1107 next PATCH if ($patch_line =~ m/^diff /);
1108 #assert($patch_line =~ m/^---/) if DEBUG;
1110 my $last_patch_line = $patch_line;
1111 $patch_line = <$fd>;
1112 chomp $patch_line;
1113 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
1115 print format_diff_from_to_header($last_patch_line, $patch_line,
1116 $diffinfo, \%from, \%to,
1117 @hash_parents);
1119 # the patch itself
1120 LINE:
1121 while ($patch_line = <$fd>) {
1122 chomp $patch_line;
1124 next PATCH if ($patch_line =~ m/^diff /);
1126 print format_diff_line($patch_line, \%from, \%to);
1129 } continue {
1130 print "</div>\n"; # class="patch"
1133 # for compact combined (--cc) format, with chunk and patch simpliciaction
1134 # patchset might be empty, but there might be unprocessed raw lines
1135 for (++$patch_idx if $patch_number > 0;
1136 $patch_idx < @$difftree;
1137 ++$patch_idx) {
1138 # read and prepare patch information
1139 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1141 # generate anchor for "patch" links in difftree / whatchanged part
1142 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1143 format_diff_cc_simplified($diffinfo, @hash_parents) .
1144 "</div>\n"; # class="patch"
1146 $patch_number++;
1149 if ($patch_number == 0) {
1150 if (@hash_parents > 1) {
1151 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
1152 } else {
1153 print "<div class=\"diff nodifferences\">No differences found</div>\n";
1157 print "</div>\n"; # class="patchset"
1160 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1162 # fills project list info (age, description, owner, forks) for each
1163 # project in the list, removing invalid projects from returned list
1164 # NOTE: modifies $projlist, but does not remove entries from it
1165 sub fill_project_list_info {
1166 my ($projlist, $check_forks) = @_;
1167 my @projects;
1169 my $show_ctags = gitweb_check_feature('ctags');
1170 PROJECT:
1171 foreach my $pr (@$projlist) {
1172 my (@activity) = git_get_last_activity($pr->{'path'});
1173 unless (@activity) {
1174 next PROJECT;
1176 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
1177 if (!defined $pr->{'descr'}) {
1178 my $descr = git_get_project_description($pr->{'path'}) || "";
1179 $descr = to_utf8($descr);
1180 $pr->{'descr_long'} = $descr;
1181 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
1183 if (!defined $pr->{'owner'}) {
1184 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
1186 if ($check_forks) {
1187 my $pname = $pr->{'path'};
1188 if (($pname =~ s/\.git$//) &&
1189 ($pname !~ /\/$/) &&
1190 (-d "$projectroot/$pname")) {
1191 $pr->{'forks'} = "-d $projectroot/$pname";
1192 } else {
1193 $pr->{'forks'} = 0;
1196 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
1197 push @projects, $pr;
1200 return @projects;
1203 sub git_project_list_body {
1204 # actually uses global variable $project
1205 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
1207 my $check_forks = gitweb_check_feature('forks');
1208 my @projects = fill_project_list_info($projlist, $check_forks);
1210 $order ||= $default_projects_order;
1211 $from = 0 unless defined $from;
1212 $to = $#projects if (!defined $to || $#projects < $to);
1214 my %order_info = (
1215 project => { key => 'path', type => 'str' },
1216 descr => { key => 'descr_long', type => 'str' },
1217 owner => { key => 'owner', type => 'str' },
1218 age => { key => 'age', type => 'num' }
1220 my $oi = $order_info{$order};
1221 if ($oi->{'type'} eq 'str') {
1222 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
1223 } else {
1224 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
1227 my $show_ctags = gitweb_check_feature('ctags');
1228 if ($show_ctags) {
1229 my %ctags;
1230 foreach my $p (@projects) {
1231 foreach my $ct (keys %{$p->{'ctags'}}) {
1232 $ctags{$ct} += $p->{'ctags'}->{$ct};
1235 my $cloud = git_populate_project_tagcloud(\%ctags);
1236 print git_show_project_tagcloud($cloud, 64);
1239 print "<table class=\"project_list\">\n";
1240 unless ($no_header) {
1241 print "<tr>\n";
1242 if ($check_forks) {
1243 print "<th></th>\n";
1245 print_sort_th('project', $order, 'Project');
1246 print_sort_th('descr', $order, 'Description');
1247 print_sort_th('owner', $order, 'Owner');
1248 print_sort_th('age', $order, 'Last Change');
1249 print "<th></th>\n" . # for links
1250 "</tr>\n";
1252 my $alternate = 1;
1253 my $tagfilter = $cgi->param('by_tag');
1254 for (my $i = $from; $i <= $to; $i++) {
1255 my $pr = $projects[$i];
1257 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
1258 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
1259 and not $pr->{'descr_long'} =~ /$searchtext/;
1260 # Weed out forks or non-matching entries of search
1261 if ($check_forks) {
1262 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
1263 $forkbase="^$forkbase" if $forkbase;
1264 next if not $searchtext and not $tagfilter and $show_ctags
1265 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
1268 if ($alternate) {
1269 print "<tr class=\"dark\">\n";
1270 } else {
1271 print "<tr class=\"light\">\n";
1273 $alternate ^= 1;
1274 if ($check_forks) {
1275 print "<td>";
1276 if ($pr->{'forks'}) {
1277 print "<!-- $pr->{'forks'} -->\n";
1278 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
1280 print "</td>\n";
1282 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
1283 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
1284 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
1285 -class => "list", -title => $pr->{'descr_long'}},
1286 esc_html($pr->{'descr'})) . "</td>\n" .
1287 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
1288 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
1289 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
1290 "<td class=\"link\">" .
1291 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
1292 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
1293 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
1294 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
1295 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
1296 "</td>\n" .
1297 "</tr>\n";
1299 if (defined $extra) {
1300 print "<tr>\n";
1301 if ($check_forks) {
1302 print "<td></td>\n";
1304 print "<td colspan=\"5\">$extra</td>\n" .
1305 "</tr>\n";
1307 print "</table>\n";
1310 sub git_log_body {
1311 # uses global variable $project
1312 my ($commitlist, $from, $to, $refs, $extra) = @_;
1314 $from = 0 unless defined $from;
1315 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1317 for (my $i = 0; $i <= $to; $i++) {
1318 my %co = %{$commitlist->[$i]};
1319 next if !%co;
1320 my $commit = $co{'id'};
1321 my $ref = format_ref_marker($refs, $commit);
1322 my %ad = parse_date($co{'author_epoch'});
1323 git_print_header_div('commit',
1324 "<span class=\"age\">$co{'age_string'}</span>" .
1325 esc_html($co{'title'}) . $ref,
1326 $commit);
1327 print "<div class=\"title_text\">\n" .
1328 "<div class=\"log_link\">\n" .
1329 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
1330 " | " .
1331 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
1332 " | " .
1333 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
1334 "<br/>\n" .
1335 "</div>\n";
1336 git_print_authorship(\%co, -tag => 'span');
1337 print "<br/>\n</div>\n";
1339 print "<div class=\"log_body\">\n";
1340 git_print_log($co{'comment'}, -final_empty_line=> 1);
1341 print "</div>\n";
1343 if ($extra) {
1344 print "<div class=\"page_nav\">\n";
1345 print "$extra\n";
1346 print "</div>\n";
1350 sub git_shortlog_body {
1351 # uses global variable $project
1352 my ($commitlist, $from, $to, $refs, $extra) = @_;
1354 $from = 0 unless defined $from;
1355 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1357 print "<table class=\"shortlog\">\n";
1358 my $alternate = 1;
1359 for (my $i = $from; $i <= $to; $i++) {
1360 my %co = %{$commitlist->[$i]};
1361 my $commit = $co{'id'};
1362 my $ref = format_ref_marker($refs, $commit);
1363 if ($alternate) {
1364 print "<tr class=\"dark\">\n";
1365 } else {
1366 print "<tr class=\"light\">\n";
1368 $alternate ^= 1;
1369 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
1370 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1371 format_author_html('td', \%co, 10) . "<td>";
1372 print format_subject_html($co{'title'}, $co{'title_short'},
1373 href(action=>"commit", hash=>$commit), $ref);
1374 print "</td>\n" .
1375 "<td class=\"link\">" .
1376 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
1377 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
1378 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
1379 my $snapshot_links = format_snapshot_links($commit);
1380 if (defined $snapshot_links) {
1381 print " | " . $snapshot_links;
1383 print "</td>\n" .
1384 "</tr>\n";
1386 if (defined $extra) {
1387 print "<tr>\n" .
1388 "<td colspan=\"4\">$extra</td>\n" .
1389 "</tr>\n";
1391 print "</table>\n";
1394 sub git_history_body {
1395 # Warning: assumes constant type (blob or tree) during history
1396 my ($commitlist, $from, $to, $refs, $extra,
1397 $file_name, $file_hash, $ftype) = @_;
1399 $from = 0 unless defined $from;
1400 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
1402 print "<table class=\"history\">\n";
1403 my $alternate = 1;
1404 for (my $i = $from; $i <= $to; $i++) {
1405 my %co = %{$commitlist->[$i]};
1406 if (!%co) {
1407 next;
1409 my $commit = $co{'id'};
1411 my $ref = format_ref_marker($refs, $commit);
1413 if ($alternate) {
1414 print "<tr class=\"dark\">\n";
1415 } else {
1416 print "<tr class=\"light\">\n";
1418 $alternate ^= 1;
1419 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1420 # shortlog: format_author_html('td', \%co, 10)
1421 format_author_html('td', \%co, 15, 3) . "<td>";
1422 # originally git_history used chop_str($co{'title'}, 50)
1423 print format_subject_html($co{'title'}, $co{'title_short'},
1424 href(action=>"commit", hash=>$commit), $ref);
1425 print "</td>\n" .
1426 "<td class=\"link\">" .
1427 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
1428 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
1430 if ($ftype eq 'blob') {
1431 my $blob_current = $file_hash;
1432 my $blob_parent = git_get_hash_by_path($commit, $file_name);
1433 if (defined $blob_current && defined $blob_parent &&
1434 $blob_current ne $blob_parent) {
1435 print " | " .
1436 $cgi->a({-href => href(action=>"blobdiff",
1437 hash=>$blob_current, hash_parent=>$blob_parent,
1438 hash_base=>$hash_base, hash_parent_base=>$commit,
1439 file_name=>$file_name)},
1440 "diff to current");
1443 print "</td>\n" .
1444 "</tr>\n";
1446 if (defined $extra) {
1447 print "<tr>\n" .
1448 "<td colspan=\"4\">$extra</td>\n" .
1449 "</tr>\n";
1451 print "</table>\n";
1454 sub git_tags_body {
1455 # uses global variable $project
1456 my ($taglist, $from, $to, $extra) = @_;
1457 $from = 0 unless defined $from;
1458 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
1460 print "<table class=\"tags\">\n";
1461 my $alternate = 1;
1462 for (my $i = $from; $i <= $to; $i++) {
1463 my $entry = $taglist->[$i];
1464 my %tag = %$entry;
1465 my $comment = $tag{'subject'};
1466 my $comment_short;
1467 if (defined $comment) {
1468 $comment_short = chop_str($comment, 30, 5);
1470 if ($alternate) {
1471 print "<tr class=\"dark\">\n";
1472 } else {
1473 print "<tr class=\"light\">\n";
1475 $alternate ^= 1;
1476 if (defined $tag{'age'}) {
1477 print "<td><i>$tag{'age'}</i></td>\n";
1478 } else {
1479 print "<td></td>\n";
1481 print "<td>" .
1482 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
1483 -class => "list name"}, esc_html($tag{'name'})) .
1484 "</td>\n" .
1485 "<td>";
1486 if (defined $comment) {
1487 print format_subject_html($comment, $comment_short,
1488 href(action=>"tag", hash=>$tag{'id'}));
1490 print "</td>\n" .
1491 "<td class=\"selflink\">";
1492 if ($tag{'type'} eq "tag") {
1493 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
1494 } else {
1495 print "&nbsp;";
1497 print "</td>\n" .
1498 "<td class=\"link\">" . " | " .
1499 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
1500 if ($tag{'reftype'} eq "commit") {
1501 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
1502 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
1503 } elsif ($tag{'reftype'} eq "blob") {
1504 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
1506 print "</td>\n" .
1507 "</tr>";
1509 if (defined $extra) {
1510 print "<tr>\n" .
1511 "<td colspan=\"5\">$extra</td>\n" .
1512 "</tr>\n";
1514 print "</table>\n";
1517 sub git_heads_body {
1518 # uses global variable $project
1519 my ($headlist, $head, $from, $to, $extra) = @_;
1520 $from = 0 unless defined $from;
1521 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
1523 print "<table class=\"heads\">\n";
1524 my $alternate = 1;
1525 for (my $i = $from; $i <= $to; $i++) {
1526 my $entry = $headlist->[$i];
1527 my %ref = %$entry;
1528 my $curr = $ref{'id'} eq $head;
1529 if ($alternate) {
1530 print "<tr class=\"dark\">\n";
1531 } else {
1532 print "<tr class=\"light\">\n";
1534 $alternate ^= 1;
1535 print "<td><i>$ref{'age'}</i></td>\n" .
1536 ($curr ? "<td class=\"current_head\">" : "<td>") .
1537 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
1538 -class => "list name"},esc_html($ref{'name'})) .
1539 "</td>\n" .
1540 "<td class=\"link\">" .
1541 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
1542 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
1543 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
1544 "</td>\n" .
1545 "</tr>";
1547 if (defined $extra) {
1548 print "<tr>\n" .
1549 "<td colspan=\"3\">$extra</td>\n" .
1550 "</tr>\n";
1552 print "</table>\n";
1555 sub git_search_grep_body {
1556 my ($commitlist, $from, $to, $extra) = @_;
1557 $from = 0 unless defined $from;
1558 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1560 print "<table class=\"commit_search\">\n";
1561 my $alternate = 1;
1562 for (my $i = $from; $i <= $to; $i++) {
1563 my %co = %{$commitlist->[$i]};
1564 if (!%co) {
1565 next;
1567 my $commit = $co{'id'};
1568 if ($alternate) {
1569 print "<tr class=\"dark\">\n";
1570 } else {
1571 print "<tr class=\"light\">\n";
1573 $alternate ^= 1;
1574 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1575 format_author_html('td', \%co, 15, 5) .
1576 "<td>" .
1577 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
1578 -class => "list subject"},
1579 chop_and_escape_str($co{'title'}, 50) . "<br/>");
1580 my $comment = $co{'comment'};
1581 foreach my $line (@$comment) {
1582 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
1583 my ($lead, $match, $trail) = ($1, $2, $3);
1584 $match = chop_str($match, 70, 5, 'center');
1585 my $contextlen = int((80 - length($match))/2);
1586 $contextlen = 30 if ($contextlen > 30);
1587 $lead = chop_str($lead, $contextlen, 10, 'left');
1588 $trail = chop_str($trail, $contextlen, 10, 'right');
1590 $lead = esc_html($lead);
1591 $match = esc_html($match);
1592 $trail = esc_html($trail);
1594 print "$lead<span class=\"match\">$match</span>$trail<br />";
1597 print "</td>\n" .
1598 "<td class=\"link\">" .
1599 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
1600 " | " .
1601 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
1602 " | " .
1603 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
1604 print "</td>\n" .
1605 "</tr>\n";
1607 if (defined $extra) {
1608 print "<tr>\n" .
1609 "<td colspan=\"3\">$extra</td>\n" .
1610 "</tr>\n";
1612 print "</table>\n";
1615 ## ======================================================================
1616 ## ======================================================================
1617 ## actions
1619 sub git_project_list {
1620 my $order = $input_params{'order'};
1621 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
1622 die_error(400, "Unknown order parameter");
1625 my @list = git_get_projects_list();
1626 if (!@list) {
1627 die_error(404, "No projects found");
1630 git_header_html();
1631 if (defined $home_text && -f $home_text) {
1632 print "<div class=\"index_include\">\n";
1633 insert_file($home_text);
1634 print "</div>\n";
1636 print $cgi->startform(-method => "get") .
1637 "<p class=\"projsearch\">Search:\n" .
1638 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
1639 "</p>" .
1640 $cgi->end_form() . "\n";
1641 git_project_list_body(\@list, $order);
1642 git_footer_html();
1645 sub git_forks {
1646 my $order = $input_params{'order'};
1647 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
1648 die_error(400, "Unknown order parameter");
1651 my @list = git_get_projects_list($project);
1652 if (!@list) {
1653 die_error(404, "No forks found");
1656 git_header_html();
1657 git_print_page_nav('','');
1658 git_print_header_div('summary', "$project forks");
1659 git_project_list_body(\@list, $order);
1660 git_footer_html();
1663 sub git_project_index {
1664 my @projects = git_get_projects_list($project);
1666 print $cgi->header(
1667 -type => 'text/plain',
1668 -charset => 'utf-8',
1669 -content_disposition => 'inline; filename="index.aux"');
1671 foreach my $pr (@projects) {
1672 if (!exists $pr->{'owner'}) {
1673 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
1676 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
1677 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
1678 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
1679 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
1680 $path =~ s/ /\+/g;
1681 $owner =~ s/ /\+/g;
1683 print "$path $owner\n";
1687 sub git_summary {
1688 my $descr = git_get_project_description($project) || "none";
1689 my %co = parse_commit("HEAD");
1690 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
1691 my $head = $co{'id'};
1693 my $owner = git_get_project_owner($project);
1695 my $refs = git_get_references();
1696 # These get_*_list functions return one more to allow us to see if
1697 # there are more ...
1698 my @taglist = git_get_tags_list(16);
1699 my @headlist = git_get_heads_list(16);
1700 my @forklist;
1701 my $check_forks = gitweb_check_feature('forks');
1703 if ($check_forks) {
1704 @forklist = git_get_projects_list($project);
1707 git_header_html();
1708 git_print_page_nav('summary','', $head);
1710 print "<div class=\"title\">&nbsp;</div>\n";
1711 print "<table class=\"projects_list\">\n" .
1712 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
1713 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
1714 if (defined $cd{'rfc2822'}) {
1715 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
1718 # use per project git URL list in $projectroot/$project/cloneurl
1719 # or make project git URL from git base URL and project name
1720 my $url_tag = "URL";
1721 my @url_list = git_get_project_url_list($project);
1722 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
1723 foreach my $git_url (@url_list) {
1724 next unless $git_url;
1725 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
1726 $url_tag = "";
1729 # Tag cloud
1730 my $show_ctags = gitweb_check_feature('ctags');
1731 if ($show_ctags) {
1732 my $ctags = git_get_project_ctags($project);
1733 my $cloud = git_populate_project_tagcloud($ctags);
1734 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
1735 print "</td>\n<td>" unless %$ctags;
1736 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
1737 print "</td>\n<td>" if %$ctags;
1738 print git_show_project_tagcloud($cloud, 48);
1739 print "</td></tr>";
1742 print "</table>\n";
1744 # If XSS prevention is on, we don't include README.html.
1745 # TODO: Allow a readme in some safe format.
1746 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
1747 print "<div class=\"title\">readme</div>\n" .
1748 "<div class=\"readme\">\n";
1749 insert_file("$projectroot/$project/README.html");
1750 print "\n</div>\n"; # class="readme"
1753 # we need to request one more than 16 (0..15) to check if
1754 # those 16 are all
1755 my @commitlist = $head ? parse_commits($head, 17) : ();
1756 if (@commitlist) {
1757 git_print_header_div('shortlog');
1758 git_shortlog_body(\@commitlist, 0, 15, $refs,
1759 $#commitlist <= 15 ? undef :
1760 $cgi->a({-href => href(action=>"shortlog")}, "..."));
1763 if (@taglist) {
1764 git_print_header_div('tags');
1765 git_tags_body(\@taglist, 0, 15,
1766 $#taglist <= 15 ? undef :
1767 $cgi->a({-href => href(action=>"tags")}, "..."));
1770 if (@headlist) {
1771 git_print_header_div('heads');
1772 git_heads_body(\@headlist, $head, 0, 15,
1773 $#headlist <= 15 ? undef :
1774 $cgi->a({-href => href(action=>"heads")}, "..."));
1777 if (@forklist) {
1778 git_print_header_div('forks');
1779 git_project_list_body(\@forklist, 'age', 0, 15,
1780 $#forklist <= 15 ? undef :
1781 $cgi->a({-href => href(action=>"forks")}, "..."),
1782 'no_header');
1785 git_footer_html();
1788 sub git_tag {
1789 my $head = git_get_head_hash($project);
1790 git_header_html();
1791 git_print_page_nav('','', $head,undef,$head);
1792 my %tag = parse_tag($hash);
1794 if (! %tag) {
1795 die_error(404, "Unknown tag object");
1798 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
1799 print "<div class=\"title_text\">\n" .
1800 "<table class=\"object_header\">\n" .
1801 "<tr>\n" .
1802 "<td>object</td>\n" .
1803 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
1804 $tag{'object'}) . "</td>\n" .
1805 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
1806 $tag{'type'}) . "</td>\n" .
1807 "</tr>\n";
1808 if (defined($tag{'author'})) {
1809 git_print_authorship_rows(\%tag, 'author');
1811 print "</table>\n\n" .
1812 "</div>\n";
1813 print "<div class=\"page_body\">";
1814 my $comment = $tag{'comment'};
1815 foreach my $line (@$comment) {
1816 chomp $line;
1817 print esc_html($line, -nbsp=>1) . "<br/>\n";
1819 print "</div>\n";
1820 git_footer_html();
1823 sub git_blame_common {
1824 my $format = shift || 'porcelain';
1825 if ($format eq 'porcelain' && $cgi->param('js')) {
1826 $format = 'incremental';
1827 $action = 'blame_incremental'; # for page title etc
1830 # permissions
1831 gitweb_check_feature('blame')
1832 or die_error(403, "Blame view not allowed");
1834 # error checking
1835 die_error(400, "No file name given") unless $file_name;
1836 $hash_base ||= git_get_head_hash($project);
1837 die_error(404, "Couldn't find base commit") unless $hash_base;
1838 my %co = parse_commit($hash_base)
1839 or die_error(404, "Commit not found");
1840 my $ftype = "blob";
1841 if (!defined $hash) {
1842 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
1843 or die_error(404, "Error looking up file");
1844 } else {
1845 $ftype = git_get_type($hash);
1846 if ($ftype !~ "blob") {
1847 die_error(400, "Object is not a blob");
1851 my $fd;
1852 if ($format eq 'incremental') {
1853 # get file contents (as base)
1854 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
1855 or die_error(500, "Open git-cat-file failed");
1856 } elsif ($format eq 'data') {
1857 # run git-blame --incremental
1858 open $fd, "-|", git_cmd(), "blame", "--incremental",
1859 $hash_base, "--", $file_name
1860 or die_error(500, "Open git-blame --incremental failed");
1861 } else {
1862 # run git-blame --porcelain
1863 open $fd, "-|", git_cmd(), "blame", '-p',
1864 $hash_base, '--', $file_name
1865 or die_error(500, "Open git-blame --porcelain failed");
1868 # incremental blame data returns early
1869 if ($format eq 'data') {
1870 print $cgi->header(
1871 -type=>"text/plain", -charset => "utf-8",
1872 -status=> "200 OK");
1873 local $| = 1; # output autoflush
1874 print while <$fd>;
1875 close $fd
1876 or print "ERROR $!\n";
1878 print 'END';
1879 if (defined $t0 && gitweb_check_feature('timed')) {
1880 print ' '.
1881 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
1882 ' '.$number_of_git_cmds;
1884 print "\n";
1886 return;
1889 # page header
1890 git_header_html();
1891 my $formats_nav =
1892 $cgi->a({-href => href(action=>"blob", -replay=>1)},
1893 "blob") .
1894 " | ";
1895 if ($format eq 'incremental') {
1896 $formats_nav .=
1897 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
1898 "blame") . " (non-incremental)";
1899 } else {
1900 $formats_nav .=
1901 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
1902 "blame") . " (incremental)";
1904 $formats_nav .=
1905 " | " .
1906 $cgi->a({-href => href(action=>"history", -replay=>1)},
1907 "history") .
1908 " | " .
1909 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
1910 "HEAD");
1911 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
1912 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
1913 git_print_page_path($file_name, $ftype, $hash_base);
1915 # page body
1916 if ($format eq 'incremental') {
1917 print "<noscript>\n<div class=\"error\"><center><b>\n".
1918 "This page requires JavaScript to run.\n Use ".
1919 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
1920 'this page').
1921 " instead.\n".
1922 "</b></center></div>\n</noscript>\n";
1924 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
1927 print qq!<div class="page_body">\n!;
1928 print qq!<div id="progress_info">... / ...</div>\n!
1929 if ($format eq 'incremental');
1930 print qq!<table id="blame_table" class="blame" width="100%">\n!.
1931 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
1932 qq!<thead>\n!.
1933 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
1934 qq!</thead>\n!.
1935 qq!<tbody>\n!;
1937 my @rev_color = qw(light dark);
1938 my $num_colors = scalar(@rev_color);
1939 my $current_color = 0;
1941 if ($format eq 'incremental') {
1942 my $color_class = $rev_color[$current_color];
1944 #contents of a file
1945 my $linenr = 0;
1946 LINE:
1947 while (my $line = <$fd>) {
1948 chomp $line;
1949 $linenr++;
1951 print qq!<tr id="l$linenr" class="$color_class">!.
1952 qq!<td class="sha1"><a href=""> </a></td>!.
1953 qq!<td class="linenr">!.
1954 qq!<a class="linenr" href="">$linenr</a></td>!;
1955 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
1956 print qq!</tr>\n!;
1959 } else { # porcelain, i.e. ordinary blame
1960 my %metainfo = (); # saves information about commits
1962 # blame data
1963 LINE:
1964 while (my $line = <$fd>) {
1965 chomp $line;
1966 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
1967 # no <lines in group> for subsequent lines in group of lines
1968 my ($full_rev, $orig_lineno, $lineno, $group_size) =
1969 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
1970 if (!exists $metainfo{$full_rev}) {
1971 $metainfo{$full_rev} = { 'nprevious' => 0 };
1973 my $meta = $metainfo{$full_rev};
1974 my $data;
1975 while ($data = <$fd>) {
1976 chomp $data;
1977 last if ($data =~ s/^\t//); # contents of line
1978 if ($data =~ /^(\S+)(?: (.*))?$/) {
1979 $meta->{$1} = $2 unless exists $meta->{$1};
1981 if ($data =~ /^previous /) {
1982 $meta->{'nprevious'}++;
1985 my $short_rev = substr($full_rev, 0, 8);
1986 my $author = $meta->{'author'};
1987 my %date =
1988 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
1989 my $date = $date{'iso-tz'};
1990 if ($group_size) {
1991 $current_color = ($current_color + 1) % $num_colors;
1993 my $tr_class = $rev_color[$current_color];
1994 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
1995 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
1996 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
1997 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
1998 if ($group_size) {
1999 print "<td class=\"sha1\"";
2000 print " title=\"". esc_html($author) . ", $date\"";
2001 print " rowspan=\"$group_size\"" if ($group_size > 1);
2002 print ">";
2003 print $cgi->a({-href => href(action=>"commit",
2004 hash=>$full_rev,
2005 file_name=>$file_name)},
2006 esc_html($short_rev));
2007 if ($group_size >= 2) {
2008 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
2009 if (@author_initials) {
2010 print "<br />" .
2011 esc_html(join('', @author_initials));
2012 # or join('.', ...)
2015 print "</td>\n";
2017 # 'previous' <sha1 of parent commit> <filename at commit>
2018 if (exists $meta->{'previous'} &&
2019 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
2020 $meta->{'parent'} = $1;
2021 $meta->{'file_parent'} = unquote($2);
2023 my $linenr_commit =
2024 exists($meta->{'parent'}) ?
2025 $meta->{'parent'} : $full_rev;
2026 my $linenr_filename =
2027 exists($meta->{'file_parent'}) ?
2028 $meta->{'file_parent'} : unquote($meta->{'filename'});
2029 my $blamed = href(action => 'blame',
2030 file_name => $linenr_filename,
2031 hash_base => $linenr_commit);
2032 print "<td class=\"linenr\">";
2033 print $cgi->a({ -href => "$blamed#l$orig_lineno",
2034 -class => "linenr" },
2035 esc_html($lineno));
2036 print "</td>";
2037 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
2038 print "</tr>\n";
2039 } # end while
2043 # footer
2044 print "</tbody>\n".
2045 "</table>\n"; # class="blame"
2046 print "</div>\n"; # class="blame_body"
2047 close $fd
2048 or print "Reading blob failed\n";
2050 git_footer_html();
2053 sub git_blame {
2054 git_blame_common();
2057 sub git_blame_incremental {
2058 git_blame_common('incremental');
2061 sub git_blame_data {
2062 git_blame_common('data');
2065 sub git_tags {
2066 my $head = git_get_head_hash($project);
2067 git_header_html();
2068 git_print_page_nav('','', $head,undef,$head);
2069 git_print_header_div('summary', $project);
2071 my @tagslist = git_get_tags_list();
2072 if (@tagslist) {
2073 git_tags_body(\@tagslist);
2075 git_footer_html();
2078 sub git_heads {
2079 my $head = git_get_head_hash($project);
2080 git_header_html();
2081 git_print_page_nav('','', $head,undef,$head);
2082 git_print_header_div('summary', $project);
2084 my @headslist = git_get_heads_list();
2085 if (@headslist) {
2086 git_heads_body(\@headslist, $head);
2088 git_footer_html();
2091 sub git_blob_plain {
2092 my $type = shift;
2093 my $expires;
2095 if (!defined $hash) {
2096 if (defined $file_name) {
2097 my $base = $hash_base || git_get_head_hash($project);
2098 $hash = git_get_hash_by_path($base, $file_name, "blob")
2099 or die_error(404, "Cannot find file");
2100 } else {
2101 die_error(400, "No file name defined");
2103 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2104 # blobs defined by non-textual hash id's can be cached
2105 $expires = "+1d";
2108 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2109 or die_error(500, "Open git-cat-file blob '$hash' failed");
2111 # content-type (can include charset)
2112 $type = blob_contenttype($fd, $file_name, $type);
2114 # "save as" filename, even when no $file_name is given
2115 my $save_as = "$hash";
2116 if (defined $file_name) {
2117 $save_as = $file_name;
2118 } elsif ($type =~ m/^text\//) {
2119 $save_as .= '.txt';
2122 # With XSS prevention on, blobs of all types except a few known safe
2123 # ones are served with "Content-Disposition: attachment" to make sure
2124 # they don't run in our security domain. For certain image types,
2125 # blob view writes an <img> tag referring to blob_plain view, and we
2126 # want to be sure not to break that by serving the image as an
2127 # attachment (though Firefox 3 doesn't seem to care).
2128 my $sandbox = $prevent_xss &&
2129 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
2131 print $cgi->header(
2132 -type => $type,
2133 -expires => $expires,
2134 -content_disposition =>
2135 ($sandbox ? 'attachment' : 'inline')
2136 . '; filename="' . $save_as . '"');
2137 local $/ = undef;
2138 binmode STDOUT, ':raw';
2139 print <$fd>;
2140 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
2141 close $fd;
2144 sub git_blob {
2145 my $expires;
2147 if (!defined $hash) {
2148 if (defined $file_name) {
2149 my $base = $hash_base || git_get_head_hash($project);
2150 $hash = git_get_hash_by_path($base, $file_name, "blob")
2151 or die_error(404, "Cannot find file");
2152 } else {
2153 die_error(400, "No file name defined");
2155 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2156 # blobs defined by non-textual hash id's can be cached
2157 $expires = "+1d";
2160 my $have_blame = gitweb_check_feature('blame');
2161 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2162 or die_error(500, "Couldn't cat $file_name, $hash");
2163 my $mimetype = blob_mimetype($fd, $file_name);
2164 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
2165 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
2166 close $fd;
2167 return git_blob_plain($mimetype);
2169 # we can have blame only for text/* mimetype
2170 $have_blame &&= ($mimetype =~ m!^text/!);
2172 my $highlight = gitweb_check_feature('highlight');
2173 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
2174 $fd = run_highlighter($fd, $highlight, $syntax)
2175 if $syntax;
2177 git_header_html(undef, $expires);
2178 my $formats_nav = '';
2179 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2180 if (defined $file_name) {
2181 if ($have_blame) {
2182 $formats_nav .=
2183 $cgi->a({-href => href(action=>"blame", -replay=>1)},
2184 "blame") .
2185 " | ";
2187 $formats_nav .=
2188 $cgi->a({-href => href(action=>"history", -replay=>1)},
2189 "history") .
2190 " | " .
2191 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
2192 "raw") .
2193 " | " .
2194 $cgi->a({-href => href(action=>"blob",
2195 hash_base=>"HEAD", file_name=>$file_name)},
2196 "HEAD");
2197 } else {
2198 $formats_nav .=
2199 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
2200 "raw");
2202 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2203 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2204 } else {
2205 print "<div class=\"page_nav\">\n" .
2206 "<br/><br/></div>\n" .
2207 "<div class=\"title\">$hash</div>\n";
2209 git_print_page_path($file_name, "blob", $hash_base);
2210 print "<div class=\"page_body\">\n";
2211 if ($mimetype =~ m!^image/!) {
2212 print qq!<img type="$mimetype"!;
2213 if ($file_name) {
2214 print qq! alt="$file_name" title="$file_name"!;
2216 print qq! src="! .
2217 href(action=>"blob_plain", hash=>$hash,
2218 hash_base=>$hash_base, file_name=>$file_name) .
2219 qq!" />\n!;
2220 } else {
2221 my $nr;
2222 while (my $line = <$fd>) {
2223 chomp $line;
2224 $nr++;
2225 $line = untabify($line);
2226 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
2227 $nr, href(-replay => 1), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
2230 close $fd
2231 or print "Reading blob failed.\n";
2232 print "</div>";
2233 git_footer_html();
2236 sub git_tree {
2237 if (!defined $hash_base) {
2238 $hash_base = "HEAD";
2240 if (!defined $hash) {
2241 if (defined $file_name) {
2242 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
2243 } else {
2244 $hash = $hash_base;
2247 die_error(404, "No such tree") unless defined($hash);
2249 my $show_sizes = gitweb_check_feature('show-sizes');
2250 my $have_blame = gitweb_check_feature('blame');
2252 my @entries = ();
2254 local $/ = "\0";
2255 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
2256 ($show_sizes ? '-l' : ()), @extra_options, $hash
2257 or die_error(500, "Open git-ls-tree failed");
2258 @entries = map { chomp; $_ } <$fd>;
2259 close $fd
2260 or die_error(404, "Reading tree failed");
2263 my $refs = git_get_references();
2264 my $ref = format_ref_marker($refs, $hash_base);
2265 git_header_html();
2266 my $basedir = '';
2267 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2268 my @views_nav = ();
2269 if (defined $file_name) {
2270 push @views_nav,
2271 $cgi->a({-href => href(action=>"history", -replay=>1)},
2272 "history"),
2273 $cgi->a({-href => href(action=>"tree",
2274 hash_base=>"HEAD", file_name=>$file_name)},
2275 "HEAD"),
2277 my $snapshot_links = format_snapshot_links($hash);
2278 if (defined $snapshot_links) {
2279 # FIXME: Should be available when we have no hash base as well.
2280 push @views_nav, $snapshot_links;
2282 git_print_page_nav('tree','', $hash_base, undef, undef,
2283 join(' | ', @views_nav));
2284 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
2285 } else {
2286 undef $hash_base;
2287 print "<div class=\"page_nav\">\n";
2288 print "<br/><br/></div>\n";
2289 print "<div class=\"title\">$hash</div>\n";
2291 if (defined $file_name) {
2292 $basedir = $file_name;
2293 if ($basedir ne '' && substr($basedir, -1) ne '/') {
2294 $basedir .= '/';
2296 git_print_page_path($file_name, 'tree', $hash_base);
2298 print "<div class=\"page_body\">\n";
2299 print "<table class=\"tree\">\n";
2300 my $alternate = 1;
2301 # '..' (top directory) link if possible
2302 if (defined $hash_base &&
2303 defined $file_name && $file_name =~ m![^/]+$!) {
2304 if ($alternate) {
2305 print "<tr class=\"dark\">\n";
2306 } else {
2307 print "<tr class=\"light\">\n";
2309 $alternate ^= 1;
2311 my $up = $file_name;
2312 $up =~ s!/?[^/]+$!!;
2313 undef $up unless $up;
2314 # based on git_print_tree_entry
2315 print '<td class="mode">' . mode_str('040000') . "</td>\n";
2316 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
2317 print '<td class="list">';
2318 print $cgi->a({-href => href(action=>"tree",
2319 hash_base=>$hash_base,
2320 file_name=>$up)},
2321 "..");
2322 print "</td>\n";
2323 print "<td class=\"link\"></td>\n";
2325 print "</tr>\n";
2327 foreach my $line (@entries) {
2328 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
2330 if ($alternate) {
2331 print "<tr class=\"dark\">\n";
2332 } else {
2333 print "<tr class=\"light\">\n";
2335 $alternate ^= 1;
2337 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
2339 print "</tr>\n";
2341 print "</table>\n" .
2342 "</div>";
2343 git_footer_html();
2346 sub snapshot_name {
2347 my ($project, $hash) = @_;
2349 # path/to/project.git -> project
2350 # path/to/project/.git -> project
2351 my $name = to_utf8($project);
2352 $name =~ s,([^/])/*\.git$,$1,;
2353 $name = basename($name);
2354 # sanitize name
2355 $name =~ s/[[:cntrl:]]/?/g;
2357 my $ver = $hash;
2358 if ($hash =~ /^[0-9a-fA-F]+$/) {
2359 # shorten SHA-1 hash
2360 my $full_hash = git_get_full_hash($project, $hash);
2361 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
2362 $ver = git_get_short_hash($project, $hash);
2364 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
2365 # tags don't need shortened SHA-1 hash
2366 $ver = $1;
2367 } else {
2368 # branches and other need shortened SHA-1 hash
2369 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
2370 $ver = $1;
2372 $ver .= '-' . git_get_short_hash($project, $hash);
2374 # in case of hierarchical branch names
2375 $ver =~ s!/!.!g;
2377 # name = project-version_string
2378 $name = "$name-$ver";
2380 return wantarray ? ($name, $name) : $name;
2383 sub git_snapshot {
2384 my $format = $input_params{'snapshot_format'};
2385 if (!@snapshot_fmts) {
2386 die_error(403, "Snapshots not allowed");
2388 # default to first supported snapshot format
2389 $format ||= $snapshot_fmts[0];
2390 if ($format !~ m/^[a-z0-9]+$/) {
2391 die_error(400, "Invalid snapshot format parameter");
2392 } elsif (!exists($known_snapshot_formats{$format})) {
2393 die_error(400, "Unknown snapshot format");
2394 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
2395 die_error(403, "Snapshot format not allowed");
2396 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
2397 die_error(403, "Unsupported snapshot format");
2400 my $type = git_get_type("$hash^{}");
2401 if (!$type) {
2402 die_error(404, 'Object does not exist');
2403 } elsif ($type eq 'blob') {
2404 die_error(400, 'Object is not a tree-ish');
2407 my ($name, $prefix) = snapshot_name($project, $hash);
2408 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
2409 my $cmd = quote_command(
2410 git_cmd(), 'archive',
2411 "--format=$known_snapshot_formats{$format}{'format'}",
2412 "--prefix=$prefix/", $hash);
2413 if (exists $known_snapshot_formats{$format}{'compressor'}) {
2414 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
2417 $filename =~ s/(["\\])/\\$1/g;
2418 print $cgi->header(
2419 -type => $known_snapshot_formats{$format}{'type'},
2420 -content_disposition => 'inline; filename="' . $filename . '"',
2421 -status => '200 OK');
2423 open my $fd, "-|", $cmd
2424 or die_error(500, "Execute git-archive failed");
2425 binmode STDOUT, ':raw';
2426 print <$fd>;
2427 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
2428 close $fd;
2431 sub git_log_generic {
2432 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
2434 my $head = git_get_head_hash($project);
2435 if (!defined $base) {
2436 $base = $head;
2438 if (!defined $page) {
2439 $page = 0;
2441 my $refs = git_get_references();
2443 my $commit_hash = $base;
2444 if (defined $parent) {
2445 $commit_hash = "$parent..$base";
2447 my @commitlist =
2448 parse_commits($commit_hash, 101, (100 * $page),
2449 defined $file_name ? ($file_name, "--full-history") : ());
2451 my $ftype;
2452 if (!defined $file_hash && defined $file_name) {
2453 # some commits could have deleted file in question,
2454 # and not have it in tree, but one of them has to have it
2455 for (my $i = 0; $i < @commitlist; $i++) {
2456 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
2457 last if defined $file_hash;
2460 if (defined $file_hash) {
2461 $ftype = git_get_type($file_hash);
2463 if (defined $file_name && !defined $ftype) {
2464 die_error(500, "Unknown type of object");
2466 my %co;
2467 if (defined $file_name) {
2468 %co = parse_commit($base)
2469 or die_error(404, "Unknown commit object");
2473 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
2474 my $next_link = '';
2475 if ($#commitlist >= 100) {
2476 $next_link =
2477 $cgi->a({-href => href(-replay=>1, page=>$page+1),
2478 -accesskey => "n", -title => "Alt-n"}, "next");
2480 my $patch_max = gitweb_get_feature('patches');
2481 if ($patch_max && !defined $file_name) {
2482 if ($patch_max < 0 || @commitlist <= $patch_max) {
2483 $paging_nav .= " &sdot; " .
2484 $cgi->a({-href => href(action=>"patches", -replay=>1)},
2485 "patches");
2489 git_header_html();
2490 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
2491 if (defined $file_name) {
2492 git_print_header_div('commit', esc_html($co{'title'}), $base);
2493 } else {
2494 git_print_header_div('summary', $project)
2496 git_print_page_path($file_name, $ftype, $hash_base)
2497 if (defined $file_name);
2499 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
2500 $file_name, $file_hash, $ftype);
2502 git_footer_html();
2505 sub git_log {
2506 git_log_generic('log', \&git_log_body,
2507 $hash, $hash_parent);
2510 sub git_commit {
2511 $hash ||= $hash_base || "HEAD";
2512 my %co = parse_commit($hash)
2513 or die_error(404, "Unknown commit object");
2515 my $parent = $co{'parent'};
2516 my $parents = $co{'parents'}; # listref
2518 # we need to prepare $formats_nav before any parameter munging
2519 my $formats_nav;
2520 if (!defined $parent) {
2521 # --root commitdiff
2522 $formats_nav .= '(initial)';
2523 } elsif (@$parents == 1) {
2524 # single parent commit
2525 $formats_nav .=
2526 '(parent: ' .
2527 $cgi->a({-href => href(action=>"commit",
2528 hash=>$parent)},
2529 esc_html(substr($parent, 0, 7))) .
2530 ')';
2531 } else {
2532 # merge commit
2533 $formats_nav .=
2534 '(merge: ' .
2535 join(' ', map {
2536 $cgi->a({-href => href(action=>"commit",
2537 hash=>$_)},
2538 esc_html(substr($_, 0, 7)));
2539 } @$parents ) .
2540 ')';
2542 if (gitweb_check_feature('patches') && @$parents <= 1) {
2543 $formats_nav .= " | " .
2544 $cgi->a({-href => href(action=>"patch", -replay=>1)},
2545 "patch");
2548 if (!defined $parent) {
2549 $parent = "--root";
2551 my @difftree;
2552 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
2553 @diff_opts,
2554 (@$parents <= 1 ? $parent : '-c'),
2555 $hash, "--"
2556 or die_error(500, "Open git-diff-tree failed");
2557 @difftree = map { chomp; $_ } <$fd>;
2558 close $fd or die_error(404, "Reading git-diff-tree failed");
2560 # non-textual hash id's can be cached
2561 my $expires;
2562 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2563 $expires = "+1d";
2565 my $refs = git_get_references();
2566 my $ref = format_ref_marker($refs, $co{'id'});
2568 git_header_html(undef, $expires);
2569 git_print_page_nav('commit', '',
2570 $hash, $co{'tree'}, $hash,
2571 $formats_nav);
2573 if (defined $co{'parent'}) {
2574 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
2575 } else {
2576 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
2578 print "<div class=\"title_text\">\n" .
2579 "<table class=\"object_header\">\n";
2580 git_print_authorship_rows(\%co);
2581 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
2582 print "<tr>" .
2583 "<td>tree</td>" .
2584 "<td class=\"sha1\">" .
2585 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
2586 class => "list"}, $co{'tree'}) .
2587 "</td>" .
2588 "<td class=\"link\">" .
2589 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
2590 "tree");
2591 my $snapshot_links = format_snapshot_links($hash);
2592 if (defined $snapshot_links) {
2593 print " | " . $snapshot_links;
2595 print "</td>" .
2596 "</tr>\n";
2598 foreach my $par (@$parents) {
2599 print "<tr>" .
2600 "<td>parent</td>" .
2601 "<td class=\"sha1\">" .
2602 $cgi->a({-href => href(action=>"commit", hash=>$par),
2603 class => "list"}, $par) .
2604 "</td>" .
2605 "<td class=\"link\">" .
2606 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
2607 " | " .
2608 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
2609 "</td>" .
2610 "</tr>\n";
2612 print "</table>".
2613 "</div>\n";
2615 print "<div class=\"page_body\">\n";
2616 git_print_log($co{'comment'});
2617 print "</div>\n";
2619 git_difftree_body(\@difftree, $hash, @$parents);
2621 git_footer_html();
2624 sub git_object {
2625 # object is defined by:
2626 # - hash or hash_base alone
2627 # - hash_base and file_name
2628 my $type;
2630 # - hash or hash_base alone
2631 if ($hash || ($hash_base && !defined $file_name)) {
2632 my $object_id = $hash || $hash_base;
2634 open my $fd, "-|", quote_command(
2635 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
2636 or die_error(404, "Object does not exist");
2637 $type = <$fd>;
2638 chomp $type;
2639 close $fd
2640 or die_error(404, "Object does not exist");
2642 # - hash_base and file_name
2643 } elsif ($hash_base && defined $file_name) {
2644 $file_name =~ s,/+$,,;
2646 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
2647 or die_error(404, "Base object does not exist");
2649 # here errors should not hapen
2650 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
2651 or die_error(500, "Open git-ls-tree failed");
2652 my $line = <$fd>;
2653 close $fd;
2655 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2656 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
2657 die_error(404, "File or directory for given base does not exist");
2659 $type = $2;
2660 $hash = $3;
2661 } else {
2662 die_error(400, "Not enough information to find object");
2665 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
2666 hash=>$hash, hash_base=>$hash_base,
2667 file_name=>$file_name),
2668 -status => '302 Found');
2671 sub git_blobdiff {
2672 my $format = shift || 'html';
2674 my $fd;
2675 my @difftree;
2676 my %diffinfo;
2677 my $expires;
2679 # preparing $fd and %diffinfo for git_patchset_body
2680 # new style URI
2681 if (defined $hash_base && defined $hash_parent_base) {
2682 if (defined $file_name) {
2683 # read raw output
2684 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2685 $hash_parent_base, $hash_base,
2686 "--", (defined $file_parent ? $file_parent : ()), $file_name
2687 or die_error(500, "Open git-diff-tree failed");
2688 @difftree = map { chomp; $_ } <$fd>;
2689 close $fd
2690 or die_error(404, "Reading git-diff-tree failed");
2691 @difftree
2692 or die_error(404, "Blob diff not found");
2694 } elsif (defined $hash &&
2695 $hash =~ /[0-9a-fA-F]{40}/) {
2696 # try to find filename from $hash
2698 # read filtered raw output
2699 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2700 $hash_parent_base, $hash_base, "--"
2701 or die_error(500, "Open git-diff-tree failed");
2702 @difftree =
2703 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
2704 # $hash == to_id
2705 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
2706 map { chomp; $_ } <$fd>;
2707 close $fd
2708 or die_error(404, "Reading git-diff-tree failed");
2709 @difftree
2710 or die_error(404, "Blob diff not found");
2712 } else {
2713 die_error(400, "Missing one of the blob diff parameters");
2716 if (@difftree > 1) {
2717 die_error(400, "Ambiguous blob diff specification");
2720 %diffinfo = parse_difftree_raw_line($difftree[0]);
2721 $file_parent ||= $diffinfo{'from_file'} || $file_name;
2722 $file_name ||= $diffinfo{'to_file'};
2724 $hash_parent ||= $diffinfo{'from_id'};
2725 $hash ||= $diffinfo{'to_id'};
2727 # non-textual hash id's can be cached
2728 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
2729 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
2730 $expires = '+1d';
2733 # open patch output
2734 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2735 '-p', ($format eq 'html' ? "--full-index" : ()),
2736 $hash_parent_base, $hash_base,
2737 "--", (defined $file_parent ? $file_parent : ()), $file_name
2738 or die_error(500, "Open git-diff-tree failed");
2741 # old/legacy style URI -- not generated anymore since 1.4.3.
2742 if (!%diffinfo) {
2743 die_error('404 Not Found', "Missing one of the blob diff parameters")
2746 # header
2747 if ($format eq 'html') {
2748 my $formats_nav =
2749 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
2750 "raw");
2751 git_header_html(undef, $expires);
2752 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2753 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2754 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2755 } else {
2756 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
2757 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
2759 if (defined $file_name) {
2760 git_print_page_path($file_name, "blob", $hash_base);
2761 } else {
2762 print "<div class=\"page_path\"></div>\n";
2765 } elsif ($format eq 'plain') {
2766 print $cgi->header(
2767 -type => 'text/plain',
2768 -charset => 'utf-8',
2769 -expires => $expires,
2770 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
2772 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
2774 } else {
2775 die_error(400, "Unknown blobdiff format");
2778 # patch
2779 if ($format eq 'html') {
2780 print "<div class=\"page_body\">\n";
2782 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
2783 close $fd;
2785 print "</div>\n"; # class="page_body"
2786 git_footer_html();
2788 } else {
2789 while (my $line = <$fd>) {
2790 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
2791 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
2793 print $line;
2795 last if $line =~ m!^\+\+\+!;
2797 local $/ = undef;
2798 print <$fd>;
2799 close $fd;
2803 sub git_blobdiff_plain {
2804 git_blobdiff('plain');
2807 sub git_commitdiff {
2808 my %params = @_;
2809 my $format = $params{-format} || 'html';
2811 my ($patch_max) = gitweb_get_feature('patches');
2812 if ($format eq 'patch') {
2813 die_error(403, "Patch view not allowed") unless $patch_max;
2816 $hash ||= $hash_base || "HEAD";
2817 my %co = parse_commit($hash)
2818 or die_error(404, "Unknown commit object");
2820 # choose format for commitdiff for merge
2821 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
2822 $hash_parent = '--cc';
2824 # we need to prepare $formats_nav before almost any parameter munging
2825 my $formats_nav;
2826 if ($format eq 'html') {
2827 $formats_nav =
2828 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
2829 "raw");
2830 if ($patch_max && @{$co{'parents'}} <= 1) {
2831 $formats_nav .= " | " .
2832 $cgi->a({-href => href(action=>"patch", -replay=>1)},
2833 "patch");
2836 if (defined $hash_parent &&
2837 $hash_parent ne '-c' && $hash_parent ne '--cc') {
2838 # commitdiff with two commits given
2839 my $hash_parent_short = $hash_parent;
2840 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
2841 $hash_parent_short = substr($hash_parent, 0, 7);
2843 $formats_nav .=
2844 ' (from';
2845 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
2846 if ($co{'parents'}[$i] eq $hash_parent) {
2847 $formats_nav .= ' parent ' . ($i+1);
2848 last;
2851 $formats_nav .= ': ' .
2852 $cgi->a({-href => href(action=>"commitdiff",
2853 hash=>$hash_parent)},
2854 esc_html($hash_parent_short)) .
2855 ')';
2856 } elsif (!$co{'parent'}) {
2857 # --root commitdiff
2858 $formats_nav .= ' (initial)';
2859 } elsif (scalar @{$co{'parents'}} == 1) {
2860 # single parent commit
2861 $formats_nav .=
2862 ' (parent: ' .
2863 $cgi->a({-href => href(action=>"commitdiff",
2864 hash=>$co{'parent'})},
2865 esc_html(substr($co{'parent'}, 0, 7))) .
2866 ')';
2867 } else {
2868 # merge commit
2869 if ($hash_parent eq '--cc') {
2870 $formats_nav .= ' | ' .
2871 $cgi->a({-href => href(action=>"commitdiff",
2872 hash=>$hash, hash_parent=>'-c')},
2873 'combined');
2874 } else { # $hash_parent eq '-c'
2875 $formats_nav .= ' | ' .
2876 $cgi->a({-href => href(action=>"commitdiff",
2877 hash=>$hash, hash_parent=>'--cc')},
2878 'compact');
2880 $formats_nav .=
2881 ' (merge: ' .
2882 join(' ', map {
2883 $cgi->a({-href => href(action=>"commitdiff",
2884 hash=>$_)},
2885 esc_html(substr($_, 0, 7)));
2886 } @{$co{'parents'}} ) .
2887 ')';
2891 my $hash_parent_param = $hash_parent;
2892 if (!defined $hash_parent_param) {
2893 # --cc for multiple parents, --root for parentless
2894 $hash_parent_param =
2895 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
2898 # read commitdiff
2899 my $fd;
2900 my @difftree;
2901 if ($format eq 'html') {
2902 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2903 "--no-commit-id", "--patch-with-raw", "--full-index",
2904 $hash_parent_param, $hash, "--"
2905 or die_error(500, "Open git-diff-tree failed");
2907 while (my $line = <$fd>) {
2908 chomp $line;
2909 # empty line ends raw part of diff-tree output
2910 last unless $line;
2911 push @difftree, scalar parse_difftree_raw_line($line);
2914 } elsif ($format eq 'plain') {
2915 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2916 '-p', $hash_parent_param, $hash, "--"
2917 or die_error(500, "Open git-diff-tree failed");
2918 } elsif ($format eq 'patch') {
2919 # For commit ranges, we limit the output to the number of
2920 # patches specified in the 'patches' feature.
2921 # For single commits, we limit the output to a single patch,
2922 # diverging from the git-format-patch default.
2923 my @commit_spec = ();
2924 if ($hash_parent) {
2925 if ($patch_max > 0) {
2926 push @commit_spec, "-$patch_max";
2928 push @commit_spec, '-n', "$hash_parent..$hash";
2929 } else {
2930 if ($params{-single}) {
2931 push @commit_spec, '-1';
2932 } else {
2933 if ($patch_max > 0) {
2934 push @commit_spec, "-$patch_max";
2936 push @commit_spec, "-n";
2938 push @commit_spec, '--root', $hash;
2940 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
2941 '--encoding=utf8', '--stdout', @commit_spec
2942 or die_error(500, "Open git-format-patch failed");
2943 } else {
2944 die_error(400, "Unknown commitdiff format");
2947 # non-textual hash id's can be cached
2948 my $expires;
2949 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2950 $expires = "+1d";
2953 # write commit message
2954 if ($format eq 'html') {
2955 my $refs = git_get_references();
2956 my $ref = format_ref_marker($refs, $co{'id'});
2958 git_header_html(undef, $expires);
2959 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
2960 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
2961 print "<div class=\"title_text\">\n" .
2962 "<table class=\"object_header\">\n";
2963 git_print_authorship_rows(\%co);
2964 print "</table>".
2965 "</div>\n";
2966 print "<div class=\"page_body\">\n";
2967 if (@{$co{'comment'}} > 1) {
2968 print "<div class=\"log\">\n";
2969 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
2970 print "</div>\n"; # class="log"
2973 } elsif ($format eq 'plain') {
2974 my $refs = git_get_references("tags");
2975 my $tagname = git_get_rev_name_tags($hash);
2976 my $filename = basename($project) . "-$hash.patch";
2978 print $cgi->header(
2979 -type => 'text/plain',
2980 -charset => 'utf-8',
2981 -expires => $expires,
2982 -content_disposition => 'inline; filename="' . "$filename" . '"');
2983 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
2984 print "From: " . to_utf8($co{'author'}) . "\n";
2985 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
2986 print "Subject: " . to_utf8($co{'title'}) . "\n";
2988 print "X-Git-Tag: $tagname\n" if $tagname;
2989 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
2991 foreach my $line (@{$co{'comment'}}) {
2992 print to_utf8($line) . "\n";
2994 print "---\n\n";
2995 } elsif ($format eq 'patch') {
2996 my $filename = basename($project) . "-$hash.patch";
2998 print $cgi->header(
2999 -type => 'text/plain',
3000 -charset => 'utf-8',
3001 -expires => $expires,
3002 -content_disposition => 'inline; filename="' . "$filename" . '"');
3005 # write patch
3006 if ($format eq 'html') {
3007 my $use_parents = !defined $hash_parent ||
3008 $hash_parent eq '-c' || $hash_parent eq '--cc';
3009 git_difftree_body(\@difftree, $hash,
3010 $use_parents ? @{$co{'parents'}} : $hash_parent);
3011 print "<br/>\n";
3013 git_patchset_body($fd, \@difftree, $hash,
3014 $use_parents ? @{$co{'parents'}} : $hash_parent);
3015 close $fd;
3016 print "</div>\n"; # class="page_body"
3017 git_footer_html();
3019 } elsif ($format eq 'plain') {
3020 local $/ = undef;
3021 print <$fd>;
3022 close $fd
3023 or print "Reading git-diff-tree failed\n";
3024 } elsif ($format eq 'patch') {
3025 local $/ = undef;
3026 print <$fd>;
3027 close $fd
3028 or print "Reading git-format-patch failed\n";
3032 sub git_commitdiff_plain {
3033 git_commitdiff(-format => 'plain');
3036 # format-patch-style patches
3037 sub git_patch {
3038 git_commitdiff(-format => 'patch', -single => 1);
3041 sub git_patches {
3042 git_commitdiff(-format => 'patch');
3045 sub git_history {
3046 git_log_generic('history', \&git_history_body,
3047 $hash_base, $hash_parent_base,
3048 $file_name, $hash);
3051 sub git_search {
3052 gitweb_check_feature('search') or die_error(403, "Search is disabled");
3053 if (!defined $searchtext) {
3054 die_error(400, "Text field is empty");
3056 if (!defined $hash) {
3057 $hash = git_get_head_hash($project);
3059 my %co = parse_commit($hash);
3060 if (!%co) {
3061 die_error(404, "Unknown commit object");
3063 if (!defined $page) {
3064 $page = 0;
3067 $searchtype ||= 'commit';
3068 if ($searchtype eq 'pickaxe') {
3069 # pickaxe may take all resources of your box and run for several minutes
3070 # with every query - so decide by yourself how public you make this feature
3071 gitweb_check_feature('pickaxe')
3072 or die_error(403, "Pickaxe is disabled");
3074 if ($searchtype eq 'grep') {
3075 gitweb_check_feature('grep')
3076 or die_error(403, "Grep is disabled");
3079 git_header_html();
3081 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
3082 my $greptype;
3083 if ($searchtype eq 'commit') {
3084 $greptype = "--grep=";
3085 } elsif ($searchtype eq 'author') {
3086 $greptype = "--author=";
3087 } elsif ($searchtype eq 'committer') {
3088 $greptype = "--committer=";
3090 $greptype .= $searchtext;
3091 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
3092 $greptype, '--regexp-ignore-case',
3093 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
3095 my $paging_nav = '';
3096 if ($page > 0) {
3097 $paging_nav .=
3098 $cgi->a({-href => href(action=>"search", hash=>$hash,
3099 searchtext=>$searchtext,
3100 searchtype=>$searchtype)},
3101 "first");
3102 $paging_nav .= " &sdot; " .
3103 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3104 -accesskey => "p", -title => "Alt-p"}, "prev");
3105 } else {
3106 $paging_nav .= "first";
3107 $paging_nav .= " &sdot; prev";
3109 my $next_link = '';
3110 if ($#commitlist >= 100) {
3111 $next_link =
3112 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3113 -accesskey => "n", -title => "Alt-n"}, "next");
3114 $paging_nav .= " &sdot; $next_link";
3115 } else {
3116 $paging_nav .= " &sdot; next";
3119 if ($#commitlist >= 100) {
3122 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
3123 git_print_header_div('commit', esc_html($co{'title'}), $hash);
3124 git_search_grep_body(\@commitlist, 0, 99, $next_link);
3127 if ($searchtype eq 'pickaxe') {
3128 git_print_page_nav('','', $hash,$co{'tree'},$hash);
3129 git_print_header_div('commit', esc_html($co{'title'}), $hash);
3131 print "<table class=\"pickaxe search\">\n";
3132 my $alternate = 1;
3133 local $/ = "\n";
3134 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
3135 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
3136 ($search_use_regexp ? '--pickaxe-regex' : ());
3137 undef %co;
3138 my @files;
3139 while (my $line = <$fd>) {
3140 chomp $line;
3141 next unless $line;
3143 my %set = parse_difftree_raw_line($line);
3144 if (defined $set{'commit'}) {
3145 # finish previous commit
3146 if (%co) {
3147 print "</td>\n" .
3148 "<td class=\"link\">" .
3149 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3150 " | " .
3151 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3152 print "</td>\n" .
3153 "</tr>\n";
3156 if ($alternate) {
3157 print "<tr class=\"dark\">\n";
3158 } else {
3159 print "<tr class=\"light\">\n";
3161 $alternate ^= 1;
3162 %co = parse_commit($set{'commit'});
3163 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
3164 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3165 "<td><i>$author</i></td>\n" .
3166 "<td>" .
3167 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3168 -class => "list subject"},
3169 chop_and_escape_str($co{'title'}, 50) . "<br/>");
3170 } elsif (defined $set{'to_id'}) {
3171 next if ($set{'to_id'} =~ m/^0{40}$/);
3173 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
3174 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
3175 -class => "list"},
3176 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
3177 "<br/>\n";
3180 close $fd;
3182 # finish last commit (warning: repetition!)
3183 if (%co) {
3184 print "</td>\n" .
3185 "<td class=\"link\">" .
3186 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3187 " | " .
3188 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3189 print "</td>\n" .
3190 "</tr>\n";
3193 print "</table>\n";
3196 if ($searchtype eq 'grep') {
3197 git_print_page_nav('','', $hash,$co{'tree'},$hash);
3198 git_print_header_div('commit', esc_html($co{'title'}), $hash);
3200 print "<table class=\"grep_search\">\n";
3201 my $alternate = 1;
3202 my $matches = 0;
3203 local $/ = "\n";
3204 open my $fd, "-|", git_cmd(), 'grep', '-n',
3205 $search_use_regexp ? ('-E', '-i') : '-F',
3206 $searchtext, $co{'tree'};
3207 my $lastfile = '';
3208 while (my $line = <$fd>) {
3209 chomp $line;
3210 my ($file, $lno, $ltext, $binary);
3211 last if ($matches++ > 1000);
3212 if ($line =~ /^Binary file (.+) matches$/) {
3213 $file = $1;
3214 $binary = 1;
3215 } else {
3216 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
3218 if ($file ne $lastfile) {
3219 $lastfile and print "</td></tr>\n";
3220 if ($alternate++) {
3221 print "<tr class=\"dark\">\n";
3222 } else {
3223 print "<tr class=\"light\">\n";
3225 print "<td class=\"list\">".
3226 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
3227 file_name=>"$file"),
3228 -class => "list"}, esc_path($file));
3229 print "</td><td>\n";
3230 $lastfile = $file;
3232 if ($binary) {
3233 print "<div class=\"binary\">Binary file</div>\n";
3234 } else {
3235 $ltext = untabify($ltext);
3236 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
3237 $ltext = esc_html($1, -nbsp=>1);
3238 $ltext .= '<span class="match">';
3239 $ltext .= esc_html($2, -nbsp=>1);
3240 $ltext .= '</span>';
3241 $ltext .= esc_html($3, -nbsp=>1);
3242 } else {
3243 $ltext = esc_html($ltext, -nbsp=>1);
3245 print "<div class=\"pre\">" .
3246 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
3247 file_name=>"$file").'#l'.$lno,
3248 -class => "linenr"}, sprintf('%4i', $lno))
3249 . ' ' . $ltext . "</div>\n";
3252 if ($lastfile) {
3253 print "</td></tr>\n";
3254 if ($matches > 1000) {
3255 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
3257 } else {
3258 print "<div class=\"diff nodifferences\">No matches found</div>\n";
3260 close $fd;
3262 print "</table>\n";
3264 git_footer_html();
3267 sub git_search_help {
3268 git_header_html();
3269 git_print_page_nav('','', $hash,$hash,$hash);
3270 print <<EOT;
3271 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
3272 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
3273 the pattern entered is recognized as the POSIX extended
3274 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
3275 insensitive).</p>
3276 <dl>
3277 <dt><b>commit</b></dt>
3278 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
3280 my $have_grep = gitweb_check_feature('grep');
3281 if ($have_grep) {
3282 print <<EOT;
3283 <dt><b>grep</b></dt>
3284 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
3285 a different one) are searched for the given pattern. On large trees, this search can take
3286 a while and put some strain on the server, so please use it with some consideration. Note that
3287 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
3288 case-sensitive.</dd>
3291 print <<EOT;
3292 <dt><b>author</b></dt>
3293 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
3294 <dt><b>committer</b></dt>
3295 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
3297 my $have_pickaxe = gitweb_check_feature('pickaxe');
3298 if ($have_pickaxe) {
3299 print <<EOT;
3300 <dt><b>pickaxe</b></dt>
3301 <dd>All commits that caused the string to appear or disappear from any file (changes that
3302 added, removed or "modified" the string) will be listed. This search can take a while and
3303 takes a lot of strain on the server, so please use it wisely. Note that since you may be
3304 interested even in changes just changing the case as well, this search is case sensitive.</dd>
3307 print "</dl>\n";
3308 git_footer_html();
3311 sub git_shortlog {
3312 git_log_generic('shortlog', \&git_shortlog_body,
3313 $hash, $hash_parent);
3316 ## ======================================================================
3317 ## ======================================================================
3318 ## edits
3320 sub git_addrepo {
3321 if (-f $projects_list) {
3322 git_header_html();
3323 git_print_page_nav('addrepo');
3324 if(param('sf')) {
3325 open my $fd, '<', $projects_list or return;
3326 $fd .= "\n";
3328 git_print_header_div();
3329 print "<div class=\"page_body\">";
3330 print "<form action=\"$my_url\" method=\"post\"><br/>";
3331 print "<table><tr class=\"dark\"><td>";
3332 print "Repository path for \$project_list: </td><td><input style=\"width:400px;\" type=\"text\" name=\"path\"/>";
3333 print "</td></tr><tr class=\"light\"><td align=\"center\" colspan=\"2\">";
3334 print "<input type=\"submit\" value=\"Add repository\" name=\"sf\"/>";
3335 print "</td></tr></table></form></div>";
3336 git_footer_html();
3337 } else {
3338 die_error(404, "Needed a static file with list of repositories (\$project_list)");
3342 ## ......................................................................
3343 ## feeds (RSS, Atom; OPML)
3345 sub git_feed {
3346 my $format = shift || 'atom';
3347 my $have_blame = gitweb_check_feature('blame');
3349 # Atom: http://www.atomenabled.org/developers/syndication/
3350 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3351 if ($format ne 'rss' && $format ne 'atom') {
3352 die_error(400, "Unknown web feed format");
3355 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
3356 my $head = $hash || 'HEAD';
3357 my @commitlist = parse_commits($head, 150, 0, $file_name);
3359 my %latest_commit;
3360 my %latest_date;
3361 my $content_type = "application/$format+xml";
3362 if (defined $cgi->http('HTTP_ACCEPT') &&
3363 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
3364 # browser (feed reader) prefers text/xml
3365 $content_type = 'text/xml';
3367 if (defined($commitlist[0])) {
3368 %latest_commit = %{$commitlist[0]};
3369 my $latest_epoch = $latest_commit{'committer_epoch'};
3370 %latest_date = parse_date($latest_epoch);
3371 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
3372 if (defined $if_modified) {
3373 my $since;
3374 if (eval { require HTTP::Date; 1; }) {
3375 $since = HTTP::Date::str2time($if_modified);
3376 } elsif (eval { require Time::ParseDate; 1; }) {
3377 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
3379 if (defined $since && $latest_epoch <= $since) {
3380 print $cgi->header(
3381 -type => $content_type,
3382 -charset => 'utf-8',
3383 -last_modified => $latest_date{'rfc2822'},
3384 -status => '304 Not Modified');
3385 return;
3388 print $cgi->header(
3389 -type => $content_type,
3390 -charset => 'utf-8',
3391 -last_modified => $latest_date{'rfc2822'});
3392 } else {
3393 print $cgi->header(
3394 -type => $content_type,
3395 -charset => 'utf-8');
3398 # Optimization: skip generating the body if client asks only
3399 # for Last-Modified date.
3400 return if ($cgi->request_method() eq 'HEAD');
3402 # header variables
3403 my $title = "$site_name - $project/$action";
3404 my $feed_type = 'log';
3405 if (defined $hash) {
3406 $title .= " - '$hash'";
3407 $feed_type = 'branch log';
3408 if (defined $file_name) {
3409 $title .= " :: $file_name";
3410 $feed_type = 'history';
3412 } elsif (defined $file_name) {
3413 $title .= " - $file_name";
3414 $feed_type = 'history';
3416 $title .= " $feed_type";
3417 my $descr = git_get_project_description($project);
3418 if (defined $descr) {
3419 $descr = esc_html($descr);
3420 } else {
3421 $descr = "$project " .
3422 ($format eq 'rss' ? 'RSS' : 'Atom') .
3423 " feed";
3425 my $owner = git_get_project_owner($project);
3426 $owner = esc_html($owner);
3428 #header
3429 my $alt_url;
3430 if (defined $file_name) {
3431 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
3432 } elsif (defined $hash) {
3433 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
3434 } else {
3435 $alt_url = href(-full=>1, action=>"summary");
3437 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
3438 if ($format eq 'rss') {
3439 print <<XML;
3440 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
3441 <channel>
3443 print "<title>$title</title>\n" .
3444 "<link>$alt_url</link>\n" .
3445 "<description>$descr</description>\n" .
3446 "<language>en</language>\n" .
3447 # project owner is responsible for 'editorial' content
3448 "<managingEditor>$owner</managingEditor>\n";
3449 if (defined $logo || defined $favicon) {
3450 # prefer the logo to the favicon, since RSS
3451 # doesn't allow both
3452 my $img = esc_url($logo || $favicon);
3453 print "<image>\n" .
3454 "<url>$img</url>\n" .
3455 "<title>$title</title>\n" .
3456 "<link>$alt_url</link>\n" .
3457 "</image>\n";
3459 if (%latest_date) {
3460 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
3461 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
3463 print "<generator>gitweb v.$version/$git_version</generator>\n";
3464 } elsif ($format eq 'atom') {
3465 print <<XML;
3466 <feed xmlns="http://www.w3.org/2005/Atom">
3468 print "<title>$title</title>\n" .
3469 "<subtitle>$descr</subtitle>\n" .
3470 '<link rel="alternate" type="text/html" href="' .
3471 $alt_url . '" />' . "\n" .
3472 '<link rel="self" type="' . $content_type . '" href="' .
3473 $cgi->self_url() . '" />' . "\n" .
3474 "<id>" . href(-full=>1) . "</id>\n" .
3475 # use project owner for feed author
3476 "<author><name>$owner</name></author>\n";
3477 if (defined $favicon) {
3478 print "<icon>" . esc_url($favicon) . "</icon>\n";
3480 if (defined $logo_url) {
3481 # not twice as wide as tall: 72 x 27 pixels
3482 print "<logo>" . esc_url($logo) . "</logo>\n";
3484 if (! %latest_date) {
3485 # dummy date to keep the feed valid until commits trickle in:
3486 print "<updated>1970-01-01T00:00:00Z</updated>\n";
3487 } else {
3488 print "<updated>$latest_date{'iso-8601'}</updated>\n";
3490 print "<generator version='$version/$git_version'>gitweb</generator>\n";
3493 # contents
3494 for (my $i = 0; $i <= $#commitlist; $i++) {
3495 my %co = %{$commitlist[$i]};
3496 my $commit = $co{'id'};
3497 # we read 150, we always show 30 and the ones more recent than 48 hours
3498 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
3499 last;
3501 my %cd = parse_date($co{'author_epoch'});
3503 # get list of changed files
3504 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3505 $co{'parent'} || "--root",
3506 $co{'id'}, "--", (defined $file_name ? $file_name : ())
3507 or next;
3508 my @difftree = map { chomp; $_ } <$fd>;
3509 close $fd
3510 or next;
3512 # print element (entry, item)
3513 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
3514 if ($format eq 'rss') {
3515 print "<item>\n" .
3516 "<title>" . esc_html($co{'title'}) . "</title>\n" .
3517 "<author>" . esc_html($co{'author'}) . "</author>\n" .
3518 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
3519 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
3520 "<link>$co_url</link>\n" .
3521 "<description>" . esc_html($co{'title'}) . "</description>\n" .
3522 "<content:encoded>" .
3523 "<![CDATA[\n";
3524 } elsif ($format eq 'atom') {
3525 print "<entry>\n" .
3526 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
3527 "<updated>$cd{'iso-8601'}</updated>\n" .
3528 "<author>\n" .
3529 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
3530 if ($co{'author_email'}) {
3531 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
3533 print "</author>\n" .
3534 # use committer for contributor
3535 "<contributor>\n" .
3536 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
3537 if ($co{'committer_email'}) {
3538 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
3540 print "</contributor>\n" .
3541 "<published>$cd{'iso-8601'}</published>\n" .
3542 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
3543 "<id>$co_url</id>\n" .
3544 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
3545 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
3547 my $comment = $co{'comment'};
3548 print "<pre>\n";
3549 foreach my $line (@$comment) {
3550 $line = esc_html($line);
3551 print "$line\n";
3553 print "</pre><ul>\n";
3554 foreach my $difftree_line (@difftree) {
3555 my %difftree = parse_difftree_raw_line($difftree_line);
3556 next if !$difftree{'from_id'};
3558 my $file = $difftree{'file'} || $difftree{'to_file'};
3560 print "<li>" .
3561 "[" .
3562 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
3563 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
3564 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
3565 file_name=>$file, file_parent=>$difftree{'from_file'}),
3566 -title => "diff"}, 'D');
3567 if ($have_blame) {
3568 print $cgi->a({-href => href(-full=>1, action=>"blame",
3569 file_name=>$file, hash_base=>$commit),
3570 -title => "blame"}, 'B');
3572 # if this is not a feed of a file history
3573 if (!defined $file_name || $file_name ne $file) {
3574 print $cgi->a({-href => href(-full=>1, action=>"history",
3575 file_name=>$file, hash=>$commit),
3576 -title => "history"}, 'H');
3578 $file = esc_path($file);
3579 print "] ".
3580 "$file</li>\n";
3582 if ($format eq 'rss') {
3583 print "</ul>]]>\n" .
3584 "</content:encoded>\n" .
3585 "</item>\n";
3586 } elsif ($format eq 'atom') {
3587 print "</ul>\n</div>\n" .
3588 "</content>\n" .
3589 "</entry>\n";
3593 # end of feed
3594 if ($format eq 'rss') {
3595 print "</channel>\n</rss>\n";
3596 } elsif ($format eq 'atom') {
3597 print "</feed>\n";
3601 sub git_rss {
3602 git_feed('rss');
3605 sub git_atom {
3606 git_feed('atom');
3609 sub git_opml {
3610 my @list = git_get_projects_list();
3612 print $cgi->header(
3613 -type => 'text/xml',
3614 -charset => 'utf-8',
3615 -content_disposition => 'inline; filename="opml.xml"');
3617 print <<XML;
3618 <?xml version="1.0" encoding="utf-8"?>
3619 <opml version="1.0">
3620 <head>
3621 <title>$site_name OPML Export</title>
3622 </head>
3623 <body>
3624 <outline text="git RSS feeds">
3627 foreach my $pr (@list) {
3628 my %proj = %$pr;
3629 my $head = git_get_head_hash($proj{'path'});
3630 if (!defined $head) {
3631 next;
3633 $git_dir = "$projectroot/$proj{'path'}";
3634 my %co = parse_commit($head);
3635 if (!%co) {
3636 next;
3639 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
3640 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
3641 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
3642 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
3644 print <<XML;
3645 </outline>
3646 </body>
3647 </opml>