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
12 use CGI
qw(:standard :escapeHTML -nosticky);
13 use CGI
::Util
qw(unescape);
14 use CGI
::Carp
qw(fatalsToBrowser);
18 use File
::Basename
qw(basename);
19 binmode STDOUT
, ':utf8';
22 CGI
->compile() if $ENV{'MOD_PERL'};
26 our $version = "debian.1.5.6.1.19.ge6b2";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute
=> 1);
30 # core git executable to use
31 # this can just be "git" if your webserver has a sensible PATH
32 our $GIT = "/usr/bin/git";
34 # absolute fs-path which will be prepended to the project path
35 #our $projectroot = "/pub/scm";
36 our $projectroot = "/pub/git";
38 # fs traversing limit for getting project list
39 # the number is relative to the projectroot
40 our $project_maxdepth = 2007;
42 # target of the home link on top of all pages
43 our $home_link = $my_uri || "/";
45 # string of the home link on top of all pages
46 # hcoop-change: Customize.
47 our $home_link_str = "HCoop";
49 # name of your site or organization to appear in page titles
50 # replace this with something more descriptive for clearer bookmarks
51 # hcoop-change: Customize.
52 our $site_name = "HCoop Git"
53 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
55 # filename of html text to include at top of each page
56 our $site_header = "";
57 # html text to include at home page
58 our $home_text = "indextext.html";
59 # filename of html text to include at bottom of each page
60 our $site_footer = "";
61 # filename of cached version of front page
62 our $cached_front_page = "/var/local/lib/gitweb/indexcache.html";
65 our @stylesheets = ("gitweb.css");
66 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
67 our $stylesheet = undef;
68 # URI of GIT logo (72x27 size)
69 our $logo = "git-logo.png";
70 # URI of GIT favicon, assumed to be image/png type
71 our $favicon = "git-favicon.png";
73 # URI and label (title) of GIT logo link
74 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
75 #our $logo_label = "git documentation";
76 our $logo_url = "http://git.or.cz/";
77 our $logo_label = "git homepage";
79 # source of projects list
80 our $projects_list = "";
82 # the width (in characters) of the projects list "Description" column
83 our $projects_list_description_width = 25;
85 # default order of projects list
86 # valid values are none, project, descr, owner, and age
87 our $default_projects_order = "project";
89 # show repository only if this file exists
90 # (only effective if this variable evaluates to true)
93 # only allow viewing of repositories also shown on the overview page
94 our $strict_export = "";
96 # list of git base URLs used for URL to where fetch project from,
97 # i.e. full URL is "$git_base_url/$project"
98 # hcoop-change: We like several URLs.
99 #our @git_base_url_list = grep { $_ ne '' } ("");
100 our @git_base_url_list = ('git://git.hcoop.net/git',
101 'http://git.hcoop.net/git');
103 # default blob_plain mimetype and default charset for text/plain blob
104 our $default_blob_plain_mimetype = 'text/plain';
105 our $default_text_plain_charset = undef;
107 # file to use for guessing MIME types before trying /etc/mime.types
108 # (relative to the current git repository)
109 our $mimetypes_file = undef;
111 # assume this charset if line contains non-UTF-8 characters;
112 # it should be valid encoding (see Encoding::Supported(3pm) for list),
113 # for which encoding all byte sequences are valid, for example
114 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
115 # could be even 'utf-8' for the old behavior)
116 our $fallback_encoding = 'latin1';
118 # rename detection options for git-diff and git-diff-tree
119 # - default is '-M', with the cost proportional to
120 # (number of removed files) * (number of new files).
121 # - more costly is '-C' (which implies '-M'), with the cost proportional to
122 # (number of changed files + number of removed files) * (number of new files)
123 # - even more costly is '-C', '--find-copies-harder' with cost
124 # (number of files in the original tree) * (number of new files)
125 # - one might want to include '-B' option, e.g. '-B', '-M'
126 our @diff_opts = ('-M'); # taken from git_commit
128 # information about snapshot formats that gitweb is capable of serving
129 our %known_snapshot_formats = (
131 # 'display' => display name,
132 # 'type' => mime type,
133 # 'suffix' => filename suffix,
134 # 'format' => --format for git-archive,
135 # 'compressor' => [compressor command and arguments]
136 # (array reference, optional)}
139 'display' => 'tar.gz',
140 'type' => 'application/x-gzip',
141 'suffix' => '.tar.gz',
143 'compressor' => ['gzip']},
146 'display' => 'tar.bz2',
147 'type' => 'application/x-bzip2',
148 'suffix' => '.tar.bz2',
150 'compressor' => ['bzip2']},
154 'type' => 'application/x-zip',
159 # Aliases so we understand old gitweb.snapshot values in repository
161 our %known_snapshot_format_aliases = (
165 # backward compatibility: legacy gitweb config support
166 'x-gzip' => undef, 'gz' => undef,
167 'x-bzip2' => undef, 'bz2' => undef,
168 'x-zip' => undef, '' => undef,
171 # You define site-wide feature defaults here; override them with
172 # $GITWEB_CONFIG as necessary.
175 # 'sub' => feature-sub (subroutine),
176 # 'override' => allow-override (boolean),
177 # 'default' => [ default options...] (array reference)}
179 # if feature is overridable (it means that allow-override has true value),
180 # then feature-sub will be called with default options as parameters;
181 # return value of feature-sub indicates if to enable specified feature
183 # if there is no 'sub' key (no feature-sub), then feature cannot be
186 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
188 # Enable the 'blame' blob view, showing the last commit that modified
189 # each line in the file. This can be very CPU-intensive.
191 # To enable system wide have in $GITWEB_CONFIG
192 # $feature{'blame'}{'default'} = [1];
193 # To have project specific config enable override in $GITWEB_CONFIG
194 # $feature{'blame'}{'override'} = 1;
195 # and in project config gitweb.blame = 0|1;
197 'sub' => \
&feature_blame
,
201 # Enable the 'snapshot' link, providing a compressed archive of any
202 # tree. This can potentially generate high traffic if you have large
205 # Value is a list of formats defined in %known_snapshot_formats that
207 # To disable system wide have in $GITWEB_CONFIG
208 # $feature{'snapshot'}{'default'} = [];
209 # To have project specific config enable override in $GITWEB_CONFIG
210 # $feature{'snapshot'}{'override'} = 1;
211 # and in project config, a comma-separated list of formats or "none"
212 # to disable. Example: gitweb.snapshot = tbz2,zip;
214 'sub' => \
&feature_snapshot
,
216 'default' => ['tgz']},
218 # Enable text search, which will list the commits which match author,
219 # committer or commit text to a given string. Enabled by default.
220 # Project specific override is not supported.
225 # Enable grep search, which will list the files in currently selected
226 # tree containing the given string. Enabled by default. This can be
227 # potentially CPU-intensive, of course.
229 # To enable system wide have in $GITWEB_CONFIG
230 # $feature{'grep'}{'default'} = [1];
231 # To have project specific config enable override in $GITWEB_CONFIG
232 # $feature{'grep'}{'override'} = 1;
233 # and in project config gitweb.grep = 0|1;
238 # Enable the pickaxe search, which will list the commits that modified
239 # a given string in a file. This can be practical and quite faster
240 # alternative to 'blame', but still potentially CPU-intensive.
242 # To enable system wide have in $GITWEB_CONFIG
243 # $feature{'pickaxe'}{'default'} = [1];
244 # To have project specific config enable override in $GITWEB_CONFIG
245 # $feature{'pickaxe'}{'override'} = 1;
246 # and in project config gitweb.pickaxe = 0|1;
248 'sub' => \
&feature_pickaxe
,
252 # Make gitweb use an alternative format of the URLs which can be
253 # more readable and natural-looking: project name is embedded
254 # directly in the path and the query string contains other
255 # auxiliary information. All gitweb installations recognize
256 # URL in either format; this configures in which formats gitweb
259 # To enable system wide have in $GITWEB_CONFIG
260 # $feature{'pathinfo'}{'default'} = [1];
261 # Project specific override is not supported.
263 # Note that you will need to change the default location of CSS,
264 # favicon, logo and possibly other files to an absolute URL. Also,
265 # if gitweb.cgi serves as your indexfile, you will need to force
266 # $my_uri to contain the script name in your $GITWEB_CONFIG.
271 # Make gitweb consider projects in project root subdirectories
272 # to be forks of existing projects. Given project $projname.git,
273 # projects matching $projname/*.git will not be shown in the main
274 # projects list, instead a '+' mark will be added to $projname
275 # there and a 'forks' view will be enabled for the project, listing
276 # all the forks. If project list is taken from a file, forks have
277 # to be listed after the main project.
279 # To enable system wide have in $GITWEB_CONFIG
280 # $feature{'forks'}{'default'} = [1];
281 # Project specific override is not supported.
287 sub gitweb_check_feature
{
289 return unless exists $feature{$name};
290 my ($sub, $override, @defaults) = (
291 $feature{$name}{'sub'},
292 $feature{$name}{'override'},
293 @
{$feature{$name}{'default'}});
294 if (!$override) { return @defaults; }
296 warn "feature $name is not overrideable";
299 return $sub->(@defaults);
303 my ($val) = git_get_project_config
('blame', '--bool');
305 if ($val eq 'true') {
307 } elsif ($val eq 'false') {
314 sub feature_snapshot
{
317 my ($val) = git_get_project_config
('snapshot');
320 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
327 my ($val) = git_get_project_config
('grep', '--bool');
329 if ($val eq 'true') {
331 } elsif ($val eq 'false') {
338 sub feature_pickaxe
{
339 my ($val) = git_get_project_config
('pickaxe', '--bool');
341 if ($val eq 'true') {
343 } elsif ($val eq 'false') {
350 # checking HEAD file with -e is fragile if the repository was
351 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
353 sub check_head_link
{
355 my $headfile = "$dir/HEAD";
356 return ((-e
$headfile) ||
357 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
360 sub check_export_ok
{
362 return (check_head_link
($dir) &&
363 (!$export_ok || -e
"$dir/$export_ok"));
366 # process alternate names for backward compatibility
367 # filter out unsupported (unknown) snapshot formats
368 sub filter_snapshot_fmts
{
372 exists $known_snapshot_format_aliases{$_} ?
373 $known_snapshot_format_aliases{$_} : $_} @fmts;
374 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
378 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl";
379 if (-e
$GITWEB_CONFIG) {
382 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "/etc/gitweb.conf";
383 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
386 # version of the core git binary
387 our $git_version = qx("$GIT" --version
) =~ m/git version (.*)$/ ?
$1 : "unknown";
389 $projects_list ||= $projectroot;
391 # ======================================================================
392 # input validation and dispatch
393 our $action = $cgi->param('a');
394 if (defined $action) {
395 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
396 die_error
(undef, "Invalid action parameter");
400 # parameters which are pathnames
401 our $project = $cgi->param('p');
402 if (defined $project) {
403 if (!validate_pathname
($project) ||
404 !(-d
"$projectroot/$project") ||
405 !check_head_link
("$projectroot/$project") ||
406 ($export_ok && !(-e
"$projectroot/$project/$export_ok")) ||
407 ($strict_export && !project_in_list
($project))) {
409 die_error
(undef, "No such project");
413 our $file_name = $cgi->param('f');
414 if (defined $file_name) {
415 if (!validate_pathname
($file_name)) {
416 die_error
(undef, "Invalid file parameter");
420 our $file_parent = $cgi->param('fp');
421 if (defined $file_parent) {
422 if (!validate_pathname
($file_parent)) {
423 die_error
(undef, "Invalid file parent parameter");
427 sub git_send_cached_front_page
{
428 my $status = shift || "200 OK";
431 # require explicit support from the UA if we are to send the page as
432 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
433 # we have to do this because MSIE sometimes globs '*/*', pretending to
434 # support xhtml+xml but choking when it gets what it asked for.
435 if (defined $cgi->http('HTTP_ACCEPT') &&
436 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
437 $cgi->Accept('application/xhtml+xml') != 0) {
438 $content_type = 'application/xhtml+xml';
440 $content_type = 'text/html';
442 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
443 -status
=> $status, -expires
=> $expires);
444 open (my $fd, $cached_front_page);
450 # Display a cached page if no parameters were provided and the
451 # "nocache" parameter was not passed.
452 our $nocache = $cgi->param('nocache');
453 our @params = $cgi->Vars;
455 if ($cached_front_page && ! defined $nocache && scalar @params == 0
456 && -f
$cached_front_page && -r
$cached_front_page) {
457 git_send_cached_front_page
();
460 # parameters which are refnames
461 our $hash = $cgi->param('h');
463 if (!validate_refname
($hash)) {
464 die_error
(undef, "Invalid hash parameter");
468 our $hash_parent = $cgi->param('hp');
469 if (defined $hash_parent) {
470 if (!validate_refname
($hash_parent)) {
471 die_error
(undef, "Invalid hash parent parameter");
475 our $hash_base = $cgi->param('hb');
476 if (defined $hash_base) {
477 if (!validate_refname
($hash_base)) {
478 die_error
(undef, "Invalid hash base parameter");
482 my %allowed_options = (
483 "--no-merges" => [ qw(rss atom log shortlog history) ],
486 our @extra_options = $cgi->param('opt');
487 if (defined @extra_options) {
488 foreach my $opt (@extra_options) {
489 if (not exists $allowed_options{$opt}) {
490 die_error
(undef, "Invalid option parameter");
492 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
493 die_error
(undef, "Invalid option parameter for this action");
498 our $hash_parent_base = $cgi->param('hpb');
499 if (defined $hash_parent_base) {
500 if (!validate_refname
($hash_parent_base)) {
501 die_error
(undef, "Invalid hash parent base parameter");
506 our $page = $cgi->param('pg');
508 if ($page =~ m/[^0-9]/) {
509 die_error
(undef, "Invalid page parameter");
513 our $searchtype = $cgi->param('st');
514 if (defined $searchtype) {
515 if ($searchtype =~ m/[^a-z]/) {
516 die_error
(undef, "Invalid searchtype parameter");
520 our $search_use_regexp = $cgi->param('sr');
522 our $searchtext = $cgi->param('s');
524 if (defined $searchtext) {
525 if (length($searchtext) < 2) {
526 die_error
(undef, "At least two characters are required for search parameter");
528 $search_regexp = $search_use_regexp ?
$searchtext : quotemeta $searchtext;
531 # now read PATH_INFO and use it as alternative to parameters
532 sub evaluate_path_info
{
533 return if defined $project;
534 my $path_info = $ENV{"PATH_INFO"};
535 return if !$path_info;
536 $path_info =~ s
,^/+,,;
537 return if !$path_info;
538 # find which part of PATH_INFO is project
539 $project = $path_info;
541 while ($project && !check_head_link
("$projectroot/$project")) {
542 $project =~ s
,/*[^/]*$,,;
545 $project = validate_pathname
($project);
547 ($export_ok && !-e
"$projectroot/$project/$export_ok") ||
548 ($strict_export && !project_in_list
($project))) {
552 # do not change any parameters if an action is given using the query string
554 $path_info =~ s
,^\Q
$project\E
/*,,;
555 my ($refname, $pathname) = split(/:/, $path_info, 2);
556 if (defined $pathname) {
557 # we got "project.git/branch:filename" or "project.git/branch:dir/"
558 # we could use git_get_type(branch:pathname), but it needs $git_dir
559 $pathname =~ s
,^/+,,;
560 if (!$pathname || substr($pathname, -1) eq "/") {
564 $action ||= "blob_plain";
566 $hash_base ||= validate_refname
($refname);
567 $file_name ||= validate_pathname
($pathname);
568 } elsif (defined $refname) {
569 # we got "project.git/branch"
570 $action ||= "shortlog";
571 $hash ||= validate_refname
($refname);
574 evaluate_path_info
();
576 # path to the current git repository
578 $git_dir = "$projectroot/$project" if $project;
582 "blame" => \
&git_blame2
,
583 "blobdiff" => \
&git_blobdiff
,
584 "blobdiff_plain" => \
&git_blobdiff_plain
,
585 "blob" => \
&git_blob
,
586 "blob_plain" => \
&git_blob_plain
,
587 "commitdiff" => \
&git_commitdiff
,
588 "commitdiff_plain" => \
&git_commitdiff_plain
,
589 "commit" => \
&git_commit
,
590 "forks" => \
&git_forks
,
591 "heads" => \
&git_heads
,
592 "history" => \
&git_history
,
595 "atom" => \
&git_atom
,
596 "search" => \
&git_search
,
597 "search_help" => \
&git_search_help
,
598 "shortlog" => \
&git_shortlog
,
599 "summary" => \
&git_summary
,
601 "tags" => \
&git_tags
,
602 "tree" => \
&git_tree
,
603 "snapshot" => \
&git_snapshot
,
604 "object" => \
&git_object
,
605 # those below don't need $project
606 "opml" => \
&git_opml
,
607 "project_list" => \
&git_project_list
,
608 "project_index" => \
&git_project_index
,
611 if (!defined $action) {
613 $action = git_get_type
($hash);
614 } elsif (defined $hash_base && defined $file_name) {
615 $action = git_get_type
("$hash_base:$file_name");
616 } elsif (defined $project) {
619 $action = 'project_list';
622 if (!defined($actions{$action})) {
623 die_error
(undef, "Unknown action");
625 if ($action !~ m/^(opml|project_list|project_index)$/ &&
627 die_error
(undef, "Project needed");
629 $actions{$action}->();
632 ## ======================================================================
637 # default is to use -absolute url() i.e. $my_uri
638 my $href = $params{-full
} ?
$my_url : $my_uri;
640 # XXX: Warning: If you touch this, check the search form for updating,
651 hash_parent_base
=> "hpb",
656 snapshot_format
=> "sf",
657 extra_options
=> "opt",
658 search_use_regexp
=> "sr",
660 my %mapping = @mapping;
662 $params{'project'} = $project unless exists $params{'project'};
664 if ($params{-replay
}) {
665 while (my ($name, $symbol) = each %mapping) {
666 if (!exists $params{$name}) {
667 # to allow for multivalued params we use arrayref form
668 $params{$name} = [ $cgi->param($symbol) ];
673 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
675 # use PATH_INFO for project name
676 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
677 delete $params{'project'};
679 # Summary just uses the project path URL
680 if (defined $params{'action'} && $params{'action'} eq 'summary') {
681 delete $params{'action'};
685 # now encode the parameters explicitly
687 for (my $i = 0; $i < @mapping; $i += 2) {
688 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
689 if (defined $params{$name}) {
690 if (ref($params{$name}) eq "ARRAY") {
691 foreach my $par (@
{$params{$name}}) {
692 push @result, $symbol . "=" . esc_param
($par);
695 push @result, $symbol . "=" . esc_param
($params{$name});
699 $href .= "?" . join(';', @result) if scalar @result;
705 ## ======================================================================
706 ## validation, quoting/unquoting and escaping
708 sub validate_pathname
{
709 my $input = shift || return undef;
711 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
712 # at the beginning, at the end, and between slashes.
713 # also this catches doubled slashes
714 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
718 if ($input =~ m!\0!) {
724 sub validate_refname
{
725 my $input = shift || return undef;
727 # textual hashes are O.K.
728 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
731 # it must be correct pathname
732 $input = validate_pathname
($input)
734 # restrictions on ref name according to git-check-ref-format
735 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
741 # decode sequences of octets in utf8 into Perl's internal form,
742 # which is utf-8 with utf8 flag set if needed. gitweb writes out
743 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
746 if (utf8
::valid
($str)) {
750 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
754 # quote unsafe chars, but keep the slash, even when it's not
755 # correct, but quoted slashes look too horrible in bookmarks
758 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
764 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
767 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
773 # replace invalid utf8 character with SUBSTITUTION sequence
778 $str = to_utf8
($str);
779 $str = $cgi->escapeHTML($str);
780 if ($opts{'-nbsp'}) {
781 $str =~ s/ / /g;
783 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
787 # quote control characters and escape filename to HTML
792 $str = to_utf8
($str);
793 $str = $cgi->escapeHTML($str);
794 if ($opts{'-nbsp'}) {
795 $str =~ s/ / /g;
797 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
801 # Make control characters "printable", using character escape codes (CEC)
805 my %es = ( # character escape codes, aka escape sequences
806 "\t" => '\t', # tab (HT)
807 "\n" => '\n', # line feed (LF)
808 "\r" => '\r', # carrige return (CR)
809 "\f" => '\f', # form feed (FF)
810 "\b" => '\b', # backspace (BS)
811 "\a" => '\a', # alarm (bell) (BEL)
812 "\e" => '\e', # escape (ESC)
813 "\013" => '\v', # vertical tab (VT)
814 "\000" => '\0', # nul character (NUL)
816 my $chr = ( (exists $es{$cntrl})
818 : sprintf('\%03o', ord($cntrl)) );
819 if ($opts{-nohtml
}) {
822 return "<span class=\"cntrl\">$chr</span>";
826 # Alternatively use unicode control pictures codepoints,
827 # Unicode "printable representation" (PR)
832 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
833 if ($opts{-nohtml
}) {
836 return "<span class=\"cntrl\">$chr</span>";
840 # git may return quoted and escaped filenames
846 my %es = ( # character escape codes, aka escape sequences
847 't' => "\t", # tab (HT, TAB)
848 'n' => "\n", # newline (NL)
849 'r' => "\r", # return (CR)
850 'f' => "\f", # form feed (FF)
851 'b' => "\b", # backspace (BS)
852 'a' => "\a", # alarm (bell) (BEL)
853 'e' => "\e", # escape (ESC)
854 'v' => "\013", # vertical tab (VT)
857 if ($seq =~ m/^[0-7]{1,3}$/) {
858 # octal char sequence
859 return chr(oct($seq));
860 } elsif (exists $es{$seq}) {
861 # C escape sequence, aka character escape code
864 # quoted ordinary character
868 if ($str =~ m/^"(.*)"$/) {
871 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
876 # escape tabs (convert tabs to spaces)
880 while ((my $pos = index($line, "\t")) != -1) {
881 if (my $count = (8 - ($pos % 8))) {
882 my $spaces = ' ' x
$count;
883 $line =~ s/\t/$spaces/;
890 sub project_in_list
{
892 my @list = git_get_projects_list
();
893 return @list && scalar(grep { $_->{'path'} eq $project } @list);
896 ## ----------------------------------------------------------------------
897 ## HTML aware string manipulation
899 # Try to chop given string on a word boundary between position
900 # $len and $len+$add_len. If there is no word boundary there,
901 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
902 # (marking chopped part) would be longer than given string.
906 my $add_len = shift || 10;
907 my $where = shift || 'right'; # 'left' | 'center' | 'right'
909 # Make sure perl knows it is utf8 encoded so we don't
910 # cut in the middle of a utf8 multibyte char.
911 $str = to_utf8
($str);
913 # allow only $len chars, but don't cut a word if it would fit in $add_len
914 # if it doesn't fit, cut it if it's still longer than the dots we would add
915 # remove chopped character entities entirely
917 # when chopping in the middle, distribute $len into left and right part
918 # return early if chopping wouldn't make string shorter
919 if ($where eq 'center') {
920 return $str if ($len + 5 >= length($str)); # filler is length 5
923 return $str if ($len + 4 >= length($str)); # filler is length 4
926 # regexps: ending and beginning with word part up to $add_len
927 my $endre = qr/.{$len}\w{0,$add_len}/;
928 my $begre = qr/\w{0,$add_len}.{$len}/;
930 if ($where eq 'left') {
931 $str =~ m/^(.*?)($begre)$/;
932 my ($lead, $body) = ($1, $2);
933 if (length($lead) > 4) {
934 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
939 } elsif ($where eq 'center') {
940 $str =~ m/^($endre)(.*)$/;
941 my ($left, $str) = ($1, $2);
942 $str =~ m/^(.*?)($begre)$/;
943 my ($mid, $right) = ($1, $2);
944 if (length($mid) > 5) {
945 $left =~ s/&[^;]*$//;
946 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
949 return "$left$mid$right";
952 $str =~ m/^($endre)(.*)$/;
955 if (length($tail) > 4) {
956 $body =~ s/&[^;]*$//;
963 # takes the same arguments as chop_str, but also wraps a <span> around the
964 # result with a title attribute if it does get chopped. Additionally, the
965 # string is HTML-escaped.
966 sub chop_and_escape_str
{
969 my $chopped = chop_str
(@_);
970 if ($chopped eq $str) {
971 return esc_html
($chopped);
973 $str =~ s/([[:cntrl:]])/?/g;
974 return $cgi->span({-title
=>$str}, esc_html
($chopped));
978 ## ----------------------------------------------------------------------
979 ## functions returning short strings
981 # CSS class for given age value (in seconds)
987 } elsif ($age < 60*60*2) {
989 } elsif ($age < 60*60*24*2) {
996 # convert age in seconds to "nn units ago" string
1001 if ($age > 60*60*24*365*2) {
1002 $age_str = (int $age/60/60/24/365);
1003 $age_str .= " years ago";
1004 } elsif ($age > 60*60*24*(365/12)*2) {
1005 $age_str = int $age/60/60/24/(365/12);
1006 $age_str .= " months ago";
1007 } elsif ($age > 60*60*24*7*2) {
1008 $age_str = int $age/60/60/24/7;
1009 $age_str .= " weeks ago";
1010 } elsif ($age > 60*60*24*2) {
1011 $age_str = int $age/60/60/24;
1012 $age_str .= " days ago";
1013 } elsif ($age > 60*60*2) {
1014 $age_str = int $age/60/60;
1015 $age_str .= " hours ago";
1016 } elsif ($age > 60*2) {
1017 $age_str = int $age/60;
1018 $age_str .= " min ago";
1019 } elsif ($age > 2) {
1020 $age_str = int $age;
1021 $age_str .= " sec ago";
1023 $age_str .= " right now";
1029 S_IFINVALID
=> 0030000,
1030 S_IFGITLINK
=> 0160000,
1033 # submodule/subproject, a commit object reference
1034 sub S_ISGITLINK
($) {
1037 return (($mode & S_IFMT
) == S_IFGITLINK
)
1040 # convert file mode in octal to symbolic file mode string
1042 my $mode = oct shift;
1044 if (S_ISGITLINK
($mode)) {
1045 return 'm---------';
1046 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1047 return 'drwxr-xr-x';
1048 } elsif (S_ISLNK
($mode)) {
1049 return 'lrwxrwxrwx';
1050 } elsif (S_ISREG
($mode)) {
1051 # git cares only about the executable bit
1052 if ($mode & S_IXUSR
) {
1053 return '-rwxr-xr-x';
1055 return '-rw-r--r--';
1058 return '----------';
1062 # convert file mode in octal to file type string
1066 if ($mode !~ m/^[0-7]+$/) {
1072 if (S_ISGITLINK
($mode)) {
1074 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1076 } elsif (S_ISLNK
($mode)) {
1078 } elsif (S_ISREG
($mode)) {
1085 # convert file mode in octal to file type description string
1086 sub file_type_long
{
1089 if ($mode !~ m/^[0-7]+$/) {
1095 if (S_ISGITLINK
($mode)) {
1097 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1099 } elsif (S_ISLNK
($mode)) {
1101 } elsif (S_ISREG
($mode)) {
1102 if ($mode & S_IXUSR
) {
1103 return "executable";
1113 ## ----------------------------------------------------------------------
1114 ## functions returning short HTML fragments, or transforming HTML fragments
1115 ## which don't belong to other sections
1117 # format line of commit message.
1118 sub format_log_line_html
{
1121 $line = esc_html
($line, -nbsp
=>1);
1122 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1125 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1126 -class => "text"}, $hash_text);
1127 $line =~ s/$hash_text/$link/;
1132 # format marker of refs pointing to given object
1133 sub format_ref_marker
{
1134 my ($refs, $id) = @_;
1137 if (defined $refs->{$id}) {
1138 foreach my $ref (@
{$refs->{$id}}) {
1139 my ($type, $name) = qw();
1140 # e.g. tags/v2.6.11 or heads/next
1141 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1149 $markers .= " <span class=\"$type\" title=\"$ref\">" .
1150 esc_html
($name) . "</span>";
1155 return ' <span class="refs">'. $markers . '</span>';
1161 # format, perhaps shortened and with markers, title line
1162 sub format_subject_html
{
1163 my ($long, $short, $href, $extra) = @_;
1164 $extra = '' unless defined($extra);
1166 if (length($short) < length($long)) {
1167 return $cgi->a({-href
=> $href, -class => "list subject",
1168 -title
=> to_utf8
($long)},
1169 esc_html
($short) . $extra);
1171 return $cgi->a({-href
=> $href, -class => "list subject"},
1172 esc_html
($long) . $extra);
1176 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1177 sub format_git_diff_header_line
{
1179 my $diffinfo = shift;
1180 my ($from, $to) = @_;
1182 if ($diffinfo->{'nparents'}) {
1184 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1185 if ($to->{'href'}) {
1186 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1187 esc_path
($to->{'file'}));
1188 } else { # file was deleted (no href)
1189 $line .= esc_path
($to->{'file'});
1193 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1194 if ($from->{'href'}) {
1195 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1196 'a/' . esc_path
($from->{'file'}));
1197 } else { # file was added (no href)
1198 $line .= 'a/' . esc_path
($from->{'file'});
1201 if ($to->{'href'}) {
1202 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1203 'b/' . esc_path
($to->{'file'}));
1204 } else { # file was deleted
1205 $line .= 'b/' . esc_path
($to->{'file'});
1209 return "<div class=\"diff header\">$line</div>\n";
1212 # format extended diff header line, before patch itself
1213 sub format_extended_diff_header_line
{
1215 my $diffinfo = shift;
1216 my ($from, $to) = @_;
1219 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1220 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1221 esc_path
($from->{'file'}));
1223 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1224 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1225 esc_path
($to->{'file'}));
1227 # match single <mode>
1228 if ($line =~ m/\s(\d{6})$/) {
1229 $line .= '<span class="info"> (' .
1230 file_type_long
($1) .
1234 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1235 # can match only for combined diff
1237 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1238 if ($from->{'href'}[$i]) {
1239 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1241 substr($diffinfo->{'from_id'}[$i],0,7));
1246 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1249 if ($to->{'href'}) {
1250 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1251 substr($diffinfo->{'to_id'},0,7));
1256 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1257 # can match only for ordinary diff
1258 my ($from_link, $to_link);
1259 if ($from->{'href'}) {
1260 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1261 substr($diffinfo->{'from_id'},0,7));
1263 $from_link = '0' x
7;
1265 if ($to->{'href'}) {
1266 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1267 substr($diffinfo->{'to_id'},0,7));
1271 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1272 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1275 return $line . "<br/>\n";
1278 # format from-file/to-file diff header
1279 sub format_diff_from_to_header
{
1280 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1285 #assert($line =~ m/^---/) if DEBUG;
1286 # no extra formatting for "^--- /dev/null"
1287 if (! $diffinfo->{'nparents'}) {
1288 # ordinary (single parent) diff
1289 if ($line =~ m!^--- "?a/!) {
1290 if ($from->{'href'}) {
1292 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1293 esc_path
($from->{'file'}));
1296 esc_path
($from->{'file'});
1299 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1302 # combined diff (merge commit)
1303 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1304 if ($from->{'href'}[$i]) {
1306 $cgi->a({-href
=>href
(action
=>"blobdiff",
1307 hash_parent
=>$diffinfo->{'from_id'}[$i],
1308 hash_parent_base
=>$parents[$i],
1309 file_parent
=>$from->{'file'}[$i],
1310 hash
=>$diffinfo->{'to_id'},
1312 file_name
=>$to->{'file'}),
1314 -title
=>"diff" . ($i+1)},
1317 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1318 esc_path
($from->{'file'}[$i]));
1320 $line = '--- /dev/null';
1322 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1327 #assert($line =~ m/^\+\+\+/) if DEBUG;
1328 # no extra formatting for "^+++ /dev/null"
1329 if ($line =~ m!^\+\+\+ "?b/!) {
1330 if ($to->{'href'}) {
1332 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1333 esc_path
($to->{'file'}));
1336 esc_path
($to->{'file'});
1339 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
1344 # create note for patch simplified by combined diff
1345 sub format_diff_cc_simplified
{
1346 my ($diffinfo, @parents) = @_;
1349 $result .= "<div class=\"diff header\">" .
1351 if (!is_deleted
($diffinfo)) {
1352 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1354 hash
=>$diffinfo->{'to_id'},
1355 file_name
=>$diffinfo->{'to_file'}),
1357 esc_path
($diffinfo->{'to_file'}));
1359 $result .= esc_path
($diffinfo->{'to_file'});
1361 $result .= "</div>\n" . # class="diff header"
1362 "<div class=\"diff nodifferences\">" .
1364 "</div>\n"; # class="diff nodifferences"
1369 # format patch (diff) line (not to be used for diff headers)
1370 sub format_diff_line
{
1372 my ($from, $to) = @_;
1373 my $diff_class = "";
1377 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1379 my $prefix = substr($line, 0, scalar @
{$from->{'href'}});
1380 if ($line =~ m/^\@{3}/) {
1381 $diff_class = " chunk_header";
1382 } elsif ($line =~ m/^\\/) {
1383 $diff_class = " incomplete";
1384 } elsif ($prefix =~ tr/+/+/) {
1385 $diff_class = " add";
1386 } elsif ($prefix =~ tr/-/-/) {
1387 $diff_class = " rem";
1390 # assume ordinary diff
1391 my $char = substr($line, 0, 1);
1393 $diff_class = " add";
1394 } elsif ($char eq '-') {
1395 $diff_class = " rem";
1396 } elsif ($char eq '@') {
1397 $diff_class = " chunk_header";
1398 } elsif ($char eq "\\") {
1399 $diff_class = " incomplete";
1402 $line = untabify
($line);
1403 if ($from && $to && $line =~ m/^\@{2} /) {
1404 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1405 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1407 $from_lines = 0 unless defined $from_lines;
1408 $to_lines = 0 unless defined $to_lines;
1410 if ($from->{'href'}) {
1411 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1412 -class=>"list"}, $from_text);
1414 if ($to->{'href'}) {
1415 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1416 -class=>"list"}, $to_text);
1418 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1419 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1420 return "<div class=\"diff$diff_class\">$line</div>\n";
1421 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1422 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1423 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1425 @from_text = split(' ', $ranges);
1426 for (my $i = 0; $i < @from_text; ++$i) {
1427 ($from_start[$i], $from_nlines[$i]) =
1428 (split(',', substr($from_text[$i], 1)), 0);
1431 $to_text = pop @from_text;
1432 $to_start = pop @from_start;
1433 $to_nlines = pop @from_nlines;
1435 $line = "<span class=\"chunk_info\">$prefix ";
1436 for (my $i = 0; $i < @from_text; ++$i) {
1437 if ($from->{'href'}[$i]) {
1438 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1439 -class=>"list"}, $from_text[$i]);
1441 $line .= $from_text[$i];
1445 if ($to->{'href'}) {
1446 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1447 -class=>"list"}, $to_text);
1451 $line .= " $prefix</span>" .
1452 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1453 return "<div class=\"diff$diff_class\">$line</div>\n";
1455 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1458 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1459 # linked. Pass the hash of the tree/commit to snapshot.
1460 sub format_snapshot_links
{
1462 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1463 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1464 my $num_fmts = @snapshot_fmts;
1465 if ($num_fmts > 1) {
1466 # A parenthesized list of links bearing format names.
1467 # e.g. "snapshot (_tar.gz_ _zip_)"
1468 return "snapshot (" . join(' ', map
1475 }, $known_snapshot_formats{$_}{'display'})
1476 , @snapshot_fmts) . ")";
1477 } elsif ($num_fmts == 1) {
1478 # A single "snapshot" link whose tooltip bears the format name.
1480 my ($fmt) = @snapshot_fmts;
1486 snapshot_format
=>$fmt
1488 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1490 } else { # $num_fmts == 0
1495 ## ......................................................................
1496 ## functions returning values to be passed, perhaps after some
1497 ## transformation, to other functions; e.g. returning arguments to href()
1499 # returns hash to be passed to href to generate gitweb URL
1500 # in -title key it returns description of link
1502 my $format = shift || 'Atom';
1503 my %res = (action
=> lc($format));
1505 # feed links are possible only for project views
1506 return unless (defined $project);
1507 # some views should link to OPML, or to generic project feed,
1508 # or don't have specific feed yet (so they should use generic)
1509 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1512 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1513 # from tag links; this also makes possible to detect branch links
1514 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1515 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1518 # find log type for feed description (title)
1520 if (defined $file_name) {
1521 $type = "history of $file_name";
1522 $type .= "/" if ($action eq 'tree');
1523 $type .= " on '$branch'" if (defined $branch);
1525 $type = "log of $branch" if (defined $branch);
1528 $res{-title
} = $type;
1529 $res{'hash'} = (defined $branch ?
"refs/heads/$branch" : undef);
1530 $res{'file_name'} = $file_name;
1535 ## ----------------------------------------------------------------------
1536 ## git utility subroutines, invoking git commands
1538 # returns path to the core git executable and the --git-dir parameter as list
1540 return $GIT, '--git-dir='.$git_dir;
1543 # quote the given arguments for passing them to the shell
1544 # quote_command("command", "arg 1", "arg with ' and ! characters")
1545 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1546 # Try to avoid using this function wherever possible.
1549 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1552 # get HEAD ref of given project as hash
1553 sub git_get_head_hash
{
1554 my $project = shift;
1555 my $o_git_dir = $git_dir;
1557 $git_dir = "$projectroot/$project";
1558 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1561 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1565 if (defined $o_git_dir) {
1566 $git_dir = $o_git_dir;
1571 # get type of given object
1575 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1577 close $fd or return;
1582 # repository configuration
1583 our $config_file = '';
1586 # store multiple values for single key as anonymous array reference
1587 # single values stored directly in the hash, not as [ <value> ]
1588 sub hash_set_multi
{
1589 my ($hash, $key, $value) = @_;
1591 if (!exists $hash->{$key}) {
1592 $hash->{$key} = $value;
1593 } elsif (!ref $hash->{$key}) {
1594 $hash->{$key} = [ $hash->{$key}, $value ];
1596 push @
{$hash->{$key}}, $value;
1600 # return hash of git project configuration
1601 # optionally limited to some section, e.g. 'gitweb'
1602 sub git_parse_project_config
{
1603 my $section_regexp = shift;
1608 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1611 while (my $keyval = <$fh>) {
1613 my ($key, $value) = split(/\n/, $keyval, 2);
1615 hash_set_multi
(\
%config, $key, $value)
1616 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1623 # convert config value to boolean, 'true' or 'false'
1624 # no value, number > 0, 'true' and 'yes' values are true
1625 # rest of values are treated as false (never as error)
1626 sub config_to_bool
{
1629 # strip leading and trailing whitespace
1633 return (!defined $val || # section.key
1634 ($val =~ /^\d+$/ && $val) || # section.key = 1
1635 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1638 # convert config value to simple decimal number
1639 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1640 # to be multiplied by 1024, 1048576, or 1073741824
1644 # strip leading and trailing whitespace
1648 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1650 # unknown unit is treated as 1
1651 return $num * ($unit eq 'g' ?
1073741824 :
1652 $unit eq 'm' ?
1048576 :
1653 $unit eq 'k' ?
1024 : 1);
1658 # convert config value to array reference, if needed
1659 sub config_to_multi
{
1662 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
1665 sub git_get_project_config
{
1666 my ($key, $type) = @_;
1669 return unless ($key);
1670 $key =~ s/^gitweb\.//;
1671 return if ($key =~ m/\W/);
1674 if (defined $type) {
1677 unless ($type eq 'bool' || $type eq 'int');
1681 if (!defined $config_file ||
1682 $config_file ne "$git_dir/config") {
1683 %config = git_parse_project_config
('gitweb');
1684 $config_file = "$git_dir/config";
1688 if (!defined $type) {
1689 return $config{"gitweb.$key"};
1690 } elsif ($type eq 'bool') {
1691 # backward compatibility: 'git config --bool' returns true/false
1692 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
1693 } elsif ($type eq 'int') {
1694 return config_to_int
($config{"gitweb.$key"});
1696 return $config{"gitweb.$key"};
1699 # get hash of given path at given ref
1700 sub git_get_hash_by_path
{
1702 my $path = shift || return undef;
1707 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1708 or die_error
(undef, "Open git-ls-tree failed");
1710 close $fd or return undef;
1712 if (!defined $line) {
1713 # there is no tree or hash given by $path at $base
1717 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1718 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1719 if (defined $type && $type ne $2) {
1720 # type doesn't match
1726 # get path of entry with given hash at given tree-ish (ref)
1727 # used to get 'from' filename for combined diff (merge commit) for renames
1728 sub git_get_path_by_hash
{
1729 my $base = shift || return;
1730 my $hash = shift || return;
1734 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1736 while (my $line = <$fd>) {
1739 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1740 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1741 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1750 ## ......................................................................
1751 ## git utility functions, directly accessing git repository
1753 sub git_get_project_description
{
1756 $git_dir = "$projectroot/$path";
1757 open my $fd, "$git_dir/description"
1758 or return git_get_project_config
('description');
1761 if (defined $descr) {
1767 sub git_get_project_url_list
{
1770 $git_dir = "$projectroot/$path";
1771 open my $fd, "$git_dir/cloneurl"
1772 or return wantarray ?
1773 @
{ config_to_multi
(git_get_project_config
('url')) } :
1774 config_to_multi
(git_get_project_config
('url'));
1775 my @git_project_url_list = map { chomp; $_ } <$fd>;
1778 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
1781 sub git_get_projects_list
{
1786 $filter =~ s/\.git$//;
1788 my ($check_forks) = gitweb_check_feature
('forks');
1790 if (-d
$projects_list) {
1791 # search in directory
1792 my $dir = $projects_list . ($filter ?
"/$filter" : '');
1793 # remove the trailing "/"
1795 my $pfxlen = length("$dir");
1796 my $pfxdepth = ($dir =~ tr!/!!);
1799 follow_fast
=> 1, # follow symbolic links
1800 follow_skip
=> 2, # ignore duplicates
1801 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1803 # skip project-list toplevel, if we get it.
1804 return if (m!^[/.]$!);
1805 # only directories can be git repositories
1806 return unless (-d
$_);
1807 # don't traverse too deep (Find is super slow on os x)
1808 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1809 $File::Find
::prune
= 1;
1813 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1814 # we check related file in $projectroot
1815 if ($check_forks and $subdir =~ m
#/.#) {
1816 $File::Find
::prune
= 1;
1817 } elsif (check_export_ok
("$projectroot/$filter/$subdir")) {
1818 push @list, { path
=> ($filter ?
"$filter/" : '') . $subdir };
1819 $File::Find
::prune
= 1;
1824 } elsif (-f
$projects_list) {
1825 # read from file(url-encoded):
1826 # 'git%2Fgit.git Linus+Torvalds'
1827 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1828 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1830 open my ($fd), $projects_list or return;
1832 while (my $line = <$fd>) {
1834 my ($path, $owner) = split ' ', $line;
1835 $path = unescape
($path);
1836 $owner = unescape
($owner);
1837 if (!defined $path) {
1840 if ($filter ne '') {
1841 # looking for forks;
1842 my $pfx = substr($path, 0, length($filter));
1843 if ($pfx ne $filter) {
1846 my $sfx = substr($path, length($filter));
1847 if ($sfx !~ /^\/.*\
.git
$/) {
1850 } elsif ($check_forks) {
1852 foreach my $filter (keys %paths) {
1853 # looking for forks;
1854 my $pfx = substr($path, 0, length($filter));
1855 if ($pfx ne $filter) {
1858 my $sfx = substr($path, length($filter));
1859 if ($sfx !~ /^\/.*\
.git
$/) {
1862 # is a fork, don't include it in
1867 if (check_export_ok
("$projectroot/$path")) {
1870 owner
=> to_utf8
($owner),
1873 (my $forks_path = $path) =~ s/\.git$//;
1874 $paths{$forks_path}++;
1882 our $gitweb_project_owner = undef;
1883 sub git_get_project_list_from_file
{
1885 return if (defined $gitweb_project_owner);
1887 $gitweb_project_owner = {};
1888 # read from file (url-encoded):
1889 # 'git%2Fgit.git Linus+Torvalds'
1890 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1891 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1892 if (-f
$projects_list) {
1893 open (my $fd , $projects_list);
1894 while (my $line = <$fd>) {
1896 my ($pr, $ow) = split ' ', $line;
1897 $pr = unescape
($pr);
1898 $ow = unescape
($ow);
1899 $gitweb_project_owner->{$pr} = to_utf8
($ow);
1905 sub git_get_project_owner
{
1906 my $project = shift;
1909 return undef unless $project;
1910 $git_dir = "$projectroot/$project";
1912 if (!defined $gitweb_project_owner) {
1913 git_get_project_list_from_file
();
1916 if (exists $gitweb_project_owner->{$project}) {
1917 $owner = $gitweb_project_owner->{$project};
1919 if (!defined $owner){
1920 $owner = git_get_project_config
('owner');
1922 if (!defined $owner) {
1923 $owner = get_file_owner
("$git_dir");
1929 sub git_get_last_activity
{
1933 $git_dir = "$projectroot/$path";
1934 open($fd, "-|", git_cmd
(), 'for-each-ref',
1935 '--format=%(committer)',
1936 '--sort=-committerdate',
1938 'refs/heads') or return;
1939 my $most_recent = <$fd>;
1940 close $fd or return;
1941 if (defined $most_recent &&
1942 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1944 my $age = time - $timestamp;
1945 return ($age, age_string
($age));
1947 return (undef, undef);
1950 sub git_get_references
{
1951 my $type = shift || "";
1953 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1954 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1955 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
1956 ($type ?
("--", "refs/$type") : ()) # use -- <pattern> if $type
1959 while (my $line = <$fd>) {
1961 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type/?[^^]+)!) {
1962 if (defined $refs{$1}) {
1963 push @
{$refs{$1}}, $2;
1969 close $fd or return;
1973 sub git_get_rev_name_tags
{
1974 my $hash = shift || return undef;
1976 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
1978 my $name_rev = <$fd>;
1981 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
1984 # catches also '$hash undefined' output
1989 ## ----------------------------------------------------------------------
1990 ## parse to hash functions
1994 my $tz = shift || "-0000";
1997 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
1998 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
1999 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2000 $date{'hour'} = $hour;
2001 $date{'minute'} = $min;
2002 $date{'mday'} = $mday;
2003 $date{'day'} = $days[$wday];
2004 $date{'month'} = $months[$mon];
2005 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2006 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2007 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2008 $mday, $months[$mon], $hour ,$min;
2009 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2010 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2012 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2013 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2014 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2015 $date{'hour_local'} = $hour;
2016 $date{'minute_local'} = $min;
2017 $date{'tz_local'} = $tz;
2018 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2019 1900+$year, $mon+1, $mday,
2020 $hour, $min, $sec, $tz);
2029 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2030 $tag{'id'} = $tag_id;
2031 while (my $line = <$fd>) {
2033 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2034 $tag{'object'} = $1;
2035 } elsif ($line =~ m/^type (.+)$/) {
2037 } elsif ($line =~ m/^tag (.+)$/) {
2039 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2040 $tag{'author'} = $1;
2043 } elsif ($line =~ m/--BEGIN/) {
2044 push @comment, $line;
2046 } elsif ($line eq "") {
2050 push @comment, <$fd>;
2051 $tag{'comment'} = \
@comment;
2052 close $fd or return;
2053 if (!defined $tag{'name'}) {
2059 sub parse_commit_text
{
2060 my ($commit_text, $withparents) = @_;
2061 my @commit_lines = split '\n', $commit_text;
2064 pop @commit_lines; # Remove '\0'
2066 if (! @commit_lines) {
2070 my $header = shift @commit_lines;
2071 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2074 ($co{'id'}, my @parents) = split ' ', $header;
2075 while (my $line = shift @commit_lines) {
2076 last if $line eq "\n";
2077 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2079 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2081 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2083 $co{'author_epoch'} = $2;
2084 $co{'author_tz'} = $3;
2085 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2086 $co{'author_name'} = $1;
2087 $co{'author_email'} = $2;
2089 $co{'author_name'} = $co{'author'};
2091 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2092 $co{'committer'} = $1;
2093 $co{'committer_epoch'} = $2;
2094 $co{'committer_tz'} = $3;
2095 $co{'committer_name'} = $co{'committer'};
2096 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2097 $co{'committer_name'} = $1;
2098 $co{'committer_email'} = $2;
2100 $co{'committer_name'} = $co{'committer'};
2104 if (!defined $co{'tree'}) {
2107 $co{'parents'} = \
@parents;
2108 $co{'parent'} = $parents[0];
2110 foreach my $title (@commit_lines) {
2113 $co{'title'} = chop_str
($title, 80, 5);
2114 # remove leading stuff of merges to make the interesting part visible
2115 if (length($title) > 50) {
2116 $title =~ s/^Automatic //;
2117 $title =~ s/^merge (of|with) /Merge ... /i;
2118 if (length($title) > 50) {
2119 $title =~ s/(http|rsync):\/\///;
2121 if (length($title) > 50) {
2122 $title =~ s/(master|www|rsync)\.//;
2124 if (length($title) > 50) {
2125 $title =~ s/kernel.org:?//;
2127 if (length($title) > 50) {
2128 $title =~ s/\/pub\/scm//;
2131 $co{'title_short'} = chop_str
($title, 50, 5);
2135 if ($co{'title'} eq "") {
2136 $co{'title'} = $co{'title_short'} = '(no commit message)';
2138 # remove added spaces
2139 foreach my $line (@commit_lines) {
2142 $co{'comment'} = \
@commit_lines;
2144 my $age = time - $co{'committer_epoch'};
2146 $co{'age_string'} = age_string
($age);
2147 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2148 if ($age > 60*60*24*7*2) {
2149 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2150 $co{'age_string_age'} = $co{'age_string'};
2152 $co{'age_string_date'} = $co{'age_string'};
2153 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2159 my ($commit_id) = @_;
2164 open my $fd, "-|", git_cmd
(), "rev-list",
2170 or die_error
(undef, "Open git-rev-list failed");
2171 %co = parse_commit_text
(<$fd>, 1);
2178 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2186 open my $fd, "-|", git_cmd
(), "rev-list",
2189 ("--max-count=" . $maxcount),
2190 ("--skip=" . $skip),
2194 ($filename ?
($filename) : ())
2195 or die_error
(undef, "Open git-rev-list failed");
2196 while (my $line = <$fd>) {
2197 my %co = parse_commit_text
($line);
2202 return wantarray ?
@cos : \
@cos;
2205 # parse line of git-diff-tree "raw" output
2206 sub parse_difftree_raw_line
{
2210 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2211 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2212 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2213 $res{'from_mode'} = $1;
2214 $res{'to_mode'} = $2;
2215 $res{'from_id'} = $3;
2217 $res{'status'} = $5;
2218 $res{'similarity'} = $6;
2219 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2220 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2222 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2225 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2226 # combined diff (for merge commit)
2227 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2228 $res{'nparents'} = length($1);
2229 $res{'from_mode'} = [ split(' ', $2) ];
2230 $res{'to_mode'} = pop @
{$res{'from_mode'}};
2231 $res{'from_id'} = [ split(' ', $3) ];
2232 $res{'to_id'} = pop @
{$res{'from_id'}};
2233 $res{'status'} = [ split('', $4) ];
2234 $res{'to_file'} = unquote
($5);
2236 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2237 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2238 $res{'commit'} = $1;
2241 return wantarray ?
%res : \
%res;
2244 # wrapper: return parsed line of git-diff-tree "raw" output
2245 # (the argument might be raw line, or parsed info)
2246 sub parsed_difftree_line
{
2247 my $line_or_ref = shift;
2249 if (ref($line_or_ref) eq "HASH") {
2250 # pre-parsed (or generated by hand)
2251 return $line_or_ref;
2253 return parse_difftree_raw_line
($line_or_ref);
2257 # parse line of git-ls-tree output
2258 sub parse_ls_tree_line
($;%) {
2263 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2264 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2272 $res{'name'} = unquote
($4);
2275 return wantarray ?
%res : \
%res;
2278 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2279 sub parse_from_to_diffinfo
{
2280 my ($diffinfo, $from, $to, @parents) = @_;
2282 if ($diffinfo->{'nparents'}) {
2284 $from->{'file'} = [];
2285 $from->{'href'} = [];
2286 fill_from_file_info
($diffinfo, @parents)
2287 unless exists $diffinfo->{'from_file'};
2288 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2289 $from->{'file'}[$i] =
2290 defined $diffinfo->{'from_file'}[$i] ?
2291 $diffinfo->{'from_file'}[$i] :
2292 $diffinfo->{'to_file'};
2293 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2294 $from->{'href'}[$i] = href
(action
=>"blob",
2295 hash_base
=>$parents[$i],
2296 hash
=>$diffinfo->{'from_id'}[$i],
2297 file_name
=>$from->{'file'}[$i]);
2299 $from->{'href'}[$i] = undef;
2303 # ordinary (not combined) diff
2304 $from->{'file'} = $diffinfo->{'from_file'};
2305 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2306 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2307 hash
=>$diffinfo->{'from_id'},
2308 file_name
=>$from->{'file'});
2310 delete $from->{'href'};
2314 $to->{'file'} = $diffinfo->{'to_file'};
2315 if (!is_deleted
($diffinfo)) { # file exists in result
2316 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2317 hash
=>$diffinfo->{'to_id'},
2318 file_name
=>$to->{'file'});
2320 delete $to->{'href'};
2324 ## ......................................................................
2325 ## parse to array of hashes functions
2327 sub git_get_heads_list
{
2331 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2332 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
2333 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2336 while (my $line = <$fd>) {
2340 my ($refinfo, $committerinfo) = split(/\0/, $line);
2341 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2342 my ($committer, $epoch, $tz) =
2343 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2344 $ref_item{'fullname'} = $name;
2345 $name =~ s!^refs/heads/!!;
2347 $ref_item{'name'} = $name;
2348 $ref_item{'id'} = $hash;
2349 $ref_item{'title'} = $title || '(no commit message)';
2350 $ref_item{'epoch'} = $epoch;
2352 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2354 $ref_item{'age'} = "unknown";
2357 push @headslist, \
%ref_item;
2361 return wantarray ?
@headslist : \
@headslist;
2364 sub git_get_tags_list
{
2368 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2369 ($limit ?
'--count='.($limit+1) : ()), '--sort=-creatordate',
2370 '--format=%(objectname) %(objecttype) %(refname) '.
2371 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2374 while (my $line = <$fd>) {
2378 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2379 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2380 my ($creator, $epoch, $tz) =
2381 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2382 $ref_item{'fullname'} = $name;
2383 $name =~ s!^refs/tags/!!;
2385 $ref_item{'type'} = $type;
2386 $ref_item{'id'} = $id;
2387 $ref_item{'name'} = $name;
2388 if ($type eq "tag") {
2389 $ref_item{'subject'} = $title;
2390 $ref_item{'reftype'} = $reftype;
2391 $ref_item{'refid'} = $refid;
2393 $ref_item{'reftype'} = $type;
2394 $ref_item{'refid'} = $id;
2397 if ($type eq "tag" || $type eq "commit") {
2398 $ref_item{'epoch'} = $epoch;
2400 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2402 $ref_item{'age'} = "unknown";
2406 push @tagslist, \
%ref_item;
2410 return wantarray ?
@tagslist : \
@tagslist;
2413 ## ----------------------------------------------------------------------
2414 ## filesystem-related functions
2416 sub get_file_owner
{
2419 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2420 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2421 if (!defined $gcos) {
2425 $owner =~ s/[,;].*$//;
2426 return to_utf8
($owner);
2429 ## ......................................................................
2430 ## mimetype related functions
2432 sub mimetype_guess_file
{
2433 my $filename = shift;
2434 my $mimemap = shift;
2435 -r
$mimemap or return undef;
2438 open(MIME
, $mimemap) or return undef;
2440 next if m/^#/; # skip comments
2441 my ($mime, $exts) = split(/\t+/);
2442 if (defined $exts) {
2443 my @exts = split(/\s+/, $exts);
2444 foreach my $ext (@exts) {
2445 $mimemap{$ext} = $mime;
2451 $filename =~ /\.([^.]*)$/;
2452 return $mimemap{$1};
2455 sub mimetype_guess
{
2456 my $filename = shift;
2458 $filename =~ /\./ or return undef;
2460 if ($mimetypes_file) {
2461 my $file = $mimetypes_file;
2462 if ($file !~ m!^/!) { # if it is relative path
2463 # it is relative to project
2464 $file = "$projectroot/$project/$file";
2466 $mime = mimetype_guess_file
($filename, $file);
2468 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2474 my $filename = shift;
2477 my $mime = mimetype_guess
($filename);
2478 $mime and return $mime;
2482 return $default_blob_plain_mimetype unless $fd;
2485 return 'text/plain';
2486 } elsif (! $filename) {
2487 return 'application/octet-stream';
2488 } elsif ($filename =~ m/\.png$/i) {
2490 } elsif ($filename =~ m/\.gif$/i) {
2492 } elsif ($filename =~ m/\.jpe?g$/i) {
2493 return 'image/jpeg';
2495 return 'application/octet-stream';
2499 sub blob_contenttype
{
2500 my ($fd, $file_name, $type) = @_;
2502 $type ||= blob_mimetype
($fd, $file_name);
2503 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2504 $type .= "; charset=$default_text_plain_charset";
2510 ## ======================================================================
2511 ## functions printing HTML: header, footer, error page
2513 sub git_header_html
{
2514 my $status = shift || "200 OK";
2515 my $expires = shift;
2517 my $title = "$site_name";
2518 if (defined $project) {
2519 $title .= " - " . to_utf8
($project);
2520 if (defined $action) {
2521 $title .= "/$action";
2522 if (defined $file_name) {
2523 $title .= " - " . esc_path
($file_name);
2524 if ($action eq "tree" && $file_name !~ m
|/$|) {
2531 # require explicit support from the UA if we are to send the page as
2532 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2533 # we have to do this because MSIE sometimes globs '*/*', pretending to
2534 # support xhtml+xml but choking when it gets what it asked for.
2535 if (defined $cgi->http('HTTP_ACCEPT') &&
2536 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2537 $cgi->Accept('application/xhtml+xml') != 0) {
2538 $content_type = 'application/xhtml+xml';
2540 $content_type = 'text/html';
2542 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2543 -status
=> $status, -expires
=> $expires);
2544 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
2546 <?xml version="1.0" encoding="utf-8"?>
2547 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2548 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2549 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2550 <!-- git core binaries version $git_version -->
2552 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2553 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2554 <meta name="robots" content="index, nofollow"/>
2555 <title>$title</title>
2557 # print out each stylesheet that exist
2558 if (defined $stylesheet) {
2559 #provides backwards capability for those people who define style sheet in a config file
2560 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2562 foreach my $stylesheet (@stylesheets) {
2563 next unless $stylesheet;
2564 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2567 if (defined $project) {
2568 my %href_params = get_feed_info
();
2569 if (!exists $href_params{'-title'}) {
2570 $href_params{'-title'} = 'log';
2573 foreach my $format qw(RSS Atom) {
2574 my $type = lc($format);
2576 '-rel' => 'alternate',
2577 '-title' => "$project - $href_params{'-title'} - $format feed",
2578 '-type' => "application/$type+xml"
2581 $href_params{'action'} = $type;
2582 $link_attr{'-href'} = href
(%href_params);
2584 "rel=\"$link_attr{'-rel'}\" ".
2585 "title=\"$link_attr{'-title'}\" ".
2586 "href=\"$link_attr{'-href'}\" ".
2587 "type=\"$link_attr{'-type'}\" ".
2590 $href_params{'extra_options'} = '--no-merges';
2591 $link_attr{'-href'} = href
(%href_params);
2592 $link_attr{'-title'} .= ' (no merges)';
2594 "rel=\"$link_attr{'-rel'}\" ".
2595 "title=\"$link_attr{'-title'}\" ".
2596 "href=\"$link_attr{'-href'}\" ".
2597 "type=\"$link_attr{'-type'}\" ".
2602 printf('<link rel="alternate" title="%s projects list" '.
2603 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2604 $site_name, href
(project
=>undef, action
=>"project_index"));
2605 printf('<link rel="alternate" title="%s projects feeds" '.
2606 'href="%s" type="text/x-opml" />'."\n",
2607 $site_name, href
(project
=>undef, action
=>"opml"));
2609 if (defined $favicon) {
2610 print qq(<link rel
="shortcut icon" href
="$favicon" type
="image/png" />\n);
2616 if (-f
$site_header) {
2617 open (my $fd, $site_header);
2622 print "<div class=\"page_header\">\n" .
2623 $cgi->a({-href
=> esc_url
($logo_url),
2624 -title
=> $logo_label},
2625 qq(<img src
="$logo" width
="72" height
="27" alt
="git" class="logo"/>));
2626 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2627 if (defined $project) {
2628 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2629 if (defined $action) {
2636 my ($have_search) = gitweb_check_feature
('search');
2637 if (defined $project && $have_search) {
2638 if (!defined $searchtext) {
2642 if (defined $hash_base) {
2643 $search_hash = $hash_base;
2644 } elsif (defined $hash) {
2645 $search_hash = $hash;
2647 $search_hash = "HEAD";
2649 my $action = $my_uri;
2650 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2651 if ($use_pathinfo) {
2652 $action .= "/".esc_url
($project);
2654 print $cgi->startform(-method
=> "get", -action
=> $action) .
2655 "<div class=\"search\">\n" .
2657 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2658 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2659 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2660 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2661 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2662 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2664 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2665 "<span title=\"Extended regular expression\">" .
2666 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2667 -checked
=> $search_use_regexp) .
2670 $cgi->end_form() . "\n";
2674 sub git_footer_html
{
2675 my $feed_class = 'rss_logo';
2677 print "<div class=\"page_footer\">\n";
2678 if (defined $project) {
2679 my $descr = git_get_project_description
($project);
2680 if (defined $descr) {
2681 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2684 my %href_params = get_feed_info
();
2685 if (!%href_params) {
2686 $feed_class .= ' generic';
2688 $href_params{'-title'} ||= 'log';
2690 foreach my $format qw(RSS Atom) {
2691 $href_params{'action'} = lc($format);
2692 print $cgi->a({-href
=> href
(%href_params),
2693 -title
=> "$href_params{'-title'} $format feed",
2694 -class => $feed_class}, $format)."\n";
2698 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2699 -class => $feed_class}, "OPML") . " ";
2700 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2701 -class => $feed_class}, "TXT") . "\n";
2703 print "</div>\n"; # class="page_footer"
2705 if (-f
$site_footer) {
2706 open (my $fd, $site_footer);
2716 my $status = shift || "403 Forbidden";
2717 my $error = shift || "Malformed query, file missing or permission denied";
2719 git_header_html
($status);
2721 <div class="page_body">
2731 ## ----------------------------------------------------------------------
2732 ## functions printing or outputting HTML: navigation
2734 sub git_print_page_nav
{
2735 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2736 $extra = '' if !defined $extra; # pager or formats
2738 my @navs = qw(summary shortlog log commit commitdiff tree);
2740 @navs = grep { $_ ne $suppress } @navs;
2743 my %arg = map { $_ => {action
=>$_} } @navs;
2744 if (defined $head) {
2745 for (qw(commit commitdiff)) {
2746 $arg{$_}{'hash'} = $head;
2748 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2749 for (qw(shortlog log)) {
2750 $arg{$_}{'hash'} = $head;
2754 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2755 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2757 print "<div class=\"page_nav\">\n" .
2759 map { $_ eq $current ?
2760 $_ : $cgi->a({-href
=> href
(%{$arg{$_}})}, "$_")
2762 print "<br/>\n$extra<br/>\n" .
2766 sub format_paging_nav
{
2767 my ($action, $hash, $head, $page, $has_next_link) = @_;
2771 if ($hash ne $head || $page) {
2772 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2774 $paging_nav .= "HEAD";
2778 $paging_nav .= " ⋅ " .
2779 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2780 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2782 $paging_nav .= " ⋅ prev";
2785 if ($has_next_link) {
2786 $paging_nav .= " ⋅ " .
2787 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2788 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2790 $paging_nav .= " ⋅ next";
2796 ## ......................................................................
2797 ## functions printing or outputting HTML: div
2799 sub git_print_header_div
{
2800 my ($action, $title, $hash, $hash_base) = @_;
2803 $args{'action'} = $action;
2804 $args{'hash'} = $hash if $hash;
2805 $args{'hash_base'} = $hash_base if $hash_base;
2807 print "<div class=\"header\">\n" .
2808 $cgi->a({-href
=> href
(%args), -class => "title"},
2809 $title ?
$title : $action) .
2813 #sub git_print_authorship (\%) {
2814 sub git_print_authorship
{
2817 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
2818 print "<div class=\"author_date\">" .
2819 esc_html
($co->{'author_name'}) .
2821 if ($ad{'hour_local'} < 6) {
2822 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2823 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2825 printf(" (%02d:%02d %s)",
2826 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2831 sub git_print_page_path
{
2837 print "<div class=\"page_path\">";
2838 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
2839 -title
=> 'tree root'}, to_utf8
("[$project]"));
2841 if (defined $name) {
2842 my @dirname = split '/', $name;
2843 my $basename = pop @dirname;
2846 foreach my $dir (@dirname) {
2847 $fullname .= ($fullname ?
'/' : '') . $dir;
2848 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
2850 -title
=> $fullname}, esc_path
($dir));
2853 if (defined $type && $type eq 'blob') {
2854 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
2856 -title
=> $name}, esc_path
($basename));
2857 } elsif (defined $type && $type eq 'tree') {
2858 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
2860 -title
=> $name}, esc_path
($basename));
2863 print esc_path
($basename);
2866 print "<br/></div>\n";
2869 # sub git_print_log (\@;%) {
2870 sub git_print_log
($;%) {
2874 if ($opts{'-remove_title'}) {
2875 # remove title, i.e. first line of log
2878 # remove leading empty lines
2879 while (defined $log->[0] && $log->[0] eq "") {
2886 foreach my $line (@
$log) {
2887 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2890 if (! $opts{'-remove_signoff'}) {
2891 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
2894 # remove signoff lines
2901 # print only one empty line
2902 # do not print empty line after signoff
2904 next if ($empty || $signoff);
2910 print format_log_line_html
($line) . "<br/>\n";
2913 if ($opts{'-final_empty_line'}) {
2914 # end with single empty line
2915 print "<br/>\n" unless $empty;
2919 # return link target (what link points to)
2920 sub git_get_link_target
{
2925 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
2929 $link_target = <$fd>;
2934 return $link_target;
2937 # given link target, and the directory (basedir) the link is in,
2938 # return target of link relative to top directory (top tree);
2939 # return undef if it is not possible (including absolute links).
2940 sub normalize_link_target
{
2941 my ($link_target, $basedir, $hash_base) = @_;
2943 # we can normalize symlink target only if $hash_base is provided
2944 return unless $hash_base;
2946 # absolute symlinks (beginning with '/') cannot be normalized
2947 return if (substr($link_target, 0, 1) eq '/');
2949 # normalize link target to path from top (root) tree (dir)
2952 $path = $basedir . '/' . $link_target;
2954 # we are in top (root) tree (dir)
2955 $path = $link_target;
2958 # remove //, /./, and /../
2960 foreach my $part (split('/', $path)) {
2961 # discard '.' and ''
2962 next if (!$part || $part eq '.');
2964 if ($part eq '..') {
2968 # link leads outside repository (outside top dir)
2972 push @path_parts, $part;
2975 $path = join('/', @path_parts);
2980 # print tree entry (row of git_tree), but without encompassing <tr> element
2981 sub git_print_tree_entry
{
2982 my ($t, $basedir, $hash_base, $have_blame) = @_;
2985 $base_key{'hash_base'} = $hash_base if defined $hash_base;
2987 # The format of a table row is: mode list link. Where mode is
2988 # the mode of the entry, list is the name of the entry, an href,
2989 # and link is the action links of the entry.
2991 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
2992 if ($t->{'type'} eq "blob") {
2993 print "<td class=\"list\">" .
2994 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
2995 file_name
=>"$basedir$t->{'name'}", %base_key),
2996 -class => "list"}, esc_path
($t->{'name'}));
2997 if (S_ISLNK
(oct $t->{'mode'})) {
2998 my $link_target = git_get_link_target
($t->{'hash'});
3000 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3001 if (defined $norm_target) {
3003 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3004 file_name
=>$norm_target),
3005 -title
=> $norm_target}, esc_path
($link_target));
3007 print " -> " . esc_path
($link_target);
3012 print "<td class=\"link\">";
3013 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3014 file_name
=>"$basedir$t->{'name'}", %base_key)},
3018 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3019 file_name
=>"$basedir$t->{'name'}", %base_key)},
3022 if (defined $hash_base) {
3024 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3025 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3029 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3030 file_name
=>"$basedir$t->{'name'}")},
3034 } elsif ($t->{'type'} eq "tree") {
3035 print "<td class=\"list\">";
3036 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3037 file_name
=>"$basedir$t->{'name'}", %base_key)},
3038 esc_path
($t->{'name'}));
3040 print "<td class=\"link\">";
3041 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3042 file_name
=>"$basedir$t->{'name'}", %base_key)},
3044 if (defined $hash_base) {
3046 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3047 file_name
=>"$basedir$t->{'name'}")},
3052 # unknown object: we can only present history for it
3053 # (this includes 'commit' object, i.e. submodule support)
3054 print "<td class=\"list\">" .
3055 esc_path
($t->{'name'}) .
3057 print "<td class=\"link\">";
3058 if (defined $hash_base) {
3059 print $cgi->a({-href
=> href
(action
=>"history",
3060 hash_base
=>$hash_base,
3061 file_name
=>"$basedir$t->{'name'}")},
3068 ## ......................................................................
3069 ## functions printing large fragments of HTML
3071 # get pre-image filenames for merge (combined) diff
3072 sub fill_from_file_info
{
3073 my ($diff, @parents) = @_;
3075 $diff->{'from_file'} = [ ];
3076 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3077 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3078 if ($diff->{'status'}[$i] eq 'R' ||
3079 $diff->{'status'}[$i] eq 'C') {
3080 $diff->{'from_file'}[$i] =
3081 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3088 # is current raw difftree line of file deletion
3090 my $diffinfo = shift;
3092 return $diffinfo->{'to_id'} eq ('0' x
40);
3095 # does patch correspond to [previous] difftree raw line
3096 # $diffinfo - hashref of parsed raw diff format
3097 # $patchinfo - hashref of parsed patch diff format
3098 # (the same keys as in $diffinfo)
3099 sub is_patch_split
{
3100 my ($diffinfo, $patchinfo) = @_;
3102 return defined $diffinfo && defined $patchinfo
3103 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3107 sub git_difftree_body
{
3108 my ($difftree, $hash, @parents) = @_;
3109 my ($parent) = $parents[0];
3110 my ($have_blame) = gitweb_check_feature
('blame');
3111 print "<div class=\"list_head\">\n";
3112 if ($#{$difftree} > 10) {
3113 print(($#{$difftree} + 1) . " files changed:\n");
3117 print "<table class=\"" .
3118 (@parents > 1 ?
"combined " : "") .
3121 # header only for combined diff in 'commitdiff' view
3122 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
3125 print "<thead><tr>\n" .
3126 "<th></th><th></th>\n"; # filename, patchN link
3127 for (my $i = 0; $i < @parents; $i++) {
3128 my $par = $parents[$i];
3130 $cgi->a({-href
=> href
(action
=>"commitdiff",
3131 hash
=>$hash, hash_parent
=>$par),
3132 -title
=> 'commitdiff to parent number ' .
3133 ($i+1) . ': ' . substr($par,0,7)},
3137 print "</tr></thead>\n<tbody>\n";
3142 foreach my $line (@
{$difftree}) {
3143 my $diff = parsed_difftree_line
($line);
3146 print "<tr class=\"dark\">\n";
3148 print "<tr class=\"light\">\n";
3152 if (exists $diff->{'nparents'}) { # combined diff
3154 fill_from_file_info
($diff, @parents)
3155 unless exists $diff->{'from_file'};
3157 if (!is_deleted
($diff)) {
3158 # file exists in the result (child) commit
3160 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3161 file_name
=>$diff->{'to_file'},
3163 -class => "list"}, esc_path
($diff->{'to_file'})) .
3167 esc_path
($diff->{'to_file'}) .
3171 if ($action eq 'commitdiff') {
3174 print "<td class=\"link\">" .
3175 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3180 my $has_history = 0;
3181 my $not_deleted = 0;
3182 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3183 my $hash_parent = $parents[$i];
3184 my $from_hash = $diff->{'from_id'}[$i];
3185 my $from_path = $diff->{'from_file'}[$i];
3186 my $status = $diff->{'status'}[$i];
3188 $has_history ||= ($status ne 'A');
3189 $not_deleted ||= ($status ne 'D');
3191 if ($status eq 'A') {
3192 print "<td class=\"link\" align=\"right\"> | </td>\n";
3193 } elsif ($status eq 'D') {
3194 print "<td class=\"link\">" .
3195 $cgi->a({-href
=> href
(action
=>"blob",
3198 file_name
=>$from_path)},
3202 if ($diff->{'to_id'} eq $from_hash) {
3203 print "<td class=\"link nochange\">";
3205 print "<td class=\"link\">";
3207 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3208 hash
=>$diff->{'to_id'},
3209 hash_parent
=>$from_hash,
3211 hash_parent_base
=>$hash_parent,
3212 file_name
=>$diff->{'to_file'},
3213 file_parent
=>$from_path)},
3219 print "<td class=\"link\">";
3221 print $cgi->a({-href
=> href
(action
=>"blob",
3222 hash
=>$diff->{'to_id'},
3223 file_name
=>$diff->{'to_file'},
3226 print " | " if ($has_history);
3229 print $cgi->a({-href
=> href
(action
=>"history",
3230 file_name
=>$diff->{'to_file'},
3237 next; # instead of 'else' clause, to avoid extra indent
3239 # else ordinary diff
3241 my ($to_mode_oct, $to_mode_str, $to_file_type);
3242 my ($from_mode_oct, $from_mode_str, $from_file_type);
3243 if ($diff->{'to_mode'} ne ('0' x
6)) {
3244 $to_mode_oct = oct $diff->{'to_mode'};
3245 if (S_ISREG
($to_mode_oct)) { # only for regular file
3246 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3248 $to_file_type = file_type
($diff->{'to_mode'});
3250 if ($diff->{'from_mode'} ne ('0' x
6)) {
3251 $from_mode_oct = oct $diff->{'from_mode'};
3252 if (S_ISREG
($to_mode_oct)) { # only for regular file
3253 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3255 $from_file_type = file_type
($diff->{'from_mode'});
3258 if ($diff->{'status'} eq "A") { # created
3259 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3260 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3261 $mode_chng .= "]</span>";
3263 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3264 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3265 -class => "list"}, esc_path
($diff->{'file'}));
3267 print "<td>$mode_chng</td>\n";
3268 print "<td class=\"link\">";
3269 if ($action eq 'commitdiff') {
3272 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3275 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3276 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3280 } elsif ($diff->{'status'} eq "D") { # deleted
3281 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3283 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3284 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3285 -class => "list"}, esc_path
($diff->{'file'}));
3287 print "<td>$mode_chng</td>\n";
3288 print "<td class=\"link\">";
3289 if ($action eq 'commitdiff') {
3292 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3295 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3296 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3299 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3300 file_name
=>$diff->{'file'})},
3303 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3304 file_name
=>$diff->{'file'})},
3308 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3309 my $mode_chnge = "";
3310 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3311 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3312 if ($from_file_type ne $to_file_type) {
3313 $mode_chnge .= " from $from_file_type to $to_file_type";
3315 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3316 if ($from_mode_str && $to_mode_str) {
3317 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3318 } elsif ($to_mode_str) {
3319 $mode_chnge .= " mode: $to_mode_str";
3322 $mode_chnge .= "]</span>\n";
3325 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3326 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3327 -class => "list"}, esc_path
($diff->{'file'}));
3329 print "<td>$mode_chnge</td>\n";
3330 print "<td class=\"link\">";
3331 if ($action eq 'commitdiff') {
3334 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3336 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3337 # "commit" view and modified file (not onlu mode changed)
3338 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3339 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3340 hash_base
=>$hash, hash_parent_base
=>$parent,
3341 file_name
=>$diff->{'file'})},
3345 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3346 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3349 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3350 file_name
=>$diff->{'file'})},
3353 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3354 file_name
=>$diff->{'file'})},
3358 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3359 my %status_name = ('R' => 'moved', 'C' => 'copied');
3360 my $nstatus = $status_name{$diff->{'status'}};
3362 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3363 # mode also for directories, so we cannot use $to_mode_str
3364 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3367 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3368 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3369 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3370 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3371 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3372 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3373 -class => "list"}, esc_path
($diff->{'from_file'})) .
3374 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3375 "<td class=\"link\">";
3376 if ($action eq 'commitdiff') {
3379 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3381 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3382 # "commit" view and modified file (not only pure rename or copy)
3383 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3384 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3385 hash_base
=>$hash, hash_parent_base
=>$parent,
3386 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3390 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3391 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3394 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3395 file_name
=>$diff->{'to_file'})},
3398 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3399 file_name
=>$diff->{'to_file'})},
3403 } # we should not encounter Unmerged (U) or Unknown (X) status
3406 print "</tbody>" if $has_header;
3410 sub git_patchset_body
{
3411 my ($fd, $difftree, $hash, @hash_parents) = @_;
3412 my ($hash_parent) = $hash_parents[0];
3414 my $is_combined = (@hash_parents > 1);
3416 my $patch_number = 0;
3422 print "<div class=\"patchset\">\n";
3424 # skip to first patch
3425 while ($patch_line = <$fd>) {
3428 last if ($patch_line =~ m/^diff /);
3432 while ($patch_line) {
3434 # parse "git diff" header line
3435 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3436 # $1 is from_name, which we do not use
3437 $to_name = unquote
($2);
3438 $to_name =~ s!^b/!!;
3439 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3440 # $1 is 'cc' or 'combined', which we do not use
3441 $to_name = unquote
($2);
3446 # check if current patch belong to current raw line
3447 # and parse raw git-diff line if needed
3448 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3449 # this is continuation of a split patch
3450 print "<div class=\"patch cont\">\n";
3452 # advance raw git-diff output if needed
3453 $patch_idx++ if defined $diffinfo;
3455 # read and prepare patch information
3456 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3458 # compact combined diff output can have some patches skipped
3459 # find which patch (using pathname of result) we are at now;
3461 while ($to_name ne $diffinfo->{'to_file'}) {
3462 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3463 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3464 "</div>\n"; # class="patch"
3469 last if $patch_idx > $#$difftree;
3470 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3474 # modifies %from, %to hashes
3475 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3477 # this is first patch for raw difftree line with $patch_idx index
3478 # we index @$difftree array from 0, but number patches from 1
3479 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3483 #assert($patch_line =~ m/^diff /) if DEBUG;
3484 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3486 # print "git diff" header
3487 print format_git_diff_header_line
($patch_line, $diffinfo,
3490 # print extended diff header
3491 print "<div class=\"diff extended_header\">\n";
3493 while ($patch_line = <$fd>) {
3496 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3498 print format_extended_diff_header_line
($patch_line, $diffinfo,
3501 print "</div>\n"; # class="diff extended_header"
3503 # from-file/to-file diff header
3504 if (! $patch_line) {
3505 print "</div>\n"; # class="patch"
3508 next PATCH
if ($patch_line =~ m/^diff /);
3509 #assert($patch_line =~ m/^---/) if DEBUG;
3511 my $last_patch_line = $patch_line;
3512 $patch_line = <$fd>;
3514 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3516 print format_diff_from_to_header
($last_patch_line, $patch_line,
3517 $diffinfo, \
%from, \
%to,
3522 while ($patch_line = <$fd>) {
3525 next PATCH
if ($patch_line =~ m/^diff /);
3527 print format_diff_line
($patch_line, \
%from, \
%to);
3531 print "</div>\n"; # class="patch"
3534 # for compact combined (--cc) format, with chunk and patch simpliciaction
3535 # patchset might be empty, but there might be unprocessed raw lines
3536 for (++$patch_idx if $patch_number > 0;
3537 $patch_idx < @
$difftree;
3539 # read and prepare patch information
3540 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3542 # generate anchor for "patch" links in difftree / whatchanged part
3543 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3544 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3545 "</div>\n"; # class="patch"
3550 if ($patch_number == 0) {
3551 if (@hash_parents > 1) {
3552 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3554 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3558 print "</div>\n"; # class="patchset"
3561 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3563 sub git_project_list_body
{
3564 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3566 my ($check_forks) = gitweb_check_feature
('forks');
3569 foreach my $pr (@
$projlist) {
3570 my (@aa) = git_get_last_activity
($pr->{'path'});
3574 ($pr->{'age'}, $pr->{'age_string'}) = @aa;
3575 if (!defined $pr->{'descr'}) {
3576 my $descr = git_get_project_description
($pr->{'path'}) || "";
3577 $pr->{'descr_long'} = to_utf8
($descr);
3578 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3580 if (!defined $pr->{'owner'}) {
3581 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3584 my $pname = $pr->{'path'};
3585 if (($pname =~ s/\.git$//) &&
3586 ($pname !~ /\/$/) &&
3587 (-d
"$projectroot/$pname")) {
3588 $pr->{'forks'} = "-d $projectroot/$pname";
3594 push @projects, $pr;
3597 $order ||= $default_projects_order;
3598 $from = 0 unless defined $from;
3599 $to = $#projects if (!defined $to || $#projects < $to);
3601 print "<table class=\"project_list\">\n";
3602 unless ($no_header) {
3605 print "<th></th>\n";
3607 if ($order eq "project") {
3608 @projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
3609 print "<th>Project</th>\n";
3612 $cgi->a({-href
=> href
(project
=>undef, order
=>'project'),
3613 -class => "header"}, "Project") .
3616 if ($order eq "descr") {
3617 @projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
3618 print "<th>Description</th>\n";
3621 $cgi->a({-href
=> href
(project
=>undef, order
=>'descr'),
3622 -class => "header"}, "Description") .
3625 if ($order eq "owner") {
3626 @projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
3627 print "<th>Owner</th>\n";
3630 $cgi->a({-href
=> href
(project
=>undef, order
=>'owner'),
3631 -class => "header"}, "Owner") .
3634 if ($order eq "age") {
3635 @projects = sort {$a->{'age'} <=> $b->{'age'}} @projects;
3636 print "<th>Last Change</th>\n";
3639 $cgi->a({-href
=> href
(project
=>undef, order
=>'age'),
3640 -class => "header"}, "Last Change") .
3643 print "<th></th>\n" .
3647 for (my $i = $from; $i <= $to; $i++) {
3648 my $pr = $projects[$i];
3650 print "<tr class=\"dark\">\n";
3652 print "<tr class=\"light\">\n";
3657 if ($pr->{'forks'}) {
3658 print "<!-- $pr->{'forks'} -->\n";
3659 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3663 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3664 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3665 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3666 -class => "list", -title
=> $pr->{'descr_long'}},
3667 esc_html
($pr->{'descr'})) . "</td>\n" .
3668 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3669 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3670 (defined $pr->{'age_string'} ?
$pr->{'age_string'} : "No commits") . "</td>\n" .
3671 "<td class=\"link\">" .
3672 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3673 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3674 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3675 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3676 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3680 if (defined $extra) {
3683 print "<td></td>\n";
3685 print "<td colspan=\"5\">$extra</td>\n" .
3691 sub git_shortlog_body
{
3692 # uses global variable $project
3693 my ($commitlist, $from, $to, $refs, $extra) = @_;
3695 $from = 0 unless defined $from;
3696 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3698 print "<table class=\"shortlog\">\n";
3700 for (my $i = $from; $i <= $to; $i++) {
3701 my %co = %{$commitlist->[$i]};
3702 my $commit = $co{'id'};
3703 my $ref = format_ref_marker
($refs, $commit);
3705 print "<tr class=\"dark\">\n";
3707 print "<tr class=\"light\">\n";
3710 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3711 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3712 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3713 "<td><i>" . $author . "</i></td>\n" .
3715 print format_subject_html
($co{'title'}, $co{'title_short'},
3716 href
(action
=>"commit", hash
=>$commit), $ref);
3718 "<td class=\"link\">" .
3719 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3720 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3721 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3722 my $snapshot_links = format_snapshot_links
($commit);
3723 if (defined $snapshot_links) {
3724 print " | " . $snapshot_links;
3729 if (defined $extra) {
3731 "<td colspan=\"4\">$extra</td>\n" .
3737 sub git_history_body
{
3738 # Warning: assumes constant type (blob or tree) during history
3739 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3741 $from = 0 unless defined $from;
3742 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3744 print "<table class=\"history\">\n";
3746 for (my $i = $from; $i <= $to; $i++) {
3747 my %co = %{$commitlist->[$i]};
3751 my $commit = $co{'id'};
3753 my $ref = format_ref_marker
($refs, $commit);
3756 print "<tr class=\"dark\">\n";
3758 print "<tr class=\"light\">\n";
3761 # shortlog uses chop_str($co{'author_name'}, 10)
3762 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
3763 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3764 "<td><i>" . $author . "</i></td>\n" .
3766 # originally git_history used chop_str($co{'title'}, 50)
3767 print format_subject_html
($co{'title'}, $co{'title_short'},
3768 href
(action
=>"commit", hash
=>$commit), $ref);
3770 "<td class=\"link\">" .
3771 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
3772 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
3774 if ($ftype eq 'blob') {
3775 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
3776 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
3777 if (defined $blob_current && defined $blob_parent &&
3778 $blob_current ne $blob_parent) {
3780 $cgi->a({-href
=> href
(action
=>"blobdiff",
3781 hash
=>$blob_current, hash_parent
=>$blob_parent,
3782 hash_base
=>$hash_base, hash_parent_base
=>$commit,
3783 file_name
=>$file_name)},
3790 if (defined $extra) {
3792 "<td colspan=\"4\">$extra</td>\n" .
3799 # uses global variable $project
3800 my ($taglist, $from, $to, $extra) = @_;
3801 $from = 0 unless defined $from;
3802 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3804 print "<table class=\"tags\">\n";
3806 for (my $i = $from; $i <= $to; $i++) {
3807 my $entry = $taglist->[$i];
3809 my $comment = $tag{'subject'};
3811 if (defined $comment) {
3812 $comment_short = chop_str
($comment, 30, 5);
3815 print "<tr class=\"dark\">\n";
3817 print "<tr class=\"light\">\n";
3820 if (defined $tag{'age'}) {
3821 print "<td><i>$tag{'age'}</i></td>\n";
3823 print "<td></td>\n";
3826 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
3827 -class => "list name"}, esc_html
($tag{'name'})) .
3830 if (defined $comment) {
3831 print format_subject_html
($comment, $comment_short,
3832 href
(action
=>"tag", hash
=>$tag{'id'}));
3835 "<td class=\"selflink\">";
3836 if ($tag{'type'} eq "tag") {
3837 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
3842 "<td class=\"link\">" . " | " .
3843 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
3844 if ($tag{'reftype'} eq "commit") {
3845 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
3846 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
3847 } elsif ($tag{'reftype'} eq "blob") {
3848 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
3853 if (defined $extra) {
3855 "<td colspan=\"5\">$extra</td>\n" .
3861 sub git_heads_body
{
3862 # uses global variable $project
3863 my ($headlist, $head, $from, $to, $extra) = @_;
3864 $from = 0 unless defined $from;
3865 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3867 print "<table class=\"heads\">\n";
3869 for (my $i = $from; $i <= $to; $i++) {
3870 my $entry = $headlist->[$i];
3872 my $curr = $ref{'id'} eq $head;
3874 print "<tr class=\"dark\">\n";
3876 print "<tr class=\"light\">\n";
3879 print "<td><i>$ref{'age'}</i></td>\n" .
3880 ($curr ?
"<td class=\"current_head\">" : "<td>") .
3881 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
3882 -class => "list name"},esc_html
($ref{'name'})) .
3884 "<td class=\"link\">" .
3885 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
3886 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
3887 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
3891 if (defined $extra) {
3893 "<td colspan=\"3\">$extra</td>\n" .
3899 sub git_search_grep_body
{
3900 my ($commitlist, $from, $to, $extra) = @_;
3901 $from = 0 unless defined $from;
3902 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3904 print "<table class=\"commit_search\">\n";
3906 for (my $i = $from; $i <= $to; $i++) {
3907 my %co = %{$commitlist->[$i]};
3911 my $commit = $co{'id'};
3913 print "<tr class=\"dark\">\n";
3915 print "<tr class=\"light\">\n";
3918 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
3919 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3920 "<td><i>" . $author . "</i></td>\n" .
3922 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
3923 -class => "list subject"},
3924 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
3925 my $comment = $co{'comment'};
3926 foreach my $line (@
$comment) {
3927 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3928 my ($lead, $match, $trail) = ($1, $2, $3);
3929 $match = chop_str
($match, 70, 5, 'center');
3930 my $contextlen = int((80 - length($match))/2);
3931 $contextlen = 30 if ($contextlen > 30);
3932 $lead = chop_str
($lead, $contextlen, 10, 'left');
3933 $trail = chop_str
($trail, $contextlen, 10, 'right');
3935 $lead = esc_html
($lead);
3936 $match = esc_html
($match);
3937 $trail = esc_html
($trail);
3939 print "$lead<span class=\"match\">$match</span>$trail<br />";
3943 "<td class=\"link\">" .
3944 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
3946 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
3948 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
3952 if (defined $extra) {
3954 "<td colspan=\"3\">$extra</td>\n" .
3960 ## ======================================================================
3961 ## ======================================================================
3964 sub git_project_list
{
3965 my $order = $cgi->param('o');
3966 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3967 die_error
(undef, "Unknown order parameter");
3970 my @list = git_get_projects_list
();
3972 die_error
(undef, "No projects found");
3976 if (-f
$home_text) {
3977 print "<div class=\"index_include\">\n";
3978 open (my $fd, $home_text);
3983 git_project_list_body
(\
@list, $order);
3988 my $order = $cgi->param('o');
3989 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3990 die_error
(undef, "Unknown order parameter");
3993 my @list = git_get_projects_list
($project);
3995 die_error
(undef, "No forks found");
3999 git_print_page_nav
('','');
4000 git_print_header_div
('summary', "$project forks");
4001 git_project_list_body
(\
@list, $order);
4005 sub git_project_index
{
4006 my @projects = git_get_projects_list
($project);
4009 -type
=> 'text/plain',
4010 -charset
=> 'utf-8',
4011 -content_disposition
=> 'inline; filename="index.aux"');
4013 foreach my $pr (@projects) {
4014 if (!exists $pr->{'owner'}) {
4015 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4018 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4019 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4020 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4021 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4025 print "$path $owner\n";
4030 my $descr = git_get_project_description
($project) || "none";
4031 my %co = parse_commit
("HEAD");
4032 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4033 my $head = $co{'id'};
4035 my $owner = git_get_project_owner
($project);
4037 my $refs = git_get_references
();
4038 # These get_*_list functions return one more to allow us to see if
4039 # there are more ...
4040 my @taglist = git_get_tags_list
(16);
4041 my @headlist = git_get_heads_list
(16);
4043 my ($check_forks) = gitweb_check_feature
('forks');
4046 @forklist = git_get_projects_list
($project);
4050 git_print_page_nav
('summary','', $head);
4052 print "<div class=\"title\"> </div>\n";
4053 print "<table class=\"projects_list\">\n" .
4054 "<tr><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4055 "<tr><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4056 if (defined $cd{'rfc2822'}) {
4057 print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4060 # use per project git URL list in $projectroot/$project/cloneurl
4061 # or make project git URL from git base URL and project name
4062 my $url_tag = "URL";
4063 my @url_list = git_get_project_url_list
($project);
4064 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4065 foreach my $git_url (@url_list) {
4066 next unless $git_url;
4067 print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
4072 if (-s
"$projectroot/$project/README.html") {
4073 if (open my $fd, "$projectroot/$project/README.html") {
4074 print "<div class=\"title\">readme</div>\n" .
4075 "<div class=\"readme\">\n";
4076 print $_ while (<$fd>);
4077 print "\n</div>\n"; # class="readme"
4082 # we need to request one more than 16 (0..15) to check if
4084 my @commitlist = $head ? parse_commits
($head, 17) : ();
4086 git_print_header_div
('shortlog');
4087 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4088 $#commitlist <= 15 ?
undef :
4089 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4093 git_print_header_div
('tags');
4094 git_tags_body
(\
@taglist, 0, 15,
4095 $#taglist <= 15 ?
undef :
4096 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4100 git_print_header_div
('heads');
4101 git_heads_body
(\
@headlist, $head, 0, 15,
4102 $#headlist <= 15 ?
undef :
4103 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4107 git_print_header_div
('forks');
4108 git_project_list_body
(\
@forklist, undef, 0, 15,
4109 $#forklist <= 15 ?
undef :
4110 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4118 my $head = git_get_head_hash
($project);
4120 git_print_page_nav
('','', $head,undef,$head);
4121 my %tag = parse_tag
($hash);
4124 die_error
(undef, "Unknown tag object");
4127 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4128 print "<div class=\"title_text\">\n" .
4129 "<table class=\"object_header\">\n" .
4131 "<td>object</td>\n" .
4132 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4133 $tag{'object'}) . "</td>\n" .
4134 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4135 $tag{'type'}) . "</td>\n" .
4137 if (defined($tag{'author'})) {
4138 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4139 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4140 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4141 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4144 print "</table>\n\n" .
4146 print "<div class=\"page_body\">";
4147 my $comment = $tag{'comment'};
4148 foreach my $line (@
$comment) {
4150 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4160 my ($have_blame) = gitweb_check_feature
('blame');
4162 die_error
('403 Permission denied', "Permission denied");
4164 die_error
('404 Not Found', "File name not defined") if (!$file_name);
4165 $hash_base ||= git_get_head_hash
($project);
4166 die_error
(undef, "Couldn't find base commit") unless ($hash_base);
4167 my %co = parse_commit
($hash_base)
4168 or die_error
(undef, "Reading commit failed");
4169 if (!defined $hash) {
4170 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4171 or die_error
(undef, "Error looking up file");
4173 $ftype = git_get_type
($hash);
4174 if ($ftype !~ "blob") {
4175 die_error
('400 Bad Request', "Object is not a blob");
4177 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4178 $file_name, $hash_base)
4179 or die_error
(undef, "Open git-blame failed");
4182 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4185 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4188 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4190 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4191 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4192 git_print_page_path
($file_name, $ftype, $hash_base);
4193 my @rev_color = (qw(light2 dark2));
4194 my $num_colors = scalar(@rev_color);
4195 my $current_color = 0;
4198 <div class="page_body">
4199 <table class="blame">
4200 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4205 last unless defined $_;
4206 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4207 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4208 if (!exists $metainfo{$full_rev}) {
4209 $metainfo{$full_rev} = {};
4211 my $meta = $metainfo{$full_rev};
4214 if (/^(\S+) (.*)$/) {
4220 my $rev = substr($full_rev, 0, 8);
4221 my $author = $meta->{'author'};
4222 my %date = parse_date
($meta->{'author-time'},
4223 $meta->{'author-tz'});
4224 my $date = $date{'iso-tz'};
4226 $current_color = ++$current_color % $num_colors;
4228 print "<tr class=\"$rev_color[$current_color]\">\n";
4230 print "<td class=\"sha1\"";
4231 print " title=\"". esc_html
($author) . ", $date\"";
4232 print " rowspan=\"$group_size\"" if ($group_size > 1);
4234 print $cgi->a({-href
=> href
(action
=>"commit",
4236 file_name
=>$file_name)},
4240 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4241 or die_error
(undef, "Open git-rev-parse failed");
4242 my $parent_commit = <$dd>;
4244 chomp($parent_commit);
4245 my $blamed = href
(action
=> 'blame',
4246 file_name
=> $meta->{'filename'},
4247 hash_base
=> $parent_commit);
4248 print "<td class=\"linenr\">";
4249 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4251 -class => "linenr" },
4254 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4260 or print "Reading blob failed\n";
4267 my ($have_blame) = gitweb_check_feature
('blame');
4269 die_error
('403 Permission denied', "Permission denied");
4271 die_error
('404 Not Found', "File name not defined") if (!$file_name);
4272 $hash_base ||= git_get_head_hash
($project);
4273 die_error
(undef, "Couldn't find base commit") unless ($hash_base);
4274 my %co = parse_commit
($hash_base)
4275 or die_error
(undef, "Reading commit failed");
4276 if (!defined $hash) {
4277 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4278 or die_error
(undef, "Error lookup file");
4280 open ($fd, "-|", git_cmd
(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
4281 or die_error
(undef, "Open git-annotate failed");
4284 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$hash, hash_base
=>$hash_base, file_name
=>$file_name)},
4287 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base, file_name
=>$file_name)},
4290 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4292 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4293 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4294 git_print_page_path
($file_name, 'blob', $hash_base);
4295 print "<div class=\"page_body\">\n";
4297 <table class="blame">
4306 my @line_class = (qw(light dark));
4307 my $line_class_len = scalar (@line_class);
4308 my $line_class_num = $#line_class;
4309 while (my $line = <$fd>) {
4321 $line_class_num = ($line_class_num + 1) % $line_class_len;
4323 if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
4330 print qq( <tr
><td colspan
="5" class="error">Unable to parse
: $line</td></tr
>\n);
4333 $short_rev = substr ($long_rev, 0, 8);
4334 $age = time () - $time;
4335 $age_str = age_string
($age);
4336 $age_str =~ s/ / /g;
4337 $age_class = age_class
($age);
4338 $author = esc_html
($author);
4339 $author =~ s/ / /g;
4341 $data = untabify
($data);
4342 $data = esc_html
($data);
4345 <tr class="$line_class[$line_class_num]">
4346 <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
4347 <td class="$age_class">$age_str</td>
4349 <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
4350 <td class="pre">$data</td>
4353 } # while (my $line = <$fd>)
4354 print "</table>\n\n";
4356 or print "Reading blob failed.\n";
4362 my $head = git_get_head_hash
($project);
4364 git_print_page_nav
('','', $head,undef,$head);
4365 git_print_header_div
('summary', $project);
4367 my @tagslist = git_get_tags_list
();
4369 git_tags_body
(\
@tagslist);
4375 my $head = git_get_head_hash
($project);
4377 git_print_page_nav
('','', $head,undef,$head);
4378 git_print_header_div
('summary', $project);
4380 my @headslist = git_get_heads_list
();
4382 git_heads_body
(\
@headslist, $head);
4387 sub git_blob_plain
{
4391 if (!defined $hash) {
4392 if (defined $file_name) {
4393 my $base = $hash_base || git_get_head_hash
($project);
4394 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4395 or die_error
(undef, "Error lookup file");
4397 die_error
(undef, "No file name defined");
4399 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4400 # blobs defined by non-textual hash id's can be cached
4404 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4405 or die_error
(undef, "Open git-cat-file blob '$hash' failed");
4407 # content-type (can include charset)
4408 $type = blob_contenttype
($fd, $file_name, $type);
4410 # "save as" filename, even when no $file_name is given
4411 my $save_as = "$hash";
4412 if (defined $file_name) {
4413 $save_as = $file_name;
4414 } elsif ($type =~ m/^text\//) {
4420 -expires
=> $expires,
4421 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4423 binmode STDOUT
, ':raw';
4425 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4433 if (!defined $hash) {
4434 if (defined $file_name) {
4435 my $base = $hash_base || git_get_head_hash
($project);
4436 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4437 or die_error
(undef, "Error lookup file");
4439 die_error
(undef, "No file name defined");
4441 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4442 # blobs defined by non-textual hash id's can be cached
4446 my ($have_blame) = gitweb_check_feature
('blame');
4447 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4448 or die_error
(undef, "Couldn't cat $file_name, $hash");
4449 my $mimetype = blob_mimetype
($fd, $file_name);
4450 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4452 return git_blob_plain
($mimetype);
4454 # we can have blame only for text/* mimetype
4455 $have_blame &&= ($mimetype =~ m!^text/!);
4457 git_header_html
(undef, $expires);
4458 my $formats_nav = '';
4459 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4460 if (defined $file_name) {
4463 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4468 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4471 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4474 $cgi->a({-href
=> href
(action
=>"blob",
4475 hash_base
=>"HEAD", file_name
=>$file_name)},
4479 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4482 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4483 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4485 print "<div class=\"page_nav\">\n" .
4486 "<br/><br/></div>\n" .
4487 "<div class=\"title\">$hash</div>\n";
4489 git_print_page_path
($file_name, "blob", $hash_base);
4490 print "<div class=\"page_body\">\n";
4491 if ($mimetype =~ m!^image/!) {
4492 print qq!<img type
="$mimetype"!;
4494 print qq! alt
="$file_name" title
="$file_name"!;
4497 href(action=>"blob_plain
", hash=>$hash,
4498 hash_base=>$hash_base, file_name=>$file_name) .
4502 while (my $line = <$fd>) {
4505 $line = untabify
($line);
4506 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4507 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4511 or print "Reading blob failed.\n";
4517 if (!defined $hash_base) {
4518 $hash_base = "HEAD";
4520 if (!defined $hash) {
4521 if (defined $file_name) {
4522 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4528 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4529 or die_error
(undef, "Open git-ls-tree failed");
4530 my @entries = map { chomp; $_ } <$fd>;
4531 close $fd or die_error
(undef, "Reading tree failed");
4534 my $refs = git_get_references
();
4535 my $ref = format_ref_marker
($refs, $hash_base);
4538 my ($have_blame) = gitweb_check_feature
('blame');
4539 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4541 if (defined $file_name) {
4543 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4545 $cgi->a({-href
=> href
(action
=>"tree",
4546 hash_base
=>"HEAD", file_name
=>$file_name)},
4549 my $snapshot_links = format_snapshot_links
($hash);
4550 if (defined $snapshot_links) {
4551 # FIXME: Should be available when we have no hash base as well.
4552 push @views_nav, $snapshot_links;
4554 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4555 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4558 print "<div class=\"page_nav\">\n";
4559 print "<br/><br/></div>\n";
4560 print "<div class=\"title\">$hash</div>\n";
4562 if (defined $file_name) {
4563 $basedir = $file_name;
4564 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4568 git_print_page_path
($file_name, 'tree', $hash_base);
4569 print "<div class=\"page_body\">\n";
4570 print "<table class=\"tree\">\n";
4572 # '..' (top directory) link if possible
4573 if (defined $hash_base &&
4574 defined $file_name && $file_name =~ m![^/]+$!) {
4576 print "<tr class=\"dark\">\n";
4578 print "<tr class=\"light\">\n";
4582 my $up = $file_name;
4583 $up =~ s!/?[^/]+$!!;
4584 undef $up unless $up;
4585 # based on git_print_tree_entry
4586 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4587 print '<td class="list">';
4588 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4592 print "<td class=\"link\"></td>\n";
4596 foreach my $line (@entries) {
4597 my %t = parse_ls_tree_line
($line, -z
=> 1);
4600 print "<tr class=\"dark\">\n";
4602 print "<tr class=\"light\">\n";
4606 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4610 print "</table>\n" .
4616 my @supported_fmts = gitweb_check_feature
('snapshot');
4617 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4619 my $format = $cgi->param('sf');
4620 if (!@supported_fmts) {
4621 die_error
('403 Permission denied', "Permission denied");
4623 # default to first supported snapshot format
4624 $format ||= $supported_fmts[0];
4625 if ($format !~ m/^[a-z0-9]+$/) {
4626 die_error
(undef, "Invalid snapshot format parameter");
4627 } elsif (!exists($known_snapshot_formats{$format})) {
4628 die_error
(undef, "Unknown snapshot format");
4629 } elsif (!grep($_ eq $format, @supported_fmts)) {
4630 die_error
(undef, "Unsupported snapshot format");
4633 if (!defined $hash) {
4634 $hash = git_get_head_hash
($project);
4637 my $name = $project;
4638 $name =~ s
,([^/])/*\
.git
$,$1,;
4639 $name = basename
($name);
4640 my $filename = to_utf8
($name);
4641 $name =~ s/\047/\047\\\047\047/g;
4643 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4644 $cmd = quote_command
(
4645 git_cmd
(), 'archive',
4646 "--format=$known_snapshot_formats{$format}{'format'}",
4647 "--prefix=$name/", $hash);
4648 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4649 $cmd .= ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}});
4653 -type
=> $known_snapshot_formats{$format}{'type'},
4654 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4655 -status
=> '200 OK');
4657 open my $fd, "-|", $cmd
4658 or die_error
(undef, "Execute git-archive failed");
4659 binmode STDOUT
, ':raw';
4661 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4666 my $head = git_get_head_hash
($project);
4667 if (!defined $hash) {
4670 if (!defined $page) {
4673 my $refs = git_get_references
();
4675 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4677 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4680 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4683 my %co = parse_commit
($hash);
4685 git_print_header_div
('summary', $project);
4686 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4688 my $to = ($#commitlist >= 99) ?
(99) : ($#commitlist);
4689 for (my $i = 0; $i <= $to; $i++) {
4690 my %co = %{$commitlist[$i]};
4692 my $commit = $co{'id'};
4693 my $ref = format_ref_marker
($refs, $commit);
4694 my %ad = parse_date
($co{'author_epoch'});
4695 git_print_header_div
('commit',
4696 "<span class=\"age\">$co{'age_string'}</span>" .
4697 esc_html
($co{'title'}) . $ref,
4699 print "<div class=\"title_text\">\n" .
4700 "<div class=\"log_link\">\n" .
4701 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4703 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4705 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4708 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4711 print "<div class=\"log_body\">\n";
4712 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4715 if ($#commitlist >= 100) {
4716 print "<div class=\"page_nav\">\n";
4717 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4718 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4725 $hash ||= $hash_base || "HEAD";
4726 my %co = parse_commit
($hash);
4728 die_error
(undef, "Unknown commit object");
4730 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4731 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4733 my $parent = $co{'parent'};
4734 my $parents = $co{'parents'}; # listref
4736 # we need to prepare $formats_nav before any parameter munging
4738 if (!defined $parent) {
4740 $formats_nav .= '(initial)';
4741 } elsif (@
$parents == 1) {
4742 # single parent commit
4745 $cgi->a({-href
=> href
(action
=>"commit",
4747 esc_html
(substr($parent, 0, 7))) .
4754 $cgi->a({-href
=> href
(action
=>"commit",
4756 esc_html
(substr($_, 0, 7)));
4761 if (!defined $parent) {
4765 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4767 (@
$parents <= 1 ?
$parent : '-c'),
4769 or die_error
(undef, "Open git-diff-tree failed");
4770 @difftree = map { chomp; $_ } <$fd>;
4771 close $fd or die_error
(undef, "Reading git-diff-tree failed");
4773 # non-textual hash id's can be cached
4775 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4778 my $refs = git_get_references
();
4779 my $ref = format_ref_marker
($refs, $co{'id'});
4781 git_header_html
(undef, $expires);
4782 git_print_page_nav
('commit', '',
4783 $hash, $co{'tree'}, $hash,
4786 if (defined $co{'parent'}) {
4787 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4789 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4791 print "<div class=\"title_text\">\n" .
4792 "<table class=\"object_header\">\n";
4793 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4795 "<td></td><td> $ad{'rfc2822'}";
4796 if ($ad{'hour_local'} < 6) {
4797 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4798 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4800 printf(" (%02d:%02d %s)",
4801 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4805 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4806 print "<tr><td></td><td> $cd{'rfc2822'}" .
4807 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4809 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4812 "<td class=\"sha1\">" .
4813 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4814 class => "list"}, $co{'tree'}) .
4816 "<td class=\"link\">" .
4817 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4819 my $snapshot_links = format_snapshot_links
($hash);
4820 if (defined $snapshot_links) {
4821 print " | " . $snapshot_links;
4826 foreach my $par (@
$parents) {
4829 "<td class=\"sha1\">" .
4830 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4831 class => "list"}, $par) .
4833 "<td class=\"link\">" .
4834 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4836 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
4843 print "<div class=\"page_body\">\n";
4844 git_print_log
($co{'comment'});
4847 git_difftree_body
(\
@difftree, $hash, @
$parents);
4853 # object is defined by:
4854 # - hash or hash_base alone
4855 # - hash_base and file_name
4858 # - hash or hash_base alone
4859 if ($hash || ($hash_base && !defined $file_name)) {
4860 my $object_id = $hash || $hash_base;
4862 open my $fd, "-|", quote_command
(
4863 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4864 or die_error
('404 Not Found', "Object does not exist");
4868 or die_error
('404 Not Found', "Object does not exist");
4870 # - hash_base and file_name
4871 } elsif ($hash_base && defined $file_name) {
4872 $file_name =~ s
,/+$,,;
4874 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
4875 or die_error
('404 Not Found', "Base object does not exist");
4877 # here errors should not hapen
4878 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
4879 or die_error
(undef, "Open git-ls-tree failed");
4883 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4884 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4885 die_error
('404 Not Found', "File or directory for given base does not exist");
4890 die_error
('404 Not Found', "Not enough information to find object");
4893 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
4894 hash
=>$hash, hash_base
=>$hash_base,
4895 file_name
=>$file_name),
4896 -status
=> '302 Found');
4900 my $format = shift || 'html';
4907 # preparing $fd and %diffinfo for git_patchset_body
4909 if (defined $hash_base && defined $hash_parent_base) {
4910 if (defined $file_name) {
4912 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4913 $hash_parent_base, $hash_base,
4914 "--", (defined $file_parent ?
$file_parent : ()), $file_name
4915 or die_error
(undef, "Open git-diff-tree failed");
4916 @difftree = map { chomp; $_ } <$fd>;
4918 or die_error
(undef, "Reading git-diff-tree failed");
4920 or die_error
('404 Not Found', "Blob diff not found");
4922 } elsif (defined $hash &&
4923 $hash =~ /[0-9a-fA-F]{40}/) {
4924 # try to find filename from $hash
4926 # read filtered raw output
4927 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4928 $hash_parent_base, $hash_base, "--"
4929 or die_error
(undef, "Open git-diff-tree failed");
4931 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4933 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4934 map { chomp; $_ } <$fd>;
4936 or die_error
(undef, "Reading git-diff-tree failed");
4938 or die_error
('404 Not Found', "Blob diff not found");
4941 die_error
('404 Not Found', "Missing one of the blob diff parameters");
4944 if (@difftree > 1) {
4945 die_error
('404 Not Found', "Ambiguous blob diff specification");
4948 %diffinfo = parse_difftree_raw_line
($difftree[0]);
4949 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4950 $file_name ||= $diffinfo{'to_file'};
4952 $hash_parent ||= $diffinfo{'from_id'};
4953 $hash ||= $diffinfo{'to_id'};
4955 # non-textual hash id's can be cached
4956 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4957 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4962 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4963 '-p', ($format eq 'html' ?
"--full-index" : ()),
4964 $hash_parent_base, $hash_base,
4965 "--", (defined $file_parent ?
$file_parent : ()), $file_name
4966 or die_error
(undef, "Open git-diff-tree failed");
4969 # old/legacy style URI
4970 if (!%diffinfo && # if new style URI failed
4971 defined $hash && defined $hash_parent) {
4972 # fake git-diff-tree raw output
4973 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4974 $diffinfo{'from_id'} = $hash_parent;
4975 $diffinfo{'to_id'} = $hash;
4976 if (defined $file_name) {
4977 if (defined $file_parent) {
4978 $diffinfo{'status'} = '2';
4979 $diffinfo{'from_file'} = $file_parent;
4980 $diffinfo{'to_file'} = $file_name;
4981 } else { # assume not renamed
4982 $diffinfo{'status'} = '1';
4983 $diffinfo{'from_file'} = $file_name;
4984 $diffinfo{'to_file'} = $file_name;
4986 } else { # no filename given
4987 $diffinfo{'status'} = '2';
4988 $diffinfo{'from_file'} = $hash_parent;
4989 $diffinfo{'to_file'} = $hash;
4992 # non-textual hash id's can be cached
4993 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4994 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4999 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
5000 '-p', ($format eq 'html' ?
"--full-index" : ()),
5001 $hash_parent, $hash, "--"
5002 or die_error
(undef, "Open git-diff failed");
5004 die_error
('404 Not Found', "Missing one of the blob diff parameters")
5009 if ($format eq 'html') {
5011 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5013 git_header_html
(undef, $expires);
5014 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5015 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5016 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5018 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5019 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5021 if (defined $file_name) {
5022 git_print_page_path
($file_name, "blob", $hash_base);
5024 print "<div class=\"page_path\"></div>\n";
5027 } elsif ($format eq 'plain') {
5029 -type
=> 'text/plain',
5030 -charset
=> 'utf-8',
5031 -expires
=> $expires,
5032 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
5034 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5037 die_error
(undef, "Unknown blobdiff format");
5041 if ($format eq 'html') {
5042 print "<div class=\"page_body\">\n";
5044 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5047 print "</div>\n"; # class="page_body"
5051 while (my $line = <$fd>) {
5052 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5053 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5057 last if $line =~ m!^\+\+\+!;
5065 sub git_blobdiff_plain
{
5066 git_blobdiff
('plain');
5069 sub git_commitdiff
{
5070 my $format = shift || 'html';
5071 $hash ||= $hash_base || "HEAD";
5072 my %co = parse_commit
($hash);
5074 die_error
(undef, "Unknown commit object");
5077 # choose format for commitdiff for merge
5078 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
5079 $hash_parent = '--cc';
5081 # we need to prepare $formats_nav before almost any parameter munging
5083 if ($format eq 'html') {
5085 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5088 if (defined $hash_parent &&
5089 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5090 # commitdiff with two commits given
5091 my $hash_parent_short = $hash_parent;
5092 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5093 $hash_parent_short = substr($hash_parent, 0, 7);
5097 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
5098 if ($co{'parents'}[$i] eq $hash_parent) {
5099 $formats_nav .= ' parent ' . ($i+1);
5103 $formats_nav .= ': ' .
5104 $cgi->a({-href
=> href
(action
=>"commitdiff",
5105 hash
=>$hash_parent)},
5106 esc_html
($hash_parent_short)) .
5108 } elsif (!$co{'parent'}) {
5110 $formats_nav .= ' (initial)';
5111 } elsif (scalar @
{$co{'parents'}} == 1) {
5112 # single parent commit
5115 $cgi->a({-href
=> href
(action
=>"commitdiff",
5116 hash
=>$co{'parent'})},
5117 esc_html
(substr($co{'parent'}, 0, 7))) .
5121 if ($hash_parent eq '--cc') {
5122 $formats_nav .= ' | ' .
5123 $cgi->a({-href
=> href
(action
=>"commitdiff",
5124 hash
=>$hash, hash_parent
=>'-c')},
5126 } else { # $hash_parent eq '-c'
5127 $formats_nav .= ' | ' .
5128 $cgi->a({-href
=> href
(action
=>"commitdiff",
5129 hash
=>$hash, hash_parent
=>'--cc')},
5135 $cgi->a({-href
=> href
(action
=>"commitdiff",
5137 esc_html
(substr($_, 0, 7)));
5138 } @
{$co{'parents'}} ) .
5143 my $hash_parent_param = $hash_parent;
5144 if (!defined $hash_parent_param) {
5145 # --cc for multiple parents, --root for parentless
5146 $hash_parent_param =
5147 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
5153 if ($format eq 'html') {
5154 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5155 "--no-commit-id", "--patch-with-raw", "--full-index",
5156 $hash_parent_param, $hash, "--"
5157 or die_error
(undef, "Open git-diff-tree failed");
5159 while (my $line = <$fd>) {
5161 # empty line ends raw part of diff-tree output
5163 push @difftree, scalar parse_difftree_raw_line
($line);
5166 } elsif ($format eq 'plain') {
5167 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5168 '-p', $hash_parent_param, $hash, "--"
5169 or die_error
(undef, "Open git-diff-tree failed");
5172 die_error
(undef, "Unknown commitdiff format");
5175 # non-textual hash id's can be cached
5177 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5181 # write commit message
5182 if ($format eq 'html') {
5183 my $refs = git_get_references
();
5184 my $ref = format_ref_marker
($refs, $co{'id'});
5186 git_header_html
(undef, $expires);
5187 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5188 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5189 git_print_authorship
(\
%co);
5190 print "<div class=\"page_body\">\n";
5191 if (@
{$co{'comment'}} > 1) {
5192 print "<div class=\"log\">\n";
5193 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5194 print "</div>\n"; # class="log"
5197 } elsif ($format eq 'plain') {
5198 my $refs = git_get_references
("tags");
5199 my $tagname = git_get_rev_name_tags
($hash);
5200 my $filename = basename
($project) . "-$hash.patch";
5203 -type
=> 'text/plain',
5204 -charset
=> 'utf-8',
5205 -expires
=> $expires,
5206 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5207 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5208 print "From: " . to_utf8
($co{'author'}) . "\n";
5209 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5210 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5212 print "X-Git-Tag: $tagname\n" if $tagname;
5213 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5215 foreach my $line (@
{$co{'comment'}}) {
5216 print to_utf8
($line) . "\n";
5222 if ($format eq 'html') {
5223 my $use_parents = !defined $hash_parent ||
5224 $hash_parent eq '-c' || $hash_parent eq '--cc';
5225 git_difftree_body
(\
@difftree, $hash,
5226 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5229 git_patchset_body
($fd, \
@difftree, $hash,
5230 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5232 print "</div>\n"; # class="page_body"
5235 } elsif ($format eq 'plain') {
5239 or print "Reading git-diff-tree failed\n";
5243 sub git_commitdiff_plain
{
5244 git_commitdiff
('plain');
5248 if (!defined $hash_base) {
5249 $hash_base = git_get_head_hash
($project);
5251 if (!defined $page) {
5255 my %co = parse_commit
($hash_base);
5257 die_error
(undef, "Unknown commit object");
5260 my $refs = git_get_references
();
5261 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5263 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5264 $file_name, "--full-history");
5266 die_error
('404 Not Found', "No such file or directory on given branch");
5269 if (!defined $hash && defined $file_name) {
5270 # some commits could have deleted file in question,
5271 # and not have it in tree, but one of them has to have it
5272 for (my $i = 0; $i <= @commitlist; $i++) {
5273 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5274 last if defined $hash;
5277 if (defined $hash) {
5278 $ftype = git_get_type
($hash);
5280 if (!defined $ftype) {
5281 die_error
(undef, "Unknown type of object");
5284 my $paging_nav = '';
5287 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5288 file_name
=>$file_name)},
5290 $paging_nav .= " ⋅ " .
5291 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5292 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5294 $paging_nav .= "first";
5295 $paging_nav .= " ⋅ prev";
5298 if ($#commitlist >= 100) {
5300 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5301 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5302 $paging_nav .= " ⋅ $next_link";
5304 $paging_nav .= " ⋅ next";
5308 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5309 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5310 git_print_page_path
($file_name, $ftype, $hash_base);
5312 git_history_body
(\
@commitlist, 0, 99,
5313 $refs, $hash_base, $ftype, $next_link);
5319 my ($have_search) = gitweb_check_feature
('search');
5320 if (!$have_search) {
5321 die_error
('403 Permission denied', "Permission denied");
5323 if (!defined $searchtext) {
5324 die_error
(undef, "Text field empty");
5326 if (!defined $hash) {
5327 $hash = git_get_head_hash
($project);
5329 my %co = parse_commit
($hash);
5331 die_error
(undef, "Unknown commit object");
5333 if (!defined $page) {
5337 $searchtype ||= 'commit';
5338 if ($searchtype eq 'pickaxe') {
5339 # pickaxe may take all resources of your box and run for several minutes
5340 # with every query - so decide by yourself how public you make this feature
5341 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5342 if (!$have_pickaxe) {
5343 die_error
('403 Permission denied', "Permission denied");
5346 if ($searchtype eq 'grep') {
5347 my ($have_grep) = gitweb_check_feature
('grep');
5349 die_error
('403 Permission denied', "Permission denied");
5355 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5357 if ($searchtype eq 'commit') {
5358 $greptype = "--grep=";
5359 } elsif ($searchtype eq 'author') {
5360 $greptype = "--author=";
5361 } elsif ($searchtype eq 'committer') {
5362 $greptype = "--committer=";
5364 $greptype .= $searchtext;
5365 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5366 $greptype, '--regexp-ignore-case',
5367 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
5369 my $paging_nav = '';
5372 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5373 searchtext
=>$searchtext,
5374 searchtype
=>$searchtype)},
5376 $paging_nav .= " ⋅ " .
5377 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5378 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5380 $paging_nav .= "first";
5381 $paging_nav .= " ⋅ prev";
5384 if ($#commitlist >= 100) {
5386 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5387 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5388 $paging_nav .= " ⋅ $next_link";
5390 $paging_nav .= " ⋅ next";
5393 if ($#commitlist >= 100) {
5396 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5397 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5398 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5401 if ($searchtype eq 'pickaxe') {
5402 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5403 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5405 print "<table class=\"pickaxe search\">\n";
5408 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5409 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5410 ($search_use_regexp ?
'--pickaxe-regex' : ());
5413 while (my $line = <$fd>) {
5417 my %set = parse_difftree_raw_line
($line);
5418 if (defined $set{'commit'}) {
5419 # finish previous commit
5422 "<td class=\"link\">" .
5423 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5425 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5431 print "<tr class=\"dark\">\n";
5433 print "<tr class=\"light\">\n";
5436 %co = parse_commit
($set{'commit'});
5437 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5438 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5439 "<td><i>$author</i></td>\n" .
5441 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5442 -class => "list subject"},
5443 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5444 } elsif (defined $set{'to_id'}) {
5445 next if ($set{'to_id'} =~ m/^0{40}$/);
5447 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5448 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5450 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5456 # finish last commit (warning: repetition!)
5459 "<td class=\"link\">" .
5460 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5462 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5470 if ($searchtype eq 'grep') {
5471 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5472 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5474 print "<table class=\"grep_search\">\n";
5478 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5479 $search_use_regexp ?
('-E', '-i') : '-F',
5480 $searchtext, $co{'tree'};
5482 while (my $line = <$fd>) {
5484 my ($file, $lno, $ltext, $binary);
5485 last if ($matches++ > 1000);
5486 if ($line =~ /^Binary file (.+) matches$/) {
5490 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5492 if ($file ne $lastfile) {
5493 $lastfile and print "</td></tr>\n";
5495 print "<tr class=\"dark\">\n";
5497 print "<tr class=\"light\">\n";
5499 print "<td class=\"list\">".
5500 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5501 file_name
=>"$file"),
5502 -class => "list"}, esc_path
($file));
5503 print "</td><td>\n";
5507 print "<div class=\"binary\">Binary file</div>\n";
5509 $ltext = untabify
($ltext);
5510 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5511 $ltext = esc_html
($1, -nbsp
=>1);
5512 $ltext .= '<span class="match">';
5513 $ltext .= esc_html
($2, -nbsp
=>1);
5514 $ltext .= '</span>';
5515 $ltext .= esc_html
($3, -nbsp
=>1);
5517 $ltext = esc_html
($ltext, -nbsp
=>1);
5519 print "<div class=\"pre\">" .
5520 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5521 file_name
=>"$file").'#l'.$lno,
5522 -class => "linenr"}, sprintf('%4i', $lno))
5523 . ' ' . $ltext . "</div>\n";
5527 print "</td></tr>\n";
5528 if ($matches > 1000) {
5529 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5532 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5541 sub git_search_help
{
5543 git_print_page_nav
('','', $hash,$hash,$hash);
5545 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5546 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5547 the pattern entered is recognized as the POSIX extended
5548 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5551 <dt><b>commit</b></dt>
5552 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5554 my ($have_grep) = gitweb_check_feature
('grep');
5557 <dt><b>grep</b></dt>
5558 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5559 a different one) are searched for the given pattern. On large trees, this search can take
5560 a while and put some strain on the server, so please use it with some consideration. Note that
5561 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5562 case-sensitive.</dd>
5566 <dt><b>author</b></dt>
5567 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5568 <dt><b>committer</b></dt>
5569 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5571 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5572 if ($have_pickaxe) {
5574 <dt><b>pickaxe</b></dt>
5575 <dd>All commits that caused the string to appear or disappear from any file (changes that
5576 added, removed or "modified" the string) will be listed. This search can take a while and
5577 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5578 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5586 my $head = git_get_head_hash
($project);
5587 if (!defined $hash) {
5590 if (!defined $page) {
5593 my $refs = git_get_references
();
5595 my @commitlist = parse_commits
($hash, 101, (100 * $page));
5597 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5599 if ($#commitlist >= 100) {
5601 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5602 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5606 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5607 git_print_header_div
('summary', $project);
5609 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5614 ## ......................................................................
5615 ## feeds (RSS, Atom; OPML)
5618 my $format = shift || 'atom';
5619 my ($have_blame) = gitweb_check_feature
('blame');
5621 # Atom: http://www.atomenabled.org/developers/syndication/
5622 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5623 if ($format ne 'rss' && $format ne 'atom') {
5624 die_error
(undef, "Unknown web feed format");
5627 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5628 my $head = $hash || 'HEAD';
5629 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5633 my $content_type = "application/$format+xml";
5634 if (defined $cgi->http('HTTP_ACCEPT') &&
5635 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5636 # browser (feed reader) prefers text/xml
5637 $content_type = 'text/xml';
5639 if (defined($commitlist[0])) {
5640 %latest_commit = %{$commitlist[0]};
5641 %latest_date = parse_date
($latest_commit{'author_epoch'});
5643 -type
=> $content_type,
5644 -charset
=> 'utf-8',
5645 -last_modified
=> $latest_date{'rfc2822'});
5648 -type
=> $content_type,
5649 -charset
=> 'utf-8');
5652 # Optimization: skip generating the body if client asks only
5653 # for Last-Modified date.
5654 return if ($cgi->request_method() eq 'HEAD');
5657 my $title = "$site_name - $project/$action";
5658 my $feed_type = 'log';
5659 if (defined $hash) {
5660 $title .= " - '$hash'";
5661 $feed_type = 'branch log';
5662 if (defined $file_name) {
5663 $title .= " :: $file_name";
5664 $feed_type = 'history';
5666 } elsif (defined $file_name) {
5667 $title .= " - $file_name";
5668 $feed_type = 'history';
5670 $title .= " $feed_type";
5671 my $descr = git_get_project_description
($project);
5672 if (defined $descr) {
5673 $descr = esc_html
($descr);
5675 $descr = "$project " .
5676 ($format eq 'rss' ?
'RSS' : 'Atom') .
5679 my $owner = git_get_project_owner
($project);
5680 $owner = esc_html
($owner);
5684 if (defined $file_name) {
5685 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5686 } elsif (defined $hash) {
5687 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5689 $alt_url = href
(-full
=>1, action
=>"summary");
5691 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
5692 if ($format eq 'rss') {
5694 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5697 print "<title>$title</title>\n" .
5698 "<link>$alt_url</link>\n" .
5699 "<description>$descr</description>\n" .
5700 "<language>en</language>\n";
5701 } elsif ($format eq 'atom') {
5703 <feed xmlns="http://www.w3.org/2005/Atom">
5705 print "<title>$title</title>\n" .
5706 "<subtitle>$descr</subtitle>\n" .
5707 '<link rel="alternate" type="text/html" href="' .
5708 $alt_url . '" />' . "\n" .
5709 '<link rel="self" type="' . $content_type . '" href="' .
5710 $cgi->self_url() . '" />' . "\n" .
5711 "<id>" . href
(-full
=>1) . "</id>\n" .
5712 # use project owner for feed author
5713 "<author><name>$owner</name></author>\n";
5714 if (defined $favicon) {
5715 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5717 if (defined $logo_url) {
5718 # not twice as wide as tall: 72 x 27 pixels
5719 print "<logo>" . esc_url
($logo) . "</logo>\n";
5721 if (! %latest_date) {
5722 # dummy date to keep the feed valid until commits trickle in:
5723 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5725 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5730 for (my $i = 0; $i <= $#commitlist; $i++) {
5731 my %co = %{$commitlist[$i]};
5732 my $commit = $co{'id'};
5733 # we read 150, we always show 30 and the ones more recent than 48 hours
5734 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5737 my %cd = parse_date
($co{'author_epoch'});
5739 # get list of changed files
5740 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5741 $co{'parent'} || "--root",
5742 $co{'id'}, "--", (defined $file_name ?
$file_name : ())
5744 my @difftree = map { chomp; $_ } <$fd>;
5748 # print element (entry, item)
5749 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5750 if ($format eq 'rss') {
5752 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5753 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5754 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5755 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5756 "<link>$co_url</link>\n" .
5757 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5758 "<content:encoded>" .
5760 } elsif ($format eq 'atom') {
5762 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5763 "<updated>$cd{'iso-8601'}</updated>\n" .
5765 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5766 if ($co{'author_email'}) {
5767 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5769 print "</author>\n" .
5770 # use committer for contributor
5772 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5773 if ($co{'committer_email'}) {
5774 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5776 print "</contributor>\n" .
5777 "<published>$cd{'iso-8601'}</published>\n" .
5778 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5779 "<id>$co_url</id>\n" .
5780 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5781 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5783 my $comment = $co{'comment'};
5785 foreach my $line (@
$comment) {
5786 $line = esc_html
($line);
5789 print "</pre><ul>\n";
5790 foreach my $difftree_line (@difftree) {
5791 my %difftree = parse_difftree_raw_line
($difftree_line);
5792 next if !$difftree{'from_id'};
5794 my $file = $difftree{'file'} || $difftree{'to_file'};
5798 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5799 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5800 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5801 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5802 -title
=> "diff"}, 'D');
5804 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5805 file_name
=>$file, hash_base
=>$commit),
5806 -title
=> "blame"}, 'B');
5808 # if this is not a feed of a file history
5809 if (!defined $file_name || $file_name ne $file) {
5810 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5811 file_name
=>$file, hash
=>$commit),
5812 -title
=> "history"}, 'H');
5814 $file = esc_path
($file);
5818 if ($format eq 'rss') {
5819 print "</ul>]]>\n" .
5820 "</content:encoded>\n" .
5822 } elsif ($format eq 'atom') {
5823 print "</ul>\n</div>\n" .
5830 if ($format eq 'rss') {
5831 print "</channel>\n</rss>\n";
5832 } elsif ($format eq 'atom') {
5846 my @list = git_get_projects_list
();
5848 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
5850 <?xml version="1.0" encoding="utf-8"?>
5851 <opml version="1.0">
5853 <title>$site_name OPML Export</title>
5856 <outline text="git RSS feeds">
5859 foreach my $pr (@list) {
5861 my $head = git_get_head_hash
($proj{'path'});
5862 if (!defined $head) {
5865 $git_dir = "$projectroot/$proj{'path'}";
5866 my %co = parse_commit
($head);
5871 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
5872 my $rss = "$my_url?p=$proj{'path'};a=rss";
5873 my $html = "$my_url?p=$proj{'path'};a=summary";
5874 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";