gitweb: Add git_merge sub
[git/gsoc2010-gitweb.git] / gitweb / gitweb.perl
blobebcf45aff2b3b89a2854253c86824f907bbabcac
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,
149 "add" => \&git_add,
150 "discard" => \&git_discard,
151 "ignore" => \&git_ignore,
152 "mv" => \&git_mv,
153 "newrepo" => \&git_newrepo,
154 "rm" => \&git_rm,
155 "reset" => \&git_reset,
158 # now read PATH_INFO and update the parameter list for missing parameters
159 sub evaluate_path_info {
160 return if defined $input_params{'project'};
161 return if !$path_info;
162 $path_info =~ s,^/+,,;
163 return if !$path_info;
165 # find which part of PATH_INFO is project
166 my $project = $path_info;
167 $project =~ s,/+$,,;
168 while ($project && !check_head_link("$projectroot/$project")) {
169 $project =~ s,/*[^/]*$,,;
171 return unless $project;
172 $input_params{'project'} = $project;
174 # do not change any parameters if an action is given using the query string
175 return if $input_params{'action'};
176 $path_info =~ s,^\Q$project\E/*,,;
178 # next, check if we have an action
179 my $action = $path_info;
180 $action =~ s,/.*$,,;
181 if (exists $actions{$action}) {
182 $path_info =~ s,^$action/*,,;
183 $input_params{'action'} = $action;
186 return if $input_params{'edit'};
187 # next, check if we have an edit
188 my $edit = $path_info;
189 $edit =~ s,/.*$,,;
190 if (exists $edits{$edit} && gitweb_check_feature('write')) {
191 $path_info =~ s,^$edit/*,,;
192 $input_params{'edit'} = $edit;
195 # list of actions that want hash_base instead of hash, but can have no
196 # pathname (f) parameter
197 my @wants_base = (
198 'tree',
199 'history',
202 # we want to catch
203 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
204 my ($parentrefname, $parentpathname, $refname, $pathname) =
205 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
207 # first, analyze the 'current' part
208 if (defined $pathname) {
209 # we got "branch:filename" or "branch:dir/"
210 # we could use git_get_type(branch:pathname), but:
211 # - it needs $git_dir
212 # - it does a git() call
213 # - the convention of terminating directories with a slash
214 # makes it superfluous
215 # - embedding the action in the PATH_INFO would make it even
216 # more superfluous
217 $pathname =~ s,^/+,,;
218 if (!$pathname || substr($pathname, -1) eq "/") {
219 $input_params{'action'} ||= "tree";
220 $pathname =~ s,/$,,;
221 } else {
222 # the default action depends on whether we had parent info
223 # or not
224 if ($parentrefname) {
225 $input_params{'action'} ||= "blobdiff_plain";
226 } else {
227 $input_params{'action'} ||= "blob_plain";
230 $input_params{'hash_base'} ||= $refname;
231 $input_params{'file_name'} ||= $pathname;
232 } elsif (defined $refname) {
233 # we got "branch". In this case we have to choose if we have to
234 # set hash or hash_base.
236 # Most of the actions without a pathname only want hash to be
237 # set, except for the ones specified in @wants_base that want
238 # hash_base instead. It should also be noted that hand-crafted
239 # links having 'history' as an action and no pathname or hash
240 # set will fail, but that happens regardless of PATH_INFO.
241 $input_params{'action'} ||= "shortlog";
242 if (grep { $_ eq $input_params{'action'} } @wants_base) {
243 $input_params{'hash_base'} ||= $refname;
244 } else {
245 $input_params{'hash'} ||= $refname;
249 # next, handle the 'parent' part, if present
250 if (defined $parentrefname) {
251 # a missing pathspec defaults to the 'current' filename, allowing e.g.
252 # someproject/blobdiff/oldrev..newrev:/filename
253 if ($parentpathname) {
254 $parentpathname =~ s,^/+,,;
255 $parentpathname =~ s,/$,,;
256 $input_params{'file_parent'} ||= $parentpathname;
257 } else {
258 $input_params{'file_parent'} ||= $input_params{'file_name'};
260 # we assume that hash_parent_base is wanted if a path was specified,
261 # or if the action wants hash_base instead of hash
262 if (defined $input_params{'file_parent'} ||
263 grep { $_ eq $input_params{'action'} } @wants_base) {
264 $input_params{'hash_parent_base'} ||= $parentrefname;
265 } else {
266 $input_params{'hash_parent'} ||= $parentrefname;
270 # for the snapshot action, we allow URLs in the form
271 # $project/snapshot/$hash.ext
272 # where .ext determines the snapshot and gets removed from the
273 # passed $refname to provide the $hash.
275 # To be able to tell that $refname includes the format extension, we
276 # require the following two conditions to be satisfied:
277 # - the hash input parameter MUST have been set from the $refname part
278 # of the URL (i.e. they must be equal)
279 # - the snapshot format MUST NOT have been defined already (e.g. from
280 # CGI parameter sf)
281 # It's also useless to try any matching unless $refname has a dot,
282 # so we check for that too
283 if (defined $input_params{'action'} &&
284 $input_params{'action'} eq 'snapshot' &&
285 defined $refname && index($refname, '.') != -1 &&
286 $refname eq $input_params{'hash'} &&
287 !defined $input_params{'snapshot_format'}) {
288 # We loop over the known snapshot formats, checking for
289 # extensions. Allowed extensions are both the defined suffix
290 # (which includes the initial dot already) and the snapshot
291 # format key itself, with a prepended dot
292 while (my ($fmt, $opt) = each %known_snapshot_formats) {
293 my $hash = $refname;
294 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
295 next;
297 my $sfx = $1;
298 # a valid suffix was found, so set the snapshot format
299 # and reset the hash parameter
300 $input_params{'snapshot_format'} = $fmt;
301 $input_params{'hash'} = $hash;
302 # we also set the format suffix to the one requested
303 # in the URL: this way a request for e.g. .tgz returns
304 # a .tgz instead of a .tar.gz
305 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
306 last;
311 sub evaluate_and_validate_params {
312 $action = $input_params{'action'};
313 if (defined $action) {
314 if (!validate_action($action)) {
315 die_error(400, "Invalid action parameter");
319 $edit = $input_params{'edit'};
320 if(defined $edit && gitweb_check_feature('write')) {
321 if(!validate_edit($edit)) {
322 die_error(400, "Invalid edit parameter");
326 # parameters which are pathnames
327 $project = $input_params{'project'};
328 if (defined $project) {
329 if (!validate_project($project)) {
330 undef $project;
331 die_error(404, "No such project");
335 $file_name = $input_params{'file_name'};
336 if (defined $file_name) {
337 if (!validate_pathname($file_name)) {
338 die_error(400, "Invalid file parameter");
342 $file_parent = $input_params{'file_parent'};
343 if (defined $file_parent) {
344 if (!validate_pathname($file_parent)) {
345 die_error(400, "Invalid file parent parameter");
349 # parameters which are refnames
350 $hash = $input_params{'hash'};
351 if (defined $hash) {
352 if (!validate_refname($hash)) {
353 die_error(400, "Invalid hash parameter");
357 $hash_parent = $input_params{'hash_parent'};
358 if (defined $hash_parent) {
359 if (!validate_refname($hash_parent)) {
360 die_error(400, "Invalid hash parent parameter");
364 $hash_base = $input_params{'hash_base'};
365 if (defined $hash_base) {
366 if (!validate_refname($hash_base)) {
367 die_error(400, "Invalid hash base parameter");
371 @extra_options = @{$input_params{'extra_options'}};
372 # @extra_options is always defined, since it can only be (currently) set from
373 # CGI, and $cgi->param() returns the empty array in array context if the param
374 # is not set
375 foreach my $opt (@extra_options) {
376 if (not exists $allowed_options{$opt}) {
377 die_error(400, "Invalid option parameter");
379 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
380 die_error(400, "Invalid option parameter for this action");
384 $hash_parent_base = $input_params{'hash_parent_base'};
385 if (defined $hash_parent_base) {
386 if (!validate_refname($hash_parent_base)) {
387 die_error(400, "Invalid hash parent base parameter");
391 # other parameters
392 $page = $input_params{'page'};
393 if (defined $page) {
394 if ($page =~ m/[^0-9]/) {
395 die_error(400, "Invalid page parameter");
399 $searchtype = $input_params{'searchtype'};
400 if (defined $searchtype) {
401 if ($searchtype =~ m/[^a-z]/) {
402 die_error(400, "Invalid searchtype parameter");
406 $search_use_regexp = $input_params{'search_use_regexp'};
408 $searchtext = $input_params{'searchtext'};
409 if (defined $searchtext) {
410 if (length($searchtext) < 2) {
411 die_error(403, "At least two characters are required for search parameter");
413 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
417 sub evaluate_git_dir {
418 $git_dir = "$projectroot/$project" if $project;
421 # custom error handler: 'die <message>' is Internal Server Error
422 sub handle_errors_html {
423 my $msg = shift; # it is already HTML escaped
425 # to avoid infinite loop where error occurs in die_error,
426 # change handler to default handler, disabling handle_errors_html
427 set_message("Error occured when inside die_error:\n$msg");
429 # you cannot jump out of die_error when called as error handler;
430 # the subroutine set via CGI::Carp::set_message is called _after_
431 # HTTP headers are already written, so it cannot write them itself
432 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
434 set_message(\&handle_errors_html);
436 # dispatch
437 sub dispatch {
438 if (!defined $action) {
439 if (defined $hash) {
440 $action = git_get_type($hash);
441 } elsif (defined $hash_base && defined $file_name) {
442 $action = git_get_type("$hash_base:$file_name");
443 } elsif (defined $project) {
444 $action = 'summary';
445 } else {
446 $action = 'project_list';
449 if (defined $edit) {
450 $action = $edit;
451 %actions = %edits;
453 if (!defined($actions{$action})) {
454 die_error(400, "Unknown action or edit");
456 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
457 !$project) {
458 die_error(400, "Project needed");
461 $actions{$action}->();
464 sub run_request {
465 our $t0 = [Time::HiRes::gettimeofday()]
466 if defined $t0;
468 evaluate_uri();
469 evaluate_gitweb_config();
470 evaluate_git_version();
471 check_loadavg();
473 # $projectroot and $projects_list might be set in gitweb config file
474 $projects_list ||= $projectroot;
476 evaluate_query_params();
477 evaluate_path_info();
478 evaluate_and_validate_params();
479 evaluate_git_dir();
481 configure_gitweb_features();
483 dispatch();
486 our $is_last_request = sub { 1 };
487 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
488 our $CGI = 'CGI';
489 sub configure_as_fcgi {
490 require CGI::Fast;
491 our $CGI = 'CGI::Fast';
493 my $request_number = 0;
494 # let each child service 100 requests
495 our $is_last_request = sub { ++$request_number > 100 };
497 sub evaluate_argv {
498 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
499 configure_as_fcgi()
500 if $script_name =~ /\.fcgi$/;
502 return unless (@ARGV);
504 require Getopt::Long;
505 Getopt::Long::GetOptions(
506 'fastcgi|fcgi|f' => \&configure_as_fcgi,
507 'nproc|n=i' => sub {
508 my ($arg, $val) = @_;
509 return unless eval { require FCGI::ProcManager; 1; };
510 my $proc_manager = FCGI::ProcManager->new({
511 n_processes => $val,
513 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
514 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
515 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
520 sub run {
521 evaluate_argv();
523 $pre_listen_hook->()
524 if $pre_listen_hook;
526 REQUEST:
527 while ($cgi = $CGI->new()) {
528 $pre_dispatch_hook->()
529 if $pre_dispatch_hook;
531 run_request();
533 $pre_dispatch_hook->()
534 if $post_dispatch_hook;
536 last REQUEST if ($is_last_request->());
539 DONE_GITWEB:
543 run();
545 if (defined caller) {
546 # wrapped in a subroutine processing requests,
547 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
548 return;
549 } else {
550 # pure CGI script, serving single request
551 exit;
554 ## ======================================================================
555 ## validation, quoting/unquoting and escaping
557 sub validate_action {
558 my $input = shift || return undef;
559 return undef unless exists $actions{$input};
560 return $input;
563 sub validate_edit {
564 my $input = shift || return undef;
565 return undef unless exists $edits{$input};
566 return $input;
569 sub validate_project {
570 my $input = shift || return undef;
571 if (!validate_pathname($input) ||
572 !(-d "$projectroot/$input") ||
573 !check_export_ok("$projectroot/$input") ||
574 ($strict_export && !project_in_list($input))) {
575 return undef;
576 } else {
577 return $input;
581 sub validate_pathname {
582 my $input = shift || return undef;
584 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
585 # at the beginning, at the end, and between slashes.
586 # also this catches doubled slashes
587 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
588 return undef;
590 # no null characters
591 if ($input =~ m!\0!) {
592 return undef;
594 return $input;
597 sub validate_refname {
598 my $input = shift || return undef;
600 # textual hashes are O.K.
601 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
602 return $input;
604 # it must be correct pathname
605 $input = validate_pathname($input)
606 or return undef;
607 # restrictions on ref name according to git-check-ref-format
608 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
609 return undef;
611 return $input;
614 ## ......................................................................
615 ## functions printing or outputting HTML: div
617 # Outputs the author name and date in long form
618 sub git_print_authorship {
619 my $co = shift;
620 my %opts = @_;
621 my $tag = $opts{-tag} || 'div';
622 my $author = $co->{'author_name'};
624 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
625 print "<$tag class=\"author_date\">" .
626 format_search_author($author, "author", esc_html($author)) .
627 " [$ad{'rfc2822'}";
628 print_local_time(%ad) if ($opts{-localtime});
629 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
630 . "</$tag>\n";
633 # Outputs table rows containing the full author or committer information,
634 # in the format expected for 'commit' view (& similia).
635 # Parameters are a commit hash reference, followed by the list of people
636 # to output information for. If the list is empty it defalts to both
637 # author and committer.
638 sub git_print_authorship_rows {
639 my $co = shift;
640 # too bad we can't use @people = @_ || ('author', 'committer')
641 my @people = @_;
642 @people = ('author', 'committer') unless @people;
643 foreach my $who (@people) {
644 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
645 print "<tr><td>$who</td><td>" .
646 format_search_author($co->{"${who}_name"}, $who,
647 esc_html($co->{"${who}_name"})) . " " .
648 format_search_author($co->{"${who}_email"}, $who,
649 esc_html("<" . $co->{"${who}_email"} . ">")) .
650 "</td><td rowspan=\"2\">" .
651 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
652 "</td></tr>\n" .
653 "<tr>" .
654 "<td></td><td> $wd{'rfc2822'}";
655 print_local_time(%wd);
656 print "</td>" .
657 "</tr>\n";
661 sub git_print_log {
662 my $log = shift;
663 my %opts = @_;
665 if ($opts{'-remove_title'}) {
666 # remove title, i.e. first line of log
667 shift @$log;
669 # remove leading empty lines
670 while (defined $log->[0] && $log->[0] eq "") {
671 shift @$log;
674 # print log
675 my $signoff = 0;
676 my $empty = 0;
677 foreach my $line (@$log) {
678 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
679 $signoff = 1;
680 $empty = 0;
681 if (! $opts{'-remove_signoff'}) {
682 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
683 next;
684 } else {
685 # remove signoff lines
686 next;
688 } else {
689 $signoff = 0;
692 # print only one empty line
693 # do not print empty line after signoff
694 if ($line eq "") {
695 next if ($empty || $signoff);
696 $empty = 1;
697 } else {
698 $empty = 0;
701 print format_log_line_html($line) . "<br/>\n";
704 if ($opts{'-final_empty_line'}) {
705 # end with single empty line
706 print "<br/>\n" unless $empty;
710 ## ......................................................................
711 ## functions printing large fragments of HTML
713 sub git_difftree_body {
714 my ($difftree, $hash, @parents) = @_;
715 my ($parent) = $parents[0];
716 my $have_blame = gitweb_check_feature('blame');
717 print "<div class=\"list_head\">\n";
718 if ($#{$difftree} > 10) {
719 print(($#{$difftree} + 1) . " files changed:\n");
721 print "</div>\n";
723 print "<table class=\"" .
724 (@parents > 1 ? "combined " : "") .
725 "diff_tree\">\n";
727 # header only for combined diff in 'commitdiff' view
728 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
729 if ($has_header) {
730 # table header
731 print "<thead><tr>\n" .
732 "<th></th><th></th>\n"; # filename, patchN link
733 for (my $i = 0; $i < @parents; $i++) {
734 my $par = $parents[$i];
735 print "<th>" .
736 $cgi->a({-href => href(action=>"commitdiff",
737 hash=>$hash, hash_parent=>$par),
738 -title => 'commitdiff to parent number ' .
739 ($i+1) . ': ' . substr($par,0,7)},
740 $i+1) .
741 "&nbsp;</th>\n";
743 print "</tr></thead>\n<tbody>\n";
746 my $alternate = 1;
747 my $patchno = 0;
748 foreach my $line (@{$difftree}) {
749 my $diff = parsed_difftree_line($line);
751 if ($alternate) {
752 print "<tr class=\"dark\">\n";
753 } else {
754 print "<tr class=\"light\">\n";
756 $alternate ^= 1;
758 if (exists $diff->{'nparents'}) { # combined diff
760 fill_from_file_info($diff, @parents)
761 unless exists $diff->{'from_file'};
763 if (!is_deleted($diff)) {
764 # file exists in the result (child) commit
765 print "<td>" .
766 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
767 file_name=>$diff->{'to_file'},
768 hash_base=>$hash),
769 -class => "list"}, esc_path($diff->{'to_file'})) .
770 "</td>\n";
771 } else {
772 print "<td>" .
773 esc_path($diff->{'to_file'}) .
774 "</td>\n";
777 if ($action eq 'commitdiff') {
778 # link to patch
779 $patchno++;
780 print "<td class=\"link\">" .
781 $cgi->a({-href => "#patch$patchno"}, "patch") .
782 " | " .
783 "</td>\n";
786 my $has_history = 0;
787 my $not_deleted = 0;
788 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
789 my $hash_parent = $parents[$i];
790 my $from_hash = $diff->{'from_id'}[$i];
791 my $from_path = $diff->{'from_file'}[$i];
792 my $status = $diff->{'status'}[$i];
794 $has_history ||= ($status ne 'A');
795 $not_deleted ||= ($status ne 'D');
797 if ($status eq 'A') {
798 print "<td class=\"link\" align=\"right\"> | </td>\n";
799 } elsif ($status eq 'D') {
800 print "<td class=\"link\">" .
801 $cgi->a({-href => href(action=>"blob",
802 hash_base=>$hash,
803 hash=>$from_hash,
804 file_name=>$from_path)},
805 "blob" . ($i+1)) .
806 " | </td>\n";
807 } else {
808 if ($diff->{'to_id'} eq $from_hash) {
809 print "<td class=\"link nochange\">";
810 } else {
811 print "<td class=\"link\">";
813 print $cgi->a({-href => href(action=>"blobdiff",
814 hash=>$diff->{'to_id'},
815 hash_parent=>$from_hash,
816 hash_base=>$hash,
817 hash_parent_base=>$hash_parent,
818 file_name=>$diff->{'to_file'},
819 file_parent=>$from_path)},
820 "diff" . ($i+1)) .
821 " | </td>\n";
825 print "<td class=\"link\">";
826 if ($not_deleted) {
827 print $cgi->a({-href => href(action=>"blob",
828 hash=>$diff->{'to_id'},
829 file_name=>$diff->{'to_file'},
830 hash_base=>$hash)},
831 "blob");
832 print " | " if ($has_history);
834 if ($has_history) {
835 print $cgi->a({-href => href(action=>"history",
836 file_name=>$diff->{'to_file'},
837 hash_base=>$hash)},
838 "history");
840 print "</td>\n";
842 print "</tr>\n";
843 next; # instead of 'else' clause, to avoid extra indent
845 # else ordinary diff
847 my ($to_mode_oct, $to_mode_str, $to_file_type);
848 my ($from_mode_oct, $from_mode_str, $from_file_type);
849 if ($diff->{'to_mode'} ne ('0' x 6)) {
850 $to_mode_oct = oct $diff->{'to_mode'};
851 if (S_ISREG($to_mode_oct)) { # only for regular file
852 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
854 $to_file_type = file_type($diff->{'to_mode'});
856 if ($diff->{'from_mode'} ne ('0' x 6)) {
857 $from_mode_oct = oct $diff->{'from_mode'};
858 if (S_ISREG($to_mode_oct)) { # only for regular file
859 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
861 $from_file_type = file_type($diff->{'from_mode'});
864 if ($diff->{'status'} eq "A") { # created
865 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
866 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
867 $mode_chng .= "]</span>";
868 print "<td>";
869 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
870 hash_base=>$hash, file_name=>$diff->{'file'}),
871 -class => "list"}, esc_path($diff->{'file'}));
872 print "</td>\n";
873 print "<td>$mode_chng</td>\n";
874 print "<td class=\"link\">";
875 if ($action eq 'commitdiff') {
876 # link to patch
877 $patchno++;
878 print $cgi->a({-href => "#patch$patchno"}, "patch");
879 print " | ";
881 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
882 hash_base=>$hash, file_name=>$diff->{'file'})},
883 "blob");
884 print "</td>\n";
886 } elsif ($diff->{'status'} eq "D") { # deleted
887 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
888 print "<td>";
889 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
890 hash_base=>$parent, file_name=>$diff->{'file'}),
891 -class => "list"}, esc_path($diff->{'file'}));
892 print "</td>\n";
893 print "<td>$mode_chng</td>\n";
894 print "<td class=\"link\">";
895 if ($action eq 'commitdiff') {
896 # link to patch
897 $patchno++;
898 print $cgi->a({-href => "#patch$patchno"}, "patch");
899 print " | ";
901 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
902 hash_base=>$parent, file_name=>$diff->{'file'})},
903 "blob") . " | ";
904 if ($have_blame) {
905 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
906 file_name=>$diff->{'file'})},
907 "blame") . " | ";
909 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
910 file_name=>$diff->{'file'})},
911 "history");
912 print "</td>\n";
914 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
915 my $mode_chnge = "";
916 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
917 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
918 if ($from_file_type ne $to_file_type) {
919 $mode_chnge .= " from $from_file_type to $to_file_type";
921 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
922 if ($from_mode_str && $to_mode_str) {
923 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
924 } elsif ($to_mode_str) {
925 $mode_chnge .= " mode: $to_mode_str";
928 $mode_chnge .= "]</span>\n";
930 print "<td>";
931 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
932 hash_base=>$hash, file_name=>$diff->{'file'}),
933 -class => "list"}, esc_path($diff->{'file'}));
934 print "</td>\n";
935 print "<td>$mode_chnge</td>\n";
936 print "<td class=\"link\">";
937 if ($action eq 'commitdiff') {
938 # link to patch
939 $patchno++;
940 print $cgi->a({-href => "#patch$patchno"}, "patch") .
941 " | ";
942 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
943 # "commit" view and modified file (not onlu mode changed)
944 print $cgi->a({-href => href(action=>"blobdiff",
945 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
946 hash_base=>$hash, hash_parent_base=>$parent,
947 file_name=>$diff->{'file'})},
948 "diff") .
949 " | ";
951 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
952 hash_base=>$hash, file_name=>$diff->{'file'})},
953 "blob") . " | ";
954 if ($have_blame) {
955 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
956 file_name=>$diff->{'file'})},
957 "blame") . " | ";
959 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
960 file_name=>$diff->{'file'})},
961 "history");
962 print "</td>\n";
964 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
965 my %status_name = ('R' => 'moved', 'C' => 'copied');
966 my $nstatus = $status_name{$diff->{'status'}};
967 my $mode_chng = "";
968 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
969 # mode also for directories, so we cannot use $to_mode_str
970 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
972 print "<td>" .
973 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
974 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
975 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
976 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
977 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
978 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
979 -class => "list"}, esc_path($diff->{'from_file'})) .
980 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
981 "<td class=\"link\">";
982 if ($action eq 'commitdiff') {
983 # link to patch
984 $patchno++;
985 print $cgi->a({-href => "#patch$patchno"}, "patch") .
986 " | ";
987 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
988 # "commit" view and modified file (not only pure rename or copy)
989 print $cgi->a({-href => href(action=>"blobdiff",
990 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
991 hash_base=>$hash, hash_parent_base=>$parent,
992 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
993 "diff") .
994 " | ";
996 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
997 hash_base=>$parent, file_name=>$diff->{'to_file'})},
998 "blob") . " | ";
999 if ($have_blame) {
1000 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
1001 file_name=>$diff->{'to_file'})},
1002 "blame") . " | ";
1004 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
1005 file_name=>$diff->{'to_file'})},
1006 "history");
1007 print "</td>\n";
1009 } # we should not encounter Unmerged (U) or Unknown (X) status
1010 print "</tr>\n";
1012 print "</tbody>" if $has_header;
1013 print "</table>\n";
1016 sub git_patchset_body {
1017 my ($fd, $difftree, $hash, @hash_parents) = @_;
1018 my ($hash_parent) = $hash_parents[0];
1020 my $is_combined = (@hash_parents > 1);
1021 my $patch_idx = 0;
1022 my $patch_number = 0;
1023 my $patch_line;
1024 my $diffinfo;
1025 my $to_name;
1026 my (%from, %to);
1028 print "<div class=\"patchset\">\n";
1030 # skip to first patch
1031 while ($patch_line = <$fd>) {
1032 chomp $patch_line;
1034 last if ($patch_line =~ m/^diff /);
1037 PATCH:
1038 while ($patch_line) {
1040 # parse "git diff" header line
1041 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
1042 # $1 is from_name, which we do not use
1043 $to_name = unquote($2);
1044 $to_name =~ s!^b/!!;
1045 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
1046 # $1 is 'cc' or 'combined', which we do not use
1047 $to_name = unquote($2);
1048 } else {
1049 $to_name = undef;
1052 # check if current patch belong to current raw line
1053 # and parse raw git-diff line if needed
1054 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
1055 # this is continuation of a split patch
1056 print "<div class=\"patch cont\">\n";
1057 } else {
1058 # advance raw git-diff output if needed
1059 $patch_idx++ if defined $diffinfo;
1061 # read and prepare patch information
1062 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1064 # compact combined diff output can have some patches skipped
1065 # find which patch (using pathname of result) we are at now;
1066 if ($is_combined) {
1067 while ($to_name ne $diffinfo->{'to_file'}) {
1068 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1069 format_diff_cc_simplified($diffinfo, @hash_parents) .
1070 "</div>\n"; # class="patch"
1072 $patch_idx++;
1073 $patch_number++;
1075 last if $patch_idx > $#$difftree;
1076 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1080 # modifies %from, %to hashes
1081 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
1083 # this is first patch for raw difftree line with $patch_idx index
1084 # we index @$difftree array from 0, but number patches from 1
1085 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1088 # git diff header
1089 #assert($patch_line =~ m/^diff /) if DEBUG;
1090 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
1091 $patch_number++;
1092 # print "git diff" header
1093 print format_git_diff_header_line($patch_line, $diffinfo,
1094 \%from, \%to);
1096 # print extended diff header
1097 print "<div class=\"diff extended_header\">\n";
1098 EXTENDED_HEADER:
1099 while ($patch_line = <$fd>) {
1100 chomp $patch_line;
1102 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
1104 print format_extended_diff_header_line($patch_line, $diffinfo,
1105 \%from, \%to);
1107 print "</div>\n"; # class="diff extended_header"
1109 # from-file/to-file diff header
1110 if (! $patch_line) {
1111 print "</div>\n"; # class="patch"
1112 last PATCH;
1114 next PATCH if ($patch_line =~ m/^diff /);
1115 #assert($patch_line =~ m/^---/) if DEBUG;
1117 my $last_patch_line = $patch_line;
1118 $patch_line = <$fd>;
1119 chomp $patch_line;
1120 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
1122 print format_diff_from_to_header($last_patch_line, $patch_line,
1123 $diffinfo, \%from, \%to,
1124 @hash_parents);
1126 # the patch itself
1127 LINE:
1128 while ($patch_line = <$fd>) {
1129 chomp $patch_line;
1131 next PATCH if ($patch_line =~ m/^diff /);
1133 print format_diff_line($patch_line, \%from, \%to);
1136 } continue {
1137 print "</div>\n"; # class="patch"
1140 # for compact combined (--cc) format, with chunk and patch simpliciaction
1141 # patchset might be empty, but there might be unprocessed raw lines
1142 for (++$patch_idx if $patch_number > 0;
1143 $patch_idx < @$difftree;
1144 ++$patch_idx) {
1145 # read and prepare patch information
1146 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
1148 # generate anchor for "patch" links in difftree / whatchanged part
1149 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1150 format_diff_cc_simplified($diffinfo, @hash_parents) .
1151 "</div>\n"; # class="patch"
1153 $patch_number++;
1156 if ($patch_number == 0) {
1157 if (@hash_parents > 1) {
1158 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
1159 } else {
1160 print "<div class=\"diff nodifferences\">No differences found</div>\n";
1164 print "</div>\n"; # class="patchset"
1167 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1169 # fills project list info (age, description, owner, forks) for each
1170 # project in the list, removing invalid projects from returned list
1171 # NOTE: modifies $projlist, but does not remove entries from it
1172 sub fill_project_list_info {
1173 my ($projlist, $check_forks) = @_;
1174 my @projects;
1176 my $show_ctags = gitweb_check_feature('ctags');
1177 PROJECT:
1178 foreach my $pr (@$projlist) {
1179 my (@activity) = git_get_last_activity($pr->{'path'});
1180 unless (@activity) {
1181 next PROJECT;
1183 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
1184 if (!defined $pr->{'descr'}) {
1185 my $descr = git_get_project_description($pr->{'path'}) || "";
1186 $descr = to_utf8($descr);
1187 $pr->{'descr_long'} = $descr;
1188 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
1190 if (!defined $pr->{'owner'}) {
1191 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
1193 if ($check_forks) {
1194 my $pname = $pr->{'path'};
1195 if (($pname =~ s/\.git$//) &&
1196 ($pname !~ /\/$/) &&
1197 (-d "$projectroot/$pname")) {
1198 $pr->{'forks'} = "-d $projectroot/$pname";
1199 } else {
1200 $pr->{'forks'} = 0;
1203 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
1204 push @projects, $pr;
1207 return @projects;
1210 sub git_project_list_body {
1211 # actually uses global variable $project
1212 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
1214 my $check_forks = gitweb_check_feature('forks');
1215 my @projects = fill_project_list_info($projlist, $check_forks);
1217 $order ||= $default_projects_order;
1218 $from = 0 unless defined $from;
1219 $to = $#projects if (!defined $to || $#projects < $to);
1221 my %order_info = (
1222 project => { key => 'path', type => 'str' },
1223 descr => { key => 'descr_long', type => 'str' },
1224 owner => { key => 'owner', type => 'str' },
1225 age => { key => 'age', type => 'num' }
1227 my $oi = $order_info{$order};
1228 if ($oi->{'type'} eq 'str') {
1229 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
1230 } else {
1231 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
1234 my $show_ctags = gitweb_check_feature('ctags');
1235 if ($show_ctags) {
1236 my %ctags;
1237 foreach my $p (@projects) {
1238 foreach my $ct (keys %{$p->{'ctags'}}) {
1239 $ctags{$ct} += $p->{'ctags'}->{$ct};
1242 my $cloud = git_populate_project_tagcloud(\%ctags);
1243 print git_show_project_tagcloud($cloud, 64);
1246 print "<table class=\"project_list\">\n";
1247 unless ($no_header) {
1248 print "<tr>\n";
1249 if ($check_forks) {
1250 print "<th></th>\n";
1252 print_sort_th('project', $order, 'Project');
1253 print_sort_th('descr', $order, 'Description');
1254 print_sort_th('owner', $order, 'Owner');
1255 print_sort_th('age', $order, 'Last Change');
1256 print "<th></th>\n" . # for links
1257 "</tr>\n";
1259 my $alternate = 1;
1260 my $tagfilter = $cgi->param('by_tag');
1261 for (my $i = $from; $i <= $to; $i++) {
1262 my $pr = $projects[$i];
1264 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
1265 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
1266 and not $pr->{'descr_long'} =~ /$searchtext/;
1267 # Weed out forks or non-matching entries of search
1268 if ($check_forks) {
1269 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
1270 $forkbase="^$forkbase" if $forkbase;
1271 next if not $searchtext and not $tagfilter and $show_ctags
1272 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
1275 if ($alternate) {
1276 print "<tr class=\"dark\">\n";
1277 } else {
1278 print "<tr class=\"light\">\n";
1280 $alternate ^= 1;
1281 if ($check_forks) {
1282 print "<td>";
1283 if ($pr->{'forks'}) {
1284 print "<!-- $pr->{'forks'} -->\n";
1285 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
1287 print "</td>\n";
1289 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
1290 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
1291 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
1292 -class => "list", -title => $pr->{'descr_long'}},
1293 esc_html($pr->{'descr'})) . "</td>\n" .
1294 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
1295 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
1296 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
1297 "<td class=\"link\">" .
1298 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
1299 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
1300 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
1301 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
1302 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
1303 "</td>\n" .
1304 "</tr>\n";
1306 if (defined $extra) {
1307 print "<tr>\n";
1308 if ($check_forks) {
1309 print "<td></td>\n";
1311 print "<td colspan=\"5\">$extra</td>\n" .
1312 "</tr>\n";
1314 print "</table>\n";
1317 sub git_log_body {
1318 # uses global variable $project
1319 my ($commitlist, $from, $to, $refs, $extra) = @_;
1321 $from = 0 unless defined $from;
1322 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1324 for (my $i = 0; $i <= $to; $i++) {
1325 my %co = %{$commitlist->[$i]};
1326 next if !%co;
1327 my $commit = $co{'id'};
1328 my $ref = format_ref_marker($refs, $commit);
1329 my %ad = parse_date($co{'author_epoch'});
1330 git_print_header_div('commit',
1331 "<span class=\"age\">$co{'age_string'}</span>" .
1332 esc_html($co{'title'}) . $ref,
1333 $commit);
1334 print "<div class=\"title_text\">\n" .
1335 "<div class=\"log_link\">\n" .
1336 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
1337 " | " .
1338 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
1339 " | " .
1340 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
1341 "<br/>\n" .
1342 "</div>\n";
1343 git_print_authorship(\%co, -tag => 'span');
1344 print "<br/>\n</div>\n";
1346 print "<div class=\"log_body\">\n";
1347 git_print_log($co{'comment'}, -final_empty_line=> 1);
1348 print "</div>\n";
1350 if ($extra) {
1351 print "<div class=\"page_nav\">\n";
1352 print "$extra\n";
1353 print "</div>\n";
1357 sub git_shortlog_body {
1358 # uses global variable $project
1359 my ($commitlist, $from, $to, $refs, $extra) = @_;
1361 $from = 0 unless defined $from;
1362 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1364 print "<table class=\"shortlog\">\n";
1365 my $alternate = 1;
1366 for (my $i = $from; $i <= $to; $i++) {
1367 my %co = %{$commitlist->[$i]};
1368 my $commit = $co{'id'};
1369 my $ref = format_ref_marker($refs, $commit);
1370 if ($alternate) {
1371 print "<tr class=\"dark\">\n";
1372 } else {
1373 print "<tr class=\"light\">\n";
1375 $alternate ^= 1;
1376 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
1377 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1378 format_author_html('td', \%co, 10) . "<td>";
1379 print format_subject_html($co{'title'}, $co{'title_short'},
1380 href(action=>"commit", hash=>$commit), $ref);
1381 print "</td>\n" .
1382 "<td class=\"link\">" .
1383 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
1384 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
1385 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
1386 my $snapshot_links = format_snapshot_links($commit);
1387 if (defined $snapshot_links) {
1388 print " | " . $snapshot_links;
1390 print "</td>\n" .
1391 "</tr>\n";
1393 if (defined $extra) {
1394 print "<tr>\n" .
1395 "<td colspan=\"4\">$extra</td>\n" .
1396 "</tr>\n";
1398 print "</table>\n";
1401 sub git_history_body {
1402 # Warning: assumes constant type (blob or tree) during history
1403 my ($commitlist, $from, $to, $refs, $extra,
1404 $file_name, $file_hash, $ftype) = @_;
1406 $from = 0 unless defined $from;
1407 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
1409 print "<table class=\"history\">\n";
1410 my $alternate = 1;
1411 for (my $i = $from; $i <= $to; $i++) {
1412 my %co = %{$commitlist->[$i]};
1413 if (!%co) {
1414 next;
1416 my $commit = $co{'id'};
1418 my $ref = format_ref_marker($refs, $commit);
1420 if ($alternate) {
1421 print "<tr class=\"dark\">\n";
1422 } else {
1423 print "<tr class=\"light\">\n";
1425 $alternate ^= 1;
1426 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1427 # shortlog: format_author_html('td', \%co, 10)
1428 format_author_html('td', \%co, 15, 3) . "<td>";
1429 # originally git_history used chop_str($co{'title'}, 50)
1430 print format_subject_html($co{'title'}, $co{'title_short'},
1431 href(action=>"commit", hash=>$commit), $ref);
1432 print "</td>\n" .
1433 "<td class=\"link\">" .
1434 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
1435 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
1437 if ($ftype eq 'blob') {
1438 my $blob_current = $file_hash;
1439 my $blob_parent = git_get_hash_by_path($commit, $file_name);
1440 if (defined $blob_current && defined $blob_parent &&
1441 $blob_current ne $blob_parent) {
1442 print " | " .
1443 $cgi->a({-href => href(action=>"blobdiff",
1444 hash=>$blob_current, hash_parent=>$blob_parent,
1445 hash_base=>$hash_base, hash_parent_base=>$commit,
1446 file_name=>$file_name)},
1447 "diff to current");
1450 print "</td>\n" .
1451 "</tr>\n";
1453 if (defined $extra) {
1454 print "<tr>\n" .
1455 "<td colspan=\"4\">$extra</td>\n" .
1456 "</tr>\n";
1458 print "</table>\n";
1461 sub git_tags_body {
1462 # uses global variable $project
1463 my ($taglist, $from, $to, $extra) = @_;
1464 $from = 0 unless defined $from;
1465 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
1467 print "<table class=\"tags\">\n";
1468 my $alternate = 1;
1469 for (my $i = $from; $i <= $to; $i++) {
1470 my $entry = $taglist->[$i];
1471 my %tag = %$entry;
1472 my $comment = $tag{'subject'};
1473 my $comment_short;
1474 if (defined $comment) {
1475 $comment_short = chop_str($comment, 30, 5);
1477 if ($alternate) {
1478 print "<tr class=\"dark\">\n";
1479 } else {
1480 print "<tr class=\"light\">\n";
1482 $alternate ^= 1;
1483 if (defined $tag{'age'}) {
1484 print "<td><i>$tag{'age'}</i></td>\n";
1485 } else {
1486 print "<td></td>\n";
1488 print "<td>" .
1489 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
1490 -class => "list name"}, esc_html($tag{'name'})) .
1491 "</td>\n" .
1492 "<td>";
1493 if (defined $comment) {
1494 print format_subject_html($comment, $comment_short,
1495 href(action=>"tag", hash=>$tag{'id'}));
1497 print "</td>\n" .
1498 "<td class=\"selflink\">";
1499 if ($tag{'type'} eq "tag") {
1500 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
1501 } else {
1502 print "&nbsp;";
1504 print "</td>\n" .
1505 "<td class=\"link\">" . " | " .
1506 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
1507 if ($tag{'reftype'} eq "commit") {
1508 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
1509 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
1510 } elsif ($tag{'reftype'} eq "blob") {
1511 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
1513 print "</td>\n" .
1514 "</tr>";
1516 if (defined $extra) {
1517 print "<tr>\n" .
1518 "<td colspan=\"5\">$extra</td>\n" .
1519 "</tr>\n";
1521 print "</table>\n";
1524 sub git_heads_body {
1525 # uses global variable $project
1526 my ($headlist, $head, $from, $to, $extra) = @_;
1527 $from = 0 unless defined $from;
1528 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
1530 print "<table class=\"heads\">\n";
1531 my $alternate = 1;
1532 for (my $i = $from; $i <= $to; $i++) {
1533 my $entry = $headlist->[$i];
1534 my %ref = %$entry;
1535 my $curr = $ref{'id'} eq $head;
1536 if ($alternate) {
1537 print "<tr class=\"dark\">\n";
1538 } else {
1539 print "<tr class=\"light\">\n";
1541 $alternate ^= 1;
1542 print "<td><i>$ref{'age'}</i></td>\n" .
1543 ($curr ? "<td class=\"current_head\">" : "<td>") .
1544 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
1545 -class => "list name"},esc_html($ref{'name'})) .
1546 "</td>\n" .
1547 "<td class=\"link\">" .
1548 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
1549 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
1550 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
1551 "</td>\n" .
1552 "</tr>";
1554 if (defined $extra) {
1555 print "<tr>\n" .
1556 "<td colspan=\"3\">$extra</td>\n" .
1557 "</tr>\n";
1559 print "</table>\n";
1562 sub git_search_grep_body {
1563 my ($commitlist, $from, $to, $extra) = @_;
1564 $from = 0 unless defined $from;
1565 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1567 print "<table class=\"commit_search\">\n";
1568 my $alternate = 1;
1569 for (my $i = $from; $i <= $to; $i++) {
1570 my %co = %{$commitlist->[$i]};
1571 if (!%co) {
1572 next;
1574 my $commit = $co{'id'};
1575 if ($alternate) {
1576 print "<tr class=\"dark\">\n";
1577 } else {
1578 print "<tr class=\"light\">\n";
1580 $alternate ^= 1;
1581 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1582 format_author_html('td', \%co, 15, 5) .
1583 "<td>" .
1584 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
1585 -class => "list subject"},
1586 chop_and_escape_str($co{'title'}, 50) . "<br/>");
1587 my $comment = $co{'comment'};
1588 foreach my $line (@$comment) {
1589 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
1590 my ($lead, $match, $trail) = ($1, $2, $3);
1591 $match = chop_str($match, 70, 5, 'center');
1592 my $contextlen = int((80 - length($match))/2);
1593 $contextlen = 30 if ($contextlen > 30);
1594 $lead = chop_str($lead, $contextlen, 10, 'left');
1595 $trail = chop_str($trail, $contextlen, 10, 'right');
1597 $lead = esc_html($lead);
1598 $match = esc_html($match);
1599 $trail = esc_html($trail);
1601 print "$lead<span class=\"match\">$match</span>$trail<br />";
1604 print "</td>\n" .
1605 "<td class=\"link\">" .
1606 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
1607 " | " .
1608 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
1609 " | " .
1610 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
1611 print "</td>\n" .
1612 "</tr>\n";
1614 if (defined $extra) {
1615 print "<tr>\n" .
1616 "<td colspan=\"3\">$extra</td>\n" .
1617 "</tr>\n";
1619 print "</table>\n";
1622 ## ======================================================================
1623 ## ======================================================================
1624 ## actions
1626 sub git_project_list {
1627 my $order = $input_params{'order'};
1628 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
1629 die_error(400, "Unknown order parameter");
1632 my @list = git_get_projects_list();
1633 if (!@list) {
1634 die_error(404, "No projects found");
1637 git_header_html();
1638 if (defined $home_text && -f $home_text) {
1639 print "<div class=\"index_include\">\n";
1640 insert_file($home_text);
1641 print "</div>\n";
1643 print $cgi->startform(-method => "get") .
1644 "<p class=\"projsearch\">Search:\n" .
1645 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
1646 "</p>" .
1647 $cgi->end_form() . "\n";
1648 git_project_list_body(\@list, $order);
1649 git_footer_html();
1652 sub git_forks {
1653 my $order = $input_params{'order'};
1654 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
1655 die_error(400, "Unknown order parameter");
1658 my @list = git_get_projects_list($project);
1659 if (!@list) {
1660 die_error(404, "No forks found");
1663 git_header_html();
1664 git_print_page_nav('','');
1665 git_print_header_div('summary', "$project forks");
1666 git_project_list_body(\@list, $order);
1667 git_footer_html();
1670 sub git_project_index {
1671 my @projects = git_get_projects_list($project);
1673 print $cgi->header(
1674 -type => 'text/plain',
1675 -charset => 'utf-8',
1676 -content_disposition => 'inline; filename="index.aux"');
1678 foreach my $pr (@projects) {
1679 if (!exists $pr->{'owner'}) {
1680 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
1683 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
1684 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
1685 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
1686 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
1687 $path =~ s/ /\+/g;
1688 $owner =~ s/ /\+/g;
1690 print "$path $owner\n";
1694 sub git_summary {
1695 my $descr = git_get_project_description($project) || "none";
1696 my %co = parse_commit("HEAD");
1697 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
1698 my $head = $co{'id'};
1700 my $owner = git_get_project_owner($project);
1702 my $refs = git_get_references();
1703 # These get_*_list functions return one more to allow us to see if
1704 # there are more ...
1705 my @taglist = git_get_tags_list(16);
1706 my @headlist = git_get_heads_list(16);
1707 my @forklist;
1708 my $check_forks = gitweb_check_feature('forks');
1710 if ($check_forks) {
1711 @forklist = git_get_projects_list($project);
1714 git_header_html();
1715 git_print_page_nav('summary','', $head);
1717 print "<div class=\"title\">&nbsp;</div>\n";
1718 print "<table class=\"projects_list\">\n" .
1719 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
1720 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
1721 if (defined $cd{'rfc2822'}) {
1722 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
1725 # use per project git URL list in $projectroot/$project/cloneurl
1726 # or make project git URL from git base URL and project name
1727 my $url_tag = "URL";
1728 my @url_list = git_get_project_url_list($project);
1729 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
1730 foreach my $git_url (@url_list) {
1731 next unless $git_url;
1732 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
1733 $url_tag = "";
1736 # Tag cloud
1737 my $show_ctags = gitweb_check_feature('ctags');
1738 if ($show_ctags) {
1739 my $ctags = git_get_project_ctags($project);
1740 my $cloud = git_populate_project_tagcloud($ctags);
1741 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
1742 print "</td>\n<td>" unless %$ctags;
1743 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
1744 print "</td>\n<td>" if %$ctags;
1745 print git_show_project_tagcloud($cloud, 48);
1746 print "</td></tr>";
1749 print "</table>\n";
1751 # If XSS prevention is on, we don't include README.html.
1752 # TODO: Allow a readme in some safe format.
1753 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
1754 print "<div class=\"title\">readme</div>\n" .
1755 "<div class=\"readme\">\n";
1756 insert_file("$projectroot/$project/README.html");
1757 print "\n</div>\n"; # class="readme"
1760 # we need to request one more than 16 (0..15) to check if
1761 # those 16 are all
1762 my @commitlist = $head ? parse_commits($head, 17) : ();
1763 if (@commitlist) {
1764 git_print_header_div('shortlog');
1765 git_shortlog_body(\@commitlist, 0, 15, $refs,
1766 $#commitlist <= 15 ? undef :
1767 $cgi->a({-href => href(action=>"shortlog")}, "..."));
1770 if (@taglist) {
1771 git_print_header_div('tags');
1772 git_tags_body(\@taglist, 0, 15,
1773 $#taglist <= 15 ? undef :
1774 $cgi->a({-href => href(action=>"tags")}, "..."));
1777 if (@headlist) {
1778 git_print_header_div('heads');
1779 git_heads_body(\@headlist, $head, 0, 15,
1780 $#headlist <= 15 ? undef :
1781 $cgi->a({-href => href(action=>"heads")}, "..."));
1784 if (@forklist) {
1785 git_print_header_div('forks');
1786 git_project_list_body(\@forklist, 'age', 0, 15,
1787 $#forklist <= 15 ? undef :
1788 $cgi->a({-href => href(action=>"forks")}, "..."),
1789 'no_header');
1792 git_footer_html();
1795 sub git_tag {
1796 my $head = git_get_head_hash($project);
1797 git_header_html();
1798 git_print_page_nav('','', $head,undef,$head);
1799 my %tag = parse_tag($hash);
1801 if (! %tag) {
1802 die_error(404, "Unknown tag object");
1805 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
1806 print "<div class=\"title_text\">\n" .
1807 "<table class=\"object_header\">\n" .
1808 "<tr>\n" .
1809 "<td>object</td>\n" .
1810 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
1811 $tag{'object'}) . "</td>\n" .
1812 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
1813 $tag{'type'}) . "</td>\n" .
1814 "</tr>\n";
1815 if (defined($tag{'author'})) {
1816 git_print_authorship_rows(\%tag, 'author');
1818 print "</table>\n\n" .
1819 "</div>\n";
1820 print "<div class=\"page_body\">";
1821 my $comment = $tag{'comment'};
1822 foreach my $line (@$comment) {
1823 chomp $line;
1824 print esc_html($line, -nbsp=>1) . "<br/>\n";
1826 print "</div>\n";
1827 git_footer_html();
1830 sub git_blame_common {
1831 my $format = shift || 'porcelain';
1832 if ($format eq 'porcelain' && $cgi->param('js')) {
1833 $format = 'incremental';
1834 $action = 'blame_incremental'; # for page title etc
1837 # permissions
1838 gitweb_check_feature('blame')
1839 or die_error(403, "Blame view not allowed");
1841 # error checking
1842 die_error(400, "No file name given") unless $file_name;
1843 $hash_base ||= git_get_head_hash($project);
1844 die_error(404, "Couldn't find base commit") unless $hash_base;
1845 my %co = parse_commit($hash_base)
1846 or die_error(404, "Commit not found");
1847 my $ftype = "blob";
1848 if (!defined $hash) {
1849 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
1850 or die_error(404, "Error looking up file");
1851 } else {
1852 $ftype = git_get_type($hash);
1853 if ($ftype !~ "blob") {
1854 die_error(400, "Object is not a blob");
1858 my $fd;
1859 if ($format eq 'incremental') {
1860 # get file contents (as base)
1861 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
1862 or die_error(500, "Open git-cat-file failed");
1863 } elsif ($format eq 'data') {
1864 # run git-blame --incremental
1865 open $fd, "-|", git_cmd(), "blame", "--incremental",
1866 $hash_base, "--", $file_name
1867 or die_error(500, "Open git-blame --incremental failed");
1868 } else {
1869 # run git-blame --porcelain
1870 open $fd, "-|", git_cmd(), "blame", '-p',
1871 $hash_base, '--', $file_name
1872 or die_error(500, "Open git-blame --porcelain failed");
1875 # incremental blame data returns early
1876 if ($format eq 'data') {
1877 print $cgi->header(
1878 -type=>"text/plain", -charset => "utf-8",
1879 -status=> "200 OK");
1880 local $| = 1; # output autoflush
1881 print while <$fd>;
1882 close $fd
1883 or print "ERROR $!\n";
1885 print 'END';
1886 if (defined $t0 && gitweb_check_feature('timed')) {
1887 print ' '.
1888 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
1889 ' '.$number_of_git_cmds;
1891 print "\n";
1893 return;
1896 # page header
1897 git_header_html();
1898 my $formats_nav =
1899 $cgi->a({-href => href(action=>"blob", -replay=>1)},
1900 "blob") .
1901 " | ";
1902 if ($format eq 'incremental') {
1903 $formats_nav .=
1904 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
1905 "blame") . " (non-incremental)";
1906 } else {
1907 $formats_nav .=
1908 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
1909 "blame") . " (incremental)";
1911 $formats_nav .=
1912 " | " .
1913 $cgi->a({-href => href(action=>"history", -replay=>1)},
1914 "history") .
1915 " | " .
1916 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
1917 "HEAD");
1918 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
1919 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
1920 git_print_page_path($file_name, $ftype, $hash_base);
1922 # page body
1923 if ($format eq 'incremental') {
1924 print "<noscript>\n<div class=\"error\"><center><b>\n".
1925 "This page requires JavaScript to run.\n Use ".
1926 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
1927 'this page').
1928 " instead.\n".
1929 "</b></center></div>\n</noscript>\n";
1931 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
1934 print qq!<div class="page_body">\n!;
1935 print qq!<div id="progress_info">... / ...</div>\n!
1936 if ($format eq 'incremental');
1937 print qq!<table id="blame_table" class="blame" width="100%">\n!.
1938 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
1939 qq!<thead>\n!.
1940 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
1941 qq!</thead>\n!.
1942 qq!<tbody>\n!;
1944 my @rev_color = qw(light dark);
1945 my $num_colors = scalar(@rev_color);
1946 my $current_color = 0;
1948 if ($format eq 'incremental') {
1949 my $color_class = $rev_color[$current_color];
1951 #contents of a file
1952 my $linenr = 0;
1953 LINE:
1954 while (my $line = <$fd>) {
1955 chomp $line;
1956 $linenr++;
1958 print qq!<tr id="l$linenr" class="$color_class">!.
1959 qq!<td class="sha1"><a href=""> </a></td>!.
1960 qq!<td class="linenr">!.
1961 qq!<a class="linenr" href="">$linenr</a></td>!;
1962 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
1963 print qq!</tr>\n!;
1966 } else { # porcelain, i.e. ordinary blame
1967 my %metainfo = (); # saves information about commits
1969 # blame data
1970 LINE:
1971 while (my $line = <$fd>) {
1972 chomp $line;
1973 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
1974 # no <lines in group> for subsequent lines in group of lines
1975 my ($full_rev, $orig_lineno, $lineno, $group_size) =
1976 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
1977 if (!exists $metainfo{$full_rev}) {
1978 $metainfo{$full_rev} = { 'nprevious' => 0 };
1980 my $meta = $metainfo{$full_rev};
1981 my $data;
1982 while ($data = <$fd>) {
1983 chomp $data;
1984 last if ($data =~ s/^\t//); # contents of line
1985 if ($data =~ /^(\S+)(?: (.*))?$/) {
1986 $meta->{$1} = $2 unless exists $meta->{$1};
1988 if ($data =~ /^previous /) {
1989 $meta->{'nprevious'}++;
1992 my $short_rev = substr($full_rev, 0, 8);
1993 my $author = $meta->{'author'};
1994 my %date =
1995 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
1996 my $date = $date{'iso-tz'};
1997 if ($group_size) {
1998 $current_color = ($current_color + 1) % $num_colors;
2000 my $tr_class = $rev_color[$current_color];
2001 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
2002 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
2003 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
2004 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
2005 if ($group_size) {
2006 print "<td class=\"sha1\"";
2007 print " title=\"". esc_html($author) . ", $date\"";
2008 print " rowspan=\"$group_size\"" if ($group_size > 1);
2009 print ">";
2010 print $cgi->a({-href => href(action=>"commit",
2011 hash=>$full_rev,
2012 file_name=>$file_name)},
2013 esc_html($short_rev));
2014 if ($group_size >= 2) {
2015 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
2016 if (@author_initials) {
2017 print "<br />" .
2018 esc_html(join('', @author_initials));
2019 # or join('.', ...)
2022 print "</td>\n";
2024 # 'previous' <sha1 of parent commit> <filename at commit>
2025 if (exists $meta->{'previous'} &&
2026 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
2027 $meta->{'parent'} = $1;
2028 $meta->{'file_parent'} = unquote($2);
2030 my $linenr_commit =
2031 exists($meta->{'parent'}) ?
2032 $meta->{'parent'} : $full_rev;
2033 my $linenr_filename =
2034 exists($meta->{'file_parent'}) ?
2035 $meta->{'file_parent'} : unquote($meta->{'filename'});
2036 my $blamed = href(action => 'blame',
2037 file_name => $linenr_filename,
2038 hash_base => $linenr_commit);
2039 print "<td class=\"linenr\">";
2040 print $cgi->a({ -href => "$blamed#l$orig_lineno",
2041 -class => "linenr" },
2042 esc_html($lineno));
2043 print "</td>";
2044 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
2045 print "</tr>\n";
2046 } # end while
2050 # footer
2051 print "</tbody>\n".
2052 "</table>\n"; # class="blame"
2053 print "</div>\n"; # class="blame_body"
2054 close $fd
2055 or print "Reading blob failed\n";
2057 git_footer_html();
2060 sub git_blame {
2061 git_blame_common();
2064 sub git_blame_incremental {
2065 git_blame_common('incremental');
2068 sub git_blame_data {
2069 git_blame_common('data');
2072 sub git_tags {
2073 my $head = git_get_head_hash($project);
2074 git_header_html();
2075 git_print_page_nav('','', $head,undef,$head);
2076 git_print_header_div('summary', $project);
2078 my @tagslist = git_get_tags_list();
2079 if (@tagslist) {
2080 git_tags_body(\@tagslist);
2082 git_footer_html();
2085 sub git_heads {
2086 my $head = git_get_head_hash($project);
2087 git_header_html();
2088 git_print_page_nav('','', $head,undef,$head);
2089 git_print_header_div('summary', $project);
2091 my @headslist = git_get_heads_list();
2092 if (@headslist) {
2093 git_heads_body(\@headslist, $head);
2095 git_footer_html();
2098 sub git_blob_plain {
2099 my $type = shift;
2100 my $expires;
2102 if (!defined $hash) {
2103 if (defined $file_name) {
2104 my $base = $hash_base || git_get_head_hash($project);
2105 $hash = git_get_hash_by_path($base, $file_name, "blob")
2106 or die_error(404, "Cannot find file");
2107 } else {
2108 die_error(400, "No file name defined");
2110 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2111 # blobs defined by non-textual hash id's can be cached
2112 $expires = "+1d";
2115 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2116 or die_error(500, "Open git-cat-file blob '$hash' failed");
2118 # content-type (can include charset)
2119 $type = blob_contenttype($fd, $file_name, $type);
2121 # "save as" filename, even when no $file_name is given
2122 my $save_as = "$hash";
2123 if (defined $file_name) {
2124 $save_as = $file_name;
2125 } elsif ($type =~ m/^text\//) {
2126 $save_as .= '.txt';
2129 # With XSS prevention on, blobs of all types except a few known safe
2130 # ones are served with "Content-Disposition: attachment" to make sure
2131 # they don't run in our security domain. For certain image types,
2132 # blob view writes an <img> tag referring to blob_plain view, and we
2133 # want to be sure not to break that by serving the image as an
2134 # attachment (though Firefox 3 doesn't seem to care).
2135 my $sandbox = $prevent_xss &&
2136 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
2138 print $cgi->header(
2139 -type => $type,
2140 -expires => $expires,
2141 -content_disposition =>
2142 ($sandbox ? 'attachment' : 'inline')
2143 . '; filename="' . $save_as . '"');
2144 local $/ = undef;
2145 binmode STDOUT, ':raw';
2146 print <$fd>;
2147 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
2148 close $fd;
2151 sub git_blob {
2152 my $expires;
2154 if (!defined $hash) {
2155 if (defined $file_name) {
2156 my $base = $hash_base || git_get_head_hash($project);
2157 $hash = git_get_hash_by_path($base, $file_name, "blob")
2158 or die_error(404, "Cannot find file");
2159 } else {
2160 die_error(400, "No file name defined");
2162 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2163 # blobs defined by non-textual hash id's can be cached
2164 $expires = "+1d";
2167 my $have_blame = gitweb_check_feature('blame');
2168 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2169 or die_error(500, "Couldn't cat $file_name, $hash");
2170 my $mimetype = blob_mimetype($fd, $file_name);
2171 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
2172 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
2173 close $fd;
2174 return git_blob_plain($mimetype);
2176 # we can have blame only for text/* mimetype
2177 $have_blame &&= ($mimetype =~ m!^text/!);
2179 my $highlight = gitweb_check_feature('highlight');
2180 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
2181 $fd = run_highlighter($fd, $highlight, $syntax)
2182 if $syntax;
2184 git_header_html(undef, $expires);
2185 my $formats_nav = '';
2186 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2187 if (defined $file_name) {
2188 if ($have_blame) {
2189 $formats_nav .=
2190 $cgi->a({-href => href(action=>"blame", -replay=>1)},
2191 "blame") .
2192 " | ";
2194 $formats_nav .=
2195 $cgi->a({-href => href(action=>"history", -replay=>1)},
2196 "history") .
2197 " | " .
2198 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
2199 "raw") .
2200 " | " .
2201 $cgi->a({-href => href(action=>"blob",
2202 hash_base=>"HEAD", file_name=>$file_name)},
2203 "HEAD");
2204 } else {
2205 $formats_nav .=
2206 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
2207 "raw");
2209 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2210 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2211 } else {
2212 print "<div class=\"page_nav\">\n" .
2213 "<br/><br/></div>\n" .
2214 "<div class=\"title\">$hash</div>\n";
2216 git_print_page_path($file_name, "blob", $hash_base);
2217 print "<div class=\"page_body\">\n";
2218 if ($mimetype =~ m!^image/!) {
2219 print qq!<img type="$mimetype"!;
2220 if ($file_name) {
2221 print qq! alt="$file_name" title="$file_name"!;
2223 print qq! src="! .
2224 href(action=>"blob_plain", hash=>$hash,
2225 hash_base=>$hash_base, file_name=>$file_name) .
2226 qq!" />\n!;
2227 } else {
2228 my $nr;
2229 while (my $line = <$fd>) {
2230 chomp $line;
2231 $nr++;
2232 $line = untabify($line);
2233 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
2234 $nr, href(-replay => 1), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
2237 close $fd
2238 or print "Reading blob failed.\n";
2239 print "</div>";
2240 git_footer_html();
2243 sub git_tree {
2244 if (!defined $hash_base) {
2245 $hash_base = "HEAD";
2247 if (!defined $hash) {
2248 if (defined $file_name) {
2249 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
2250 } else {
2251 $hash = $hash_base;
2254 die_error(404, "No such tree") unless defined($hash);
2256 my $show_sizes = gitweb_check_feature('show-sizes');
2257 my $have_blame = gitweb_check_feature('blame');
2259 my @entries = ();
2261 local $/ = "\0";
2262 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
2263 ($show_sizes ? '-l' : ()), @extra_options, $hash
2264 or die_error(500, "Open git-ls-tree failed");
2265 @entries = map { chomp; $_ } <$fd>;
2266 close $fd
2267 or die_error(404, "Reading tree failed");
2270 my $refs = git_get_references();
2271 my $ref = format_ref_marker($refs, $hash_base);
2272 git_header_html();
2273 my $basedir = '';
2274 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2275 my @views_nav = ();
2276 if (defined $file_name) {
2277 push @views_nav,
2278 $cgi->a({-href => href(action=>"history", -replay=>1)},
2279 "history"),
2280 $cgi->a({-href => href(action=>"tree",
2281 hash_base=>"HEAD", file_name=>$file_name)},
2282 "HEAD"),
2284 my $snapshot_links = format_snapshot_links($hash);
2285 if (defined $snapshot_links) {
2286 # FIXME: Should be available when we have no hash base as well.
2287 push @views_nav, $snapshot_links;
2289 git_print_page_nav('tree','', $hash_base, undef, undef,
2290 join(' | ', @views_nav));
2291 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
2292 } else {
2293 undef $hash_base;
2294 print "<div class=\"page_nav\">\n";
2295 print "<br/><br/></div>\n";
2296 print "<div class=\"title\">$hash</div>\n";
2298 if (defined $file_name) {
2299 $basedir = $file_name;
2300 if ($basedir ne '' && substr($basedir, -1) ne '/') {
2301 $basedir .= '/';
2303 git_print_page_path($file_name, 'tree', $hash_base);
2305 print "<div class=\"page_body\">\n";
2306 print "<table class=\"tree\">\n";
2307 my $alternate = 1;
2308 # '..' (top directory) link if possible
2309 if (defined $hash_base &&
2310 defined $file_name && $file_name =~ m![^/]+$!) {
2311 if ($alternate) {
2312 print "<tr class=\"dark\">\n";
2313 } else {
2314 print "<tr class=\"light\">\n";
2316 $alternate ^= 1;
2318 my $up = $file_name;
2319 $up =~ s!/?[^/]+$!!;
2320 undef $up unless $up;
2321 # based on git_print_tree_entry
2322 print '<td class="mode">' . mode_str('040000') . "</td>\n";
2323 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
2324 print '<td class="list">';
2325 print $cgi->a({-href => href(action=>"tree",
2326 hash_base=>$hash_base,
2327 file_name=>$up)},
2328 "..");
2329 print "</td>\n";
2330 print "<td class=\"link\"></td>\n";
2332 print "</tr>\n";
2334 foreach my $line (@entries) {
2335 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
2337 if ($alternate) {
2338 print "<tr class=\"dark\">\n";
2339 } else {
2340 print "<tr class=\"light\">\n";
2342 $alternate ^= 1;
2344 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
2346 print "</tr>\n";
2348 print "</table>\n" .
2349 "</div>";
2350 git_footer_html();
2353 sub snapshot_name {
2354 my ($project, $hash) = @_;
2356 # path/to/project.git -> project
2357 # path/to/project/.git -> project
2358 my $name = to_utf8($project);
2359 $name =~ s,([^/])/*\.git$,$1,;
2360 $name = basename($name);
2361 # sanitize name
2362 $name =~ s/[[:cntrl:]]/?/g;
2364 my $ver = $hash;
2365 if ($hash =~ /^[0-9a-fA-F]+$/) {
2366 # shorten SHA-1 hash
2367 my $full_hash = git_get_full_hash($project, $hash);
2368 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
2369 $ver = git_get_short_hash($project, $hash);
2371 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
2372 # tags don't need shortened SHA-1 hash
2373 $ver = $1;
2374 } else {
2375 # branches and other need shortened SHA-1 hash
2376 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
2377 $ver = $1;
2379 $ver .= '-' . git_get_short_hash($project, $hash);
2381 # in case of hierarchical branch names
2382 $ver =~ s!/!.!g;
2384 # name = project-version_string
2385 $name = "$name-$ver";
2387 return wantarray ? ($name, $name) : $name;
2390 sub git_snapshot {
2391 my $format = $input_params{'snapshot_format'};
2392 if (!@snapshot_fmts) {
2393 die_error(403, "Snapshots not allowed");
2395 # default to first supported snapshot format
2396 $format ||= $snapshot_fmts[0];
2397 if ($format !~ m/^[a-z0-9]+$/) {
2398 die_error(400, "Invalid snapshot format parameter");
2399 } elsif (!exists($known_snapshot_formats{$format})) {
2400 die_error(400, "Unknown snapshot format");
2401 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
2402 die_error(403, "Snapshot format not allowed");
2403 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
2404 die_error(403, "Unsupported snapshot format");
2407 my $type = git_get_type("$hash^{}");
2408 if (!$type) {
2409 die_error(404, 'Object does not exist');
2410 } elsif ($type eq 'blob') {
2411 die_error(400, 'Object is not a tree-ish');
2414 my ($name, $prefix) = snapshot_name($project, $hash);
2415 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
2416 my $cmd = quote_command(
2417 git_cmd(), 'archive',
2418 "--format=$known_snapshot_formats{$format}{'format'}",
2419 "--prefix=$prefix/", $hash);
2420 if (exists $known_snapshot_formats{$format}{'compressor'}) {
2421 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
2424 $filename =~ s/(["\\])/\\$1/g;
2425 print $cgi->header(
2426 -type => $known_snapshot_formats{$format}{'type'},
2427 -content_disposition => 'inline; filename="' . $filename . '"',
2428 -status => '200 OK');
2430 open my $fd, "-|", $cmd
2431 or die_error(500, "Execute git-archive failed");
2432 binmode STDOUT, ':raw';
2433 print <$fd>;
2434 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
2435 close $fd;
2438 sub git_log_generic {
2439 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
2441 my $head = git_get_head_hash($project);
2442 if (!defined $base) {
2443 $base = $head;
2445 if (!defined $page) {
2446 $page = 0;
2448 my $refs = git_get_references();
2450 my $commit_hash = $base;
2451 if (defined $parent) {
2452 $commit_hash = "$parent..$base";
2454 my @commitlist =
2455 parse_commits($commit_hash, 101, (100 * $page),
2456 defined $file_name ? ($file_name, "--full-history") : ());
2458 my $ftype;
2459 if (!defined $file_hash && defined $file_name) {
2460 # some commits could have deleted file in question,
2461 # and not have it in tree, but one of them has to have it
2462 for (my $i = 0; $i < @commitlist; $i++) {
2463 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
2464 last if defined $file_hash;
2467 if (defined $file_hash) {
2468 $ftype = git_get_type($file_hash);
2470 if (defined $file_name && !defined $ftype) {
2471 die_error(500, "Unknown type of object");
2473 my %co;
2474 if (defined $file_name) {
2475 %co = parse_commit($base)
2476 or die_error(404, "Unknown commit object");
2480 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
2481 my $next_link = '';
2482 if ($#commitlist >= 100) {
2483 $next_link =
2484 $cgi->a({-href => href(-replay=>1, page=>$page+1),
2485 -accesskey => "n", -title => "Alt-n"}, "next");
2487 my $patch_max = gitweb_get_feature('patches');
2488 if ($patch_max && !defined $file_name) {
2489 if ($patch_max < 0 || @commitlist <= $patch_max) {
2490 $paging_nav .= " &sdot; " .
2491 $cgi->a({-href => href(action=>"patches", -replay=>1)},
2492 "patches");
2496 git_header_html();
2497 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
2498 if (defined $file_name) {
2499 git_print_header_div('commit', esc_html($co{'title'}), $base);
2500 } else {
2501 git_print_header_div('summary', $project)
2503 git_print_page_path($file_name, $ftype, $hash_base)
2504 if (defined $file_name);
2506 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
2507 $file_name, $file_hash, $ftype);
2509 git_footer_html();
2512 sub git_log {
2513 git_log_generic('log', \&git_log_body,
2514 $hash, $hash_parent);
2517 sub git_commit {
2518 $hash ||= $hash_base || "HEAD";
2519 my %co = parse_commit($hash)
2520 or die_error(404, "Unknown commit object");
2522 my $parent = $co{'parent'};
2523 my $parents = $co{'parents'}; # listref
2525 # we need to prepare $formats_nav before any parameter munging
2526 my $formats_nav;
2527 if (!defined $parent) {
2528 # --root commitdiff
2529 $formats_nav .= '(initial)';
2530 } elsif (@$parents == 1) {
2531 # single parent commit
2532 $formats_nav .=
2533 '(parent: ' .
2534 $cgi->a({-href => href(action=>"commit",
2535 hash=>$parent)},
2536 esc_html(substr($parent, 0, 7))) .
2537 ')';
2538 } else {
2539 # merge commit
2540 $formats_nav .=
2541 '(merge: ' .
2542 join(' ', map {
2543 $cgi->a({-href => href(action=>"commit",
2544 hash=>$_)},
2545 esc_html(substr($_, 0, 7)));
2546 } @$parents ) .
2547 ')';
2549 if (gitweb_check_feature('patches') && @$parents <= 1) {
2550 $formats_nav .= " | " .
2551 $cgi->a({-href => href(action=>"patch", -replay=>1)},
2552 "patch");
2555 if (!defined $parent) {
2556 $parent = "--root";
2558 my @difftree;
2559 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
2560 @diff_opts,
2561 (@$parents <= 1 ? $parent : '-c'),
2562 $hash, "--"
2563 or die_error(500, "Open git-diff-tree failed");
2564 @difftree = map { chomp; $_ } <$fd>;
2565 close $fd or die_error(404, "Reading git-diff-tree failed");
2567 # non-textual hash id's can be cached
2568 my $expires;
2569 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2570 $expires = "+1d";
2572 my $refs = git_get_references();
2573 my $ref = format_ref_marker($refs, $co{'id'});
2575 git_header_html(undef, $expires);
2576 git_print_page_nav('commit', '',
2577 $hash, $co{'tree'}, $hash,
2578 $formats_nav);
2580 if (defined $co{'parent'}) {
2581 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
2582 } else {
2583 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
2585 print "<div class=\"title_text\">\n" .
2586 "<table class=\"object_header\">\n";
2587 git_print_authorship_rows(\%co);
2588 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
2589 print "<tr>" .
2590 "<td>tree</td>" .
2591 "<td class=\"sha1\">" .
2592 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
2593 class => "list"}, $co{'tree'}) .
2594 "</td>" .
2595 "<td class=\"link\">" .
2596 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
2597 "tree");
2598 my $snapshot_links = format_snapshot_links($hash);
2599 if (defined $snapshot_links) {
2600 print " | " . $snapshot_links;
2602 print "</td>" .
2603 "</tr>\n";
2605 foreach my $par (@$parents) {
2606 print "<tr>" .
2607 "<td>parent</td>" .
2608 "<td class=\"sha1\">" .
2609 $cgi->a({-href => href(action=>"commit", hash=>$par),
2610 class => "list"}, $par) .
2611 "</td>" .
2612 "<td class=\"link\">" .
2613 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
2614 " | " .
2615 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
2616 "</td>" .
2617 "</tr>\n";
2619 print "</table>".
2620 "</div>\n";
2622 print "<div class=\"page_body\">\n";
2623 git_print_log($co{'comment'});
2624 print "</div>\n";
2626 git_difftree_body(\@difftree, $hash, @$parents);
2628 git_footer_html();
2631 sub git_object {
2632 # object is defined by:
2633 # - hash or hash_base alone
2634 # - hash_base and file_name
2635 my $type;
2637 # - hash or hash_base alone
2638 if ($hash || ($hash_base && !defined $file_name)) {
2639 my $object_id = $hash || $hash_base;
2641 open my $fd, "-|", quote_command(
2642 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
2643 or die_error(404, "Object does not exist");
2644 $type = <$fd>;
2645 chomp $type;
2646 close $fd
2647 or die_error(404, "Object does not exist");
2649 # - hash_base and file_name
2650 } elsif ($hash_base && defined $file_name) {
2651 $file_name =~ s,/+$,,;
2653 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
2654 or die_error(404, "Base object does not exist");
2656 # here errors should not hapen
2657 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
2658 or die_error(500, "Open git-ls-tree failed");
2659 my $line = <$fd>;
2660 close $fd;
2662 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2663 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
2664 die_error(404, "File or directory for given base does not exist");
2666 $type = $2;
2667 $hash = $3;
2668 } else {
2669 die_error(400, "Not enough information to find object");
2672 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
2673 hash=>$hash, hash_base=>$hash_base,
2674 file_name=>$file_name),
2675 -status => '302 Found');
2678 sub git_blobdiff {
2679 my $format = shift || 'html';
2681 my $fd;
2682 my @difftree;
2683 my %diffinfo;
2684 my $expires;
2686 # preparing $fd and %diffinfo for git_patchset_body
2687 # new style URI
2688 if (defined $hash_base && defined $hash_parent_base) {
2689 if (defined $file_name) {
2690 # read raw output
2691 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2692 $hash_parent_base, $hash_base,
2693 "--", (defined $file_parent ? $file_parent : ()), $file_name
2694 or die_error(500, "Open git-diff-tree failed");
2695 @difftree = map { chomp; $_ } <$fd>;
2696 close $fd
2697 or die_error(404, "Reading git-diff-tree failed");
2698 @difftree
2699 or die_error(404, "Blob diff not found");
2701 } elsif (defined $hash &&
2702 $hash =~ /[0-9a-fA-F]{40}/) {
2703 # try to find filename from $hash
2705 # read filtered raw output
2706 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2707 $hash_parent_base, $hash_base, "--"
2708 or die_error(500, "Open git-diff-tree failed");
2709 @difftree =
2710 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
2711 # $hash == to_id
2712 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
2713 map { chomp; $_ } <$fd>;
2714 close $fd
2715 or die_error(404, "Reading git-diff-tree failed");
2716 @difftree
2717 or die_error(404, "Blob diff not found");
2719 } else {
2720 die_error(400, "Missing one of the blob diff parameters");
2723 if (@difftree > 1) {
2724 die_error(400, "Ambiguous blob diff specification");
2727 %diffinfo = parse_difftree_raw_line($difftree[0]);
2728 $file_parent ||= $diffinfo{'from_file'} || $file_name;
2729 $file_name ||= $diffinfo{'to_file'};
2731 $hash_parent ||= $diffinfo{'from_id'};
2732 $hash ||= $diffinfo{'to_id'};
2734 # non-textual hash id's can be cached
2735 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
2736 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
2737 $expires = '+1d';
2740 # open patch output
2741 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2742 '-p', ($format eq 'html' ? "--full-index" : ()),
2743 $hash_parent_base, $hash_base,
2744 "--", (defined $file_parent ? $file_parent : ()), $file_name
2745 or die_error(500, "Open git-diff-tree failed");
2748 # old/legacy style URI -- not generated anymore since 1.4.3.
2749 if (!%diffinfo) {
2750 die_error('404 Not Found', "Missing one of the blob diff parameters")
2753 # header
2754 if ($format eq 'html') {
2755 my $formats_nav =
2756 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
2757 "raw");
2758 git_header_html(undef, $expires);
2759 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2760 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2761 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2762 } else {
2763 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
2764 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
2766 if (defined $file_name) {
2767 git_print_page_path($file_name, "blob", $hash_base);
2768 } else {
2769 print "<div class=\"page_path\"></div>\n";
2772 } elsif ($format eq 'plain') {
2773 print $cgi->header(
2774 -type => 'text/plain',
2775 -charset => 'utf-8',
2776 -expires => $expires,
2777 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
2779 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
2781 } else {
2782 die_error(400, "Unknown blobdiff format");
2785 # patch
2786 if ($format eq 'html') {
2787 print "<div class=\"page_body\">\n";
2789 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
2790 close $fd;
2792 print "</div>\n"; # class="page_body"
2793 git_footer_html();
2795 } else {
2796 while (my $line = <$fd>) {
2797 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
2798 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
2800 print $line;
2802 last if $line =~ m!^\+\+\+!;
2804 local $/ = undef;
2805 print <$fd>;
2806 close $fd;
2810 sub git_blobdiff_plain {
2811 git_blobdiff('plain');
2814 sub git_commitdiff {
2815 my %params = @_;
2816 my $format = $params{-format} || 'html';
2818 my ($patch_max) = gitweb_get_feature('patches');
2819 if ($format eq 'patch') {
2820 die_error(403, "Patch view not allowed") unless $patch_max;
2823 $hash ||= $hash_base || "HEAD";
2824 my %co = parse_commit($hash)
2825 or die_error(404, "Unknown commit object");
2827 # choose format for commitdiff for merge
2828 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
2829 $hash_parent = '--cc';
2831 # we need to prepare $formats_nav before almost any parameter munging
2832 my $formats_nav;
2833 if ($format eq 'html') {
2834 $formats_nav =
2835 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
2836 "raw");
2837 if ($patch_max && @{$co{'parents'}} <= 1) {
2838 $formats_nav .= " | " .
2839 $cgi->a({-href => href(action=>"patch", -replay=>1)},
2840 "patch");
2843 if (defined $hash_parent &&
2844 $hash_parent ne '-c' && $hash_parent ne '--cc') {
2845 # commitdiff with two commits given
2846 my $hash_parent_short = $hash_parent;
2847 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
2848 $hash_parent_short = substr($hash_parent, 0, 7);
2850 $formats_nav .=
2851 ' (from';
2852 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
2853 if ($co{'parents'}[$i] eq $hash_parent) {
2854 $formats_nav .= ' parent ' . ($i+1);
2855 last;
2858 $formats_nav .= ': ' .
2859 $cgi->a({-href => href(action=>"commitdiff",
2860 hash=>$hash_parent)},
2861 esc_html($hash_parent_short)) .
2862 ')';
2863 } elsif (!$co{'parent'}) {
2864 # --root commitdiff
2865 $formats_nav .= ' (initial)';
2866 } elsif (scalar @{$co{'parents'}} == 1) {
2867 # single parent commit
2868 $formats_nav .=
2869 ' (parent: ' .
2870 $cgi->a({-href => href(action=>"commitdiff",
2871 hash=>$co{'parent'})},
2872 esc_html(substr($co{'parent'}, 0, 7))) .
2873 ')';
2874 } else {
2875 # merge commit
2876 if ($hash_parent eq '--cc') {
2877 $formats_nav .= ' | ' .
2878 $cgi->a({-href => href(action=>"commitdiff",
2879 hash=>$hash, hash_parent=>'-c')},
2880 'combined');
2881 } else { # $hash_parent eq '-c'
2882 $formats_nav .= ' | ' .
2883 $cgi->a({-href => href(action=>"commitdiff",
2884 hash=>$hash, hash_parent=>'--cc')},
2885 'compact');
2887 $formats_nav .=
2888 ' (merge: ' .
2889 join(' ', map {
2890 $cgi->a({-href => href(action=>"commitdiff",
2891 hash=>$_)},
2892 esc_html(substr($_, 0, 7)));
2893 } @{$co{'parents'}} ) .
2894 ')';
2898 my $hash_parent_param = $hash_parent;
2899 if (!defined $hash_parent_param) {
2900 # --cc for multiple parents, --root for parentless
2901 $hash_parent_param =
2902 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
2905 # read commitdiff
2906 my $fd;
2907 my @difftree;
2908 if ($format eq 'html') {
2909 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2910 "--no-commit-id", "--patch-with-raw", "--full-index",
2911 $hash_parent_param, $hash, "--"
2912 or die_error(500, "Open git-diff-tree failed");
2914 while (my $line = <$fd>) {
2915 chomp $line;
2916 # empty line ends raw part of diff-tree output
2917 last unless $line;
2918 push @difftree, scalar parse_difftree_raw_line($line);
2921 } elsif ($format eq 'plain') {
2922 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
2923 '-p', $hash_parent_param, $hash, "--"
2924 or die_error(500, "Open git-diff-tree failed");
2925 } elsif ($format eq 'patch') {
2926 # For commit ranges, we limit the output to the number of
2927 # patches specified in the 'patches' feature.
2928 # For single commits, we limit the output to a single patch,
2929 # diverging from the git-format-patch default.
2930 my @commit_spec = ();
2931 if ($hash_parent) {
2932 if ($patch_max > 0) {
2933 push @commit_spec, "-$patch_max";
2935 push @commit_spec, '-n', "$hash_parent..$hash";
2936 } else {
2937 if ($params{-single}) {
2938 push @commit_spec, '-1';
2939 } else {
2940 if ($patch_max > 0) {
2941 push @commit_spec, "-$patch_max";
2943 push @commit_spec, "-n";
2945 push @commit_spec, '--root', $hash;
2947 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
2948 '--encoding=utf8', '--stdout', @commit_spec
2949 or die_error(500, "Open git-format-patch failed");
2950 } else {
2951 die_error(400, "Unknown commitdiff format");
2954 # non-textual hash id's can be cached
2955 my $expires;
2956 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2957 $expires = "+1d";
2960 # write commit message
2961 if ($format eq 'html') {
2962 my $refs = git_get_references();
2963 my $ref = format_ref_marker($refs, $co{'id'});
2965 git_header_html(undef, $expires);
2966 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
2967 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
2968 print "<div class=\"title_text\">\n" .
2969 "<table class=\"object_header\">\n";
2970 git_print_authorship_rows(\%co);
2971 print "</table>".
2972 "</div>\n";
2973 print "<div class=\"page_body\">\n";
2974 if (@{$co{'comment'}} > 1) {
2975 print "<div class=\"log\">\n";
2976 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
2977 print "</div>\n"; # class="log"
2980 } elsif ($format eq 'plain') {
2981 my $refs = git_get_references("tags");
2982 my $tagname = git_get_rev_name_tags($hash);
2983 my $filename = basename($project) . "-$hash.patch";
2985 print $cgi->header(
2986 -type => 'text/plain',
2987 -charset => 'utf-8',
2988 -expires => $expires,
2989 -content_disposition => 'inline; filename="' . "$filename" . '"');
2990 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
2991 print "From: " . to_utf8($co{'author'}) . "\n";
2992 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
2993 print "Subject: " . to_utf8($co{'title'}) . "\n";
2995 print "X-Git-Tag: $tagname\n" if $tagname;
2996 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
2998 foreach my $line (@{$co{'comment'}}) {
2999 print to_utf8($line) . "\n";
3001 print "---\n\n";
3002 } elsif ($format eq 'patch') {
3003 my $filename = basename($project) . "-$hash.patch";
3005 print $cgi->header(
3006 -type => 'text/plain',
3007 -charset => 'utf-8',
3008 -expires => $expires,
3009 -content_disposition => 'inline; filename="' . "$filename" . '"');
3012 # write patch
3013 if ($format eq 'html') {
3014 my $use_parents = !defined $hash_parent ||
3015 $hash_parent eq '-c' || $hash_parent eq '--cc';
3016 git_difftree_body(\@difftree, $hash,
3017 $use_parents ? @{$co{'parents'}} : $hash_parent);
3018 print "<br/>\n";
3020 git_patchset_body($fd, \@difftree, $hash,
3021 $use_parents ? @{$co{'parents'}} : $hash_parent);
3022 close $fd;
3023 print "</div>\n"; # class="page_body"
3024 git_footer_html();
3026 } elsif ($format eq 'plain') {
3027 local $/ = undef;
3028 print <$fd>;
3029 close $fd
3030 or print "Reading git-diff-tree failed\n";
3031 } elsif ($format eq 'patch') {
3032 local $/ = undef;
3033 print <$fd>;
3034 close $fd
3035 or print "Reading git-format-patch failed\n";
3039 sub git_commitdiff_plain {
3040 git_commitdiff(-format => 'plain');
3043 # format-patch-style patches
3044 sub git_patch {
3045 git_commitdiff(-format => 'patch', -single => 1);
3048 sub git_patches {
3049 git_commitdiff(-format => 'patch');
3052 sub git_history {
3053 git_log_generic('history', \&git_history_body,
3054 $hash_base, $hash_parent_base,
3055 $file_name, $hash);
3058 sub git_search {
3059 gitweb_check_feature('search') or die_error(403, "Search is disabled");
3060 if (!defined $searchtext) {
3061 die_error(400, "Text field is empty");
3063 if (!defined $hash) {
3064 $hash = git_get_head_hash($project);
3066 my %co = parse_commit($hash);
3067 if (!%co) {
3068 die_error(404, "Unknown commit object");
3070 if (!defined $page) {
3071 $page = 0;
3074 $searchtype ||= 'commit';
3075 if ($searchtype eq 'pickaxe') {
3076 # pickaxe may take all resources of your box and run for several minutes
3077 # with every query - so decide by yourself how public you make this feature
3078 gitweb_check_feature('pickaxe')
3079 or die_error(403, "Pickaxe is disabled");
3081 if ($searchtype eq 'grep') {
3082 gitweb_check_feature('grep')
3083 or die_error(403, "Grep is disabled");
3086 git_header_html();
3088 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
3089 my $greptype;
3090 if ($searchtype eq 'commit') {
3091 $greptype = "--grep=";
3092 } elsif ($searchtype eq 'author') {
3093 $greptype = "--author=";
3094 } elsif ($searchtype eq 'committer') {
3095 $greptype = "--committer=";
3097 $greptype .= $searchtext;
3098 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
3099 $greptype, '--regexp-ignore-case',
3100 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
3102 my $paging_nav = '';
3103 if ($page > 0) {
3104 $paging_nav .=
3105 $cgi->a({-href => href(action=>"search", hash=>$hash,
3106 searchtext=>$searchtext,
3107 searchtype=>$searchtype)},
3108 "first");
3109 $paging_nav .= " &sdot; " .
3110 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3111 -accesskey => "p", -title => "Alt-p"}, "prev");
3112 } else {
3113 $paging_nav .= "first";
3114 $paging_nav .= " &sdot; prev";
3116 my $next_link = '';
3117 if ($#commitlist >= 100) {
3118 $next_link =
3119 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3120 -accesskey => "n", -title => "Alt-n"}, "next");
3121 $paging_nav .= " &sdot; $next_link";
3122 } else {
3123 $paging_nav .= " &sdot; next";
3126 if ($#commitlist >= 100) {
3129 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
3130 git_print_header_div('commit', esc_html($co{'title'}), $hash);
3131 git_search_grep_body(\@commitlist, 0, 99, $next_link);
3134 if ($searchtype eq 'pickaxe') {
3135 git_print_page_nav('','', $hash,$co{'tree'},$hash);
3136 git_print_header_div('commit', esc_html($co{'title'}), $hash);
3138 print "<table class=\"pickaxe search\">\n";
3139 my $alternate = 1;
3140 local $/ = "\n";
3141 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
3142 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
3143 ($search_use_regexp ? '--pickaxe-regex' : ());
3144 undef %co;
3145 my @files;
3146 while (my $line = <$fd>) {
3147 chomp $line;
3148 next unless $line;
3150 my %set = parse_difftree_raw_line($line);
3151 if (defined $set{'commit'}) {
3152 # finish previous commit
3153 if (%co) {
3154 print "</td>\n" .
3155 "<td class=\"link\">" .
3156 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3157 " | " .
3158 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3159 print "</td>\n" .
3160 "</tr>\n";
3163 if ($alternate) {
3164 print "<tr class=\"dark\">\n";
3165 } else {
3166 print "<tr class=\"light\">\n";
3168 $alternate ^= 1;
3169 %co = parse_commit($set{'commit'});
3170 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
3171 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3172 "<td><i>$author</i></td>\n" .
3173 "<td>" .
3174 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
3175 -class => "list subject"},
3176 chop_and_escape_str($co{'title'}, 50) . "<br/>");
3177 } elsif (defined $set{'to_id'}) {
3178 next if ($set{'to_id'} =~ m/^0{40}$/);
3180 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
3181 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
3182 -class => "list"},
3183 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
3184 "<br/>\n";
3187 close $fd;
3189 # finish last commit (warning: repetition!)
3190 if (%co) {
3191 print "</td>\n" .
3192 "<td class=\"link\">" .
3193 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3194 " | " .
3195 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
3196 print "</td>\n" .
3197 "</tr>\n";
3200 print "</table>\n";
3203 if ($searchtype eq 'grep') {
3204 git_print_page_nav('','', $hash,$co{'tree'},$hash);
3205 git_print_header_div('commit', esc_html($co{'title'}), $hash);
3207 print "<table class=\"grep_search\">\n";
3208 my $alternate = 1;
3209 my $matches = 0;
3210 local $/ = "\n";
3211 open my $fd, "-|", git_cmd(), 'grep', '-n',
3212 $search_use_regexp ? ('-E', '-i') : '-F',
3213 $searchtext, $co{'tree'};
3214 my $lastfile = '';
3215 while (my $line = <$fd>) {
3216 chomp $line;
3217 my ($file, $lno, $ltext, $binary);
3218 last if ($matches++ > 1000);
3219 if ($line =~ /^Binary file (.+) matches$/) {
3220 $file = $1;
3221 $binary = 1;
3222 } else {
3223 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
3225 if ($file ne $lastfile) {
3226 $lastfile and print "</td></tr>\n";
3227 if ($alternate++) {
3228 print "<tr class=\"dark\">\n";
3229 } else {
3230 print "<tr class=\"light\">\n";
3232 print "<td class=\"list\">".
3233 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
3234 file_name=>"$file"),
3235 -class => "list"}, esc_path($file));
3236 print "</td><td>\n";
3237 $lastfile = $file;
3239 if ($binary) {
3240 print "<div class=\"binary\">Binary file</div>\n";
3241 } else {
3242 $ltext = untabify($ltext);
3243 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
3244 $ltext = esc_html($1, -nbsp=>1);
3245 $ltext .= '<span class="match">';
3246 $ltext .= esc_html($2, -nbsp=>1);
3247 $ltext .= '</span>';
3248 $ltext .= esc_html($3, -nbsp=>1);
3249 } else {
3250 $ltext = esc_html($ltext, -nbsp=>1);
3252 print "<div class=\"pre\">" .
3253 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
3254 file_name=>"$file").'#l'.$lno,
3255 -class => "linenr"}, sprintf('%4i', $lno))
3256 . ' ' . $ltext . "</div>\n";
3259 if ($lastfile) {
3260 print "</td></tr>\n";
3261 if ($matches > 1000) {
3262 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
3264 } else {
3265 print "<div class=\"diff nodifferences\">No matches found</div>\n";
3267 close $fd;
3269 print "</table>\n";
3271 git_footer_html();
3274 sub git_search_help {
3275 git_header_html();
3276 git_print_page_nav('','', $hash,$hash,$hash);
3277 print <<EOT;
3278 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
3279 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
3280 the pattern entered is recognized as the POSIX extended
3281 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
3282 insensitive).</p>
3283 <dl>
3284 <dt><b>commit</b></dt>
3285 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
3287 my $have_grep = gitweb_check_feature('grep');
3288 if ($have_grep) {
3289 print <<EOT;
3290 <dt><b>grep</b></dt>
3291 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
3292 a different one) are searched for the given pattern. On large trees, this search can take
3293 a while and put some strain on the server, so please use it with some consideration. Note that
3294 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
3295 case-sensitive.</dd>
3298 print <<EOT;
3299 <dt><b>author</b></dt>
3300 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
3301 <dt><b>committer</b></dt>
3302 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
3304 my $have_pickaxe = gitweb_check_feature('pickaxe');
3305 if ($have_pickaxe) {
3306 print <<EOT;
3307 <dt><b>pickaxe</b></dt>
3308 <dd>All commits that caused the string to appear or disappear from any file (changes that
3309 added, removed or "modified" the string) will be listed. This search can take a while and
3310 takes a lot of strain on the server, so please use it wisely. Note that since you may be
3311 interested even in changes just changing the case as well, this search is case sensitive.</dd>
3314 print "</dl>\n";
3315 git_footer_html();
3318 sub git_shortlog {
3319 git_log_generic('shortlog', \&git_shortlog_body,
3320 $hash, $hash_parent);
3323 ## ======================================================================
3324 ## ======================================================================
3325 ## edits
3327 sub git_addrepo {
3328 if (-f $projects_list) {
3329 git_header_html();
3330 git_print_page_nav('addrepo');
3331 if(param('sf')) {
3332 open my $fd, '<', $projects_list or return;
3333 $fd .= "\n".escape(param('path'))." ";
3335 git_print_header_div('summary',$project);
3336 print "<div class=\"page_body\">";
3337 print "<form action=\"$my_url\" method=\"post\"><br/>";
3338 print "<table><tr class=\"dark\"><td>";
3339 print "Repository path for \$project_list: </td><td><input style=\"width:400px;\" type=\"text\" name=\"path\"/>";
3340 print "</td></tr><tr class=\"light\"><td align=\"center\" colspan=\"2\">";
3341 print "<input type=\"submit\" value=\"Add repository\" name=\"sf\"/>";
3342 print "</td></tr></table></form></div>";
3343 git_footer_html();
3344 } else {
3345 die_error(404, "Needed a static file with list of repositories (\$project_list)");
3349 sub git_newrepo {
3350 git_header_html();
3351 git_print_page_nav('newrepo');
3352 git_print_header_div('summary',$project);
3354 print "<br/><div class=\"title\">Create new repository</div>";
3355 print "<form action=\"$my_url\" method=\"post\"><br/>";
3356 print "<table><tr class=\"dark\"><td>";
3357 print "Absolute Repository path: </td><td><input style=\"width:400px;\" type=\"text\" name=\"path\"/>";
3358 print "</td></tr><tr class=\"light\"><td align=\"center\" colspan=\"2\">";
3359 print "<input type=\"submit\" value=\"Create repository\" name=\"sf\"/>";
3360 print "</td></tr></table></form>";
3361 print "<div class=\"title\">Clone a repository</div>";
3362 print "<form action=\"$my_url\" method=\"post\"><br/>";
3363 print "<table><tr class=\"dark\"><td>";
3364 print "Absolute Repository path: </td><td><input style=\"width:400px;\" type=\"text\" name=\"path\"/>";
3365 print "</td></tr><tr class=\"light\"><td align=\"center\" colspan=\"2\">";
3366 print "<input type=\"submit\" value=\"Clone repository\" name=\"sf\"/>";
3367 print "</td></tr></table></form>";
3369 git_footer_html();
3372 sub git_add {
3373 open my $fd, "-|", git_cmd(), "add", $file_name
3374 or die_error(500,"Open git-add failed");
3377 sub git_rm {
3378 open my $fd, "-|", git_cmd(), "rm", $file_name
3379 or die_error(500,"Open git-rm failed");
3382 sub git_reset {
3383 open my $fd, "-|", git_cmd(), "reset", "HEAD", $file_name
3384 or die_error(500,"Open git-reset failed");
3387 sub git_discard {
3388 git_reset();
3389 open my $fd, "-|", git_cmd(), "checkout", "--", $file_name
3390 or die_error(500,"Open git-checkout failed");
3393 sub git_mv {
3394 git_header_html();
3395 git_print_page_nav();
3396 git_print_header_div('status',$project);
3397 if (param('sf')) {
3398 open my $fh, "-|", git_cmd(), "mv", param('src'), param('dest')
3399 or die_error(500, "Open git-mv failed");
3401 print "<div class=\"page_body\">";
3402 print "<form action=\"$my_url\" method=\"post\"><br/>";
3403 print "<table><tr class=\"dark\"><td>";
3404 print "Source file name: </td><td><input style=\"width:400px;\" type=\"text\" name=\"src\"/>";
3405 print "</td></tr><tr class=\"light\"><td>";
3406 print "Destination file path: </td><td><input style=\"width:400px;\" type=\"text\" name=\"dest\"/>";
3407 print "</td></tr><tr class=\"dark\"><td align=\"center\" colspan=\"2\">";
3408 print "<input type=\"submit\" value=\"Move file\" name=\"sf\"/>";
3409 print "</td></tr></table></form></div>";
3410 git_footer_html();
3413 sub git_ignore {
3414 # Ex: "/home/pavan/Coding" / "gsoc/code/.git" "ignore"
3415 my $gitignore = "$projectroot/$project"."ignore";
3416 my $fd = '';
3417 if (-f $gitignore) {
3418 open $fd, '<', $gitignore or return;
3420 $fd .= "\n$file_name";
3421 open $fd, '>', $gitignore or return;
3424 sub git_commit_code {
3425 open my $fd, "-|", git_cmd(), "commit", "-s", "-m",
3426 "\"".param('subject').' '.param('body')."\""
3427 or die_error(500,"Open git-commit failed");
3428 git_header_html();
3429 git_print_page_nav();
3430 git_print_header_div('status',$project);
3431 while (my $line = <$fd>) {
3432 chomp $line;
3433 print $line."<br/>";
3435 git_footer_html();
3438 sub git_tag_create {
3439 open my $fd, "-|", git_cmd(), "tag", "-s", "-m",
3440 "\"".param('msg')."\""
3441 or die_error(500,"Open git-tag failed");
3444 sub git_branch_create {
3445 open my $fd, "-|", git_cmd(), "branch", param('name')
3446 or die_error(500,"Open git-branch failed");
3449 sub git_branch_delete_without_merge {
3450 open my $fd, "-|", git_cmd(), "branch", "-D", param('name')
3451 or die_error(500,"Open git-branch failed");
3454 sub git_branch_delete_with_merge {
3455 open my $fd, "-|", git_cmd(), "branch", "-d", param('name')
3456 or die_error(500,"Open git-branch failed");
3459 sub git_branch_move {
3460 open my $fd, "-|", git_cmd(), "branch", "-m",
3461 param('oldname'), param('newname')
3462 or die_error(500,"Open git-branch failed");
3465 sub git_merge {
3466 open my $fd, "-|", git_cmd(), "merge"
3467 param('first_branch'), param('second_branch')
3468 or die_error(500,"Open git-merge failed");
3471 # git_header_html();
3472 # git_print_page_nav();
3473 # git_print_header_div('status',$project);
3474 # git_footer_html();
3476 ## ......................................................................
3477 ## feeds (RSS, Atom; OPML)
3479 sub git_feed {
3480 my $format = shift || 'atom';
3481 my $have_blame = gitweb_check_feature('blame');
3483 # Atom: http://www.atomenabled.org/developers/syndication/
3484 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3485 if ($format ne 'rss' && $format ne 'atom') {
3486 die_error(400, "Unknown web feed format");
3489 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
3490 my $head = $hash || 'HEAD';
3491 my @commitlist = parse_commits($head, 150, 0, $file_name);
3493 my %latest_commit;
3494 my %latest_date;
3495 my $content_type = "application/$format+xml";
3496 if (defined $cgi->http('HTTP_ACCEPT') &&
3497 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
3498 # browser (feed reader) prefers text/xml
3499 $content_type = 'text/xml';
3501 if (defined($commitlist[0])) {
3502 %latest_commit = %{$commitlist[0]};
3503 my $latest_epoch = $latest_commit{'committer_epoch'};
3504 %latest_date = parse_date($latest_epoch);
3505 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
3506 if (defined $if_modified) {
3507 my $since;
3508 if (eval { require HTTP::Date; 1; }) {
3509 $since = HTTP::Date::str2time($if_modified);
3510 } elsif (eval { require Time::ParseDate; 1; }) {
3511 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
3513 if (defined $since && $latest_epoch <= $since) {
3514 print $cgi->header(
3515 -type => $content_type,
3516 -charset => 'utf-8',
3517 -last_modified => $latest_date{'rfc2822'},
3518 -status => '304 Not Modified');
3519 return;
3522 print $cgi->header(
3523 -type => $content_type,
3524 -charset => 'utf-8',
3525 -last_modified => $latest_date{'rfc2822'});
3526 } else {
3527 print $cgi->header(
3528 -type => $content_type,
3529 -charset => 'utf-8');
3532 # Optimization: skip generating the body if client asks only
3533 # for Last-Modified date.
3534 return if ($cgi->request_method() eq 'HEAD');
3536 # header variables
3537 my $title = "$site_name - $project/$action";
3538 my $feed_type = 'log';
3539 if (defined $hash) {
3540 $title .= " - '$hash'";
3541 $feed_type = 'branch log';
3542 if (defined $file_name) {
3543 $title .= " :: $file_name";
3544 $feed_type = 'history';
3546 } elsif (defined $file_name) {
3547 $title .= " - $file_name";
3548 $feed_type = 'history';
3550 $title .= " $feed_type";
3551 my $descr = git_get_project_description($project);
3552 if (defined $descr) {
3553 $descr = esc_html($descr);
3554 } else {
3555 $descr = "$project " .
3556 ($format eq 'rss' ? 'RSS' : 'Atom') .
3557 " feed";
3559 my $owner = git_get_project_owner($project);
3560 $owner = esc_html($owner);
3562 #header
3563 my $alt_url;
3564 if (defined $file_name) {
3565 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
3566 } elsif (defined $hash) {
3567 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
3568 } else {
3569 $alt_url = href(-full=>1, action=>"summary");
3571 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
3572 if ($format eq 'rss') {
3573 print <<XML;
3574 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
3575 <channel>
3577 print "<title>$title</title>\n" .
3578 "<link>$alt_url</link>\n" .
3579 "<description>$descr</description>\n" .
3580 "<language>en</language>\n" .
3581 # project owner is responsible for 'editorial' content
3582 "<managingEditor>$owner</managingEditor>\n";
3583 if (defined $logo || defined $favicon) {
3584 # prefer the logo to the favicon, since RSS
3585 # doesn't allow both
3586 my $img = esc_url($logo || $favicon);
3587 print "<image>\n" .
3588 "<url>$img</url>\n" .
3589 "<title>$title</title>\n" .
3590 "<link>$alt_url</link>\n" .
3591 "</image>\n";
3593 if (%latest_date) {
3594 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
3595 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
3597 print "<generator>gitweb v.$version/$git_version</generator>\n";
3598 } elsif ($format eq 'atom') {
3599 print <<XML;
3600 <feed xmlns="http://www.w3.org/2005/Atom">
3602 print "<title>$title</title>\n" .
3603 "<subtitle>$descr</subtitle>\n" .
3604 '<link rel="alternate" type="text/html" href="' .
3605 $alt_url . '" />' . "\n" .
3606 '<link rel="self" type="' . $content_type . '" href="' .
3607 $cgi->self_url() . '" />' . "\n" .
3608 "<id>" . href(-full=>1) . "</id>\n" .
3609 # use project owner for feed author
3610 "<author><name>$owner</name></author>\n";
3611 if (defined $favicon) {
3612 print "<icon>" . esc_url($favicon) . "</icon>\n";
3614 if (defined $logo_url) {
3615 # not twice as wide as tall: 72 x 27 pixels
3616 print "<logo>" . esc_url($logo) . "</logo>\n";
3618 if (! %latest_date) {
3619 # dummy date to keep the feed valid until commits trickle in:
3620 print "<updated>1970-01-01T00:00:00Z</updated>\n";
3621 } else {
3622 print "<updated>$latest_date{'iso-8601'}</updated>\n";
3624 print "<generator version='$version/$git_version'>gitweb</generator>\n";
3627 # contents
3628 for (my $i = 0; $i <= $#commitlist; $i++) {
3629 my %co = %{$commitlist[$i]};
3630 my $commit = $co{'id'};
3631 # we read 150, we always show 30 and the ones more recent than 48 hours
3632 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
3633 last;
3635 my %cd = parse_date($co{'author_epoch'});
3637 # get list of changed files
3638 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3639 $co{'parent'} || "--root",
3640 $co{'id'}, "--", (defined $file_name ? $file_name : ())
3641 or next;
3642 my @difftree = map { chomp; $_ } <$fd>;
3643 close $fd
3644 or next;
3646 # print element (entry, item)
3647 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
3648 if ($format eq 'rss') {
3649 print "<item>\n" .
3650 "<title>" . esc_html($co{'title'}) . "</title>\n" .
3651 "<author>" . esc_html($co{'author'}) . "</author>\n" .
3652 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
3653 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
3654 "<link>$co_url</link>\n" .
3655 "<description>" . esc_html($co{'title'}) . "</description>\n" .
3656 "<content:encoded>" .
3657 "<![CDATA[\n";
3658 } elsif ($format eq 'atom') {
3659 print "<entry>\n" .
3660 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
3661 "<updated>$cd{'iso-8601'}</updated>\n" .
3662 "<author>\n" .
3663 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
3664 if ($co{'author_email'}) {
3665 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
3667 print "</author>\n" .
3668 # use committer for contributor
3669 "<contributor>\n" .
3670 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
3671 if ($co{'committer_email'}) {
3672 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
3674 print "</contributor>\n" .
3675 "<published>$cd{'iso-8601'}</published>\n" .
3676 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
3677 "<id>$co_url</id>\n" .
3678 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
3679 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
3681 my $comment = $co{'comment'};
3682 print "<pre>\n";
3683 foreach my $line (@$comment) {
3684 $line = esc_html($line);
3685 print "$line\n";
3687 print "</pre><ul>\n";
3688 foreach my $difftree_line (@difftree) {
3689 my %difftree = parse_difftree_raw_line($difftree_line);
3690 next if !$difftree{'from_id'};
3692 my $file = $difftree{'file'} || $difftree{'to_file'};
3694 print "<li>" .
3695 "[" .
3696 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
3697 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
3698 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
3699 file_name=>$file, file_parent=>$difftree{'from_file'}),
3700 -title => "diff"}, 'D');
3701 if ($have_blame) {
3702 print $cgi->a({-href => href(-full=>1, action=>"blame",
3703 file_name=>$file, hash_base=>$commit),
3704 -title => "blame"}, 'B');
3706 # if this is not a feed of a file history
3707 if (!defined $file_name || $file_name ne $file) {
3708 print $cgi->a({-href => href(-full=>1, action=>"history",
3709 file_name=>$file, hash=>$commit),
3710 -title => "history"}, 'H');
3712 $file = esc_path($file);
3713 print "] ".
3714 "$file</li>\n";
3716 if ($format eq 'rss') {
3717 print "</ul>]]>\n" .
3718 "</content:encoded>\n" .
3719 "</item>\n";
3720 } elsif ($format eq 'atom') {
3721 print "</ul>\n</div>\n" .
3722 "</content>\n" .
3723 "</entry>\n";
3727 # end of feed
3728 if ($format eq 'rss') {
3729 print "</channel>\n</rss>\n";
3730 } elsif ($format eq 'atom') {
3731 print "</feed>\n";
3735 sub git_rss {
3736 git_feed('rss');
3739 sub git_atom {
3740 git_feed('atom');
3743 sub git_opml {
3744 my @list = git_get_projects_list();
3746 print $cgi->header(
3747 -type => 'text/xml',
3748 -charset => 'utf-8',
3749 -content_disposition => 'inline; filename="opml.xml"');
3751 print <<XML;
3752 <?xml version="1.0" encoding="utf-8"?>
3753 <opml version="1.0">
3754 <head>
3755 <title>$site_name OPML Export</title>
3756 </head>
3757 <body>
3758 <outline text="git RSS feeds">
3761 foreach my $pr (@list) {
3762 my %proj = %$pr;
3763 my $head = git_get_head_hash($proj{'path'});
3764 if (!defined $head) {
3765 next;
3767 $git_dir = "$projectroot/$proj{'path'}";
3768 my %co = parse_commit($head);
3769 if (!%co) {
3770 next;
3773 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
3774 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
3775 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
3776 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
3778 print <<XML;
3779 </outline>
3780 </body>
3781 </opml>