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 # core git executable to use
31 # this can just be "git" if your webserver has a sensible PATH
32 our $GIT = "++GIT_BINDIR++/git";
34 # absolute fs-path which will be prepended to the project path
35 #our $projectroot = "/pub/scm";
36 our $projectroot = "++GITWEB_PROJECTROOT++";
38 # fs traversing limit for getting project list
39 # the number is relative to the projectroot
40 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
42 # target of the home link on top of all pages
43 our $home_link = $my_uri || "/";
45 # string of the home link on top of all pages
46 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
48 # name of your site or organization to appear in page titles
49 # replace this with something more descriptive for clearer bookmarks
50 our $site_name = "++GITWEB_SITENAME++"
51 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
53 # filename of html text to include at top of each page
54 our $site_header = "++GITWEB_SITE_HEADER++";
55 # html text to include at home page
56 our $home_text = "++GITWEB_HOMETEXT++";
57 # filename of html text to include at bottom of each page
58 our $site_footer = "++GITWEB_SITE_FOOTER++";
61 our @stylesheets = ("++GITWEB_CSS++");
62 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
63 our $stylesheet = undef;
64 # URI of GIT logo (72x27 size)
65 our $logo = "++GITWEB_LOGO++";
66 # URI of GIT favicon, assumed to be image/png type
67 our $favicon = "++GITWEB_FAVICON++";
69 # URI and label (title) of GIT logo link
70 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
71 #our $logo_label = "git documentation";
72 our $logo_url = "http://git.or.cz/";
73 our $logo_label = "git homepage";
75 # source of projects list
76 our $projects_list = "++GITWEB_LIST++";
78 # the width (in characters) of the projects list "Description" column
79 our $projects_list_description_width = 25;
81 # default order of projects list
82 # valid values are none, project, descr, owner, and age
83 our $default_projects_order = "project";
85 # show repository only if this file exists
86 # (only effective if this variable evaluates to true)
87 our $export_ok = "++GITWEB_EXPORT_OK++";
89 # only allow viewing of repositories also shown on the overview page
90 our $strict_export = "++GITWEB_STRICT_EXPORT++";
92 # list of git base URLs used for URL to where fetch project from,
93 # i.e. full URL is "$git_base_url/$project"
94 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
96 # default blob_plain mimetype and default charset for text/plain blob
97 our $default_blob_plain_mimetype = 'text/plain';
98 our $default_text_plain_charset = undef;
100 # file to use for guessing MIME types before trying /etc/mime.types
101 # (relative to the current git repository)
102 our $mimetypes_file = undef;
104 # assume this charset if line contains non-UTF-8 characters;
105 # it should be valid encoding (see Encoding::Supported(3pm) for list),
106 # for which encoding all byte sequences are valid, for example
107 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
108 # could be even 'utf-8' for the old behavior)
109 our $fallback_encoding = 'latin1';
111 # rename detection options for git-diff and git-diff-tree
112 # - default is '-M', with the cost proportional to
113 # (number of removed files) * (number of new files).
114 # - more costly is '-C' (which implies '-M'), with the cost proportional to
115 # (number of changed files + number of removed files) * (number of new files)
116 # - even more costly is '-C', '--find-copies-harder' with cost
117 # (number of files in the original tree) * (number of new files)
118 # - one might want to include '-B' option, e.g. '-B', '-M'
119 our @diff_opts = ('-M'); # taken from git_commit
121 # information about snapshot formats that gitweb is capable of serving
122 our %known_snapshot_formats = (
124 # 'display' => display name,
125 # 'type' => mime type,
126 # 'suffix' => filename suffix,
127 # 'format' => --format for git-archive,
128 # 'compressor' => [compressor command and arguments]
129 # (array reference, optional)}
132 'display' => 'tar.gz',
133 'type' => 'application/x-gzip',
134 'suffix' => '.tar.gz',
136 'compressor' => ['gzip']},
139 'display' => 'tar.bz2',
140 'type' => 'application/x-bzip2',
141 'suffix' => '.tar.bz2',
143 'compressor' => ['bzip2']},
147 'type' => 'application/x-zip',
152 # Aliases so we understand old gitweb.snapshot values in repository
154 our %known_snapshot_format_aliases = (
158 # backward compatibility: legacy gitweb config support
159 'x-gzip' => undef, 'gz' => undef,
160 'x-bzip2' => undef, 'bz2' => undef,
161 'x-zip' => undef, '' => undef,
164 # You define site-wide feature defaults here; override them with
165 # $GITWEB_CONFIG as necessary.
168 # 'sub' => feature-sub (subroutine),
169 # 'override' => allow-override (boolean),
170 # 'default' => [ default options...] (array reference)}
172 # if feature is overridable (it means that allow-override has true value),
173 # then feature-sub will be called with default options as parameters;
174 # return value of feature-sub indicates if to enable specified feature
176 # if there is no 'sub' key (no feature-sub), then feature cannot be
179 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
181 # Enable the 'blame' blob view, showing the last commit that modified
182 # each line in the file. This can be very CPU-intensive.
184 # To enable system wide have in $GITWEB_CONFIG
185 # $feature{'blame'}{'default'} = [1];
186 # To have project specific config enable override in $GITWEB_CONFIG
187 # $feature{'blame'}{'override'} = 1;
188 # and in project config gitweb.blame = 0|1;
190 'sub' => \
&feature_blame
,
194 # Enable the 'snapshot' link, providing a compressed archive of any
195 # tree. This can potentially generate high traffic if you have large
198 # Value is a list of formats defined in %known_snapshot_formats that
200 # To disable system wide have in $GITWEB_CONFIG
201 # $feature{'snapshot'}{'default'} = [];
202 # To have project specific config enable override in $GITWEB_CONFIG
203 # $feature{'snapshot'}{'override'} = 1;
204 # and in project config, a comma-separated list of formats or "none"
205 # to disable. Example: gitweb.snapshot = tbz2,zip;
207 'sub' => \
&feature_snapshot
,
209 'default' => ['tgz']},
211 # Enable text search, which will list the commits which match author,
212 # committer or commit text to a given string. Enabled by default.
213 # Project specific override is not supported.
218 # Enable grep search, which will list the files in currently selected
219 # tree containing the given string. Enabled by default. This can be
220 # potentially CPU-intensive, of course.
222 # To enable system wide have in $GITWEB_CONFIG
223 # $feature{'grep'}{'default'} = [1];
224 # To have project specific config enable override in $GITWEB_CONFIG
225 # $feature{'grep'}{'override'} = 1;
226 # and in project config gitweb.grep = 0|1;
231 # Enable the pickaxe search, which will list the commits that modified
232 # a given string in a file. This can be practical and quite faster
233 # alternative to 'blame', but still potentially CPU-intensive.
235 # To enable system wide have in $GITWEB_CONFIG
236 # $feature{'pickaxe'}{'default'} = [1];
237 # To have project specific config enable override in $GITWEB_CONFIG
238 # $feature{'pickaxe'}{'override'} = 1;
239 # and in project config gitweb.pickaxe = 0|1;
241 'sub' => \
&feature_pickaxe
,
245 # Make gitweb use an alternative format of the URLs which can be
246 # more readable and natural-looking: project name is embedded
247 # directly in the path and the query string contains other
248 # auxiliary information. All gitweb installations recognize
249 # URL in either format; this configures in which formats gitweb
252 # To enable system wide have in $GITWEB_CONFIG
253 # $feature{'pathinfo'}{'default'} = [1];
254 # Project specific override is not supported.
256 # Note that you will need to change the default location of CSS,
257 # favicon, logo and possibly other files to an absolute URL. Also,
258 # if gitweb.cgi serves as your indexfile, you will need to force
259 # $my_uri to contain the script name in your $GITWEB_CONFIG.
264 # Make gitweb consider projects in project root subdirectories
265 # to be forks of existing projects. Given project $projname.git,
266 # projects matching $projname/*.git will not be shown in the main
267 # projects list, instead a '+' mark will be added to $projname
268 # there and a 'forks' view will be enabled for the project, listing
269 # all the forks. If project list is taken from a file, forks have
270 # to be listed after the main project.
272 # To enable system wide have in $GITWEB_CONFIG
273 # $feature{'forks'}{'default'} = [1];
274 # Project specific override is not supported.
279 # Allow gitweb scan project content tags described in ctags/
280 # of project repository, and display the popular Web 2.0-ish
281 # "tag cloud" near the project list. Note that this is something
282 # COMPLETELY different from the normal Git tags.
284 # gitweb by itself can show existing tags, but it does not handle
285 # tagging itself; you need an external application for that.
286 # For an example script, check Girocco's cgi/tagproj.cgi.
288 # To enable system wide have in $GITWEB_CONFIG
289 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
290 # Project specific override is not supported.
296 sub gitweb_check_feature
{
298 return unless exists $feature{$name};
299 my ($sub, $override, @defaults) = (
300 $feature{$name}{'sub'},
301 $feature{$name}{'override'},
302 @
{$feature{$name}{'default'}});
303 if (!$override) { return @defaults; }
305 warn "feature $name is not overrideable";
308 return $sub->(@defaults);
312 my ($val) = git_get_project_config
('blame', '--bool');
314 if ($val eq 'true') {
316 } elsif ($val eq 'false') {
323 sub feature_snapshot
{
326 my ($val) = git_get_project_config
('snapshot');
329 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
336 my ($val) = git_get_project_config
('grep', '--bool');
338 if ($val eq 'true') {
340 } elsif ($val eq 'false') {
347 sub feature_pickaxe
{
348 my ($val) = git_get_project_config
('pickaxe', '--bool');
350 if ($val eq 'true') {
352 } elsif ($val eq 'false') {
359 # checking HEAD file with -e is fragile if the repository was
360 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
362 sub check_head_link
{
364 my $headfile = "$dir/HEAD";
365 return ((-e
$headfile) ||
366 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
369 sub check_export_ok
{
371 return (check_head_link
($dir) &&
372 (!$export_ok || -e
"$dir/$export_ok"));
375 # process alternate names for backward compatibility
376 # filter out unsupported (unknown) snapshot formats
377 sub filter_snapshot_fmts
{
381 exists $known_snapshot_format_aliases{$_} ?
382 $known_snapshot_format_aliases{$_} : $_} @fmts;
383 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
387 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
388 if (-e
$GITWEB_CONFIG) {
391 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
392 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
395 # version of the core git binary
396 our $git_version = qx("$GIT" --version
) =~ m/git version (.*)$/ ?
$1 : "unknown";
398 $projects_list ||= $projectroot;
400 # ======================================================================
401 # input validation and dispatch
402 our $action = $cgi->param('a');
403 if (defined $action) {
404 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
405 die_error
(400, "Invalid action parameter");
409 # parameters which are pathnames
410 our $project = $cgi->param('p');
411 if (defined $project) {
412 if (!validate_pathname
($project) ||
413 !(-d
"$projectroot/$project") ||
414 !check_head_link
("$projectroot/$project") ||
415 ($export_ok && !(-e
"$projectroot/$project/$export_ok")) ||
416 ($strict_export && !project_in_list
($project))) {
418 die_error
(404, "No such project");
422 our $file_name = $cgi->param('f');
423 if (defined $file_name) {
424 if (!validate_pathname
($file_name)) {
425 die_error
(400, "Invalid file parameter");
429 our $file_parent = $cgi->param('fp');
430 if (defined $file_parent) {
431 if (!validate_pathname
($file_parent)) {
432 die_error
(400, "Invalid file parent parameter");
436 # parameters which are refnames
437 our $hash = $cgi->param('h');
439 if (!validate_refname
($hash)) {
440 die_error
(400, "Invalid hash parameter");
444 our $hash_parent = $cgi->param('hp');
445 if (defined $hash_parent) {
446 if (!validate_refname
($hash_parent)) {
447 die_error
(400, "Invalid hash parent parameter");
451 our $hash_base = $cgi->param('hb');
452 if (defined $hash_base) {
453 if (!validate_refname
($hash_base)) {
454 die_error
(400, "Invalid hash base parameter");
458 my %allowed_options = (
459 "--no-merges" => [ qw(rss atom log shortlog history) ],
462 our @extra_options = $cgi->param('opt');
463 if (defined @extra_options) {
464 foreach my $opt (@extra_options) {
465 if (not exists $allowed_options{$opt}) {
466 die_error
(400, "Invalid option parameter");
468 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
469 die_error
(400, "Invalid option parameter for this action");
474 our $hash_parent_base = $cgi->param('hpb');
475 if (defined $hash_parent_base) {
476 if (!validate_refname
($hash_parent_base)) {
477 die_error
(400, "Invalid hash parent base parameter");
482 our $page = $cgi->param('pg');
484 if ($page =~ m/[^0-9]/) {
485 die_error
(400, "Invalid page parameter");
489 our $searchtype = $cgi->param('st');
490 if (defined $searchtype) {
491 if ($searchtype =~ m/[^a-z]/) {
492 die_error
(400, "Invalid searchtype parameter");
496 our $search_use_regexp = $cgi->param('sr');
498 our $searchtext = $cgi->param('s');
500 if (defined $searchtext) {
501 if (length($searchtext) < 2) {
502 die_error
(403, "At least two characters are required for search parameter");
504 $search_regexp = $search_use_regexp ?
$searchtext : quotemeta $searchtext;
507 # now read PATH_INFO and use it as alternative to parameters
508 sub evaluate_path_info
{
509 return if defined $project;
510 my $path_info = $ENV{"PATH_INFO"};
511 return if !$path_info;
512 $path_info =~ s
,^/+,,;
513 return if !$path_info;
514 # find which part of PATH_INFO is project
515 $project = $path_info;
517 while ($project && !check_head_link
("$projectroot/$project")) {
518 $project =~ s
,/*[^/]*$,,;
521 $project = validate_pathname
($project);
523 ($export_ok && !-e
"$projectroot/$project/$export_ok") ||
524 ($strict_export && !project_in_list
($project))) {
528 # do not change any parameters if an action is given using the query string
530 $path_info =~ s
,^\Q
$project\E
/*,,;
531 my ($refname, $pathname) = split(/:/, $path_info, 2);
532 if (defined $pathname) {
533 # we got "project.git/branch:filename" or "project.git/branch:dir/"
534 # we could use git_get_type(branch:pathname), but it needs $git_dir
535 $pathname =~ s
,^/+,,;
536 if (!$pathname || substr($pathname, -1) eq "/") {
540 $action ||= "blob_plain";
542 $hash_base ||= validate_refname
($refname);
543 $file_name ||= validate_pathname
($pathname);
544 } elsif (defined $refname) {
545 # we got "project.git/branch"
546 $action ||= "shortlog";
547 $hash ||= validate_refname
($refname);
550 evaluate_path_info
();
552 # path to the current git repository
554 $git_dir = "$projectroot/$project" if $project;
558 "blame" => \
&git_blame
,
559 "blobdiff" => \
&git_blobdiff
,
560 "blobdiff_plain" => \
&git_blobdiff_plain
,
561 "blob" => \
&git_blob
,
562 "blob_plain" => \
&git_blob_plain
,
563 "commitdiff" => \
&git_commitdiff
,
564 "commitdiff_plain" => \
&git_commitdiff_plain
,
565 "commit" => \
&git_commit
,
566 "forks" => \
&git_forks
,
567 "heads" => \
&git_heads
,
568 "history" => \
&git_history
,
571 "atom" => \
&git_atom
,
572 "search" => \
&git_search
,
573 "search_help" => \
&git_search_help
,
574 "shortlog" => \
&git_shortlog
,
575 "summary" => \
&git_summary
,
577 "tags" => \
&git_tags
,
578 "tree" => \
&git_tree
,
579 "snapshot" => \
&git_snapshot
,
580 "object" => \
&git_object
,
581 # those below don't need $project
582 "opml" => \
&git_opml
,
583 "project_list" => \
&git_project_list
,
584 "project_index" => \
&git_project_index
,
587 if (!defined $action) {
589 $action = git_get_type
($hash);
590 } elsif (defined $hash_base && defined $file_name) {
591 $action = git_get_type
("$hash_base:$file_name");
592 } elsif (defined $project) {
595 $action = 'project_list';
598 if (!defined($actions{$action})) {
599 die_error
(400, "Unknown action");
601 if ($action !~ m/^(opml|project_list|project_index)$/ &&
603 die_error
(400, "Project needed");
605 $actions{$action}->();
608 ## ======================================================================
613 # default is to use -absolute url() i.e. $my_uri
614 my $href = $params{-full
} ?
$my_url : $my_uri;
616 # XXX: Warning: If you touch this, check the search form for updating,
627 hash_parent_base
=> "hpb",
632 snapshot_format
=> "sf",
633 extra_options
=> "opt",
634 search_use_regexp
=> "sr",
636 my %mapping = @mapping;
638 $params{'project'} = $project unless exists $params{'project'};
640 if ($params{-replay
}) {
641 while (my ($name, $symbol) = each %mapping) {
642 if (!exists $params{$name}) {
643 # to allow for multivalued params we use arrayref form
644 $params{$name} = [ $cgi->param($symbol) ];
649 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
651 # use PATH_INFO for project name
652 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
653 delete $params{'project'};
655 # Summary just uses the project path URL
656 if (defined $params{'action'} && $params{'action'} eq 'summary') {
657 delete $params{'action'};
661 # now encode the parameters explicitly
663 for (my $i = 0; $i < @mapping; $i += 2) {
664 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
665 if (defined $params{$name}) {
666 if (ref($params{$name}) eq "ARRAY") {
667 foreach my $par (@
{$params{$name}}) {
668 push @result, $symbol . "=" . esc_param
($par);
671 push @result, $symbol . "=" . esc_param
($params{$name});
675 $href .= "?" . join(';', @result) if scalar @result;
681 ## ======================================================================
682 ## validation, quoting/unquoting and escaping
684 sub validate_pathname
{
685 my $input = shift || return undef;
687 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
688 # at the beginning, at the end, and between slashes.
689 # also this catches doubled slashes
690 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
694 if ($input =~ m!\0!) {
700 sub validate_refname
{
701 my $input = shift || return undef;
703 # textual hashes are O.K.
704 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
707 # it must be correct pathname
708 $input = validate_pathname
($input)
710 # restrictions on ref name according to git-check-ref-format
711 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
717 # decode sequences of octets in utf8 into Perl's internal form,
718 # which is utf-8 with utf8 flag set if needed. gitweb writes out
719 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
722 if (utf8
::valid
($str)) {
726 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
730 # quote unsafe chars, but keep the slash, even when it's not
731 # correct, but quoted slashes look too horrible in bookmarks
734 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
740 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
743 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
749 # replace invalid utf8 character with SUBSTITUTION sequence
754 $str = to_utf8
($str);
755 $str = $cgi->escapeHTML($str);
756 if ($opts{'-nbsp'}) {
757 $str =~ s/ / /g;
759 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
763 # quote control characters and escape filename to HTML
768 $str = to_utf8
($str);
769 $str = $cgi->escapeHTML($str);
770 if ($opts{'-nbsp'}) {
771 $str =~ s/ / /g;
773 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
777 # Make control characters "printable", using character escape codes (CEC)
781 my %es = ( # character escape codes, aka escape sequences
782 "\t" => '\t', # tab (HT)
783 "\n" => '\n', # line feed (LF)
784 "\r" => '\r', # carrige return (CR)
785 "\f" => '\f', # form feed (FF)
786 "\b" => '\b', # backspace (BS)
787 "\a" => '\a', # alarm (bell) (BEL)
788 "\e" => '\e', # escape (ESC)
789 "\013" => '\v', # vertical tab (VT)
790 "\000" => '\0', # nul character (NUL)
792 my $chr = ( (exists $es{$cntrl})
794 : sprintf('\%03o', ord($cntrl)) );
795 if ($opts{-nohtml
}) {
798 return "<span class=\"cntrl\">$chr</span>";
802 # Alternatively use unicode control pictures codepoints,
803 # Unicode "printable representation" (PR)
808 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
809 if ($opts{-nohtml
}) {
812 return "<span class=\"cntrl\">$chr</span>";
816 # git may return quoted and escaped filenames
822 my %es = ( # character escape codes, aka escape sequences
823 't' => "\t", # tab (HT, TAB)
824 'n' => "\n", # newline (NL)
825 'r' => "\r", # return (CR)
826 'f' => "\f", # form feed (FF)
827 'b' => "\b", # backspace (BS)
828 'a' => "\a", # alarm (bell) (BEL)
829 'e' => "\e", # escape (ESC)
830 'v' => "\013", # vertical tab (VT)
833 if ($seq =~ m/^[0-7]{1,3}$/) {
834 # octal char sequence
835 return chr(oct($seq));
836 } elsif (exists $es{$seq}) {
837 # C escape sequence, aka character escape code
840 # quoted ordinary character
844 if ($str =~ m/^"(.*)"$/) {
847 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
852 # escape tabs (convert tabs to spaces)
856 while ((my $pos = index($line, "\t")) != -1) {
857 if (my $count = (8 - ($pos % 8))) {
858 my $spaces = ' ' x
$count;
859 $line =~ s/\t/$spaces/;
866 sub project_in_list
{
868 my @list = git_get_projects_list
();
869 return @list && scalar(grep { $_->{'path'} eq $project } @list);
872 ## ----------------------------------------------------------------------
873 ## HTML aware string manipulation
875 # Try to chop given string on a word boundary between position
876 # $len and $len+$add_len. If there is no word boundary there,
877 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
878 # (marking chopped part) would be longer than given string.
882 my $add_len = shift || 10;
883 my $where = shift || 'right'; # 'left' | 'center' | 'right'
885 # Make sure perl knows it is utf8 encoded so we don't
886 # cut in the middle of a utf8 multibyte char.
887 $str = to_utf8
($str);
889 # allow only $len chars, but don't cut a word if it would fit in $add_len
890 # if it doesn't fit, cut it if it's still longer than the dots we would add
891 # remove chopped character entities entirely
893 # when chopping in the middle, distribute $len into left and right part
894 # return early if chopping wouldn't make string shorter
895 if ($where eq 'center') {
896 return $str if ($len + 5 >= length($str)); # filler is length 5
899 return $str if ($len + 4 >= length($str)); # filler is length 4
902 # regexps: ending and beginning with word part up to $add_len
903 my $endre = qr/.{$len}\w{0,$add_len}/;
904 my $begre = qr/\w{0,$add_len}.{$len}/;
906 if ($where eq 'left') {
907 $str =~ m/^(.*?)($begre)$/;
908 my ($lead, $body) = ($1, $2);
909 if (length($lead) > 4) {
910 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
915 } elsif ($where eq 'center') {
916 $str =~ m/^($endre)(.*)$/;
917 my ($left, $str) = ($1, $2);
918 $str =~ m/^(.*?)($begre)$/;
919 my ($mid, $right) = ($1, $2);
920 if (length($mid) > 5) {
921 $left =~ s/&[^;]*$//;
922 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
925 return "$left$mid$right";
928 $str =~ m/^($endre)(.*)$/;
931 if (length($tail) > 4) {
932 $body =~ s/&[^;]*$//;
939 # takes the same arguments as chop_str, but also wraps a <span> around the
940 # result with a title attribute if it does get chopped. Additionally, the
941 # string is HTML-escaped.
942 sub chop_and_escape_str
{
945 my $chopped = chop_str
(@_);
946 if ($chopped eq $str) {
947 return esc_html
($chopped);
949 $str =~ s/([[:cntrl:]])/?/g;
950 return $cgi->span({-title
=>$str}, esc_html
($chopped));
954 ## ----------------------------------------------------------------------
955 ## functions returning short strings
957 # CSS class for given age value (in seconds)
963 } elsif ($age < 60*60*2) {
965 } elsif ($age < 60*60*24*2) {
972 # convert age in seconds to "nn units ago" string
977 if ($age > 60*60*24*365*2) {
978 $age_str = (int $age/60/60/24/365);
979 $age_str .= " years ago";
980 } elsif ($age > 60*60*24*(365/12)*2) {
981 $age_str = int $age/60/60/24/(365/12);
982 $age_str .= " months ago";
983 } elsif ($age > 60*60*24*7*2) {
984 $age_str = int $age/60/60/24/7;
985 $age_str .= " weeks ago";
986 } elsif ($age > 60*60*24*2) {
987 $age_str = int $age/60/60/24;
988 $age_str .= " days ago";
989 } elsif ($age > 60*60*2) {
990 $age_str = int $age/60/60;
991 $age_str .= " hours ago";
992 } elsif ($age > 60*2) {
993 $age_str = int $age/60;
994 $age_str .= " min ago";
997 $age_str .= " sec ago";
999 $age_str .= " right now";
1005 S_IFINVALID
=> 0030000,
1006 S_IFGITLINK
=> 0160000,
1009 # submodule/subproject, a commit object reference
1010 sub S_ISGITLINK
($) {
1013 return (($mode & S_IFMT
) == S_IFGITLINK
)
1016 # convert file mode in octal to symbolic file mode string
1018 my $mode = oct shift;
1020 if (S_ISGITLINK
($mode)) {
1021 return 'm---------';
1022 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1023 return 'drwxr-xr-x';
1024 } elsif (S_ISLNK
($mode)) {
1025 return 'lrwxrwxrwx';
1026 } elsif (S_ISREG
($mode)) {
1027 # git cares only about the executable bit
1028 if ($mode & S_IXUSR
) {
1029 return '-rwxr-xr-x';
1031 return '-rw-r--r--';
1034 return '----------';
1038 # convert file mode in octal to file type string
1042 if ($mode !~ m/^[0-7]+$/) {
1048 if (S_ISGITLINK
($mode)) {
1050 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1052 } elsif (S_ISLNK
($mode)) {
1054 } elsif (S_ISREG
($mode)) {
1061 # convert file mode in octal to file type description string
1062 sub file_type_long
{
1065 if ($mode !~ m/^[0-7]+$/) {
1071 if (S_ISGITLINK
($mode)) {
1073 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1075 } elsif (S_ISLNK
($mode)) {
1077 } elsif (S_ISREG
($mode)) {
1078 if ($mode & S_IXUSR
) {
1079 return "executable";
1089 ## ----------------------------------------------------------------------
1090 ## functions returning short HTML fragments, or transforming HTML fragments
1091 ## which don't belong to other sections
1093 # format line of commit message.
1094 sub format_log_line_html
{
1097 $line = esc_html
($line, -nbsp
=>1);
1098 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1101 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1102 -class => "text"}, $hash_text);
1103 $line =~ s/$hash_text/$link/;
1108 # format marker of refs pointing to given object
1110 # the destination action is chosen based on object type and current context:
1111 # - for annotated tags, we choose the tag view unless it's the current view
1112 # already, in which case we go to shortlog view
1113 # - for other refs, we keep the current view if we're in history, shortlog or
1114 # log view, and select shortlog otherwise
1115 sub format_ref_marker
{
1116 my ($refs, $id) = @_;
1119 if (defined $refs->{$id}) {
1120 foreach my $ref (@
{$refs->{$id}}) {
1121 # this code exploits the fact that non-lightweight tags are the
1122 # only indirect objects, and that they are the only objects for which
1123 # we want to use tag instead of shortlog as action
1124 my ($type, $name) = qw();
1125 my $indirect = ($ref =~ s/\^\{\}$//);
1126 # e.g. tags/v2.6.11 or heads/next
1127 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1136 $class .= " indirect" if $indirect;
1138 my $dest_action = "shortlog";
1141 $dest_action = "tag" unless $action eq "tag";
1142 } elsif ($action =~ /^(history|(short)?log)$/) {
1143 $dest_action = $action;
1147 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1150 my $link = $cgi->a({
1152 action
=>$dest_action,
1156 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1162 return ' <span class="refs">'. $markers . '</span>';
1168 # format, perhaps shortened and with markers, title line
1169 sub format_subject_html
{
1170 my ($long, $short, $href, $extra) = @_;
1171 $extra = '' unless defined($extra);
1173 if (length($short) < length($long)) {
1174 return $cgi->a({-href
=> $href, -class => "list subject",
1175 -title
=> to_utf8
($long)},
1176 esc_html
($short) . $extra);
1178 return $cgi->a({-href
=> $href, -class => "list subject"},
1179 esc_html
($long) . $extra);
1183 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1184 sub format_git_diff_header_line
{
1186 my $diffinfo = shift;
1187 my ($from, $to) = @_;
1189 if ($diffinfo->{'nparents'}) {
1191 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1192 if ($to->{'href'}) {
1193 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1194 esc_path
($to->{'file'}));
1195 } else { # file was deleted (no href)
1196 $line .= esc_path
($to->{'file'});
1200 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1201 if ($from->{'href'}) {
1202 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1203 'a/' . esc_path
($from->{'file'}));
1204 } else { # file was added (no href)
1205 $line .= 'a/' . esc_path
($from->{'file'});
1208 if ($to->{'href'}) {
1209 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1210 'b/' . esc_path
($to->{'file'}));
1211 } else { # file was deleted
1212 $line .= 'b/' . esc_path
($to->{'file'});
1216 return "<div class=\"diff header\">$line</div>\n";
1219 # format extended diff header line, before patch itself
1220 sub format_extended_diff_header_line
{
1222 my $diffinfo = shift;
1223 my ($from, $to) = @_;
1226 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1227 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1228 esc_path
($from->{'file'}));
1230 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1231 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1232 esc_path
($to->{'file'}));
1234 # match single <mode>
1235 if ($line =~ m/\s(\d{6})$/) {
1236 $line .= '<span class="info"> (' .
1237 file_type_long
($1) .
1241 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1242 # can match only for combined diff
1244 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1245 if ($from->{'href'}[$i]) {
1246 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1248 substr($diffinfo->{'from_id'}[$i],0,7));
1253 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1256 if ($to->{'href'}) {
1257 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1258 substr($diffinfo->{'to_id'},0,7));
1263 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1264 # can match only for ordinary diff
1265 my ($from_link, $to_link);
1266 if ($from->{'href'}) {
1267 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1268 substr($diffinfo->{'from_id'},0,7));
1270 $from_link = '0' x
7;
1272 if ($to->{'href'}) {
1273 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1274 substr($diffinfo->{'to_id'},0,7));
1278 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1279 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1282 return $line . "<br/>\n";
1285 # format from-file/to-file diff header
1286 sub format_diff_from_to_header
{
1287 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1292 #assert($line =~ m/^---/) if DEBUG;
1293 # no extra formatting for "^--- /dev/null"
1294 if (! $diffinfo->{'nparents'}) {
1295 # ordinary (single parent) diff
1296 if ($line =~ m!^--- "?a/!) {
1297 if ($from->{'href'}) {
1299 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1300 esc_path
($from->{'file'}));
1303 esc_path
($from->{'file'});
1306 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1309 # combined diff (merge commit)
1310 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1311 if ($from->{'href'}[$i]) {
1313 $cgi->a({-href
=>href
(action
=>"blobdiff",
1314 hash_parent
=>$diffinfo->{'from_id'}[$i],
1315 hash_parent_base
=>$parents[$i],
1316 file_parent
=>$from->{'file'}[$i],
1317 hash
=>$diffinfo->{'to_id'},
1319 file_name
=>$to->{'file'}),
1321 -title
=>"diff" . ($i+1)},
1324 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1325 esc_path
($from->{'file'}[$i]));
1327 $line = '--- /dev/null';
1329 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1334 #assert($line =~ m/^\+\+\+/) if DEBUG;
1335 # no extra formatting for "^+++ /dev/null"
1336 if ($line =~ m!^\+\+\+ "?b/!) {
1337 if ($to->{'href'}) {
1339 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1340 esc_path
($to->{'file'}));
1343 esc_path
($to->{'file'});
1346 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
1351 # create note for patch simplified by combined diff
1352 sub format_diff_cc_simplified
{
1353 my ($diffinfo, @parents) = @_;
1356 $result .= "<div class=\"diff header\">" .
1358 if (!is_deleted
($diffinfo)) {
1359 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1361 hash
=>$diffinfo->{'to_id'},
1362 file_name
=>$diffinfo->{'to_file'}),
1364 esc_path
($diffinfo->{'to_file'}));
1366 $result .= esc_path
($diffinfo->{'to_file'});
1368 $result .= "</div>\n" . # class="diff header"
1369 "<div class=\"diff nodifferences\">" .
1371 "</div>\n"; # class="diff nodifferences"
1376 # format patch (diff) line (not to be used for diff headers)
1377 sub format_diff_line
{
1379 my ($from, $to) = @_;
1380 my $diff_class = "";
1384 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1386 my $prefix = substr($line, 0, scalar @
{$from->{'href'}});
1387 if ($line =~ m/^\@{3}/) {
1388 $diff_class = " chunk_header";
1389 } elsif ($line =~ m/^\\/) {
1390 $diff_class = " incomplete";
1391 } elsif ($prefix =~ tr/+/+/) {
1392 $diff_class = " add";
1393 } elsif ($prefix =~ tr/-/-/) {
1394 $diff_class = " rem";
1397 # assume ordinary diff
1398 my $char = substr($line, 0, 1);
1400 $diff_class = " add";
1401 } elsif ($char eq '-') {
1402 $diff_class = " rem";
1403 } elsif ($char eq '@') {
1404 $diff_class = " chunk_header";
1405 } elsif ($char eq "\\") {
1406 $diff_class = " incomplete";
1409 $line = untabify
($line);
1410 if ($from && $to && $line =~ m/^\@{2} /) {
1411 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1412 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1414 $from_lines = 0 unless defined $from_lines;
1415 $to_lines = 0 unless defined $to_lines;
1417 if ($from->{'href'}) {
1418 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1419 -class=>"list"}, $from_text);
1421 if ($to->{'href'}) {
1422 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1423 -class=>"list"}, $to_text);
1425 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1426 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1427 return "<div class=\"diff$diff_class\">$line</div>\n";
1428 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1429 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1430 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1432 @from_text = split(' ', $ranges);
1433 for (my $i = 0; $i < @from_text; ++$i) {
1434 ($from_start[$i], $from_nlines[$i]) =
1435 (split(',', substr($from_text[$i], 1)), 0);
1438 $to_text = pop @from_text;
1439 $to_start = pop @from_start;
1440 $to_nlines = pop @from_nlines;
1442 $line = "<span class=\"chunk_info\">$prefix ";
1443 for (my $i = 0; $i < @from_text; ++$i) {
1444 if ($from->{'href'}[$i]) {
1445 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1446 -class=>"list"}, $from_text[$i]);
1448 $line .= $from_text[$i];
1452 if ($to->{'href'}) {
1453 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1454 -class=>"list"}, $to_text);
1458 $line .= " $prefix</span>" .
1459 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1460 return "<div class=\"diff$diff_class\">$line</div>\n";
1462 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1465 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1466 # linked. Pass the hash of the tree/commit to snapshot.
1467 sub format_snapshot_links
{
1469 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1470 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1471 my $num_fmts = @snapshot_fmts;
1472 if ($num_fmts > 1) {
1473 # A parenthesized list of links bearing format names.
1474 # e.g. "snapshot (_tar.gz_ _zip_)"
1475 return "snapshot (" . join(' ', map
1482 }, $known_snapshot_formats{$_}{'display'})
1483 , @snapshot_fmts) . ")";
1484 } elsif ($num_fmts == 1) {
1485 # A single "snapshot" link whose tooltip bears the format name.
1487 my ($fmt) = @snapshot_fmts;
1493 snapshot_format
=>$fmt
1495 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1497 } else { # $num_fmts == 0
1502 ## ......................................................................
1503 ## functions returning values to be passed, perhaps after some
1504 ## transformation, to other functions; e.g. returning arguments to href()
1506 # returns hash to be passed to href to generate gitweb URL
1507 # in -title key it returns description of link
1509 my $format = shift || 'Atom';
1510 my %res = (action
=> lc($format));
1512 # feed links are possible only for project views
1513 return unless (defined $project);
1514 # some views should link to OPML, or to generic project feed,
1515 # or don't have specific feed yet (so they should use generic)
1516 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1519 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1520 # from tag links; this also makes possible to detect branch links
1521 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1522 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1525 # find log type for feed description (title)
1527 if (defined $file_name) {
1528 $type = "history of $file_name";
1529 $type .= "/" if ($action eq 'tree');
1530 $type .= " on '$branch'" if (defined $branch);
1532 $type = "log of $branch" if (defined $branch);
1535 $res{-title
} = $type;
1536 $res{'hash'} = (defined $branch ?
"refs/heads/$branch" : undef);
1537 $res{'file_name'} = $file_name;
1542 ## ----------------------------------------------------------------------
1543 ## git utility subroutines, invoking git commands
1545 # returns path to the core git executable and the --git-dir parameter as list
1547 return $GIT, '--git-dir='.$git_dir;
1550 # quote the given arguments for passing them to the shell
1551 # quote_command("command", "arg 1", "arg with ' and ! characters")
1552 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1553 # Try to avoid using this function wherever possible.
1556 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1559 # get HEAD ref of given project as hash
1560 sub git_get_head_hash
{
1561 my $project = shift;
1562 my $o_git_dir = $git_dir;
1564 $git_dir = "$projectroot/$project";
1565 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1568 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1572 if (defined $o_git_dir) {
1573 $git_dir = $o_git_dir;
1578 # get type of given object
1582 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1584 close $fd or return;
1589 # repository configuration
1590 our $config_file = '';
1593 # store multiple values for single key as anonymous array reference
1594 # single values stored directly in the hash, not as [ <value> ]
1595 sub hash_set_multi
{
1596 my ($hash, $key, $value) = @_;
1598 if (!exists $hash->{$key}) {
1599 $hash->{$key} = $value;
1600 } elsif (!ref $hash->{$key}) {
1601 $hash->{$key} = [ $hash->{$key}, $value ];
1603 push @
{$hash->{$key}}, $value;
1607 # return hash of git project configuration
1608 # optionally limited to some section, e.g. 'gitweb'
1609 sub git_parse_project_config
{
1610 my $section_regexp = shift;
1615 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1618 while (my $keyval = <$fh>) {
1620 my ($key, $value) = split(/\n/, $keyval, 2);
1622 hash_set_multi
(\
%config, $key, $value)
1623 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1630 # convert config value to boolean, 'true' or 'false'
1631 # no value, number > 0, 'true' and 'yes' values are true
1632 # rest of values are treated as false (never as error)
1633 sub config_to_bool
{
1636 # strip leading and trailing whitespace
1640 return (!defined $val || # section.key
1641 ($val =~ /^\d+$/ && $val) || # section.key = 1
1642 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1645 # convert config value to simple decimal number
1646 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1647 # to be multiplied by 1024, 1048576, or 1073741824
1651 # strip leading and trailing whitespace
1655 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1657 # unknown unit is treated as 1
1658 return $num * ($unit eq 'g' ?
1073741824 :
1659 $unit eq 'm' ?
1048576 :
1660 $unit eq 'k' ?
1024 : 1);
1665 # convert config value to array reference, if needed
1666 sub config_to_multi
{
1669 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
1672 sub git_get_project_config
{
1673 my ($key, $type) = @_;
1676 return unless ($key);
1677 $key =~ s/^gitweb\.//;
1678 return if ($key =~ m/\W/);
1681 if (defined $type) {
1684 unless ($type eq 'bool' || $type eq 'int');
1688 if (!defined $config_file ||
1689 $config_file ne "$git_dir/config") {
1690 %config = git_parse_project_config
('gitweb');
1691 $config_file = "$git_dir/config";
1695 if (!defined $type) {
1696 return $config{"gitweb.$key"};
1697 } elsif ($type eq 'bool') {
1698 # backward compatibility: 'git config --bool' returns true/false
1699 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
1700 } elsif ($type eq 'int') {
1701 return config_to_int
($config{"gitweb.$key"});
1703 return $config{"gitweb.$key"};
1706 # get hash of given path at given ref
1707 sub git_get_hash_by_path
{
1709 my $path = shift || return undef;
1714 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1715 or die_error
(500, "Open git-ls-tree failed");
1717 close $fd or return undef;
1719 if (!defined $line) {
1720 # there is no tree or hash given by $path at $base
1724 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1725 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1726 if (defined $type && $type ne $2) {
1727 # type doesn't match
1733 # get path of entry with given hash at given tree-ish (ref)
1734 # used to get 'from' filename for combined diff (merge commit) for renames
1735 sub git_get_path_by_hash
{
1736 my $base = shift || return;
1737 my $hash = shift || return;
1741 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1743 while (my $line = <$fd>) {
1746 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1747 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1748 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1757 ## ......................................................................
1758 ## git utility functions, directly accessing git repository
1760 sub git_get_project_description
{
1763 $git_dir = "$projectroot/$path";
1764 open my $fd, "$git_dir/description"
1765 or return git_get_project_config
('description');
1768 if (defined $descr) {
1774 sub git_get_project_ctags
{
1778 $git_dir = "$projectroot/$path";
1779 foreach (<$git_dir/ctags/*>) {
1780 open CT
, $_ or next;
1784 my $ctag = $_; $ctag =~ s
#.*/##;
1785 $ctags->{$ctag} = $val;
1790 sub git_populate_project_tagcloud
{
1793 # First, merge different-cased tags; tags vote on casing
1795 foreach (keys %$ctags) {
1796 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
1797 if (not $ctags_lc{lc $_}->{topcount
}
1798 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
1799 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
1800 $ctags_lc{lc $_}->{topname
} = $_;
1805 if (eval { require HTML
::TagCloud
; 1; }) {
1806 $cloud = HTML
::TagCloud
->new;
1807 foreach (sort keys %ctags_lc) {
1808 # Pad the title with spaces so that the cloud looks
1810 my $title = $ctags_lc{$_}->{topname
};
1811 $title =~ s/ / /g;
1812 $title =~ s/^/ /g;
1813 $title =~ s/$/ /g;
1814 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count
});
1817 $cloud = \
%ctags_lc;
1822 sub git_show_project_tagcloud
{
1823 my ($cloud, $count) = @_;
1824 print STDERR
ref($cloud)."..\n";
1825 if (ref $cloud eq 'HTML::TagCloud') {
1826 return $cloud->html_and_css($count);
1828 my @tags = sort { $cloud->{$a}->{count
} <=> $cloud->{$b}->{count
} } keys %$cloud;
1829 return '<p align="center">' . join (', ', map {
1830 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1831 } splice(@tags, 0, $count)) . '</p>';
1835 sub git_get_project_url_list
{
1838 $git_dir = "$projectroot/$path";
1839 open my $fd, "$git_dir/cloneurl"
1840 or return wantarray ?
1841 @
{ config_to_multi
(git_get_project_config
('url')) } :
1842 config_to_multi
(git_get_project_config
('url'));
1843 my @git_project_url_list = map { chomp; $_ } <$fd>;
1846 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
1849 sub git_get_projects_list
{
1854 $filter =~ s/\.git$//;
1856 my ($check_forks) = gitweb_check_feature
('forks');
1858 if (-d
$projects_list) {
1859 # search in directory
1860 my $dir = $projects_list . ($filter ?
"/$filter" : '');
1861 # remove the trailing "/"
1863 my $pfxlen = length("$dir");
1864 my $pfxdepth = ($dir =~ tr!/!!);
1867 follow_fast
=> 1, # follow symbolic links
1868 follow_skip
=> 2, # ignore duplicates
1869 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1871 # skip project-list toplevel, if we get it.
1872 return if (m!^[/.]$!);
1873 # only directories can be git repositories
1874 return unless (-d
$_);
1875 # don't traverse too deep (Find is super slow on os x)
1876 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1877 $File::Find
::prune
= 1;
1881 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1882 # we check related file in $projectroot
1883 if ($check_forks and $subdir =~ m
#/.#) {
1884 $File::Find
::prune
= 1;
1885 } elsif (check_export_ok
("$projectroot/$filter/$subdir")) {
1886 push @list, { path
=> ($filter ?
"$filter/" : '') . $subdir };
1887 $File::Find
::prune
= 1;
1892 } elsif (-f
$projects_list) {
1893 # read from file(url-encoded):
1894 # 'git%2Fgit.git Linus+Torvalds'
1895 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1896 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1898 open my ($fd), $projects_list or return;
1900 while (my $line = <$fd>) {
1902 my ($path, $owner) = split ' ', $line;
1903 $path = unescape
($path);
1904 $owner = unescape
($owner);
1905 if (!defined $path) {
1908 if ($filter ne '') {
1909 # looking for forks;
1910 my $pfx = substr($path, 0, length($filter));
1911 if ($pfx ne $filter) {
1914 my $sfx = substr($path, length($filter));
1915 if ($sfx !~ /^\/.*\
.git
$/) {
1918 } elsif ($check_forks) {
1920 foreach my $filter (keys %paths) {
1921 # looking for forks;
1922 my $pfx = substr($path, 0, length($filter));
1923 if ($pfx ne $filter) {
1926 my $sfx = substr($path, length($filter));
1927 if ($sfx !~ /^\/.*\
.git
$/) {
1930 # is a fork, don't include it in
1935 if (check_export_ok
("$projectroot/$path")) {
1938 owner
=> to_utf8
($owner),
1941 (my $forks_path = $path) =~ s/\.git$//;
1942 $paths{$forks_path}++;
1950 our $gitweb_project_owner = undef;
1951 sub git_get_project_list_from_file
{
1953 return if (defined $gitweb_project_owner);
1955 $gitweb_project_owner = {};
1956 # read from file (url-encoded):
1957 # 'git%2Fgit.git Linus+Torvalds'
1958 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1959 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1960 if (-f
$projects_list) {
1961 open (my $fd , $projects_list);
1962 while (my $line = <$fd>) {
1964 my ($pr, $ow) = split ' ', $line;
1965 $pr = unescape
($pr);
1966 $ow = unescape
($ow);
1967 $gitweb_project_owner->{$pr} = to_utf8
($ow);
1973 sub git_get_project_owner
{
1974 my $project = shift;
1977 return undef unless $project;
1978 $git_dir = "$projectroot/$project";
1980 if (!defined $gitweb_project_owner) {
1981 git_get_project_list_from_file
();
1984 if (exists $gitweb_project_owner->{$project}) {
1985 $owner = $gitweb_project_owner->{$project};
1987 if (!defined $owner){
1988 $owner = git_get_project_config
('owner');
1990 if (!defined $owner) {
1991 $owner = get_file_owner
("$git_dir");
1997 sub git_get_last_activity
{
2001 $git_dir = "$projectroot/$path";
2002 open($fd, "-|", git_cmd
(), 'for-each-ref',
2003 '--format=%(committer)',
2004 '--sort=-committerdate',
2006 'refs/heads') or return;
2007 my $most_recent = <$fd>;
2008 close $fd or return;
2009 if (defined $most_recent &&
2010 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2012 my $age = time - $timestamp;
2013 return ($age, age_string
($age));
2015 return (undef, undef);
2018 sub git_get_references
{
2019 my $type = shift || "";
2021 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2022 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2023 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
2024 ($type ?
("--", "refs/$type") : ()) # use -- <pattern> if $type
2027 while (my $line = <$fd>) {
2029 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2030 if (defined $refs{$1}) {
2031 push @
{$refs{$1}}, $2;
2037 close $fd or return;
2041 sub git_get_rev_name_tags
{
2042 my $hash = shift || return undef;
2044 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
2046 my $name_rev = <$fd>;
2049 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2052 # catches also '$hash undefined' output
2057 ## ----------------------------------------------------------------------
2058 ## parse to hash functions
2062 my $tz = shift || "-0000";
2065 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2066 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2067 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2068 $date{'hour'} = $hour;
2069 $date{'minute'} = $min;
2070 $date{'mday'} = $mday;
2071 $date{'day'} = $days[$wday];
2072 $date{'month'} = $months[$mon];
2073 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2074 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2075 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2076 $mday, $months[$mon], $hour ,$min;
2077 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2078 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2080 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2081 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2082 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2083 $date{'hour_local'} = $hour;
2084 $date{'minute_local'} = $min;
2085 $date{'tz_local'} = $tz;
2086 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2087 1900+$year, $mon+1, $mday,
2088 $hour, $min, $sec, $tz);
2097 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2098 $tag{'id'} = $tag_id;
2099 while (my $line = <$fd>) {
2101 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2102 $tag{'object'} = $1;
2103 } elsif ($line =~ m/^type (.+)$/) {
2105 } elsif ($line =~ m/^tag (.+)$/) {
2107 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2108 $tag{'author'} = $1;
2111 } elsif ($line =~ m/--BEGIN/) {
2112 push @comment, $line;
2114 } elsif ($line eq "") {
2118 push @comment, <$fd>;
2119 $tag{'comment'} = \
@comment;
2120 close $fd or return;
2121 if (!defined $tag{'name'}) {
2127 sub parse_commit_text
{
2128 my ($commit_text, $withparents) = @_;
2129 my @commit_lines = split '\n', $commit_text;
2132 pop @commit_lines; # Remove '\0'
2134 if (! @commit_lines) {
2138 my $header = shift @commit_lines;
2139 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2142 ($co{'id'}, my @parents) = split ' ', $header;
2143 while (my $line = shift @commit_lines) {
2144 last if $line eq "\n";
2145 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2147 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2149 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2151 $co{'author_epoch'} = $2;
2152 $co{'author_tz'} = $3;
2153 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2154 $co{'author_name'} = $1;
2155 $co{'author_email'} = $2;
2157 $co{'author_name'} = $co{'author'};
2159 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2160 $co{'committer'} = $1;
2161 $co{'committer_epoch'} = $2;
2162 $co{'committer_tz'} = $3;
2163 $co{'committer_name'} = $co{'committer'};
2164 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2165 $co{'committer_name'} = $1;
2166 $co{'committer_email'} = $2;
2168 $co{'committer_name'} = $co{'committer'};
2172 if (!defined $co{'tree'}) {
2175 $co{'parents'} = \
@parents;
2176 $co{'parent'} = $parents[0];
2178 foreach my $title (@commit_lines) {
2181 $co{'title'} = chop_str
($title, 80, 5);
2182 # remove leading stuff of merges to make the interesting part visible
2183 if (length($title) > 50) {
2184 $title =~ s/^Automatic //;
2185 $title =~ s/^merge (of|with) /Merge ... /i;
2186 if (length($title) > 50) {
2187 $title =~ s/(http|rsync):\/\///;
2189 if (length($title) > 50) {
2190 $title =~ s/(master|www|rsync)\.//;
2192 if (length($title) > 50) {
2193 $title =~ s/kernel.org:?//;
2195 if (length($title) > 50) {
2196 $title =~ s/\/pub\/scm//;
2199 $co{'title_short'} = chop_str
($title, 50, 5);
2203 if (! defined $co{'title'} || $co{'title'} eq "") {
2204 $co{'title'} = $co{'title_short'} = '(no commit message)';
2206 # remove added spaces
2207 foreach my $line (@commit_lines) {
2210 $co{'comment'} = \
@commit_lines;
2212 my $age = time - $co{'committer_epoch'};
2214 $co{'age_string'} = age_string
($age);
2215 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2216 if ($age > 60*60*24*7*2) {
2217 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2218 $co{'age_string_age'} = $co{'age_string'};
2220 $co{'age_string_date'} = $co{'age_string'};
2221 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2227 my ($commit_id) = @_;
2232 open my $fd, "-|", git_cmd
(), "rev-list",
2238 or die_error
(500, "Open git-rev-list failed");
2239 %co = parse_commit_text
(<$fd>, 1);
2246 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2254 open my $fd, "-|", git_cmd
(), "rev-list",
2257 ("--max-count=" . $maxcount),
2258 ("--skip=" . $skip),
2262 ($filename ?
($filename) : ())
2263 or die_error
(500, "Open git-rev-list failed");
2264 while (my $line = <$fd>) {
2265 my %co = parse_commit_text
($line);
2270 return wantarray ?
@cos : \
@cos;
2273 # parse line of git-diff-tree "raw" output
2274 sub parse_difftree_raw_line
{
2278 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2279 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2280 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2281 $res{'from_mode'} = $1;
2282 $res{'to_mode'} = $2;
2283 $res{'from_id'} = $3;
2285 $res{'status'} = $5;
2286 $res{'similarity'} = $6;
2287 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2288 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2290 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2293 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2294 # combined diff (for merge commit)
2295 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2296 $res{'nparents'} = length($1);
2297 $res{'from_mode'} = [ split(' ', $2) ];
2298 $res{'to_mode'} = pop @
{$res{'from_mode'}};
2299 $res{'from_id'} = [ split(' ', $3) ];
2300 $res{'to_id'} = pop @
{$res{'from_id'}};
2301 $res{'status'} = [ split('', $4) ];
2302 $res{'to_file'} = unquote
($5);
2304 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2305 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2306 $res{'commit'} = $1;
2309 return wantarray ?
%res : \
%res;
2312 # wrapper: return parsed line of git-diff-tree "raw" output
2313 # (the argument might be raw line, or parsed info)
2314 sub parsed_difftree_line
{
2315 my $line_or_ref = shift;
2317 if (ref($line_or_ref) eq "HASH") {
2318 # pre-parsed (or generated by hand)
2319 return $line_or_ref;
2321 return parse_difftree_raw_line
($line_or_ref);
2325 # parse line of git-ls-tree output
2326 sub parse_ls_tree_line
($;%) {
2331 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2332 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2340 $res{'name'} = unquote
($4);
2343 return wantarray ?
%res : \
%res;
2346 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2347 sub parse_from_to_diffinfo
{
2348 my ($diffinfo, $from, $to, @parents) = @_;
2350 if ($diffinfo->{'nparents'}) {
2352 $from->{'file'} = [];
2353 $from->{'href'} = [];
2354 fill_from_file_info
($diffinfo, @parents)
2355 unless exists $diffinfo->{'from_file'};
2356 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2357 $from->{'file'}[$i] =
2358 defined $diffinfo->{'from_file'}[$i] ?
2359 $diffinfo->{'from_file'}[$i] :
2360 $diffinfo->{'to_file'};
2361 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2362 $from->{'href'}[$i] = href
(action
=>"blob",
2363 hash_base
=>$parents[$i],
2364 hash
=>$diffinfo->{'from_id'}[$i],
2365 file_name
=>$from->{'file'}[$i]);
2367 $from->{'href'}[$i] = undef;
2371 # ordinary (not combined) diff
2372 $from->{'file'} = $diffinfo->{'from_file'};
2373 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2374 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2375 hash
=>$diffinfo->{'from_id'},
2376 file_name
=>$from->{'file'});
2378 delete $from->{'href'};
2382 $to->{'file'} = $diffinfo->{'to_file'};
2383 if (!is_deleted
($diffinfo)) { # file exists in result
2384 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2385 hash
=>$diffinfo->{'to_id'},
2386 file_name
=>$to->{'file'});
2388 delete $to->{'href'};
2392 ## ......................................................................
2393 ## parse to array of hashes functions
2395 sub git_get_heads_list
{
2399 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2400 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
2401 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2404 while (my $line = <$fd>) {
2408 my ($refinfo, $committerinfo) = split(/\0/, $line);
2409 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2410 my ($committer, $epoch, $tz) =
2411 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2412 $ref_item{'fullname'} = $name;
2413 $name =~ s!^refs/heads/!!;
2415 $ref_item{'name'} = $name;
2416 $ref_item{'id'} = $hash;
2417 $ref_item{'title'} = $title || '(no commit message)';
2418 $ref_item{'epoch'} = $epoch;
2420 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2422 $ref_item{'age'} = "unknown";
2425 push @headslist, \
%ref_item;
2429 return wantarray ?
@headslist : \
@headslist;
2432 sub git_get_tags_list
{
2436 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2437 ($limit ?
'--count='.($limit+1) : ()), '--sort=-creatordate',
2438 '--format=%(objectname) %(objecttype) %(refname) '.
2439 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2442 while (my $line = <$fd>) {
2446 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2447 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2448 my ($creator, $epoch, $tz) =
2449 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2450 $ref_item{'fullname'} = $name;
2451 $name =~ s!^refs/tags/!!;
2453 $ref_item{'type'} = $type;
2454 $ref_item{'id'} = $id;
2455 $ref_item{'name'} = $name;
2456 if ($type eq "tag") {
2457 $ref_item{'subject'} = $title;
2458 $ref_item{'reftype'} = $reftype;
2459 $ref_item{'refid'} = $refid;
2461 $ref_item{'reftype'} = $type;
2462 $ref_item{'refid'} = $id;
2465 if ($type eq "tag" || $type eq "commit") {
2466 $ref_item{'epoch'} = $epoch;
2468 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2470 $ref_item{'age'} = "unknown";
2474 push @tagslist, \
%ref_item;
2478 return wantarray ?
@tagslist : \
@tagslist;
2481 ## ----------------------------------------------------------------------
2482 ## filesystem-related functions
2484 sub get_file_owner
{
2487 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2488 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2489 if (!defined $gcos) {
2493 $owner =~ s/[,;].*$//;
2494 return to_utf8
($owner);
2497 ## ......................................................................
2498 ## mimetype related functions
2500 sub mimetype_guess_file
{
2501 my $filename = shift;
2502 my $mimemap = shift;
2503 -r
$mimemap or return undef;
2506 open(MIME
, $mimemap) or return undef;
2508 next if m/^#/; # skip comments
2509 my ($mime, $exts) = split(/\t+/);
2510 if (defined $exts) {
2511 my @exts = split(/\s+/, $exts);
2512 foreach my $ext (@exts) {
2513 $mimemap{$ext} = $mime;
2519 $filename =~ /\.([^.]*)$/;
2520 return $mimemap{$1};
2523 sub mimetype_guess
{
2524 my $filename = shift;
2526 $filename =~ /\./ or return undef;
2528 if ($mimetypes_file) {
2529 my $file = $mimetypes_file;
2530 if ($file !~ m!^/!) { # if it is relative path
2531 # it is relative to project
2532 $file = "$projectroot/$project/$file";
2534 $mime = mimetype_guess_file
($filename, $file);
2536 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2542 my $filename = shift;
2545 my $mime = mimetype_guess
($filename);
2546 $mime and return $mime;
2550 return $default_blob_plain_mimetype unless $fd;
2553 return 'text/plain';
2554 } elsif (! $filename) {
2555 return 'application/octet-stream';
2556 } elsif ($filename =~ m/\.png$/i) {
2558 } elsif ($filename =~ m/\.gif$/i) {
2560 } elsif ($filename =~ m/\.jpe?g$/i) {
2561 return 'image/jpeg';
2563 return 'application/octet-stream';
2567 sub blob_contenttype
{
2568 my ($fd, $file_name, $type) = @_;
2570 $type ||= blob_mimetype
($fd, $file_name);
2571 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2572 $type .= "; charset=$default_text_plain_charset";
2578 ## ======================================================================
2579 ## functions printing HTML: header, footer, error page
2581 sub git_header_html
{
2582 my $status = shift || "200 OK";
2583 my $expires = shift;
2585 my $title = "$site_name";
2586 if (defined $project) {
2587 $title .= " - " . to_utf8
($project);
2588 if (defined $action) {
2589 $title .= "/$action";
2590 if (defined $file_name) {
2591 $title .= " - " . esc_path
($file_name);
2592 if ($action eq "tree" && $file_name !~ m
|/$|) {
2599 # require explicit support from the UA if we are to send the page as
2600 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2601 # we have to do this because MSIE sometimes globs '*/*', pretending to
2602 # support xhtml+xml but choking when it gets what it asked for.
2603 if (defined $cgi->http('HTTP_ACCEPT') &&
2604 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2605 $cgi->Accept('application/xhtml+xml') != 0) {
2606 $content_type = 'application/xhtml+xml';
2608 $content_type = 'text/html';
2610 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2611 -status
=> $status, -expires
=> $expires);
2612 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
2614 <?xml version="1.0" encoding="utf-8"?>
2615 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2616 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2617 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2618 <!-- git core binaries version $git_version -->
2620 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2621 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2622 <meta name="robots" content="index, nofollow"/>
2623 <title>$title</title>
2625 # print out each stylesheet that exist
2626 if (defined $stylesheet) {
2627 #provides backwards capability for those people who define style sheet in a config file
2628 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2630 foreach my $stylesheet (@stylesheets) {
2631 next unless $stylesheet;
2632 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2635 if (defined $project) {
2636 my %href_params = get_feed_info
();
2637 if (!exists $href_params{'-title'}) {
2638 $href_params{'-title'} = 'log';
2641 foreach my $format qw(RSS Atom) {
2642 my $type = lc($format);
2644 '-rel' => 'alternate',
2645 '-title' => "$project - $href_params{'-title'} - $format feed",
2646 '-type' => "application/$type+xml"
2649 $href_params{'action'} = $type;
2650 $link_attr{'-href'} = href
(%href_params);
2652 "rel=\"$link_attr{'-rel'}\" ".
2653 "title=\"$link_attr{'-title'}\" ".
2654 "href=\"$link_attr{'-href'}\" ".
2655 "type=\"$link_attr{'-type'}\" ".
2658 $href_params{'extra_options'} = '--no-merges';
2659 $link_attr{'-href'} = href
(%href_params);
2660 $link_attr{'-title'} .= ' (no merges)';
2662 "rel=\"$link_attr{'-rel'}\" ".
2663 "title=\"$link_attr{'-title'}\" ".
2664 "href=\"$link_attr{'-href'}\" ".
2665 "type=\"$link_attr{'-type'}\" ".
2670 printf('<link rel="alternate" title="%s projects list" '.
2671 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2672 $site_name, href
(project
=>undef, action
=>"project_index"));
2673 printf('<link rel="alternate" title="%s projects feeds" '.
2674 'href="%s" type="text/x-opml" />'."\n",
2675 $site_name, href
(project
=>undef, action
=>"opml"));
2677 if (defined $favicon) {
2678 print qq(<link rel
="shortcut icon" href
="$favicon" type
="image/png" />\n);
2684 if (-f
$site_header) {
2685 open (my $fd, $site_header);
2690 print "<div class=\"page_header\">\n" .
2691 $cgi->a({-href
=> esc_url
($logo_url),
2692 -title
=> $logo_label},
2693 qq(<img src
="$logo" width
="72" height
="27" alt
="git" class="logo"/>));
2694 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2695 if (defined $project) {
2696 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2697 if (defined $action) {
2704 my ($have_search) = gitweb_check_feature
('search');
2705 if (defined $project && $have_search) {
2706 if (!defined $searchtext) {
2710 if (defined $hash_base) {
2711 $search_hash = $hash_base;
2712 } elsif (defined $hash) {
2713 $search_hash = $hash;
2715 $search_hash = "HEAD";
2717 my $action = $my_uri;
2718 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2719 if ($use_pathinfo) {
2720 $action .= "/".esc_url
($project);
2722 print $cgi->startform(-method
=> "get", -action
=> $action) .
2723 "<div class=\"search\">\n" .
2725 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2726 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2727 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2728 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2729 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2730 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2732 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2733 "<span title=\"Extended regular expression\">" .
2734 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2735 -checked
=> $search_use_regexp) .
2738 $cgi->end_form() . "\n";
2742 sub git_footer_html
{
2743 my $feed_class = 'rss_logo';
2745 print "<div class=\"page_footer\">\n";
2746 if (defined $project) {
2747 my $descr = git_get_project_description
($project);
2748 if (defined $descr) {
2749 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2752 my %href_params = get_feed_info
();
2753 if (!%href_params) {
2754 $feed_class .= ' generic';
2756 $href_params{'-title'} ||= 'log';
2758 foreach my $format qw(RSS Atom) {
2759 $href_params{'action'} = lc($format);
2760 print $cgi->a({-href
=> href
(%href_params),
2761 -title
=> "$href_params{'-title'} $format feed",
2762 -class => $feed_class}, $format)."\n";
2766 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2767 -class => $feed_class}, "OPML") . " ";
2768 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2769 -class => $feed_class}, "TXT") . "\n";
2771 print "</div>\n"; # class="page_footer"
2773 if (-f
$site_footer) {
2774 open (my $fd, $site_footer);
2783 # die_error(<http_status_code>, <error_message>)
2784 # Example: die_error(404, 'Hash not found')
2785 # By convention, use the following status codes (as defined in RFC 2616):
2786 # 400: Invalid or missing CGI parameters, or
2787 # requested object exists but has wrong type.
2788 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2789 # this server or project.
2790 # 404: Requested object/revision/project doesn't exist.
2791 # 500: The server isn't configured properly, or
2792 # an internal error occurred (e.g. failed assertions caused by bugs), or
2793 # an unknown error occurred (e.g. the git binary died unexpectedly).
2795 my $status = shift || 500;
2796 my $error = shift || "Internal server error";
2798 my %http_responses = (400 => '400 Bad Request',
2799 403 => '403 Forbidden',
2800 404 => '404 Not Found',
2801 500 => '500 Internal Server Error');
2802 git_header_html
($http_responses{$status});
2804 <div class="page_body">
2814 ## ----------------------------------------------------------------------
2815 ## functions printing or outputting HTML: navigation
2817 sub git_print_page_nav
{
2818 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2819 $extra = '' if !defined $extra; # pager or formats
2821 my @navs = qw(summary shortlog log commit commitdiff tree);
2823 @navs = grep { $_ ne $suppress } @navs;
2826 my %arg = map { $_ => {action
=>$_} } @navs;
2827 if (defined $head) {
2828 for (qw(commit commitdiff)) {
2829 $arg{$_}{'hash'} = $head;
2831 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2832 for (qw(shortlog log)) {
2833 $arg{$_}{'hash'} = $head;
2837 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2838 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2840 print "<div class=\"page_nav\">\n" .
2842 map { $_ eq $current ?
2843 $_ : $cgi->a({-href
=> href
(%{$arg{$_}})}, "$_")
2845 print "<br/>\n$extra<br/>\n" .
2849 sub format_paging_nav
{
2850 my ($action, $hash, $head, $page, $has_next_link) = @_;
2854 if ($hash ne $head || $page) {
2855 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2857 $paging_nav .= "HEAD";
2861 $paging_nav .= " ⋅ " .
2862 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2863 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2865 $paging_nav .= " ⋅ prev";
2868 if ($has_next_link) {
2869 $paging_nav .= " ⋅ " .
2870 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2871 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2873 $paging_nav .= " ⋅ next";
2879 ## ......................................................................
2880 ## functions printing or outputting HTML: div
2882 sub git_print_header_div
{
2883 my ($action, $title, $hash, $hash_base) = @_;
2886 $args{'action'} = $action;
2887 $args{'hash'} = $hash if $hash;
2888 $args{'hash_base'} = $hash_base if $hash_base;
2890 print "<div class=\"header\">\n" .
2891 $cgi->a({-href
=> href
(%args), -class => "title"},
2892 $title ?
$title : $action) .
2896 #sub git_print_authorship (\%) {
2897 sub git_print_authorship
{
2900 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
2901 print "<div class=\"author_date\">" .
2902 esc_html
($co->{'author_name'}) .
2904 if ($ad{'hour_local'} < 6) {
2905 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2906 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2908 printf(" (%02d:%02d %s)",
2909 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2914 sub git_print_page_path
{
2920 print "<div class=\"page_path\">";
2921 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
2922 -title
=> 'tree root'}, to_utf8
("[$project]"));
2924 if (defined $name) {
2925 my @dirname = split '/', $name;
2926 my $basename = pop @dirname;
2929 foreach my $dir (@dirname) {
2930 $fullname .= ($fullname ?
'/' : '') . $dir;
2931 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
2933 -title
=> $fullname}, esc_path
($dir));
2936 if (defined $type && $type eq 'blob') {
2937 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
2939 -title
=> $name}, esc_path
($basename));
2940 } elsif (defined $type && $type eq 'tree') {
2941 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
2943 -title
=> $name}, esc_path
($basename));
2946 print esc_path
($basename);
2949 print "<br/></div>\n";
2952 # sub git_print_log (\@;%) {
2953 sub git_print_log
($;%) {
2957 if ($opts{'-remove_title'}) {
2958 # remove title, i.e. first line of log
2961 # remove leading empty lines
2962 while (defined $log->[0] && $log->[0] eq "") {
2969 foreach my $line (@
$log) {
2970 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2973 if (! $opts{'-remove_signoff'}) {
2974 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
2977 # remove signoff lines
2984 # print only one empty line
2985 # do not print empty line after signoff
2987 next if ($empty || $signoff);
2993 print format_log_line_html
($line) . "<br/>\n";
2996 if ($opts{'-final_empty_line'}) {
2997 # end with single empty line
2998 print "<br/>\n" unless $empty;
3002 # return link target (what link points to)
3003 sub git_get_link_target
{
3008 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
3012 $link_target = <$fd>;
3017 return $link_target;
3020 # given link target, and the directory (basedir) the link is in,
3021 # return target of link relative to top directory (top tree);
3022 # return undef if it is not possible (including absolute links).
3023 sub normalize_link_target
{
3024 my ($link_target, $basedir, $hash_base) = @_;
3026 # we can normalize symlink target only if $hash_base is provided
3027 return unless $hash_base;
3029 # absolute symlinks (beginning with '/') cannot be normalized
3030 return if (substr($link_target, 0, 1) eq '/');
3032 # normalize link target to path from top (root) tree (dir)
3035 $path = $basedir . '/' . $link_target;
3037 # we are in top (root) tree (dir)
3038 $path = $link_target;
3041 # remove //, /./, and /../
3043 foreach my $part (split('/', $path)) {
3044 # discard '.' and ''
3045 next if (!$part || $part eq '.');
3047 if ($part eq '..') {
3051 # link leads outside repository (outside top dir)
3055 push @path_parts, $part;
3058 $path = join('/', @path_parts);
3063 # print tree entry (row of git_tree), but without encompassing <tr> element
3064 sub git_print_tree_entry
{
3065 my ($t, $basedir, $hash_base, $have_blame) = @_;
3068 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3070 # The format of a table row is: mode list link. Where mode is
3071 # the mode of the entry, list is the name of the entry, an href,
3072 # and link is the action links of the entry.
3074 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3075 if ($t->{'type'} eq "blob") {
3076 print "<td class=\"list\">" .
3077 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3078 file_name
=>"$basedir$t->{'name'}", %base_key),
3079 -class => "list"}, esc_path
($t->{'name'}));
3080 if (S_ISLNK
(oct $t->{'mode'})) {
3081 my $link_target = git_get_link_target
($t->{'hash'});
3083 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3084 if (defined $norm_target) {
3086 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3087 file_name
=>$norm_target),
3088 -title
=> $norm_target}, esc_path
($link_target));
3090 print " -> " . esc_path
($link_target);
3095 print "<td class=\"link\">";
3096 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3097 file_name
=>"$basedir$t->{'name'}", %base_key)},
3101 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3102 file_name
=>"$basedir$t->{'name'}", %base_key)},
3105 if (defined $hash_base) {
3107 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3108 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3112 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3113 file_name
=>"$basedir$t->{'name'}")},
3117 } elsif ($t->{'type'} eq "tree") {
3118 print "<td class=\"list\">";
3119 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3120 file_name
=>"$basedir$t->{'name'}", %base_key)},
3121 esc_path
($t->{'name'}));
3123 print "<td class=\"link\">";
3124 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3125 file_name
=>"$basedir$t->{'name'}", %base_key)},
3127 if (defined $hash_base) {
3129 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3130 file_name
=>"$basedir$t->{'name'}")},
3135 # unknown object: we can only present history for it
3136 # (this includes 'commit' object, i.e. submodule support)
3137 print "<td class=\"list\">" .
3138 esc_path
($t->{'name'}) .
3140 print "<td class=\"link\">";
3141 if (defined $hash_base) {
3142 print $cgi->a({-href
=> href
(action
=>"history",
3143 hash_base
=>$hash_base,
3144 file_name
=>"$basedir$t->{'name'}")},
3151 ## ......................................................................
3152 ## functions printing large fragments of HTML
3154 # get pre-image filenames for merge (combined) diff
3155 sub fill_from_file_info
{
3156 my ($diff, @parents) = @_;
3158 $diff->{'from_file'} = [ ];
3159 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3160 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3161 if ($diff->{'status'}[$i] eq 'R' ||
3162 $diff->{'status'}[$i] eq 'C') {
3163 $diff->{'from_file'}[$i] =
3164 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3171 # is current raw difftree line of file deletion
3173 my $diffinfo = shift;
3175 return $diffinfo->{'to_id'} eq ('0' x
40);
3178 # does patch correspond to [previous] difftree raw line
3179 # $diffinfo - hashref of parsed raw diff format
3180 # $patchinfo - hashref of parsed patch diff format
3181 # (the same keys as in $diffinfo)
3182 sub is_patch_split
{
3183 my ($diffinfo, $patchinfo) = @_;
3185 return defined $diffinfo && defined $patchinfo
3186 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3190 sub git_difftree_body
{
3191 my ($difftree, $hash, @parents) = @_;
3192 my ($parent) = $parents[0];
3193 my ($have_blame) = gitweb_check_feature
('blame');
3194 print "<div class=\"list_head\">\n";
3195 if ($#{$difftree} > 10) {
3196 print(($#{$difftree} + 1) . " files changed:\n");
3200 print "<table class=\"" .
3201 (@parents > 1 ?
"combined " : "") .
3204 # header only for combined diff in 'commitdiff' view
3205 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
3208 print "<thead><tr>\n" .
3209 "<th></th><th></th>\n"; # filename, patchN link
3210 for (my $i = 0; $i < @parents; $i++) {
3211 my $par = $parents[$i];
3213 $cgi->a({-href
=> href
(action
=>"commitdiff",
3214 hash
=>$hash, hash_parent
=>$par),
3215 -title
=> 'commitdiff to parent number ' .
3216 ($i+1) . ': ' . substr($par,0,7)},
3220 print "</tr></thead>\n<tbody>\n";
3225 foreach my $line (@
{$difftree}) {
3226 my $diff = parsed_difftree_line
($line);
3229 print "<tr class=\"dark\">\n";
3231 print "<tr class=\"light\">\n";
3235 if (exists $diff->{'nparents'}) { # combined diff
3237 fill_from_file_info
($diff, @parents)
3238 unless exists $diff->{'from_file'};
3240 if (!is_deleted
($diff)) {
3241 # file exists in the result (child) commit
3243 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3244 file_name
=>$diff->{'to_file'},
3246 -class => "list"}, esc_path
($diff->{'to_file'})) .
3250 esc_path
($diff->{'to_file'}) .
3254 if ($action eq 'commitdiff') {
3257 print "<td class=\"link\">" .
3258 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3263 my $has_history = 0;
3264 my $not_deleted = 0;
3265 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3266 my $hash_parent = $parents[$i];
3267 my $from_hash = $diff->{'from_id'}[$i];
3268 my $from_path = $diff->{'from_file'}[$i];
3269 my $status = $diff->{'status'}[$i];
3271 $has_history ||= ($status ne 'A');
3272 $not_deleted ||= ($status ne 'D');
3274 if ($status eq 'A') {
3275 print "<td class=\"link\" align=\"right\"> | </td>\n";
3276 } elsif ($status eq 'D') {
3277 print "<td class=\"link\">" .
3278 $cgi->a({-href
=> href
(action
=>"blob",
3281 file_name
=>$from_path)},
3285 if ($diff->{'to_id'} eq $from_hash) {
3286 print "<td class=\"link nochange\">";
3288 print "<td class=\"link\">";
3290 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3291 hash
=>$diff->{'to_id'},
3292 hash_parent
=>$from_hash,
3294 hash_parent_base
=>$hash_parent,
3295 file_name
=>$diff->{'to_file'},
3296 file_parent
=>$from_path)},
3302 print "<td class=\"link\">";
3304 print $cgi->a({-href
=> href
(action
=>"blob",
3305 hash
=>$diff->{'to_id'},
3306 file_name
=>$diff->{'to_file'},
3309 print " | " if ($has_history);
3312 print $cgi->a({-href
=> href
(action
=>"history",
3313 file_name
=>$diff->{'to_file'},
3320 next; # instead of 'else' clause, to avoid extra indent
3322 # else ordinary diff
3324 my ($to_mode_oct, $to_mode_str, $to_file_type);
3325 my ($from_mode_oct, $from_mode_str, $from_file_type);
3326 if ($diff->{'to_mode'} ne ('0' x
6)) {
3327 $to_mode_oct = oct $diff->{'to_mode'};
3328 if (S_ISREG
($to_mode_oct)) { # only for regular file
3329 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3331 $to_file_type = file_type
($diff->{'to_mode'});
3333 if ($diff->{'from_mode'} ne ('0' x
6)) {
3334 $from_mode_oct = oct $diff->{'from_mode'};
3335 if (S_ISREG
($to_mode_oct)) { # only for regular file
3336 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3338 $from_file_type = file_type
($diff->{'from_mode'});
3341 if ($diff->{'status'} eq "A") { # created
3342 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3343 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3344 $mode_chng .= "]</span>";
3346 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3347 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3348 -class => "list"}, esc_path
($diff->{'file'}));
3350 print "<td>$mode_chng</td>\n";
3351 print "<td class=\"link\">";
3352 if ($action eq 'commitdiff') {
3355 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3358 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3359 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3363 } elsif ($diff->{'status'} eq "D") { # deleted
3364 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3366 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3367 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3368 -class => "list"}, esc_path
($diff->{'file'}));
3370 print "<td>$mode_chng</td>\n";
3371 print "<td class=\"link\">";
3372 if ($action eq 'commitdiff') {
3375 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3378 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3379 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3382 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3383 file_name
=>$diff->{'file'})},
3386 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3387 file_name
=>$diff->{'file'})},
3391 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3392 my $mode_chnge = "";
3393 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3394 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3395 if ($from_file_type ne $to_file_type) {
3396 $mode_chnge .= " from $from_file_type to $to_file_type";
3398 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3399 if ($from_mode_str && $to_mode_str) {
3400 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3401 } elsif ($to_mode_str) {
3402 $mode_chnge .= " mode: $to_mode_str";
3405 $mode_chnge .= "]</span>\n";
3408 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3409 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3410 -class => "list"}, esc_path
($diff->{'file'}));
3412 print "<td>$mode_chnge</td>\n";
3413 print "<td class=\"link\">";
3414 if ($action eq 'commitdiff') {
3417 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3419 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3420 # "commit" view and modified file (not onlu mode changed)
3421 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3422 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3423 hash_base
=>$hash, hash_parent_base
=>$parent,
3424 file_name
=>$diff->{'file'})},
3428 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3429 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3432 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3433 file_name
=>$diff->{'file'})},
3436 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3437 file_name
=>$diff->{'file'})},
3441 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3442 my %status_name = ('R' => 'moved', 'C' => 'copied');
3443 my $nstatus = $status_name{$diff->{'status'}};
3445 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3446 # mode also for directories, so we cannot use $to_mode_str
3447 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3450 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3451 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3452 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3453 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3454 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3455 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3456 -class => "list"}, esc_path
($diff->{'from_file'})) .
3457 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3458 "<td class=\"link\">";
3459 if ($action eq 'commitdiff') {
3462 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3464 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3465 # "commit" view and modified file (not only pure rename or copy)
3466 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3467 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3468 hash_base
=>$hash, hash_parent_base
=>$parent,
3469 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3473 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3474 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3477 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3478 file_name
=>$diff->{'to_file'})},
3481 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3482 file_name
=>$diff->{'to_file'})},
3486 } # we should not encounter Unmerged (U) or Unknown (X) status
3489 print "</tbody>" if $has_header;
3493 sub git_patchset_body
{
3494 my ($fd, $difftree, $hash, @hash_parents) = @_;
3495 my ($hash_parent) = $hash_parents[0];
3497 my $is_combined = (@hash_parents > 1);
3499 my $patch_number = 0;
3505 print "<div class=\"patchset\">\n";
3507 # skip to first patch
3508 while ($patch_line = <$fd>) {
3511 last if ($patch_line =~ m/^diff /);
3515 while ($patch_line) {
3517 # parse "git diff" header line
3518 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3519 # $1 is from_name, which we do not use
3520 $to_name = unquote
($2);
3521 $to_name =~ s!^b/!!;
3522 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3523 # $1 is 'cc' or 'combined', which we do not use
3524 $to_name = unquote
($2);
3529 # check if current patch belong to current raw line
3530 # and parse raw git-diff line if needed
3531 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3532 # this is continuation of a split patch
3533 print "<div class=\"patch cont\">\n";
3535 # advance raw git-diff output if needed
3536 $patch_idx++ if defined $diffinfo;
3538 # read and prepare patch information
3539 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3541 # compact combined diff output can have some patches skipped
3542 # find which patch (using pathname of result) we are at now;
3544 while ($to_name ne $diffinfo->{'to_file'}) {
3545 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3546 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3547 "</div>\n"; # class="patch"
3552 last if $patch_idx > $#$difftree;
3553 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3557 # modifies %from, %to hashes
3558 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3560 # this is first patch for raw difftree line with $patch_idx index
3561 # we index @$difftree array from 0, but number patches from 1
3562 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3566 #assert($patch_line =~ m/^diff /) if DEBUG;
3567 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3569 # print "git diff" header
3570 print format_git_diff_header_line
($patch_line, $diffinfo,
3573 # print extended diff header
3574 print "<div class=\"diff extended_header\">\n";
3576 while ($patch_line = <$fd>) {
3579 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3581 print format_extended_diff_header_line
($patch_line, $diffinfo,
3584 print "</div>\n"; # class="diff extended_header"
3586 # from-file/to-file diff header
3587 if (! $patch_line) {
3588 print "</div>\n"; # class="patch"
3591 next PATCH
if ($patch_line =~ m/^diff /);
3592 #assert($patch_line =~ m/^---/) if DEBUG;
3594 my $last_patch_line = $patch_line;
3595 $patch_line = <$fd>;
3597 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3599 print format_diff_from_to_header
($last_patch_line, $patch_line,
3600 $diffinfo, \
%from, \
%to,
3605 while ($patch_line = <$fd>) {
3608 next PATCH
if ($patch_line =~ m/^diff /);
3610 print format_diff_line
($patch_line, \
%from, \
%to);
3614 print "</div>\n"; # class="patch"
3617 # for compact combined (--cc) format, with chunk and patch simpliciaction
3618 # patchset might be empty, but there might be unprocessed raw lines
3619 for (++$patch_idx if $patch_number > 0;
3620 $patch_idx < @
$difftree;
3622 # read and prepare patch information
3623 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3625 # generate anchor for "patch" links in difftree / whatchanged part
3626 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3627 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3628 "</div>\n"; # class="patch"
3633 if ($patch_number == 0) {
3634 if (@hash_parents > 1) {
3635 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3637 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3641 print "</div>\n"; # class="patchset"
3644 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3646 # fills project list info (age, description, owner, forks) for each
3647 # project in the list, removing invalid projects from returned list
3648 # NOTE: modifies $projlist, but does not remove entries from it
3649 sub fill_project_list_info
{
3650 my ($projlist, $check_forks) = @_;
3653 my $show_ctags = gitweb_check_feature
('ctags');
3655 foreach my $pr (@
$projlist) {
3656 my (@activity) = git_get_last_activity
($pr->{'path'});
3657 unless (@activity) {
3660 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3661 if (!defined $pr->{'descr'}) {
3662 my $descr = git_get_project_description
($pr->{'path'}) || "";
3663 $descr = to_utf8
($descr);
3664 $pr->{'descr_long'} = $descr;
3665 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3667 if (!defined $pr->{'owner'}) {
3668 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3671 my $pname = $pr->{'path'};
3672 if (($pname =~ s/\.git$//) &&
3673 ($pname !~ /\/$/) &&
3674 (-d
"$projectroot/$pname")) {
3675 $pr->{'forks'} = "-d $projectroot/$pname";
3680 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
3681 push @projects, $pr;
3687 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3688 # (changing $list), or generating 'sort by $name' replay link otherwise
3690 my ($str_sort, $name, $order, $key, $header, $list) = @_;
3692 $header ||= ucfirst($name);
3694 if ($order eq $name) {
3696 @
$list = sort {$a->{$key} cmp $b->{$key}} @
$list;
3698 @
$list = sort {$a->{$key} <=> $b->{$key}} @
$list;
3700 print "<th>$header</th>\n";
3703 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3704 -class => "header"}, $header) .
3709 sub print_sort_th_str
{
3710 print_sort_th
(1, @_);
3713 sub print_sort_th_num
{
3714 print_sort_th
(0, @_);
3717 sub git_project_list_body
{
3718 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3720 my ($check_forks) = gitweb_check_feature
('forks');
3721 my @projects = fill_project_list_info
($projlist, $check_forks);
3723 $order ||= $default_projects_order;
3724 $from = 0 unless defined $from;
3725 $to = $#projects if (!defined $to || $#projects < $to);
3727 my $show_ctags = gitweb_check_feature
('ctags');
3730 foreach my $p (@projects) {
3731 foreach my $ct (keys %{$p->{'ctags'}}) {
3732 $ctags{$ct} += $p->{'ctags'}->{$ct};
3735 my $cloud = git_populate_project_tagcloud
(\
%ctags);
3736 print git_show_project_tagcloud
($cloud, 64);
3739 print "<table class=\"project_list\">\n";
3740 unless ($no_header) {
3743 print "<th></th>\n";
3745 print_sort_th_str
('project', $order, 'path',
3746 'Project', \
@projects);
3747 print_sort_th_str
('descr', $order, 'descr_long',
3748 'Description', \
@projects);
3749 print_sort_th_str
('owner', $order, 'owner',
3750 'Owner', \
@projects);
3751 print_sort_th_num
('age', $order, 'age',
3752 'Last Change', \
@projects);
3753 print "<th></th>\n" . # for links
3757 my $tagfilter = $cgi->param('by_tag');
3758 for (my $i = $from; $i <= $to; $i++) {
3759 my $pr = $projects[$i];
3760 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3762 print "<tr class=\"dark\">\n";
3764 print "<tr class=\"light\">\n";
3769 if ($pr->{'forks'}) {
3770 print "<!-- $pr->{'forks'} -->\n";
3771 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3775 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3776 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3777 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3778 -class => "list", -title
=> $pr->{'descr_long'}},
3779 esc_html
($pr->{'descr'})) . "</td>\n" .
3780 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3781 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3782 (defined $pr->{'age_string'} ?
$pr->{'age_string'} : "No commits") . "</td>\n" .
3783 "<td class=\"link\">" .
3784 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3785 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3786 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3787 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3788 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3792 if (defined $extra) {
3795 print "<td></td>\n";
3797 print "<td colspan=\"5\">$extra</td>\n" .
3803 sub git_shortlog_body
{
3804 # uses global variable $project
3805 my ($commitlist, $from, $to, $refs, $extra) = @_;
3807 $from = 0 unless defined $from;
3808 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3810 print "<table class=\"shortlog\">\n";
3812 for (my $i = $from; $i <= $to; $i++) {
3813 my %co = %{$commitlist->[$i]};
3814 my $commit = $co{'id'};
3815 my $ref = format_ref_marker
($refs, $commit);
3817 print "<tr class=\"dark\">\n";
3819 print "<tr class=\"light\">\n";
3822 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3823 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3824 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3825 "<td><i>" . $author . "</i></td>\n" .
3827 print format_subject_html
($co{'title'}, $co{'title_short'},
3828 href
(action
=>"commit", hash
=>$commit), $ref);
3830 "<td class=\"link\">" .
3831 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3832 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3833 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3834 my $snapshot_links = format_snapshot_links
($commit);
3835 if (defined $snapshot_links) {
3836 print " | " . $snapshot_links;
3841 if (defined $extra) {
3843 "<td colspan=\"4\">$extra</td>\n" .
3849 sub git_history_body
{
3850 # Warning: assumes constant type (blob or tree) during history
3851 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3853 $from = 0 unless defined $from;
3854 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3856 print "<table class=\"history\">\n";
3858 for (my $i = $from; $i <= $to; $i++) {
3859 my %co = %{$commitlist->[$i]};
3863 my $commit = $co{'id'};
3865 my $ref = format_ref_marker
($refs, $commit);
3868 print "<tr class=\"dark\">\n";
3870 print "<tr class=\"light\">\n";
3873 # shortlog uses chop_str($co{'author_name'}, 10)
3874 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
3875 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3876 "<td><i>" . $author . "</i></td>\n" .
3878 # originally git_history used chop_str($co{'title'}, 50)
3879 print format_subject_html
($co{'title'}, $co{'title_short'},
3880 href
(action
=>"commit", hash
=>$commit), $ref);
3882 "<td class=\"link\">" .
3883 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
3884 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
3886 if ($ftype eq 'blob') {
3887 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
3888 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
3889 if (defined $blob_current && defined $blob_parent &&
3890 $blob_current ne $blob_parent) {
3892 $cgi->a({-href
=> href
(action
=>"blobdiff",
3893 hash
=>$blob_current, hash_parent
=>$blob_parent,
3894 hash_base
=>$hash_base, hash_parent_base
=>$commit,
3895 file_name
=>$file_name)},
3902 if (defined $extra) {
3904 "<td colspan=\"4\">$extra</td>\n" .
3911 # uses global variable $project
3912 my ($taglist, $from, $to, $extra) = @_;
3913 $from = 0 unless defined $from;
3914 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3916 print "<table class=\"tags\">\n";
3918 for (my $i = $from; $i <= $to; $i++) {
3919 my $entry = $taglist->[$i];
3921 my $comment = $tag{'subject'};
3923 if (defined $comment) {
3924 $comment_short = chop_str
($comment, 30, 5);
3927 print "<tr class=\"dark\">\n";
3929 print "<tr class=\"light\">\n";
3932 if (defined $tag{'age'}) {
3933 print "<td><i>$tag{'age'}</i></td>\n";
3935 print "<td></td>\n";
3938 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
3939 -class => "list name"}, esc_html
($tag{'name'})) .
3942 if (defined $comment) {
3943 print format_subject_html
($comment, $comment_short,
3944 href
(action
=>"tag", hash
=>$tag{'id'}));
3947 "<td class=\"selflink\">";
3948 if ($tag{'type'} eq "tag") {
3949 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
3954 "<td class=\"link\">" . " | " .
3955 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
3956 if ($tag{'reftype'} eq "commit") {
3957 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
3958 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
3959 } elsif ($tag{'reftype'} eq "blob") {
3960 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
3965 if (defined $extra) {
3967 "<td colspan=\"5\">$extra</td>\n" .
3973 sub git_heads_body
{
3974 # uses global variable $project
3975 my ($headlist, $head, $from, $to, $extra) = @_;
3976 $from = 0 unless defined $from;
3977 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3979 print "<table class=\"heads\">\n";
3981 for (my $i = $from; $i <= $to; $i++) {
3982 my $entry = $headlist->[$i];
3984 my $curr = $ref{'id'} eq $head;
3986 print "<tr class=\"dark\">\n";
3988 print "<tr class=\"light\">\n";
3991 print "<td><i>$ref{'age'}</i></td>\n" .
3992 ($curr ?
"<td class=\"current_head\">" : "<td>") .
3993 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
3994 -class => "list name"},esc_html
($ref{'name'})) .
3996 "<td class=\"link\">" .
3997 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
3998 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
3999 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
4003 if (defined $extra) {
4005 "<td colspan=\"3\">$extra</td>\n" .
4011 sub git_search_grep_body
{
4012 my ($commitlist, $from, $to, $extra) = @_;
4013 $from = 0 unless defined $from;
4014 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4016 print "<table class=\"commit_search\">\n";
4018 for (my $i = $from; $i <= $to; $i++) {
4019 my %co = %{$commitlist->[$i]};
4023 my $commit = $co{'id'};
4025 print "<tr class=\"dark\">\n";
4027 print "<tr class=\"light\">\n";
4030 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
4031 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4032 "<td><i>" . $author . "</i></td>\n" .
4034 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
4035 -class => "list subject"},
4036 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
4037 my $comment = $co{'comment'};
4038 foreach my $line (@
$comment) {
4039 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4040 my ($lead, $match, $trail) = ($1, $2, $3);
4041 $match = chop_str
($match, 70, 5, 'center');
4042 my $contextlen = int((80 - length($match))/2);
4043 $contextlen = 30 if ($contextlen > 30);
4044 $lead = chop_str
($lead, $contextlen, 10, 'left');
4045 $trail = chop_str
($trail, $contextlen, 10, 'right');
4047 $lead = esc_html
($lead);
4048 $match = esc_html
($match);
4049 $trail = esc_html
($trail);
4051 print "$lead<span class=\"match\">$match</span>$trail<br />";
4055 "<td class=\"link\">" .
4056 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4058 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4060 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4064 if (defined $extra) {
4066 "<td colspan=\"3\">$extra</td>\n" .
4072 ## ======================================================================
4073 ## ======================================================================
4076 sub git_project_list
{
4077 my $order = $cgi->param('o');
4078 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4079 die_error
(400, "Unknown order parameter");
4082 my @list = git_get_projects_list
();
4084 die_error
(404, "No projects found");
4088 if (-f
$home_text) {
4089 print "<div class=\"index_include\">\n";
4090 open (my $fd, $home_text);
4095 git_project_list_body
(\
@list, $order);
4100 my $order = $cgi->param('o');
4101 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4102 die_error
(400, "Unknown order parameter");
4105 my @list = git_get_projects_list
($project);
4107 die_error
(404, "No forks found");
4111 git_print_page_nav
('','');
4112 git_print_header_div
('summary', "$project forks");
4113 git_project_list_body
(\
@list, $order);
4117 sub git_project_index
{
4118 my @projects = git_get_projects_list
($project);
4121 -type
=> 'text/plain',
4122 -charset
=> 'utf-8',
4123 -content_disposition
=> 'inline; filename="index.aux"');
4125 foreach my $pr (@projects) {
4126 if (!exists $pr->{'owner'}) {
4127 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4130 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4131 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4132 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4133 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4137 print "$path $owner\n";
4142 my $descr = git_get_project_description
($project) || "none";
4143 my %co = parse_commit
("HEAD");
4144 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4145 my $head = $co{'id'};
4147 my $owner = git_get_project_owner
($project);
4149 my $refs = git_get_references
();
4150 # These get_*_list functions return one more to allow us to see if
4151 # there are more ...
4152 my @taglist = git_get_tags_list
(16);
4153 my @headlist = git_get_heads_list
(16);
4155 my ($check_forks) = gitweb_check_feature
('forks');
4158 @forklist = git_get_projects_list
($project);
4162 git_print_page_nav
('summary','', $head);
4164 print "<div class=\"title\"> </div>\n";
4165 print "<table class=\"projects_list\">\n" .
4166 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4167 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4168 if (defined $cd{'rfc2822'}) {
4169 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4172 # use per project git URL list in $projectroot/$project/cloneurl
4173 # or make project git URL from git base URL and project name
4174 my $url_tag = "URL";
4175 my @url_list = git_get_project_url_list
($project);
4176 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4177 foreach my $git_url (@url_list) {
4178 next unless $git_url;
4179 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4184 my $show_ctags = (gitweb_check_feature
('ctags'))[0];
4186 my $ctags = git_get_project_ctags
($project);
4187 my $cloud = git_populate_project_tagcloud
($ctags);
4188 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4189 print "</td>\n<td>" unless %$ctags;
4190 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4191 print "</td>\n<td>" if %$ctags;
4192 print git_show_project_tagcloud
($cloud, 48);
4198 if (-s
"$projectroot/$project/README.html") {
4199 if (open my $fd, "$projectroot/$project/README.html") {
4200 print "<div class=\"title\">readme</div>\n" .
4201 "<div class=\"readme\">\n";
4202 print $_ while (<$fd>);
4203 print "\n</div>\n"; # class="readme"
4208 # we need to request one more than 16 (0..15) to check if
4210 my @commitlist = $head ? parse_commits
($head, 17) : ();
4212 git_print_header_div
('shortlog');
4213 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4214 $#commitlist <= 15 ?
undef :
4215 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4219 git_print_header_div
('tags');
4220 git_tags_body
(\
@taglist, 0, 15,
4221 $#taglist <= 15 ?
undef :
4222 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4226 git_print_header_div
('heads');
4227 git_heads_body
(\
@headlist, $head, 0, 15,
4228 $#headlist <= 15 ?
undef :
4229 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4233 git_print_header_div
('forks');
4234 git_project_list_body
(\
@forklist, undef, 0, 15,
4235 $#forklist <= 15 ?
undef :
4236 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4244 my $head = git_get_head_hash
($project);
4246 git_print_page_nav
('','', $head,undef,$head);
4247 my %tag = parse_tag
($hash);
4250 die_error
(404, "Unknown tag object");
4253 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4254 print "<div class=\"title_text\">\n" .
4255 "<table class=\"object_header\">\n" .
4257 "<td>object</td>\n" .
4258 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4259 $tag{'object'}) . "</td>\n" .
4260 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4261 $tag{'type'}) . "</td>\n" .
4263 if (defined($tag{'author'})) {
4264 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4265 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4266 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4267 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4270 print "</table>\n\n" .
4272 print "<div class=\"page_body\">";
4273 my $comment = $tag{'comment'};
4274 foreach my $line (@
$comment) {
4276 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4286 gitweb_check_feature
('blame')
4287 or die_error
(403, "Blame view not allowed");
4289 die_error
(400, "No file name given") unless $file_name;
4290 $hash_base ||= git_get_head_hash
($project);
4291 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4292 my %co = parse_commit
($hash_base)
4293 or die_error
(404, "Commit not found");
4294 if (!defined $hash) {
4295 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4296 or die_error
(404, "Error looking up file");
4298 $ftype = git_get_type
($hash);
4299 if ($ftype !~ "blob") {
4300 die_error
(400, "Object is not a blob");
4302 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4303 $file_name, $hash_base)
4304 or die_error
(500, "Open git-blame failed");
4307 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4310 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4313 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4315 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4316 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4317 git_print_page_path
($file_name, $ftype, $hash_base);
4318 my @rev_color = (qw(light2 dark2));
4319 my $num_colors = scalar(@rev_color);
4320 my $current_color = 0;
4323 <div class="page_body">
4324 <table class="blame">
4325 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4330 last unless defined $_;
4331 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4332 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4333 if (!exists $metainfo{$full_rev}) {
4334 $metainfo{$full_rev} = {};
4336 my $meta = $metainfo{$full_rev};
4339 if (/^(\S+) (.*)$/) {
4345 my $rev = substr($full_rev, 0, 8);
4346 my $author = $meta->{'author'};
4347 my %date = parse_date
($meta->{'author-time'},
4348 $meta->{'author-tz'});
4349 my $date = $date{'iso-tz'};
4351 $current_color = ++$current_color % $num_colors;
4353 print "<tr class=\"$rev_color[$current_color]\">\n";
4355 print "<td class=\"sha1\"";
4356 print " title=\"". esc_html
($author) . ", $date\"";
4357 print " rowspan=\"$group_size\"" if ($group_size > 1);
4359 print $cgi->a({-href
=> href
(action
=>"commit",
4361 file_name
=>$file_name)},
4365 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4366 or die_error
(500, "Open git-rev-parse failed");
4367 my $parent_commit = <$dd>;
4369 chomp($parent_commit);
4370 my $blamed = href
(action
=> 'blame',
4371 file_name
=> $meta->{'filename'},
4372 hash_base
=> $parent_commit);
4373 print "<td class=\"linenr\">";
4374 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4376 -class => "linenr" },
4379 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4385 or print "Reading blob failed\n";
4390 my $head = git_get_head_hash
($project);
4392 git_print_page_nav
('','', $head,undef,$head);
4393 git_print_header_div
('summary', $project);
4395 my @tagslist = git_get_tags_list
();
4397 git_tags_body
(\
@tagslist);
4403 my $head = git_get_head_hash
($project);
4405 git_print_page_nav
('','', $head,undef,$head);
4406 git_print_header_div
('summary', $project);
4408 my @headslist = git_get_heads_list
();
4410 git_heads_body
(\
@headslist, $head);
4415 sub git_blob_plain
{
4419 if (!defined $hash) {
4420 if (defined $file_name) {
4421 my $base = $hash_base || git_get_head_hash
($project);
4422 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4423 or die_error
(404, "Cannot find file");
4425 die_error
(400, "No file name defined");
4427 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4428 # blobs defined by non-textual hash id's can be cached
4432 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4433 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4435 # content-type (can include charset)
4436 $type = blob_contenttype
($fd, $file_name, $type);
4438 # "save as" filename, even when no $file_name is given
4439 my $save_as = "$hash";
4440 if (defined $file_name) {
4441 $save_as = $file_name;
4442 } elsif ($type =~ m/^text\//) {
4448 -expires
=> $expires,
4449 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4451 binmode STDOUT
, ':raw';
4453 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4461 if (!defined $hash) {
4462 if (defined $file_name) {
4463 my $base = $hash_base || git_get_head_hash
($project);
4464 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4465 or die_error
(404, "Cannot find file");
4467 die_error
(400, "No file name defined");
4469 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4470 # blobs defined by non-textual hash id's can be cached
4474 my ($have_blame) = gitweb_check_feature
('blame');
4475 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4476 or die_error
(500, "Couldn't cat $file_name, $hash");
4477 my $mimetype = blob_mimetype
($fd, $file_name);
4478 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4480 return git_blob_plain
($mimetype);
4482 # we can have blame only for text/* mimetype
4483 $have_blame &&= ($mimetype =~ m!^text/!);
4485 git_header_html
(undef, $expires);
4486 my $formats_nav = '';
4487 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4488 if (defined $file_name) {
4491 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4496 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4499 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4502 $cgi->a({-href
=> href
(action
=>"blob",
4503 hash_base
=>"HEAD", file_name
=>$file_name)},
4507 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4510 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4511 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4513 print "<div class=\"page_nav\">\n" .
4514 "<br/><br/></div>\n" .
4515 "<div class=\"title\">$hash</div>\n";
4517 git_print_page_path
($file_name, "blob", $hash_base);
4518 print "<div class=\"page_body\">\n";
4519 if ($mimetype =~ m!^image/!) {
4520 print qq!<img type
="$mimetype"!;
4522 print qq! alt
="$file_name" title
="$file_name"!;
4525 href(action=>"blob_plain
", hash=>$hash,
4526 hash_base=>$hash_base, file_name=>$file_name) .
4530 while (my $line = <$fd>) {
4533 $line = untabify
($line);
4534 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4535 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4539 or print "Reading blob failed.\n";
4545 if (!defined $hash_base) {
4546 $hash_base = "HEAD";
4548 if (!defined $hash) {
4549 if (defined $file_name) {
4550 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4556 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4557 or die_error
(500, "Open git-ls-tree failed");
4558 my @entries = map { chomp; $_ } <$fd>;
4559 close $fd or die_error
(404, "Reading tree failed");
4562 my $refs = git_get_references
();
4563 my $ref = format_ref_marker
($refs, $hash_base);
4566 my ($have_blame) = gitweb_check_feature
('blame');
4567 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4569 if (defined $file_name) {
4571 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4573 $cgi->a({-href
=> href
(action
=>"tree",
4574 hash_base
=>"HEAD", file_name
=>$file_name)},
4577 my $snapshot_links = format_snapshot_links
($hash);
4578 if (defined $snapshot_links) {
4579 # FIXME: Should be available when we have no hash base as well.
4580 push @views_nav, $snapshot_links;
4582 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4583 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4586 print "<div class=\"page_nav\">\n";
4587 print "<br/><br/></div>\n";
4588 print "<div class=\"title\">$hash</div>\n";
4590 if (defined $file_name) {
4591 $basedir = $file_name;
4592 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4596 git_print_page_path
($file_name, 'tree', $hash_base);
4597 print "<div class=\"page_body\">\n";
4598 print "<table class=\"tree\">\n";
4600 # '..' (top directory) link if possible
4601 if (defined $hash_base &&
4602 defined $file_name && $file_name =~ m![^/]+$!) {
4604 print "<tr class=\"dark\">\n";
4606 print "<tr class=\"light\">\n";
4610 my $up = $file_name;
4611 $up =~ s!/?[^/]+$!!;
4612 undef $up unless $up;
4613 # based on git_print_tree_entry
4614 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4615 print '<td class="list">';
4616 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4620 print "<td class=\"link\"></td>\n";
4624 foreach my $line (@entries) {
4625 my %t = parse_ls_tree_line
($line, -z
=> 1);
4628 print "<tr class=\"dark\">\n";
4630 print "<tr class=\"light\">\n";
4634 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4638 print "</table>\n" .
4644 my @supported_fmts = gitweb_check_feature
('snapshot');
4645 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4647 my $format = $cgi->param('sf');
4648 if (!@supported_fmts) {
4649 die_error
(403, "Snapshots not allowed");
4651 # default to first supported snapshot format
4652 $format ||= $supported_fmts[0];
4653 if ($format !~ m/^[a-z0-9]+$/) {
4654 die_error
(400, "Invalid snapshot format parameter");
4655 } elsif (!exists($known_snapshot_formats{$format})) {
4656 die_error
(400, "Unknown snapshot format");
4657 } elsif (!grep($_ eq $format, @supported_fmts)) {
4658 die_error
(403, "Unsupported snapshot format");
4661 if (!defined $hash) {
4662 $hash = git_get_head_hash
($project);
4665 my $name = $project;
4666 $name =~ s
,([^/])/*\
.git
$,$1,;
4667 $name = basename
($name);
4668 my $filename = to_utf8
($name);
4669 $name =~ s/\047/\047\\\047\047/g;
4671 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4672 $cmd = quote_command
(
4673 git_cmd
(), 'archive',
4674 "--format=$known_snapshot_formats{$format}{'format'}",
4675 "--prefix=$name/", $hash);
4676 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4677 $cmd .= ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}});
4681 -type
=> $known_snapshot_formats{$format}{'type'},
4682 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4683 -status
=> '200 OK');
4685 open my $fd, "-|", $cmd
4686 or die_error
(500, "Execute git-archive failed");
4687 binmode STDOUT
, ':raw';
4689 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4694 my $head = git_get_head_hash
($project);
4695 if (!defined $hash) {
4698 if (!defined $page) {
4701 my $refs = git_get_references
();
4703 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4705 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4708 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4711 my %co = parse_commit
($hash);
4713 git_print_header_div
('summary', $project);
4714 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4716 my $to = ($#commitlist >= 99) ?
(99) : ($#commitlist);
4717 for (my $i = 0; $i <= $to; $i++) {
4718 my %co = %{$commitlist[$i]};
4720 my $commit = $co{'id'};
4721 my $ref = format_ref_marker
($refs, $commit);
4722 my %ad = parse_date
($co{'author_epoch'});
4723 git_print_header_div
('commit',
4724 "<span class=\"age\">$co{'age_string'}</span>" .
4725 esc_html
($co{'title'}) . $ref,
4727 print "<div class=\"title_text\">\n" .
4728 "<div class=\"log_link\">\n" .
4729 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4731 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4733 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4736 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4739 print "<div class=\"log_body\">\n";
4740 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4743 if ($#commitlist >= 100) {
4744 print "<div class=\"page_nav\">\n";
4745 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4746 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4753 $hash ||= $hash_base || "HEAD";
4754 my %co = parse_commit
($hash)
4755 or die_error
(404, "Unknown commit object");
4756 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4757 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4759 my $parent = $co{'parent'};
4760 my $parents = $co{'parents'}; # listref
4762 # we need to prepare $formats_nav before any parameter munging
4764 if (!defined $parent) {
4766 $formats_nav .= '(initial)';
4767 } elsif (@
$parents == 1) {
4768 # single parent commit
4771 $cgi->a({-href
=> href
(action
=>"commit",
4773 esc_html
(substr($parent, 0, 7))) .
4780 $cgi->a({-href
=> href
(action
=>"commit",
4782 esc_html
(substr($_, 0, 7)));
4787 if (!defined $parent) {
4791 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4793 (@
$parents <= 1 ?
$parent : '-c'),
4795 or die_error
(500, "Open git-diff-tree failed");
4796 @difftree = map { chomp; $_ } <$fd>;
4797 close $fd or die_error
(404, "Reading git-diff-tree failed");
4799 # non-textual hash id's can be cached
4801 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4804 my $refs = git_get_references
();
4805 my $ref = format_ref_marker
($refs, $co{'id'});
4807 git_header_html
(undef, $expires);
4808 git_print_page_nav
('commit', '',
4809 $hash, $co{'tree'}, $hash,
4812 if (defined $co{'parent'}) {
4813 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4815 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4817 print "<div class=\"title_text\">\n" .
4818 "<table class=\"object_header\">\n";
4819 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4821 "<td></td><td> $ad{'rfc2822'}";
4822 if ($ad{'hour_local'} < 6) {
4823 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4824 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4826 printf(" (%02d:%02d %s)",
4827 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4831 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4832 print "<tr><td></td><td> $cd{'rfc2822'}" .
4833 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4835 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4838 "<td class=\"sha1\">" .
4839 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4840 class => "list"}, $co{'tree'}) .
4842 "<td class=\"link\">" .
4843 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4845 my $snapshot_links = format_snapshot_links
($hash);
4846 if (defined $snapshot_links) {
4847 print " | " . $snapshot_links;
4852 foreach my $par (@
$parents) {
4855 "<td class=\"sha1\">" .
4856 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4857 class => "list"}, $par) .
4859 "<td class=\"link\">" .
4860 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4862 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
4869 print "<div class=\"page_body\">\n";
4870 git_print_log
($co{'comment'});
4873 git_difftree_body
(\
@difftree, $hash, @
$parents);
4879 # object is defined by:
4880 # - hash or hash_base alone
4881 # - hash_base and file_name
4884 # - hash or hash_base alone
4885 if ($hash || ($hash_base && !defined $file_name)) {
4886 my $object_id = $hash || $hash_base;
4888 open my $fd, "-|", quote_command
(
4889 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4890 or die_error
(404, "Object does not exist");
4894 or die_error
(404, "Object does not exist");
4896 # - hash_base and file_name
4897 } elsif ($hash_base && defined $file_name) {
4898 $file_name =~ s
,/+$,,;
4900 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
4901 or die_error
(404, "Base object does not exist");
4903 # here errors should not hapen
4904 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
4905 or die_error
(500, "Open git-ls-tree failed");
4909 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4910 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4911 die_error
(404, "File or directory for given base does not exist");
4916 die_error
(400, "Not enough information to find object");
4919 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
4920 hash
=>$hash, hash_base
=>$hash_base,
4921 file_name
=>$file_name),
4922 -status
=> '302 Found');
4926 my $format = shift || 'html';
4933 # preparing $fd and %diffinfo for git_patchset_body
4935 if (defined $hash_base && defined $hash_parent_base) {
4936 if (defined $file_name) {
4938 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4939 $hash_parent_base, $hash_base,
4940 "--", (defined $file_parent ?
$file_parent : ()), $file_name
4941 or die_error
(500, "Open git-diff-tree failed");
4942 @difftree = map { chomp; $_ } <$fd>;
4944 or die_error
(404, "Reading git-diff-tree failed");
4946 or die_error
(404, "Blob diff not found");
4948 } elsif (defined $hash &&
4949 $hash =~ /[0-9a-fA-F]{40}/) {
4950 # try to find filename from $hash
4952 # read filtered raw output
4953 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4954 $hash_parent_base, $hash_base, "--"
4955 or die_error
(500, "Open git-diff-tree failed");
4957 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4959 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4960 map { chomp; $_ } <$fd>;
4962 or die_error
(404, "Reading git-diff-tree failed");
4964 or die_error
(404, "Blob diff not found");
4967 die_error
(400, "Missing one of the blob diff parameters");
4970 if (@difftree > 1) {
4971 die_error
(400, "Ambiguous blob diff specification");
4974 %diffinfo = parse_difftree_raw_line
($difftree[0]);
4975 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4976 $file_name ||= $diffinfo{'to_file'};
4978 $hash_parent ||= $diffinfo{'from_id'};
4979 $hash ||= $diffinfo{'to_id'};
4981 # non-textual hash id's can be cached
4982 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4983 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4988 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4989 '-p', ($format eq 'html' ?
"--full-index" : ()),
4990 $hash_parent_base, $hash_base,
4991 "--", (defined $file_parent ?
$file_parent : ()), $file_name
4992 or die_error
(500, "Open git-diff-tree failed");
4995 # old/legacy style URI
4996 if (!%diffinfo && # if new style URI failed
4997 defined $hash && defined $hash_parent) {
4998 # fake git-diff-tree raw output
4999 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5000 $diffinfo{'from_id'} = $hash_parent;
5001 $diffinfo{'to_id'} = $hash;
5002 if (defined $file_name) {
5003 if (defined $file_parent) {
5004 $diffinfo{'status'} = '2';
5005 $diffinfo{'from_file'} = $file_parent;
5006 $diffinfo{'to_file'} = $file_name;
5007 } else { # assume not renamed
5008 $diffinfo{'status'} = '1';
5009 $diffinfo{'from_file'} = $file_name;
5010 $diffinfo{'to_file'} = $file_name;
5012 } else { # no filename given
5013 $diffinfo{'status'} = '2';
5014 $diffinfo{'from_file'} = $hash_parent;
5015 $diffinfo{'to_file'} = $hash;
5018 # non-textual hash id's can be cached
5019 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5020 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5025 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
5026 '-p', ($format eq 'html' ?
"--full-index" : ()),
5027 $hash_parent, $hash, "--"
5028 or die_error
(500, "Open git-diff failed");
5030 die_error
(400, "Missing one of the blob diff parameters")
5035 if ($format eq 'html') {
5037 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5039 git_header_html
(undef, $expires);
5040 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5041 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5042 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5044 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5045 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5047 if (defined $file_name) {
5048 git_print_page_path
($file_name, "blob", $hash_base);
5050 print "<div class=\"page_path\"></div>\n";
5053 } elsif ($format eq 'plain') {
5055 -type
=> 'text/plain',
5056 -charset
=> 'utf-8',
5057 -expires
=> $expires,
5058 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
5060 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5063 die_error
(400, "Unknown blobdiff format");
5067 if ($format eq 'html') {
5068 print "<div class=\"page_body\">\n";
5070 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5073 print "</div>\n"; # class="page_body"
5077 while (my $line = <$fd>) {
5078 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5079 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5083 last if $line =~ m!^\+\+\+!;
5091 sub git_blobdiff_plain
{
5092 git_blobdiff
('plain');
5095 sub git_commitdiff
{
5096 my $format = shift || 'html';
5097 $hash ||= $hash_base || "HEAD";
5098 my %co = parse_commit
($hash)
5099 or die_error
(404, "Unknown commit object");
5101 # choose format for commitdiff for merge
5102 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
5103 $hash_parent = '--cc';
5105 # we need to prepare $formats_nav before almost any parameter munging
5107 if ($format eq 'html') {
5109 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5112 if (defined $hash_parent &&
5113 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5114 # commitdiff with two commits given
5115 my $hash_parent_short = $hash_parent;
5116 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5117 $hash_parent_short = substr($hash_parent, 0, 7);
5121 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
5122 if ($co{'parents'}[$i] eq $hash_parent) {
5123 $formats_nav .= ' parent ' . ($i+1);
5127 $formats_nav .= ': ' .
5128 $cgi->a({-href
=> href
(action
=>"commitdiff",
5129 hash
=>$hash_parent)},
5130 esc_html
($hash_parent_short)) .
5132 } elsif (!$co{'parent'}) {
5134 $formats_nav .= ' (initial)';
5135 } elsif (scalar @
{$co{'parents'}} == 1) {
5136 # single parent commit
5139 $cgi->a({-href
=> href
(action
=>"commitdiff",
5140 hash
=>$co{'parent'})},
5141 esc_html
(substr($co{'parent'}, 0, 7))) .
5145 if ($hash_parent eq '--cc') {
5146 $formats_nav .= ' | ' .
5147 $cgi->a({-href
=> href
(action
=>"commitdiff",
5148 hash
=>$hash, hash_parent
=>'-c')},
5150 } else { # $hash_parent eq '-c'
5151 $formats_nav .= ' | ' .
5152 $cgi->a({-href
=> href
(action
=>"commitdiff",
5153 hash
=>$hash, hash_parent
=>'--cc')},
5159 $cgi->a({-href
=> href
(action
=>"commitdiff",
5161 esc_html
(substr($_, 0, 7)));
5162 } @
{$co{'parents'}} ) .
5167 my $hash_parent_param = $hash_parent;
5168 if (!defined $hash_parent_param) {
5169 # --cc for multiple parents, --root for parentless
5170 $hash_parent_param =
5171 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
5177 if ($format eq 'html') {
5178 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5179 "--no-commit-id", "--patch-with-raw", "--full-index",
5180 $hash_parent_param, $hash, "--"
5181 or die_error
(500, "Open git-diff-tree failed");
5183 while (my $line = <$fd>) {
5185 # empty line ends raw part of diff-tree output
5187 push @difftree, scalar parse_difftree_raw_line
($line);
5190 } elsif ($format eq 'plain') {
5191 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5192 '-p', $hash_parent_param, $hash, "--"
5193 or die_error
(500, "Open git-diff-tree failed");
5196 die_error
(400, "Unknown commitdiff format");
5199 # non-textual hash id's can be cached
5201 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5205 # write commit message
5206 if ($format eq 'html') {
5207 my $refs = git_get_references
();
5208 my $ref = format_ref_marker
($refs, $co{'id'});
5210 git_header_html
(undef, $expires);
5211 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5212 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5213 git_print_authorship
(\
%co);
5214 print "<div class=\"page_body\">\n";
5215 if (@
{$co{'comment'}} > 1) {
5216 print "<div class=\"log\">\n";
5217 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5218 print "</div>\n"; # class="log"
5221 } elsif ($format eq 'plain') {
5222 my $refs = git_get_references
("tags");
5223 my $tagname = git_get_rev_name_tags
($hash);
5224 my $filename = basename
($project) . "-$hash.patch";
5227 -type
=> 'text/plain',
5228 -charset
=> 'utf-8',
5229 -expires
=> $expires,
5230 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5231 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5232 print "From: " . to_utf8
($co{'author'}) . "\n";
5233 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5234 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5236 print "X-Git-Tag: $tagname\n" if $tagname;
5237 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5239 foreach my $line (@
{$co{'comment'}}) {
5240 print to_utf8
($line) . "\n";
5246 if ($format eq 'html') {
5247 my $use_parents = !defined $hash_parent ||
5248 $hash_parent eq '-c' || $hash_parent eq '--cc';
5249 git_difftree_body
(\
@difftree, $hash,
5250 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5253 git_patchset_body
($fd, \
@difftree, $hash,
5254 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5256 print "</div>\n"; # class="page_body"
5259 } elsif ($format eq 'plain') {
5263 or print "Reading git-diff-tree failed\n";
5267 sub git_commitdiff_plain
{
5268 git_commitdiff
('plain');
5272 if (!defined $hash_base) {
5273 $hash_base = git_get_head_hash
($project);
5275 if (!defined $page) {
5279 my %co = parse_commit
($hash_base)
5280 or die_error
(404, "Unknown commit object");
5282 my $refs = git_get_references
();
5283 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5285 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5286 $file_name, "--full-history")
5287 or die_error
(404, "No such file or directory on given branch");
5289 if (!defined $hash && defined $file_name) {
5290 # some commits could have deleted file in question,
5291 # and not have it in tree, but one of them has to have it
5292 for (my $i = 0; $i <= @commitlist; $i++) {
5293 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5294 last if defined $hash;
5297 if (defined $hash) {
5298 $ftype = git_get_type
($hash);
5300 if (!defined $ftype) {
5301 die_error
(500, "Unknown type of object");
5304 my $paging_nav = '';
5307 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5308 file_name
=>$file_name)},
5310 $paging_nav .= " ⋅ " .
5311 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5312 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5314 $paging_nav .= "first";
5315 $paging_nav .= " ⋅ prev";
5318 if ($#commitlist >= 100) {
5320 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5321 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5322 $paging_nav .= " ⋅ $next_link";
5324 $paging_nav .= " ⋅ next";
5328 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5329 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5330 git_print_page_path
($file_name, $ftype, $hash_base);
5332 git_history_body
(\
@commitlist, 0, 99,
5333 $refs, $hash_base, $ftype, $next_link);
5339 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5340 if (!defined $searchtext) {
5341 die_error
(400, "Text field is empty");
5343 if (!defined $hash) {
5344 $hash = git_get_head_hash
($project);
5346 my %co = parse_commit
($hash);
5348 die_error
(404, "Unknown commit object");
5350 if (!defined $page) {
5354 $searchtype ||= 'commit';
5355 if ($searchtype eq 'pickaxe') {
5356 # pickaxe may take all resources of your box and run for several minutes
5357 # with every query - so decide by yourself how public you make this feature
5358 gitweb_check_feature
('pickaxe')
5359 or die_error
(403, "Pickaxe is disabled");
5361 if ($searchtype eq 'grep') {
5362 gitweb_check_feature
('grep')
5363 or die_error
(403, "Grep is disabled");
5368 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5370 if ($searchtype eq 'commit') {
5371 $greptype = "--grep=";
5372 } elsif ($searchtype eq 'author') {
5373 $greptype = "--author=";
5374 } elsif ($searchtype eq 'committer') {
5375 $greptype = "--committer=";
5377 $greptype .= $searchtext;
5378 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5379 $greptype, '--regexp-ignore-case',
5380 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
5382 my $paging_nav = '';
5385 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5386 searchtext
=>$searchtext,
5387 searchtype
=>$searchtype)},
5389 $paging_nav .= " ⋅ " .
5390 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5391 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5393 $paging_nav .= "first";
5394 $paging_nav .= " ⋅ prev";
5397 if ($#commitlist >= 100) {
5399 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5400 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5401 $paging_nav .= " ⋅ $next_link";
5403 $paging_nav .= " ⋅ next";
5406 if ($#commitlist >= 100) {
5409 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5410 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5411 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5414 if ($searchtype eq 'pickaxe') {
5415 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5416 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5418 print "<table class=\"pickaxe search\">\n";
5421 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5422 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5423 ($search_use_regexp ?
'--pickaxe-regex' : ());
5426 while (my $line = <$fd>) {
5430 my %set = parse_difftree_raw_line
($line);
5431 if (defined $set{'commit'}) {
5432 # finish previous commit
5435 "<td class=\"link\">" .
5436 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5438 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5444 print "<tr class=\"dark\">\n";
5446 print "<tr class=\"light\">\n";
5449 %co = parse_commit
($set{'commit'});
5450 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5451 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5452 "<td><i>$author</i></td>\n" .
5454 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5455 -class => "list subject"},
5456 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5457 } elsif (defined $set{'to_id'}) {
5458 next if ($set{'to_id'} =~ m/^0{40}$/);
5460 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5461 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5463 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5469 # finish last commit (warning: repetition!)
5472 "<td class=\"link\">" .
5473 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5475 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5483 if ($searchtype eq 'grep') {
5484 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5485 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5487 print "<table class=\"grep_search\">\n";
5491 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5492 $search_use_regexp ?
('-E', '-i') : '-F',
5493 $searchtext, $co{'tree'};
5495 while (my $line = <$fd>) {
5497 my ($file, $lno, $ltext, $binary);
5498 last if ($matches++ > 1000);
5499 if ($line =~ /^Binary file (.+) matches$/) {
5503 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5505 if ($file ne $lastfile) {
5506 $lastfile and print "</td></tr>\n";
5508 print "<tr class=\"dark\">\n";
5510 print "<tr class=\"light\">\n";
5512 print "<td class=\"list\">".
5513 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5514 file_name
=>"$file"),
5515 -class => "list"}, esc_path
($file));
5516 print "</td><td>\n";
5520 print "<div class=\"binary\">Binary file</div>\n";
5522 $ltext = untabify
($ltext);
5523 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5524 $ltext = esc_html
($1, -nbsp
=>1);
5525 $ltext .= '<span class="match">';
5526 $ltext .= esc_html
($2, -nbsp
=>1);
5527 $ltext .= '</span>';
5528 $ltext .= esc_html
($3, -nbsp
=>1);
5530 $ltext = esc_html
($ltext, -nbsp
=>1);
5532 print "<div class=\"pre\">" .
5533 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5534 file_name
=>"$file").'#l'.$lno,
5535 -class => "linenr"}, sprintf('%4i', $lno))
5536 . ' ' . $ltext . "</div>\n";
5540 print "</td></tr>\n";
5541 if ($matches > 1000) {
5542 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5545 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5554 sub git_search_help
{
5556 git_print_page_nav
('','', $hash,$hash,$hash);
5558 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5559 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5560 the pattern entered is recognized as the POSIX extended
5561 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5564 <dt><b>commit</b></dt>
5565 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5567 my ($have_grep) = gitweb_check_feature
('grep');
5570 <dt><b>grep</b></dt>
5571 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5572 a different one) are searched for the given pattern. On large trees, this search can take
5573 a while and put some strain on the server, so please use it with some consideration. Note that
5574 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5575 case-sensitive.</dd>
5579 <dt><b>author</b></dt>
5580 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5581 <dt><b>committer</b></dt>
5582 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5584 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5585 if ($have_pickaxe) {
5587 <dt><b>pickaxe</b></dt>
5588 <dd>All commits that caused the string to appear or disappear from any file (changes that
5589 added, removed or "modified" the string) will be listed. This search can take a while and
5590 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5591 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5599 my $head = git_get_head_hash
($project);
5600 if (!defined $hash) {
5603 if (!defined $page) {
5606 my $refs = git_get_references
();
5608 my @commitlist = parse_commits
($hash, 101, (100 * $page));
5610 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5612 if ($#commitlist >= 100) {
5614 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5615 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5619 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5620 git_print_header_div
('summary', $project);
5622 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5627 ## ......................................................................
5628 ## feeds (RSS, Atom; OPML)
5631 my $format = shift || 'atom';
5632 my ($have_blame) = gitweb_check_feature
('blame');
5634 # Atom: http://www.atomenabled.org/developers/syndication/
5635 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5636 if ($format ne 'rss' && $format ne 'atom') {
5637 die_error
(400, "Unknown web feed format");
5640 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5641 my $head = $hash || 'HEAD';
5642 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5646 my $content_type = "application/$format+xml";
5647 if (defined $cgi->http('HTTP_ACCEPT') &&
5648 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5649 # browser (feed reader) prefers text/xml
5650 $content_type = 'text/xml';
5652 if (defined($commitlist[0])) {
5653 %latest_commit = %{$commitlist[0]};
5654 %latest_date = parse_date
($latest_commit{'author_epoch'});
5656 -type
=> $content_type,
5657 -charset
=> 'utf-8',
5658 -last_modified
=> $latest_date{'rfc2822'});
5661 -type
=> $content_type,
5662 -charset
=> 'utf-8');
5665 # Optimization: skip generating the body if client asks only
5666 # for Last-Modified date.
5667 return if ($cgi->request_method() eq 'HEAD');
5670 my $title = "$site_name - $project/$action";
5671 my $feed_type = 'log';
5672 if (defined $hash) {
5673 $title .= " - '$hash'";
5674 $feed_type = 'branch log';
5675 if (defined $file_name) {
5676 $title .= " :: $file_name";
5677 $feed_type = 'history';
5679 } elsif (defined $file_name) {
5680 $title .= " - $file_name";
5681 $feed_type = 'history';
5683 $title .= " $feed_type";
5684 my $descr = git_get_project_description
($project);
5685 if (defined $descr) {
5686 $descr = esc_html
($descr);
5688 $descr = "$project " .
5689 ($format eq 'rss' ?
'RSS' : 'Atom') .
5692 my $owner = git_get_project_owner
($project);
5693 $owner = esc_html
($owner);
5697 if (defined $file_name) {
5698 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5699 } elsif (defined $hash) {
5700 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5702 $alt_url = href
(-full
=>1, action
=>"summary");
5704 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
5705 if ($format eq 'rss') {
5707 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5710 print "<title>$title</title>\n" .
5711 "<link>$alt_url</link>\n" .
5712 "<description>$descr</description>\n" .
5713 "<language>en</language>\n";
5714 } elsif ($format eq 'atom') {
5716 <feed xmlns="http://www.w3.org/2005/Atom">
5718 print "<title>$title</title>\n" .
5719 "<subtitle>$descr</subtitle>\n" .
5720 '<link rel="alternate" type="text/html" href="' .
5721 $alt_url . '" />' . "\n" .
5722 '<link rel="self" type="' . $content_type . '" href="' .
5723 $cgi->self_url() . '" />' . "\n" .
5724 "<id>" . href
(-full
=>1) . "</id>\n" .
5725 # use project owner for feed author
5726 "<author><name>$owner</name></author>\n";
5727 if (defined $favicon) {
5728 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5730 if (defined $logo_url) {
5731 # not twice as wide as tall: 72 x 27 pixels
5732 print "<logo>" . esc_url
($logo) . "</logo>\n";
5734 if (! %latest_date) {
5735 # dummy date to keep the feed valid until commits trickle in:
5736 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5738 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5743 for (my $i = 0; $i <= $#commitlist; $i++) {
5744 my %co = %{$commitlist[$i]};
5745 my $commit = $co{'id'};
5746 # we read 150, we always show 30 and the ones more recent than 48 hours
5747 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5750 my %cd = parse_date
($co{'author_epoch'});
5752 # get list of changed files
5753 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5754 $co{'parent'} || "--root",
5755 $co{'id'}, "--", (defined $file_name ?
$file_name : ())
5757 my @difftree = map { chomp; $_ } <$fd>;
5761 # print element (entry, item)
5762 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5763 if ($format eq 'rss') {
5765 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5766 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5767 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5768 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5769 "<link>$co_url</link>\n" .
5770 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5771 "<content:encoded>" .
5773 } elsif ($format eq 'atom') {
5775 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5776 "<updated>$cd{'iso-8601'}</updated>\n" .
5778 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5779 if ($co{'author_email'}) {
5780 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5782 print "</author>\n" .
5783 # use committer for contributor
5785 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5786 if ($co{'committer_email'}) {
5787 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5789 print "</contributor>\n" .
5790 "<published>$cd{'iso-8601'}</published>\n" .
5791 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5792 "<id>$co_url</id>\n" .
5793 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5794 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5796 my $comment = $co{'comment'};
5798 foreach my $line (@
$comment) {
5799 $line = esc_html
($line);
5802 print "</pre><ul>\n";
5803 foreach my $difftree_line (@difftree) {
5804 my %difftree = parse_difftree_raw_line
($difftree_line);
5805 next if !$difftree{'from_id'};
5807 my $file = $difftree{'file'} || $difftree{'to_file'};
5811 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5812 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5813 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5814 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5815 -title
=> "diff"}, 'D');
5817 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5818 file_name
=>$file, hash_base
=>$commit),
5819 -title
=> "blame"}, 'B');
5821 # if this is not a feed of a file history
5822 if (!defined $file_name || $file_name ne $file) {
5823 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5824 file_name
=>$file, hash
=>$commit),
5825 -title
=> "history"}, 'H');
5827 $file = esc_path
($file);
5831 if ($format eq 'rss') {
5832 print "</ul>]]>\n" .
5833 "</content:encoded>\n" .
5835 } elsif ($format eq 'atom') {
5836 print "</ul>\n</div>\n" .
5843 if ($format eq 'rss') {
5844 print "</channel>\n</rss>\n";
5845 } elsif ($format eq 'atom') {
5859 my @list = git_get_projects_list
();
5861 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
5863 <?xml version="1.0" encoding="utf-8"?>
5864 <opml version="1.0">
5866 <title>$site_name OPML Export</title>
5869 <outline text="git RSS feeds">
5872 foreach my $pr (@list) {
5874 my $head = git_get_head_hash
($proj{'path'});
5875 if (!defined $head) {
5878 $git_dir = "$projectroot/$proj{'path'}";
5879 my %co = parse_commit
($head);
5884 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
5885 my $rss = "$my_url?p=$proj{'path'};a=rss";
5886 my $html = "$my_url?p=$proj{'path'};a=summary";
5887 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";