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
14 # __DIR__ is taken from Dir::Self __DIR__ fragment
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);
24 use File
::Basename
qw(basename);
26 binmode STDOUT
, ':utf8';
32 use Gitweb
::RepoConfig
;
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++");
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.
80 if( -e
'/proc/loadavg' ){
81 open my $fd, '<', '/proc/loadavg'
83 my @load = split(/\s+/, scalar <$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.
92 # additional checks for load average should go here for things that don't export
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
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
,
123 "patch" => \
&git_patch
,
124 "patches" => \
&git_patches
,
126 "atom" => \
&git_atom
,
127 "search" => \
&git_search
,
128 "search_help" => \
&git_search_help
,
129 "shortlog" => \
&git_shortlog
,
130 "summary" => \
&git_summary
,
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
144 #already existing subroutines
146 "summary" => \
&git_summary
,
148 "addrepo" => \
&git_addrepo
,
151 # now read PATH_INFO and update the parameter list for missing parameters
152 sub evaluate_path_info
{
153 return if defined $input_params{'project'};
154 return if !$path_info;
155 $path_info =~ s
,^/+,,;
156 return if !$path_info;
158 # find which part of PATH_INFO is project
159 my $project = $path_info;
161 while ($project && !check_head_link
("$projectroot/$project")) {
162 $project =~ s
,/*[^/]*$,,;
164 return unless $project;
165 $input_params{'project'} = $project;
167 # do not change any parameters if an action is given using the query string
168 return if $input_params{'action'};
169 $path_info =~ s
,^\Q
$project\E
/*,,;
171 # next, check if we have an action
172 my $action = $path_info;
174 if (exists $actions{$action}) {
175 $path_info =~ s
,^$action/*,,;
176 $input_params{'action'} = $action;
179 return if $input_params{'edit'};
180 # next, check if we have an edit
181 my $edit = $path_info;
183 if (exists $edits{$edit} && gitweb_check_feature
('write')) {
184 $path_info =~ s
,^$edit/*,,;
185 $input_params{'edit'} = $edit;
188 # list of actions that want hash_base instead of hash, but can have no
189 # pathname (f) parameter
196 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
197 my ($parentrefname, $parentpathname, $refname, $pathname) =
198 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
200 # first, analyze the 'current' part
201 if (defined $pathname) {
202 # we got "branch:filename" or "branch:dir/"
203 # we could use git_get_type(branch:pathname), but:
204 # - it needs $git_dir
205 # - it does a git() call
206 # - the convention of terminating directories with a slash
207 # makes it superfluous
208 # - embedding the action in the PATH_INFO would make it even
210 $pathname =~ s
,^/+,,;
211 if (!$pathname || substr($pathname, -1) eq "/") {
212 $input_params{'action'} ||= "tree";
215 # the default action depends on whether we had parent info
217 if ($parentrefname) {
218 $input_params{'action'} ||= "blobdiff_plain";
220 $input_params{'action'} ||= "blob_plain";
223 $input_params{'hash_base'} ||= $refname;
224 $input_params{'file_name'} ||= $pathname;
225 } elsif (defined $refname) {
226 # we got "branch". In this case we have to choose if we have to
227 # set hash or hash_base.
229 # Most of the actions without a pathname only want hash to be
230 # set, except for the ones specified in @wants_base that want
231 # hash_base instead. It should also be noted that hand-crafted
232 # links having 'history' as an action and no pathname or hash
233 # set will fail, but that happens regardless of PATH_INFO.
234 $input_params{'action'} ||= "shortlog";
235 if (grep { $_ eq $input_params{'action'} } @wants_base) {
236 $input_params{'hash_base'} ||= $refname;
238 $input_params{'hash'} ||= $refname;
242 # next, handle the 'parent' part, if present
243 if (defined $parentrefname) {
244 # a missing pathspec defaults to the 'current' filename, allowing e.g.
245 # someproject/blobdiff/oldrev..newrev:/filename
246 if ($parentpathname) {
247 $parentpathname =~ s
,^/+,,;
248 $parentpathname =~ s
,/$,,;
249 $input_params{'file_parent'} ||= $parentpathname;
251 $input_params{'file_parent'} ||= $input_params{'file_name'};
253 # we assume that hash_parent_base is wanted if a path was specified,
254 # or if the action wants hash_base instead of hash
255 if (defined $input_params{'file_parent'} ||
256 grep { $_ eq $input_params{'action'} } @wants_base) {
257 $input_params{'hash_parent_base'} ||= $parentrefname;
259 $input_params{'hash_parent'} ||= $parentrefname;
263 # for the snapshot action, we allow URLs in the form
264 # $project/snapshot/$hash.ext
265 # where .ext determines the snapshot and gets removed from the
266 # passed $refname to provide the $hash.
268 # To be able to tell that $refname includes the format extension, we
269 # require the following two conditions to be satisfied:
270 # - the hash input parameter MUST have been set from the $refname part
271 # of the URL (i.e. they must be equal)
272 # - the snapshot format MUST NOT have been defined already (e.g. from
274 # It's also useless to try any matching unless $refname has a dot,
275 # so we check for that too
276 if (defined $input_params{'action'} &&
277 $input_params{'action'} eq 'snapshot' &&
278 defined $refname && index($refname, '.') != -1 &&
279 $refname eq $input_params{'hash'} &&
280 !defined $input_params{'snapshot_format'}) {
281 # We loop over the known snapshot formats, checking for
282 # extensions. Allowed extensions are both the defined suffix
283 # (which includes the initial dot already) and the snapshot
284 # format key itself, with a prepended dot
285 while (my ($fmt, $opt) = each %known_snapshot_formats) {
287 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
291 # a valid suffix was found, so set the snapshot format
292 # and reset the hash parameter
293 $input_params{'snapshot_format'} = $fmt;
294 $input_params{'hash'} = $hash;
295 # we also set the format suffix to the one requested
296 # in the URL: this way a request for e.g. .tgz returns
297 # a .tgz instead of a .tar.gz
298 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
304 sub evaluate_and_validate_params
{
305 $action = $input_params{'action'};
306 if (defined $action) {
307 if (!validate_action
($action)) {
308 die_error
(400, "Invalid action parameter");
312 $edit = $input_params{'edit'};
313 if(defined $edit && gitweb_check_feature
('write')) {
314 if(!validate_edit
($edit)) {
315 die_error
(400, "Invalid edit parameter");
319 # parameters which are pathnames
320 $project = $input_params{'project'};
321 if (defined $project) {
322 if (!validate_project
($project)) {
324 die_error
(404, "No such project");
328 $file_name = $input_params{'file_name'};
329 if (defined $file_name) {
330 if (!validate_pathname
($file_name)) {
331 die_error
(400, "Invalid file parameter");
335 $file_parent = $input_params{'file_parent'};
336 if (defined $file_parent) {
337 if (!validate_pathname
($file_parent)) {
338 die_error
(400, "Invalid file parent parameter");
342 # parameters which are refnames
343 $hash = $input_params{'hash'};
345 if (!validate_refname
($hash)) {
346 die_error
(400, "Invalid hash parameter");
350 $hash_parent = $input_params{'hash_parent'};
351 if (defined $hash_parent) {
352 if (!validate_refname
($hash_parent)) {
353 die_error
(400, "Invalid hash parent parameter");
357 $hash_base = $input_params{'hash_base'};
358 if (defined $hash_base) {
359 if (!validate_refname
($hash_base)) {
360 die_error
(400, "Invalid hash base parameter");
364 @extra_options = @
{$input_params{'extra_options'}};
365 # @extra_options is always defined, since it can only be (currently) set from
366 # CGI, and $cgi->param() returns the empty array in array context if the param
368 foreach my $opt (@extra_options) {
369 if (not exists $allowed_options{$opt}) {
370 die_error
(400, "Invalid option parameter");
372 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
373 die_error
(400, "Invalid option parameter for this action");
377 $hash_parent_base = $input_params{'hash_parent_base'};
378 if (defined $hash_parent_base) {
379 if (!validate_refname
($hash_parent_base)) {
380 die_error
(400, "Invalid hash parent base parameter");
385 $page = $input_params{'page'};
387 if ($page =~ m/[^0-9]/) {
388 die_error
(400, "Invalid page parameter");
392 $searchtype = $input_params{'searchtype'};
393 if (defined $searchtype) {
394 if ($searchtype =~ m/[^a-z]/) {
395 die_error
(400, "Invalid searchtype parameter");
399 $search_use_regexp = $input_params{'search_use_regexp'};
401 $searchtext = $input_params{'searchtext'};
402 if (defined $searchtext) {
403 if (length($searchtext) < 2) {
404 die_error
(403, "At least two characters are required for search parameter");
406 $search_regexp = $search_use_regexp ?
$searchtext : quotemeta $searchtext;
410 sub evaluate_git_dir
{
411 $git_dir = "$projectroot/$project" if $project;
414 # custom error handler: 'die <message>' is Internal Server Error
415 sub handle_errors_html
{
416 my $msg = shift; # it is already HTML escaped
418 # to avoid infinite loop where error occurs in die_error,
419 # change handler to default handler, disabling handle_errors_html
420 set_message
("Error occured when inside die_error:\n$msg");
422 # you cannot jump out of die_error when called as error handler;
423 # the subroutine set via CGI::Carp::set_message is called _after_
424 # HTTP headers are already written, so it cannot write them itself
425 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
427 set_message
(\
&handle_errors_html
);
431 if (!defined $action) {
433 $action = git_get_type
($hash);
434 } elsif (defined $hash_base && defined $file_name) {
435 $action = git_get_type
("$hash_base:$file_name");
436 } elsif (defined $project) {
439 $action = 'project_list';
446 if (!defined($actions{$action})) {
447 die_error
(400, "Unknown action or edit");
449 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
451 die_error
(400, "Project needed");
454 $actions{$action}->();
458 our $t0 = [Time
::HiRes
::gettimeofday
()]
462 evaluate_gitweb_config
();
463 evaluate_git_version
();
466 # $projectroot and $projects_list might be set in gitweb config file
467 $projects_list ||= $projectroot;
469 evaluate_query_params
();
470 evaluate_path_info
();
471 evaluate_and_validate_params
();
474 configure_gitweb_features
();
479 our $is_last_request = sub { 1 };
480 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
482 sub configure_as_fcgi
{
484 our $CGI = 'CGI::Fast';
486 my $request_number = 0;
487 # let each child service 100 requests
488 our $is_last_request = sub { ++$request_number > 100 };
491 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
493 if $script_name =~ /\.fcgi$/;
495 return unless (@ARGV);
497 require Getopt
::Long
;
498 Getopt
::Long
::GetOptions
(
499 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
501 my ($arg, $val) = @_;
502 return unless eval { require FCGI
::ProcManager
; 1; };
503 my $proc_manager = FCGI
::ProcManager
->new({
506 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
507 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
508 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
520 while ($cgi = $CGI->new()) {
521 $pre_dispatch_hook->()
522 if $pre_dispatch_hook;
526 $pre_dispatch_hook->()
527 if $post_dispatch_hook;
529 last REQUEST
if ($is_last_request->());
538 if (defined caller) {
539 # wrapped in a subroutine processing requests,
540 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
543 # pure CGI script, serving single request
547 ## ======================================================================
548 ## validation, quoting/unquoting and escaping
550 sub validate_action
{
551 my $input = shift || return undef;
552 return undef unless exists $actions{$input};
557 my $input = shift || return undef;
558 return undef unless exists $edits{$input};
562 sub validate_project
{
563 my $input = shift || return undef;
564 if (!validate_pathname
($input) ||
565 !(-d
"$projectroot/$input") ||
566 !check_export_ok
("$projectroot/$input") ||
567 ($strict_export && !project_in_list
($input))) {
574 sub validate_pathname
{
575 my $input = shift || return undef;
577 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
578 # at the beginning, at the end, and between slashes.
579 # also this catches doubled slashes
580 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
584 if ($input =~ m!\0!) {
590 sub validate_refname
{
591 my $input = shift || return undef;
593 # textual hashes are O.K.
594 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
597 # it must be correct pathname
598 $input = validate_pathname
($input)
600 # restrictions on ref name according to git-check-ref-format
601 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
607 ## ......................................................................
608 ## functions printing or outputting HTML: div
610 # Outputs the author name and date in long form
611 sub git_print_authorship
{
614 my $tag = $opts{-tag
} || 'div';
615 my $author = $co->{'author_name'};
617 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
618 print "<$tag class=\"author_date\">" .
619 format_search_author
($author, "author", esc_html
($author)) .
621 print_local_time
(%ad) if ($opts{-localtime});
622 print "]" . git_get_avatar
($co->{'author_email'}, -pad_before
=> 1)
626 # Outputs table rows containing the full author or committer information,
627 # in the format expected for 'commit' view (& similia).
628 # Parameters are a commit hash reference, followed by the list of people
629 # to output information for. If the list is empty it defalts to both
630 # author and committer.
631 sub git_print_authorship_rows
{
633 # too bad we can't use @people = @_ || ('author', 'committer')
635 @people = ('author', 'committer') unless @people;
636 foreach my $who (@people) {
637 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
638 print "<tr><td>$who</td><td>" .
639 format_search_author
($co->{"${who}_name"}, $who,
640 esc_html
($co->{"${who}_name"})) . " " .
641 format_search_author
($co->{"${who}_email"}, $who,
642 esc_html
("<" . $co->{"${who}_email"} . ">")) .
643 "</td><td rowspan=\"2\">" .
644 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
647 "<td></td><td> $wd{'rfc2822'}";
648 print_local_time
(%wd);
658 if ($opts{'-remove_title'}) {
659 # remove title, i.e. first line of log
662 # remove leading empty lines
663 while (defined $log->[0] && $log->[0] eq "") {
670 foreach my $line (@
$log) {
671 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
674 if (! $opts{'-remove_signoff'}) {
675 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
678 # remove signoff lines
685 # print only one empty line
686 # do not print empty line after signoff
688 next if ($empty || $signoff);
694 print format_log_line_html
($line) . "<br/>\n";
697 if ($opts{'-final_empty_line'}) {
698 # end with single empty line
699 print "<br/>\n" unless $empty;
703 ## ......................................................................
704 ## functions printing large fragments of HTML
706 sub git_difftree_body
{
707 my ($difftree, $hash, @parents) = @_;
708 my ($parent) = $parents[0];
709 my $have_blame = gitweb_check_feature
('blame');
710 print "<div class=\"list_head\">\n";
711 if ($#{$difftree} > 10) {
712 print(($#{$difftree} + 1) . " files changed:\n");
716 print "<table class=\"" .
717 (@parents > 1 ?
"combined " : "") .
720 # header only for combined diff in 'commitdiff' view
721 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
724 print "<thead><tr>\n" .
725 "<th></th><th></th>\n"; # filename, patchN link
726 for (my $i = 0; $i < @parents; $i++) {
727 my $par = $parents[$i];
729 $cgi->a({-href
=> href
(action
=>"commitdiff",
730 hash
=>$hash, hash_parent
=>$par),
731 -title
=> 'commitdiff to parent number ' .
732 ($i+1) . ': ' . substr($par,0,7)},
736 print "</tr></thead>\n<tbody>\n";
741 foreach my $line (@
{$difftree}) {
742 my $diff = parsed_difftree_line
($line);
745 print "<tr class=\"dark\">\n";
747 print "<tr class=\"light\">\n";
751 if (exists $diff->{'nparents'}) { # combined diff
753 fill_from_file_info
($diff, @parents)
754 unless exists $diff->{'from_file'};
756 if (!is_deleted
($diff)) {
757 # file exists in the result (child) commit
759 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
760 file_name
=>$diff->{'to_file'},
762 -class => "list"}, esc_path
($diff->{'to_file'})) .
766 esc_path
($diff->{'to_file'}) .
770 if ($action eq 'commitdiff') {
773 print "<td class=\"link\">" .
774 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
781 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
782 my $hash_parent = $parents[$i];
783 my $from_hash = $diff->{'from_id'}[$i];
784 my $from_path = $diff->{'from_file'}[$i];
785 my $status = $diff->{'status'}[$i];
787 $has_history ||= ($status ne 'A');
788 $not_deleted ||= ($status ne 'D');
790 if ($status eq 'A') {
791 print "<td class=\"link\" align=\"right\"> | </td>\n";
792 } elsif ($status eq 'D') {
793 print "<td class=\"link\">" .
794 $cgi->a({-href
=> href
(action
=>"blob",
797 file_name
=>$from_path)},
801 if ($diff->{'to_id'} eq $from_hash) {
802 print "<td class=\"link nochange\">";
804 print "<td class=\"link\">";
806 print $cgi->a({-href
=> href
(action
=>"blobdiff",
807 hash
=>$diff->{'to_id'},
808 hash_parent
=>$from_hash,
810 hash_parent_base
=>$hash_parent,
811 file_name
=>$diff->{'to_file'},
812 file_parent
=>$from_path)},
818 print "<td class=\"link\">";
820 print $cgi->a({-href
=> href
(action
=>"blob",
821 hash
=>$diff->{'to_id'},
822 file_name
=>$diff->{'to_file'},
825 print " | " if ($has_history);
828 print $cgi->a({-href
=> href
(action
=>"history",
829 file_name
=>$diff->{'to_file'},
836 next; # instead of 'else' clause, to avoid extra indent
840 my ($to_mode_oct, $to_mode_str, $to_file_type);
841 my ($from_mode_oct, $from_mode_str, $from_file_type);
842 if ($diff->{'to_mode'} ne ('0' x
6)) {
843 $to_mode_oct = oct $diff->{'to_mode'};
844 if (S_ISREG
($to_mode_oct)) { # only for regular file
845 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
847 $to_file_type = file_type
($diff->{'to_mode'});
849 if ($diff->{'from_mode'} ne ('0' x
6)) {
850 $from_mode_oct = oct $diff->{'from_mode'};
851 if (S_ISREG
($to_mode_oct)) { # only for regular file
852 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
854 $from_file_type = file_type
($diff->{'from_mode'});
857 if ($diff->{'status'} eq "A") { # created
858 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
859 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
860 $mode_chng .= "]</span>";
862 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
863 hash_base
=>$hash, file_name
=>$diff->{'file'}),
864 -class => "list"}, esc_path
($diff->{'file'}));
866 print "<td>$mode_chng</td>\n";
867 print "<td class=\"link\">";
868 if ($action eq 'commitdiff') {
871 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
874 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
875 hash_base
=>$hash, file_name
=>$diff->{'file'})},
879 } elsif ($diff->{'status'} eq "D") { # deleted
880 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
882 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
883 hash_base
=>$parent, file_name
=>$diff->{'file'}),
884 -class => "list"}, esc_path
($diff->{'file'}));
886 print "<td>$mode_chng</td>\n";
887 print "<td class=\"link\">";
888 if ($action eq 'commitdiff') {
891 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
894 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
895 hash_base
=>$parent, file_name
=>$diff->{'file'})},
898 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
899 file_name
=>$diff->{'file'})},
902 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
903 file_name
=>$diff->{'file'})},
907 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
909 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
910 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
911 if ($from_file_type ne $to_file_type) {
912 $mode_chnge .= " from $from_file_type to $to_file_type";
914 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
915 if ($from_mode_str && $to_mode_str) {
916 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
917 } elsif ($to_mode_str) {
918 $mode_chnge .= " mode: $to_mode_str";
921 $mode_chnge .= "]</span>\n";
924 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
925 hash_base
=>$hash, file_name
=>$diff->{'file'}),
926 -class => "list"}, esc_path
($diff->{'file'}));
928 print "<td>$mode_chnge</td>\n";
929 print "<td class=\"link\">";
930 if ($action eq 'commitdiff') {
933 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
935 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
936 # "commit" view and modified file (not onlu mode changed)
937 print $cgi->a({-href
=> href
(action
=>"blobdiff",
938 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
939 hash_base
=>$hash, hash_parent_base
=>$parent,
940 file_name
=>$diff->{'file'})},
944 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
945 hash_base
=>$hash, file_name
=>$diff->{'file'})},
948 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
949 file_name
=>$diff->{'file'})},
952 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
953 file_name
=>$diff->{'file'})},
957 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
958 my %status_name = ('R' => 'moved', 'C' => 'copied');
959 my $nstatus = $status_name{$diff->{'status'}};
961 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
962 # mode also for directories, so we cannot use $to_mode_str
963 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
966 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
967 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
968 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
969 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
970 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
971 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
972 -class => "list"}, esc_path
($diff->{'from_file'})) .
973 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
974 "<td class=\"link\">";
975 if ($action eq 'commitdiff') {
978 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
980 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
981 # "commit" view and modified file (not only pure rename or copy)
982 print $cgi->a({-href
=> href
(action
=>"blobdiff",
983 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
984 hash_base
=>$hash, hash_parent_base
=>$parent,
985 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
989 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
990 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
993 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
994 file_name
=>$diff->{'to_file'})},
997 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
998 file_name
=>$diff->{'to_file'})},
1002 } # we should not encounter Unmerged (U) or Unknown (X) status
1005 print "</tbody>" if $has_header;
1009 sub git_patchset_body
{
1010 my ($fd, $difftree, $hash, @hash_parents) = @_;
1011 my ($hash_parent) = $hash_parents[0];
1013 my $is_combined = (@hash_parents > 1);
1015 my $patch_number = 0;
1021 print "<div class=\"patchset\">\n";
1023 # skip to first patch
1024 while ($patch_line = <$fd>) {
1027 last if ($patch_line =~ m/^diff /);
1031 while ($patch_line) {
1033 # parse "git diff" header line
1034 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
1035 # $1 is from_name, which we do not use
1036 $to_name = unquote
($2);
1037 $to_name =~ s!^b/!!;
1038 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
1039 # $1 is 'cc' or 'combined', which we do not use
1040 $to_name = unquote
($2);
1045 # check if current patch belong to current raw line
1046 # and parse raw git-diff line if needed
1047 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
1048 # this is continuation of a split patch
1049 print "<div class=\"patch cont\">\n";
1051 # advance raw git-diff output if needed
1052 $patch_idx++ if defined $diffinfo;
1054 # read and prepare patch information
1055 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
1057 # compact combined diff output can have some patches skipped
1058 # find which patch (using pathname of result) we are at now;
1060 while ($to_name ne $diffinfo->{'to_file'}) {
1061 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1062 format_diff_cc_simplified
($diffinfo, @hash_parents) .
1063 "</div>\n"; # class="patch"
1068 last if $patch_idx > $#$difftree;
1069 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
1073 # modifies %from, %to hashes
1074 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
1076 # this is first patch for raw difftree line with $patch_idx index
1077 # we index @$difftree array from 0, but number patches from 1
1078 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1082 #assert($patch_line =~ m/^diff /) if DEBUG;
1083 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
1085 # print "git diff" header
1086 print format_git_diff_header_line
($patch_line, $diffinfo,
1089 # print extended diff header
1090 print "<div class=\"diff extended_header\">\n";
1092 while ($patch_line = <$fd>) {
1095 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
1097 print format_extended_diff_header_line
($patch_line, $diffinfo,
1100 print "</div>\n"; # class="diff extended_header"
1102 # from-file/to-file diff header
1103 if (! $patch_line) {
1104 print "</div>\n"; # class="patch"
1107 next PATCH
if ($patch_line =~ m/^diff /);
1108 #assert($patch_line =~ m/^---/) if DEBUG;
1110 my $last_patch_line = $patch_line;
1111 $patch_line = <$fd>;
1113 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
1115 print format_diff_from_to_header
($last_patch_line, $patch_line,
1116 $diffinfo, \
%from, \
%to,
1121 while ($patch_line = <$fd>) {
1124 next PATCH
if ($patch_line =~ m/^diff /);
1126 print format_diff_line
($patch_line, \
%from, \
%to);
1130 print "</div>\n"; # class="patch"
1133 # for compact combined (--cc) format, with chunk and patch simpliciaction
1134 # patchset might be empty, but there might be unprocessed raw lines
1135 for (++$patch_idx if $patch_number > 0;
1136 $patch_idx < @
$difftree;
1138 # read and prepare patch information
1139 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
1141 # generate anchor for "patch" links in difftree / whatchanged part
1142 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
1143 format_diff_cc_simplified
($diffinfo, @hash_parents) .
1144 "</div>\n"; # class="patch"
1149 if ($patch_number == 0) {
1150 if (@hash_parents > 1) {
1151 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
1153 print "<div class=\"diff nodifferences\">No differences found</div>\n";
1157 print "</div>\n"; # class="patchset"
1160 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1162 # fills project list info (age, description, owner, forks) for each
1163 # project in the list, removing invalid projects from returned list
1164 # NOTE: modifies $projlist, but does not remove entries from it
1165 sub fill_project_list_info
{
1166 my ($projlist, $check_forks) = @_;
1169 my $show_ctags = gitweb_check_feature
('ctags');
1171 foreach my $pr (@
$projlist) {
1172 my (@activity) = git_get_last_activity
($pr->{'path'});
1173 unless (@activity) {
1176 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
1177 if (!defined $pr->{'descr'}) {
1178 my $descr = git_get_project_description
($pr->{'path'}) || "";
1179 $descr = to_utf8
($descr);
1180 $pr->{'descr_long'} = $descr;
1181 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
1183 if (!defined $pr->{'owner'}) {
1184 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
1187 my $pname = $pr->{'path'};
1188 if (($pname =~ s/\.git$//) &&
1189 ($pname !~ /\/$/) &&
1190 (-d
"$projectroot/$pname")) {
1191 $pr->{'forks'} = "-d $projectroot/$pname";
1196 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
1197 push @projects, $pr;
1203 sub git_project_list_body
{
1204 # actually uses global variable $project
1205 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
1207 my $check_forks = gitweb_check_feature
('forks');
1208 my @projects = fill_project_list_info
($projlist, $check_forks);
1210 $order ||= $default_projects_order;
1211 $from = 0 unless defined $from;
1212 $to = $#projects if (!defined $to || $#projects < $to);
1215 project
=> { key
=> 'path', type
=> 'str' },
1216 descr
=> { key
=> 'descr_long', type
=> 'str' },
1217 owner
=> { key
=> 'owner', type
=> 'str' },
1218 age
=> { key
=> 'age', type
=> 'num' }
1220 my $oi = $order_info{$order};
1221 if ($oi->{'type'} eq 'str') {
1222 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
1224 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
1227 my $show_ctags = gitweb_check_feature
('ctags');
1230 foreach my $p (@projects) {
1231 foreach my $ct (keys %{$p->{'ctags'}}) {
1232 $ctags{$ct} += $p->{'ctags'}->{$ct};
1235 my $cloud = git_populate_project_tagcloud
(\
%ctags);
1236 print git_show_project_tagcloud
($cloud, 64);
1239 print "<table class=\"project_list\">\n";
1240 unless ($no_header) {
1243 print "<th></th>\n";
1245 print_sort_th
('project', $order, 'Project');
1246 print_sort_th
('descr', $order, 'Description');
1247 print_sort_th
('owner', $order, 'Owner');
1248 print_sort_th
('age', $order, 'Last Change');
1249 print "<th></th>\n" . # for links
1253 my $tagfilter = $cgi->param('by_tag');
1254 for (my $i = $from; $i <= $to; $i++) {
1255 my $pr = $projects[$i];
1257 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
1258 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
1259 and not $pr->{'descr_long'} =~ /$searchtext/;
1260 # Weed out forks or non-matching entries of search
1262 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s
#\.git$#/#;
1263 $forkbase="^$forkbase" if $forkbase;
1264 next if not $searchtext and not $tagfilter and $show_ctags
1265 and $pr->{'path'} =~ m
#$forkbase.*/.*#; # regexp-safe
1269 print "<tr class=\"dark\">\n";
1271 print "<tr class=\"light\">\n";
1276 if ($pr->{'forks'}) {
1277 print "<!-- $pr->{'forks'} -->\n";
1278 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
1282 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
1283 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
1284 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
1285 -class => "list", -title
=> $pr->{'descr_long'}},
1286 esc_html
($pr->{'descr'})) . "</td>\n" .
1287 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
1288 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
1289 (defined $pr->{'age_string'} ?
$pr->{'age_string'} : "No commits") . "</td>\n" .
1290 "<td class=\"link\">" .
1291 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
1292 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
1293 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
1294 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
1295 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
1299 if (defined $extra) {
1302 print "<td></td>\n";
1304 print "<td colspan=\"5\">$extra</td>\n" .
1311 # uses global variable $project
1312 my ($commitlist, $from, $to, $refs, $extra) = @_;
1314 $from = 0 unless defined $from;
1315 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1317 for (my $i = 0; $i <= $to; $i++) {
1318 my %co = %{$commitlist->[$i]};
1320 my $commit = $co{'id'};
1321 my $ref = format_ref_marker
($refs, $commit);
1322 my %ad = parse_date
($co{'author_epoch'});
1323 git_print_header_div
('commit',
1324 "<span class=\"age\">$co{'age_string'}</span>" .
1325 esc_html
($co{'title'}) . $ref,
1327 print "<div class=\"title_text\">\n" .
1328 "<div class=\"log_link\">\n" .
1329 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
1331 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
1333 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
1336 git_print_authorship
(\
%co, -tag
=> 'span');
1337 print "<br/>\n</div>\n";
1339 print "<div class=\"log_body\">\n";
1340 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
1344 print "<div class=\"page_nav\">\n";
1350 sub git_shortlog_body
{
1351 # uses global variable $project
1352 my ($commitlist, $from, $to, $refs, $extra) = @_;
1354 $from = 0 unless defined $from;
1355 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1357 print "<table class=\"shortlog\">\n";
1359 for (my $i = $from; $i <= $to; $i++) {
1360 my %co = %{$commitlist->[$i]};
1361 my $commit = $co{'id'};
1362 my $ref = format_ref_marker
($refs, $commit);
1364 print "<tr class=\"dark\">\n";
1366 print "<tr class=\"light\">\n";
1369 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
1370 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1371 format_author_html
('td', \
%co, 10) . "<td>";
1372 print format_subject_html
($co{'title'}, $co{'title_short'},
1373 href
(action
=>"commit", hash
=>$commit), $ref);
1375 "<td class=\"link\">" .
1376 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
1377 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
1378 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
1379 my $snapshot_links = format_snapshot_links
($commit);
1380 if (defined $snapshot_links) {
1381 print " | " . $snapshot_links;
1386 if (defined $extra) {
1388 "<td colspan=\"4\">$extra</td>\n" .
1394 sub git_history_body
{
1395 # Warning: assumes constant type (blob or tree) during history
1396 my ($commitlist, $from, $to, $refs, $extra,
1397 $file_name, $file_hash, $ftype) = @_;
1399 $from = 0 unless defined $from;
1400 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
1402 print "<table class=\"history\">\n";
1404 for (my $i = $from; $i <= $to; $i++) {
1405 my %co = %{$commitlist->[$i]};
1409 my $commit = $co{'id'};
1411 my $ref = format_ref_marker
($refs, $commit);
1414 print "<tr class=\"dark\">\n";
1416 print "<tr class=\"light\">\n";
1419 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1420 # shortlog: format_author_html('td', \%co, 10)
1421 format_author_html
('td', \
%co, 15, 3) . "<td>";
1422 # originally git_history used chop_str($co{'title'}, 50)
1423 print format_subject_html
($co{'title'}, $co{'title_short'},
1424 href
(action
=>"commit", hash
=>$commit), $ref);
1426 "<td class=\"link\">" .
1427 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
1428 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
1430 if ($ftype eq 'blob') {
1431 my $blob_current = $file_hash;
1432 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
1433 if (defined $blob_current && defined $blob_parent &&
1434 $blob_current ne $blob_parent) {
1436 $cgi->a({-href
=> href
(action
=>"blobdiff",
1437 hash
=>$blob_current, hash_parent
=>$blob_parent,
1438 hash_base
=>$hash_base, hash_parent_base
=>$commit,
1439 file_name
=>$file_name)},
1446 if (defined $extra) {
1448 "<td colspan=\"4\">$extra</td>\n" .
1455 # uses global variable $project
1456 my ($taglist, $from, $to, $extra) = @_;
1457 $from = 0 unless defined $from;
1458 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
1460 print "<table class=\"tags\">\n";
1462 for (my $i = $from; $i <= $to; $i++) {
1463 my $entry = $taglist->[$i];
1465 my $comment = $tag{'subject'};
1467 if (defined $comment) {
1468 $comment_short = chop_str
($comment, 30, 5);
1471 print "<tr class=\"dark\">\n";
1473 print "<tr class=\"light\">\n";
1476 if (defined $tag{'age'}) {
1477 print "<td><i>$tag{'age'}</i></td>\n";
1479 print "<td></td>\n";
1482 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
1483 -class => "list name"}, esc_html
($tag{'name'})) .
1486 if (defined $comment) {
1487 print format_subject_html
($comment, $comment_short,
1488 href
(action
=>"tag", hash
=>$tag{'id'}));
1491 "<td class=\"selflink\">";
1492 if ($tag{'type'} eq "tag") {
1493 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
1498 "<td class=\"link\">" . " | " .
1499 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
1500 if ($tag{'reftype'} eq "commit") {
1501 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
1502 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
1503 } elsif ($tag{'reftype'} eq "blob") {
1504 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
1509 if (defined $extra) {
1511 "<td colspan=\"5\">$extra</td>\n" .
1517 sub git_heads_body
{
1518 # uses global variable $project
1519 my ($headlist, $head, $from, $to, $extra) = @_;
1520 $from = 0 unless defined $from;
1521 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
1523 print "<table class=\"heads\">\n";
1525 for (my $i = $from; $i <= $to; $i++) {
1526 my $entry = $headlist->[$i];
1528 my $curr = $ref{'id'} eq $head;
1530 print "<tr class=\"dark\">\n";
1532 print "<tr class=\"light\">\n";
1535 print "<td><i>$ref{'age'}</i></td>\n" .
1536 ($curr ?
"<td class=\"current_head\">" : "<td>") .
1537 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
1538 -class => "list name"},esc_html
($ref{'name'})) .
1540 "<td class=\"link\">" .
1541 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
1542 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
1543 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
1547 if (defined $extra) {
1549 "<td colspan=\"3\">$extra</td>\n" .
1555 sub git_search_grep_body
{
1556 my ($commitlist, $from, $to, $extra) = @_;
1557 $from = 0 unless defined $from;
1558 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
1560 print "<table class=\"commit_search\">\n";
1562 for (my $i = $from; $i <= $to; $i++) {
1563 my %co = %{$commitlist->[$i]};
1567 my $commit = $co{'id'};
1569 print "<tr class=\"dark\">\n";
1571 print "<tr class=\"light\">\n";
1574 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1575 format_author_html
('td', \
%co, 15, 5) .
1577 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
1578 -class => "list subject"},
1579 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
1580 my $comment = $co{'comment'};
1581 foreach my $line (@
$comment) {
1582 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
1583 my ($lead, $match, $trail) = ($1, $2, $3);
1584 $match = chop_str
($match, 70, 5, 'center');
1585 my $contextlen = int((80 - length($match))/2);
1586 $contextlen = 30 if ($contextlen > 30);
1587 $lead = chop_str
($lead, $contextlen, 10, 'left');
1588 $trail = chop_str
($trail, $contextlen, 10, 'right');
1590 $lead = esc_html
($lead);
1591 $match = esc_html
($match);
1592 $trail = esc_html
($trail);
1594 print "$lead<span class=\"match\">$match</span>$trail<br />";
1598 "<td class=\"link\">" .
1599 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
1601 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
1603 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
1607 if (defined $extra) {
1609 "<td colspan=\"3\">$extra</td>\n" .
1615 ## ======================================================================
1616 ## ======================================================================
1619 sub git_project_list
{
1620 my $order = $input_params{'order'};
1621 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
1622 die_error
(400, "Unknown order parameter");
1625 my @list = git_get_projects_list
();
1627 die_error
(404, "No projects found");
1631 if (defined $home_text && -f
$home_text) {
1632 print "<div class=\"index_include\">\n";
1633 insert_file
($home_text);
1636 print $cgi->startform(-method
=> "get") .
1637 "<p class=\"projsearch\">Search:\n" .
1638 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
1640 $cgi->end_form() . "\n";
1641 git_project_list_body
(\
@list, $order);
1646 my $order = $input_params{'order'};
1647 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
1648 die_error
(400, "Unknown order parameter");
1651 my @list = git_get_projects_list
($project);
1653 die_error
(404, "No forks found");
1657 git_print_page_nav
('','');
1658 git_print_header_div
('summary', "$project forks");
1659 git_project_list_body
(\
@list, $order);
1663 sub git_project_index
{
1664 my @projects = git_get_projects_list
($project);
1667 -type
=> 'text/plain',
1668 -charset
=> 'utf-8',
1669 -content_disposition
=> 'inline; filename="index.aux"');
1671 foreach my $pr (@projects) {
1672 if (!exists $pr->{'owner'}) {
1673 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
1676 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
1677 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
1678 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
1679 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
1683 print "$path $owner\n";
1688 my $descr = git_get_project_description
($project) || "none";
1689 my %co = parse_commit
("HEAD");
1690 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
1691 my $head = $co{'id'};
1693 my $owner = git_get_project_owner
($project);
1695 my $refs = git_get_references
();
1696 # These get_*_list functions return one more to allow us to see if
1697 # there are more ...
1698 my @taglist = git_get_tags_list
(16);
1699 my @headlist = git_get_heads_list
(16);
1701 my $check_forks = gitweb_check_feature
('forks');
1704 @forklist = git_get_projects_list
($project);
1708 git_print_page_nav
('summary','', $head);
1710 print "<div class=\"title\"> </div>\n";
1711 print "<table class=\"projects_list\">\n" .
1712 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
1713 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
1714 if (defined $cd{'rfc2822'}) {
1715 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
1718 # use per project git URL list in $projectroot/$project/cloneurl
1719 # or make project git URL from git base URL and project name
1720 my $url_tag = "URL";
1721 my @url_list = git_get_project_url_list
($project);
1722 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
1723 foreach my $git_url (@url_list) {
1724 next unless $git_url;
1725 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
1730 my $show_ctags = gitweb_check_feature
('ctags');
1732 my $ctags = git_get_project_ctags
($project);
1733 my $cloud = git_populate_project_tagcloud
($ctags);
1734 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
1735 print "</td>\n<td>" unless %$ctags;
1736 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
1737 print "</td>\n<td>" if %$ctags;
1738 print git_show_project_tagcloud
($cloud, 48);
1744 # If XSS prevention is on, we don't include README.html.
1745 # TODO: Allow a readme in some safe format.
1746 if (!$prevent_xss && -s
"$projectroot/$project/README.html") {
1747 print "<div class=\"title\">readme</div>\n" .
1748 "<div class=\"readme\">\n";
1749 insert_file
("$projectroot/$project/README.html");
1750 print "\n</div>\n"; # class="readme"
1753 # we need to request one more than 16 (0..15) to check if
1755 my @commitlist = $head ? parse_commits
($head, 17) : ();
1757 git_print_header_div
('shortlog');
1758 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
1759 $#commitlist <= 15 ?
undef :
1760 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
1764 git_print_header_div
('tags');
1765 git_tags_body
(\
@taglist, 0, 15,
1766 $#taglist <= 15 ?
undef :
1767 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
1771 git_print_header_div
('heads');
1772 git_heads_body
(\
@headlist, $head, 0, 15,
1773 $#headlist <= 15 ?
undef :
1774 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
1778 git_print_header_div
('forks');
1779 git_project_list_body
(\
@forklist, 'age', 0, 15,
1780 $#forklist <= 15 ?
undef :
1781 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
1789 my $head = git_get_head_hash
($project);
1791 git_print_page_nav
('','', $head,undef,$head);
1792 my %tag = parse_tag
($hash);
1795 die_error
(404, "Unknown tag object");
1798 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
1799 print "<div class=\"title_text\">\n" .
1800 "<table class=\"object_header\">\n" .
1802 "<td>object</td>\n" .
1803 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
1804 $tag{'object'}) . "</td>\n" .
1805 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
1806 $tag{'type'}) . "</td>\n" .
1808 if (defined($tag{'author'})) {
1809 git_print_authorship_rows
(\
%tag, 'author');
1811 print "</table>\n\n" .
1813 print "<div class=\"page_body\">";
1814 my $comment = $tag{'comment'};
1815 foreach my $line (@
$comment) {
1817 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
1823 sub git_blame_common
{
1824 my $format = shift || 'porcelain';
1825 if ($format eq 'porcelain' && $cgi->param('js')) {
1826 $format = 'incremental';
1827 $action = 'blame_incremental'; # for page title etc
1831 gitweb_check_feature
('blame')
1832 or die_error
(403, "Blame view not allowed");
1835 die_error
(400, "No file name given") unless $file_name;
1836 $hash_base ||= git_get_head_hash
($project);
1837 die_error
(404, "Couldn't find base commit") unless $hash_base;
1838 my %co = parse_commit
($hash_base)
1839 or die_error
(404, "Commit not found");
1841 if (!defined $hash) {
1842 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
1843 or die_error
(404, "Error looking up file");
1845 $ftype = git_get_type
($hash);
1846 if ($ftype !~ "blob") {
1847 die_error
(400, "Object is not a blob");
1852 if ($format eq 'incremental') {
1853 # get file contents (as base)
1854 open $fd, "-|", git_cmd
(), 'cat-file', 'blob', $hash
1855 or die_error
(500, "Open git-cat-file failed");
1856 } elsif ($format eq 'data') {
1857 # run git-blame --incremental
1858 open $fd, "-|", git_cmd
(), "blame", "--incremental",
1859 $hash_base, "--", $file_name
1860 or die_error
(500, "Open git-blame --incremental failed");
1862 # run git-blame --porcelain
1863 open $fd, "-|", git_cmd
(), "blame", '-p',
1864 $hash_base, '--', $file_name
1865 or die_error
(500, "Open git-blame --porcelain failed");
1868 # incremental blame data returns early
1869 if ($format eq 'data') {
1871 -type
=>"text/plain", -charset
=> "utf-8",
1872 -status
=> "200 OK");
1873 local $| = 1; # output autoflush
1876 or print "ERROR $!\n";
1879 if (defined $t0 && gitweb_check_feature
('timed')) {
1881 Time
::HiRes
::tv_interval
($t0, [Time
::HiRes
::gettimeofday
()]).
1882 ' '.$number_of_git_cmds;
1892 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
1895 if ($format eq 'incremental') {
1897 $cgi->a({-href
=> href
(action
=>"blame", javascript
=>0, -replay
=>1)},
1898 "blame") . " (non-incremental)";
1901 $cgi->a({-href
=> href
(action
=>"blame_incremental", -replay
=>1)},
1902 "blame") . " (incremental)";
1906 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
1909 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
1911 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
1912 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
1913 git_print_page_path
($file_name, $ftype, $hash_base);
1916 if ($format eq 'incremental') {
1917 print "<noscript>\n<div class=\"error\"><center><b>\n".
1918 "This page requires JavaScript to run.\n Use ".
1919 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
1922 "</b></center></div>\n</noscript>\n";
1924 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
1927 print qq!<div
class="page_body">\n!;
1928 print qq!<div id
="progress_info">... / ...</div
>\n!
1929 if ($format eq 'incremental');
1930 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
1931 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
1933 qq!<tr
><th
>Commit
</th><th>Line</th
><th
>Data
</th></tr
>\n!.
1937 my @rev_color = qw(light dark);
1938 my $num_colors = scalar(@rev_color);
1939 my $current_color = 0;
1941 if ($format eq 'incremental') {
1942 my $color_class = $rev_color[$current_color];
1947 while (my $line = <$fd>) {
1951 print qq!<tr id
="l$linenr" class="$color_class">!.
1952 qq!<td
class="sha1"><a href
=""> </a></td
>!.
1953 qq!<td
class="linenr">!.
1954 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
1955 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
1959 } else { # porcelain, i.e. ordinary blame
1960 my %metainfo = (); # saves information about commits
1964 while (my $line = <$fd>) {
1966 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
1967 # no <lines in group> for subsequent lines in group of lines
1968 my ($full_rev, $orig_lineno, $lineno, $group_size) =
1969 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
1970 if (!exists $metainfo{$full_rev}) {
1971 $metainfo{$full_rev} = { 'nprevious' => 0 };
1973 my $meta = $metainfo{$full_rev};
1975 while ($data = <$fd>) {
1977 last if ($data =~ s/^\t//); # contents of line
1978 if ($data =~ /^(\S+)(?: (.*))?$/) {
1979 $meta->{$1} = $2 unless exists $meta->{$1};
1981 if ($data =~ /^previous /) {
1982 $meta->{'nprevious'}++;
1985 my $short_rev = substr($full_rev, 0, 8);
1986 my $author = $meta->{'author'};
1988 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
1989 my $date = $date{'iso-tz'};
1991 $current_color = ($current_color + 1) % $num_colors;
1993 my $tr_class = $rev_color[$current_color];
1994 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
1995 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
1996 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
1997 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
1999 print "<td class=\"sha1\"";
2000 print " title=\"". esc_html
($author) . ", $date\"";
2001 print " rowspan=\"$group_size\"" if ($group_size > 1);
2003 print $cgi->a({-href
=> href
(action
=>"commit",
2005 file_name
=>$file_name)},
2006 esc_html
($short_rev));
2007 if ($group_size >= 2) {
2008 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
2009 if (@author_initials) {
2011 esc_html
(join('', @author_initials));
2017 # 'previous' <sha1 of parent commit> <filename at commit>
2018 if (exists $meta->{'previous'} &&
2019 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
2020 $meta->{'parent'} = $1;
2021 $meta->{'file_parent'} = unquote
($2);
2024 exists($meta->{'parent'}) ?
2025 $meta->{'parent'} : $full_rev;
2026 my $linenr_filename =
2027 exists($meta->{'file_parent'}) ?
2028 $meta->{'file_parent'} : unquote
($meta->{'filename'});
2029 my $blamed = href
(action
=> 'blame',
2030 file_name
=> $linenr_filename,
2031 hash_base
=> $linenr_commit);
2032 print "<td class=\"linenr\">";
2033 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
2034 -class => "linenr" },
2037 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
2045 "</table>\n"; # class="blame"
2046 print "</div>\n"; # class="blame_body"
2048 or print "Reading blob failed\n";
2057 sub git_blame_incremental
{
2058 git_blame_common
('incremental');
2061 sub git_blame_data
{
2062 git_blame_common
('data');
2066 my $head = git_get_head_hash
($project);
2068 git_print_page_nav
('','', $head,undef,$head);
2069 git_print_header_div
('summary', $project);
2071 my @tagslist = git_get_tags_list
();
2073 git_tags_body
(\
@tagslist);
2079 my $head = git_get_head_hash
($project);
2081 git_print_page_nav
('','', $head,undef,$head);
2082 git_print_header_div
('summary', $project);
2084 my @headslist = git_get_heads_list
();
2086 git_heads_body
(\
@headslist, $head);
2091 sub git_blob_plain
{
2095 if (!defined $hash) {
2096 if (defined $file_name) {
2097 my $base = $hash_base || git_get_head_hash
($project);
2098 $hash = git_get_hash_by_path
($base, $file_name, "blob")
2099 or die_error
(404, "Cannot find file");
2101 die_error
(400, "No file name defined");
2103 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2104 # blobs defined by non-textual hash id's can be cached
2108 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
2109 or die_error
(500, "Open git-cat-file blob '$hash' failed");
2111 # content-type (can include charset)
2112 $type = blob_contenttype
($fd, $file_name, $type);
2114 # "save as" filename, even when no $file_name is given
2115 my $save_as = "$hash";
2116 if (defined $file_name) {
2117 $save_as = $file_name;
2118 } elsif ($type =~ m/^text\//) {
2122 # With XSS prevention on, blobs of all types except a few known safe
2123 # ones are served with "Content-Disposition: attachment" to make sure
2124 # they don't run in our security domain. For certain image types,
2125 # blob view writes an <img> tag referring to blob_plain view, and we
2126 # want to be sure not to break that by serving the image as an
2127 # attachment (though Firefox 3 doesn't seem to care).
2128 my $sandbox = $prevent_xss &&
2129 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
2133 -expires
=> $expires,
2134 -content_disposition
=>
2135 ($sandbox ?
'attachment' : 'inline')
2136 . '; filename="' . $save_as . '"');
2138 binmode STDOUT
, ':raw';
2140 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
2147 if (!defined $hash) {
2148 if (defined $file_name) {
2149 my $base = $hash_base || git_get_head_hash
($project);
2150 $hash = git_get_hash_by_path
($base, $file_name, "blob")
2151 or die_error
(404, "Cannot find file");
2153 die_error
(400, "No file name defined");
2155 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2156 # blobs defined by non-textual hash id's can be cached
2160 my $have_blame = gitweb_check_feature
('blame');
2161 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
2162 or die_error
(500, "Couldn't cat $file_name, $hash");
2163 my $mimetype = blob_mimetype
($fd, $file_name);
2164 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
2165 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
2167 return git_blob_plain
($mimetype);
2169 # we can have blame only for text/* mimetype
2170 $have_blame &&= ($mimetype =~ m!^text/!);
2172 my $highlight = gitweb_check_feature
('highlight');
2173 my $syntax = guess_file_syntax
($highlight, $mimetype, $file_name);
2174 $fd = run_highlighter
($fd, $highlight, $syntax)
2177 git_header_html
(undef, $expires);
2178 my $formats_nav = '';
2179 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
2180 if (defined $file_name) {
2183 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
2188 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
2191 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
2194 $cgi->a({-href
=> href
(action
=>"blob",
2195 hash_base
=>"HEAD", file_name
=>$file_name)},
2199 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
2202 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2203 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
2205 print "<div class=\"page_nav\">\n" .
2206 "<br/><br/></div>\n" .
2207 "<div class=\"title\">$hash</div>\n";
2209 git_print_page_path
($file_name, "blob", $hash_base);
2210 print "<div class=\"page_body\">\n";
2211 if ($mimetype =~ m!^image/!) {
2212 print qq!<img type
="$mimetype"!;
2214 print qq! alt
="$file_name" title
="$file_name"!;
2217 href(action=>"blob_plain
", hash=>$hash,
2218 hash_base=>$hash_base, file_name=>$file_name) .
2222 while (my $line = <$fd>) {
2225 $line = untabify
($line);
2226 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i</a> %s</div
>\n!,
2227 $nr, href
(-replay
=> 1), $nr, $nr, $syntax ?
$line : esc_html
($line, -nbsp
=>1);
2231 or print "Reading blob failed.\n";
2237 if (!defined $hash_base) {
2238 $hash_base = "HEAD";
2240 if (!defined $hash) {
2241 if (defined $file_name) {
2242 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
2247 die_error
(404, "No such tree") unless defined($hash);
2249 my $show_sizes = gitweb_check_feature
('show-sizes');
2250 my $have_blame = gitweb_check_feature
('blame');
2255 open my $fd, "-|", git_cmd
(), "ls-tree", '-z',
2256 ($show_sizes ?
'-l' : ()), @extra_options, $hash
2257 or die_error
(500, "Open git-ls-tree failed");
2258 @entries = map { chomp; $_ } <$fd>;
2260 or die_error
(404, "Reading tree failed");
2263 my $refs = git_get_references
();
2264 my $ref = format_ref_marker
($refs, $hash_base);
2267 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
2269 if (defined $file_name) {
2271 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
2273 $cgi->a({-href
=> href
(action
=>"tree",
2274 hash_base
=>"HEAD", file_name
=>$file_name)},
2277 my $snapshot_links = format_snapshot_links
($hash);
2278 if (defined $snapshot_links) {
2279 # FIXME: Should be available when we have no hash base as well.
2280 push @views_nav, $snapshot_links;
2282 git_print_page_nav
('tree','', $hash_base, undef, undef,
2283 join(' | ', @views_nav));
2284 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
2287 print "<div class=\"page_nav\">\n";
2288 print "<br/><br/></div>\n";
2289 print "<div class=\"title\">$hash</div>\n";
2291 if (defined $file_name) {
2292 $basedir = $file_name;
2293 if ($basedir ne '' && substr($basedir, -1) ne '/') {
2296 git_print_page_path
($file_name, 'tree', $hash_base);
2298 print "<div class=\"page_body\">\n";
2299 print "<table class=\"tree\">\n";
2301 # '..' (top directory) link if possible
2302 if (defined $hash_base &&
2303 defined $file_name && $file_name =~ m![^/]+$!) {
2305 print "<tr class=\"dark\">\n";
2307 print "<tr class=\"light\">\n";
2311 my $up = $file_name;
2312 $up =~ s!/?[^/]+$!!;
2313 undef $up unless $up;
2314 # based on git_print_tree_entry
2315 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
2316 print '<td class="size"> </td>'."\n" if $show_sizes;
2317 print '<td class="list">';
2318 print $cgi->a({-href
=> href
(action
=>"tree",
2319 hash_base
=>$hash_base,
2323 print "<td class=\"link\"></td>\n";
2327 foreach my $line (@entries) {
2328 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
2331 print "<tr class=\"dark\">\n";
2333 print "<tr class=\"light\">\n";
2337 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
2341 print "</table>\n" .
2347 my ($project, $hash) = @_;
2349 # path/to/project.git -> project
2350 # path/to/project/.git -> project
2351 my $name = to_utf8
($project);
2352 $name =~ s
,([^/])/*\
.git
$,$1,;
2353 $name = basename
($name);
2355 $name =~ s/[[:cntrl:]]/?/g;
2358 if ($hash =~ /^[0-9a-fA-F]+$/) {
2359 # shorten SHA-1 hash
2360 my $full_hash = git_get_full_hash
($project, $hash);
2361 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
2362 $ver = git_get_short_hash
($project, $hash);
2364 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
2365 # tags don't need shortened SHA-1 hash
2368 # branches and other need shortened SHA-1 hash
2369 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
2372 $ver .= '-' . git_get_short_hash
($project, $hash);
2374 # in case of hierarchical branch names
2377 # name = project-version_string
2378 $name = "$name-$ver";
2380 return wantarray ?
($name, $name) : $name;
2384 my $format = $input_params{'snapshot_format'};
2385 if (!@snapshot_fmts) {
2386 die_error
(403, "Snapshots not allowed");
2388 # default to first supported snapshot format
2389 $format ||= $snapshot_fmts[0];
2390 if ($format !~ m/^[a-z0-9]+$/) {
2391 die_error
(400, "Invalid snapshot format parameter");
2392 } elsif (!exists($known_snapshot_formats{$format})) {
2393 die_error
(400, "Unknown snapshot format");
2394 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
2395 die_error
(403, "Snapshot format not allowed");
2396 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
2397 die_error
(403, "Unsupported snapshot format");
2400 my $type = git_get_type
("$hash^{}");
2402 die_error
(404, 'Object does not exist');
2403 } elsif ($type eq 'blob') {
2404 die_error
(400, 'Object is not a tree-ish');
2407 my ($name, $prefix) = snapshot_name
($project, $hash);
2408 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
2409 my $cmd = quote_command
(
2410 git_cmd
(), 'archive',
2411 "--format=$known_snapshot_formats{$format}{'format'}",
2412 "--prefix=$prefix/", $hash);
2413 if (exists $known_snapshot_formats{$format}{'compressor'}) {
2414 $cmd .= ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}});
2417 $filename =~ s/(["\\])/\\$1/g;
2419 -type
=> $known_snapshot_formats{$format}{'type'},
2420 -content_disposition
=> 'inline; filename="' . $filename . '"',
2421 -status
=> '200 OK');
2423 open my $fd, "-|", $cmd
2424 or die_error
(500, "Execute git-archive failed");
2425 binmode STDOUT
, ':raw';
2427 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
2431 sub git_log_generic
{
2432 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
2434 my $head = git_get_head_hash
($project);
2435 if (!defined $base) {
2438 if (!defined $page) {
2441 my $refs = git_get_references
();
2443 my $commit_hash = $base;
2444 if (defined $parent) {
2445 $commit_hash = "$parent..$base";
2448 parse_commits
($commit_hash, 101, (100 * $page),
2449 defined $file_name ?
($file_name, "--full-history") : ());
2452 if (!defined $file_hash && defined $file_name) {
2453 # some commits could have deleted file in question,
2454 # and not have it in tree, but one of them has to have it
2455 for (my $i = 0; $i < @commitlist; $i++) {
2456 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
2457 last if defined $file_hash;
2460 if (defined $file_hash) {
2461 $ftype = git_get_type
($file_hash);
2463 if (defined $file_name && !defined $ftype) {
2464 die_error
(500, "Unknown type of object");
2467 if (defined $file_name) {
2468 %co = parse_commit
($base)
2469 or die_error
(404, "Unknown commit object");
2473 my $paging_nav = format_paging_nav
($fmt_name, $page, $#commitlist >= 100);
2475 if ($#commitlist >= 100) {
2477 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2478 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2480 my $patch_max = gitweb_get_feature
('patches');
2481 if ($patch_max && !defined $file_name) {
2482 if ($patch_max < 0 || @commitlist <= $patch_max) {
2483 $paging_nav .= " ⋅ " .
2484 $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
2490 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
2491 if (defined $file_name) {
2492 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
2494 git_print_header_div
('summary', $project)
2496 git_print_page_path
($file_name, $ftype, $hash_base)
2497 if (defined $file_name);
2499 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
2500 $file_name, $file_hash, $ftype);
2506 git_log_generic
('log', \
&git_log_body
,
2507 $hash, $hash_parent);
2511 $hash ||= $hash_base || "HEAD";
2512 my %co = parse_commit
($hash)
2513 or die_error
(404, "Unknown commit object");
2515 my $parent = $co{'parent'};
2516 my $parents = $co{'parents'}; # listref
2518 # we need to prepare $formats_nav before any parameter munging
2520 if (!defined $parent) {
2522 $formats_nav .= '(initial)';
2523 } elsif (@
$parents == 1) {
2524 # single parent commit
2527 $cgi->a({-href
=> href
(action
=>"commit",
2529 esc_html
(substr($parent, 0, 7))) .
2536 $cgi->a({-href
=> href
(action
=>"commit",
2538 esc_html
(substr($_, 0, 7)));
2542 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
2543 $formats_nav .= " | " .
2544 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
2548 if (!defined $parent) {
2552 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
2554 (@
$parents <= 1 ?
$parent : '-c'),
2556 or die_error
(500, "Open git-diff-tree failed");
2557 @difftree = map { chomp; $_ } <$fd>;
2558 close $fd or die_error
(404, "Reading git-diff-tree failed");
2560 # non-textual hash id's can be cached
2562 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2565 my $refs = git_get_references
();
2566 my $ref = format_ref_marker
($refs, $co{'id'});
2568 git_header_html
(undef, $expires);
2569 git_print_page_nav
('commit', '',
2570 $hash, $co{'tree'}, $hash,
2573 if (defined $co{'parent'}) {
2574 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
2576 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
2578 print "<div class=\"title_text\">\n" .
2579 "<table class=\"object_header\">\n";
2580 git_print_authorship_rows
(\
%co);
2581 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
2584 "<td class=\"sha1\">" .
2585 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
2586 class => "list"}, $co{'tree'}) .
2588 "<td class=\"link\">" .
2589 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
2591 my $snapshot_links = format_snapshot_links
($hash);
2592 if (defined $snapshot_links) {
2593 print " | " . $snapshot_links;
2598 foreach my $par (@
$parents) {
2601 "<td class=\"sha1\">" .
2602 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
2603 class => "list"}, $par) .
2605 "<td class=\"link\">" .
2606 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
2608 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
2615 print "<div class=\"page_body\">\n";
2616 git_print_log
($co{'comment'});
2619 git_difftree_body
(\
@difftree, $hash, @
$parents);
2625 # object is defined by:
2626 # - hash or hash_base alone
2627 # - hash_base and file_name
2630 # - hash or hash_base alone
2631 if ($hash || ($hash_base && !defined $file_name)) {
2632 my $object_id = $hash || $hash_base;
2634 open my $fd, "-|", quote_command
(
2635 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
2636 or die_error
(404, "Object does not exist");
2640 or die_error
(404, "Object does not exist");
2642 # - hash_base and file_name
2643 } elsif ($hash_base && defined $file_name) {
2644 $file_name =~ s
,/+$,,;
2646 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
2647 or die_error
(404, "Base object does not exist");
2649 # here errors should not hapen
2650 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
2651 or die_error
(500, "Open git-ls-tree failed");
2655 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2656 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
2657 die_error
(404, "File or directory for given base does not exist");
2662 die_error
(400, "Not enough information to find object");
2665 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
2666 hash
=>$hash, hash_base
=>$hash_base,
2667 file_name
=>$file_name),
2668 -status
=> '302 Found');
2672 my $format = shift || 'html';
2679 # preparing $fd and %diffinfo for git_patchset_body
2681 if (defined $hash_base && defined $hash_parent_base) {
2682 if (defined $file_name) {
2684 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
2685 $hash_parent_base, $hash_base,
2686 "--", (defined $file_parent ?
$file_parent : ()), $file_name
2687 or die_error
(500, "Open git-diff-tree failed");
2688 @difftree = map { chomp; $_ } <$fd>;
2690 or die_error
(404, "Reading git-diff-tree failed");
2692 or die_error
(404, "Blob diff not found");
2694 } elsif (defined $hash &&
2695 $hash =~ /[0-9a-fA-F]{40}/) {
2696 # try to find filename from $hash
2698 # read filtered raw output
2699 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
2700 $hash_parent_base, $hash_base, "--"
2701 or die_error
(500, "Open git-diff-tree failed");
2703 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
2705 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
2706 map { chomp; $_ } <$fd>;
2708 or die_error
(404, "Reading git-diff-tree failed");
2710 or die_error
(404, "Blob diff not found");
2713 die_error
(400, "Missing one of the blob diff parameters");
2716 if (@difftree > 1) {
2717 die_error
(400, "Ambiguous blob diff specification");
2720 %diffinfo = parse_difftree_raw_line
($difftree[0]);
2721 $file_parent ||= $diffinfo{'from_file'} || $file_name;
2722 $file_name ||= $diffinfo{'to_file'};
2724 $hash_parent ||= $diffinfo{'from_id'};
2725 $hash ||= $diffinfo{'to_id'};
2727 # non-textual hash id's can be cached
2728 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
2729 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
2734 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
2735 '-p', ($format eq 'html' ?
"--full-index" : ()),
2736 $hash_parent_base, $hash_base,
2737 "--", (defined $file_parent ?
$file_parent : ()), $file_name
2738 or die_error
(500, "Open git-diff-tree failed");
2741 # old/legacy style URI -- not generated anymore since 1.4.3.
2743 die_error
('404 Not Found', "Missing one of the blob diff parameters")
2747 if ($format eq 'html') {
2749 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
2751 git_header_html
(undef, $expires);
2752 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
2753 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
2754 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
2756 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
2757 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
2759 if (defined $file_name) {
2760 git_print_page_path
($file_name, "blob", $hash_base);
2762 print "<div class=\"page_path\"></div>\n";
2765 } elsif ($format eq 'plain') {
2767 -type
=> 'text/plain',
2768 -charset
=> 'utf-8',
2769 -expires
=> $expires,
2770 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
2772 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
2775 die_error
(400, "Unknown blobdiff format");
2779 if ($format eq 'html') {
2780 print "<div class=\"page_body\">\n";
2782 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
2785 print "</div>\n"; # class="page_body"
2789 while (my $line = <$fd>) {
2790 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
2791 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
2795 last if $line =~ m!^\+\+\+!;
2803 sub git_blobdiff_plain
{
2804 git_blobdiff
('plain');
2807 sub git_commitdiff
{
2809 my $format = $params{-format
} || 'html';
2811 my ($patch_max) = gitweb_get_feature
('patches');
2812 if ($format eq 'patch') {
2813 die_error
(403, "Patch view not allowed") unless $patch_max;
2816 $hash ||= $hash_base || "HEAD";
2817 my %co = parse_commit
($hash)
2818 or die_error
(404, "Unknown commit object");
2820 # choose format for commitdiff for merge
2821 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
2822 $hash_parent = '--cc';
2824 # we need to prepare $formats_nav before almost any parameter munging
2826 if ($format eq 'html') {
2828 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
2830 if ($patch_max && @
{$co{'parents'}} <= 1) {
2831 $formats_nav .= " | " .
2832 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
2836 if (defined $hash_parent &&
2837 $hash_parent ne '-c' && $hash_parent ne '--cc') {
2838 # commitdiff with two commits given
2839 my $hash_parent_short = $hash_parent;
2840 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
2841 $hash_parent_short = substr($hash_parent, 0, 7);
2845 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
2846 if ($co{'parents'}[$i] eq $hash_parent) {
2847 $formats_nav .= ' parent ' . ($i+1);
2851 $formats_nav .= ': ' .
2852 $cgi->a({-href
=> href
(action
=>"commitdiff",
2853 hash
=>$hash_parent)},
2854 esc_html
($hash_parent_short)) .
2856 } elsif (!$co{'parent'}) {
2858 $formats_nav .= ' (initial)';
2859 } elsif (scalar @
{$co{'parents'}} == 1) {
2860 # single parent commit
2863 $cgi->a({-href
=> href
(action
=>"commitdiff",
2864 hash
=>$co{'parent'})},
2865 esc_html
(substr($co{'parent'}, 0, 7))) .
2869 if ($hash_parent eq '--cc') {
2870 $formats_nav .= ' | ' .
2871 $cgi->a({-href
=> href
(action
=>"commitdiff",
2872 hash
=>$hash, hash_parent
=>'-c')},
2874 } else { # $hash_parent eq '-c'
2875 $formats_nav .= ' | ' .
2876 $cgi->a({-href
=> href
(action
=>"commitdiff",
2877 hash
=>$hash, hash_parent
=>'--cc')},
2883 $cgi->a({-href
=> href
(action
=>"commitdiff",
2885 esc_html
(substr($_, 0, 7)));
2886 } @
{$co{'parents'}} ) .
2891 my $hash_parent_param = $hash_parent;
2892 if (!defined $hash_parent_param) {
2893 # --cc for multiple parents, --root for parentless
2894 $hash_parent_param =
2895 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
2901 if ($format eq 'html') {
2902 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
2903 "--no-commit-id", "--patch-with-raw", "--full-index",
2904 $hash_parent_param, $hash, "--"
2905 or die_error
(500, "Open git-diff-tree failed");
2907 while (my $line = <$fd>) {
2909 # empty line ends raw part of diff-tree output
2911 push @difftree, scalar parse_difftree_raw_line
($line);
2914 } elsif ($format eq 'plain') {
2915 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
2916 '-p', $hash_parent_param, $hash, "--"
2917 or die_error
(500, "Open git-diff-tree failed");
2918 } elsif ($format eq 'patch') {
2919 # For commit ranges, we limit the output to the number of
2920 # patches specified in the 'patches' feature.
2921 # For single commits, we limit the output to a single patch,
2922 # diverging from the git-format-patch default.
2923 my @commit_spec = ();
2925 if ($patch_max > 0) {
2926 push @commit_spec, "-$patch_max";
2928 push @commit_spec, '-n', "$hash_parent..$hash";
2930 if ($params{-single
}) {
2931 push @commit_spec, '-1';
2933 if ($patch_max > 0) {
2934 push @commit_spec, "-$patch_max";
2936 push @commit_spec, "-n";
2938 push @commit_spec, '--root', $hash;
2940 open $fd, "-|", git_cmd
(), "format-patch", @diff_opts,
2941 '--encoding=utf8', '--stdout', @commit_spec
2942 or die_error
(500, "Open git-format-patch failed");
2944 die_error
(400, "Unknown commitdiff format");
2947 # non-textual hash id's can be cached
2949 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
2953 # write commit message
2954 if ($format eq 'html') {
2955 my $refs = git_get_references
();
2956 my $ref = format_ref_marker
($refs, $co{'id'});
2958 git_header_html
(undef, $expires);
2959 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
2960 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
2961 print "<div class=\"title_text\">\n" .
2962 "<table class=\"object_header\">\n";
2963 git_print_authorship_rows
(\
%co);
2966 print "<div class=\"page_body\">\n";
2967 if (@
{$co{'comment'}} > 1) {
2968 print "<div class=\"log\">\n";
2969 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
2970 print "</div>\n"; # class="log"
2973 } elsif ($format eq 'plain') {
2974 my $refs = git_get_references
("tags");
2975 my $tagname = git_get_rev_name_tags
($hash);
2976 my $filename = basename
($project) . "-$hash.patch";
2979 -type
=> 'text/plain',
2980 -charset
=> 'utf-8',
2981 -expires
=> $expires,
2982 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
2983 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
2984 print "From: " . to_utf8
($co{'author'}) . "\n";
2985 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
2986 print "Subject: " . to_utf8
($co{'title'}) . "\n";
2988 print "X-Git-Tag: $tagname\n" if $tagname;
2989 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
2991 foreach my $line (@
{$co{'comment'}}) {
2992 print to_utf8
($line) . "\n";
2995 } elsif ($format eq 'patch') {
2996 my $filename = basename
($project) . "-$hash.patch";
2999 -type
=> 'text/plain',
3000 -charset
=> 'utf-8',
3001 -expires
=> $expires,
3002 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
3006 if ($format eq 'html') {
3007 my $use_parents = !defined $hash_parent ||
3008 $hash_parent eq '-c' || $hash_parent eq '--cc';
3009 git_difftree_body
(\
@difftree, $hash,
3010 $use_parents ? @
{$co{'parents'}} : $hash_parent);
3013 git_patchset_body
($fd, \
@difftree, $hash,
3014 $use_parents ? @
{$co{'parents'}} : $hash_parent);
3016 print "</div>\n"; # class="page_body"
3019 } elsif ($format eq 'plain') {
3023 or print "Reading git-diff-tree failed\n";
3024 } elsif ($format eq 'patch') {
3028 or print "Reading git-format-patch failed\n";
3032 sub git_commitdiff_plain
{
3033 git_commitdiff
(-format
=> 'plain');
3036 # format-patch-style patches
3038 git_commitdiff
(-format
=> 'patch', -single
=> 1);
3042 git_commitdiff
(-format
=> 'patch');
3046 git_log_generic
('history', \
&git_history_body
,
3047 $hash_base, $hash_parent_base,
3052 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
3053 if (!defined $searchtext) {
3054 die_error
(400, "Text field is empty");
3056 if (!defined $hash) {
3057 $hash = git_get_head_hash
($project);
3059 my %co = parse_commit
($hash);
3061 die_error
(404, "Unknown commit object");
3063 if (!defined $page) {
3067 $searchtype ||= 'commit';
3068 if ($searchtype eq 'pickaxe') {
3069 # pickaxe may take all resources of your box and run for several minutes
3070 # with every query - so decide by yourself how public you make this feature
3071 gitweb_check_feature
('pickaxe')
3072 or die_error
(403, "Pickaxe is disabled");
3074 if ($searchtype eq 'grep') {
3075 gitweb_check_feature
('grep')
3076 or die_error
(403, "Grep is disabled");
3081 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
3083 if ($searchtype eq 'commit') {
3084 $greptype = "--grep=";
3085 } elsif ($searchtype eq 'author') {
3086 $greptype = "--author=";
3087 } elsif ($searchtype eq 'committer') {
3088 $greptype = "--committer=";
3090 $greptype .= $searchtext;
3091 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
3092 $greptype, '--regexp-ignore-case',
3093 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
3095 my $paging_nav = '';
3098 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
3099 searchtext
=>$searchtext,
3100 searchtype
=>$searchtype)},
3102 $paging_nav .= " ⋅ " .
3103 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
3104 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
3106 $paging_nav .= "first";
3107 $paging_nav .= " ⋅ prev";
3110 if ($#commitlist >= 100) {
3112 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
3113 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
3114 $paging_nav .= " ⋅ $next_link";
3116 $paging_nav .= " ⋅ next";
3119 if ($#commitlist >= 100) {
3122 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
3123 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
3124 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
3127 if ($searchtype eq 'pickaxe') {
3128 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
3129 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
3131 print "<table class=\"pickaxe search\">\n";
3134 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
3135 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
3136 ($search_use_regexp ?
'--pickaxe-regex' : ());
3139 while (my $line = <$fd>) {
3143 my %set = parse_difftree_raw_line
($line);
3144 if (defined $set{'commit'}) {
3145 # finish previous commit
3148 "<td class=\"link\">" .
3149 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
3151 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
3157 print "<tr class=\"dark\">\n";
3159 print "<tr class=\"light\">\n";
3162 %co = parse_commit
($set{'commit'});
3163 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
3164 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3165 "<td><i>$author</i></td>\n" .
3167 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
3168 -class => "list subject"},
3169 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
3170 } elsif (defined $set{'to_id'}) {
3171 next if ($set{'to_id'} =~ m/^0{40}$/);
3173 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
3174 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
3176 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
3182 # finish last commit (warning: repetition!)
3185 "<td class=\"link\">" .
3186 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
3188 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
3196 if ($searchtype eq 'grep') {
3197 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
3198 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
3200 print "<table class=\"grep_search\">\n";
3204 open my $fd, "-|", git_cmd
(), 'grep', '-n',
3205 $search_use_regexp ?
('-E', '-i') : '-F',
3206 $searchtext, $co{'tree'};
3208 while (my $line = <$fd>) {
3210 my ($file, $lno, $ltext, $binary);
3211 last if ($matches++ > 1000);
3212 if ($line =~ /^Binary file (.+) matches$/) {
3216 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
3218 if ($file ne $lastfile) {
3219 $lastfile and print "</td></tr>\n";
3221 print "<tr class=\"dark\">\n";
3223 print "<tr class=\"light\">\n";
3225 print "<td class=\"list\">".
3226 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
3227 file_name
=>"$file"),
3228 -class => "list"}, esc_path
($file));
3229 print "</td><td>\n";
3233 print "<div class=\"binary\">Binary file</div>\n";
3235 $ltext = untabify
($ltext);
3236 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
3237 $ltext = esc_html
($1, -nbsp
=>1);
3238 $ltext .= '<span class="match">';
3239 $ltext .= esc_html
($2, -nbsp
=>1);
3240 $ltext .= '</span>';
3241 $ltext .= esc_html
($3, -nbsp
=>1);
3243 $ltext = esc_html
($ltext, -nbsp
=>1);
3245 print "<div class=\"pre\">" .
3246 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
3247 file_name
=>"$file").'#l'.$lno,
3248 -class => "linenr"}, sprintf('%4i', $lno))
3249 . ' ' . $ltext . "</div>\n";
3253 print "</td></tr>\n";
3254 if ($matches > 1000) {
3255 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
3258 print "<div class=\"diff nodifferences\">No matches found</div>\n";
3267 sub git_search_help
{
3269 git_print_page_nav
('','', $hash,$hash,$hash);
3271 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
3272 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
3273 the pattern entered is recognized as the POSIX extended
3274 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
3277 <dt><b>commit</b></dt>
3278 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
3280 my $have_grep = gitweb_check_feature
('grep');
3283 <dt><b>grep</b></dt>
3284 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
3285 a different one) are searched for the given pattern. On large trees, this search can take
3286 a while and put some strain on the server, so please use it with some consideration. Note that
3287 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
3288 case-sensitive.</dd>
3292 <dt><b>author</b></dt>
3293 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
3294 <dt><b>committer</b></dt>
3295 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
3297 my $have_pickaxe = gitweb_check_feature
('pickaxe');
3298 if ($have_pickaxe) {
3300 <dt><b>pickaxe</b></dt>
3301 <dd>All commits that caused the string to appear or disappear from any file (changes that
3302 added, removed or "modified" the string) will be listed. This search can take a while and
3303 takes a lot of strain on the server, so please use it wisely. Note that since you may be
3304 interested even in changes just changing the case as well, this search is case sensitive.</dd>
3312 git_log_generic
('shortlog', \
&git_shortlog_body
,
3313 $hash, $hash_parent);
3316 ## ======================================================================
3317 ## ======================================================================
3321 if (-f
$projects_list) {
3323 git_print_page_nav
('addrepo');
3325 open my $fd, '<', $projects_list or return;
3328 git_print_header_div
();
3329 print "<div class=\"page_body\">";
3330 print "<form action=\"$my_url\" method=\"post\"><br/>";
3331 print "<table><tr class=\"dark\"><td>";
3332 print "Repository path for \$project_list: </td><td><input style=\"width:400px;\" type=\"text\" name=\"path\"/>";
3333 print "</td></tr><tr class=\"light\"><td align=\"center\" colspan=\"2\">";
3334 print "<input type=\"submit\" value=\"Add repository\" name=\"sf\"/>";
3335 print "</td></tr></table></form></div>";
3338 die_error
(404, "Needed a static file with list of repositories (\$project_list)");
3342 ## ......................................................................
3343 ## feeds (RSS, Atom; OPML)
3346 my $format = shift || 'atom';
3347 my $have_blame = gitweb_check_feature
('blame');
3349 # Atom: http://www.atomenabled.org/developers/syndication/
3350 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3351 if ($format ne 'rss' && $format ne 'atom') {
3352 die_error
(400, "Unknown web feed format");
3355 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
3356 my $head = $hash || 'HEAD';
3357 my @commitlist = parse_commits
($head, 150, 0, $file_name);
3361 my $content_type = "application/$format+xml";
3362 if (defined $cgi->http('HTTP_ACCEPT') &&
3363 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
3364 # browser (feed reader) prefers text/xml
3365 $content_type = 'text/xml';
3367 if (defined($commitlist[0])) {
3368 %latest_commit = %{$commitlist[0]};
3369 my $latest_epoch = $latest_commit{'committer_epoch'};
3370 %latest_date = parse_date
($latest_epoch);
3371 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
3372 if (defined $if_modified) {
3374 if (eval { require HTTP
::Date
; 1; }) {
3375 $since = HTTP
::Date
::str2time
($if_modified);
3376 } elsif (eval { require Time
::ParseDate
; 1; }) {
3377 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
3379 if (defined $since && $latest_epoch <= $since) {
3381 -type
=> $content_type,
3382 -charset
=> 'utf-8',
3383 -last_modified
=> $latest_date{'rfc2822'},
3384 -status
=> '304 Not Modified');
3389 -type
=> $content_type,
3390 -charset
=> 'utf-8',
3391 -last_modified
=> $latest_date{'rfc2822'});
3394 -type
=> $content_type,
3395 -charset
=> 'utf-8');
3398 # Optimization: skip generating the body if client asks only
3399 # for Last-Modified date.
3400 return if ($cgi->request_method() eq 'HEAD');
3403 my $title = "$site_name - $project/$action";
3404 my $feed_type = 'log';
3405 if (defined $hash) {
3406 $title .= " - '$hash'";
3407 $feed_type = 'branch log';
3408 if (defined $file_name) {
3409 $title .= " :: $file_name";
3410 $feed_type = 'history';
3412 } elsif (defined $file_name) {
3413 $title .= " - $file_name";
3414 $feed_type = 'history';
3416 $title .= " $feed_type";
3417 my $descr = git_get_project_description
($project);
3418 if (defined $descr) {
3419 $descr = esc_html
($descr);
3421 $descr = "$project " .
3422 ($format eq 'rss' ?
'RSS' : 'Atom') .
3425 my $owner = git_get_project_owner
($project);
3426 $owner = esc_html
($owner);
3430 if (defined $file_name) {
3431 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
3432 } elsif (defined $hash) {
3433 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
3435 $alt_url = href
(-full
=>1, action
=>"summary");
3437 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
3438 if ($format eq 'rss') {
3440 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
3443 print "<title>$title</title>\n" .
3444 "<link>$alt_url</link>\n" .
3445 "<description>$descr</description>\n" .
3446 "<language>en</language>\n" .
3447 # project owner is responsible for 'editorial' content
3448 "<managingEditor>$owner</managingEditor>\n";
3449 if (defined $logo || defined $favicon) {
3450 # prefer the logo to the favicon, since RSS
3451 # doesn't allow both
3452 my $img = esc_url
($logo || $favicon);
3454 "<url>$img</url>\n" .
3455 "<title>$title</title>\n" .
3456 "<link>$alt_url</link>\n" .
3460 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
3461 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
3463 print "<generator>gitweb v.$version/$git_version</generator>\n";
3464 } elsif ($format eq 'atom') {
3466 <feed xmlns="http://www.w3.org/2005/Atom">
3468 print "<title>$title</title>\n" .
3469 "<subtitle>$descr</subtitle>\n" .
3470 '<link rel="alternate" type="text/html" href="' .
3471 $alt_url . '" />' . "\n" .
3472 '<link rel="self" type="' . $content_type . '" href="' .
3473 $cgi->self_url() . '" />' . "\n" .
3474 "<id>" . href
(-full
=>1) . "</id>\n" .
3475 # use project owner for feed author
3476 "<author><name>$owner</name></author>\n";
3477 if (defined $favicon) {
3478 print "<icon>" . esc_url
($favicon) . "</icon>\n";
3480 if (defined $logo_url) {
3481 # not twice as wide as tall: 72 x 27 pixels
3482 print "<logo>" . esc_url
($logo) . "</logo>\n";
3484 if (! %latest_date) {
3485 # dummy date to keep the feed valid until commits trickle in:
3486 print "<updated>1970-01-01T00:00:00Z</updated>\n";
3488 print "<updated>$latest_date{'iso-8601'}</updated>\n";
3490 print "<generator version='$version/$git_version'>gitweb</generator>\n";
3494 for (my $i = 0; $i <= $#commitlist; $i++) {
3495 my %co = %{$commitlist[$i]};
3496 my $commit = $co{'id'};
3497 # we read 150, we always show 30 and the ones more recent than 48 hours
3498 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
3501 my %cd = parse_date
($co{'author_epoch'});
3503 # get list of changed files
3504 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
3505 $co{'parent'} || "--root",
3506 $co{'id'}, "--", (defined $file_name ?
$file_name : ())
3508 my @difftree = map { chomp; $_ } <$fd>;
3512 # print element (entry, item)
3513 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
3514 if ($format eq 'rss') {
3516 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
3517 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
3518 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
3519 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
3520 "<link>$co_url</link>\n" .
3521 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
3522 "<content:encoded>" .
3524 } elsif ($format eq 'atom') {
3526 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
3527 "<updated>$cd{'iso-8601'}</updated>\n" .
3529 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
3530 if ($co{'author_email'}) {
3531 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
3533 print "</author>\n" .
3534 # use committer for contributor
3536 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
3537 if ($co{'committer_email'}) {
3538 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
3540 print "</contributor>\n" .
3541 "<published>$cd{'iso-8601'}</published>\n" .
3542 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
3543 "<id>$co_url</id>\n" .
3544 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
3545 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
3547 my $comment = $co{'comment'};
3549 foreach my $line (@
$comment) {
3550 $line = esc_html
($line);
3553 print "</pre><ul>\n";
3554 foreach my $difftree_line (@difftree) {
3555 my %difftree = parse_difftree_raw_line
($difftree_line);
3556 next if !$difftree{'from_id'};
3558 my $file = $difftree{'file'} || $difftree{'to_file'};
3562 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
3563 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
3564 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
3565 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
3566 -title
=> "diff"}, 'D');
3568 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
3569 file_name
=>$file, hash_base
=>$commit),
3570 -title
=> "blame"}, 'B');
3572 # if this is not a feed of a file history
3573 if (!defined $file_name || $file_name ne $file) {
3574 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
3575 file_name
=>$file, hash
=>$commit),
3576 -title
=> "history"}, 'H');
3578 $file = esc_path
($file);
3582 if ($format eq 'rss') {
3583 print "</ul>]]>\n" .
3584 "</content:encoded>\n" .
3586 } elsif ($format eq 'atom') {
3587 print "</ul>\n</div>\n" .
3594 if ($format eq 'rss') {
3595 print "</channel>\n</rss>\n";
3596 } elsif ($format eq 'atom') {
3610 my @list = git_get_projects_list
();
3613 -type
=> 'text/xml',
3614 -charset
=> 'utf-8',
3615 -content_disposition
=> 'inline; filename="opml.xml"');
3618 <?xml version="1.0" encoding="utf-8"?>
3619 <opml version="1.0">
3621 <title>$site_name OPML Export</title>
3624 <outline text="git RSS feeds">
3627 foreach my $pr (@list) {
3629 my $head = git_get_head_hash
($proj{'path'});
3630 if (!defined $head) {
3633 $git_dir = "$projectroot/$proj{'path'}";
3634 my %co = parse_commit
($head);
3639 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
3640 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
3641 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
3642 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";