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;
71 # URI of GIT logo (72x27 size)
72 our $logo = "++GITWEB_LOGO++";
73 # URI of GIT favicon, assumed to be image/png type
74 our $favicon = "++GITWEB_FAVICON++";
76 # URI and label (title) of GIT logo link
77 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
78 #our $logo_label = "git documentation";
79 our $logo_url = "http://git.or.cz/";
80 our $logo_label = "git homepage";
82 # source of projects list
83 our $projects_list = "++GITWEB_LIST++";
85 # the width (in characters) of the projects list "Description" column
86 our $projects_list_description_width = 25;
88 # default order of projects list
89 # valid values are none, project, descr, owner, and age
90 our $default_projects_order = "project";
92 # show repository only if this file exists
93 # (only effective if this variable evaluates to true)
94 our $export_ok = "++GITWEB_EXPORT_OK++";
96 # only allow viewing of repositories also shown on the overview page
97 our $strict_export = "++GITWEB_STRICT_EXPORT++";
99 # list of git base URLs used for URL to where fetch project from,
100 # i.e. full URL is "$git_base_url/$project"
101 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
103 # default blob_plain mimetype and default charset for text/plain blob
104 our $default_blob_plain_mimetype = 'text/plain';
105 our $default_text_plain_charset = undef;
107 # file to use for guessing MIME types before trying /etc/mime.types
108 # (relative to the current git repository)
109 our $mimetypes_file = undef;
111 # assume this charset if line contains non-UTF-8 characters;
112 # it should be valid encoding (see Encoding::Supported(3pm) for list),
113 # for which encoding all byte sequences are valid, for example
114 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
115 # could be even 'utf-8' for the old behavior)
116 our $fallback_encoding = 'latin1';
118 # rename detection options for git-diff and git-diff-tree
119 # - default is '-M', with the cost proportional to
120 # (number of removed files) * (number of new files).
121 # - more costly is '-C' (which implies '-M'), with the cost proportional to
122 # (number of changed files + number of removed files) * (number of new files)
123 # - even more costly is '-C', '--find-copies-harder' with cost
124 # (number of files in the original tree) * (number of new files)
125 # - one might want to include '-B' option, e.g. '-B', '-M'
126 our @diff_opts = ('-M'); # taken from git_commit
128 # information about snapshot formats that gitweb is capable of serving
129 our %known_snapshot_formats = (
131 # 'display' => display name,
132 # 'type' => mime type,
133 # 'suffix' => filename suffix,
134 # 'format' => --format for git-archive,
135 # 'compressor' => [compressor command and arguments]
136 # (array reference, optional)}
139 'display' => 'tar.gz',
140 'type' => 'application/x-gzip',
141 'suffix' => '.tar.gz',
143 'compressor' => ['gzip']},
146 'display' => 'tar.bz2',
147 'type' => 'application/x-bzip2',
148 'suffix' => '.tar.bz2',
150 'compressor' => ['bzip2']},
154 'type' => 'application/x-zip',
159 # Aliases so we understand old gitweb.snapshot values in repository
161 our %known_snapshot_format_aliases = (
165 # backward compatibility: legacy gitweb config support
166 'x-gzip' => undef, 'gz' => undef,
167 'x-bzip2' => undef, 'bz2' => undef,
168 'x-zip' => undef, '' => undef,
171 # You define site-wide feature defaults here; override them with
172 # $GITWEB_CONFIG as necessary.
175 # 'sub' => feature-sub (subroutine),
176 # 'override' => allow-override (boolean),
177 # 'default' => [ default options...] (array reference)}
179 # if feature is overridable (it means that allow-override has true value),
180 # then feature-sub will be called with default options as parameters;
181 # return value of feature-sub indicates if to enable specified feature
183 # if there is no 'sub' key (no feature-sub), then feature cannot be
186 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
188 # Enable the 'blame' blob view, showing the last commit that modified
189 # each line in the file. This can be very CPU-intensive.
191 # To enable system wide have in $GITWEB_CONFIG
192 # $feature{'blame'}{'default'} = [1];
193 # To have project specific config enable override in $GITWEB_CONFIG
194 # $feature{'blame'}{'override'} = 1;
195 # and in project config gitweb.blame = 0|1;
197 'sub' => \
&feature_blame
,
201 # Enable the 'snapshot' link, providing a compressed archive of any
202 # tree. This can potentially generate high traffic if you have large
205 # Value is a list of formats defined in %known_snapshot_formats that
207 # To disable system wide have in $GITWEB_CONFIG
208 # $feature{'snapshot'}{'default'} = [];
209 # To have project specific config enable override in $GITWEB_CONFIG
210 # $feature{'snapshot'}{'override'} = 1;
211 # and in project config, a comma-separated list of formats or "none"
212 # to disable. Example: gitweb.snapshot = tbz2,zip;
214 'sub' => \
&feature_snapshot
,
216 'default' => ['tgz']},
218 # Enable text search, which will list the commits which match author,
219 # committer or commit text to a given string. Enabled by default.
220 # Project specific override is not supported.
225 # Enable grep search, which will list the files in currently selected
226 # tree containing the given string. Enabled by default. This can be
227 # potentially CPU-intensive, of course.
229 # To enable system wide have in $GITWEB_CONFIG
230 # $feature{'grep'}{'default'} = [1];
231 # To have project specific config enable override in $GITWEB_CONFIG
232 # $feature{'grep'}{'override'} = 1;
233 # and in project config gitweb.grep = 0|1;
238 # Enable the pickaxe search, which will list the commits that modified
239 # a given string in a file. This can be practical and quite faster
240 # alternative to 'blame', but still potentially CPU-intensive.
242 # To enable system wide have in $GITWEB_CONFIG
243 # $feature{'pickaxe'}{'default'} = [1];
244 # To have project specific config enable override in $GITWEB_CONFIG
245 # $feature{'pickaxe'}{'override'} = 1;
246 # and in project config gitweb.pickaxe = 0|1;
248 'sub' => \
&feature_pickaxe
,
252 # Make gitweb use an alternative format of the URLs which can be
253 # more readable and natural-looking: project name is embedded
254 # directly in the path and the query string contains other
255 # auxiliary information. All gitweb installations recognize
256 # URL in either format; this configures in which formats gitweb
259 # To enable system wide have in $GITWEB_CONFIG
260 # $feature{'pathinfo'}{'default'} = [1];
261 # Project specific override is not supported.
263 # Note that you will need to change the default location of CSS,
264 # favicon, logo and possibly other files to an absolute URL. Also,
265 # if gitweb.cgi serves as your indexfile, you will need to force
266 # $my_uri to contain the script name in your $GITWEB_CONFIG.
271 # Make gitweb consider projects in project root subdirectories
272 # to be forks of existing projects. Given project $projname.git,
273 # projects matching $projname/*.git will not be shown in the main
274 # projects list, instead a '+' mark will be added to $projname
275 # there and a 'forks' view will be enabled for the project, listing
276 # all the forks. If project list is taken from a file, forks have
277 # to be listed after the main project.
279 # To enable system wide have in $GITWEB_CONFIG
280 # $feature{'forks'}{'default'} = [1];
281 # Project specific override is not supported.
286 # Allow gitweb scan project content tags described in ctags/
287 # of project repository, and display the popular Web 2.0-ish
288 # "tag cloud" near the project list. Note that this is something
289 # COMPLETELY different from the normal Git tags.
291 # gitweb by itself can show existing tags, but it does not handle
292 # tagging itself; you need an external application for that.
293 # For an example script, check Girocco's cgi/tagproj.cgi.
294 # You may want to install the HTML::TagCloud Perl module to get
295 # a pretty tag cloud instead of just a list of tags.
297 # To enable system wide have in $GITWEB_CONFIG
298 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
299 # Project specific override is not supported.
305 sub gitweb_check_feature
{
307 return unless exists $feature{$name};
308 my ($sub, $override, @defaults) = (
309 $feature{$name}{'sub'},
310 $feature{$name}{'override'},
311 @
{$feature{$name}{'default'}});
312 if (!$override) { return @defaults; }
314 warn "feature $name is not overrideable";
317 return $sub->(@defaults);
321 my ($val) = git_get_project_config
('blame', '--bool');
323 if ($val eq 'true') {
325 } elsif ($val eq 'false') {
332 sub feature_snapshot
{
335 my ($val) = git_get_project_config
('snapshot');
338 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
345 my ($val) = git_get_project_config
('grep', '--bool');
347 if ($val eq 'true') {
349 } elsif ($val eq 'false') {
356 sub feature_pickaxe
{
357 my ($val) = git_get_project_config
('pickaxe', '--bool');
359 if ($val eq 'true') {
361 } elsif ($val eq 'false') {
368 # checking HEAD file with -e is fragile if the repository was
369 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
371 sub check_head_link
{
373 my $headfile = "$dir/HEAD";
374 return ((-e
$headfile) ||
375 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
378 sub check_export_ok
{
380 return (check_head_link
($dir) &&
381 (!$export_ok || -e
"$dir/$export_ok"));
384 # process alternate names for backward compatibility
385 # filter out unsupported (unknown) snapshot formats
386 sub filter_snapshot_fmts
{
390 exists $known_snapshot_format_aliases{$_} ?
391 $known_snapshot_format_aliases{$_} : $_} @fmts;
392 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
396 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
397 if (-e
$GITWEB_CONFIG) {
400 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
401 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
404 # version of the core git binary
405 our $git_version = qx("$GIT" --version
) =~ m/git version (.*)$/ ?
$1 : "unknown";
407 $projects_list ||= $projectroot;
409 # ======================================================================
410 # input validation and dispatch
411 our $action = $cgi->param('a');
412 if (defined $action) {
413 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
414 die_error
(400, "Invalid action parameter");
418 # parameters which are pathnames
419 our $project = $cgi->param('p');
420 if (defined $project) {
421 if (!validate_pathname
($project) ||
422 !(-d
"$projectroot/$project") ||
423 !check_head_link
("$projectroot/$project") ||
424 ($export_ok && !(-e
"$projectroot/$project/$export_ok")) ||
425 ($strict_export && !project_in_list
($project))) {
427 die_error
(404, "No such project");
431 our $file_name = $cgi->param('f');
432 if (defined $file_name) {
433 if (!validate_pathname
($file_name)) {
434 die_error
(400, "Invalid file parameter");
438 our $file_parent = $cgi->param('fp');
439 if (defined $file_parent) {
440 if (!validate_pathname
($file_parent)) {
441 die_error
(400, "Invalid file parent parameter");
445 # parameters which are refnames
446 our $hash = $cgi->param('h');
448 if (!validate_refname
($hash)) {
449 die_error
(400, "Invalid hash parameter");
453 our $hash_parent = $cgi->param('hp');
454 if (defined $hash_parent) {
455 if (!validate_refname
($hash_parent)) {
456 die_error
(400, "Invalid hash parent parameter");
460 our $hash_base = $cgi->param('hb');
461 if (defined $hash_base) {
462 if (!validate_refname
($hash_base)) {
463 die_error
(400, "Invalid hash base parameter");
467 my %allowed_options = (
468 "--no-merges" => [ qw(rss atom log shortlog history) ],
471 our @extra_options = $cgi->param('opt');
472 if (defined @extra_options) {
473 foreach my $opt (@extra_options) {
474 if (not exists $allowed_options{$opt}) {
475 die_error
(400, "Invalid option parameter");
477 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
478 die_error
(400, "Invalid option parameter for this action");
483 our $hash_parent_base = $cgi->param('hpb');
484 if (defined $hash_parent_base) {
485 if (!validate_refname
($hash_parent_base)) {
486 die_error
(400, "Invalid hash parent base parameter");
491 our $page = $cgi->param('pg');
493 if ($page =~ m/[^0-9]/) {
494 die_error
(400, "Invalid page parameter");
498 our $searchtype = $cgi->param('st');
499 if (defined $searchtype) {
500 if ($searchtype =~ m/[^a-z]/) {
501 die_error
(400, "Invalid searchtype parameter");
505 our $search_use_regexp = $cgi->param('sr');
507 our $searchtext = $cgi->param('s');
509 if (defined $searchtext) {
510 if (length($searchtext) < 2) {
511 die_error
(403, "At least two characters are required for search parameter");
513 $search_regexp = $search_use_regexp ?
$searchtext : quotemeta $searchtext;
516 # now read PATH_INFO and use it as alternative to parameters
517 sub evaluate_path_info
{
518 return if defined $project;
519 my $path_info = $ENV{"PATH_INFO"};
520 return if !$path_info;
521 $path_info =~ s
,^/+,,;
522 return if !$path_info;
523 # find which part of PATH_INFO is project
524 $project = $path_info;
526 while ($project && !check_head_link
("$projectroot/$project")) {
527 $project =~ s
,/*[^/]*$,,;
530 $project = validate_pathname
($project);
532 ($export_ok && !-e
"$projectroot/$project/$export_ok") ||
533 ($strict_export && !project_in_list
($project))) {
537 # do not change any parameters if an action is given using the query string
539 $path_info =~ s
,^\Q
$project\E
/*,,;
540 my ($refname, $pathname) = split(/:/, $path_info, 2);
541 if (defined $pathname) {
542 # we got "project.git/branch:filename" or "project.git/branch:dir/"
543 # we could use git_get_type(branch:pathname), but it needs $git_dir
544 $pathname =~ s
,^/+,,;
545 if (!$pathname || substr($pathname, -1) eq "/") {
549 $action ||= "blob_plain";
551 $hash_base ||= validate_refname
($refname);
552 $file_name ||= validate_pathname
($pathname);
553 } elsif (defined $refname) {
554 # we got "project.git/branch"
555 $action ||= "shortlog";
556 $hash ||= validate_refname
($refname);
559 evaluate_path_info
();
561 # path to the current git repository
563 $git_dir = "$projectroot/$project" if $project;
567 "blame" => \
&git_blame
,
568 "blobdiff" => \
&git_blobdiff
,
569 "blobdiff_plain" => \
&git_blobdiff_plain
,
570 "blob" => \
&git_blob
,
571 "blob_plain" => \
&git_blob_plain
,
572 "commitdiff" => \
&git_commitdiff
,
573 "commitdiff_plain" => \
&git_commitdiff_plain
,
574 "commit" => \
&git_commit
,
575 "forks" => \
&git_forks
,
576 "heads" => \
&git_heads
,
577 "history" => \
&git_history
,
580 "atom" => \
&git_atom
,
581 "search" => \
&git_search
,
582 "search_help" => \
&git_search_help
,
583 "shortlog" => \
&git_shortlog
,
584 "summary" => \
&git_summary
,
586 "tags" => \
&git_tags
,
587 "tree" => \
&git_tree
,
588 "snapshot" => \
&git_snapshot
,
589 "object" => \
&git_object
,
590 # those below don't need $project
591 "opml" => \
&git_opml
,
592 "project_list" => \
&git_project_list
,
593 "project_index" => \
&git_project_index
,
596 if (!defined $action) {
598 $action = git_get_type
($hash);
599 } elsif (defined $hash_base && defined $file_name) {
600 $action = git_get_type
("$hash_base:$file_name");
601 } elsif (defined $project) {
604 $action = 'project_list';
607 if (!defined($actions{$action})) {
608 die_error
(400, "Unknown action");
610 if ($action !~ m/^(opml|project_list|project_index)$/ &&
612 die_error
(400, "Project needed");
614 $actions{$action}->();
617 ## ======================================================================
622 # default is to use -absolute url() i.e. $my_uri
623 my $href = $params{-full
} ?
$my_url : $my_uri;
625 # XXX: Warning: If you touch this, check the search form for updating,
636 hash_parent_base
=> "hpb",
641 snapshot_format
=> "sf",
642 extra_options
=> "opt",
643 search_use_regexp
=> "sr",
645 my %mapping = @mapping;
647 $params{'project'} = $project unless exists $params{'project'};
649 if ($params{-replay
}) {
650 while (my ($name, $symbol) = each %mapping) {
651 if (!exists $params{$name}) {
652 # to allow for multivalued params we use arrayref form
653 $params{$name} = [ $cgi->param($symbol) ];
658 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
660 # use PATH_INFO for project name
661 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
662 delete $params{'project'};
664 # Summary just uses the project path URL
665 if (defined $params{'action'} && $params{'action'} eq 'summary') {
666 delete $params{'action'};
670 # now encode the parameters explicitly
672 for (my $i = 0; $i < @mapping; $i += 2) {
673 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
674 if (defined $params{$name}) {
675 if (ref($params{$name}) eq "ARRAY") {
676 foreach my $par (@
{$params{$name}}) {
677 push @result, $symbol . "=" . esc_param
($par);
680 push @result, $symbol . "=" . esc_param
($params{$name});
684 $href .= "?" . join(';', @result) if scalar @result;
690 ## ======================================================================
691 ## validation, quoting/unquoting and escaping
693 sub validate_pathname
{
694 my $input = shift || return undef;
696 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
697 # at the beginning, at the end, and between slashes.
698 # also this catches doubled slashes
699 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
703 if ($input =~ m!\0!) {
709 sub validate_refname
{
710 my $input = shift || return undef;
712 # textual hashes are O.K.
713 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
716 # it must be correct pathname
717 $input = validate_pathname
($input)
719 # restrictions on ref name according to git-check-ref-format
720 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
726 # decode sequences of octets in utf8 into Perl's internal form,
727 # which is utf-8 with utf8 flag set if needed. gitweb writes out
728 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
731 if (utf8
::valid
($str)) {
735 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
739 # quote unsafe chars, but keep the slash, even when it's not
740 # correct, but quoted slashes look too horrible in bookmarks
743 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
749 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
752 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
758 # replace invalid utf8 character with SUBSTITUTION sequence
763 $str = to_utf8
($str);
764 $str = $cgi->escapeHTML($str);
765 if ($opts{'-nbsp'}) {
766 $str =~ s/ / /g;
768 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
772 # quote control characters and escape filename to HTML
777 $str = to_utf8
($str);
778 $str = $cgi->escapeHTML($str);
779 if ($opts{'-nbsp'}) {
780 $str =~ s/ / /g;
782 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
786 # Make control characters "printable", using character escape codes (CEC)
790 my %es = ( # character escape codes, aka escape sequences
791 "\t" => '\t', # tab (HT)
792 "\n" => '\n', # line feed (LF)
793 "\r" => '\r', # carrige return (CR)
794 "\f" => '\f', # form feed (FF)
795 "\b" => '\b', # backspace (BS)
796 "\a" => '\a', # alarm (bell) (BEL)
797 "\e" => '\e', # escape (ESC)
798 "\013" => '\v', # vertical tab (VT)
799 "\000" => '\0', # nul character (NUL)
801 my $chr = ( (exists $es{$cntrl})
803 : sprintf('\%2x', ord($cntrl)) );
804 if ($opts{-nohtml
}) {
807 return "<span class=\"cntrl\">$chr</span>";
811 # Alternatively use unicode control pictures codepoints,
812 # Unicode "printable representation" (PR)
817 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
818 if ($opts{-nohtml
}) {
821 return "<span class=\"cntrl\">$chr</span>";
825 # git may return quoted and escaped filenames
831 my %es = ( # character escape codes, aka escape sequences
832 't' => "\t", # tab (HT, TAB)
833 'n' => "\n", # newline (NL)
834 'r' => "\r", # return (CR)
835 'f' => "\f", # form feed (FF)
836 'b' => "\b", # backspace (BS)
837 'a' => "\a", # alarm (bell) (BEL)
838 'e' => "\e", # escape (ESC)
839 'v' => "\013", # vertical tab (VT)
842 if ($seq =~ m/^[0-7]{1,3}$/) {
843 # octal char sequence
844 return chr(oct($seq));
845 } elsif (exists $es{$seq}) {
846 # C escape sequence, aka character escape code
849 # quoted ordinary character
853 if ($str =~ m/^"(.*)"$/) {
856 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
861 # escape tabs (convert tabs to spaces)
865 while ((my $pos = index($line, "\t")) != -1) {
866 if (my $count = (8 - ($pos % 8))) {
867 my $spaces = ' ' x
$count;
868 $line =~ s/\t/$spaces/;
875 sub project_in_list
{
877 my @list = git_get_projects_list
();
878 return @list && scalar(grep { $_->{'path'} eq $project } @list);
881 ## ----------------------------------------------------------------------
882 ## HTML aware string manipulation
884 # Try to chop given string on a word boundary between position
885 # $len and $len+$add_len. If there is no word boundary there,
886 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
887 # (marking chopped part) would be longer than given string.
891 my $add_len = shift || 10;
892 my $where = shift || 'right'; # 'left' | 'center' | 'right'
894 # Make sure perl knows it is utf8 encoded so we don't
895 # cut in the middle of a utf8 multibyte char.
896 $str = to_utf8
($str);
898 # allow only $len chars, but don't cut a word if it would fit in $add_len
899 # if it doesn't fit, cut it if it's still longer than the dots we would add
900 # remove chopped character entities entirely
902 # when chopping in the middle, distribute $len into left and right part
903 # return early if chopping wouldn't make string shorter
904 if ($where eq 'center') {
905 return $str if ($len + 5 >= length($str)); # filler is length 5
908 return $str if ($len + 4 >= length($str)); # filler is length 4
911 # regexps: ending and beginning with word part up to $add_len
912 my $endre = qr/.{$len}\w{0,$add_len}/;
913 my $begre = qr/\w{0,$add_len}.{$len}/;
915 if ($where eq 'left') {
916 $str =~ m/^(.*?)($begre)$/;
917 my ($lead, $body) = ($1, $2);
918 if (length($lead) > 4) {
919 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
924 } elsif ($where eq 'center') {
925 $str =~ m/^($endre)(.*)$/;
926 my ($left, $str) = ($1, $2);
927 $str =~ m/^(.*?)($begre)$/;
928 my ($mid, $right) = ($1, $2);
929 if (length($mid) > 5) {
930 $left =~ s/&[^;]*$//;
931 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
934 return "$left$mid$right";
937 $str =~ m/^($endre)(.*)$/;
940 if (length($tail) > 4) {
941 $body =~ s/&[^;]*$//;
948 # takes the same arguments as chop_str, but also wraps a <span> around the
949 # result with a title attribute if it does get chopped. Additionally, the
950 # string is HTML-escaped.
951 sub chop_and_escape_str
{
954 my $chopped = chop_str
(@_);
955 if ($chopped eq $str) {
956 return esc_html
($chopped);
958 $str =~ s/([[:cntrl:]])/?/g;
959 return $cgi->span({-title
=>$str}, esc_html
($chopped));
963 ## ----------------------------------------------------------------------
964 ## functions returning short strings
966 # CSS class for given age value (in seconds)
972 } elsif ($age < 60*60*2) {
974 } elsif ($age < 60*60*24*2) {
981 # convert age in seconds to "nn units ago" string
986 if ($age > 60*60*24*365*2) {
987 $age_str = (int $age/60/60/24/365);
988 $age_str .= " years ago";
989 } elsif ($age > 60*60*24*(365/12)*2) {
990 $age_str = int $age/60/60/24/(365/12);
991 $age_str .= " months ago";
992 } elsif ($age > 60*60*24*7*2) {
993 $age_str = int $age/60/60/24/7;
994 $age_str .= " weeks ago";
995 } elsif ($age > 60*60*24*2) {
996 $age_str = int $age/60/60/24;
997 $age_str .= " days ago";
998 } elsif ($age > 60*60*2) {
999 $age_str = int $age/60/60;
1000 $age_str .= " hours ago";
1001 } elsif ($age > 60*2) {
1002 $age_str = int $age/60;
1003 $age_str .= " min ago";
1004 } elsif ($age > 2) {
1005 $age_str = int $age;
1006 $age_str .= " sec ago";
1008 $age_str .= " right now";
1014 S_IFINVALID
=> 0030000,
1015 S_IFGITLINK
=> 0160000,
1018 # submodule/subproject, a commit object reference
1019 sub S_ISGITLINK
($) {
1022 return (($mode & S_IFMT
) == S_IFGITLINK
)
1025 # convert file mode in octal to symbolic file mode string
1027 my $mode = oct shift;
1029 if (S_ISGITLINK
($mode)) {
1030 return 'm---------';
1031 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1032 return 'drwxr-xr-x';
1033 } elsif (S_ISLNK
($mode)) {
1034 return 'lrwxrwxrwx';
1035 } elsif (S_ISREG
($mode)) {
1036 # git cares only about the executable bit
1037 if ($mode & S_IXUSR
) {
1038 return '-rwxr-xr-x';
1040 return '-rw-r--r--';
1043 return '----------';
1047 # convert file mode in octal to file type string
1051 if ($mode !~ m/^[0-7]+$/) {
1057 if (S_ISGITLINK
($mode)) {
1059 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1061 } elsif (S_ISLNK
($mode)) {
1063 } elsif (S_ISREG
($mode)) {
1070 # convert file mode in octal to file type description string
1071 sub file_type_long
{
1074 if ($mode !~ m/^[0-7]+$/) {
1080 if (S_ISGITLINK
($mode)) {
1082 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1084 } elsif (S_ISLNK
($mode)) {
1086 } elsif (S_ISREG
($mode)) {
1087 if ($mode & S_IXUSR
) {
1088 return "executable";
1098 ## ----------------------------------------------------------------------
1099 ## functions returning short HTML fragments, or transforming HTML fragments
1100 ## which don't belong to other sections
1102 # format line of commit message.
1103 sub format_log_line_html
{
1106 $line = esc_html
($line, -nbsp
=>1);
1107 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1110 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1111 -class => "text"}, $hash_text);
1112 $line =~ s/$hash_text/$link/;
1117 # format marker of refs pointing to given object
1119 # the destination action is chosen based on object type and current context:
1120 # - for annotated tags, we choose the tag view unless it's the current view
1121 # already, in which case we go to shortlog view
1122 # - for other refs, we keep the current view if we're in history, shortlog or
1123 # log view, and select shortlog otherwise
1124 sub format_ref_marker
{
1125 my ($refs, $id) = @_;
1128 if (defined $refs->{$id}) {
1129 foreach my $ref (@
{$refs->{$id}}) {
1130 # this code exploits the fact that non-lightweight tags are the
1131 # only indirect objects, and that they are the only objects for which
1132 # we want to use tag instead of shortlog as action
1133 my ($type, $name) = qw();
1134 my $indirect = ($ref =~ s/\^\{\}$//);
1135 # e.g. tags/v2.6.11 or heads/next
1136 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1145 $class .= " indirect" if $indirect;
1147 my $dest_action = "shortlog";
1150 $dest_action = "tag" unless $action eq "tag";
1151 } elsif ($action =~ /^(history|(short)?log)$/) {
1152 $dest_action = $action;
1156 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1159 my $link = $cgi->a({
1161 action
=>$dest_action,
1165 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1171 return ' <span class="refs">'. $markers . '</span>';
1177 # format, perhaps shortened and with markers, title line
1178 sub format_subject_html
{
1179 my ($long, $short, $href, $extra) = @_;
1180 $extra = '' unless defined($extra);
1182 if (length($short) < length($long)) {
1183 return $cgi->a({-href
=> $href, -class => "list subject",
1184 -title
=> to_utf8
($long)},
1185 esc_html
($short) . $extra);
1187 return $cgi->a({-href
=> $href, -class => "list subject"},
1188 esc_html
($long) . $extra);
1192 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1193 sub format_git_diff_header_line
{
1195 my $diffinfo = shift;
1196 my ($from, $to) = @_;
1198 if ($diffinfo->{'nparents'}) {
1200 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1201 if ($to->{'href'}) {
1202 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1203 esc_path
($to->{'file'}));
1204 } else { # file was deleted (no href)
1205 $line .= esc_path
($to->{'file'});
1209 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1210 if ($from->{'href'}) {
1211 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1212 'a/' . esc_path
($from->{'file'}));
1213 } else { # file was added (no href)
1214 $line .= 'a/' . esc_path
($from->{'file'});
1217 if ($to->{'href'}) {
1218 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1219 'b/' . esc_path
($to->{'file'}));
1220 } else { # file was deleted
1221 $line .= 'b/' . esc_path
($to->{'file'});
1225 return "<div class=\"diff header\">$line</div>\n";
1228 # format extended diff header line, before patch itself
1229 sub format_extended_diff_header_line
{
1231 my $diffinfo = shift;
1232 my ($from, $to) = @_;
1235 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1236 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1237 esc_path
($from->{'file'}));
1239 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1240 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1241 esc_path
($to->{'file'}));
1243 # match single <mode>
1244 if ($line =~ m/\s(\d{6})$/) {
1245 $line .= '<span class="info"> (' .
1246 file_type_long
($1) .
1250 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1251 # can match only for combined diff
1253 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1254 if ($from->{'href'}[$i]) {
1255 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1257 substr($diffinfo->{'from_id'}[$i],0,7));
1262 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1265 if ($to->{'href'}) {
1266 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1267 substr($diffinfo->{'to_id'},0,7));
1272 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1273 # can match only for ordinary diff
1274 my ($from_link, $to_link);
1275 if ($from->{'href'}) {
1276 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1277 substr($diffinfo->{'from_id'},0,7));
1279 $from_link = '0' x
7;
1281 if ($to->{'href'}) {
1282 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1283 substr($diffinfo->{'to_id'},0,7));
1287 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1288 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1291 return $line . "<br/>\n";
1294 # format from-file/to-file diff header
1295 sub format_diff_from_to_header
{
1296 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1301 #assert($line =~ m/^---/) if DEBUG;
1302 # no extra formatting for "^--- /dev/null"
1303 if (! $diffinfo->{'nparents'}) {
1304 # ordinary (single parent) diff
1305 if ($line =~ m!^--- "?a/!) {
1306 if ($from->{'href'}) {
1308 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1309 esc_path
($from->{'file'}));
1312 esc_path
($from->{'file'});
1315 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1318 # combined diff (merge commit)
1319 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1320 if ($from->{'href'}[$i]) {
1322 $cgi->a({-href
=>href
(action
=>"blobdiff",
1323 hash_parent
=>$diffinfo->{'from_id'}[$i],
1324 hash_parent_base
=>$parents[$i],
1325 file_parent
=>$from->{'file'}[$i],
1326 hash
=>$diffinfo->{'to_id'},
1328 file_name
=>$to->{'file'}),
1330 -title
=>"diff" . ($i+1)},
1333 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1334 esc_path
($from->{'file'}[$i]));
1336 $line = '--- /dev/null';
1338 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1343 #assert($line =~ m/^\+\+\+/) if DEBUG;
1344 # no extra formatting for "^+++ /dev/null"
1345 if ($line =~ m!^\+\+\+ "?b/!) {
1346 if ($to->{'href'}) {
1348 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1349 esc_path
($to->{'file'}));
1352 esc_path
($to->{'file'});
1355 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
1360 # create note for patch simplified by combined diff
1361 sub format_diff_cc_simplified
{
1362 my ($diffinfo, @parents) = @_;
1365 $result .= "<div class=\"diff header\">" .
1367 if (!is_deleted
($diffinfo)) {
1368 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1370 hash
=>$diffinfo->{'to_id'},
1371 file_name
=>$diffinfo->{'to_file'}),
1373 esc_path
($diffinfo->{'to_file'}));
1375 $result .= esc_path
($diffinfo->{'to_file'});
1377 $result .= "</div>\n" . # class="diff header"
1378 "<div class=\"diff nodifferences\">" .
1380 "</div>\n"; # class="diff nodifferences"
1385 # format patch (diff) line (not to be used for diff headers)
1386 sub format_diff_line
{
1388 my ($from, $to) = @_;
1389 my $diff_class = "";
1393 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1395 my $prefix = substr($line, 0, scalar @
{$from->{'href'}});
1396 if ($line =~ m/^\@{3}/) {
1397 $diff_class = " chunk_header";
1398 } elsif ($line =~ m/^\\/) {
1399 $diff_class = " incomplete";
1400 } elsif ($prefix =~ tr/+/+/) {
1401 $diff_class = " add";
1402 } elsif ($prefix =~ tr/-/-/) {
1403 $diff_class = " rem";
1406 # assume ordinary diff
1407 my $char = substr($line, 0, 1);
1409 $diff_class = " add";
1410 } elsif ($char eq '-') {
1411 $diff_class = " rem";
1412 } elsif ($char eq '@') {
1413 $diff_class = " chunk_header";
1414 } elsif ($char eq "\\") {
1415 $diff_class = " incomplete";
1418 $line = untabify
($line);
1419 if ($from && $to && $line =~ m/^\@{2} /) {
1420 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1421 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1423 $from_lines = 0 unless defined $from_lines;
1424 $to_lines = 0 unless defined $to_lines;
1426 if ($from->{'href'}) {
1427 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1428 -class=>"list"}, $from_text);
1430 if ($to->{'href'}) {
1431 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1432 -class=>"list"}, $to_text);
1434 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1435 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1436 return "<div class=\"diff$diff_class\">$line</div>\n";
1437 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1438 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1439 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1441 @from_text = split(' ', $ranges);
1442 for (my $i = 0; $i < @from_text; ++$i) {
1443 ($from_start[$i], $from_nlines[$i]) =
1444 (split(',', substr($from_text[$i], 1)), 0);
1447 $to_text = pop @from_text;
1448 $to_start = pop @from_start;
1449 $to_nlines = pop @from_nlines;
1451 $line = "<span class=\"chunk_info\">$prefix ";
1452 for (my $i = 0; $i < @from_text; ++$i) {
1453 if ($from->{'href'}[$i]) {
1454 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1455 -class=>"list"}, $from_text[$i]);
1457 $line .= $from_text[$i];
1461 if ($to->{'href'}) {
1462 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1463 -class=>"list"}, $to_text);
1467 $line .= " $prefix</span>" .
1468 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1469 return "<div class=\"diff$diff_class\">$line</div>\n";
1471 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1474 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1475 # linked. Pass the hash of the tree/commit to snapshot.
1476 sub format_snapshot_links
{
1478 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1479 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1480 my $num_fmts = @snapshot_fmts;
1481 if ($num_fmts > 1) {
1482 # A parenthesized list of links bearing format names.
1483 # e.g. "snapshot (_tar.gz_ _zip_)"
1484 return "snapshot (" . join(' ', map
1491 }, $known_snapshot_formats{$_}{'display'})
1492 , @snapshot_fmts) . ")";
1493 } elsif ($num_fmts == 1) {
1494 # A single "snapshot" link whose tooltip bears the format name.
1496 my ($fmt) = @snapshot_fmts;
1502 snapshot_format
=>$fmt
1504 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1506 } else { # $num_fmts == 0
1511 ## ......................................................................
1512 ## functions returning values to be passed, perhaps after some
1513 ## transformation, to other functions; e.g. returning arguments to href()
1515 # returns hash to be passed to href to generate gitweb URL
1516 # in -title key it returns description of link
1518 my $format = shift || 'Atom';
1519 my %res = (action
=> lc($format));
1521 # feed links are possible only for project views
1522 return unless (defined $project);
1523 # some views should link to OPML, or to generic project feed,
1524 # or don't have specific feed yet (so they should use generic)
1525 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1528 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1529 # from tag links; this also makes possible to detect branch links
1530 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1531 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1534 # find log type for feed description (title)
1536 if (defined $file_name) {
1537 $type = "history of $file_name";
1538 $type .= "/" if ($action eq 'tree');
1539 $type .= " on '$branch'" if (defined $branch);
1541 $type = "log of $branch" if (defined $branch);
1544 $res{-title
} = $type;
1545 $res{'hash'} = (defined $branch ?
"refs/heads/$branch" : undef);
1546 $res{'file_name'} = $file_name;
1551 ## ----------------------------------------------------------------------
1552 ## git utility subroutines, invoking git commands
1554 # returns path to the core git executable and the --git-dir parameter as list
1556 return $GIT, '--git-dir='.$git_dir;
1559 # quote the given arguments for passing them to the shell
1560 # quote_command("command", "arg 1", "arg with ' and ! characters")
1561 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1562 # Try to avoid using this function wherever possible.
1565 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1568 # get HEAD ref of given project as hash
1569 sub git_get_head_hash
{
1570 my $project = shift;
1571 my $o_git_dir = $git_dir;
1573 $git_dir = "$projectroot/$project";
1574 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1577 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1581 if (defined $o_git_dir) {
1582 $git_dir = $o_git_dir;
1587 # get type of given object
1591 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1593 close $fd or return;
1598 # repository configuration
1599 our $config_file = '';
1602 # store multiple values for single key as anonymous array reference
1603 # single values stored directly in the hash, not as [ <value> ]
1604 sub hash_set_multi
{
1605 my ($hash, $key, $value) = @_;
1607 if (!exists $hash->{$key}) {
1608 $hash->{$key} = $value;
1609 } elsif (!ref $hash->{$key}) {
1610 $hash->{$key} = [ $hash->{$key}, $value ];
1612 push @
{$hash->{$key}}, $value;
1616 # return hash of git project configuration
1617 # optionally limited to some section, e.g. 'gitweb'
1618 sub git_parse_project_config
{
1619 my $section_regexp = shift;
1624 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1627 while (my $keyval = <$fh>) {
1629 my ($key, $value) = split(/\n/, $keyval, 2);
1631 hash_set_multi
(\
%config, $key, $value)
1632 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1639 # convert config value to boolean, 'true' or 'false'
1640 # no value, number > 0, 'true' and 'yes' values are true
1641 # rest of values are treated as false (never as error)
1642 sub config_to_bool
{
1645 # strip leading and trailing whitespace
1649 return (!defined $val || # section.key
1650 ($val =~ /^\d+$/ && $val) || # section.key = 1
1651 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1654 # convert config value to simple decimal number
1655 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1656 # to be multiplied by 1024, 1048576, or 1073741824
1660 # strip leading and trailing whitespace
1664 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1666 # unknown unit is treated as 1
1667 return $num * ($unit eq 'g' ?
1073741824 :
1668 $unit eq 'm' ?
1048576 :
1669 $unit eq 'k' ?
1024 : 1);
1674 # convert config value to array reference, if needed
1675 sub config_to_multi
{
1678 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
1681 sub git_get_project_config
{
1682 my ($key, $type) = @_;
1685 return unless ($key);
1686 $key =~ s/^gitweb\.//;
1687 return if ($key =~ m/\W/);
1690 if (defined $type) {
1693 unless ($type eq 'bool' || $type eq 'int');
1697 if (!defined $config_file ||
1698 $config_file ne "$git_dir/config") {
1699 %config = git_parse_project_config
('gitweb');
1700 $config_file = "$git_dir/config";
1704 if (!defined $type) {
1705 return $config{"gitweb.$key"};
1706 } elsif ($type eq 'bool') {
1707 # backward compatibility: 'git config --bool' returns true/false
1708 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
1709 } elsif ($type eq 'int') {
1710 return config_to_int
($config{"gitweb.$key"});
1712 return $config{"gitweb.$key"};
1715 # get hash of given path at given ref
1716 sub git_get_hash_by_path
{
1718 my $path = shift || return undef;
1723 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1724 or die_error
(500, "Open git-ls-tree failed");
1726 close $fd or return undef;
1728 if (!defined $line) {
1729 # there is no tree or hash given by $path at $base
1733 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1734 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1735 if (defined $type && $type ne $2) {
1736 # type doesn't match
1742 # get path of entry with given hash at given tree-ish (ref)
1743 # used to get 'from' filename for combined diff (merge commit) for renames
1744 sub git_get_path_by_hash
{
1745 my $base = shift || return;
1746 my $hash = shift || return;
1750 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1752 while (my $line = <$fd>) {
1755 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1756 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1757 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1766 ## ......................................................................
1767 ## git utility functions, directly accessing git repository
1769 sub git_get_project_description
{
1772 $git_dir = "$projectroot/$path";
1773 open my $fd, "$git_dir/description"
1774 or return git_get_project_config
('description');
1777 if (defined $descr) {
1783 sub git_get_project_ctags
{
1787 $git_dir = "$projectroot/$path";
1788 foreach (<$git_dir/ctags/*>) {
1789 open CT
, $_ or next;
1793 my $ctag = $_; $ctag =~ s
#.*/##;
1794 $ctags->{$ctag} = $val;
1799 sub git_populate_project_tagcloud
{
1802 # First, merge different-cased tags; tags vote on casing
1804 foreach (keys %$ctags) {
1805 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
1806 if (not $ctags_lc{lc $_}->{topcount
}
1807 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
1808 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
1809 $ctags_lc{lc $_}->{topname
} = $_;
1814 if (eval { require HTML
::TagCloud
; 1; }) {
1815 $cloud = HTML
::TagCloud
->new;
1816 foreach (sort keys %ctags_lc) {
1817 # Pad the title with spaces so that the cloud looks
1819 my $title = $ctags_lc{$_}->{topname
};
1820 $title =~ s/ / /g;
1821 $title =~ s/^/ /g;
1822 $title =~ s/$/ /g;
1823 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count
});
1826 $cloud = \
%ctags_lc;
1831 sub git_show_project_tagcloud
{
1832 my ($cloud, $count) = @_;
1833 print STDERR
ref($cloud)."..\n";
1834 if (ref $cloud eq 'HTML::TagCloud') {
1835 return $cloud->html_and_css($count);
1837 my @tags = sort { $cloud->{$a}->{count
} <=> $cloud->{$b}->{count
} } keys %$cloud;
1838 return '<p align="center">' . join (', ', map {
1839 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1840 } splice(@tags, 0, $count)) . '</p>';
1844 sub git_get_project_url_list
{
1847 $git_dir = "$projectroot/$path";
1848 open my $fd, "$git_dir/cloneurl"
1849 or return wantarray ?
1850 @
{ config_to_multi
(git_get_project_config
('url')) } :
1851 config_to_multi
(git_get_project_config
('url'));
1852 my @git_project_url_list = map { chomp; $_ } <$fd>;
1855 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
1858 sub git_get_projects_list
{
1863 $filter =~ s/\.git$//;
1865 my ($check_forks) = gitweb_check_feature
('forks');
1867 if (-d
$projects_list) {
1868 # search in directory
1869 my $dir = $projects_list . ($filter ?
"/$filter" : '');
1870 # remove the trailing "/"
1872 my $pfxlen = length("$dir");
1873 my $pfxdepth = ($dir =~ tr!/!!);
1876 follow_fast
=> 1, # follow symbolic links
1877 follow_skip
=> 2, # ignore duplicates
1878 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1880 # skip project-list toplevel, if we get it.
1881 return if (m!^[/.]$!);
1882 # only directories can be git repositories
1883 return unless (-d
$_);
1884 # don't traverse too deep (Find is super slow on os x)
1885 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1886 $File::Find
::prune
= 1;
1890 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1891 # we check related file in $projectroot
1892 if ($check_forks and $subdir =~ m
#/.#) {
1893 $File::Find
::prune
= 1;
1894 } elsif (check_export_ok
("$projectroot/$filter/$subdir")) {
1895 push @list, { path
=> ($filter ?
"$filter/" : '') . $subdir };
1896 $File::Find
::prune
= 1;
1901 } elsif (-f
$projects_list) {
1902 # read from file(url-encoded):
1903 # 'git%2Fgit.git Linus+Torvalds'
1904 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1905 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1907 open my ($fd), $projects_list or return;
1909 while (my $line = <$fd>) {
1911 my ($path, $owner) = split ' ', $line;
1912 $path = unescape
($path);
1913 $owner = unescape
($owner);
1914 if (!defined $path) {
1917 if ($filter ne '') {
1918 # looking for forks;
1919 my $pfx = substr($path, 0, length($filter));
1920 if ($pfx ne $filter) {
1923 my $sfx = substr($path, length($filter));
1924 if ($sfx !~ /^\/.*\
.git
$/) {
1927 } elsif ($check_forks) {
1929 foreach my $filter (keys %paths) {
1930 # looking for forks;
1931 my $pfx = substr($path, 0, length($filter));
1932 if ($pfx ne $filter) {
1935 my $sfx = substr($path, length($filter));
1936 if ($sfx !~ /^\/.*\
.git
$/) {
1939 # is a fork, don't include it in
1944 if (check_export_ok
("$projectroot/$path")) {
1947 owner
=> to_utf8
($owner),
1950 (my $forks_path = $path) =~ s/\.git$//;
1951 $paths{$forks_path}++;
1959 our $gitweb_project_owner = undef;
1960 sub git_get_project_list_from_file
{
1962 return if (defined $gitweb_project_owner);
1964 $gitweb_project_owner = {};
1965 # read from file (url-encoded):
1966 # 'git%2Fgit.git Linus+Torvalds'
1967 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1968 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1969 if (-f
$projects_list) {
1970 open (my $fd , $projects_list);
1971 while (my $line = <$fd>) {
1973 my ($pr, $ow) = split ' ', $line;
1974 $pr = unescape
($pr);
1975 $ow = unescape
($ow);
1976 $gitweb_project_owner->{$pr} = to_utf8
($ow);
1982 sub git_get_project_owner
{
1983 my $project = shift;
1986 return undef unless $project;
1987 $git_dir = "$projectroot/$project";
1989 if (!defined $gitweb_project_owner) {
1990 git_get_project_list_from_file
();
1993 if (exists $gitweb_project_owner->{$project}) {
1994 $owner = $gitweb_project_owner->{$project};
1996 if (!defined $owner){
1997 $owner = git_get_project_config
('owner');
1999 if (!defined $owner) {
2000 $owner = get_file_owner
("$git_dir");
2006 sub git_get_last_activity
{
2010 $git_dir = "$projectroot/$path";
2011 open($fd, "-|", git_cmd
(), 'for-each-ref',
2012 '--format=%(committer)',
2013 '--sort=-committerdate',
2015 'refs/heads') or return;
2016 my $most_recent = <$fd>;
2017 close $fd or return;
2018 if (defined $most_recent &&
2019 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2021 my $age = time - $timestamp;
2022 return ($age, age_string
($age));
2024 return (undef, undef);
2027 sub git_get_references
{
2028 my $type = shift || "";
2030 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2031 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2032 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
2033 ($type ?
("--", "refs/$type") : ()) # use -- <pattern> if $type
2036 while (my $line = <$fd>) {
2038 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2039 if (defined $refs{$1}) {
2040 push @
{$refs{$1}}, $2;
2046 close $fd or return;
2050 sub git_get_rev_name_tags
{
2051 my $hash = shift || return undef;
2053 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
2055 my $name_rev = <$fd>;
2058 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2061 # catches also '$hash undefined' output
2066 ## ----------------------------------------------------------------------
2067 ## parse to hash functions
2071 my $tz = shift || "-0000";
2074 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2075 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2076 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2077 $date{'hour'} = $hour;
2078 $date{'minute'} = $min;
2079 $date{'mday'} = $mday;
2080 $date{'day'} = $days[$wday];
2081 $date{'month'} = $months[$mon];
2082 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2083 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2084 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2085 $mday, $months[$mon], $hour ,$min;
2086 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2087 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2089 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2090 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2091 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2092 $date{'hour_local'} = $hour;
2093 $date{'minute_local'} = $min;
2094 $date{'tz_local'} = $tz;
2095 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2096 1900+$year, $mon+1, $mday,
2097 $hour, $min, $sec, $tz);
2106 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2107 $tag{'id'} = $tag_id;
2108 while (my $line = <$fd>) {
2110 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2111 $tag{'object'} = $1;
2112 } elsif ($line =~ m/^type (.+)$/) {
2114 } elsif ($line =~ m/^tag (.+)$/) {
2116 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2117 $tag{'author'} = $1;
2120 } elsif ($line =~ m/--BEGIN/) {
2121 push @comment, $line;
2123 } elsif ($line eq "") {
2127 push @comment, <$fd>;
2128 $tag{'comment'} = \
@comment;
2129 close $fd or return;
2130 if (!defined $tag{'name'}) {
2136 sub parse_commit_text
{
2137 my ($commit_text, $withparents) = @_;
2138 my @commit_lines = split '\n', $commit_text;
2141 pop @commit_lines; # Remove '\0'
2143 if (! @commit_lines) {
2147 my $header = shift @commit_lines;
2148 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2151 ($co{'id'}, my @parents) = split ' ', $header;
2152 while (my $line = shift @commit_lines) {
2153 last if $line eq "\n";
2154 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2156 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2158 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2160 $co{'author_epoch'} = $2;
2161 $co{'author_tz'} = $3;
2162 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2163 $co{'author_name'} = $1;
2164 $co{'author_email'} = $2;
2166 $co{'author_name'} = $co{'author'};
2168 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2169 $co{'committer'} = $1;
2170 $co{'committer_epoch'} = $2;
2171 $co{'committer_tz'} = $3;
2172 $co{'committer_name'} = $co{'committer'};
2173 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2174 $co{'committer_name'} = $1;
2175 $co{'committer_email'} = $2;
2177 $co{'committer_name'} = $co{'committer'};
2181 if (!defined $co{'tree'}) {
2184 $co{'parents'} = \
@parents;
2185 $co{'parent'} = $parents[0];
2187 foreach my $title (@commit_lines) {
2190 $co{'title'} = chop_str
($title, 80, 5);
2191 # remove leading stuff of merges to make the interesting part visible
2192 if (length($title) > 50) {
2193 $title =~ s/^Automatic //;
2194 $title =~ s/^merge (of|with) /Merge ... /i;
2195 if (length($title) > 50) {
2196 $title =~ s/(http|rsync):\/\///;
2198 if (length($title) > 50) {
2199 $title =~ s/(master|www|rsync)\.//;
2201 if (length($title) > 50) {
2202 $title =~ s/kernel.org:?//;
2204 if (length($title) > 50) {
2205 $title =~ s/\/pub\/scm//;
2208 $co{'title_short'} = chop_str
($title, 50, 5);
2212 if (! defined $co{'title'} || $co{'title'} eq "") {
2213 $co{'title'} = $co{'title_short'} = '(no commit message)';
2215 # remove added spaces
2216 foreach my $line (@commit_lines) {
2219 $co{'comment'} = \
@commit_lines;
2221 my $age = time - $co{'committer_epoch'};
2223 $co{'age_string'} = age_string
($age);
2224 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2225 if ($age > 60*60*24*7*2) {
2226 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2227 $co{'age_string_age'} = $co{'age_string'};
2229 $co{'age_string_date'} = $co{'age_string'};
2230 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2236 my ($commit_id) = @_;
2241 open my $fd, "-|", git_cmd
(), "rev-list",
2247 or die_error
(500, "Open git-rev-list failed");
2248 %co = parse_commit_text
(<$fd>, 1);
2255 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2263 open my $fd, "-|", git_cmd
(), "rev-list",
2266 ("--max-count=" . $maxcount),
2267 ("--skip=" . $skip),
2271 ($filename ?
($filename) : ())
2272 or die_error
(500, "Open git-rev-list failed");
2273 while (my $line = <$fd>) {
2274 my %co = parse_commit_text
($line);
2279 return wantarray ?
@cos : \
@cos;
2282 # parse line of git-diff-tree "raw" output
2283 sub parse_difftree_raw_line
{
2287 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2288 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2289 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2290 $res{'from_mode'} = $1;
2291 $res{'to_mode'} = $2;
2292 $res{'from_id'} = $3;
2294 $res{'status'} = $5;
2295 $res{'similarity'} = $6;
2296 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2297 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2299 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2302 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2303 # combined diff (for merge commit)
2304 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2305 $res{'nparents'} = length($1);
2306 $res{'from_mode'} = [ split(' ', $2) ];
2307 $res{'to_mode'} = pop @
{$res{'from_mode'}};
2308 $res{'from_id'} = [ split(' ', $3) ];
2309 $res{'to_id'} = pop @
{$res{'from_id'}};
2310 $res{'status'} = [ split('', $4) ];
2311 $res{'to_file'} = unquote
($5);
2313 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2314 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2315 $res{'commit'} = $1;
2318 return wantarray ?
%res : \
%res;
2321 # wrapper: return parsed line of git-diff-tree "raw" output
2322 # (the argument might be raw line, or parsed info)
2323 sub parsed_difftree_line
{
2324 my $line_or_ref = shift;
2326 if (ref($line_or_ref) eq "HASH") {
2327 # pre-parsed (or generated by hand)
2328 return $line_or_ref;
2330 return parse_difftree_raw_line
($line_or_ref);
2334 # parse line of git-ls-tree output
2335 sub parse_ls_tree_line
($;%) {
2340 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2341 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2349 $res{'name'} = unquote
($4);
2352 return wantarray ?
%res : \
%res;
2355 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2356 sub parse_from_to_diffinfo
{
2357 my ($diffinfo, $from, $to, @parents) = @_;
2359 if ($diffinfo->{'nparents'}) {
2361 $from->{'file'} = [];
2362 $from->{'href'} = [];
2363 fill_from_file_info
($diffinfo, @parents)
2364 unless exists $diffinfo->{'from_file'};
2365 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2366 $from->{'file'}[$i] =
2367 defined $diffinfo->{'from_file'}[$i] ?
2368 $diffinfo->{'from_file'}[$i] :
2369 $diffinfo->{'to_file'};
2370 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2371 $from->{'href'}[$i] = href
(action
=>"blob",
2372 hash_base
=>$parents[$i],
2373 hash
=>$diffinfo->{'from_id'}[$i],
2374 file_name
=>$from->{'file'}[$i]);
2376 $from->{'href'}[$i] = undef;
2380 # ordinary (not combined) diff
2381 $from->{'file'} = $diffinfo->{'from_file'};
2382 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2383 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2384 hash
=>$diffinfo->{'from_id'},
2385 file_name
=>$from->{'file'});
2387 delete $from->{'href'};
2391 $to->{'file'} = $diffinfo->{'to_file'};
2392 if (!is_deleted
($diffinfo)) { # file exists in result
2393 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2394 hash
=>$diffinfo->{'to_id'},
2395 file_name
=>$to->{'file'});
2397 delete $to->{'href'};
2401 ## ......................................................................
2402 ## parse to array of hashes functions
2404 sub git_get_heads_list
{
2408 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2409 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
2410 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2413 while (my $line = <$fd>) {
2417 my ($refinfo, $committerinfo) = split(/\0/, $line);
2418 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2419 my ($committer, $epoch, $tz) =
2420 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2421 $ref_item{'fullname'} = $name;
2422 $name =~ s!^refs/heads/!!;
2424 $ref_item{'name'} = $name;
2425 $ref_item{'id'} = $hash;
2426 $ref_item{'title'} = $title || '(no commit message)';
2427 $ref_item{'epoch'} = $epoch;
2429 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2431 $ref_item{'age'} = "unknown";
2434 push @headslist, \
%ref_item;
2438 return wantarray ?
@headslist : \
@headslist;
2441 sub git_get_tags_list
{
2445 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2446 ($limit ?
'--count='.($limit+1) : ()), '--sort=-creatordate',
2447 '--format=%(objectname) %(objecttype) %(refname) '.
2448 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2451 while (my $line = <$fd>) {
2455 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2456 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2457 my ($creator, $epoch, $tz) =
2458 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2459 $ref_item{'fullname'} = $name;
2460 $name =~ s!^refs/tags/!!;
2462 $ref_item{'type'} = $type;
2463 $ref_item{'id'} = $id;
2464 $ref_item{'name'} = $name;
2465 if ($type eq "tag") {
2466 $ref_item{'subject'} = $title;
2467 $ref_item{'reftype'} = $reftype;
2468 $ref_item{'refid'} = $refid;
2470 $ref_item{'reftype'} = $type;
2471 $ref_item{'refid'} = $id;
2474 if ($type eq "tag" || $type eq "commit") {
2475 $ref_item{'epoch'} = $epoch;
2477 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2479 $ref_item{'age'} = "unknown";
2483 push @tagslist, \
%ref_item;
2487 return wantarray ?
@tagslist : \
@tagslist;
2490 ## ----------------------------------------------------------------------
2491 ## filesystem-related functions
2493 sub get_file_owner
{
2496 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2497 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2498 if (!defined $gcos) {
2502 $owner =~ s/[,;].*$//;
2503 return to_utf8
($owner);
2506 ## ......................................................................
2507 ## mimetype related functions
2509 sub mimetype_guess_file
{
2510 my $filename = shift;
2511 my $mimemap = shift;
2512 -r
$mimemap or return undef;
2515 open(MIME
, $mimemap) or return undef;
2517 next if m/^#/; # skip comments
2518 my ($mime, $exts) = split(/\t+/);
2519 if (defined $exts) {
2520 my @exts = split(/\s+/, $exts);
2521 foreach my $ext (@exts) {
2522 $mimemap{$ext} = $mime;
2528 $filename =~ /\.([^.]*)$/;
2529 return $mimemap{$1};
2532 sub mimetype_guess
{
2533 my $filename = shift;
2535 $filename =~ /\./ or return undef;
2537 if ($mimetypes_file) {
2538 my $file = $mimetypes_file;
2539 if ($file !~ m!^/!) { # if it is relative path
2540 # it is relative to project
2541 $file = "$projectroot/$project/$file";
2543 $mime = mimetype_guess_file
($filename, $file);
2545 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2551 my $filename = shift;
2554 my $mime = mimetype_guess
($filename);
2555 $mime and return $mime;
2559 return $default_blob_plain_mimetype unless $fd;
2562 return 'text/plain';
2563 } elsif (! $filename) {
2564 return 'application/octet-stream';
2565 } elsif ($filename =~ m/\.png$/i) {
2567 } elsif ($filename =~ m/\.gif$/i) {
2569 } elsif ($filename =~ m/\.jpe?g$/i) {
2570 return 'image/jpeg';
2572 return 'application/octet-stream';
2576 sub blob_contenttype
{
2577 my ($fd, $file_name, $type) = @_;
2579 $type ||= blob_mimetype
($fd, $file_name);
2580 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2581 $type .= "; charset=$default_text_plain_charset";
2587 ## ======================================================================
2588 ## functions printing HTML: header, footer, error page
2590 sub git_header_html
{
2591 my $status = shift || "200 OK";
2592 my $expires = shift;
2594 my $title = "$site_name";
2595 if (defined $project) {
2596 $title .= " - " . to_utf8
($project);
2597 if (defined $action) {
2598 $title .= "/$action";
2599 if (defined $file_name) {
2600 $title .= " - " . esc_path
($file_name);
2601 if ($action eq "tree" && $file_name !~ m
|/$|) {
2608 # require explicit support from the UA if we are to send the page as
2609 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2610 # we have to do this because MSIE sometimes globs '*/*', pretending to
2611 # support xhtml+xml but choking when it gets what it asked for.
2612 if (defined $cgi->http('HTTP_ACCEPT') &&
2613 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2614 $cgi->Accept('application/xhtml+xml') != 0) {
2615 $content_type = 'application/xhtml+xml';
2617 $content_type = 'text/html';
2619 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2620 -status
=> $status, -expires
=> $expires);
2621 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
2623 <?xml version="1.0" encoding="utf-8"?>
2624 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2625 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2626 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2627 <!-- git core binaries version $git_version -->
2629 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2630 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2631 <meta name="robots" content="index, nofollow"/>
2632 <title>$title</title>
2634 # print out each stylesheet that exist
2635 if (defined $stylesheet) {
2636 #provides backwards capability for those people who define style sheet in a config file
2637 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2639 foreach my $stylesheet (@stylesheets) {
2640 next unless $stylesheet;
2641 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2644 if (defined $project) {
2645 my %href_params = get_feed_info
();
2646 if (!exists $href_params{'-title'}) {
2647 $href_params{'-title'} = 'log';
2650 foreach my $format qw(RSS Atom) {
2651 my $type = lc($format);
2653 '-rel' => 'alternate',
2654 '-title' => "$project - $href_params{'-title'} - $format feed",
2655 '-type' => "application/$type+xml"
2658 $href_params{'action'} = $type;
2659 $link_attr{'-href'} = href
(%href_params);
2661 "rel=\"$link_attr{'-rel'}\" ".
2662 "title=\"$link_attr{'-title'}\" ".
2663 "href=\"$link_attr{'-href'}\" ".
2664 "type=\"$link_attr{'-type'}\" ".
2667 $href_params{'extra_options'} = '--no-merges';
2668 $link_attr{'-href'} = href
(%href_params);
2669 $link_attr{'-title'} .= ' (no merges)';
2671 "rel=\"$link_attr{'-rel'}\" ".
2672 "title=\"$link_attr{'-title'}\" ".
2673 "href=\"$link_attr{'-href'}\" ".
2674 "type=\"$link_attr{'-type'}\" ".
2679 printf('<link rel="alternate" title="%s projects list" '.
2680 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2681 $site_name, href
(project
=>undef, action
=>"project_index"));
2682 printf('<link rel="alternate" title="%s projects feeds" '.
2683 'href="%s" type="text/x-opml" />'."\n",
2684 $site_name, href
(project
=>undef, action
=>"opml"));
2686 if (defined $favicon) {
2687 print qq(<link rel
="shortcut icon" href
="$favicon" type
="image/png" />\n);
2693 if (-f
$site_header) {
2694 open (my $fd, $site_header);
2699 print "<div class=\"page_header\">\n" .
2700 $cgi->a({-href
=> esc_url
($logo_url),
2701 -title
=> $logo_label},
2702 qq(<img src
="$logo" width
="72" height
="27" alt
="git" class="logo"/>));
2703 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2704 if (defined $project) {
2705 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2706 if (defined $action) {
2713 my ($have_search) = gitweb_check_feature
('search');
2714 if (defined $project && $have_search) {
2715 if (!defined $searchtext) {
2719 if (defined $hash_base) {
2720 $search_hash = $hash_base;
2721 } elsif (defined $hash) {
2722 $search_hash = $hash;
2724 $search_hash = "HEAD";
2726 my $action = $my_uri;
2727 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2728 if ($use_pathinfo) {
2729 $action .= "/".esc_url
($project);
2731 print $cgi->startform(-method
=> "get", -action
=> $action) .
2732 "<div class=\"search\">\n" .
2734 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2735 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2736 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2737 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2738 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2739 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2741 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2742 "<span title=\"Extended regular expression\">" .
2743 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2744 -checked
=> $search_use_regexp) .
2747 $cgi->end_form() . "\n";
2751 sub git_footer_html
{
2752 my $feed_class = 'rss_logo';
2754 print "<div class=\"page_footer\">\n";
2755 if (defined $project) {
2756 my $descr = git_get_project_description
($project);
2757 if (defined $descr) {
2758 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2761 my %href_params = get_feed_info
();
2762 if (!%href_params) {
2763 $feed_class .= ' generic';
2765 $href_params{'-title'} ||= 'log';
2767 foreach my $format qw(RSS Atom) {
2768 $href_params{'action'} = lc($format);
2769 print $cgi->a({-href
=> href
(%href_params),
2770 -title
=> "$href_params{'-title'} $format feed",
2771 -class => $feed_class}, $format)."\n";
2775 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2776 -class => $feed_class}, "OPML") . " ";
2777 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2778 -class => $feed_class}, "TXT") . "\n";
2780 print "</div>\n"; # class="page_footer"
2782 if (-f
$site_footer) {
2783 open (my $fd, $site_footer);
2792 # die_error(<http_status_code>, <error_message>)
2793 # Example: die_error(404, 'Hash not found')
2794 # By convention, use the following status codes (as defined in RFC 2616):
2795 # 400: Invalid or missing CGI parameters, or
2796 # requested object exists but has wrong type.
2797 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2798 # this server or project.
2799 # 404: Requested object/revision/project doesn't exist.
2800 # 500: The server isn't configured properly, or
2801 # an internal error occurred (e.g. failed assertions caused by bugs), or
2802 # an unknown error occurred (e.g. the git binary died unexpectedly).
2804 my $status = shift || 500;
2805 my $error = shift || "Internal server error";
2807 my %http_responses = (400 => '400 Bad Request',
2808 403 => '403 Forbidden',
2809 404 => '404 Not Found',
2810 500 => '500 Internal Server Error');
2811 git_header_html
($http_responses{$status});
2813 <div class="page_body">
2823 ## ----------------------------------------------------------------------
2824 ## functions printing or outputting HTML: navigation
2826 sub git_print_page_nav
{
2827 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2828 $extra = '' if !defined $extra; # pager or formats
2830 my @navs = qw(summary shortlog log commit commitdiff tree);
2832 @navs = grep { $_ ne $suppress } @navs;
2835 my %arg = map { $_ => {action
=>$_} } @navs;
2836 if (defined $head) {
2837 for (qw(commit commitdiff)) {
2838 $arg{$_}{'hash'} = $head;
2840 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2841 for (qw(shortlog log)) {
2842 $arg{$_}{'hash'} = $head;
2846 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2847 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2849 print "<div class=\"page_nav\">\n" .
2851 map { $_ eq $current ?
2852 $_ : $cgi->a({-href
=> href
(%{$arg{$_}})}, "$_")
2854 print "<br/>\n$extra<br/>\n" .
2858 sub format_paging_nav
{
2859 my ($action, $hash, $head, $page, $has_next_link) = @_;
2863 if ($hash ne $head || $page) {
2864 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2866 $paging_nav .= "HEAD";
2870 $paging_nav .= " ⋅ " .
2871 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2872 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2874 $paging_nav .= " ⋅ prev";
2877 if ($has_next_link) {
2878 $paging_nav .= " ⋅ " .
2879 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2880 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2882 $paging_nav .= " ⋅ next";
2888 ## ......................................................................
2889 ## functions printing or outputting HTML: div
2891 sub git_print_header_div
{
2892 my ($action, $title, $hash, $hash_base) = @_;
2895 $args{'action'} = $action;
2896 $args{'hash'} = $hash if $hash;
2897 $args{'hash_base'} = $hash_base if $hash_base;
2899 print "<div class=\"header\">\n" .
2900 $cgi->a({-href
=> href
(%args), -class => "title"},
2901 $title ?
$title : $action) .
2905 #sub git_print_authorship (\%) {
2906 sub git_print_authorship
{
2909 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
2910 print "<div class=\"author_date\">" .
2911 esc_html
($co->{'author_name'}) .
2913 if ($ad{'hour_local'} < 6) {
2914 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2915 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2917 printf(" (%02d:%02d %s)",
2918 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2923 sub git_print_page_path
{
2929 print "<div class=\"page_path\">";
2930 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
2931 -title
=> 'tree root'}, to_utf8
("[$project]"));
2933 if (defined $name) {
2934 my @dirname = split '/', $name;
2935 my $basename = pop @dirname;
2938 foreach my $dir (@dirname) {
2939 $fullname .= ($fullname ?
'/' : '') . $dir;
2940 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
2942 -title
=> $fullname}, esc_path
($dir));
2945 if (defined $type && $type eq 'blob') {
2946 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
2948 -title
=> $name}, esc_path
($basename));
2949 } elsif (defined $type && $type eq 'tree') {
2950 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
2952 -title
=> $name}, esc_path
($basename));
2955 print esc_path
($basename);
2958 print "<br/></div>\n";
2961 # sub git_print_log (\@;%) {
2962 sub git_print_log
($;%) {
2966 if ($opts{'-remove_title'}) {
2967 # remove title, i.e. first line of log
2970 # remove leading empty lines
2971 while (defined $log->[0] && $log->[0] eq "") {
2978 foreach my $line (@
$log) {
2979 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2982 if (! $opts{'-remove_signoff'}) {
2983 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
2986 # remove signoff lines
2993 # print only one empty line
2994 # do not print empty line after signoff
2996 next if ($empty || $signoff);
3002 print format_log_line_html
($line) . "<br/>\n";
3005 if ($opts{'-final_empty_line'}) {
3006 # end with single empty line
3007 print "<br/>\n" unless $empty;
3011 # return link target (what link points to)
3012 sub git_get_link_target
{
3017 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
3021 $link_target = <$fd>;
3026 return $link_target;
3029 # given link target, and the directory (basedir) the link is in,
3030 # return target of link relative to top directory (top tree);
3031 # return undef if it is not possible (including absolute links).
3032 sub normalize_link_target
{
3033 my ($link_target, $basedir, $hash_base) = @_;
3035 # we can normalize symlink target only if $hash_base is provided
3036 return unless $hash_base;
3038 # absolute symlinks (beginning with '/') cannot be normalized
3039 return if (substr($link_target, 0, 1) eq '/');
3041 # normalize link target to path from top (root) tree (dir)
3044 $path = $basedir . '/' . $link_target;
3046 # we are in top (root) tree (dir)
3047 $path = $link_target;
3050 # remove //, /./, and /../
3052 foreach my $part (split('/', $path)) {
3053 # discard '.' and ''
3054 next if (!$part || $part eq '.');
3056 if ($part eq '..') {
3060 # link leads outside repository (outside top dir)
3064 push @path_parts, $part;
3067 $path = join('/', @path_parts);
3072 # print tree entry (row of git_tree), but without encompassing <tr> element
3073 sub git_print_tree_entry
{
3074 my ($t, $basedir, $hash_base, $have_blame) = @_;
3077 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3079 # The format of a table row is: mode list link. Where mode is
3080 # the mode of the entry, list is the name of the entry, an href,
3081 # and link is the action links of the entry.
3083 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3084 if ($t->{'type'} eq "blob") {
3085 print "<td class=\"list\">" .
3086 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3087 file_name
=>"$basedir$t->{'name'}", %base_key),
3088 -class => "list"}, esc_path
($t->{'name'}));
3089 if (S_ISLNK
(oct $t->{'mode'})) {
3090 my $link_target = git_get_link_target
($t->{'hash'});
3092 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3093 if (defined $norm_target) {
3095 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3096 file_name
=>$norm_target),
3097 -title
=> $norm_target}, esc_path
($link_target));
3099 print " -> " . esc_path
($link_target);
3104 print "<td class=\"link\">";
3105 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3106 file_name
=>"$basedir$t->{'name'}", %base_key)},
3110 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3111 file_name
=>"$basedir$t->{'name'}", %base_key)},
3114 if (defined $hash_base) {
3116 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3117 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3121 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3122 file_name
=>"$basedir$t->{'name'}")},
3126 } elsif ($t->{'type'} eq "tree") {
3127 print "<td class=\"list\">";
3128 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3129 file_name
=>"$basedir$t->{'name'}", %base_key)},
3130 esc_path
($t->{'name'}));
3132 print "<td class=\"link\">";
3133 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3134 file_name
=>"$basedir$t->{'name'}", %base_key)},
3136 if (defined $hash_base) {
3138 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3139 file_name
=>"$basedir$t->{'name'}")},
3144 # unknown object: we can only present history for it
3145 # (this includes 'commit' object, i.e. submodule support)
3146 print "<td class=\"list\">" .
3147 esc_path
($t->{'name'}) .
3149 print "<td class=\"link\">";
3150 if (defined $hash_base) {
3151 print $cgi->a({-href
=> href
(action
=>"history",
3152 hash_base
=>$hash_base,
3153 file_name
=>"$basedir$t->{'name'}")},
3160 ## ......................................................................
3161 ## functions printing large fragments of HTML
3163 # get pre-image filenames for merge (combined) diff
3164 sub fill_from_file_info
{
3165 my ($diff, @parents) = @_;
3167 $diff->{'from_file'} = [ ];
3168 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3169 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3170 if ($diff->{'status'}[$i] eq 'R' ||
3171 $diff->{'status'}[$i] eq 'C') {
3172 $diff->{'from_file'}[$i] =
3173 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3180 # is current raw difftree line of file deletion
3182 my $diffinfo = shift;
3184 return $diffinfo->{'to_id'} eq ('0' x
40);
3187 # does patch correspond to [previous] difftree raw line
3188 # $diffinfo - hashref of parsed raw diff format
3189 # $patchinfo - hashref of parsed patch diff format
3190 # (the same keys as in $diffinfo)
3191 sub is_patch_split
{
3192 my ($diffinfo, $patchinfo) = @_;
3194 return defined $diffinfo && defined $patchinfo
3195 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3199 sub git_difftree_body
{
3200 my ($difftree, $hash, @parents) = @_;
3201 my ($parent) = $parents[0];
3202 my ($have_blame) = gitweb_check_feature
('blame');
3203 print "<div class=\"list_head\">\n";
3204 if ($#{$difftree} > 10) {
3205 print(($#{$difftree} + 1) . " files changed:\n");
3209 print "<table class=\"" .
3210 (@parents > 1 ?
"combined " : "") .
3213 # header only for combined diff in 'commitdiff' view
3214 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
3217 print "<thead><tr>\n" .
3218 "<th></th><th></th>\n"; # filename, patchN link
3219 for (my $i = 0; $i < @parents; $i++) {
3220 my $par = $parents[$i];
3222 $cgi->a({-href
=> href
(action
=>"commitdiff",
3223 hash
=>$hash, hash_parent
=>$par),
3224 -title
=> 'commitdiff to parent number ' .
3225 ($i+1) . ': ' . substr($par,0,7)},
3229 print "</tr></thead>\n<tbody>\n";
3234 foreach my $line (@
{$difftree}) {
3235 my $diff = parsed_difftree_line
($line);
3238 print "<tr class=\"dark\">\n";
3240 print "<tr class=\"light\">\n";
3244 if (exists $diff->{'nparents'}) { # combined diff
3246 fill_from_file_info
($diff, @parents)
3247 unless exists $diff->{'from_file'};
3249 if (!is_deleted
($diff)) {
3250 # file exists in the result (child) commit
3252 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3253 file_name
=>$diff->{'to_file'},
3255 -class => "list"}, esc_path
($diff->{'to_file'})) .
3259 esc_path
($diff->{'to_file'}) .
3263 if ($action eq 'commitdiff') {
3266 print "<td class=\"link\">" .
3267 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3272 my $has_history = 0;
3273 my $not_deleted = 0;
3274 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3275 my $hash_parent = $parents[$i];
3276 my $from_hash = $diff->{'from_id'}[$i];
3277 my $from_path = $diff->{'from_file'}[$i];
3278 my $status = $diff->{'status'}[$i];
3280 $has_history ||= ($status ne 'A');
3281 $not_deleted ||= ($status ne 'D');
3283 if ($status eq 'A') {
3284 print "<td class=\"link\" align=\"right\"> | </td>\n";
3285 } elsif ($status eq 'D') {
3286 print "<td class=\"link\">" .
3287 $cgi->a({-href
=> href
(action
=>"blob",
3290 file_name
=>$from_path)},
3294 if ($diff->{'to_id'} eq $from_hash) {
3295 print "<td class=\"link nochange\">";
3297 print "<td class=\"link\">";
3299 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3300 hash
=>$diff->{'to_id'},
3301 hash_parent
=>$from_hash,
3303 hash_parent_base
=>$hash_parent,
3304 file_name
=>$diff->{'to_file'},
3305 file_parent
=>$from_path)},
3311 print "<td class=\"link\">";
3313 print $cgi->a({-href
=> href
(action
=>"blob",
3314 hash
=>$diff->{'to_id'},
3315 file_name
=>$diff->{'to_file'},
3318 print " | " if ($has_history);
3321 print $cgi->a({-href
=> href
(action
=>"history",
3322 file_name
=>$diff->{'to_file'},
3329 next; # instead of 'else' clause, to avoid extra indent
3331 # else ordinary diff
3333 my ($to_mode_oct, $to_mode_str, $to_file_type);
3334 my ($from_mode_oct, $from_mode_str, $from_file_type);
3335 if ($diff->{'to_mode'} ne ('0' x
6)) {
3336 $to_mode_oct = oct $diff->{'to_mode'};
3337 if (S_ISREG
($to_mode_oct)) { # only for regular file
3338 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3340 $to_file_type = file_type
($diff->{'to_mode'});
3342 if ($diff->{'from_mode'} ne ('0' x
6)) {
3343 $from_mode_oct = oct $diff->{'from_mode'};
3344 if (S_ISREG
($to_mode_oct)) { # only for regular file
3345 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3347 $from_file_type = file_type
($diff->{'from_mode'});
3350 if ($diff->{'status'} eq "A") { # created
3351 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3352 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3353 $mode_chng .= "]</span>";
3355 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3356 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3357 -class => "list"}, esc_path
($diff->{'file'}));
3359 print "<td>$mode_chng</td>\n";
3360 print "<td class=\"link\">";
3361 if ($action eq 'commitdiff') {
3364 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3367 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3368 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3372 } elsif ($diff->{'status'} eq "D") { # deleted
3373 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3375 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3376 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3377 -class => "list"}, esc_path
($diff->{'file'}));
3379 print "<td>$mode_chng</td>\n";
3380 print "<td class=\"link\">";
3381 if ($action eq 'commitdiff') {
3384 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3387 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3388 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3391 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3392 file_name
=>$diff->{'file'})},
3395 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3396 file_name
=>$diff->{'file'})},
3400 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3401 my $mode_chnge = "";
3402 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3403 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3404 if ($from_file_type ne $to_file_type) {
3405 $mode_chnge .= " from $from_file_type to $to_file_type";
3407 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3408 if ($from_mode_str && $to_mode_str) {
3409 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3410 } elsif ($to_mode_str) {
3411 $mode_chnge .= " mode: $to_mode_str";
3414 $mode_chnge .= "]</span>\n";
3417 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3418 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3419 -class => "list"}, esc_path
($diff->{'file'}));
3421 print "<td>$mode_chnge</td>\n";
3422 print "<td class=\"link\">";
3423 if ($action eq 'commitdiff') {
3426 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3428 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3429 # "commit" view and modified file (not onlu mode changed)
3430 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3431 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3432 hash_base
=>$hash, hash_parent_base
=>$parent,
3433 file_name
=>$diff->{'file'})},
3437 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3438 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3441 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3442 file_name
=>$diff->{'file'})},
3445 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3446 file_name
=>$diff->{'file'})},
3450 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3451 my %status_name = ('R' => 'moved', 'C' => 'copied');
3452 my $nstatus = $status_name{$diff->{'status'}};
3454 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3455 # mode also for directories, so we cannot use $to_mode_str
3456 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3459 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3460 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3461 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3462 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3463 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3464 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3465 -class => "list"}, esc_path
($diff->{'from_file'})) .
3466 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3467 "<td class=\"link\">";
3468 if ($action eq 'commitdiff') {
3471 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3473 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3474 # "commit" view and modified file (not only pure rename or copy)
3475 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3476 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3477 hash_base
=>$hash, hash_parent_base
=>$parent,
3478 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3482 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3483 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3486 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3487 file_name
=>$diff->{'to_file'})},
3490 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3491 file_name
=>$diff->{'to_file'})},
3495 } # we should not encounter Unmerged (U) or Unknown (X) status
3498 print "</tbody>" if $has_header;
3502 sub git_patchset_body
{
3503 my ($fd, $difftree, $hash, @hash_parents) = @_;
3504 my ($hash_parent) = $hash_parents[0];
3506 my $is_combined = (@hash_parents > 1);
3508 my $patch_number = 0;
3514 print "<div class=\"patchset\">\n";
3516 # skip to first patch
3517 while ($patch_line = <$fd>) {
3520 last if ($patch_line =~ m/^diff /);
3524 while ($patch_line) {
3526 # parse "git diff" header line
3527 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3528 # $1 is from_name, which we do not use
3529 $to_name = unquote
($2);
3530 $to_name =~ s!^b/!!;
3531 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3532 # $1 is 'cc' or 'combined', which we do not use
3533 $to_name = unquote
($2);
3538 # check if current patch belong to current raw line
3539 # and parse raw git-diff line if needed
3540 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3541 # this is continuation of a split patch
3542 print "<div class=\"patch cont\">\n";
3544 # advance raw git-diff output if needed
3545 $patch_idx++ if defined $diffinfo;
3547 # read and prepare patch information
3548 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3550 # compact combined diff output can have some patches skipped
3551 # find which patch (using pathname of result) we are at now;
3553 while ($to_name ne $diffinfo->{'to_file'}) {
3554 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3555 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3556 "</div>\n"; # class="patch"
3561 last if $patch_idx > $#$difftree;
3562 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3566 # modifies %from, %to hashes
3567 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3569 # this is first patch for raw difftree line with $patch_idx index
3570 # we index @$difftree array from 0, but number patches from 1
3571 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3575 #assert($patch_line =~ m/^diff /) if DEBUG;
3576 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3578 # print "git diff" header
3579 print format_git_diff_header_line
($patch_line, $diffinfo,
3582 # print extended diff header
3583 print "<div class=\"diff extended_header\">\n";
3585 while ($patch_line = <$fd>) {
3588 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3590 print format_extended_diff_header_line
($patch_line, $diffinfo,
3593 print "</div>\n"; # class="diff extended_header"
3595 # from-file/to-file diff header
3596 if (! $patch_line) {
3597 print "</div>\n"; # class="patch"
3600 next PATCH
if ($patch_line =~ m/^diff /);
3601 #assert($patch_line =~ m/^---/) if DEBUG;
3603 my $last_patch_line = $patch_line;
3604 $patch_line = <$fd>;
3606 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3608 print format_diff_from_to_header
($last_patch_line, $patch_line,
3609 $diffinfo, \
%from, \
%to,
3614 while ($patch_line = <$fd>) {
3617 next PATCH
if ($patch_line =~ m/^diff /);
3619 print format_diff_line
($patch_line, \
%from, \
%to);
3623 print "</div>\n"; # class="patch"
3626 # for compact combined (--cc) format, with chunk and patch simpliciaction
3627 # patchset might be empty, but there might be unprocessed raw lines
3628 for (++$patch_idx if $patch_number > 0;
3629 $patch_idx < @
$difftree;
3631 # read and prepare patch information
3632 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3634 # generate anchor for "patch" links in difftree / whatchanged part
3635 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3636 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3637 "</div>\n"; # class="patch"
3642 if ($patch_number == 0) {
3643 if (@hash_parents > 1) {
3644 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3646 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3650 print "</div>\n"; # class="patchset"
3653 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3655 # fills project list info (age, description, owner, forks) for each
3656 # project in the list, removing invalid projects from returned list
3657 # NOTE: modifies $projlist, but does not remove entries from it
3658 sub fill_project_list_info
{
3659 my ($projlist, $check_forks) = @_;
3662 my $show_ctags = gitweb_check_feature
('ctags');
3664 foreach my $pr (@
$projlist) {
3665 my (@activity) = git_get_last_activity
($pr->{'path'});
3666 unless (@activity) {
3669 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3670 if (!defined $pr->{'descr'}) {
3671 my $descr = git_get_project_description
($pr->{'path'}) || "";
3672 $descr = to_utf8
($descr);
3673 $pr->{'descr_long'} = $descr;
3674 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3676 if (!defined $pr->{'owner'}) {
3677 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3680 my $pname = $pr->{'path'};
3681 if (($pname =~ s/\.git$//) &&
3682 ($pname !~ /\/$/) &&
3683 (-d
"$projectroot/$pname")) {
3684 $pr->{'forks'} = "-d $projectroot/$pname";
3689 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
3690 push @projects, $pr;
3696 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3697 # (changing $list), or generating 'sort by $name' replay link otherwise
3699 my ($str_sort, $name, $order, $key, $header, $list) = @_;
3701 $header ||= ucfirst($name);
3703 if ($order eq $name) {
3705 @
$list = sort {$a->{$key} cmp $b->{$key}} @
$list;
3707 @
$list = sort {$a->{$key} <=> $b->{$key}} @
$list;
3709 print "<th>$header</th>\n";
3712 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3713 -class => "header"}, $header) .
3718 sub print_sort_th_str
{
3719 print_sort_th
(1, @_);
3722 sub print_sort_th_num
{
3723 print_sort_th
(0, @_);
3726 sub git_project_list_body
{
3727 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3729 my ($check_forks) = gitweb_check_feature
('forks');
3730 my @projects = fill_project_list_info
($projlist, $check_forks);
3732 $order ||= $default_projects_order;
3733 $from = 0 unless defined $from;
3734 $to = $#projects if (!defined $to || $#projects < $to);
3736 my $show_ctags = gitweb_check_feature
('ctags');
3739 foreach my $p (@projects) {
3740 foreach my $ct (keys %{$p->{'ctags'}}) {
3741 $ctags{$ct} += $p->{'ctags'}->{$ct};
3744 my $cloud = git_populate_project_tagcloud
(\
%ctags);
3745 print git_show_project_tagcloud
($cloud, 64);
3748 print "<table class=\"project_list\">\n";
3749 unless ($no_header) {
3752 print "<th></th>\n";
3754 print_sort_th_str
('project', $order, 'path',
3755 'Project', \
@projects);
3756 print_sort_th_str
('descr', $order, 'descr_long',
3757 'Description', \
@projects);
3758 print_sort_th_str
('owner', $order, 'owner',
3759 'Owner', \
@projects);
3760 print_sort_th_num
('age', $order, 'age',
3761 'Last Change', \
@projects);
3762 print "<th></th>\n" . # for links
3766 my $tagfilter = $cgi->param('by_tag');
3767 for (my $i = $from; $i <= $to; $i++) {
3768 my $pr = $projects[$i];
3769 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3771 print "<tr class=\"dark\">\n";
3773 print "<tr class=\"light\">\n";
3778 if ($pr->{'forks'}) {
3779 print "<!-- $pr->{'forks'} -->\n";
3780 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3784 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3785 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3786 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3787 -class => "list", -title
=> $pr->{'descr_long'}},
3788 esc_html
($pr->{'descr'})) . "</td>\n" .
3789 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3790 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3791 (defined $pr->{'age_string'} ?
$pr->{'age_string'} : "No commits") . "</td>\n" .
3792 "<td class=\"link\">" .
3793 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3794 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3795 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3796 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3797 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3801 if (defined $extra) {
3804 print "<td></td>\n";
3806 print "<td colspan=\"5\">$extra</td>\n" .
3812 sub git_shortlog_body
{
3813 # uses global variable $project
3814 my ($commitlist, $from, $to, $refs, $extra) = @_;
3816 $from = 0 unless defined $from;
3817 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3819 print "<table class=\"shortlog\">\n";
3821 for (my $i = $from; $i <= $to; $i++) {
3822 my %co = %{$commitlist->[$i]};
3823 my $commit = $co{'id'};
3824 my $ref = format_ref_marker
($refs, $commit);
3826 print "<tr class=\"dark\">\n";
3828 print "<tr class=\"light\">\n";
3831 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3832 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3833 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3834 "<td><i>" . $author . "</i></td>\n" .
3836 print format_subject_html
($co{'title'}, $co{'title_short'},
3837 href
(action
=>"commit", hash
=>$commit), $ref);
3839 "<td class=\"link\">" .
3840 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3841 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3842 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3843 my $snapshot_links = format_snapshot_links
($commit);
3844 if (defined $snapshot_links) {
3845 print " | " . $snapshot_links;
3850 if (defined $extra) {
3852 "<td colspan=\"4\">$extra</td>\n" .
3858 sub git_history_body
{
3859 # Warning: assumes constant type (blob or tree) during history
3860 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3862 $from = 0 unless defined $from;
3863 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3865 print "<table class=\"history\">\n";
3867 for (my $i = $from; $i <= $to; $i++) {
3868 my %co = %{$commitlist->[$i]};
3872 my $commit = $co{'id'};
3874 my $ref = format_ref_marker
($refs, $commit);
3877 print "<tr class=\"dark\">\n";
3879 print "<tr class=\"light\">\n";
3882 # shortlog uses chop_str($co{'author_name'}, 10)
3883 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
3884 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3885 "<td><i>" . $author . "</i></td>\n" .
3887 # originally git_history used chop_str($co{'title'}, 50)
3888 print format_subject_html
($co{'title'}, $co{'title_short'},
3889 href
(action
=>"commit", hash
=>$commit), $ref);
3891 "<td class=\"link\">" .
3892 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
3893 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
3895 if ($ftype eq 'blob') {
3896 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
3897 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
3898 if (defined $blob_current && defined $blob_parent &&
3899 $blob_current ne $blob_parent) {
3901 $cgi->a({-href
=> href
(action
=>"blobdiff",
3902 hash
=>$blob_current, hash_parent
=>$blob_parent,
3903 hash_base
=>$hash_base, hash_parent_base
=>$commit,
3904 file_name
=>$file_name)},
3911 if (defined $extra) {
3913 "<td colspan=\"4\">$extra</td>\n" .
3920 # uses global variable $project
3921 my ($taglist, $from, $to, $extra) = @_;
3922 $from = 0 unless defined $from;
3923 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3925 print "<table class=\"tags\">\n";
3927 for (my $i = $from; $i <= $to; $i++) {
3928 my $entry = $taglist->[$i];
3930 my $comment = $tag{'subject'};
3932 if (defined $comment) {
3933 $comment_short = chop_str
($comment, 30, 5);
3936 print "<tr class=\"dark\">\n";
3938 print "<tr class=\"light\">\n";
3941 if (defined $tag{'age'}) {
3942 print "<td><i>$tag{'age'}</i></td>\n";
3944 print "<td></td>\n";
3947 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
3948 -class => "list name"}, esc_html
($tag{'name'})) .
3951 if (defined $comment) {
3952 print format_subject_html
($comment, $comment_short,
3953 href
(action
=>"tag", hash
=>$tag{'id'}));
3956 "<td class=\"selflink\">";
3957 if ($tag{'type'} eq "tag") {
3958 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
3963 "<td class=\"link\">" . " | " .
3964 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
3965 if ($tag{'reftype'} eq "commit") {
3966 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
3967 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
3968 } elsif ($tag{'reftype'} eq "blob") {
3969 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
3974 if (defined $extra) {
3976 "<td colspan=\"5\">$extra</td>\n" .
3982 sub git_heads_body
{
3983 # uses global variable $project
3984 my ($headlist, $head, $from, $to, $extra) = @_;
3985 $from = 0 unless defined $from;
3986 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3988 print "<table class=\"heads\">\n";
3990 for (my $i = $from; $i <= $to; $i++) {
3991 my $entry = $headlist->[$i];
3993 my $curr = $ref{'id'} eq $head;
3995 print "<tr class=\"dark\">\n";
3997 print "<tr class=\"light\">\n";
4000 print "<td><i>$ref{'age'}</i></td>\n" .
4001 ($curr ?
"<td class=\"current_head\">" : "<td>") .
4002 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
4003 -class => "list name"},esc_html
($ref{'name'})) .
4005 "<td class=\"link\">" .
4006 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
4007 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
4008 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
4012 if (defined $extra) {
4014 "<td colspan=\"3\">$extra</td>\n" .
4020 sub git_search_grep_body
{
4021 my ($commitlist, $from, $to, $extra) = @_;
4022 $from = 0 unless defined $from;
4023 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4025 print "<table class=\"commit_search\">\n";
4027 for (my $i = $from; $i <= $to; $i++) {
4028 my %co = %{$commitlist->[$i]};
4032 my $commit = $co{'id'};
4034 print "<tr class=\"dark\">\n";
4036 print "<tr class=\"light\">\n";
4039 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
4040 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4041 "<td><i>" . $author . "</i></td>\n" .
4043 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
4044 -class => "list subject"},
4045 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
4046 my $comment = $co{'comment'};
4047 foreach my $line (@
$comment) {
4048 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4049 my ($lead, $match, $trail) = ($1, $2, $3);
4050 $match = chop_str
($match, 70, 5, 'center');
4051 my $contextlen = int((80 - length($match))/2);
4052 $contextlen = 30 if ($contextlen > 30);
4053 $lead = chop_str
($lead, $contextlen, 10, 'left');
4054 $trail = chop_str
($trail, $contextlen, 10, 'right');
4056 $lead = esc_html
($lead);
4057 $match = esc_html
($match);
4058 $trail = esc_html
($trail);
4060 print "$lead<span class=\"match\">$match</span>$trail<br />";
4064 "<td class=\"link\">" .
4065 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4067 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4069 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4073 if (defined $extra) {
4075 "<td colspan=\"3\">$extra</td>\n" .
4081 ## ======================================================================
4082 ## ======================================================================
4085 sub git_project_list
{
4086 my $order = $cgi->param('o');
4087 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4088 die_error
(400, "Unknown order parameter");
4091 my @list = git_get_projects_list
();
4093 die_error
(404, "No projects found");
4097 if (-f
$home_text) {
4098 print "<div class=\"index_include\">\n";
4099 open (my $fd, $home_text);
4104 git_project_list_body
(\
@list, $order);
4109 my $order = $cgi->param('o');
4110 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4111 die_error
(400, "Unknown order parameter");
4114 my @list = git_get_projects_list
($project);
4116 die_error
(404, "No forks found");
4120 git_print_page_nav
('','');
4121 git_print_header_div
('summary', "$project forks");
4122 git_project_list_body
(\
@list, $order);
4126 sub git_project_index
{
4127 my @projects = git_get_projects_list
($project);
4130 -type
=> 'text/plain',
4131 -charset
=> 'utf-8',
4132 -content_disposition
=> 'inline; filename="index.aux"');
4134 foreach my $pr (@projects) {
4135 if (!exists $pr->{'owner'}) {
4136 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4139 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4140 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4141 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4142 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4146 print "$path $owner\n";
4151 my $descr = git_get_project_description
($project) || "none";
4152 my %co = parse_commit
("HEAD");
4153 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4154 my $head = $co{'id'};
4156 my $owner = git_get_project_owner
($project);
4158 my $refs = git_get_references
();
4159 # These get_*_list functions return one more to allow us to see if
4160 # there are more ...
4161 my @taglist = git_get_tags_list
(16);
4162 my @headlist = git_get_heads_list
(16);
4164 my ($check_forks) = gitweb_check_feature
('forks');
4167 @forklist = git_get_projects_list
($project);
4171 git_print_page_nav
('summary','', $head);
4173 print "<div class=\"title\"> </div>\n";
4174 print "<table class=\"projects_list\">\n" .
4175 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4176 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4177 if (defined $cd{'rfc2822'}) {
4178 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4181 # use per project git URL list in $projectroot/$project/cloneurl
4182 # or make project git URL from git base URL and project name
4183 my $url_tag = "URL";
4184 my @url_list = git_get_project_url_list
($project);
4185 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4186 foreach my $git_url (@url_list) {
4187 next unless $git_url;
4188 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4193 my $show_ctags = (gitweb_check_feature
('ctags'))[0];
4195 my $ctags = git_get_project_ctags
($project);
4196 my $cloud = git_populate_project_tagcloud
($ctags);
4197 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4198 print "</td>\n<td>" unless %$ctags;
4199 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4200 print "</td>\n<td>" if %$ctags;
4201 print git_show_project_tagcloud
($cloud, 48);
4207 if (-s
"$projectroot/$project/README.html") {
4208 if (open my $fd, "$projectroot/$project/README.html") {
4209 print "<div class=\"title\">readme</div>\n" .
4210 "<div class=\"readme\">\n";
4211 print $_ while (<$fd>);
4212 print "\n</div>\n"; # class="readme"
4217 # we need to request one more than 16 (0..15) to check if
4219 my @commitlist = $head ? parse_commits
($head, 17) : ();
4221 git_print_header_div
('shortlog');
4222 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4223 $#commitlist <= 15 ?
undef :
4224 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4228 git_print_header_div
('tags');
4229 git_tags_body
(\
@taglist, 0, 15,
4230 $#taglist <= 15 ?
undef :
4231 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4235 git_print_header_div
('heads');
4236 git_heads_body
(\
@headlist, $head, 0, 15,
4237 $#headlist <= 15 ?
undef :
4238 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4242 git_print_header_div
('forks');
4243 git_project_list_body
(\
@forklist, undef, 0, 15,
4244 $#forklist <= 15 ?
undef :
4245 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4253 my $head = git_get_head_hash
($project);
4255 git_print_page_nav
('','', $head,undef,$head);
4256 my %tag = parse_tag
($hash);
4259 die_error
(404, "Unknown tag object");
4262 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4263 print "<div class=\"title_text\">\n" .
4264 "<table class=\"object_header\">\n" .
4266 "<td>object</td>\n" .
4267 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4268 $tag{'object'}) . "</td>\n" .
4269 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4270 $tag{'type'}) . "</td>\n" .
4272 if (defined($tag{'author'})) {
4273 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4274 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4275 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4276 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4279 print "</table>\n\n" .
4281 print "<div class=\"page_body\">";
4282 my $comment = $tag{'comment'};
4283 foreach my $line (@
$comment) {
4285 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4295 gitweb_check_feature
('blame')
4296 or die_error
(403, "Blame view not allowed");
4298 die_error
(400, "No file name given") unless $file_name;
4299 $hash_base ||= git_get_head_hash
($project);
4300 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4301 my %co = parse_commit
($hash_base)
4302 or die_error
(404, "Commit not found");
4303 if (!defined $hash) {
4304 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4305 or die_error
(404, "Error looking up file");
4307 $ftype = git_get_type
($hash);
4308 if ($ftype !~ "blob") {
4309 die_error
(400, "Object is not a blob");
4311 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4312 $file_name, $hash_base)
4313 or die_error
(500, "Open git-blame failed");
4316 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4319 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4322 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4324 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4325 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4326 git_print_page_path
($file_name, $ftype, $hash_base);
4327 my @rev_color = (qw(light2 dark2));
4328 my $num_colors = scalar(@rev_color);
4329 my $current_color = 0;
4332 <div class="page_body">
4333 <table class="blame">
4334 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4339 last unless defined $_;
4340 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4341 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4342 if (!exists $metainfo{$full_rev}) {
4343 $metainfo{$full_rev} = {};
4345 my $meta = $metainfo{$full_rev};
4348 if (/^(\S+) (.*)$/) {
4354 my $rev = substr($full_rev, 0, 8);
4355 my $author = $meta->{'author'};
4356 my %date = parse_date
($meta->{'author-time'},
4357 $meta->{'author-tz'});
4358 my $date = $date{'iso-tz'};
4360 $current_color = ++$current_color % $num_colors;
4362 print "<tr class=\"$rev_color[$current_color]\">\n";
4364 print "<td class=\"sha1\"";
4365 print " title=\"". esc_html
($author) . ", $date\"";
4366 print " rowspan=\"$group_size\"" if ($group_size > 1);
4368 print $cgi->a({-href
=> href
(action
=>"commit",
4370 file_name
=>$file_name)},
4374 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4375 or die_error
(500, "Open git-rev-parse failed");
4376 my $parent_commit = <$dd>;
4378 chomp($parent_commit);
4379 my $blamed = href
(action
=> 'blame',
4380 file_name
=> $meta->{'filename'},
4381 hash_base
=> $parent_commit);
4382 print "<td class=\"linenr\">";
4383 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4385 -class => "linenr" },
4388 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4394 or print "Reading blob failed\n";
4399 my $head = git_get_head_hash
($project);
4401 git_print_page_nav
('','', $head,undef,$head);
4402 git_print_header_div
('summary', $project);
4404 my @tagslist = git_get_tags_list
();
4406 git_tags_body
(\
@tagslist);
4412 my $head = git_get_head_hash
($project);
4414 git_print_page_nav
('','', $head,undef,$head);
4415 git_print_header_div
('summary', $project);
4417 my @headslist = git_get_heads_list
();
4419 git_heads_body
(\
@headslist, $head);
4424 sub git_blob_plain
{
4428 if (!defined $hash) {
4429 if (defined $file_name) {
4430 my $base = $hash_base || git_get_head_hash
($project);
4431 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4432 or die_error
(404, "Cannot find file");
4434 die_error
(400, "No file name defined");
4436 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4437 # blobs defined by non-textual hash id's can be cached
4441 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4442 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4444 # content-type (can include charset)
4445 $type = blob_contenttype
($fd, $file_name, $type);
4447 # "save as" filename, even when no $file_name is given
4448 my $save_as = "$hash";
4449 if (defined $file_name) {
4450 $save_as = $file_name;
4451 } elsif ($type =~ m/^text\//) {
4457 -expires
=> $expires,
4458 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4460 binmode STDOUT
, ':raw';
4462 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4470 if (!defined $hash) {
4471 if (defined $file_name) {
4472 my $base = $hash_base || git_get_head_hash
($project);
4473 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4474 or die_error
(404, "Cannot find file");
4476 die_error
(400, "No file name defined");
4478 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4479 # blobs defined by non-textual hash id's can be cached
4483 my ($have_blame) = gitweb_check_feature
('blame');
4484 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4485 or die_error
(500, "Couldn't cat $file_name, $hash");
4486 my $mimetype = blob_mimetype
($fd, $file_name);
4487 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4489 return git_blob_plain
($mimetype);
4491 # we can have blame only for text/* mimetype
4492 $have_blame &&= ($mimetype =~ m!^text/!);
4494 git_header_html
(undef, $expires);
4495 my $formats_nav = '';
4496 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4497 if (defined $file_name) {
4500 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4505 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4508 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4511 $cgi->a({-href
=> href
(action
=>"blob",
4512 hash_base
=>"HEAD", file_name
=>$file_name)},
4516 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4519 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4520 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4522 print "<div class=\"page_nav\">\n" .
4523 "<br/><br/></div>\n" .
4524 "<div class=\"title\">$hash</div>\n";
4526 git_print_page_path
($file_name, "blob", $hash_base);
4527 print "<div class=\"page_body\">\n";
4528 if ($mimetype =~ m!^image/!) {
4529 print qq!<img type
="$mimetype"!;
4531 print qq! alt
="$file_name" title
="$file_name"!;
4534 href(action=>"blob_plain
", hash=>$hash,
4535 hash_base=>$hash_base, file_name=>$file_name) .
4539 while (my $line = <$fd>) {
4542 $line = untabify
($line);
4543 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4544 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4548 or print "Reading blob failed.\n";
4554 if (!defined $hash_base) {
4555 $hash_base = "HEAD";
4557 if (!defined $hash) {
4558 if (defined $file_name) {
4559 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4564 die_error
(404, "No such tree") unless defined($hash);
4566 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4567 or die_error
(500, "Open git-ls-tree failed");
4568 my @entries = map { chomp; $_ } <$fd>;
4569 close $fd or die_error
(404, "Reading tree failed");
4572 my $refs = git_get_references
();
4573 my $ref = format_ref_marker
($refs, $hash_base);
4576 my ($have_blame) = gitweb_check_feature
('blame');
4577 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4579 if (defined $file_name) {
4581 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4583 $cgi->a({-href
=> href
(action
=>"tree",
4584 hash_base
=>"HEAD", file_name
=>$file_name)},
4587 my $snapshot_links = format_snapshot_links
($hash);
4588 if (defined $snapshot_links) {
4589 # FIXME: Should be available when we have no hash base as well.
4590 push @views_nav, $snapshot_links;
4592 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4593 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4596 print "<div class=\"page_nav\">\n";
4597 print "<br/><br/></div>\n";
4598 print "<div class=\"title\">$hash</div>\n";
4600 if (defined $file_name) {
4601 $basedir = $file_name;
4602 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4605 git_print_page_path
($file_name, 'tree', $hash_base);
4607 print "<div class=\"page_body\">\n";
4608 print "<table class=\"tree\">\n";
4610 # '..' (top directory) link if possible
4611 if (defined $hash_base &&
4612 defined $file_name && $file_name =~ m![^/]+$!) {
4614 print "<tr class=\"dark\">\n";
4616 print "<tr class=\"light\">\n";
4620 my $up = $file_name;
4621 $up =~ s!/?[^/]+$!!;
4622 undef $up unless $up;
4623 # based on git_print_tree_entry
4624 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4625 print '<td class="list">';
4626 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4630 print "<td class=\"link\"></td>\n";
4634 foreach my $line (@entries) {
4635 my %t = parse_ls_tree_line
($line, -z
=> 1);
4638 print "<tr class=\"dark\">\n";
4640 print "<tr class=\"light\">\n";
4644 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4648 print "</table>\n" .
4654 my @supported_fmts = gitweb_check_feature
('snapshot');
4655 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4657 my $format = $cgi->param('sf');
4658 if (!@supported_fmts) {
4659 die_error
(403, "Snapshots not allowed");
4661 # default to first supported snapshot format
4662 $format ||= $supported_fmts[0];
4663 if ($format !~ m/^[a-z0-9]+$/) {
4664 die_error
(400, "Invalid snapshot format parameter");
4665 } elsif (!exists($known_snapshot_formats{$format})) {
4666 die_error
(400, "Unknown snapshot format");
4667 } elsif (!grep($_ eq $format, @supported_fmts)) {
4668 die_error
(403, "Unsupported snapshot format");
4671 if (!defined $hash) {
4672 $hash = git_get_head_hash
($project);
4675 my $name = $project;
4676 $name =~ s
,([^/])/*\
.git
$,$1,;
4677 $name = basename
($name);
4678 my $filename = to_utf8
($name);
4679 $name =~ s/\047/\047\\\047\047/g;
4681 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4682 $cmd = quote_command
(
4683 git_cmd
(), 'archive',
4684 "--format=$known_snapshot_formats{$format}{'format'}",
4685 "--prefix=$name/", $hash);
4686 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4687 $cmd .= ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}});
4691 -type
=> $known_snapshot_formats{$format}{'type'},
4692 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4693 -status
=> '200 OK');
4695 open my $fd, "-|", $cmd
4696 or die_error
(500, "Execute git-archive failed");
4697 binmode STDOUT
, ':raw';
4699 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4704 my $head = git_get_head_hash
($project);
4705 if (!defined $hash) {
4708 if (!defined $page) {
4711 my $refs = git_get_references
();
4713 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4715 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4718 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4721 my %co = parse_commit
($hash);
4723 git_print_header_div
('summary', $project);
4724 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4726 my $to = ($#commitlist >= 99) ?
(99) : ($#commitlist);
4727 for (my $i = 0; $i <= $to; $i++) {
4728 my %co = %{$commitlist[$i]};
4730 my $commit = $co{'id'};
4731 my $ref = format_ref_marker
($refs, $commit);
4732 my %ad = parse_date
($co{'author_epoch'});
4733 git_print_header_div
('commit',
4734 "<span class=\"age\">$co{'age_string'}</span>" .
4735 esc_html
($co{'title'}) . $ref,
4737 print "<div class=\"title_text\">\n" .
4738 "<div class=\"log_link\">\n" .
4739 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4741 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4743 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4746 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4749 print "<div class=\"log_body\">\n";
4750 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4753 if ($#commitlist >= 100) {
4754 print "<div class=\"page_nav\">\n";
4755 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4756 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4763 $hash ||= $hash_base || "HEAD";
4764 my %co = parse_commit
($hash)
4765 or die_error
(404, "Unknown commit object");
4766 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4767 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4769 my $parent = $co{'parent'};
4770 my $parents = $co{'parents'}; # listref
4772 # we need to prepare $formats_nav before any parameter munging
4774 if (!defined $parent) {
4776 $formats_nav .= '(initial)';
4777 } elsif (@
$parents == 1) {
4778 # single parent commit
4781 $cgi->a({-href
=> href
(action
=>"commit",
4783 esc_html
(substr($parent, 0, 7))) .
4790 $cgi->a({-href
=> href
(action
=>"commit",
4792 esc_html
(substr($_, 0, 7)));
4797 if (!defined $parent) {
4801 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4803 (@
$parents <= 1 ?
$parent : '-c'),
4805 or die_error
(500, "Open git-diff-tree failed");
4806 @difftree = map { chomp; $_ } <$fd>;
4807 close $fd or die_error
(404, "Reading git-diff-tree failed");
4809 # non-textual hash id's can be cached
4811 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4814 my $refs = git_get_references
();
4815 my $ref = format_ref_marker
($refs, $co{'id'});
4817 git_header_html
(undef, $expires);
4818 git_print_page_nav
('commit', '',
4819 $hash, $co{'tree'}, $hash,
4822 if (defined $co{'parent'}) {
4823 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4825 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4827 print "<div class=\"title_text\">\n" .
4828 "<table class=\"object_header\">\n";
4829 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4831 "<td></td><td> $ad{'rfc2822'}";
4832 if ($ad{'hour_local'} < 6) {
4833 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4834 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4836 printf(" (%02d:%02d %s)",
4837 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4841 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4842 print "<tr><td></td><td> $cd{'rfc2822'}" .
4843 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4845 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4848 "<td class=\"sha1\">" .
4849 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4850 class => "list"}, $co{'tree'}) .
4852 "<td class=\"link\">" .
4853 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4855 my $snapshot_links = format_snapshot_links
($hash);
4856 if (defined $snapshot_links) {
4857 print " | " . $snapshot_links;
4862 foreach my $par (@
$parents) {
4865 "<td class=\"sha1\">" .
4866 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4867 class => "list"}, $par) .
4869 "<td class=\"link\">" .
4870 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4872 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
4879 print "<div class=\"page_body\">\n";
4880 git_print_log
($co{'comment'});
4883 git_difftree_body
(\
@difftree, $hash, @
$parents);
4889 # object is defined by:
4890 # - hash or hash_base alone
4891 # - hash_base and file_name
4894 # - hash or hash_base alone
4895 if ($hash || ($hash_base && !defined $file_name)) {
4896 my $object_id = $hash || $hash_base;
4898 open my $fd, "-|", quote_command
(
4899 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4900 or die_error
(404, "Object does not exist");
4904 or die_error
(404, "Object does not exist");
4906 # - hash_base and file_name
4907 } elsif ($hash_base && defined $file_name) {
4908 $file_name =~ s
,/+$,,;
4910 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
4911 or die_error
(404, "Base object does not exist");
4913 # here errors should not hapen
4914 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
4915 or die_error
(500, "Open git-ls-tree failed");
4919 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4920 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4921 die_error
(404, "File or directory for given base does not exist");
4926 die_error
(400, "Not enough information to find object");
4929 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
4930 hash
=>$hash, hash_base
=>$hash_base,
4931 file_name
=>$file_name),
4932 -status
=> '302 Found');
4936 my $format = shift || 'html';
4943 # preparing $fd and %diffinfo for git_patchset_body
4945 if (defined $hash_base && defined $hash_parent_base) {
4946 if (defined $file_name) {
4948 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4949 $hash_parent_base, $hash_base,
4950 "--", (defined $file_parent ?
$file_parent : ()), $file_name
4951 or die_error
(500, "Open git-diff-tree failed");
4952 @difftree = map { chomp; $_ } <$fd>;
4954 or die_error
(404, "Reading git-diff-tree failed");
4956 or die_error
(404, "Blob diff not found");
4958 } elsif (defined $hash &&
4959 $hash =~ /[0-9a-fA-F]{40}/) {
4960 # try to find filename from $hash
4962 # read filtered raw output
4963 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4964 $hash_parent_base, $hash_base, "--"
4965 or die_error
(500, "Open git-diff-tree failed");
4967 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4969 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4970 map { chomp; $_ } <$fd>;
4972 or die_error
(404, "Reading git-diff-tree failed");
4974 or die_error
(404, "Blob diff not found");
4977 die_error
(400, "Missing one of the blob diff parameters");
4980 if (@difftree > 1) {
4981 die_error
(400, "Ambiguous blob diff specification");
4984 %diffinfo = parse_difftree_raw_line
($difftree[0]);
4985 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4986 $file_name ||= $diffinfo{'to_file'};
4988 $hash_parent ||= $diffinfo{'from_id'};
4989 $hash ||= $diffinfo{'to_id'};
4991 # non-textual hash id's can be cached
4992 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4993 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4998 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4999 '-p', ($format eq 'html' ?
"--full-index" : ()),
5000 $hash_parent_base, $hash_base,
5001 "--", (defined $file_parent ?
$file_parent : ()), $file_name
5002 or die_error
(500, "Open git-diff-tree failed");
5005 # old/legacy style URI
5006 if (!%diffinfo && # if new style URI failed
5007 defined $hash && defined $hash_parent) {
5008 # fake git-diff-tree raw output
5009 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5010 $diffinfo{'from_id'} = $hash_parent;
5011 $diffinfo{'to_id'} = $hash;
5012 if (defined $file_name) {
5013 if (defined $file_parent) {
5014 $diffinfo{'status'} = '2';
5015 $diffinfo{'from_file'} = $file_parent;
5016 $diffinfo{'to_file'} = $file_name;
5017 } else { # assume not renamed
5018 $diffinfo{'status'} = '1';
5019 $diffinfo{'from_file'} = $file_name;
5020 $diffinfo{'to_file'} = $file_name;
5022 } else { # no filename given
5023 $diffinfo{'status'} = '2';
5024 $diffinfo{'from_file'} = $hash_parent;
5025 $diffinfo{'to_file'} = $hash;
5028 # non-textual hash id's can be cached
5029 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5030 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5035 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
5036 '-p', ($format eq 'html' ?
"--full-index" : ()),
5037 $hash_parent, $hash, "--"
5038 or die_error
(500, "Open git-diff failed");
5040 die_error
(400, "Missing one of the blob diff parameters")
5045 if ($format eq 'html') {
5047 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5049 git_header_html
(undef, $expires);
5050 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5051 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5052 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5054 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5055 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5057 if (defined $file_name) {
5058 git_print_page_path
($file_name, "blob", $hash_base);
5060 print "<div class=\"page_path\"></div>\n";
5063 } elsif ($format eq 'plain') {
5065 -type
=> 'text/plain',
5066 -charset
=> 'utf-8',
5067 -expires
=> $expires,
5068 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
5070 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5073 die_error
(400, "Unknown blobdiff format");
5077 if ($format eq 'html') {
5078 print "<div class=\"page_body\">\n";
5080 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5083 print "</div>\n"; # class="page_body"
5087 while (my $line = <$fd>) {
5088 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5089 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5093 last if $line =~ m!^\+\+\+!;
5101 sub git_blobdiff_plain
{
5102 git_blobdiff
('plain');
5105 sub git_commitdiff
{
5106 my $format = shift || 'html';
5107 $hash ||= $hash_base || "HEAD";
5108 my %co = parse_commit
($hash)
5109 or die_error
(404, "Unknown commit object");
5111 # choose format for commitdiff for merge
5112 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
5113 $hash_parent = '--cc';
5115 # we need to prepare $formats_nav before almost any parameter munging
5117 if ($format eq 'html') {
5119 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5122 if (defined $hash_parent &&
5123 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5124 # commitdiff with two commits given
5125 my $hash_parent_short = $hash_parent;
5126 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5127 $hash_parent_short = substr($hash_parent, 0, 7);
5131 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
5132 if ($co{'parents'}[$i] eq $hash_parent) {
5133 $formats_nav .= ' parent ' . ($i+1);
5137 $formats_nav .= ': ' .
5138 $cgi->a({-href
=> href
(action
=>"commitdiff",
5139 hash
=>$hash_parent)},
5140 esc_html
($hash_parent_short)) .
5142 } elsif (!$co{'parent'}) {
5144 $formats_nav .= ' (initial)';
5145 } elsif (scalar @
{$co{'parents'}} == 1) {
5146 # single parent commit
5149 $cgi->a({-href
=> href
(action
=>"commitdiff",
5150 hash
=>$co{'parent'})},
5151 esc_html
(substr($co{'parent'}, 0, 7))) .
5155 if ($hash_parent eq '--cc') {
5156 $formats_nav .= ' | ' .
5157 $cgi->a({-href
=> href
(action
=>"commitdiff",
5158 hash
=>$hash, hash_parent
=>'-c')},
5160 } else { # $hash_parent eq '-c'
5161 $formats_nav .= ' | ' .
5162 $cgi->a({-href
=> href
(action
=>"commitdiff",
5163 hash
=>$hash, hash_parent
=>'--cc')},
5169 $cgi->a({-href
=> href
(action
=>"commitdiff",
5171 esc_html
(substr($_, 0, 7)));
5172 } @
{$co{'parents'}} ) .
5177 my $hash_parent_param = $hash_parent;
5178 if (!defined $hash_parent_param) {
5179 # --cc for multiple parents, --root for parentless
5180 $hash_parent_param =
5181 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
5187 if ($format eq 'html') {
5188 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5189 "--no-commit-id", "--patch-with-raw", "--full-index",
5190 $hash_parent_param, $hash, "--"
5191 or die_error
(500, "Open git-diff-tree failed");
5193 while (my $line = <$fd>) {
5195 # empty line ends raw part of diff-tree output
5197 push @difftree, scalar parse_difftree_raw_line
($line);
5200 } elsif ($format eq 'plain') {
5201 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5202 '-p', $hash_parent_param, $hash, "--"
5203 or die_error
(500, "Open git-diff-tree failed");
5206 die_error
(400, "Unknown commitdiff format");
5209 # non-textual hash id's can be cached
5211 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5215 # write commit message
5216 if ($format eq 'html') {
5217 my $refs = git_get_references
();
5218 my $ref = format_ref_marker
($refs, $co{'id'});
5220 git_header_html
(undef, $expires);
5221 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5222 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5223 git_print_authorship
(\
%co);
5224 print "<div class=\"page_body\">\n";
5225 if (@
{$co{'comment'}} > 1) {
5226 print "<div class=\"log\">\n";
5227 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5228 print "</div>\n"; # class="log"
5231 } elsif ($format eq 'plain') {
5232 my $refs = git_get_references
("tags");
5233 my $tagname = git_get_rev_name_tags
($hash);
5234 my $filename = basename
($project) . "-$hash.patch";
5237 -type
=> 'text/plain',
5238 -charset
=> 'utf-8',
5239 -expires
=> $expires,
5240 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5241 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5242 print "From: " . to_utf8
($co{'author'}) . "\n";
5243 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5244 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5246 print "X-Git-Tag: $tagname\n" if $tagname;
5247 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5249 foreach my $line (@
{$co{'comment'}}) {
5250 print to_utf8
($line) . "\n";
5256 if ($format eq 'html') {
5257 my $use_parents = !defined $hash_parent ||
5258 $hash_parent eq '-c' || $hash_parent eq '--cc';
5259 git_difftree_body
(\
@difftree, $hash,
5260 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5263 git_patchset_body
($fd, \
@difftree, $hash,
5264 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5266 print "</div>\n"; # class="page_body"
5269 } elsif ($format eq 'plain') {
5273 or print "Reading git-diff-tree failed\n";
5277 sub git_commitdiff_plain
{
5278 git_commitdiff
('plain');
5282 if (!defined $hash_base) {
5283 $hash_base = git_get_head_hash
($project);
5285 if (!defined $page) {
5289 my %co = parse_commit
($hash_base)
5290 or die_error
(404, "Unknown commit object");
5292 my $refs = git_get_references
();
5293 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5295 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5296 $file_name, "--full-history")
5297 or die_error
(404, "No such file or directory on given branch");
5299 if (!defined $hash && defined $file_name) {
5300 # some commits could have deleted file in question,
5301 # and not have it in tree, but one of them has to have it
5302 for (my $i = 0; $i <= @commitlist; $i++) {
5303 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5304 last if defined $hash;
5307 if (defined $hash) {
5308 $ftype = git_get_type
($hash);
5310 if (!defined $ftype) {
5311 die_error
(500, "Unknown type of object");
5314 my $paging_nav = '';
5317 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5318 file_name
=>$file_name)},
5320 $paging_nav .= " ⋅ " .
5321 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5322 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5324 $paging_nav .= "first";
5325 $paging_nav .= " ⋅ prev";
5328 if ($#commitlist >= 100) {
5330 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5331 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5332 $paging_nav .= " ⋅ $next_link";
5334 $paging_nav .= " ⋅ next";
5338 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5339 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5340 git_print_page_path
($file_name, $ftype, $hash_base);
5342 git_history_body
(\
@commitlist, 0, 99,
5343 $refs, $hash_base, $ftype, $next_link);
5349 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5350 if (!defined $searchtext) {
5351 die_error
(400, "Text field is empty");
5353 if (!defined $hash) {
5354 $hash = git_get_head_hash
($project);
5356 my %co = parse_commit
($hash);
5358 die_error
(404, "Unknown commit object");
5360 if (!defined $page) {
5364 $searchtype ||= 'commit';
5365 if ($searchtype eq 'pickaxe') {
5366 # pickaxe may take all resources of your box and run for several minutes
5367 # with every query - so decide by yourself how public you make this feature
5368 gitweb_check_feature
('pickaxe')
5369 or die_error
(403, "Pickaxe is disabled");
5371 if ($searchtype eq 'grep') {
5372 gitweb_check_feature
('grep')
5373 or die_error
(403, "Grep is disabled");
5378 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5380 if ($searchtype eq 'commit') {
5381 $greptype = "--grep=";
5382 } elsif ($searchtype eq 'author') {
5383 $greptype = "--author=";
5384 } elsif ($searchtype eq 'committer') {
5385 $greptype = "--committer=";
5387 $greptype .= $searchtext;
5388 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5389 $greptype, '--regexp-ignore-case',
5390 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
5392 my $paging_nav = '';
5395 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5396 searchtext
=>$searchtext,
5397 searchtype
=>$searchtype)},
5399 $paging_nav .= " ⋅ " .
5400 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5401 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5403 $paging_nav .= "first";
5404 $paging_nav .= " ⋅ prev";
5407 if ($#commitlist >= 100) {
5409 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5410 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5411 $paging_nav .= " ⋅ $next_link";
5413 $paging_nav .= " ⋅ next";
5416 if ($#commitlist >= 100) {
5419 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5420 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5421 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5424 if ($searchtype eq 'pickaxe') {
5425 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5426 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5428 print "<table class=\"pickaxe search\">\n";
5431 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5432 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5433 ($search_use_regexp ?
'--pickaxe-regex' : ());
5436 while (my $line = <$fd>) {
5440 my %set = parse_difftree_raw_line
($line);
5441 if (defined $set{'commit'}) {
5442 # finish previous commit
5445 "<td class=\"link\">" .
5446 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5448 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5454 print "<tr class=\"dark\">\n";
5456 print "<tr class=\"light\">\n";
5459 %co = parse_commit
($set{'commit'});
5460 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5461 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5462 "<td><i>$author</i></td>\n" .
5464 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5465 -class => "list subject"},
5466 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5467 } elsif (defined $set{'to_id'}) {
5468 next if ($set{'to_id'} =~ m/^0{40}$/);
5470 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5471 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5473 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5479 # finish last commit (warning: repetition!)
5482 "<td class=\"link\">" .
5483 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5485 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5493 if ($searchtype eq 'grep') {
5494 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5495 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5497 print "<table class=\"grep_search\">\n";
5501 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5502 $search_use_regexp ?
('-E', '-i') : '-F',
5503 $searchtext, $co{'tree'};
5505 while (my $line = <$fd>) {
5507 my ($file, $lno, $ltext, $binary);
5508 last if ($matches++ > 1000);
5509 if ($line =~ /^Binary file (.+) matches$/) {
5513 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5515 if ($file ne $lastfile) {
5516 $lastfile and print "</td></tr>\n";
5518 print "<tr class=\"dark\">\n";
5520 print "<tr class=\"light\">\n";
5522 print "<td class=\"list\">".
5523 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5524 file_name
=>"$file"),
5525 -class => "list"}, esc_path
($file));
5526 print "</td><td>\n";
5530 print "<div class=\"binary\">Binary file</div>\n";
5532 $ltext = untabify
($ltext);
5533 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5534 $ltext = esc_html
($1, -nbsp
=>1);
5535 $ltext .= '<span class="match">';
5536 $ltext .= esc_html
($2, -nbsp
=>1);
5537 $ltext .= '</span>';
5538 $ltext .= esc_html
($3, -nbsp
=>1);
5540 $ltext = esc_html
($ltext, -nbsp
=>1);
5542 print "<div class=\"pre\">" .
5543 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5544 file_name
=>"$file").'#l'.$lno,
5545 -class => "linenr"}, sprintf('%4i', $lno))
5546 . ' ' . $ltext . "</div>\n";
5550 print "</td></tr>\n";
5551 if ($matches > 1000) {
5552 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5555 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5564 sub git_search_help
{
5566 git_print_page_nav
('','', $hash,$hash,$hash);
5568 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5569 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5570 the pattern entered is recognized as the POSIX extended
5571 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5574 <dt><b>commit</b></dt>
5575 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5577 my ($have_grep) = gitweb_check_feature
('grep');
5580 <dt><b>grep</b></dt>
5581 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5582 a different one) are searched for the given pattern. On large trees, this search can take
5583 a while and put some strain on the server, so please use it with some consideration. Note that
5584 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5585 case-sensitive.</dd>
5589 <dt><b>author</b></dt>
5590 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5591 <dt><b>committer</b></dt>
5592 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5594 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5595 if ($have_pickaxe) {
5597 <dt><b>pickaxe</b></dt>
5598 <dd>All commits that caused the string to appear or disappear from any file (changes that
5599 added, removed or "modified" the string) will be listed. This search can take a while and
5600 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5601 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5609 my $head = git_get_head_hash
($project);
5610 if (!defined $hash) {
5613 if (!defined $page) {
5616 my $refs = git_get_references
();
5618 my $commit_hash = $hash;
5619 if (defined $hash_parent) {
5620 $commit_hash = "$hash_parent..$hash";
5622 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5624 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5626 if ($#commitlist >= 100) {
5628 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5629 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5633 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5634 git_print_header_div
('summary', $project);
5636 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5641 ## ......................................................................
5642 ## feeds (RSS, Atom; OPML)
5645 my $format = shift || 'atom';
5646 my ($have_blame) = gitweb_check_feature
('blame');
5648 # Atom: http://www.atomenabled.org/developers/syndication/
5649 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5650 if ($format ne 'rss' && $format ne 'atom') {
5651 die_error
(400, "Unknown web feed format");
5654 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5655 my $head = $hash || 'HEAD';
5656 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5660 my $content_type = "application/$format+xml";
5661 if (defined $cgi->http('HTTP_ACCEPT') &&
5662 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5663 # browser (feed reader) prefers text/xml
5664 $content_type = 'text/xml';
5666 if (defined($commitlist[0])) {
5667 %latest_commit = %{$commitlist[0]};
5668 %latest_date = parse_date
($latest_commit{'author_epoch'});
5670 -type
=> $content_type,
5671 -charset
=> 'utf-8',
5672 -last_modified
=> $latest_date{'rfc2822'});
5675 -type
=> $content_type,
5676 -charset
=> 'utf-8');
5679 # Optimization: skip generating the body if client asks only
5680 # for Last-Modified date.
5681 return if ($cgi->request_method() eq 'HEAD');
5684 my $title = "$site_name - $project/$action";
5685 my $feed_type = 'log';
5686 if (defined $hash) {
5687 $title .= " - '$hash'";
5688 $feed_type = 'branch log';
5689 if (defined $file_name) {
5690 $title .= " :: $file_name";
5691 $feed_type = 'history';
5693 } elsif (defined $file_name) {
5694 $title .= " - $file_name";
5695 $feed_type = 'history';
5697 $title .= " $feed_type";
5698 my $descr = git_get_project_description
($project);
5699 if (defined $descr) {
5700 $descr = esc_html
($descr);
5702 $descr = "$project " .
5703 ($format eq 'rss' ?
'RSS' : 'Atom') .
5706 my $owner = git_get_project_owner
($project);
5707 $owner = esc_html
($owner);
5711 if (defined $file_name) {
5712 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5713 } elsif (defined $hash) {
5714 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5716 $alt_url = href
(-full
=>1, action
=>"summary");
5718 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
5719 if ($format eq 'rss') {
5721 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5724 print "<title>$title</title>\n" .
5725 "<link>$alt_url</link>\n" .
5726 "<description>$descr</description>\n" .
5727 "<language>en</language>\n";
5728 } elsif ($format eq 'atom') {
5730 <feed xmlns="http://www.w3.org/2005/Atom">
5732 print "<title>$title</title>\n" .
5733 "<subtitle>$descr</subtitle>\n" .
5734 '<link rel="alternate" type="text/html" href="' .
5735 $alt_url . '" />' . "\n" .
5736 '<link rel="self" type="' . $content_type . '" href="' .
5737 $cgi->self_url() . '" />' . "\n" .
5738 "<id>" . href
(-full
=>1) . "</id>\n" .
5739 # use project owner for feed author
5740 "<author><name>$owner</name></author>\n";
5741 if (defined $favicon) {
5742 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5744 if (defined $logo_url) {
5745 # not twice as wide as tall: 72 x 27 pixels
5746 print "<logo>" . esc_url
($logo) . "</logo>\n";
5748 if (! %latest_date) {
5749 # dummy date to keep the feed valid until commits trickle in:
5750 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5752 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5757 for (my $i = 0; $i <= $#commitlist; $i++) {
5758 my %co = %{$commitlist[$i]};
5759 my $commit = $co{'id'};
5760 # we read 150, we always show 30 and the ones more recent than 48 hours
5761 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5764 my %cd = parse_date
($co{'author_epoch'});
5766 # get list of changed files
5767 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5768 $co{'parent'} || "--root",
5769 $co{'id'}, "--", (defined $file_name ?
$file_name : ())
5771 my @difftree = map { chomp; $_ } <$fd>;
5775 # print element (entry, item)
5776 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5777 if ($format eq 'rss') {
5779 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5780 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5781 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5782 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5783 "<link>$co_url</link>\n" .
5784 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5785 "<content:encoded>" .
5787 } elsif ($format eq 'atom') {
5789 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5790 "<updated>$cd{'iso-8601'}</updated>\n" .
5792 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5793 if ($co{'author_email'}) {
5794 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5796 print "</author>\n" .
5797 # use committer for contributor
5799 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5800 if ($co{'committer_email'}) {
5801 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5803 print "</contributor>\n" .
5804 "<published>$cd{'iso-8601'}</published>\n" .
5805 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5806 "<id>$co_url</id>\n" .
5807 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5808 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5810 my $comment = $co{'comment'};
5812 foreach my $line (@
$comment) {
5813 $line = esc_html
($line);
5816 print "</pre><ul>\n";
5817 foreach my $difftree_line (@difftree) {
5818 my %difftree = parse_difftree_raw_line
($difftree_line);
5819 next if !$difftree{'from_id'};
5821 my $file = $difftree{'file'} || $difftree{'to_file'};
5825 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5826 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5827 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5828 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5829 -title
=> "diff"}, 'D');
5831 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5832 file_name
=>$file, hash_base
=>$commit),
5833 -title
=> "blame"}, 'B');
5835 # if this is not a feed of a file history
5836 if (!defined $file_name || $file_name ne $file) {
5837 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5838 file_name
=>$file, hash
=>$commit),
5839 -title
=> "history"}, 'H');
5841 $file = esc_path
($file);
5845 if ($format eq 'rss') {
5846 print "</ul>]]>\n" .
5847 "</content:encoded>\n" .
5849 } elsif ($format eq 'atom') {
5850 print "</ul>\n</div>\n" .
5857 if ($format eq 'rss') {
5858 print "</channel>\n</rss>\n";
5859 } elsif ($format eq 'atom') {
5873 my @list = git_get_projects_list
();
5875 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
5877 <?xml version="1.0" encoding="utf-8"?>
5878 <opml version="1.0">
5880 <title>$site_name OPML Export</title>
5883 <outline text="git RSS feeds">
5886 foreach my $pr (@list) {
5888 my $head = git_get_head_hash
($proj{'path'});
5889 if (!defined $head) {
5892 $git_dir = "$projectroot/$proj{'path'}";
5893 my %co = parse_commit
($head);
5898 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
5899 my $rss = "$my_url?p=$proj{'path'};a=rss";
5900 my $html = "$my_url?p=$proj{'path'};a=summary";
5901 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";