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 = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute
=> 1);
30 # if we're called with PATH_INFO, we have to strip that
31 # from the URL to find our real URL
32 if (my $path_info = $ENV{"PATH_INFO"}) {
33 $my_url =~ s
,\Q
$path_info\E
$,,;
34 $my_uri =~ s
,\Q
$path_info\E
$,,;
37 # core git executable to use
38 # this can just be "git" if your webserver has a sensible PATH
39 our $GIT = "++GIT_BINDIR++/git";
41 # absolute fs-path which will be prepended to the project path
42 #our $projectroot = "/pub/scm";
43 our $projectroot = "++GITWEB_PROJECTROOT++";
45 # fs traversing limit for getting project list
46 # the number is relative to the projectroot
47 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
49 # target of the home link on top of all pages
50 our $home_link = $my_uri || "/";
52 # string of the home link on top of all pages
53 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
55 # name of your site or organization to appear in page titles
56 # replace this with something more descriptive for clearer bookmarks
57 our $site_name = "++GITWEB_SITENAME++"
58 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
60 # filename of html text to include at top of each page
61 our $site_header = "++GITWEB_SITE_HEADER++";
62 # html text to include at home page
63 our $home_text = "++GITWEB_HOMETEXT++";
64 # filename of html text to include at bottom of each page
65 our $site_footer = "++GITWEB_SITE_FOOTER++";
68 our @stylesheets = ("++GITWEB_CSS++");
69 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
70 our $stylesheet = undef;
72 # URI of GIT logo (72x27 size)
73 our $logo = "++GITWEB_LOGO++";
74 # URI of GIT favicon, assumed to be image/png type
75 our $favicon = "++GITWEB_FAVICON++";
77 # URI and label (title) of GIT logo link
78 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
79 #our $logo_label = "git documentation";
80 our $logo_url = "http://git.or.cz/";
81 our $logo_label = "git homepage";
83 # source of projects list
84 our $projects_list = "++GITWEB_LIST++";
86 # the width (in characters) of the projects list "Description" column
87 our $projects_list_description_width = 25;
89 # default order of projects list
90 # valid values are none, project, descr, owner, and age
91 our $default_projects_order = "project";
93 # show repository only if this file exists
94 # (only effective if this variable evaluates to true)
95 our $export_ok = "++GITWEB_EXPORT_OK++";
97 # only allow viewing of repositories also shown on the overview page
98 our $strict_export = "++GITWEB_STRICT_EXPORT++";
100 # list of git base URLs used for URL to where fetch project from,
101 # i.e. full URL is "$git_base_url/$project"
102 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
104 # default blob_plain mimetype and default charset for text/plain blob
105 our $default_blob_plain_mimetype = 'text/plain';
106 our $default_text_plain_charset = undef;
108 # file to use for guessing MIME types before trying /etc/mime.types
109 # (relative to the current git repository)
110 our $mimetypes_file = undef;
112 # assume this charset if line contains non-UTF-8 characters;
113 # it should be valid encoding (see Encoding::Supported(3pm) for list),
114 # for which encoding all byte sequences are valid, for example
115 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
116 # could be even 'utf-8' for the old behavior)
117 our $fallback_encoding = 'latin1';
119 # rename detection options for git-diff and git-diff-tree
120 # - default is '-M', with the cost proportional to
121 # (number of removed files) * (number of new files).
122 # - more costly is '-C' (which implies '-M'), with the cost proportional to
123 # (number of changed files + number of removed files) * (number of new files)
124 # - even more costly is '-C', '--find-copies-harder' with cost
125 # (number of files in the original tree) * (number of new files)
126 # - one might want to include '-B' option, e.g. '-B', '-M'
127 our @diff_opts = ('-M'); # taken from git_commit
129 # information about snapshot formats that gitweb is capable of serving
130 our %known_snapshot_formats = (
132 # 'display' => display name,
133 # 'type' => mime type,
134 # 'suffix' => filename suffix,
135 # 'format' => --format for git-archive,
136 # 'compressor' => [compressor command and arguments]
137 # (array reference, optional)}
140 'display' => 'tar.gz',
141 'type' => 'application/x-gzip',
142 'suffix' => '.tar.gz',
144 'compressor' => ['gzip']},
147 'display' => 'tar.bz2',
148 'type' => 'application/x-bzip2',
149 'suffix' => '.tar.bz2',
151 'compressor' => ['bzip2']},
155 'type' => 'application/x-zip',
160 # Aliases so we understand old gitweb.snapshot values in repository
162 our %known_snapshot_format_aliases = (
166 # backward compatibility: legacy gitweb config support
167 'x-gzip' => undef, 'gz' => undef,
168 'x-bzip2' => undef, 'bz2' => undef,
169 'x-zip' => undef, '' => undef,
172 # You define site-wide feature defaults here; override them with
173 # $GITWEB_CONFIG as necessary.
176 # 'sub' => feature-sub (subroutine),
177 # 'override' => allow-override (boolean),
178 # 'default' => [ default options...] (array reference)}
180 # if feature is overridable (it means that allow-override has true value),
181 # then feature-sub will be called with default options as parameters;
182 # return value of feature-sub indicates if to enable specified feature
184 # if there is no 'sub' key (no feature-sub), then feature cannot be
187 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
189 # Enable the 'blame' blob view, showing the last commit that modified
190 # each line in the file. This can be very CPU-intensive.
192 # To enable system wide have in $GITWEB_CONFIG
193 # $feature{'blame'}{'default'} = [1];
194 # To have project specific config enable override in $GITWEB_CONFIG
195 # $feature{'blame'}{'override'} = 1;
196 # and in project config gitweb.blame = 0|1;
198 'sub' => \
&feature_blame
,
202 # Enable the 'snapshot' link, providing a compressed archive of any
203 # tree. This can potentially generate high traffic if you have large
206 # Value is a list of formats defined in %known_snapshot_formats that
208 # To disable system wide have in $GITWEB_CONFIG
209 # $feature{'snapshot'}{'default'} = [];
210 # To have project specific config enable override in $GITWEB_CONFIG
211 # $feature{'snapshot'}{'override'} = 1;
212 # and in project config, a comma-separated list of formats or "none"
213 # to disable. Example: gitweb.snapshot = tbz2,zip;
215 'sub' => \
&feature_snapshot
,
217 'default' => ['tgz']},
219 # Enable text search, which will list the commits which match author,
220 # committer or commit text to a given string. Enabled by default.
221 # Project specific override is not supported.
226 # Enable grep search, which will list the files in currently selected
227 # tree containing the given string. Enabled by default. This can be
228 # potentially CPU-intensive, of course.
230 # To enable system wide have in $GITWEB_CONFIG
231 # $feature{'grep'}{'default'} = [1];
232 # To have project specific config enable override in $GITWEB_CONFIG
233 # $feature{'grep'}{'override'} = 1;
234 # and in project config gitweb.grep = 0|1;
239 # Enable the pickaxe search, which will list the commits that modified
240 # a given string in a file. This can be practical and quite faster
241 # alternative to 'blame', but still potentially CPU-intensive.
243 # To enable system wide have in $GITWEB_CONFIG
244 # $feature{'pickaxe'}{'default'} = [1];
245 # To have project specific config enable override in $GITWEB_CONFIG
246 # $feature{'pickaxe'}{'override'} = 1;
247 # and in project config gitweb.pickaxe = 0|1;
249 'sub' => \
&feature_pickaxe
,
253 # Make gitweb use an alternative format of the URLs which can be
254 # more readable and natural-looking: project name is embedded
255 # directly in the path and the query string contains other
256 # auxiliary information. All gitweb installations recognize
257 # URL in either format; this configures in which formats gitweb
260 # To enable system wide have in $GITWEB_CONFIG
261 # $feature{'pathinfo'}{'default'} = [1];
262 # Project specific override is not supported.
264 # Note that you will need to change the default location of CSS,
265 # favicon, logo and possibly other files to an absolute URL. Also,
266 # if gitweb.cgi serves as your indexfile, you will need to force
267 # $my_uri to contain the script name in your $GITWEB_CONFIG.
272 # Make gitweb consider projects in project root subdirectories
273 # to be forks of existing projects. Given project $projname.git,
274 # projects matching $projname/*.git will not be shown in the main
275 # projects list, instead a '+' mark will be added to $projname
276 # there and a 'forks' view will be enabled for the project, listing
277 # all the forks. If project list is taken from a file, forks have
278 # to be listed after the main project.
280 # To enable system wide have in $GITWEB_CONFIG
281 # $feature{'forks'}{'default'} = [1];
282 # Project specific override is not supported.
288 sub gitweb_check_feature
{
290 return unless exists $feature{$name};
291 my ($sub, $override, @defaults) = (
292 $feature{$name}{'sub'},
293 $feature{$name}{'override'},
294 @{$feature{$name}{'default'}});
295 if (!$override) { return @defaults; }
297 warn "feature $name is not overrideable";
300 return $sub->(@defaults);
304 my ($val) = git_get_project_config
('blame', '--bool');
306 if ($val eq 'true') {
308 } elsif ($val eq 'false') {
315 sub feature_snapshot
{
318 my ($val) = git_get_project_config
('snapshot');
321 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
328 my ($val) = git_get_project_config
('grep', '--bool');
330 if ($val eq 'true') {
332 } elsif ($val eq 'false') {
339 sub feature_pickaxe
{
340 my ($val) = git_get_project_config
('pickaxe', '--bool');
342 if ($val eq 'true') {
344 } elsif ($val eq 'false') {
351 # checking HEAD file with -e is fragile if the repository was
352 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
354 sub check_head_link
{
356 my $headfile = "$dir/HEAD";
357 return ((-e
$headfile) ||
358 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
361 sub check_export_ok
{
363 return (check_head_link
($dir) &&
364 (!$export_ok || -e
"$dir/$export_ok"));
367 # process alternate names for backward compatibility
368 # filter out unsupported (unknown) snapshot formats
369 sub filter_snapshot_fmts
{
373 exists $known_snapshot_format_aliases{$_} ?
374 $known_snapshot_format_aliases{$_} : $_} @fmts;
375 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
379 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
380 if (-e
$GITWEB_CONFIG) {
383 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
384 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
387 # version of the core git binary
388 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
390 $projects_list ||= $projectroot;
392 # ======================================================================
393 # input validation and dispatch
394 our $action = $cgi->param('a');
395 if (defined $action) {
396 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
397 die_error
(400, "Invalid action parameter");
401 # parameters which are pathnames
402 our $project = $cgi->param('p');
403 if (defined $project) {
404 if (!validate_pathname
($project) ||
405 !(-d
"$projectroot/$project") ||
406 !check_head_link
("$projectroot/$project") ||
407 ($export_ok && !(-e
"$projectroot/$project/$export_ok")) ||
408 ($strict_export && !project_in_list
($project))) {
410 die_error
(404, "No such project");
414 our $file_name = $cgi->param('f');
415 if (defined $file_name) {
416 if (!validate_pathname
($file_name)) {
417 die_error
(400, "Invalid file parameter");
421 our $file_parent = $cgi->param('fp');
422 if (defined $file_parent) {
423 if (!validate_pathname
($file_parent)) {
424 die_error
(400, "Invalid file parent parameter");
428 # parameters which are refnames
429 our $hash = $cgi->param('h');
431 if (!validate_refname
($hash)) {
432 die_error
(400, "Invalid hash parameter");
436 our $hash_parent = $cgi->param('hp');
437 if (defined $hash_parent) {
438 if (!validate_refname
($hash_parent)) {
439 die_error
(400, "Invalid hash parent parameter");
443 our $hash_base = $cgi->param('hb');
444 if (defined $hash_base) {
445 if (!validate_refname
($hash_base)) {
446 die_error
(400, "Invalid hash base parameter");
450 my %allowed_options = (
451 "--no-merges" => [ qw(rss atom log shortlog history) ],
454 our @extra_options = $cgi->param('opt');
455 if (defined @extra_options) {
456 foreach my $opt (@extra_options) {
457 if (not exists $allowed_options{$opt}) {
458 die_error
(400, "Invalid option parameter");
460 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
461 die_error
(400, "Invalid option parameter for this action");
466 our $hash_parent_base = $cgi->param('hpb');
467 if (defined $hash_parent_base) {
468 if (!validate_refname
($hash_parent_base)) {
469 die_error
(400, "Invalid hash parent base parameter");
474 our $page = $cgi->param('pg');
476 if ($page =~ m/[^0-9]/) {
477 die_error
(400, "Invalid page parameter");
481 our $searchtype = $cgi->param('st');
482 if (defined $searchtype) {
483 if ($searchtype =~ m/[^a-z]/) {
484 die_error
(400, "Invalid searchtype parameter");
488 our $search_use_regexp = $cgi->param('sr');
490 our $searchtext = $cgi->param('s');
492 if (defined $searchtext) {
493 if (length($searchtext) < 2) {
494 die_error
(403, "At least two characters are required for search parameter");
496 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
499 # now read PATH_INFO and use it as alternative to parameters
500 sub evaluate_path_info
{
501 return if defined $project;
502 my $path_info = $ENV{"PATH_INFO"};
503 return if !$path_info;
504 $path_info =~ s
,^/+,,;
505 return if !$path_info;
506 # find which part of PATH_INFO is project
507 $project = $path_info;
509 while ($project && !check_head_link
("$projectroot/$project")) {
510 $project =~ s
,/*[^/]*$,,;
513 $project = validate_pathname
($project);
515 ($export_ok && !-e
"$projectroot/$project/$export_ok") ||
516 ($strict_export && !project_in_list
($project))) {
520 # do not change any parameters if an action is given using the query string
522 $path_info =~ s
,^\Q
$project\E
/*,,;
523 my ($refname, $pathname) = split(/:/, $path_info, 2);
524 if (defined $pathname) {
525 # we got "project.git/branch:filename" or "project.git/branch:dir/"
526 # we could use git_get_type(branch:pathname), but it needs $git_dir
527 $pathname =~ s
,^/+,,;
528 if (!$pathname || substr($pathname, -1) eq "/") {
532 $action ||= "blob_plain";
534 $hash_base ||= validate_refname
($refname);
535 $file_name ||= validate_pathname
($pathname);
536 } elsif (defined $refname) {
537 # we got "project.git/branch"
538 $action ||= "shortlog";
539 $hash ||= validate_refname
($refname);
542 evaluate_path_info
();
544 # path to the current git repository
546 $git_dir = "$projectroot/$project" if $project;
550 "blame" => \
&git_blame
,
551 "blobdiff" => \
&git_blobdiff
,
552 "blobdiff_plain" => \
&git_blobdiff_plain
,
553 "blob" => \
&git_blob
,
554 "blob_plain" => \
&git_blob_plain
,
555 "commitdiff" => \
&git_commitdiff
,
556 "commitdiff_plain" => \
&git_commitdiff_plain
,
557 "commit" => \
&git_commit
,
558 "forks" => \
&git_forks
,
559 "heads" => \
&git_heads
,
560 "history" => \
&git_history
,
563 "atom" => \
&git_atom
,
564 "search" => \
&git_search
,
565 "search_help" => \
&git_search_help
,
566 "shortlog" => \
&git_shortlog
,
567 "summary" => \
&git_summary
,
569 "tags" => \
&git_tags
,
570 "tree" => \
&git_tree
,
571 "snapshot" => \
&git_snapshot
,
572 "object" => \
&git_object
,
573 # those below don't need $project
574 "opml" => \
&git_opml
,
575 "project_list" => \
&git_project_list
,
576 "project_index" => \
&git_project_index
,
579 if (!defined $action) {
581 $action = git_get_type
($hash);
582 } elsif (defined $hash_base && defined $file_name) {
583 $action = git_get_type
("$hash_base:$file_name");
584 } elsif (defined $project) {
587 $action = 'project_list';
590 if (!defined($actions{$action})) {
591 die_error
(400, "Unknown action");
593 if ($action !~ m/^(opml|project_list|project_index)$/ &&
595 die_error
(400, "Project needed");
597 $actions{$action}->();
600 ## ======================================================================
605 # default is to use -absolute url() i.e. $my_uri
606 my $href = $params{-full
} ? $my_url : $my_uri;
608 # XXX: Warning: If you touch this, check the search form for updating,
619 hash_parent_base
=> "hpb",
624 snapshot_format
=> "sf",
625 extra_options
=> "opt",
626 search_use_regexp
=> "sr",
628 my %mapping = @mapping;
630 $params{'project'} = $project unless exists $params{'project'};
632 if ($params{-replay
}) {
633 while (my ($name, $symbol) = each %mapping) {
634 if (!exists $params{$name}) {
635 # to allow for multivalued params we use arrayref form
636 $params{$name} = [ $cgi->param($symbol) ];
641 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
643 # use PATH_INFO for project name
644 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
645 delete $params{'project'};
647 # Summary just uses the project path URL
648 if (defined $params{'action'} && $params{'action'} eq 'summary') {
649 delete $params{'action'};
653 # now encode the parameters explicitly
655 for (my $i = 0; $i < @mapping; $i += 2) {
656 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
657 if (defined $params{$name}) {
658 if (ref($params{$name}) eq "ARRAY") {
659 foreach my $par (@{$params{$name}}) {
660 push @result, $symbol . "=" . esc_param
($par);
663 push @result, $symbol . "=" . esc_param
($params{$name});
667 $href .= "?" . join(';', @result) if scalar @result;
673 ## ======================================================================
674 ## validation, quoting/unquoting and escaping
676 sub validate_pathname
{
677 my $input = shift || return undef;
679 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
680 # at the beginning, at the end, and between slashes.
681 # also this catches doubled slashes
682 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
686 if ($input =~ m!\0!) {
692 sub validate_refname
{
693 my $input = shift || return undef;
695 # textual hashes are O.K.
696 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
699 # it must be correct pathname
700 $input = validate_pathname
($input)
702 # restrictions on ref name according to git-check-ref-format
703 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
709 # decode sequences of octets in utf8 into Perl's internal form,
710 # which is utf-8 with utf8 flag set if needed. gitweb writes out
711 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
714 if (utf8
::valid
($str)) {
718 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
722 # quote unsafe chars, but keep the slash, even when it's not
723 # correct, but quoted slashes look too horrible in bookmarks
726 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
732 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
735 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
741 # replace invalid utf8 character with SUBSTITUTION sequence
746 $str = to_utf8
($str);
747 $str = $cgi->escapeHTML($str);
748 if ($opts{'-nbsp'}) {
749 $str =~ s/ / /g;
751 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
755 # quote control characters and escape filename to HTML
760 $str = to_utf8
($str);
761 $str = $cgi->escapeHTML($str);
762 if ($opts{'-nbsp'}) {
763 $str =~ s/ / /g;
765 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
769 # Make control characters "printable", using character escape codes (CEC)
773 my %es = ( # character escape codes, aka escape sequences
774 "\t" => '\t', # tab (HT)
775 "\n" => '\n', # line feed (LF)
776 "\r" => '\r', # carrige return (CR)
777 "\f" => '\f', # form feed (FF)
778 "\b" => '\b', # backspace (BS)
779 "\a" => '\a', # alarm (bell) (BEL)
780 "\e" => '\e', # escape (ESC)
781 "\013" => '\v', # vertical tab (VT)
782 "\000" => '\0', # nul character (NUL)
784 my $chr = ( (exists $es{$cntrl})
786 : sprintf('\%2x', ord($cntrl)) );
787 if ($opts{-nohtml
}) {
790 return "<span class=\"cntrl\">$chr</span>";
794 # Alternatively use unicode control pictures codepoints,
795 # Unicode "printable representation" (PR)
800 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
801 if ($opts{-nohtml
}) {
804 return "<span class=\"cntrl\">$chr</span>";
808 # git may return quoted and escaped filenames
814 my %es = ( # character escape codes, aka escape sequences
815 't' => "\t", # tab (HT, TAB)
816 'n' => "\n", # newline (NL)
817 'r' => "\r", # return (CR)
818 'f' => "\f", # form feed (FF)
819 'b' => "\b", # backspace (BS)
820 'a' => "\a", # alarm (bell) (BEL)
821 'e' => "\e", # escape (ESC)
822 'v' => "\013", # vertical tab (VT)
825 if ($seq =~ m/^[0-7]{1,3}$/) {
826 # octal char sequence
827 return chr(oct($seq));
828 } elsif (exists $es{$seq}) {
829 # C escape sequence, aka character escape code
832 # quoted ordinary character
836 if ($str =~ m/^"(.*)"$/) {
839 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
844 # escape tabs (convert tabs to spaces)
848 while ((my $pos = index($line, "\t")) != -1) {
849 if (my $count = (8 - ($pos % 8))) {
850 my $spaces = ' ' x
$count;
851 $line =~ s/\t/$spaces/;
858 sub project_in_list
{
860 my @list = git_get_projects_list
();
861 return @list && scalar(grep { $_->{'path'} eq $project } @list);
864 ## ----------------------------------------------------------------------
865 ## HTML aware string manipulation
867 # Try to chop given string on a word boundary between position
868 # $len and $len+$add_len. If there is no word boundary there,
869 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
870 # (marking chopped part) would be longer than given string.
874 my $add_len = shift || 10;
875 my $where = shift || 'right'; # 'left' | 'center' | 'right'
877 # Make sure perl knows it is utf8 encoded so we don't
878 # cut in the middle of a utf8 multibyte char.
879 $str = to_utf8
($str);
881 # allow only $len chars, but don't cut a word if it would fit in $add_len
882 # if it doesn't fit, cut it if it's still longer than the dots we would add
883 # remove chopped character entities entirely
885 # when chopping in the middle, distribute $len into left and right part
886 # return early if chopping wouldn't make string shorter
887 if ($where eq 'center') {
888 return $str if ($len + 5 >= length($str)); # filler is length 5
891 return $str if ($len + 4 >= length($str)); # filler is length 4
894 # regexps: ending and beginning with word part up to $add_len
895 my $endre = qr/.{$len}\w{0,$add_len}/;
896 my $begre = qr/\w{0,$add_len}.{$len}/;
898 if ($where eq 'left') {
899 $str =~ m/^(.*?)($begre)$/;
900 my ($lead, $body) = ($1, $2);
901 if (length($lead) > 4) {
902 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
907 } elsif ($where eq 'center') {
908 $str =~ m/^($endre)(.*)$/;
909 my ($left, $str) = ($1, $2);
910 $str =~ m/^(.*?)($begre)$/;
911 my ($mid, $right) = ($1, $2);
912 if (length($mid) > 5) {
913 $left =~ s/&[^;]*$//;
914 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
917 return "$left$mid$right";
920 $str =~ m/^($endre)(.*)$/;
923 if (length($tail) > 4) {
924 $body =~ s/&[^;]*$//;
931 # takes the same arguments as chop_str, but also wraps a <span> around the
932 # result with a title attribute if it does get chopped. Additionally, the
933 # string is HTML-escaped.
934 sub chop_and_escape_str
{
937 my $chopped = chop_str
(@_);
938 if ($chopped eq $str) {
939 return esc_html
($chopped);
941 $str =~ s/([[:cntrl:]])/?/g;
942 return $cgi->span({-title
=>$str}, esc_html
($chopped));
946 ## ----------------------------------------------------------------------
947 ## functions returning short strings
949 # CSS class for given age value (in seconds)
955 } elsif ($age < 60*60*2) {
957 } elsif ($age < 60*60*24*2) {
964 # convert age in seconds to "nn units ago" string
969 if ($age > 60*60*24*365*2) {
970 $age_str = (int $age/60/60/24/365);
971 $age_str .= " years ago";
972 } elsif ($age > 60*60*24*(365/12)*2) {
973 $age_str = int $age/60/60/24/(365/12);
974 $age_str .= " months ago";
975 } elsif ($age > 60*60*24*7*2) {
976 $age_str = int $age/60/60/24/7;
977 $age_str .= " weeks ago";
978 } elsif ($age > 60*60*24*2) {
979 $age_str = int $age/60/60/24;
980 $age_str .= " days ago";
981 } elsif ($age > 60*60*2) {
982 $age_str = int $age/60/60;
983 $age_str .= " hours ago";
984 } elsif ($age > 60*2) {
985 $age_str = int $age/60;
986 $age_str .= " min ago";
989 $age_str .= " sec ago";
991 $age_str .= " right now";
997 S_IFINVALID
=> 0030000,
998 S_IFGITLINK
=> 0160000,
1001 # submodule/subproject, a commit object reference
1002 sub S_ISGITLINK
($) {
1005 return (($mode & S_IFMT
) == S_IFGITLINK
)
1008 # convert file mode in octal to symbolic file mode string
1010 my $mode = oct shift;
1012 if (S_ISGITLINK
($mode)) {
1013 return 'm---------';
1014 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1015 return 'drwxr-xr-x';
1016 } elsif (S_ISLNK
($mode)) {
1017 return 'lrwxrwxrwx';
1018 } elsif (S_ISREG
($mode)) {
1019 # git cares only about the executable bit
1020 if ($mode & S_IXUSR
) {
1021 return '-rwxr-xr-x';
1023 return '-rw-r--r--';
1026 return '----------';
1030 # convert file mode in octal to file type string
1034 if ($mode !~ m/^[0-7]+$/) {
1040 if (S_ISGITLINK
($mode)) {
1042 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1044 } elsif (S_ISLNK
($mode)) {
1046 } elsif (S_ISREG
($mode)) {
1053 # convert file mode in octal to file type description string
1054 sub file_type_long
{
1057 if ($mode !~ m/^[0-7]+$/) {
1063 if (S_ISGITLINK
($mode)) {
1065 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1067 } elsif (S_ISLNK
($mode)) {
1069 } elsif (S_ISREG
($mode)) {
1070 if ($mode & S_IXUSR
) {
1071 return "executable";
1081 ## ----------------------------------------------------------------------
1082 ## functions returning short HTML fragments, or transforming HTML fragments
1083 ## which don't belong to other sections
1085 # format line of commit message.
1086 sub format_log_line_html
{
1089 $line = esc_html
($line, -nbsp
=>1);
1090 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1093 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1094 -class => "text"}, $hash_text);
1095 $line =~ s/$hash_text/$link/;
1100 # format marker of refs pointing to given object
1102 # the destination action is chosen based on object type and current context:
1103 # - for annotated tags, we choose the tag view unless it's the current view
1104 # already, in which case we go to shortlog view
1105 # - for other refs, we keep the current view if we're in history, shortlog or
1106 # log view, and select shortlog otherwise
1107 sub format_ref_marker
{
1108 my ($refs, $id) = @_;
1111 if (defined $refs->{$id}) {
1112 foreach my $ref (@{$refs->{$id}}) {
1113 # this code exploits the fact that non-lightweight tags are the
1114 # only indirect objects, and that they are the only objects for which
1115 # we want to use tag instead of shortlog as action
1116 my ($type, $name) = qw();
1117 my $indirect = ($ref =~ s/\^\{\}$//);
1118 # e.g. tags/v2.6.11 or heads/next
1119 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1128 $class .= " indirect" if $indirect;
1130 my $dest_action = "shortlog";
1133 $dest_action = "tag" unless $action eq "tag";
1134 } elsif ($action =~ /^(history|(short)?log)$/) {
1135 $dest_action = $action;
1139 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1142 my $link = $cgi->a({
1144 action
=>$dest_action,
1148 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1154 return ' <span class="refs">'. $markers . '</span>';
1160 # format, perhaps shortened and with markers, title line
1161 sub format_subject_html
{
1162 my ($long, $short, $href, $extra) = @_;
1163 $extra = '' unless defined($extra);
1165 if (length($short) < length($long)) {
1166 return $cgi->a({-href
=> $href, -class => "list subject",
1167 -title
=> to_utf8
($long)},
1168 esc_html
($short) . $extra);
1170 return $cgi->a({-href
=> $href, -class => "list subject"},
1171 esc_html
($long) . $extra);
1175 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1176 sub format_git_diff_header_line
{
1178 my $diffinfo = shift;
1179 my ($from, $to) = @_;
1181 if ($diffinfo->{'nparents'}) {
1183 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1184 if ($to->{'href'}) {
1185 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1186 esc_path
($to->{'file'}));
1187 } else { # file was deleted (no href)
1188 $line .= esc_path
($to->{'file'});
1192 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1193 if ($from->{'href'}) {
1194 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1195 'a/' . esc_path
($from->{'file'}));
1196 } else { # file was added (no href)
1197 $line .= 'a/' . esc_path
($from->{'file'});
1200 if ($to->{'href'}) {
1201 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1202 'b/' . esc_path
($to->{'file'}));
1203 } else { # file was deleted
1204 $line .= 'b/' . esc_path
($to->{'file'});
1208 return "<div class=\"diff header\">$line</div>\n";
1211 # format extended diff header line, before patch itself
1212 sub format_extended_diff_header_line
{
1214 my $diffinfo = shift;
1215 my ($from, $to) = @_;
1218 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1219 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1220 esc_path
($from->{'file'}));
1222 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1223 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1224 esc_path
($to->{'file'}));
1226 # match single <mode>
1227 if ($line =~ m/\s(\d{6})$/) {
1228 $line .= '<span class="info"> (' .
1229 file_type_long
($1) .
1233 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1234 # can match only for combined diff
1236 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1237 if ($from->{'href'}[$i]) {
1238 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1240 substr($diffinfo->{'from_id'}[$i],0,7));
1245 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1248 if ($to->{'href'}) {
1249 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1250 substr($diffinfo->{'to_id'},0,7));
1255 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1256 # can match only for ordinary diff
1257 my ($from_link, $to_link);
1258 if ($from->{'href'}) {
1259 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1260 substr($diffinfo->{'from_id'},0,7));
1262 $from_link = '0' x
7;
1264 if ($to->{'href'}) {
1265 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1266 substr($diffinfo->{'to_id'},0,7));
1270 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1271 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1274 return $line . "<br/>\n";
1277 # format from-file/to-file diff header
1278 sub format_diff_from_to_header
{
1279 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1284 #assert($line =~ m/^---/) if DEBUG;
1285 # no extra formatting for "^--- /dev/null"
1286 if (! $diffinfo->{'nparents'}) {
1287 # ordinary (single parent) diff
1288 if ($line =~ m!^--- "?a/!) {
1289 if ($from->{'href'}) {
1291 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1292 esc_path
($from->{'file'}));
1295 esc_path
($from->{'file'});
1298 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1301 # combined diff (merge commit)
1302 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1303 if ($from->{'href'}[$i]) {
1305 $cgi->a({-href
=>href
(action
=>"blobdiff",
1306 hash_parent
=>$diffinfo->{'from_id'}[$i],
1307 hash_parent_base
=>$parents[$i],
1308 file_parent
=>$from->{'file'}[$i],
1309 hash
=>$diffinfo->{'to_id'},
1311 file_name
=>$to->{'file'}),
1313 -title
=>"diff" . ($i+1)},
1316 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1317 esc_path
($from->{'file'}[$i]));
1319 $line = '--- /dev/null';
1321 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1326 #assert($line =~ m/^\+\+\+/) if DEBUG;
1327 # no extra formatting for "^+++ /dev/null"
1328 if ($line =~ m!^\+\+\+ "?b/!) {
1329 if ($to->{'href'}) {
1331 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1332 esc_path
($to->{'file'}));
1335 esc_path
($to->{'file'});
1338 $result .= qq
!<div
class="diff to_file">$line</div
>\n!;
1343 # create note for patch simplified by combined diff
1344 sub format_diff_cc_simplified
{
1345 my ($diffinfo, @parents) = @_;
1348 $result .= "<div class=\"diff header\">" .
1350 if (!is_deleted
($diffinfo)) {
1351 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1353 hash
=>$diffinfo->{'to_id'},
1354 file_name
=>$diffinfo->{'to_file'}),
1356 esc_path
($diffinfo->{'to_file'}));
1358 $result .= esc_path
($diffinfo->{'to_file'});
1360 $result .= "</div>\n" . # class="diff header"
1361 "<div class=\"diff nodifferences\">" .
1363 "</div>\n"; # class="diff nodifferences"
1368 # format patch (diff) line (not to be used for diff headers)
1369 sub format_diff_line
{
1371 my ($from, $to) = @_;
1372 my $diff_class = "";
1376 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1378 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1379 if ($line =~ m/^\@{3}/) {
1380 $diff_class = " chunk_header";
1381 } elsif ($line =~ m/^\\/) {
1382 $diff_class = " incomplete";
1383 } elsif ($prefix =~ tr/+/+/) {
1384 $diff_class = " add";
1385 } elsif ($prefix =~ tr/-/-/) {
1386 $diff_class = " rem";
1389 # assume ordinary diff
1390 my $char = substr($line, 0, 1);
1392 $diff_class = " add";
1393 } elsif ($char eq '-') {
1394 $diff_class = " rem";
1395 } elsif ($char eq '@') {
1396 $diff_class = " chunk_header";
1397 } elsif ($char eq "\\") {
1398 $diff_class = " incomplete";
1401 $line = untabify
($line);
1402 if ($from && $to && $line =~ m/^\@{2} /) {
1403 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1404 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1406 $from_lines = 0 unless defined $from_lines;
1407 $to_lines = 0 unless defined $to_lines;
1409 if ($from->{'href'}) {
1410 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1411 -class=>"list"}, $from_text);
1413 if ($to->{'href'}) {
1414 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1415 -class=>"list"}, $to_text);
1417 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1418 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1419 return "<div class=\"diff$diff_class\">$line</div>\n";
1420 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1421 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1422 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1424 @from_text = split(' ', $ranges);
1425 for (my $i = 0; $i < @from_text; ++$i) {
1426 ($from_start[$i], $from_nlines[$i]) =
1427 (split(',', substr($from_text[$i], 1)), 0);
1430 $to_text = pop @from_text;
1431 $to_start = pop @from_start;
1432 $to_nlines = pop @from_nlines;
1434 $line = "<span class=\"chunk_info\">$prefix ";
1435 for (my $i = 0; $i < @from_text; ++$i) {
1436 if ($from->{'href'}[$i]) {
1437 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1438 -class=>"list"}, $from_text[$i]);
1440 $line .= $from_text[$i];
1444 if ($to->{'href'}) {
1445 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1446 -class=>"list"}, $to_text);
1450 $line .= " $prefix</span>" .
1451 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1452 return "<div class=\"diff$diff_class\">$line</div>\n";
1454 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1457 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1458 # linked. Pass the hash of the tree/commit to snapshot.
1459 sub format_snapshot_links
{
1461 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1462 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1463 my $num_fmts = @snapshot_fmts;
1464 if ($num_fmts > 1) {
1465 # A parenthesized list of links bearing format names.
1466 # e.g. "snapshot (_tar.gz_ _zip_)"
1467 return "snapshot (" . join(' ', map
1474 }, $known_snapshot_formats{$_}{'display'})
1475 , @snapshot_fmts) . ")";
1476 } elsif ($num_fmts == 1) {
1477 # A single "snapshot" link whose tooltip bears the format name.
1479 my ($fmt) = @snapshot_fmts;
1485 snapshot_format
=>$fmt
1487 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1489 } else { # $num_fmts == 0
1494 ## ......................................................................
1495 ## functions returning values to be passed, perhaps after some
1496 ## transformation, to other functions; e.g. returning arguments to href()
1498 # returns hash to be passed to href to generate gitweb URL
1499 # in -title key it returns description of link
1501 my $format = shift || 'Atom';
1502 my %res = (action
=> lc($format));
1504 # feed links are possible only for project views
1505 return unless (defined $project);
1506 # some views should link to OPML, or to generic project feed,
1507 # or don't have specific feed yet (so they should use generic)
1508 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1511 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1512 # from tag links; this also makes possible to detect branch links
1513 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1514 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1517 # find log type for feed description (title)
1519 if (defined $file_name) {
1520 $type = "history of $file_name";
1521 $type .= "/" if ($action eq 'tree');
1522 $type .= " on '$branch'" if (defined $branch);
1524 $type = "log of $branch" if (defined $branch);
1527 $res{-title
} = $type;
1528 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1529 $res{'file_name'} = $file_name;
1534 ## ----------------------------------------------------------------------
1535 ## git utility subroutines, invoking git commands
1537 # returns path to the core git executable and the --git-dir parameter as list
1539 return $GIT, '--git-dir='.$git_dir;
1542 # quote the given arguments for passing them to the shell
1543 # quote_command("command", "arg 1", "arg with ' and ! characters")
1544 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1545 # Try to avoid using this function wherever possible.
1548 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1551 # get HEAD ref of given project as hash
1552 sub git_get_head_hash
{
1553 my $project = shift;
1554 my $o_git_dir = $git_dir;
1556 $git_dir = "$projectroot/$project";
1557 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1560 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1564 if (defined $o_git_dir) {
1565 $git_dir = $o_git_dir;
1570 # get type of given object
1574 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1576 close $fd or return;
1581 # repository configuration
1582 our $config_file = '';
1585 # store multiple values for single key as anonymous array reference
1586 # single values stored directly in the hash, not as [ <value> ]
1587 sub hash_set_multi
{
1588 my ($hash, $key, $value) = @_;
1590 if (!exists $hash->{$key}) {
1591 $hash->{$key} = $value;
1592 } elsif (!ref $hash->{$key}) {
1593 $hash->{$key} = [ $hash->{$key}, $value ];
1595 push @{$hash->{$key}}, $value;
1599 # return hash of git project configuration
1600 # optionally limited to some section, e.g. 'gitweb'
1601 sub git_parse_project_config
{
1602 my $section_regexp = shift;
1607 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1610 while (my $keyval = <$fh>) {
1612 my ($key, $value) = split(/\n/, $keyval, 2);
1614 hash_set_multi
(\
%config, $key, $value)
1615 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1622 # convert config value to boolean, 'true' or 'false'
1623 # no value, number > 0, 'true' and 'yes' values are true
1624 # rest of values are treated as false (never as error)
1625 sub config_to_bool
{
1628 # strip leading and trailing whitespace
1632 return (!defined $val || # section.key
1633 ($val =~ /^\d+$/ && $val) || # section.key = 1
1634 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1637 # convert config value to simple decimal number
1638 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1639 # to be multiplied by 1024, 1048576, or 1073741824
1643 # strip leading and trailing whitespace
1647 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1649 # unknown unit is treated as 1
1650 return $num * ($unit eq 'g' ? 1073741824 :
1651 $unit eq 'm' ? 1048576 :
1652 $unit eq 'k' ? 1024 : 1);
1657 # convert config value to array reference, if needed
1658 sub config_to_multi
{
1661 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1664 sub git_get_project_config
{
1665 my ($key, $type) = @_;
1668 return unless ($key);
1669 $key =~ s/^gitweb\.//;
1670 return if ($key =~ m/\W/);
1673 if (defined $type) {
1676 unless ($type eq 'bool' || $type eq 'int');
1680 if (!defined $config_file ||
1681 $config_file ne "$git_dir/config") {
1682 %config = git_parse_project_config
('gitweb');
1683 $config_file = "$git_dir/config";
1687 if (!defined $type) {
1688 return $config{"gitweb.$key"};
1689 } elsif ($type eq 'bool') {
1690 # backward compatibility: 'git config --bool' returns true/false
1691 return config_to_bool
($config{"gitweb.$key"}) ? 'true' : 'false';
1692 } elsif ($type eq 'int') {
1693 return config_to_int
($config{"gitweb.$key"});
1695 return $config{"gitweb.$key"};
1698 # get hash of given path at given ref
1699 sub git_get_hash_by_path
{
1701 my $path = shift || return undef;
1706 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1707 or die_error
(500, "Open git-ls-tree failed");
1709 close $fd or return undef;
1711 if (!defined $line) {
1712 # there is no tree or hash given by $path at $base
1716 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1717 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1718 if (defined $type && $type ne $2) {
1719 # type doesn't match
1725 # get path of entry with given hash at given tree-ish (ref)
1726 # used to get 'from' filename for combined diff (merge commit) for renames
1727 sub git_get_path_by_hash
{
1728 my $base = shift || return;
1729 my $hash = shift || return;
1733 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1735 while (my $line = <$fd>) {
1738 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1739 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1740 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1749 ## ......................................................................
1750 ## git utility functions, directly accessing git repository
1752 sub git_get_project_description
{
1755 $git_dir = "$projectroot/$path";
1756 open my $fd, "$git_dir/description"
1757 or return git_get_project_config
('description');
1760 if (defined $descr) {
1766 sub git_get_project_url_list
{
1769 $git_dir = "$projectroot/$path";
1770 open my $fd, "$git_dir/cloneurl"
1771 or return wantarray ?
1772 @{ config_to_multi
(git_get_project_config
('url')) } :
1773 config_to_multi
(git_get_project_config
('url'));
1774 my @git_project_url_list = map { chomp; $_ } <$fd>;
1777 return wantarray ? @git_project_url_list : \
@git_project_url_list;
1780 sub git_get_projects_list
{
1785 $filter =~ s/\.git$//;
1787 my ($check_forks) = gitweb_check_feature
('forks');
1789 if (-d
$projects_list) {
1790 # search in directory
1791 my $dir = $projects_list . ($filter ? "/$filter" : '');
1792 # remove the trailing "/"
1794 my $pfxlen = length("$dir");
1795 my $pfxdepth = ($dir =~ tr!/!!);
1798 follow_fast
=> 1, # follow symbolic links
1799 follow_skip
=> 2, # ignore duplicates
1800 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1802 # skip project-list toplevel, if we get it.
1803 return if (m!^[/.]$!);
1804 # only directories can be git repositories
1805 return unless (-d
$_);
1806 # don't traverse too deep (Find is super slow on os x)
1807 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1808 $File::Find
::prune
= 1;
1812 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1813 # we check related file in $projectroot
1814 if ($check_forks and $subdir =~ m
#/.#) {
1815 $File::Find
::prune
= 1;
1816 } elsif (check_export_ok
("$projectroot/$filter/$subdir")) {
1817 push @list, { path
=> ($filter ? "$filter/" : '') . $subdir };
1818 $File::Find
::prune
= 1;
1823 } elsif (-f
$projects_list) {
1824 # read from file(url-encoded):
1825 # 'git%2Fgit.git Linus+Torvalds'
1826 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1827 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1829 open my ($fd), $projects_list or return;
1831 while (my $line = <$fd>) {
1833 my ($path, $owner) = split ' ', $line;
1834 $path = unescape
($path);
1835 $owner = unescape
($owner);
1836 if (!defined $path) {
1839 if ($filter ne '') {
1840 # looking for forks;
1841 my $pfx = substr($path, 0, length($filter));
1842 if ($pfx ne $filter) {
1845 my $sfx = substr($path, length($filter));
1846 if ($sfx !~ /^\/.*\
.git
$/) {
1849 } elsif ($check_forks) {
1851 foreach my $filter (keys %paths) {
1852 # looking for forks;
1853 my $pfx = substr($path, 0, length($filter));
1854 if ($pfx ne $filter) {
1857 my $sfx = substr($path, length($filter));
1858 if ($sfx !~ /^\/.*\
.git
$/) {
1861 # is a fork, don't include it in
1866 if (check_export_ok
("$projectroot/$path")) {
1869 owner
=> to_utf8
($owner),
1872 (my $forks_path = $path) =~ s/\.git$//;
1873 $paths{$forks_path}++;
1881 our $gitweb_project_owner = undef;
1882 sub git_get_project_list_from_file
{
1884 return if (defined $gitweb_project_owner);
1886 $gitweb_project_owner = {};
1887 # read from file (url-encoded):
1888 # 'git%2Fgit.git Linus+Torvalds'
1889 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1890 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1891 if (-f
$projects_list) {
1892 open (my $fd , $projects_list);
1893 while (my $line = <$fd>) {
1895 my ($pr, $ow) = split ' ', $line;
1896 $pr = unescape
($pr);
1897 $ow = unescape
($ow);
1898 $gitweb_project_owner->{$pr} = to_utf8
($ow);
1904 sub git_get_project_owner
{
1905 my $project = shift;
1908 return undef unless $project;
1909 $git_dir = "$projectroot/$project";
1911 if (!defined $gitweb_project_owner) {
1912 git_get_project_list_from_file
();
1915 if (exists $gitweb_project_owner->{$project}) {
1916 $owner = $gitweb_project_owner->{$project};
1918 if (!defined $owner){
1919 $owner = git_get_project_config
('owner');
1921 if (!defined $owner) {
1922 $owner = get_file_owner
("$git_dir");
1928 sub git_get_last_activity
{
1932 $git_dir = "$projectroot/$path";
1933 open($fd, "-|", git_cmd
(), 'for-each-ref',
1934 '--format=%(committer)',
1935 '--sort=-committerdate',
1937 'refs/heads') or return;
1938 my $most_recent = <$fd>;
1939 close $fd or return;
1940 if (defined $most_recent &&
1941 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1943 my $age = time - $timestamp;
1944 return ($age, age_string
($age));
1946 return (undef, undef);
1949 sub git_get_references
{
1950 my $type = shift || "";
1952 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1953 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1954 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
1955 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
1958 while (my $line = <$fd>) {
1960 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
1961 if (defined $refs{$1}) {
1962 push @{$refs{$1}}, $2;
1968 close $fd or return;
1972 sub git_get_rev_name_tags
{
1973 my $hash = shift || return undef;
1975 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
1977 my $name_rev = <$fd>;
1980 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
1983 # catches also '$hash undefined' output
1988 ## ----------------------------------------------------------------------
1989 ## parse to hash functions
1993 my $tz = shift || "-0000";
1996 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
1997 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
1998 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
1999 $date{'hour'} = $hour;
2000 $date{'minute'} = $min;
2001 $date{'mday'} = $mday;
2002 $date{'day'} = $days[$wday];
2003 $date{'month'} = $months[$mon];
2004 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2005 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2006 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2007 $mday, $months[$mon], $hour ,$min;
2008 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2009 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2011 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2012 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2013 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2014 $date{'hour_local'} = $hour;
2015 $date{'minute_local'} = $min;
2016 $date{'tz_local'} = $tz;
2017 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2018 1900+$year, $mon+1, $mday,
2019 $hour, $min, $sec, $tz);
2028 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2029 $tag{'id'} = $tag_id;
2030 while (my $line = <$fd>) {
2032 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2033 $tag{'object'} = $1;
2034 } elsif ($line =~ m/^type (.+)$/) {
2036 } elsif ($line =~ m/^tag (.+)$/) {
2038 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2039 $tag{'author'} = $1;
2042 } elsif ($line =~ m/--BEGIN/) {
2043 push @comment, $line;
2045 } elsif ($line eq "") {
2049 push @comment, <$fd>;
2050 $tag{'comment'} = \
@comment;
2051 close $fd or return;
2052 if (!defined $tag{'name'}) {
2058 sub parse_commit_text
{
2059 my ($commit_text, $withparents) = @_;
2060 my @commit_lines = split '\n', $commit_text;
2063 pop @commit_lines; # Remove '\0'
2065 if (! @commit_lines) {
2069 my $header = shift @commit_lines;
2070 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2073 ($co{'id'}, my @parents) = split ' ', $header;
2074 while (my $line = shift @commit_lines) {
2075 last if $line eq "\n";
2076 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2078 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2080 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2082 $co{'author_epoch'} = $2;
2083 $co{'author_tz'} = $3;
2084 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2085 $co{'author_name'} = $1;
2086 $co{'author_email'} = $2;
2088 $co{'author_name'} = $co{'author'};
2090 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2091 $co{'committer'} = $1;
2092 $co{'committer_epoch'} = $2;
2093 $co{'committer_tz'} = $3;
2094 $co{'committer_name'} = $co{'committer'};
2095 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2096 $co{'committer_name'} = $1;
2097 $co{'committer_email'} = $2;
2099 $co{'committer_name'} = $co{'committer'};
2103 if (!defined $co{'tree'}) {
2106 $co{'parents'} = \
@parents;
2107 $co{'parent'} = $parents[0];
2109 foreach my $title (@commit_lines) {
2112 $co{'title'} = chop_str
($title, 80, 5);
2113 # remove leading stuff of merges to make the interesting part visible
2114 if (length($title) > 50) {
2115 $title =~ s/^Automatic //;
2116 $title =~ s/^merge (of|with) /Merge ... /i;
2117 if (length($title) > 50) {
2118 $title =~ s/(http|rsync):\/\///;
2120 if (length($title) > 50) {
2121 $title =~ s/(master|www|rsync)\.//;
2123 if (length($title) > 50) {
2124 $title =~ s/kernel.org:?//;
2126 if (length($title) > 50) {
2127 $title =~ s/\/pub\/scm//;
2130 $co{'title_short'} = chop_str
($title, 50, 5);
2134 if (! defined $co{'title'} || $co{'title'} eq "") {
2135 $co{'title'} = $co{'title_short'} = '(no commit message)';
2137 # remove added spaces
2138 foreach my $line (@commit_lines) {
2141 $co{'comment'} = \
@commit_lines;
2143 my $age = time - $co{'committer_epoch'};
2145 $co{'age_string'} = age_string
($age);
2146 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2147 if ($age > 60*60*24*7*2) {
2148 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2149 $co{'age_string_age'} = $co{'age_string'};
2151 $co{'age_string_date'} = $co{'age_string'};
2152 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2158 my ($commit_id) = @_;
2163 open my $fd, "-|", git_cmd
(), "rev-list",
2169 or die_error
(500, "Open git-rev-list failed");
2170 %co = parse_commit_text
(<$fd>, 1);
2177 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2185 open my $fd, "-|", git_cmd
(), "rev-list",
2188 ("--max-count=" . $maxcount),
2189 ("--skip=" . $skip),
2193 ($filename ? ($filename) : ())
2194 or die_error
(500, "Open git-rev-list failed");
2195 while (my $line = <$fd>) {
2196 my %co = parse_commit_text
($line);
2201 return wantarray ? @cos : \
@cos;
2204 # parse line of git-diff-tree "raw" output
2205 sub parse_difftree_raw_line
{
2209 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2210 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2211 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2212 $res{'from_mode'} = $1;
2213 $res{'to_mode'} = $2;
2214 $res{'from_id'} = $3;
2216 $res{'status'} = $5;
2217 $res{'similarity'} = $6;
2218 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2219 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2221 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2224 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2225 # combined diff (for merge commit)
2226 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2227 $res{'nparents'} = length($1);
2228 $res{'from_mode'} = [ split(' ', $2) ];
2229 $res{'to_mode'} = pop @{$res{'from_mode'}};
2230 $res{'from_id'} = [ split(' ', $3) ];
2231 $res{'to_id'} = pop @{$res{'from_id'}};
2232 $res{'status'} = [ split('', $4) ];
2233 $res{'to_file'} = unquote
($5);
2235 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2236 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2237 $res{'commit'} = $1;
2240 return wantarray ? %res : \
%res;
2243 # wrapper: return parsed line of git-diff-tree "raw" output
2244 # (the argument might be raw line, or parsed info)
2245 sub parsed_difftree_line
{
2246 my $line_or_ref = shift;
2248 if (ref($line_or_ref) eq "HASH") {
2249 # pre-parsed (or generated by hand)
2250 return $line_or_ref;
2252 return parse_difftree_raw_line
($line_or_ref);
2256 # parse line of git-ls-tree output
2257 sub parse_ls_tree_line
($;%) {
2262 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2263 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2271 $res{'name'} = unquote
($4);
2274 return wantarray ? %res : \
%res;
2277 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2278 sub parse_from_to_diffinfo
{
2279 my ($diffinfo, $from, $to, @parents) = @_;
2281 if ($diffinfo->{'nparents'}) {
2283 $from->{'file'} = [];
2284 $from->{'href'} = [];
2285 fill_from_file_info
($diffinfo, @parents)
2286 unless exists $diffinfo->{'from_file'};
2287 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2288 $from->{'file'}[$i] =
2289 defined $diffinfo->{'from_file'}[$i] ?
2290 $diffinfo->{'from_file'}[$i] :
2291 $diffinfo->{'to_file'};
2292 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2293 $from->{'href'}[$i] = href
(action
=>"blob",
2294 hash_base
=>$parents[$i],
2295 hash
=>$diffinfo->{'from_id'}[$i],
2296 file_name
=>$from->{'file'}[$i]);
2298 $from->{'href'}[$i] = undef;
2302 # ordinary (not combined) diff
2303 $from->{'file'} = $diffinfo->{'from_file'};
2304 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2305 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2306 hash
=>$diffinfo->{'from_id'},
2307 file_name
=>$from->{'file'});
2309 delete $from->{'href'};
2313 $to->{'file'} = $diffinfo->{'to_file'};
2314 if (!is_deleted
($diffinfo)) { # file exists in result
2315 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2316 hash
=>$diffinfo->{'to_id'},
2317 file_name
=>$to->{'file'});
2319 delete $to->{'href'};
2323 ## ......................................................................
2324 ## parse to array of hashes functions
2326 sub git_get_heads_list
{
2330 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2331 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2332 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2335 while (my $line = <$fd>) {
2339 my ($refinfo, $committerinfo) = split(/\0/, $line);
2340 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2341 my ($committer, $epoch, $tz) =
2342 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2343 $ref_item{'fullname'} = $name;
2344 $name =~ s!^refs/heads/!!;
2346 $ref_item{'name'} = $name;
2347 $ref_item{'id'} = $hash;
2348 $ref_item{'title'} = $title || '(no commit message)';
2349 $ref_item{'epoch'} = $epoch;
2351 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2353 $ref_item{'age'} = "unknown";
2356 push @headslist, \
%ref_item;
2360 return wantarray ? @headslist : \
@headslist;
2363 sub git_get_tags_list
{
2367 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2368 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2369 '--format=%(objectname) %(objecttype) %(refname) '.
2370 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2373 while (my $line = <$fd>) {
2377 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2378 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2379 my ($creator, $epoch, $tz) =
2380 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2381 $ref_item{'fullname'} = $name;
2382 $name =~ s!^refs/tags/!!;
2384 $ref_item{'type'} = $type;
2385 $ref_item{'id'} = $id;
2386 $ref_item{'name'} = $name;
2387 if ($type eq "tag") {
2388 $ref_item{'subject'} = $title;
2389 $ref_item{'reftype'} = $reftype;
2390 $ref_item{'refid'} = $refid;
2392 $ref_item{'reftype'} = $type;
2393 $ref_item{'refid'} = $id;
2396 if ($type eq "tag" || $type eq "commit") {
2397 $ref_item{'epoch'} = $epoch;
2399 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2401 $ref_item{'age'} = "unknown";
2405 push @tagslist, \
%ref_item;
2409 return wantarray ? @tagslist : \
@tagslist;
2412 ## ----------------------------------------------------------------------
2413 ## filesystem-related functions
2415 sub get_file_owner
{
2418 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2419 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2420 if (!defined $gcos) {
2424 $owner =~ s/[,;].*$//;
2425 return to_utf8
($owner);
2428 ## ......................................................................
2429 ## mimetype related functions
2431 sub mimetype_guess_file
{
2432 my $filename = shift;
2433 my $mimemap = shift;
2434 -r
$mimemap or return undef;
2437 open(MIME
, $mimemap) or return undef;
2439 next if m/^#/; # skip comments
2440 my ($mime, $exts) = split(/\t+/);
2441 if (defined $exts) {
2442 my @exts = split(/\s+/, $exts);
2443 foreach my $ext (@exts) {
2444 $mimemap{$ext} = $mime;
2450 $filename =~ /\.([^.]*)$/;
2451 return $mimemap{$1};
2454 sub mimetype_guess
{
2455 my $filename = shift;
2457 $filename =~ /\./ or return undef;
2459 if ($mimetypes_file) {
2460 my $file = $mimetypes_file;
2461 if ($file !~ m!^/!) { # if it is relative path
2462 # it is relative to project
2463 $file = "$projectroot/$project/$file";
2465 $mime = mimetype_guess_file
($filename, $file);
2467 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2473 my $filename = shift;
2476 my $mime = mimetype_guess
($filename);
2477 $mime and return $mime;
2481 return $default_blob_plain_mimetype unless $fd;
2484 return 'text/plain';
2485 } elsif (! $filename) {
2486 return 'application/octet-stream';
2487 } elsif ($filename =~ m/\.png$/i) {
2489 } elsif ($filename =~ m/\.gif$/i) {
2491 } elsif ($filename =~ m/\.jpe?g$/i) {
2492 return 'image/jpeg';
2494 return 'application/octet-stream';
2498 sub blob_contenttype
{
2499 my ($fd, $file_name, $type) = @_;
2501 $type ||= blob_mimetype
($fd, $file_name);
2502 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2503 $type .= "; charset=$default_text_plain_charset";
2509 ## ======================================================================
2510 ## functions printing HTML: header, footer, error page
2512 sub git_header_html
{
2513 my $status = shift || "200 OK";
2514 my $expires = shift;
2516 my $title = "$site_name";
2517 if (defined $project) {
2518 $title .= " - " . to_utf8
($project);
2519 if (defined $action) {
2520 $title .= "/$action";
2521 if (defined $file_name) {
2522 $title .= " - " . esc_path
($file_name);
2523 if ($action eq "tree" && $file_name !~ m
|/$|) {
2530 # require explicit support from the UA if we are to send the page as
2531 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2532 # we have to do this because MSIE sometimes globs '*/*', pretending to
2533 # support xhtml+xml but choking when it gets what it asked for.
2534 if (defined $cgi->http('HTTP_ACCEPT') &&
2535 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2536 $cgi->Accept('application/xhtml+xml') != 0) {
2537 $content_type = 'application/xhtml+xml';
2539 $content_type = 'text/html';
2541 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2542 -status
=> $status, -expires
=> $expires);
2543 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2545 <?xml version="1.0" encoding="utf-8"?>
2546 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2547 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2548 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2549 <!-- git core binaries version $git_version -->
2551 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2552 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2553 <meta name="robots" content="index, nofollow"/>
2554 <title>$title</title>
2556 # print out each stylesheet that exist
2557 if (defined $stylesheet) {
2558 #provides backwards capability for those people who define style sheet in a config file
2559 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2561 foreach my $stylesheet (@stylesheets) {
2562 next unless $stylesheet;
2563 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2566 if (defined $project) {
2567 my %href_params = get_feed_info
();
2568 if (!exists $href_params{'-title'}) {
2569 $href_params{'-title'} = 'log';
2572 foreach my $format qw(RSS Atom) {
2573 my $type = lc($format);
2575 '-rel' => 'alternate',
2576 '-title' => "$project - $href_params{'-title'} - $format feed",
2577 '-type' => "application/$type+xml"
2580 $href_params{'action'} = $type;
2581 $link_attr{'-href'} = href
(%href_params);
2583 "rel=\"$link_attr{'-rel'}\" ".
2584 "title=\"$link_attr{'-title'}\" ".
2585 "href=\"$link_attr{'-href'}\" ".
2586 "type=\"$link_attr{'-type'}\" ".
2589 $href_params{'extra_options'} = '--no-merges';
2590 $link_attr{'-href'} = href
(%href_params);
2591 $link_attr{'-title'} .= ' (no merges)';
2593 "rel=\"$link_attr{'-rel'}\" ".
2594 "title=\"$link_attr{'-title'}\" ".
2595 "href=\"$link_attr{'-href'}\" ".
2596 "type=\"$link_attr{'-type'}\" ".
2601 printf('<link rel="alternate" title="%s projects list" '.
2602 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2603 $site_name, href
(project
=>undef, action
=>"project_index"));
2604 printf('<link rel="alternate" title="%s projects feeds" '.
2605 'href="%s" type="text/x-opml" />'."\n",
2606 $site_name, href
(project
=>undef, action
=>"opml"));
2608 if (defined $favicon) {
2609 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2615 if (-f
$site_header) {
2616 open (my $fd, $site_header);
2621 print "<div class=\"page_header\">\n" .
2622 $cgi->a({-href
=> esc_url
($logo_url),
2623 -title
=> $logo_label},
2624 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2625 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2626 if (defined $project) {
2627 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2628 if (defined $action) {
2635 my ($have_search) = gitweb_check_feature
('search');
2636 if (defined $project && $have_search) {
2637 if (!defined $searchtext) {
2641 if (defined $hash_base) {
2642 $search_hash = $hash_base;
2643 } elsif (defined $hash) {
2644 $search_hash = $hash;
2646 $search_hash = "HEAD";
2648 my $action = $my_uri;
2649 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2650 if ($use_pathinfo) {
2651 $action .= "/".esc_url
($project);
2653 print $cgi->startform(-method => "get", -action
=> $action) .
2654 "<div class=\"search\">\n" .
2656 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2657 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2658 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2659 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2660 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2661 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2663 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2664 "<span title=\"Extended regular expression\">" .
2665 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2666 -checked
=> $search_use_regexp) .
2669 $cgi->end_form() . "\n";
2673 sub git_footer_html
{
2674 my $feed_class = 'rss_logo';
2676 print "<div class=\"page_footer\">\n";
2677 if (defined $project) {
2678 my $descr = git_get_project_description
($project);
2679 if (defined $descr) {
2680 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2683 my %href_params = get_feed_info
();
2684 if (!%href_params) {
2685 $feed_class .= ' generic';
2687 $href_params{'-title'} ||= 'log';
2689 foreach my $format qw(RSS Atom) {
2690 $href_params{'action'} = lc($format);
2691 print $cgi->a({-href
=> href
(%href_params),
2692 -title
=> "$href_params{'-title'} $format feed",
2693 -class => $feed_class}, $format)."\n";
2697 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2698 -class => $feed_class}, "OPML") . " ";
2699 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2700 -class => $feed_class}, "TXT") . "\n";
2702 print "</div>\n"; # class="page_footer"
2704 if (-f
$site_footer) {
2705 open (my $fd, $site_footer);
2714 # die_error(<http_status_code>, <error_message>)
2715 # Example: die_error(404, 'Hash not found')
2716 # By convention, use the following status codes (as defined in RFC 2616):
2717 # 400: Invalid or missing CGI parameters, or
2718 # requested object exists but has wrong type.
2719 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2720 # this server or project.
2721 # 404: Requested object/revision/project doesn't exist.
2722 # 500: The server isn't configured properly, or
2723 # an internal error occurred (e.g. failed assertions caused by bugs), or
2724 # an unknown error occurred (e.g. the git binary died unexpectedly).
2726 my $status = shift || 500;
2727 my $error = shift || "Internal server error";
2729 my %http_responses = (400 => '400 Bad Request',
2730 403 => '403 Forbidden',
2731 404 => '404 Not Found',
2732 500 => '500 Internal Server Error');
2733 git_header_html
($http_responses{$status});
2735 <div class="page_body">
2745 ## ----------------------------------------------------------------------
2746 ## functions printing or outputting HTML: navigation
2748 sub git_print_page_nav
{
2749 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2750 $extra = '' if !defined $extra; # pager or formats
2752 my @navs = qw(summary shortlog log commit commitdiff tree);
2754 @navs = grep { $_ ne $suppress } @navs;
2757 my %arg = map { $_ => {action
=>$_} } @navs;
2758 if (defined $head) {
2759 for (qw(commit commitdiff)) {
2760 $arg{$_}{'hash'} = $head;
2762 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2763 for (qw(shortlog log)) {
2764 $arg{$_}{'hash'} = $head;
2768 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2769 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2771 print "<div class=\"page_nav\">\n" .
2773 map { $_ eq $current ?
2774 $_ : $cgi->a({-href
=> href
(%{$arg{$_}})}, "$_")
2776 print "<br/>\n$extra<br/>\n" .
2780 sub format_paging_nav
{
2781 my ($action, $hash, $head, $page, $has_next_link) = @_;
2785 if ($hash ne $head || $page) {
2786 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2788 $paging_nav .= "HEAD";
2792 $paging_nav .= " ⋅ " .
2793 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2794 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2796 $paging_nav .= " ⋅ prev";
2799 if ($has_next_link) {
2800 $paging_nav .= " ⋅ " .
2801 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2802 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2804 $paging_nav .= " ⋅ next";
2810 ## ......................................................................
2811 ## functions printing or outputting HTML: div
2813 sub git_print_header_div
{
2814 my ($action, $title, $hash, $hash_base) = @_;
2817 $args{'action'} = $action;
2818 $args{'hash'} = $hash if $hash;
2819 $args{'hash_base'} = $hash_base if $hash_base;
2821 print "<div class=\"header\">\n" .
2822 $cgi->a({-href
=> href
(%args), -class => "title"},
2823 $title ? $title : $action) .
2827 #sub git_print_authorship (\%) {
2828 sub git_print_authorship
{
2831 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
2832 print "<div class=\"author_date\">" .
2833 esc_html
($co->{'author_name'}) .
2835 if ($ad{'hour_local'} < 6) {
2836 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2837 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2839 printf(" (%02d:%02d %s)",
2840 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2845 sub git_print_page_path
{
2851 print "<div class=\"page_path\">";
2852 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
2853 -title
=> 'tree root'}, to_utf8
("[$project]"));
2855 if (defined $name) {
2856 my @dirname = split '/', $name;
2857 my $basename = pop @dirname;
2860 foreach my $dir (@dirname) {
2861 $fullname .= ($fullname ? '/' : '') . $dir;
2862 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
2864 -title
=> $fullname}, esc_path
($dir));
2867 if (defined $type && $type eq 'blob') {
2868 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
2870 -title
=> $name}, esc_path
($basename));
2871 } elsif (defined $type && $type eq 'tree') {
2872 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
2874 -title
=> $name}, esc_path
($basename));
2877 print esc_path
($basename);
2880 print "<br/></div>\n";
2883 # sub git_print_log (\@;%) {
2884 sub git_print_log
($;%) {
2888 if ($opts{'-remove_title'}) {
2889 # remove title, i.e. first line of log
2892 # remove leading empty lines
2893 while (defined $log->[0] && $log->[0] eq "") {
2900 foreach my $line (@$log) {
2901 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2904 if (! $opts{'-remove_signoff'}) {
2905 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
2908 # remove signoff lines
2915 # print only one empty line
2916 # do not print empty line after signoff
2918 next if ($empty || $signoff);
2924 print format_log_line_html
($line) . "<br/>\n";
2927 if ($opts{'-final_empty_line'}) {
2928 # end with single empty line
2929 print "<br/>\n" unless $empty;
2933 # return link target (what link points to)
2934 sub git_get_link_target
{
2939 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
2943 $link_target = <$fd>;
2948 return $link_target;
2951 # given link target, and the directory (basedir) the link is in,
2952 # return target of link relative to top directory (top tree);
2953 # return undef if it is not possible (including absolute links).
2954 sub normalize_link_target
{
2955 my ($link_target, $basedir, $hash_base) = @_;
2957 # we can normalize symlink target only if $hash_base is provided
2958 return unless $hash_base;
2960 # absolute symlinks (beginning with '/') cannot be normalized
2961 return if (substr($link_target, 0, 1) eq '/');
2963 # normalize link target to path from top (root) tree (dir)
2966 $path = $basedir . '/' . $link_target;
2968 # we are in top (root) tree (dir)
2969 $path = $link_target;
2972 # remove //, /./, and /../
2974 foreach my $part (split('/', $path)) {
2975 # discard '.' and ''
2976 next if (!$part || $part eq '.');
2978 if ($part eq '..') {
2982 # link leads outside repository (outside top dir)
2986 push @path_parts, $part;
2989 $path = join('/', @path_parts);
2994 # print tree entry (row of git_tree), but without encompassing <tr> element
2995 sub git_print_tree_entry
{
2996 my ($t, $basedir, $hash_base, $have_blame) = @_;
2999 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3001 # The format of a table row is: mode list link. Where mode is
3002 # the mode of the entry, list is the name of the entry, an href,
3003 # and link is the action links of the entry.
3005 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3006 if ($t->{'type'} eq "blob") {
3007 print "<td class=\"list\">" .
3008 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3009 file_name
=>"$basedir$t->{'name'}", %base_key),
3010 -class => "list"}, esc_path
($t->{'name'}));
3011 if (S_ISLNK
(oct $t->{'mode'})) {
3012 my $link_target = git_get_link_target
($t->{'hash'});
3014 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3015 if (defined $norm_target) {
3017 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3018 file_name
=>$norm_target),
3019 -title
=> $norm_target}, esc_path
($link_target));
3021 print " -> " . esc_path
($link_target);
3026 print "<td class=\"link\">";
3027 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3028 file_name
=>"$basedir$t->{'name'}", %base_key)},
3032 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3033 file_name
=>"$basedir$t->{'name'}", %base_key)},
3036 if (defined $hash_base) {
3038 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3039 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3043 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3044 file_name
=>"$basedir$t->{'name'}")},
3048 } elsif ($t->{'type'} eq "tree") {
3049 print "<td class=\"list\">";
3050 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3051 file_name
=>"$basedir$t->{'name'}", %base_key)},
3052 esc_path
($t->{'name'}));
3054 print "<td class=\"link\">";
3055 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3056 file_name
=>"$basedir$t->{'name'}", %base_key)},
3058 if (defined $hash_base) {
3060 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3061 file_name
=>"$basedir$t->{'name'}")},
3066 # unknown object: we can only present history for it
3067 # (this includes 'commit' object, i.e. submodule support)
3068 print "<td class=\"list\">" .
3069 esc_path
($t->{'name'}) .
3071 print "<td class=\"link\">";
3072 if (defined $hash_base) {
3073 print $cgi->a({-href
=> href
(action
=>"history",
3074 hash_base
=>$hash_base,
3075 file_name
=>"$basedir$t->{'name'}")},
3082 ## ......................................................................
3083 ## functions printing large fragments of HTML
3085 # get pre-image filenames for merge (combined) diff
3086 sub fill_from_file_info
{
3087 my ($diff, @parents) = @_;
3089 $diff->{'from_file'} = [ ];
3090 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3091 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3092 if ($diff->{'status'}[$i] eq 'R' ||
3093 $diff->{'status'}[$i] eq 'C') {
3094 $diff->{'from_file'}[$i] =
3095 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3102 # is current raw difftree line of file deletion
3104 my $diffinfo = shift;
3106 return $diffinfo->{'to_id'} eq ('0' x
40);
3109 # does patch correspond to [previous] difftree raw line
3110 # $diffinfo - hashref of parsed raw diff format
3111 # $patchinfo - hashref of parsed patch diff format
3112 # (the same keys as in $diffinfo)
3113 sub is_patch_split
{
3114 my ($diffinfo, $patchinfo) = @_;
3116 return defined $diffinfo && defined $patchinfo
3117 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3121 sub git_difftree_body
{
3122 my ($difftree, $hash, @parents) = @_;
3123 my ($parent) = $parents[0];
3124 my ($have_blame) = gitweb_check_feature
('blame');
3125 print "<div class=\"list_head\">\n";
3126 if ($#{$difftree} > 10) {
3127 print(($#{$difftree} + 1) . " files changed:\n");
3131 print "<table class=\"" .
3132 (@parents > 1 ? "combined " : "") .
3135 # header only for combined diff in 'commitdiff' view
3136 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3139 print "<thead><tr>\n" .
3140 "<th></th><th></th>\n"; # filename, patchN link
3141 for (my $i = 0; $i < @parents; $i++) {
3142 my $par = $parents[$i];
3144 $cgi->a({-href
=> href
(action
=>"commitdiff",
3145 hash
=>$hash, hash_parent
=>$par),
3146 -title
=> 'commitdiff to parent number ' .
3147 ($i+1) . ': ' . substr($par,0,7)},
3151 print "</tr></thead>\n<tbody>\n";
3156 foreach my $line (@{$difftree}) {
3157 my $diff = parsed_difftree_line
($line);
3160 print "<tr class=\"dark\">\n";
3162 print "<tr class=\"light\">\n";
3166 if (exists $diff->{'nparents'}) { # combined diff
3168 fill_from_file_info
($diff, @parents)
3169 unless exists $diff->{'from_file'};
3171 if (!is_deleted
($diff)) {
3172 # file exists in the result (child) commit
3174 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3175 file_name
=>$diff->{'to_file'},
3177 -class => "list"}, esc_path
($diff->{'to_file'})) .
3181 esc_path
($diff->{'to_file'}) .
3185 if ($action eq 'commitdiff') {
3188 print "<td class=\"link\">" .
3189 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3194 my $has_history = 0;
3195 my $not_deleted = 0;
3196 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3197 my $hash_parent = $parents[$i];
3198 my $from_hash = $diff->{'from_id'}[$i];
3199 my $from_path = $diff->{'from_file'}[$i];
3200 my $status = $diff->{'status'}[$i];
3202 $has_history ||= ($status ne 'A');
3203 $not_deleted ||= ($status ne 'D');
3205 if ($status eq 'A') {
3206 print "<td class=\"link\" align=\"right\"> | </td>\n";
3207 } elsif ($status eq 'D') {
3208 print "<td class=\"link\">" .
3209 $cgi->a({-href
=> href
(action
=>"blob",
3212 file_name
=>$from_path)},
3216 if ($diff->{'to_id'} eq $from_hash) {
3217 print "<td class=\"link nochange\">";
3219 print "<td class=\"link\">";
3221 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3222 hash
=>$diff->{'to_id'},
3223 hash_parent
=>$from_hash,
3225 hash_parent_base
=>$hash_parent,
3226 file_name
=>$diff->{'to_file'},
3227 file_parent
=>$from_path)},
3233 print "<td class=\"link\">";
3235 print $cgi->a({-href
=> href
(action
=>"blob",
3236 hash
=>$diff->{'to_id'},
3237 file_name
=>$diff->{'to_file'},
3240 print " | " if ($has_history);
3243 print $cgi->a({-href
=> href
(action
=>"history",
3244 file_name
=>$diff->{'to_file'},
3251 next; # instead of 'else' clause, to avoid extra indent
3253 # else ordinary diff
3255 my ($to_mode_oct, $to_mode_str, $to_file_type);
3256 my ($from_mode_oct, $from_mode_str, $from_file_type);
3257 if ($diff->{'to_mode'} ne ('0' x
6)) {
3258 $to_mode_oct = oct $diff->{'to_mode'};
3259 if (S_ISREG
($to_mode_oct)) { # only for regular file
3260 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3262 $to_file_type = file_type
($diff->{'to_mode'});
3264 if ($diff->{'from_mode'} ne ('0' x
6)) {
3265 $from_mode_oct = oct $diff->{'from_mode'};
3266 if (S_ISREG
($to_mode_oct)) { # only for regular file
3267 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3269 $from_file_type = file_type
($diff->{'from_mode'});
3272 if ($diff->{'status'} eq "A") { # created
3273 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3274 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3275 $mode_chng .= "]</span>";
3277 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3278 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3279 -class => "list"}, esc_path
($diff->{'file'}));
3281 print "<td>$mode_chng</td>\n";
3282 print "<td class=\"link\">";
3283 if ($action eq 'commitdiff') {
3286 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3289 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3290 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3294 } elsif ($diff->{'status'} eq "D") { # deleted
3295 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3297 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3298 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3299 -class => "list"}, esc_path
($diff->{'file'}));
3301 print "<td>$mode_chng</td>\n";
3302 print "<td class=\"link\">";
3303 if ($action eq 'commitdiff') {
3306 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3309 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3310 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3313 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3314 file_name
=>$diff->{'file'})},
3317 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3318 file_name
=>$diff->{'file'})},
3322 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3323 my $mode_chnge = "";
3324 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3325 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3326 if ($from_file_type ne $to_file_type) {
3327 $mode_chnge .= " from $from_file_type to $to_file_type";
3329 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3330 if ($from_mode_str && $to_mode_str) {
3331 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3332 } elsif ($to_mode_str) {
3333 $mode_chnge .= " mode: $to_mode_str";
3336 $mode_chnge .= "]</span>\n";
3339 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3340 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3341 -class => "list"}, esc_path
($diff->{'file'}));
3343 print "<td>$mode_chnge</td>\n";
3344 print "<td class=\"link\">";
3345 if ($action eq 'commitdiff') {
3348 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3350 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3351 # "commit" view and modified file (not onlu mode changed)
3352 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3353 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3354 hash_base
=>$hash, hash_parent_base
=>$parent,
3355 file_name
=>$diff->{'file'})},
3359 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3360 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3363 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3364 file_name
=>$diff->{'file'})},
3367 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3368 file_name
=>$diff->{'file'})},
3372 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3373 my %status_name = ('R' => 'moved', 'C' => 'copied');
3374 my $nstatus = $status_name{$diff->{'status'}};
3376 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3377 # mode also for directories, so we cannot use $to_mode_str
3378 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3381 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3382 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3383 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3384 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3385 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3386 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3387 -class => "list"}, esc_path
($diff->{'from_file'})) .
3388 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3389 "<td class=\"link\">";
3390 if ($action eq 'commitdiff') {
3393 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3395 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3396 # "commit" view and modified file (not only pure rename or copy)
3397 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3398 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3399 hash_base
=>$hash, hash_parent_base
=>$parent,
3400 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3404 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3405 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3408 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3409 file_name
=>$diff->{'to_file'})},
3412 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3413 file_name
=>$diff->{'to_file'})},
3417 } # we should not encounter Unmerged (U) or Unknown (X) status
3420 print "</tbody>" if $has_header;
3424 sub git_patchset_body
{
3425 my ($fd, $difftree, $hash, @hash_parents) = @_;
3426 my ($hash_parent) = $hash_parents[0];
3428 my $is_combined = (@hash_parents > 1);
3430 my $patch_number = 0;
3436 print "<div class=\"patchset\">\n";
3438 # skip to first patch
3439 while ($patch_line = <$fd>) {
3442 last if ($patch_line =~ m/^diff /);
3446 while ($patch_line) {
3448 # parse "git diff" header line
3449 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3450 # $1 is from_name, which we do not use
3451 $to_name = unquote
($2);
3452 $to_name =~ s!^b/!!;
3453 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3454 # $1 is 'cc' or 'combined', which we do not use
3455 $to_name = unquote
($2);
3460 # check if current patch belong to current raw line
3461 # and parse raw git-diff line if needed
3462 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3463 # this is continuation of a split patch
3464 print "<div class=\"patch cont\">\n";
3466 # advance raw git-diff output if needed
3467 $patch_idx++ if defined $diffinfo;
3469 # read and prepare patch information
3470 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3472 # compact combined diff output can have some patches skipped
3473 # find which patch (using pathname of result) we are at now;
3475 while ($to_name ne $diffinfo->{'to_file'}) {
3476 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3477 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3478 "</div>\n"; # class="patch"
3483 last if $patch_idx > $#$difftree;
3484 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3488 # modifies %from, %to hashes
3489 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3491 # this is first patch for raw difftree line with $patch_idx index
3492 # we index @$difftree array from 0, but number patches from 1
3493 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3497 #assert($patch_line =~ m/^diff /) if DEBUG;
3498 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3500 # print "git diff" header
3501 print format_git_diff_header_line
($patch_line, $diffinfo,
3504 # print extended diff header
3505 print "<div class=\"diff extended_header\">\n";
3507 while ($patch_line = <$fd>) {
3510 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3512 print format_extended_diff_header_line
($patch_line, $diffinfo,
3515 print "</div>\n"; # class="diff extended_header"
3517 # from-file/to-file diff header
3518 if (! $patch_line) {
3519 print "</div>\n"; # class="patch"
3522 next PATCH
if ($patch_line =~ m/^diff /);
3523 #assert($patch_line =~ m/^---/) if DEBUG;
3525 my $last_patch_line = $patch_line;
3526 $patch_line = <$fd>;
3528 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3530 print format_diff_from_to_header
($last_patch_line, $patch_line,
3531 $diffinfo, \
%from, \
%to,
3536 while ($patch_line = <$fd>) {
3539 next PATCH
if ($patch_line =~ m/^diff /);
3541 print format_diff_line
($patch_line, \
%from, \
%to);
3545 print "</div>\n"; # class="patch"
3548 # for compact combined (--cc) format, with chunk and patch simpliciaction
3549 # patchset might be empty, but there might be unprocessed raw lines
3550 for (++$patch_idx if $patch_number > 0;
3551 $patch_idx < @$difftree;
3553 # read and prepare patch information
3554 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3556 # generate anchor for "patch" links in difftree / whatchanged part
3557 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3558 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3559 "</div>\n"; # class="patch"
3564 if ($patch_number == 0) {
3565 if (@hash_parents > 1) {
3566 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3568 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3572 print "</div>\n"; # class="patchset"
3575 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3577 # fills project list info (age, description, owner, forks) for each
3578 # project in the list, removing invalid projects from returned list
3579 # NOTE: modifies $projlist, but does not remove entries from it
3580 sub fill_project_list_info
{
3581 my ($projlist, $check_forks) = @_;
3585 foreach my $pr (@$projlist) {
3586 my (@activity) = git_get_last_activity
($pr->{'path'});
3587 unless (@activity) {
3590 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3591 if (!defined $pr->{'descr'}) {
3592 my $descr = git_get_project_description
($pr->{'path'}) || "";
3593 $descr = to_utf8
($descr);
3594 $pr->{'descr_long'} = $descr;
3595 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3597 if (!defined $pr->{'owner'}) {
3598 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3601 my $pname = $pr->{'path'};
3602 if (($pname =~ s/\.git$//) &&
3603 ($pname !~ /\/$/) &&
3604 (-d
"$projectroot/$pname")) {
3605 $pr->{'forks'} = "-d $projectroot/$pname";
3610 push @projects, $pr;
3616 # print 'sort by' <th> element, generating 'sort by $name' replay link
3617 # if that order is not selected
3619 my ($name, $order, $header) = @_;
3620 $header ||= ucfirst($name);
3622 if ($order eq $name) {
3623 print "<th>$header</th>\n";
3626 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3627 -class => "header"}, $header) .
3632 sub git_project_list_body
{
3633 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3635 my ($check_forks) = gitweb_check_feature
('forks');
3636 my @projects = fill_project_list_info
($projlist, $check_forks);
3638 $order ||= $default_projects_order;
3639 $from = 0 unless defined $from;
3640 $to = $#projects if (!defined $to || $#projects < $to);
3643 project
=> { key
=> 'path', type
=> 'str' },
3644 descr
=> { key
=> 'descr_long', type
=> 'str' },
3645 owner
=> { key
=> 'owner', type
=> 'str' },
3646 age
=> { key
=> 'age', type
=> 'num' }
3648 my $oi = $order_info{$order};
3649 if ($oi->{'type'} eq 'str') {
3650 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3652 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3655 print "<table class=\"project_list\">\n";
3656 unless ($no_header) {
3659 print "<th></th>\n";
3661 print_sort_th
('project', $order, 'Project');
3662 print_sort_th
('descr', $order, 'Description');
3663 print_sort_th
('owner', $order, 'Owner');
3664 print_sort_th
('age', $order, 'Last Change');
3665 print "<th></th>\n" . # for links
3669 for (my $i = $from; $i <= $to; $i++) {
3670 my $pr = $projects[$i];
3672 print "<tr class=\"dark\">\n";
3674 print "<tr class=\"light\">\n";
3679 if ($pr->{'forks'}) {
3680 print "<!-- $pr->{'forks'} -->\n";
3681 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3685 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3686 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3687 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3688 -class => "list", -title
=> $pr->{'descr_long'}},
3689 esc_html
($pr->{'descr'})) . "</td>\n" .
3690 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3691 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3692 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3693 "<td class=\"link\">" .
3694 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3695 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3696 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3697 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3698 ($pr->{'forks'} ? " | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3702 if (defined $extra) {
3705 print "<td></td>\n";
3707 print "<td colspan=\"5\">$extra</td>\n" .
3713 sub git_shortlog_body
{
3714 # uses global variable $project
3715 my ($commitlist, $from, $to, $refs, $extra) = @_;
3717 $from = 0 unless defined $from;
3718 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3720 print "<table class=\"shortlog\">\n";
3722 for (my $i = $from; $i <= $to; $i++) {
3723 my %co = %{$commitlist->[$i]};
3724 my $commit = $co{'id'};
3725 my $ref = format_ref_marker
($refs, $commit);
3727 print "<tr class=\"dark\">\n";
3729 print "<tr class=\"light\">\n";
3732 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3733 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3734 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3735 "<td><i>" . $author . "</i></td>\n" .
3737 print format_subject_html
($co{'title'}, $co{'title_short'},
3738 href
(action
=>"commit", hash
=>$commit), $ref);
3740 "<td class=\"link\">" .
3741 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3742 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3743 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3744 my $snapshot_links = format_snapshot_links
($commit);
3745 if (defined $snapshot_links) {
3746 print " | " . $snapshot_links;
3751 if (defined $extra) {
3753 "<td colspan=\"4\">$extra</td>\n" .
3759 sub git_history_body
{
3760 # Warning: assumes constant type (blob or tree) during history
3761 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3763 $from = 0 unless defined $from;
3764 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3766 print "<table class=\"history\">\n";
3768 for (my $i = $from; $i <= $to; $i++) {
3769 my %co = %{$commitlist->[$i]};
3773 my $commit = $co{'id'};
3775 my $ref = format_ref_marker
($refs, $commit);
3778 print "<tr class=\"dark\">\n";
3780 print "<tr class=\"light\">\n";
3783 # shortlog uses chop_str($co{'author_name'}, 10)
3784 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
3785 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3786 "<td><i>" . $author . "</i></td>\n" .
3788 # originally git_history used chop_str($co{'title'}, 50)
3789 print format_subject_html
($co{'title'}, $co{'title_short'},
3790 href
(action
=>"commit", hash
=>$commit), $ref);
3792 "<td class=\"link\">" .
3793 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
3794 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
3796 if ($ftype eq 'blob') {
3797 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
3798 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
3799 if (defined $blob_current && defined $blob_parent &&
3800 $blob_current ne $blob_parent) {
3802 $cgi->a({-href
=> href
(action
=>"blobdiff",
3803 hash
=>$blob_current, hash_parent
=>$blob_parent,
3804 hash_base
=>$hash_base, hash_parent_base
=>$commit,
3805 file_name
=>$file_name)},
3812 if (defined $extra) {
3814 "<td colspan=\"4\">$extra</td>\n" .
3821 # uses global variable $project
3822 my ($taglist, $from, $to, $extra) = @_;
3823 $from = 0 unless defined $from;
3824 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3826 print "<table class=\"tags\">\n";
3828 for (my $i = $from; $i <= $to; $i++) {
3829 my $entry = $taglist->[$i];
3831 my $comment = $tag{'subject'};
3833 if (defined $comment) {
3834 $comment_short = chop_str
($comment, 30, 5);
3837 print "<tr class=\"dark\">\n";
3839 print "<tr class=\"light\">\n";
3842 if (defined $tag{'age'}) {
3843 print "<td><i>$tag{'age'}</i></td>\n";
3845 print "<td></td>\n";
3848 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
3849 -class => "list name"}, esc_html
($tag{'name'})) .
3852 if (defined $comment) {
3853 print format_subject_html
($comment, $comment_short,
3854 href
(action
=>"tag", hash
=>$tag{'id'}));
3857 "<td class=\"selflink\">";
3858 if ($tag{'type'} eq "tag") {
3859 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
3864 "<td class=\"link\">" . " | " .
3865 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
3866 if ($tag{'reftype'} eq "commit") {
3867 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
3868 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
3869 } elsif ($tag{'reftype'} eq "blob") {
3870 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
3875 if (defined $extra) {
3877 "<td colspan=\"5\">$extra</td>\n" .
3883 sub git_heads_body
{
3884 # uses global variable $project
3885 my ($headlist, $head, $from, $to, $extra) = @_;
3886 $from = 0 unless defined $from;
3887 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3889 print "<table class=\"heads\">\n";
3891 for (my $i = $from; $i <= $to; $i++) {
3892 my $entry = $headlist->[$i];
3894 my $curr = $ref{'id'} eq $head;
3896 print "<tr class=\"dark\">\n";
3898 print "<tr class=\"light\">\n";
3901 print "<td><i>$ref{'age'}</i></td>\n" .
3902 ($curr ? "<td class=\"current_head\">" : "<td>") .
3903 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
3904 -class => "list name"},esc_html
($ref{'name'})) .
3906 "<td class=\"link\">" .
3907 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
3908 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
3909 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
3913 if (defined $extra) {
3915 "<td colspan=\"3\">$extra</td>\n" .
3921 sub git_search_grep_body
{
3922 my ($commitlist, $from, $to, $extra) = @_;
3923 $from = 0 unless defined $from;
3924 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3926 print "<table class=\"commit_search\">\n";
3928 for (my $i = $from; $i <= $to; $i++) {
3929 my %co = %{$commitlist->[$i]};
3933 my $commit = $co{'id'};
3935 print "<tr class=\"dark\">\n";
3937 print "<tr class=\"light\">\n";
3940 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
3941 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3942 "<td><i>" . $author . "</i></td>\n" .
3944 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
3945 -class => "list subject"},
3946 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
3947 my $comment = $co{'comment'};
3948 foreach my $line (@$comment) {
3949 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3950 my ($lead, $match, $trail) = ($1, $2, $3);
3951 $match = chop_str
($match, 70, 5, 'center');
3952 my $contextlen = int((80 - length($match))/2);
3953 $contextlen = 30 if ($contextlen > 30);
3954 $lead = chop_str
($lead, $contextlen, 10, 'left');
3955 $trail = chop_str
($trail, $contextlen, 10, 'right');
3957 $lead = esc_html
($lead);
3958 $match = esc_html
($match);
3959 $trail = esc_html
($trail);
3961 print "$lead<span class=\"match\">$match</span>$trail<br />";
3965 "<td class=\"link\">" .
3966 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
3968 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
3970 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
3974 if (defined $extra) {
3976 "<td colspan=\"3\">$extra</td>\n" .
3982 ## ======================================================================
3983 ## ======================================================================
3986 sub git_project_list
{
3987 my $order = $cgi->param('o');
3988 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
3989 die_error
(400, "Unknown order parameter");
3992 my @list = git_get_projects_list
();
3994 die_error
(404, "No projects found");
3998 if (-f
$home_text) {
3999 print "<div class=\"index_include\">\n";
4000 open (my $fd, $home_text);
4005 git_project_list_body
(\
@list, $order);
4010 my $order = $cgi->param('o');
4011 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4012 die_error
(400, "Unknown order parameter");
4015 my @list = git_get_projects_list
($project);
4017 die_error
(404, "No forks found");
4021 git_print_page_nav
('','');
4022 git_print_header_div
('summary', "$project forks");
4023 git_project_list_body
(\
@list, $order);
4027 sub git_project_index
{
4028 my @projects = git_get_projects_list
($project);
4031 -type
=> 'text/plain',
4032 -charset
=> 'utf-8',
4033 -content_disposition
=> 'inline; filename="index.aux"');
4035 foreach my $pr (@projects) {
4036 if (!exists $pr->{'owner'}) {
4037 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4040 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4041 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4042 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4043 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4047 print "$path $owner\n";
4052 my $descr = git_get_project_description
($project) || "none";
4053 my %co = parse_commit
("HEAD");
4054 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4055 my $head = $co{'id'};
4057 my $owner = git_get_project_owner
($project);
4059 my $refs = git_get_references
();
4060 # These get_*_list functions return one more to allow us to see if
4061 # there are more ...
4062 my @taglist = git_get_tags_list
(16);
4063 my @headlist = git_get_heads_list
(16);
4065 my ($check_forks) = gitweb_check_feature
('forks');
4068 @forklist = git_get_projects_list
($project);
4072 git_print_page_nav
('summary','', $head);
4074 print "<div class=\"title\"> </div>\n";
4075 print "<table class=\"projects_list\">\n" .
4076 "<tr><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4077 "<tr><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4078 if (defined $cd{'rfc2822'}) {
4079 print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4082 # use per project git URL list in $projectroot/$project/cloneurl
4083 # or make project git URL from git base URL and project name
4084 my $url_tag = "URL";
4085 my @url_list = git_get_project_url_list
($project);
4086 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4087 foreach my $git_url (@url_list) {
4088 next unless $git_url;
4089 print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
4094 if (-s
"$projectroot/$project/README.html") {
4095 if (open my $fd, "$projectroot/$project/README.html") {
4096 print "<div class=\"title\">readme</div>\n" .
4097 "<div class=\"readme\">\n";
4098 print $_ while (<$fd>);
4099 print "\n</div>\n"; # class="readme"
4104 # we need to request one more than 16 (0..15) to check if
4106 my @commitlist = $head ? parse_commits
($head, 17) : ();
4108 git_print_header_div
('shortlog');
4109 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4110 $#commitlist <= 15 ? undef :
4111 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4115 git_print_header_div
('tags');
4116 git_tags_body
(\
@taglist, 0, 15,
4117 $#taglist <= 15 ? undef :
4118 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4122 git_print_header_div
('heads');
4123 git_heads_body
(\
@headlist, $head, 0, 15,
4124 $#headlist <= 15 ? undef :
4125 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4129 git_print_header_div
('forks');
4130 git_project_list_body
(\
@forklist, 'age', 0, 15,
4131 $#forklist <= 15 ? undef :
4132 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4140 my $head = git_get_head_hash
($project);
4142 git_print_page_nav
('','', $head,undef,$head);
4143 my %tag = parse_tag
($hash);
4146 die_error
(404, "Unknown tag object");
4149 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4150 print "<div class=\"title_text\">\n" .
4151 "<table class=\"object_header\">\n" .
4153 "<td>object</td>\n" .
4154 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4155 $tag{'object'}) . "</td>\n" .
4156 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4157 $tag{'type'}) . "</td>\n" .
4159 if (defined($tag{'author'})) {
4160 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4161 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4162 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4163 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4166 print "</table>\n\n" .
4168 print "<div class=\"page_body\">";
4169 my $comment = $tag{'comment'};
4170 foreach my $line (@$comment) {
4172 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4182 gitweb_check_feature
('blame')
4183 or die_error
(403, "Blame view not allowed");
4185 die_error
(400, "No file name given") unless $file_name;
4186 $hash_base ||= git_get_head_hash
($project);
4187 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4188 my %co = parse_commit
($hash_base)
4189 or die_error
(404, "Commit not found");
4190 if (!defined $hash) {
4191 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4192 or die_error
(404, "Error looking up file");
4194 $ftype = git_get_type
($hash);
4195 if ($ftype !~ "blob") {
4196 die_error
(400, "Object is not a blob");
4198 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4199 $file_name, $hash_base)
4200 or die_error
(500, "Open git-blame failed");
4203 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4206 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4209 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4211 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4212 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4213 git_print_page_path
($file_name, $ftype, $hash_base);
4214 my @rev_color = (qw(light2 dark2));
4215 my $num_colors = scalar(@rev_color);
4216 my $current_color = 0;
4219 <div class="page_body">
4220 <table class="blame">
4221 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4226 last unless defined $_;
4227 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4228 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4229 if (!exists $metainfo{$full_rev}) {
4230 $metainfo{$full_rev} = {};
4232 my $meta = $metainfo{$full_rev};
4235 if (/^(\S+) (.*)$/) {
4241 my $rev = substr($full_rev, 0, 8);
4242 my $author = $meta->{'author'};
4243 my %date = parse_date
($meta->{'author-time'},
4244 $meta->{'author-tz'});
4245 my $date = $date{'iso-tz'};
4247 $current_color = ++$current_color % $num_colors;
4249 print "<tr class=\"$rev_color[$current_color]\">\n";
4251 print "<td class=\"sha1\"";
4252 print " title=\"". esc_html
($author) . ", $date\"";
4253 print " rowspan=\"$group_size\"" if ($group_size > 1);
4255 print $cgi->a({-href
=> href
(action
=>"commit",
4257 file_name
=>$file_name)},
4261 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4262 or die_error
(500, "Open git-rev-parse failed");
4263 my $parent_commit = <$dd>;
4265 chomp($parent_commit);
4266 my $blamed = href
(action
=> 'blame',
4267 file_name
=> $meta->{'filename'},
4268 hash_base
=> $parent_commit);
4269 print "<td class=\"linenr\">";
4270 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4272 -class => "linenr" },
4275 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4281 or print "Reading blob failed\n";
4286 my $head = git_get_head_hash
($project);
4288 git_print_page_nav
('','', $head,undef,$head);
4289 git_print_header_div
('summary', $project);
4291 my @tagslist = git_get_tags_list
();
4293 git_tags_body
(\
@tagslist);
4299 my $head = git_get_head_hash
($project);
4301 git_print_page_nav
('','', $head,undef,$head);
4302 git_print_header_div
('summary', $project);
4304 my @headslist = git_get_heads_list
();
4306 git_heads_body
(\
@headslist, $head);
4311 sub git_blob_plain
{
4315 if (!defined $hash) {
4316 if (defined $file_name) {
4317 my $base = $hash_base || git_get_head_hash
($project);
4318 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4319 or die_error
(404, "Cannot find file");
4321 die_error
(400, "No file name defined");
4323 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4324 # blobs defined by non-textual hash id's can be cached
4328 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4329 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4331 # content-type (can include charset)
4332 $type = blob_contenttype
($fd, $file_name, $type);
4334 # "save as" filename, even when no $file_name is given
4335 my $save_as = "$hash";
4336 if (defined $file_name) {
4337 $save_as = $file_name;
4338 } elsif ($type =~ m/^text\//) {
4344 -expires
=> $expires,
4345 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4347 binmode STDOUT
, ':raw';
4349 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4357 if (!defined $hash) {
4358 if (defined $file_name) {
4359 my $base = $hash_base || git_get_head_hash
($project);
4360 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4361 or die_error
(404, "Cannot find file");
4363 die_error
(400, "No file name defined");
4365 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4366 # blobs defined by non-textual hash id's can be cached
4370 my ($have_blame) = gitweb_check_feature
('blame');
4371 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4372 or die_error
(500, "Couldn't cat $file_name, $hash");
4373 my $mimetype = blob_mimetype
($fd, $file_name);
4374 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4376 return git_blob_plain
($mimetype);
4378 # we can have blame only for text/* mimetype
4379 $have_blame &&= ($mimetype =~ m!^text/!);
4381 git_header_html
(undef, $expires);
4382 my $formats_nav = '';
4383 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4384 if (defined $file_name) {
4387 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4392 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4395 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4398 $cgi->a({-href
=> href
(action
=>"blob",
4399 hash_base
=>"HEAD", file_name
=>$file_name)},
4403 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4406 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4407 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4409 print "<div class=\"page_nav\">\n" .
4410 "<br/><br/></div>\n" .
4411 "<div class=\"title\">$hash</div>\n";
4413 git_print_page_path
($file_name, "blob", $hash_base);
4414 print "<div class=\"page_body\">\n";
4415 if ($mimetype =~ m!^image/!) {
4416 print qq
!<img type
="$mimetype"!;
4418 print qq
! alt
="$file_name" title
="$file_name"!;
4421 href(action=>"blob_plain
", hash=>$hash,
4422 hash_base=>$hash_base, file_name=>$file_name) .
4426 while (my $line = <$fd>) {
4429 $line = untabify
($line);
4430 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4431 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4435 or print "Reading blob failed.\n";
4441 if (!defined $hash_base) {
4442 $hash_base = "HEAD";
4444 if (!defined $hash) {
4445 if (defined $file_name) {
4446 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4452 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4453 or die_error
(500, "Open git-ls-tree failed");
4454 my @entries = map { chomp; $_ } <$fd>;
4455 close $fd or die_error
(404, "Reading tree failed");
4458 my $refs = git_get_references
();
4459 my $ref = format_ref_marker
($refs, $hash_base);
4462 my ($have_blame) = gitweb_check_feature
('blame');
4463 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4465 if (defined $file_name) {
4467 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4469 $cgi->a({-href
=> href
(action
=>"tree",
4470 hash_base
=>"HEAD", file_name
=>$file_name)},
4473 my $snapshot_links = format_snapshot_links
($hash);
4474 if (defined $snapshot_links) {
4475 # FIXME: Should be available when we have no hash base as well.
4476 push @views_nav, $snapshot_links;
4478 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4479 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4482 print "<div class=\"page_nav\">\n";
4483 print "<br/><br/></div>\n";
4484 print "<div class=\"title\">$hash</div>\n";
4486 if (defined $file_name) {
4487 $basedir = $file_name;
4488 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4492 git_print_page_path
($file_name, 'tree', $hash_base);
4493 print "<div class=\"page_body\">\n";
4494 print "<table class=\"tree\">\n";
4496 # '..' (top directory) link if possible
4497 if (defined $hash_base &&
4498 defined $file_name && $file_name =~ m![^/]+$!) {
4500 print "<tr class=\"dark\">\n";
4502 print "<tr class=\"light\">\n";
4506 my $up = $file_name;
4507 $up =~ s!/?[^/]+$!!;
4508 undef $up unless $up;
4509 # based on git_print_tree_entry
4510 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4511 print '<td class="list">';
4512 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4516 print "<td class=\"link\"></td>\n";
4520 foreach my $line (@entries) {
4521 my %t = parse_ls_tree_line
($line, -z
=> 1);
4524 print "<tr class=\"dark\">\n";
4526 print "<tr class=\"light\">\n";
4530 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4534 print "</table>\n" .
4540 my @supported_fmts = gitweb_check_feature
('snapshot');
4541 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4543 my $format = $cgi->param('sf');
4544 if (!@supported_fmts) {
4545 die_error
(403, "Snapshots not allowed");
4547 # default to first supported snapshot format
4548 $format ||= $supported_fmts[0];
4549 if ($format !~ m/^[a-z0-9]+$/) {
4550 die_error
(400, "Invalid snapshot format parameter");
4551 } elsif (!exists($known_snapshot_formats{$format})) {
4552 die_error
(400, "Unknown snapshot format");
4553 } elsif (!grep($_ eq $format, @supported_fmts)) {
4554 die_error
(403, "Unsupported snapshot format");
4557 if (!defined $hash) {
4558 $hash = git_get_head_hash
($project);
4561 my $name = $project;
4562 $name =~ s
,([^/])/*\
.git
$,$1,;
4563 $name = basename
($name);
4564 my $filename = to_utf8
($name);
4565 $name =~ s/\047/\047\\\047\047/g;
4567 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4568 $cmd = quote_command
(
4569 git_cmd
(), 'archive',
4570 "--format=$known_snapshot_formats{$format}{'format'}",
4571 "--prefix=$name/", $hash);
4572 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4573 $cmd .= ' | ' . quote_command
(@{$known_snapshot_formats{$format}{'compressor'}});
4577 -type
=> $known_snapshot_formats{$format}{'type'},
4578 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4579 -status
=> '200 OK');
4581 open my $fd, "-|", $cmd
4582 or die_error
(500, "Execute git-archive failed");
4583 binmode STDOUT
, ':raw';
4585 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4590 my $head = git_get_head_hash
($project);
4591 if (!defined $hash) {
4594 if (!defined $page) {
4597 my $refs = git_get_references
();
4599 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4601 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4604 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4607 my %co = parse_commit
($hash);
4609 git_print_header_div
('summary', $project);
4610 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4612 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4613 for (my $i = 0; $i <= $to; $i++) {
4614 my %co = %{$commitlist[$i]};
4616 my $commit = $co{'id'};
4617 my $ref = format_ref_marker
($refs, $commit);
4618 my %ad = parse_date
($co{'author_epoch'});
4619 git_print_header_div
('commit',
4620 "<span class=\"age\">$co{'age_string'}</span>" .
4621 esc_html
($co{'title'}) . $ref,
4623 print "<div class=\"title_text\">\n" .
4624 "<div class=\"log_link\">\n" .
4625 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4627 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4629 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4632 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4635 print "<div class=\"log_body\">\n";
4636 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4639 if ($#commitlist >= 100) {
4640 print "<div class=\"page_nav\">\n";
4641 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4642 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4649 $hash ||= $hash_base || "HEAD";
4650 my %co = parse_commit
($hash)
4651 or die_error
(404, "Unknown commit object");
4652 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4653 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4655 my $parent = $co{'parent'};
4656 my $parents = $co{'parents'}; # listref
4658 # we need to prepare $formats_nav before any parameter munging
4660 if (!defined $parent) {
4662 $formats_nav .= '(initial)';
4663 } elsif (@$parents == 1) {
4664 # single parent commit
4667 $cgi->a({-href
=> href
(action
=>"commit",
4669 esc_html
(substr($parent, 0, 7))) .
4676 $cgi->a({-href
=> href
(action
=>"commit",
4678 esc_html
(substr($_, 0, 7)));
4683 if (!defined $parent) {
4687 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4689 (@$parents <= 1 ? $parent : '-c'),
4691 or die_error
(500, "Open git-diff-tree failed");
4692 @difftree = map { chomp; $_ } <$fd>;
4693 close $fd or die_error
(404, "Reading git-diff-tree failed");
4695 # non-textual hash id's can be cached
4697 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4700 my $refs = git_get_references
();
4701 my $ref = format_ref_marker
($refs, $co{'id'});
4703 git_header_html
(undef, $expires);
4704 git_print_page_nav
('commit', '',
4705 $hash, $co{'tree'}, $hash,
4708 if (defined $co{'parent'}) {
4709 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4711 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4713 print "<div class=\"title_text\">\n" .
4714 "<table class=\"object_header\">\n";
4715 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4717 "<td></td><td> $ad{'rfc2822'}";
4718 if ($ad{'hour_local'} < 6) {
4719 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4720 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4722 printf(" (%02d:%02d %s)",
4723 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4727 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4728 print "<tr><td></td><td> $cd{'rfc2822'}" .
4729 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4731 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4734 "<td class=\"sha1\">" .
4735 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4736 class => "list"}, $co{'tree'}) .
4738 "<td class=\"link\">" .
4739 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4741 my $snapshot_links = format_snapshot_links
($hash);
4742 if (defined $snapshot_links) {
4743 print " | " . $snapshot_links;
4748 foreach my $par (@$parents) {
4751 "<td class=\"sha1\">" .
4752 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4753 class => "list"}, $par) .
4755 "<td class=\"link\">" .
4756 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4758 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
4765 print "<div class=\"page_body\">\n";
4766 git_print_log
($co{'comment'});
4769 git_difftree_body
(\
@difftree, $hash, @$parents);
4775 # object is defined by:
4776 # - hash or hash_base alone
4777 # - hash_base and file_name
4780 # - hash or hash_base alone
4781 if ($hash || ($hash_base && !defined $file_name)) {
4782 my $object_id = $hash || $hash_base;
4784 open my $fd, "-|", quote_command
(
4785 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4786 or die_error
(404, "Object does not exist");
4790 or die_error
(404, "Object does not exist");
4792 # - hash_base and file_name
4793 } elsif ($hash_base && defined $file_name) {
4794 $file_name =~ s
,/+$,,;
4796 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
4797 or die_error
(404, "Base object does not exist");
4799 # here errors should not hapen
4800 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
4801 or die_error
(500, "Open git-ls-tree failed");
4805 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4806 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4807 die_error
(404, "File or directory for given base does not exist");
4812 die_error
(400, "Not enough information to find object");
4815 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
4816 hash
=>$hash, hash_base
=>$hash_base,
4817 file_name
=>$file_name),
4818 -status
=> '302 Found');
4822 my $format = shift || 'html';
4829 # preparing $fd and %diffinfo for git_patchset_body
4831 if (defined $hash_base && defined $hash_parent_base) {
4832 if (defined $file_name) {
4834 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4835 $hash_parent_base, $hash_base,
4836 "--", (defined $file_parent ? $file_parent : ()), $file_name
4837 or die_error
(500, "Open git-diff-tree failed");
4838 @difftree = map { chomp; $_ } <$fd>;
4840 or die_error
(404, "Reading git-diff-tree failed");
4842 or die_error
(404, "Blob diff not found");
4844 } elsif (defined $hash &&
4845 $hash =~ /[0-9a-fA-F]{40}/) {
4846 # try to find filename from $hash
4848 # read filtered raw output
4849 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4850 $hash_parent_base, $hash_base, "--"
4851 or die_error
(500, "Open git-diff-tree failed");
4853 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4855 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4856 map { chomp; $_ } <$fd>;
4858 or die_error
(404, "Reading git-diff-tree failed");
4860 or die_error
(404, "Blob diff not found");
4863 die_error
(400, "Missing one of the blob diff parameters");
4866 if (@difftree > 1) {
4867 die_error
(400, "Ambiguous blob diff specification");
4870 %diffinfo = parse_difftree_raw_line
($difftree[0]);
4871 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4872 $file_name ||= $diffinfo{'to_file'};
4874 $hash_parent ||= $diffinfo{'from_id'};
4875 $hash ||= $diffinfo{'to_id'};
4877 # non-textual hash id's can be cached
4878 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4879 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4884 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4885 '-p', ($format eq 'html' ? "--full-index" : ()),
4886 $hash_parent_base, $hash_base,
4887 "--", (defined $file_parent ? $file_parent : ()), $file_name
4888 or die_error
(500, "Open git-diff-tree failed");
4891 # old/legacy style URI
4892 if (!%diffinfo && # if new style URI failed
4893 defined $hash && defined $hash_parent) {
4894 # fake git-diff-tree raw output
4895 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4896 $diffinfo{'from_id'} = $hash_parent;
4897 $diffinfo{'to_id'} = $hash;
4898 if (defined $file_name) {
4899 if (defined $file_parent) {
4900 $diffinfo{'status'} = '2';
4901 $diffinfo{'from_file'} = $file_parent;
4902 $diffinfo{'to_file'} = $file_name;
4903 } else { # assume not renamed
4904 $diffinfo{'status'} = '1';
4905 $diffinfo{'from_file'} = $file_name;
4906 $diffinfo{'to_file'} = $file_name;
4908 } else { # no filename given
4909 $diffinfo{'status'} = '2';
4910 $diffinfo{'from_file'} = $hash_parent;
4911 $diffinfo{'to_file'} = $hash;
4914 # non-textual hash id's can be cached
4915 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4916 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4921 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
4922 '-p', ($format eq 'html' ? "--full-index" : ()),
4923 $hash_parent, $hash, "--"
4924 or die_error
(500, "Open git-diff failed");
4926 die_error
(400, "Missing one of the blob diff parameters")
4931 if ($format eq 'html') {
4933 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
4935 git_header_html
(undef, $expires);
4936 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4937 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4938 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4940 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4941 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4943 if (defined $file_name) {
4944 git_print_page_path
($file_name, "blob", $hash_base);
4946 print "<div class=\"page_path\"></div>\n";
4949 } elsif ($format eq 'plain') {
4951 -type
=> 'text/plain',
4952 -charset
=> 'utf-8',
4953 -expires
=> $expires,
4954 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
4956 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4959 die_error
(400, "Unknown blobdiff format");
4963 if ($format eq 'html') {
4964 print "<div class=\"page_body\">\n";
4966 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
4969 print "</div>\n"; # class="page_body"
4973 while (my $line = <$fd>) {
4974 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
4975 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
4979 last if $line =~ m!^\+\+\+!;
4987 sub git_blobdiff_plain
{
4988 git_blobdiff
('plain');
4991 sub git_commitdiff
{
4992 my $format = shift || 'html';
4993 $hash ||= $hash_base || "HEAD";
4994 my %co = parse_commit
($hash)
4995 or die_error
(404, "Unknown commit object");
4997 # choose format for commitdiff for merge
4998 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
4999 $hash_parent = '--cc';
5001 # we need to prepare $formats_nav before almost any parameter munging
5003 if ($format eq 'html') {
5005 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5008 if (defined $hash_parent &&
5009 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5010 # commitdiff with two commits given
5011 my $hash_parent_short = $hash_parent;
5012 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5013 $hash_parent_short = substr($hash_parent, 0, 7);
5017 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5018 if ($co{'parents'}[$i] eq $hash_parent) {
5019 $formats_nav .= ' parent ' . ($i+1);
5023 $formats_nav .= ': ' .
5024 $cgi->a({-href
=> href
(action
=>"commitdiff",
5025 hash
=>$hash_parent)},
5026 esc_html
($hash_parent_short)) .
5028 } elsif (!$co{'parent'}) {
5030 $formats_nav .= ' (initial)';
5031 } elsif (scalar @{$co{'parents'}} == 1) {
5032 # single parent commit
5035 $cgi->a({-href
=> href
(action
=>"commitdiff",
5036 hash
=>$co{'parent'})},
5037 esc_html
(substr($co{'parent'}, 0, 7))) .
5041 if ($hash_parent eq '--cc') {
5042 $formats_nav .= ' | ' .
5043 $cgi->a({-href
=> href
(action
=>"commitdiff",
5044 hash
=>$hash, hash_parent
=>'-c')},
5046 } else { # $hash_parent eq '-c'
5047 $formats_nav .= ' | ' .
5048 $cgi->a({-href
=> href
(action
=>"commitdiff",
5049 hash
=>$hash, hash_parent
=>'--cc')},
5055 $cgi->a({-href
=> href
(action
=>"commitdiff",
5057 esc_html
(substr($_, 0, 7)));
5058 } @{$co{'parents'}} ) .
5063 my $hash_parent_param = $hash_parent;
5064 if (!defined $hash_parent_param) {
5065 # --cc for multiple parents, --root for parentless
5066 $hash_parent_param =
5067 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5073 if ($format eq 'html') {
5074 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5075 "--no-commit-id", "--patch-with-raw", "--full-index",
5076 $hash_parent_param, $hash, "--"
5077 or die_error
(500, "Open git-diff-tree failed");
5079 while (my $line = <$fd>) {
5081 # empty line ends raw part of diff-tree output
5083 push @difftree, scalar parse_difftree_raw_line
($line);
5086 } elsif ($format eq 'plain') {
5087 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5088 '-p', $hash_parent_param, $hash, "--"
5089 or die_error
(500, "Open git-diff-tree failed");
5092 die_error
(400, "Unknown commitdiff format");
5095 # non-textual hash id's can be cached
5097 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5101 # write commit message
5102 if ($format eq 'html') {
5103 my $refs = git_get_references
();
5104 my $ref = format_ref_marker
($refs, $co{'id'});
5106 git_header_html
(undef, $expires);
5107 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5108 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5109 git_print_authorship
(\
%co);
5110 print "<div class=\"page_body\">\n";
5111 if (@{$co{'comment'}} > 1) {
5112 print "<div class=\"log\">\n";
5113 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5114 print "</div>\n"; # class="log"
5117 } elsif ($format eq 'plain') {
5118 my $refs = git_get_references
("tags");
5119 my $tagname = git_get_rev_name_tags
($hash);
5120 my $filename = basename
($project) . "-$hash.patch";
5123 -type
=> 'text/plain',
5124 -charset
=> 'utf-8',
5125 -expires
=> $expires,
5126 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5127 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5128 print "From: " . to_utf8
($co{'author'}) . "\n";
5129 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5130 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5132 print "X-Git-Tag: $tagname\n" if $tagname;
5133 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5135 foreach my $line (@{$co{'comment'}}) {
5136 print to_utf8
($line) . "\n";
5142 if ($format eq 'html') {
5143 my $use_parents = !defined $hash_parent ||
5144 $hash_parent eq '-c' || $hash_parent eq '--cc';
5145 git_difftree_body
(\
@difftree, $hash,
5146 $use_parents ? @{$co{'parents'}} : $hash_parent);
5149 git_patchset_body
($fd, \
@difftree, $hash,
5150 $use_parents ? @{$co{'parents'}} : $hash_parent);
5152 print "</div>\n"; # class="page_body"
5155 } elsif ($format eq 'plain') {
5159 or print "Reading git-diff-tree failed\n";
5163 sub git_commitdiff_plain
{
5164 git_commitdiff
('plain');
5168 if (!defined $hash_base) {
5169 $hash_base = git_get_head_hash
($project);
5171 if (!defined $page) {
5175 my %co = parse_commit
($hash_base)
5176 or die_error
(404, "Unknown commit object");
5178 my $refs = git_get_references
();
5179 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5181 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5182 $file_name, "--full-history")
5183 or die_error
(404, "No such file or directory on given branch");
5185 if (!defined $hash && defined $file_name) {
5186 # some commits could have deleted file in question,
5187 # and not have it in tree, but one of them has to have it
5188 for (my $i = 0; $i <= @commitlist; $i++) {
5189 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5190 last if defined $hash;
5193 if (defined $hash) {
5194 $ftype = git_get_type
($hash);
5196 if (!defined $ftype) {
5197 die_error
(500, "Unknown type of object");
5200 my $paging_nav = '';
5203 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5204 file_name
=>$file_name)},
5206 $paging_nav .= " ⋅ " .
5207 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5208 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5210 $paging_nav .= "first";
5211 $paging_nav .= " ⋅ prev";
5214 if ($#commitlist >= 100) {
5216 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5217 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5218 $paging_nav .= " ⋅ $next_link";
5220 $paging_nav .= " ⋅ next";
5224 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5225 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5226 git_print_page_path
($file_name, $ftype, $hash_base);
5228 git_history_body
(\
@commitlist, 0, 99,
5229 $refs, $hash_base, $ftype, $next_link);
5235 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5236 if (!defined $searchtext) {
5237 die_error
(400, "Text field is empty");
5239 if (!defined $hash) {
5240 $hash = git_get_head_hash
($project);
5242 my %co = parse_commit
($hash);
5244 die_error
(404, "Unknown commit object");
5246 if (!defined $page) {
5250 $searchtype ||= 'commit';
5251 if ($searchtype eq 'pickaxe') {
5252 # pickaxe may take all resources of your box and run for several minutes
5253 # with every query - so decide by yourself how public you make this feature
5254 gitweb_check_feature
('pickaxe')
5255 or die_error
(403, "Pickaxe is disabled");
5257 if ($searchtype eq 'grep') {
5258 gitweb_check_feature
('grep')
5259 or die_error
(403, "Grep is disabled");
5264 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5266 if ($searchtype eq 'commit') {
5267 $greptype = "--grep=";
5268 } elsif ($searchtype eq 'author') {
5269 $greptype = "--author=";
5270 } elsif ($searchtype eq 'committer') {
5271 $greptype = "--committer=";
5273 $greptype .= $searchtext;
5274 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5275 $greptype, '--regexp-ignore-case',
5276 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5278 my $paging_nav = '';
5281 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5282 searchtext
=>$searchtext,
5283 searchtype
=>$searchtype)},
5285 $paging_nav .= " ⋅ " .
5286 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5287 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5289 $paging_nav .= "first";
5290 $paging_nav .= " ⋅ prev";
5293 if ($#commitlist >= 100) {
5295 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5296 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5297 $paging_nav .= " ⋅ $next_link";
5299 $paging_nav .= " ⋅ next";
5302 if ($#commitlist >= 100) {
5305 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5306 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5307 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5310 if ($searchtype eq 'pickaxe') {
5311 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5312 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5314 print "<table class=\"pickaxe search\">\n";
5317 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5318 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5319 ($search_use_regexp ? '--pickaxe-regex' : ());
5322 while (my $line = <$fd>) {
5326 my %set = parse_difftree_raw_line
($line);
5327 if (defined $set{'commit'}) {
5328 # finish previous commit
5331 "<td class=\"link\">" .
5332 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5334 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5340 print "<tr class=\"dark\">\n";
5342 print "<tr class=\"light\">\n";
5345 %co = parse_commit
($set{'commit'});
5346 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5347 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5348 "<td><i>$author</i></td>\n" .
5350 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5351 -class => "list subject"},
5352 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5353 } elsif (defined $set{'to_id'}) {
5354 next if ($set{'to_id'} =~ m/^0{40}$/);
5356 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5357 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5359 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5365 # finish last commit (warning: repetition!)
5368 "<td class=\"link\">" .
5369 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5371 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5379 if ($searchtype eq 'grep') {
5380 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5381 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5383 print "<table class=\"grep_search\">\n";
5387 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5388 $search_use_regexp ? ('-E', '-i') : '-F',
5389 $searchtext, $co{'tree'};
5391 while (my $line = <$fd>) {
5393 my ($file, $lno, $ltext, $binary);
5394 last if ($matches++ > 1000);
5395 if ($line =~ /^Binary file (.+) matches$/) {
5399 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5401 if ($file ne $lastfile) {
5402 $lastfile and print "</td></tr>\n";
5404 print "<tr class=\"dark\">\n";
5406 print "<tr class=\"light\">\n";
5408 print "<td class=\"list\">".
5409 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5410 file_name
=>"$file"),
5411 -class => "list"}, esc_path
($file));
5412 print "</td><td>\n";
5416 print "<div class=\"binary\">Binary file</div>\n";
5418 $ltext = untabify
($ltext);
5419 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5420 $ltext = esc_html
($1, -nbsp
=>1);
5421 $ltext .= '<span class="match">';
5422 $ltext .= esc_html
($2, -nbsp
=>1);
5423 $ltext .= '</span>';
5424 $ltext .= esc_html
($3, -nbsp
=>1);
5426 $ltext = esc_html
($ltext, -nbsp
=>1);
5428 print "<div class=\"pre\">" .
5429 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5430 file_name
=>"$file").'#l'.$lno,
5431 -class => "linenr"}, sprintf('%4i', $lno))
5432 . ' ' . $ltext . "</div>\n";
5436 print "</td></tr>\n";
5437 if ($matches > 1000) {
5438 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5441 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5450 sub git_search_help
{
5452 git_print_page_nav
('','', $hash,$hash,$hash);
5454 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5455 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5456 the pattern entered is recognized as the POSIX extended
5457 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5460 <dt><b>commit</b></dt>
5461 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5463 my ($have_grep) = gitweb_check_feature
('grep');
5466 <dt><b>grep</b></dt>
5467 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5468 a different one) are searched for the given pattern. On large trees, this search can take
5469 a while and put some strain on the server, so please use it with some consideration. Note that
5470 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5471 case-sensitive.</dd>
5475 <dt><b>author</b></dt>
5476 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5477 <dt><b>committer</b></dt>
5478 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5480 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5481 if ($have_pickaxe) {
5483 <dt><b>pickaxe</b></dt>
5484 <dd>All commits that caused the string to appear or disappear from any file (changes that
5485 added, removed or "modified" the string) will be listed. This search can take a while and
5486 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5487 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5495 my $head = git_get_head_hash
($project);
5496 if (!defined $hash) {
5499 if (!defined $page) {
5502 my $refs = git_get_references
();
5504 my $commit_hash = $hash;
5505 if (defined $hash_parent) {
5506 $commit_hash = "$hash_parent..$hash";
5508 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5510 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5512 if ($#commitlist >= 100) {
5514 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5515 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5519 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5520 git_print_header_div
('summary', $project);
5522 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5527 ## ......................................................................
5528 ## feeds (RSS, Atom; OPML)
5531 my $format = shift || 'atom';
5532 my ($have_blame) = gitweb_check_feature
('blame');
5534 # Atom: http://www.atomenabled.org/developers/syndication/
5535 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5536 if ($format ne 'rss' && $format ne 'atom') {
5537 die_error
(400, "Unknown web feed format");
5540 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5541 my $head = $hash || 'HEAD';
5542 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5546 my $content_type = "application/$format+xml";
5547 if (defined $cgi->http('HTTP_ACCEPT') &&
5548 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5549 # browser (feed reader) prefers text/xml
5550 $content_type = 'text/xml';
5552 if (defined($commitlist[0])) {
5553 %latest_commit = %{$commitlist[0]};
5554 %latest_date = parse_date
($latest_commit{'author_epoch'});
5556 -type
=> $content_type,
5557 -charset
=> 'utf-8',
5558 -last_modified
=> $latest_date{'rfc2822'});
5561 -type
=> $content_type,
5562 -charset
=> 'utf-8');
5565 # Optimization: skip generating the body if client asks only
5566 # for Last-Modified date.
5567 return if ($cgi->request_method() eq 'HEAD');
5570 my $title = "$site_name - $project/$action";
5571 my $feed_type = 'log';
5572 if (defined $hash) {
5573 $title .= " - '$hash'";
5574 $feed_type = 'branch log';
5575 if (defined $file_name) {
5576 $title .= " :: $file_name";
5577 $feed_type = 'history';
5579 } elsif (defined $file_name) {
5580 $title .= " - $file_name";
5581 $feed_type = 'history';
5583 $title .= " $feed_type";
5584 my $descr = git_get_project_description
($project);
5585 if (defined $descr) {
5586 $descr = esc_html
($descr);
5588 $descr = "$project " .
5589 ($format eq 'rss' ? 'RSS' : 'Atom') .
5592 my $owner = git_get_project_owner
($project);
5593 $owner = esc_html
($owner);
5597 if (defined $file_name) {
5598 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5599 } elsif (defined $hash) {
5600 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5602 $alt_url = href
(-full
=>1, action
=>"summary");
5604 print qq
!<?xml version
="1.0" encoding
="utf-8"?>\n!;
5605 if ($format eq 'rss') {
5607 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5610 print "<title>$title</title>\n" .
5611 "<link>$alt_url</link>\n" .
5612 "<description>$descr</description>\n" .
5613 "<language>en</language>\n";
5614 } elsif ($format eq 'atom') {
5616 <feed xmlns="http://www.w3.org/2005/Atom">
5618 print "<title>$title</title>\n" .
5619 "<subtitle>$descr</subtitle>\n" .
5620 '<link rel="alternate" type="text/html" href="' .
5621 $alt_url . '" />' . "\n" .
5622 '<link rel="self" type="' . $content_type . '" href="' .
5623 $cgi->self_url() . '" />' . "\n" .
5624 "<id>" . href
(-full
=>1) . "</id>\n" .
5625 # use project owner for feed author
5626 "<author><name>$owner</name></author>\n";
5627 if (defined $favicon) {
5628 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5630 if (defined $logo_url) {
5631 # not twice as wide as tall: 72 x 27 pixels
5632 print "<logo>" . esc_url
($logo) . "</logo>\n";
5634 if (! %latest_date) {
5635 # dummy date to keep the feed valid until commits trickle in:
5636 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5638 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5643 for (my $i = 0; $i <= $#commitlist; $i++) {
5644 my %co = %{$commitlist[$i]};
5645 my $commit = $co{'id'};
5646 # we read 150, we always show 30 and the ones more recent than 48 hours
5647 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5650 my %cd = parse_date
($co{'author_epoch'});
5652 # get list of changed files
5653 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5654 $co{'parent'} || "--root",
5655 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5657 my @difftree = map { chomp; $_ } <$fd>;
5661 # print element (entry, item)
5662 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5663 if ($format eq 'rss') {
5665 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5666 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5667 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5668 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5669 "<link>$co_url</link>\n" .
5670 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5671 "<content:encoded>" .
5673 } elsif ($format eq 'atom') {
5675 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5676 "<updated>$cd{'iso-8601'}</updated>\n" .
5678 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5679 if ($co{'author_email'}) {
5680 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5682 print "</author>\n" .
5683 # use committer for contributor
5685 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5686 if ($co{'committer_email'}) {
5687 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5689 print "</contributor>\n" .
5690 "<published>$cd{'iso-8601'}</published>\n" .
5691 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5692 "<id>$co_url</id>\n" .
5693 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5694 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5696 my $comment = $co{'comment'};
5698 foreach my $line (@$comment) {
5699 $line = esc_html
($line);
5702 print "</pre><ul>\n";
5703 foreach my $difftree_line (@difftree) {
5704 my %difftree = parse_difftree_raw_line
($difftree_line);
5705 next if !$difftree{'from_id'};
5707 my $file = $difftree{'file'} || $difftree{'to_file'};
5711 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5712 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5713 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5714 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5715 -title
=> "diff"}, 'D');
5717 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5718 file_name
=>$file, hash_base
=>$commit),
5719 -title
=> "blame"}, 'B');
5721 # if this is not a feed of a file history
5722 if (!defined $file_name || $file_name ne $file) {
5723 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5724 file_name
=>$file, hash
=>$commit),
5725 -title
=> "history"}, 'H');
5727 $file = esc_path
($file);
5731 if ($format eq 'rss') {
5732 print "</ul>]]>\n" .
5733 "</content:encoded>\n" .
5735 } elsif ($format eq 'atom') {
5736 print "</ul>\n</div>\n" .
5743 if ($format eq 'rss') {
5744 print "</channel>\n</rss>\n";
5745 } elsif ($format eq 'atom') {
5759 my @list = git_get_projects_list
();
5761 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
5763 <?xml version="1.0" encoding="utf-8"?>
5764 <opml version="1.0">
5766 <title>$site_name OPML Export</title>
5769 <outline text="git RSS feeds">
5772 foreach my $pr (@list) {
5774 my $head = git_get_head_hash
($proj{'path'});
5775 if (!defined $head) {
5778 $git_dir = "$projectroot/$proj{'path'}";
5779 my %co = parse_commit
($head);
5784 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
5785 my $rss = "$my_url?p=$proj{'path'};a=rss";
5786 my $html = "$my_url?p=$proj{'path'};a=summary";
5787 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";