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