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 id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4077 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4078 if (defined $cd{'rfc2822'}) {
4079 print "<tr id=\"metadata_lchange\"><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 class=\"metadata_url\"><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");
4451 die_error
(404, "No such tree") unless defined($hash);
4453 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4454 or die_error
(500, "Open git-ls-tree failed");
4455 my @entries = map { chomp; $_ } <$fd>;
4456 close $fd or die_error
(404, "Reading tree failed");
4459 my $refs = git_get_references
();
4460 my $ref = format_ref_marker
($refs, $hash_base);
4463 my ($have_blame) = gitweb_check_feature
('blame');
4464 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4466 if (defined $file_name) {
4468 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4470 $cgi->a({-href
=> href
(action
=>"tree",
4471 hash_base
=>"HEAD", file_name
=>$file_name)},
4474 my $snapshot_links = format_snapshot_links
($hash);
4475 if (defined $snapshot_links) {
4476 # FIXME: Should be available when we have no hash base as well.
4477 push @views_nav, $snapshot_links;
4479 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4480 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4483 print "<div class=\"page_nav\">\n";
4484 print "<br/><br/></div>\n";
4485 print "<div class=\"title\">$hash</div>\n";
4487 if (defined $file_name) {
4488 $basedir = $file_name;
4489 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4492 git_print_page_path
($file_name, 'tree', $hash_base);
4494 print "<div class=\"page_body\">\n";
4495 print "<table class=\"tree\">\n";
4497 # '..' (top directory) link if possible
4498 if (defined $hash_base &&
4499 defined $file_name && $file_name =~ m![^/]+$!) {
4501 print "<tr class=\"dark\">\n";
4503 print "<tr class=\"light\">\n";
4507 my $up = $file_name;
4508 $up =~ s!/?[^/]+$!!;
4509 undef $up unless $up;
4510 # based on git_print_tree_entry
4511 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4512 print '<td class="list">';
4513 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4517 print "<td class=\"link\"></td>\n";
4521 foreach my $line (@entries) {
4522 my %t = parse_ls_tree_line
($line, -z
=> 1);
4525 print "<tr class=\"dark\">\n";
4527 print "<tr class=\"light\">\n";
4531 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4535 print "</table>\n" .
4541 my @supported_fmts = gitweb_check_feature
('snapshot');
4542 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4544 my $format = $cgi->param('sf');
4545 if (!@supported_fmts) {
4546 die_error
(403, "Snapshots not allowed");
4548 # default to first supported snapshot format
4549 $format ||= $supported_fmts[0];
4550 if ($format !~ m/^[a-z0-9]+$/) {
4551 die_error
(400, "Invalid snapshot format parameter");
4552 } elsif (!exists($known_snapshot_formats{$format})) {
4553 die_error
(400, "Unknown snapshot format");
4554 } elsif (!grep($_ eq $format, @supported_fmts)) {
4555 die_error
(403, "Unsupported snapshot format");
4558 if (!defined $hash) {
4559 $hash = git_get_head_hash
($project);
4562 my $name = $project;
4563 $name =~ s
,([^/])/*\
.git
$,$1,;
4564 $name = basename
($name);
4565 my $filename = to_utf8
($name);
4566 $name =~ s/\047/\047\\\047\047/g;
4568 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4569 $cmd = quote_command
(
4570 git_cmd
(), 'archive',
4571 "--format=$known_snapshot_formats{$format}{'format'}",
4572 "--prefix=$name/", $hash);
4573 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4574 $cmd .= ' | ' . quote_command
(@{$known_snapshot_formats{$format}{'compressor'}});
4578 -type
=> $known_snapshot_formats{$format}{'type'},
4579 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4580 -status
=> '200 OK');
4582 open my $fd, "-|", $cmd
4583 or die_error
(500, "Execute git-archive failed");
4584 binmode STDOUT
, ':raw';
4586 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4591 my $head = git_get_head_hash
($project);
4592 if (!defined $hash) {
4595 if (!defined $page) {
4598 my $refs = git_get_references
();
4600 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4602 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4605 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4608 my %co = parse_commit
($hash);
4610 git_print_header_div
('summary', $project);
4611 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4613 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4614 for (my $i = 0; $i <= $to; $i++) {
4615 my %co = %{$commitlist[$i]};
4617 my $commit = $co{'id'};
4618 my $ref = format_ref_marker
($refs, $commit);
4619 my %ad = parse_date
($co{'author_epoch'});
4620 git_print_header_div
('commit',
4621 "<span class=\"age\">$co{'age_string'}</span>" .
4622 esc_html
($co{'title'}) . $ref,
4624 print "<div class=\"title_text\">\n" .
4625 "<div class=\"log_link\">\n" .
4626 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4628 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4630 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4633 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4636 print "<div class=\"log_body\">\n";
4637 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4640 if ($#commitlist >= 100) {
4641 print "<div class=\"page_nav\">\n";
4642 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4643 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4650 $hash ||= $hash_base || "HEAD";
4651 my %co = parse_commit
($hash)
4652 or die_error
(404, "Unknown commit object");
4653 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4654 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4656 my $parent = $co{'parent'};
4657 my $parents = $co{'parents'}; # listref
4659 # we need to prepare $formats_nav before any parameter munging
4661 if (!defined $parent) {
4663 $formats_nav .= '(initial)';
4664 } elsif (@$parents == 1) {
4665 # single parent commit
4668 $cgi->a({-href
=> href
(action
=>"commit",
4670 esc_html
(substr($parent, 0, 7))) .
4677 $cgi->a({-href
=> href
(action
=>"commit",
4679 esc_html
(substr($_, 0, 7)));
4684 if (!defined $parent) {
4688 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4690 (@$parents <= 1 ? $parent : '-c'),
4692 or die_error
(500, "Open git-diff-tree failed");
4693 @difftree = map { chomp; $_ } <$fd>;
4694 close $fd or die_error
(404, "Reading git-diff-tree failed");
4696 # non-textual hash id's can be cached
4698 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4701 my $refs = git_get_references
();
4702 my $ref = format_ref_marker
($refs, $co{'id'});
4704 git_header_html
(undef, $expires);
4705 git_print_page_nav
('commit', '',
4706 $hash, $co{'tree'}, $hash,
4709 if (defined $co{'parent'}) {
4710 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4712 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4714 print "<div class=\"title_text\">\n" .
4715 "<table class=\"object_header\">\n";
4716 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4718 "<td></td><td> $ad{'rfc2822'}";
4719 if ($ad{'hour_local'} < 6) {
4720 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4721 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4723 printf(" (%02d:%02d %s)",
4724 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4728 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4729 print "<tr><td></td><td> $cd{'rfc2822'}" .
4730 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4732 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4735 "<td class=\"sha1\">" .
4736 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4737 class => "list"}, $co{'tree'}) .
4739 "<td class=\"link\">" .
4740 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4742 my $snapshot_links = format_snapshot_links
($hash);
4743 if (defined $snapshot_links) {
4744 print " | " . $snapshot_links;
4749 foreach my $par (@$parents) {
4752 "<td class=\"sha1\">" .
4753 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4754 class => "list"}, $par) .
4756 "<td class=\"link\">" .
4757 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4759 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
4766 print "<div class=\"page_body\">\n";
4767 git_print_log
($co{'comment'});
4770 git_difftree_body
(\
@difftree, $hash, @$parents);
4776 # object is defined by:
4777 # - hash or hash_base alone
4778 # - hash_base and file_name
4781 # - hash or hash_base alone
4782 if ($hash || ($hash_base && !defined $file_name)) {
4783 my $object_id = $hash || $hash_base;
4785 open my $fd, "-|", quote_command
(
4786 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4787 or die_error
(404, "Object does not exist");
4791 or die_error
(404, "Object does not exist");
4793 # - hash_base and file_name
4794 } elsif ($hash_base && defined $file_name) {
4795 $file_name =~ s
,/+$,,;
4797 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
4798 or die_error
(404, "Base object does not exist");
4800 # here errors should not hapen
4801 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
4802 or die_error
(500, "Open git-ls-tree failed");
4806 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4807 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4808 die_error
(404, "File or directory for given base does not exist");
4813 die_error
(400, "Not enough information to find object");
4816 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
4817 hash
=>$hash, hash_base
=>$hash_base,
4818 file_name
=>$file_name),
4819 -status
=> '302 Found');
4823 my $format = shift || 'html';
4830 # preparing $fd and %diffinfo for git_patchset_body
4832 if (defined $hash_base && defined $hash_parent_base) {
4833 if (defined $file_name) {
4835 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4836 $hash_parent_base, $hash_base,
4837 "--", (defined $file_parent ? $file_parent : ()), $file_name
4838 or die_error
(500, "Open git-diff-tree failed");
4839 @difftree = map { chomp; $_ } <$fd>;
4841 or die_error
(404, "Reading git-diff-tree failed");
4843 or die_error
(404, "Blob diff not found");
4845 } elsif (defined $hash &&
4846 $hash =~ /[0-9a-fA-F]{40}/) {
4847 # try to find filename from $hash
4849 # read filtered raw output
4850 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4851 $hash_parent_base, $hash_base, "--"
4852 or die_error
(500, "Open git-diff-tree failed");
4854 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4856 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4857 map { chomp; $_ } <$fd>;
4859 or die_error
(404, "Reading git-diff-tree failed");
4861 or die_error
(404, "Blob diff not found");
4864 die_error
(400, "Missing one of the blob diff parameters");
4867 if (@difftree > 1) {
4868 die_error
(400, "Ambiguous blob diff specification");
4871 %diffinfo = parse_difftree_raw_line
($difftree[0]);
4872 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4873 $file_name ||= $diffinfo{'to_file'};
4875 $hash_parent ||= $diffinfo{'from_id'};
4876 $hash ||= $diffinfo{'to_id'};
4878 # non-textual hash id's can be cached
4879 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4880 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4885 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4886 '-p', ($format eq 'html' ? "--full-index" : ()),
4887 $hash_parent_base, $hash_base,
4888 "--", (defined $file_parent ? $file_parent : ()), $file_name
4889 or die_error
(500, "Open git-diff-tree failed");
4892 # old/legacy style URI
4893 if (!%diffinfo && # if new style URI failed
4894 defined $hash && defined $hash_parent) {
4895 # fake git-diff-tree raw output
4896 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4897 $diffinfo{'from_id'} = $hash_parent;
4898 $diffinfo{'to_id'} = $hash;
4899 if (defined $file_name) {
4900 if (defined $file_parent) {
4901 $diffinfo{'status'} = '2';
4902 $diffinfo{'from_file'} = $file_parent;
4903 $diffinfo{'to_file'} = $file_name;
4904 } else { # assume not renamed
4905 $diffinfo{'status'} = '1';
4906 $diffinfo{'from_file'} = $file_name;
4907 $diffinfo{'to_file'} = $file_name;
4909 } else { # no filename given
4910 $diffinfo{'status'} = '2';
4911 $diffinfo{'from_file'} = $hash_parent;
4912 $diffinfo{'to_file'} = $hash;
4915 # non-textual hash id's can be cached
4916 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4917 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4922 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
4923 '-p', ($format eq 'html' ? "--full-index" : ()),
4924 $hash_parent, $hash, "--"
4925 or die_error
(500, "Open git-diff failed");
4927 die_error
(400, "Missing one of the blob diff parameters")
4932 if ($format eq 'html') {
4934 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
4936 git_header_html
(undef, $expires);
4937 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4938 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4939 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4941 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4942 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4944 if (defined $file_name) {
4945 git_print_page_path
($file_name, "blob", $hash_base);
4947 print "<div class=\"page_path\"></div>\n";
4950 } elsif ($format eq 'plain') {
4952 -type
=> 'text/plain',
4953 -charset
=> 'utf-8',
4954 -expires
=> $expires,
4955 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
4957 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4960 die_error
(400, "Unknown blobdiff format");
4964 if ($format eq 'html') {
4965 print "<div class=\"page_body\">\n";
4967 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
4970 print "</div>\n"; # class="page_body"
4974 while (my $line = <$fd>) {
4975 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
4976 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
4980 last if $line =~ m!^\+\+\+!;
4988 sub git_blobdiff_plain
{
4989 git_blobdiff
('plain');
4992 sub git_commitdiff
{
4993 my $format = shift || 'html';
4994 $hash ||= $hash_base || "HEAD";
4995 my %co = parse_commit
($hash)
4996 or die_error
(404, "Unknown commit object");
4998 # choose format for commitdiff for merge
4999 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5000 $hash_parent = '--cc';
5002 # we need to prepare $formats_nav before almost any parameter munging
5004 if ($format eq 'html') {
5006 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5009 if (defined $hash_parent &&
5010 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5011 # commitdiff with two commits given
5012 my $hash_parent_short = $hash_parent;
5013 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5014 $hash_parent_short = substr($hash_parent, 0, 7);
5018 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5019 if ($co{'parents'}[$i] eq $hash_parent) {
5020 $formats_nav .= ' parent ' . ($i+1);
5024 $formats_nav .= ': ' .
5025 $cgi->a({-href
=> href
(action
=>"commitdiff",
5026 hash
=>$hash_parent)},
5027 esc_html
($hash_parent_short)) .
5029 } elsif (!$co{'parent'}) {
5031 $formats_nav .= ' (initial)';
5032 } elsif (scalar @{$co{'parents'}} == 1) {
5033 # single parent commit
5036 $cgi->a({-href
=> href
(action
=>"commitdiff",
5037 hash
=>$co{'parent'})},
5038 esc_html
(substr($co{'parent'}, 0, 7))) .
5042 if ($hash_parent eq '--cc') {
5043 $formats_nav .= ' | ' .
5044 $cgi->a({-href
=> href
(action
=>"commitdiff",
5045 hash
=>$hash, hash_parent
=>'-c')},
5047 } else { # $hash_parent eq '-c'
5048 $formats_nav .= ' | ' .
5049 $cgi->a({-href
=> href
(action
=>"commitdiff",
5050 hash
=>$hash, hash_parent
=>'--cc')},
5056 $cgi->a({-href
=> href
(action
=>"commitdiff",
5058 esc_html
(substr($_, 0, 7)));
5059 } @{$co{'parents'}} ) .
5064 my $hash_parent_param = $hash_parent;
5065 if (!defined $hash_parent_param) {
5066 # --cc for multiple parents, --root for parentless
5067 $hash_parent_param =
5068 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5074 if ($format eq 'html') {
5075 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5076 "--no-commit-id", "--patch-with-raw", "--full-index",
5077 $hash_parent_param, $hash, "--"
5078 or die_error
(500, "Open git-diff-tree failed");
5080 while (my $line = <$fd>) {
5082 # empty line ends raw part of diff-tree output
5084 push @difftree, scalar parse_difftree_raw_line
($line);
5087 } elsif ($format eq 'plain') {
5088 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5089 '-p', $hash_parent_param, $hash, "--"
5090 or die_error
(500, "Open git-diff-tree failed");
5093 die_error
(400, "Unknown commitdiff format");
5096 # non-textual hash id's can be cached
5098 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5102 # write commit message
5103 if ($format eq 'html') {
5104 my $refs = git_get_references
();
5105 my $ref = format_ref_marker
($refs, $co{'id'});
5107 git_header_html
(undef, $expires);
5108 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5109 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5110 git_print_authorship
(\
%co);
5111 print "<div class=\"page_body\">\n";
5112 if (@{$co{'comment'}} > 1) {
5113 print "<div class=\"log\">\n";
5114 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5115 print "</div>\n"; # class="log"
5118 } elsif ($format eq 'plain') {
5119 my $refs = git_get_references
("tags");
5120 my $tagname = git_get_rev_name_tags
($hash);
5121 my $filename = basename
($project) . "-$hash.patch";
5124 -type
=> 'text/plain',
5125 -charset
=> 'utf-8',
5126 -expires
=> $expires,
5127 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5128 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5129 print "From: " . to_utf8
($co{'author'}) . "\n";
5130 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5131 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5133 print "X-Git-Tag: $tagname\n" if $tagname;
5134 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5136 foreach my $line (@{$co{'comment'}}) {
5137 print to_utf8
($line) . "\n";
5143 if ($format eq 'html') {
5144 my $use_parents = !defined $hash_parent ||
5145 $hash_parent eq '-c' || $hash_parent eq '--cc';
5146 git_difftree_body
(\
@difftree, $hash,
5147 $use_parents ? @{$co{'parents'}} : $hash_parent);
5150 git_patchset_body
($fd, \
@difftree, $hash,
5151 $use_parents ? @{$co{'parents'}} : $hash_parent);
5153 print "</div>\n"; # class="page_body"
5156 } elsif ($format eq 'plain') {
5160 or print "Reading git-diff-tree failed\n";
5164 sub git_commitdiff_plain
{
5165 git_commitdiff
('plain');
5169 if (!defined $hash_base) {
5170 $hash_base = git_get_head_hash
($project);
5172 if (!defined $page) {
5176 my %co = parse_commit
($hash_base)
5177 or die_error
(404, "Unknown commit object");
5179 my $refs = git_get_references
();
5180 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5182 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5183 $file_name, "--full-history")
5184 or die_error
(404, "No such file or directory on given branch");
5186 if (!defined $hash && defined $file_name) {
5187 # some commits could have deleted file in question,
5188 # and not have it in tree, but one of them has to have it
5189 for (my $i = 0; $i <= @commitlist; $i++) {
5190 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5191 last if defined $hash;
5194 if (defined $hash) {
5195 $ftype = git_get_type
($hash);
5197 if (!defined $ftype) {
5198 die_error
(500, "Unknown type of object");
5201 my $paging_nav = '';
5204 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5205 file_name
=>$file_name)},
5207 $paging_nav .= " ⋅ " .
5208 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5209 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5211 $paging_nav .= "first";
5212 $paging_nav .= " ⋅ prev";
5215 if ($#commitlist >= 100) {
5217 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5218 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5219 $paging_nav .= " ⋅ $next_link";
5221 $paging_nav .= " ⋅ next";
5225 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5226 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5227 git_print_page_path
($file_name, $ftype, $hash_base);
5229 git_history_body
(\
@commitlist, 0, 99,
5230 $refs, $hash_base, $ftype, $next_link);
5236 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5237 if (!defined $searchtext) {
5238 die_error
(400, "Text field is empty");
5240 if (!defined $hash) {
5241 $hash = git_get_head_hash
($project);
5243 my %co = parse_commit
($hash);
5245 die_error
(404, "Unknown commit object");
5247 if (!defined $page) {
5251 $searchtype ||= 'commit';
5252 if ($searchtype eq 'pickaxe') {
5253 # pickaxe may take all resources of your box and run for several minutes
5254 # with every query - so decide by yourself how public you make this feature
5255 gitweb_check_feature
('pickaxe')
5256 or die_error
(403, "Pickaxe is disabled");
5258 if ($searchtype eq 'grep') {
5259 gitweb_check_feature
('grep')
5260 or die_error
(403, "Grep is disabled");
5265 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5267 if ($searchtype eq 'commit') {
5268 $greptype = "--grep=";
5269 } elsif ($searchtype eq 'author') {
5270 $greptype = "--author=";
5271 } elsif ($searchtype eq 'committer') {
5272 $greptype = "--committer=";
5274 $greptype .= $searchtext;
5275 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5276 $greptype, '--regexp-ignore-case',
5277 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5279 my $paging_nav = '';
5282 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5283 searchtext
=>$searchtext,
5284 searchtype
=>$searchtype)},
5286 $paging_nav .= " ⋅ " .
5287 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5288 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5290 $paging_nav .= "first";
5291 $paging_nav .= " ⋅ prev";
5294 if ($#commitlist >= 100) {
5296 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5297 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5298 $paging_nav .= " ⋅ $next_link";
5300 $paging_nav .= " ⋅ next";
5303 if ($#commitlist >= 100) {
5306 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5307 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5308 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5311 if ($searchtype eq 'pickaxe') {
5312 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5313 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5315 print "<table class=\"pickaxe search\">\n";
5318 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5319 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5320 ($search_use_regexp ? '--pickaxe-regex' : ());
5323 while (my $line = <$fd>) {
5327 my %set = parse_difftree_raw_line
($line);
5328 if (defined $set{'commit'}) {
5329 # finish previous commit
5332 "<td class=\"link\">" .
5333 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5335 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5341 print "<tr class=\"dark\">\n";
5343 print "<tr class=\"light\">\n";
5346 %co = parse_commit
($set{'commit'});
5347 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5348 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5349 "<td><i>$author</i></td>\n" .
5351 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5352 -class => "list subject"},
5353 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5354 } elsif (defined $set{'to_id'}) {
5355 next if ($set{'to_id'} =~ m/^0{40}$/);
5357 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5358 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5360 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5366 # finish last commit (warning: repetition!)
5369 "<td class=\"link\">" .
5370 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5372 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5380 if ($searchtype eq 'grep') {
5381 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5382 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5384 print "<table class=\"grep_search\">\n";
5388 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5389 $search_use_regexp ? ('-E', '-i') : '-F',
5390 $searchtext, $co{'tree'};
5392 while (my $line = <$fd>) {
5394 my ($file, $lno, $ltext, $binary);
5395 last if ($matches++ > 1000);
5396 if ($line =~ /^Binary file (.+) matches$/) {
5400 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5402 if ($file ne $lastfile) {
5403 $lastfile and print "</td></tr>\n";
5405 print "<tr class=\"dark\">\n";
5407 print "<tr class=\"light\">\n";
5409 print "<td class=\"list\">".
5410 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5411 file_name
=>"$file"),
5412 -class => "list"}, esc_path
($file));
5413 print "</td><td>\n";
5417 print "<div class=\"binary\">Binary file</div>\n";
5419 $ltext = untabify
($ltext);
5420 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5421 $ltext = esc_html
($1, -nbsp
=>1);
5422 $ltext .= '<span class="match">';
5423 $ltext .= esc_html
($2, -nbsp
=>1);
5424 $ltext .= '</span>';
5425 $ltext .= esc_html
($3, -nbsp
=>1);
5427 $ltext = esc_html
($ltext, -nbsp
=>1);
5429 print "<div class=\"pre\">" .
5430 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5431 file_name
=>"$file").'#l'.$lno,
5432 -class => "linenr"}, sprintf('%4i', $lno))
5433 . ' ' . $ltext . "</div>\n";
5437 print "</td></tr>\n";
5438 if ($matches > 1000) {
5439 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5442 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5451 sub git_search_help
{
5453 git_print_page_nav
('','', $hash,$hash,$hash);
5455 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5456 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5457 the pattern entered is recognized as the POSIX extended
5458 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5461 <dt><b>commit</b></dt>
5462 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5464 my ($have_grep) = gitweb_check_feature
('grep');
5467 <dt><b>grep</b></dt>
5468 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5469 a different one) are searched for the given pattern. On large trees, this search can take
5470 a while and put some strain on the server, so please use it with some consideration. Note that
5471 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5472 case-sensitive.</dd>
5476 <dt><b>author</b></dt>
5477 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5478 <dt><b>committer</b></dt>
5479 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5481 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5482 if ($have_pickaxe) {
5484 <dt><b>pickaxe</b></dt>
5485 <dd>All commits that caused the string to appear or disappear from any file (changes that
5486 added, removed or "modified" the string) will be listed. This search can take a while and
5487 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5488 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5496 my $head = git_get_head_hash
($project);
5497 if (!defined $hash) {
5500 if (!defined $page) {
5503 my $refs = git_get_references
();
5505 my $commit_hash = $hash;
5506 if (defined $hash_parent) {
5507 $commit_hash = "$hash_parent..$hash";
5509 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5511 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5513 if ($#commitlist >= 100) {
5515 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5516 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5520 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5521 git_print_header_div
('summary', $project);
5523 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5528 ## ......................................................................
5529 ## feeds (RSS, Atom; OPML)
5532 my $format = shift || 'atom';
5533 my ($have_blame) = gitweb_check_feature
('blame');
5535 # Atom: http://www.atomenabled.org/developers/syndication/
5536 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5537 if ($format ne 'rss' && $format ne 'atom') {
5538 die_error
(400, "Unknown web feed format");
5541 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5542 my $head = $hash || 'HEAD';
5543 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5547 my $content_type = "application/$format+xml";
5548 if (defined $cgi->http('HTTP_ACCEPT') &&
5549 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5550 # browser (feed reader) prefers text/xml
5551 $content_type = 'text/xml';
5553 if (defined($commitlist[0])) {
5554 %latest_commit = %{$commitlist[0]};
5555 %latest_date = parse_date
($latest_commit{'author_epoch'});
5557 -type
=> $content_type,
5558 -charset
=> 'utf-8',
5559 -last_modified
=> $latest_date{'rfc2822'});
5562 -type
=> $content_type,
5563 -charset
=> 'utf-8');
5566 # Optimization: skip generating the body if client asks only
5567 # for Last-Modified date.
5568 return if ($cgi->request_method() eq 'HEAD');
5571 my $title = "$site_name - $project/$action";
5572 my $feed_type = 'log';
5573 if (defined $hash) {
5574 $title .= " - '$hash'";
5575 $feed_type = 'branch log';
5576 if (defined $file_name) {
5577 $title .= " :: $file_name";
5578 $feed_type = 'history';
5580 } elsif (defined $file_name) {
5581 $title .= " - $file_name";
5582 $feed_type = 'history';
5584 $title .= " $feed_type";
5585 my $descr = git_get_project_description
($project);
5586 if (defined $descr) {
5587 $descr = esc_html
($descr);
5589 $descr = "$project " .
5590 ($format eq 'rss' ? 'RSS' : 'Atom') .
5593 my $owner = git_get_project_owner
($project);
5594 $owner = esc_html
($owner);
5598 if (defined $file_name) {
5599 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5600 } elsif (defined $hash) {
5601 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5603 $alt_url = href
(-full
=>1, action
=>"summary");
5605 print qq
!<?xml version
="1.0" encoding
="utf-8"?>\n!;
5606 if ($format eq 'rss') {
5608 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5611 print "<title>$title</title>\n" .
5612 "<link>$alt_url</link>\n" .
5613 "<description>$descr</description>\n" .
5614 "<language>en</language>\n";
5615 } elsif ($format eq 'atom') {
5617 <feed xmlns="http://www.w3.org/2005/Atom">
5619 print "<title>$title</title>\n" .
5620 "<subtitle>$descr</subtitle>\n" .
5621 '<link rel="alternate" type="text/html" href="' .
5622 $alt_url . '" />' . "\n" .
5623 '<link rel="self" type="' . $content_type . '" href="' .
5624 $cgi->self_url() . '" />' . "\n" .
5625 "<id>" . href
(-full
=>1) . "</id>\n" .
5626 # use project owner for feed author
5627 "<author><name>$owner</name></author>\n";
5628 if (defined $favicon) {
5629 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5631 if (defined $logo_url) {
5632 # not twice as wide as tall: 72 x 27 pixels
5633 print "<logo>" . esc_url
($logo) . "</logo>\n";
5635 if (! %latest_date) {
5636 # dummy date to keep the feed valid until commits trickle in:
5637 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5639 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5644 for (my $i = 0; $i <= $#commitlist; $i++) {
5645 my %co = %{$commitlist[$i]};
5646 my $commit = $co{'id'};
5647 # we read 150, we always show 30 and the ones more recent than 48 hours
5648 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5651 my %cd = parse_date
($co{'author_epoch'});
5653 # get list of changed files
5654 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5655 $co{'parent'} || "--root",
5656 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5658 my @difftree = map { chomp; $_ } <$fd>;
5662 # print element (entry, item)
5663 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5664 if ($format eq 'rss') {
5666 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5667 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5668 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5669 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5670 "<link>$co_url</link>\n" .
5671 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5672 "<content:encoded>" .
5674 } elsif ($format eq 'atom') {
5676 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5677 "<updated>$cd{'iso-8601'}</updated>\n" .
5679 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5680 if ($co{'author_email'}) {
5681 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5683 print "</author>\n" .
5684 # use committer for contributor
5686 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5687 if ($co{'committer_email'}) {
5688 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5690 print "</contributor>\n" .
5691 "<published>$cd{'iso-8601'}</published>\n" .
5692 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5693 "<id>$co_url</id>\n" .
5694 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5695 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5697 my $comment = $co{'comment'};
5699 foreach my $line (@$comment) {
5700 $line = esc_html
($line);
5703 print "</pre><ul>\n";
5704 foreach my $difftree_line (@difftree) {
5705 my %difftree = parse_difftree_raw_line
($difftree_line);
5706 next if !$difftree{'from_id'};
5708 my $file = $difftree{'file'} || $difftree{'to_file'};
5712 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5713 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5714 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5715 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5716 -title
=> "diff"}, 'D');
5718 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5719 file_name
=>$file, hash_base
=>$commit),
5720 -title
=> "blame"}, 'B');
5722 # if this is not a feed of a file history
5723 if (!defined $file_name || $file_name ne $file) {
5724 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5725 file_name
=>$file, hash
=>$commit),
5726 -title
=> "history"}, 'H');
5728 $file = esc_path
($file);
5732 if ($format eq 'rss') {
5733 print "</ul>]]>\n" .
5734 "</content:encoded>\n" .
5736 } elsif ($format eq 'atom') {
5737 print "</ul>\n</div>\n" .
5744 if ($format eq 'rss') {
5745 print "</channel>\n</rss>\n";
5746 } elsif ($format eq 'atom') {
5760 my @list = git_get_projects_list
();
5762 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
5764 <?xml version="1.0" encoding="utf-8"?>
5765 <opml version="1.0">
5767 <title>$site_name OPML Export</title>
5770 <outline text="git RSS feeds">
5773 foreach my $pr (@list) {
5775 my $head = git_get_head_hash
($proj{'path'});
5776 if (!defined $head) {
5779 $git_dir = "$projectroot/$proj{'path'}";
5780 my %co = parse_commit
($head);
5785 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
5786 my $rss = "$my_url?p=$proj{'path'};a=rss";
5787 my $html = "$my_url?p=$proj{'path'};a=summary";
5788 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";