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 our $gitwebjs = "++GITWEB_GITWEBJS++";
71 # URI and label (title) of GIT logo link
72 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
73 #our $logo_label = "git documentation";
74 our $logo_url = "http://git.or.cz/";
75 our $logo_label = "git homepage";
77 # source of projects list
78 our $projects_list = "++GITWEB_LIST++";
80 # the width (in characters) of the projects list "Description" column
81 our $projects_list_description_width = 25;
83 # default order of projects list
84 # valid values are none, project, descr, owner, and age
85 our $default_projects_order = "project";
87 # show repository only if this file exists
88 # (only effective if this variable evaluates to true)
89 our $export_ok = "++GITWEB_EXPORT_OK++";
91 # only allow viewing of repositories also shown on the overview page
92 our $strict_export = "++GITWEB_STRICT_EXPORT++";
94 # list of git base URLs used for URL to where fetch project from,
95 # i.e. full URL is "$git_base_url/$project"
96 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
98 # default blob_plain mimetype and default charset for text/plain blob
99 our $default_blob_plain_mimetype = 'text/plain';
100 our $default_text_plain_charset = undef;
102 # file to use for guessing MIME types before trying /etc/mime.types
103 # (relative to the current git repository)
104 our $mimetypes_file = undef;
106 # assume this charset if line contains non-UTF-8 characters;
107 # it should be valid encoding (see Encoding::Supported(3pm) for list),
108 # for which encoding all byte sequences are valid, for example
109 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
110 # could be even 'utf-8' for the old behavior)
111 our $fallback_encoding = 'latin1';
113 # rename detection options for git-diff and git-diff-tree
114 # - default is '-M', with the cost proportional to
115 # (number of removed files) * (number of new files).
116 # - more costly is '-C' (which implies '-M'), with the cost proportional to
117 # (number of changed files + number of removed files) * (number of new files)
118 # - even more costly is '-C', '--find-copies-harder' with cost
119 # (number of files in the original tree) * (number of new files)
120 # - one might want to include '-B' option, e.g. '-B', '-M'
121 our @diff_opts = ('-M'); # taken from git_commit
123 # projects list cache for busy sites with many projects;
124 # if you set this to non-zero, it will be used as the cached
125 # index lifetime in minutes
127 # the cached list version is stored in $cache_dir/$cache_name and can
128 # be tweaked by other scripts running with the same uid as gitweb -
129 # use this ONLY at secure installations; only single gitweb project
130 # root per system is supported, unless you tweak configuration!
131 our $projlist_cache_lifetime = 0; # in minutes
132 # FHS compliant $cache_dir would be "/var/cache/gitweb"
134 (defined $ENV{'TMPDIR'} ?
$ENV{'TMPDIR'} : '/tmp').'/gitweb';
135 our $projlist_cache_name = 'gitweb.index.cache';
137 # information about snapshot formats that gitweb is capable of serving
138 our %known_snapshot_formats = (
140 # 'display' => display name,
141 # 'type' => mime type,
142 # 'suffix' => filename suffix,
143 # 'format' => --format for git-archive,
144 # 'compressor' => [compressor command and arguments]
145 # (array reference, optional)}
148 'display' => 'tar.gz',
149 'type' => 'application/x-gzip',
150 'suffix' => '.tar.gz',
152 'compressor' => ['gzip']},
155 'display' => 'tar.bz2',
156 'type' => 'application/x-bzip2',
157 'suffix' => '.tar.bz2',
159 'compressor' => ['bzip2']},
163 'type' => 'application/x-zip',
168 # Aliases so we understand old gitweb.snapshot values in repository
170 our %known_snapshot_format_aliases = (
174 # backward compatibility: legacy gitweb config support
175 'x-gzip' => undef, 'gz' => undef,
176 'x-bzip2' => undef, 'bz2' => undef,
177 'x-zip' => undef, '' => undef,
180 # You define site-wide feature defaults here; override them with
181 # $GITWEB_CONFIG as necessary.
184 # 'sub' => feature-sub (subroutine),
185 # 'override' => allow-override (boolean),
186 # 'default' => [ default options...] (array reference)}
188 # if feature is overridable (it means that allow-override has true value),
189 # then feature-sub will be called with default options as parameters;
190 # return value of feature-sub indicates if to enable specified feature
192 # if there is no 'sub' key (no feature-sub), then feature cannot be
195 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
197 # Enable the 'blame' blob view, showing the last commit that modified
198 # each line in the file. This can be very CPU-intensive.
200 # To enable system wide have in $GITWEB_CONFIG
201 # $feature{'blame'}{'default'} = [1];
202 # To have project specific config enable override in $GITWEB_CONFIG
203 # $feature{'blame'}{'override'} = 1;
204 # and in project config gitweb.blame = 0|1;
206 'sub' => \
&feature_blame
,
210 # Enable the 'snapshot' link, providing a compressed archive of any
211 # tree. This can potentially generate high traffic if you have large
214 # Value is a list of formats defined in %known_snapshot_formats that
216 # To disable system wide have in $GITWEB_CONFIG
217 # $feature{'snapshot'}{'default'} = [];
218 # To have project specific config enable override in $GITWEB_CONFIG
219 # $feature{'snapshot'}{'override'} = 1;
220 # and in project config, a comma-separated list of formats or "none"
221 # to disable. Example: gitweb.snapshot = tbz2,zip;
223 'sub' => \
&feature_snapshot
,
225 'default' => ['tgz']},
227 # Enable text search, which will list the commits which match author,
228 # committer or commit text to a given string. Enabled by default.
229 # Project specific override is not supported.
234 # Enable grep search, which will list the files in currently selected
235 # tree containing the given string. Enabled by default. This can be
236 # potentially CPU-intensive, of course.
238 # To enable system wide have in $GITWEB_CONFIG
239 # $feature{'grep'}{'default'} = [1];
240 # To have project specific config enable override in $GITWEB_CONFIG
241 # $feature{'grep'}{'override'} = 1;
242 # and in project config gitweb.grep = 0|1;
247 # Enable the pickaxe search, which will list the commits that modified
248 # a given string in a file. This can be practical and quite faster
249 # alternative to 'blame', but still potentially CPU-intensive.
251 # To enable system wide have in $GITWEB_CONFIG
252 # $feature{'pickaxe'}{'default'} = [1];
253 # To have project specific config enable override in $GITWEB_CONFIG
254 # $feature{'pickaxe'}{'override'} = 1;
255 # and in project config gitweb.pickaxe = 0|1;
257 'sub' => \
&feature_pickaxe
,
261 # Make gitweb use an alternative format of the URLs which can be
262 # more readable and natural-looking: project name is embedded
263 # directly in the path and the query string contains other
264 # auxiliary information. All gitweb installations recognize
265 # URL in either format; this configures in which formats gitweb
268 # To enable system wide have in $GITWEB_CONFIG
269 # $feature{'pathinfo'}{'default'} = [1];
270 # Project specific override is not supported.
272 # Note that you will need to change the default location of CSS,
273 # favicon, logo and possibly other files to an absolute URL. Also,
274 # if gitweb.cgi serves as your indexfile, you will need to force
275 # $my_uri to contain the script name in your $GITWEB_CONFIG.
280 # Make gitweb consider projects in project root subdirectories
281 # to be forks of existing projects. Given project $projname.git,
282 # projects matching $projname/*.git will not be shown in the main
283 # projects list, instead a '+' mark will be added to $projname
284 # there and a 'forks' view will be enabled for the project, listing
285 # all the forks. If project list is taken from a file, forks have
286 # to be listed after the main project.
288 # To enable system wide have in $GITWEB_CONFIG
289 # $feature{'forks'}{'default'} = [1];
290 # Project specific override is not supported.
295 # Insert custom links to the action bar of all project pages.
296 # This enables you mainly to link to third-party scripts integrating
297 # into gitweb; e.g. git-browser for graphical history representation
298 # or custom web-based repository administration interface.
300 # The 'default' value consists of a list of triplets in the form
301 # (label, link, position) where position is the label after which
302 # to inster the link and link is a format string where %n expands
303 # to the project name, %f to the project path within the filesystem,
304 # %h to the current hash (h gitweb parameter) and %b to the current
305 # hash base (hb gitweb parameter).
307 # To enable system wide have in $GITWEB_CONFIG e.g.
308 # $feature{'actions'}{'default'} = [('graphiclog',
309 # '/git-browser/by-commit.html?r=%n', 'summary')];
310 # Project specific override is not supported.
315 # Allow gitweb scan project content tags described in ctags/
316 # of project repository, and display the popular Web 2.0-ish
317 # "tag cloud" near the project list. Note that this is something
318 # COMPLETELY different from the normal Git tags.
320 # gitweb by itself can show existing tags, but it does not handle
321 # tagging itself; you need an external application for that.
322 # For an example script, check Girocco's cgi/tagproj.cgi.
324 # To enable system wide have in $GITWEB_CONFIG
325 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
326 # Project specific override is not supported.
332 sub gitweb_check_feature
{
334 return unless exists $feature{$name};
335 my ($sub, $override, @defaults) = (
336 $feature{$name}{'sub'},
337 $feature{$name}{'override'},
338 @
{$feature{$name}{'default'}});
339 if (!$override) { return @defaults; }
341 warn "feature $name is not overrideable";
344 return $sub->(@defaults);
348 my ($val) = git_get_project_config
('blame', '--bool');
350 if ($val eq 'true') {
352 } elsif ($val eq 'false') {
359 sub feature_snapshot
{
362 my ($val) = git_get_project_config
('snapshot');
365 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
372 my ($val) = git_get_project_config
('grep', '--bool');
374 if ($val eq 'true') {
376 } elsif ($val eq 'false') {
383 sub feature_pickaxe
{
384 my ($val) = git_get_project_config
('pickaxe', '--bool');
386 if ($val eq 'true') {
388 } elsif ($val eq 'false') {
395 # checking HEAD file with -e is fragile if the repository was
396 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
398 sub check_head_link
{
400 my $headfile = "$dir/HEAD";
401 return ((-e
$headfile) ||
402 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
405 sub check_export_ok
{
407 return (check_head_link
($dir) &&
408 (!$export_ok || -e
"$dir/$export_ok"));
411 # process alternate names for backward compatibility
412 # filter out unsupported (unknown) snapshot formats
413 sub filter_snapshot_fmts
{
417 exists $known_snapshot_format_aliases{$_} ?
418 $known_snapshot_format_aliases{$_} : $_} @fmts;
419 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
423 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
424 if (-e
$GITWEB_CONFIG) {
427 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
428 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
431 # version of the core git binary
432 our $git_version = qx("$GIT" --version
) =~ m/git version (.*)$/ ?
$1 : "unknown";
434 $projects_list ||= $projectroot;
436 # ======================================================================
437 # input validation and dispatch
438 our $action = $cgi->param('a');
439 if (defined $action) {
440 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
441 die_error
(400, "Invalid action parameter");
445 # parameters which are pathnames
446 our $project = $cgi->param('p');
447 if (defined $project) {
448 if (!validate_pathname
($project) ||
449 !(-d
"$projectroot/$project") ||
450 !check_head_link
("$projectroot/$project") ||
451 ($export_ok && !(-e
"$projectroot/$project/$export_ok")) ||
452 ($strict_export && !project_in_list
($project))) {
454 die_error
(404, "No such project");
458 our $file_name = $cgi->param('f');
459 if (defined $file_name) {
460 if (!validate_pathname
($file_name)) {
461 die_error
(400, "Invalid file parameter");
465 our $file_parent = $cgi->param('fp');
466 if (defined $file_parent) {
467 if (!validate_pathname
($file_parent)) {
468 die_error
(400, "Invalid file parent parameter");
472 # parameters which are refnames
473 our $hash = $cgi->param('h');
475 if (!validate_refname
($hash)) {
476 die_error
(400, "Invalid hash parameter");
480 our $hash_parent = $cgi->param('hp');
481 if (defined $hash_parent) {
482 if (!validate_refname
($hash_parent)) {
483 die_error
(400, "Invalid hash parent parameter");
487 our $hash_base = $cgi->param('hb');
488 if (defined $hash_base) {
489 if (!validate_refname
($hash_base)) {
490 die_error
(400, "Invalid hash base parameter");
494 my %allowed_options = (
495 "--no-merges" => [ qw(rss atom log shortlog history) ],
498 our @extra_options = $cgi->param('opt');
499 if (defined @extra_options) {
500 foreach my $opt (@extra_options) {
501 if (not exists $allowed_options{$opt}) {
502 die_error
(400, "Invalid option parameter");
504 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
505 die_error
(400, "Invalid option parameter for this action");
510 our $hash_parent_base = $cgi->param('hpb');
511 if (defined $hash_parent_base) {
512 if (!validate_refname
($hash_parent_base)) {
513 die_error
(400, "Invalid hash parent base parameter");
518 our $page = $cgi->param('pg');
520 if ($page =~ m/[^0-9]/) {
521 die_error
(400, "Invalid page parameter");
525 our $searchtype = $cgi->param('st');
526 if (defined $searchtype) {
527 if ($searchtype =~ m/[^a-z]/) {
528 die_error
(400, "Invalid searchtype parameter");
532 our $search_use_regexp = $cgi->param('sr');
534 our $searchtext = $cgi->param('s');
536 if (defined $searchtext) {
537 if (length($searchtext) < 2) {
538 die_error
(403, "At least two characters are required for search parameter");
540 $search_regexp = $search_use_regexp ?
$searchtext : quotemeta $searchtext;
543 # now read PATH_INFO and use it as alternative to parameters
544 sub evaluate_path_info
{
545 return if defined $project;
546 my $path_info = $ENV{"PATH_INFO"};
547 return if !$path_info;
548 $path_info =~ s
,^/+,,;
549 return if !$path_info;
550 # find which part of PATH_INFO is project
551 $project = $path_info;
553 while ($project && !check_head_link
("$projectroot/$project")) {
554 $project =~ s
,/*[^/]*$,,;
557 $project = validate_pathname
($project);
559 ($export_ok && !-e
"$projectroot/$project/$export_ok") ||
560 ($strict_export && !project_in_list
($project))) {
564 # do not change any parameters if an action is given using the query string
566 $path_info =~ s
,^\Q
$project\E
/*,,;
567 my ($refname, $pathname) = split(/:/, $path_info, 2);
568 if (defined $pathname) {
569 # we got "project.git/branch:filename" or "project.git/branch:dir/"
570 # we could use git_get_type(branch:pathname), but it needs $git_dir
571 $pathname =~ s
,^/+,,;
572 if (!$pathname || substr($pathname, -1) eq "/") {
576 $action ||= "blob_plain";
578 $hash_base ||= validate_refname
($refname);
579 $file_name ||= validate_pathname
($pathname);
580 } elsif (defined $refname) {
581 # we got "project.git/branch"
582 $action ||= "shortlog";
583 $hash ||= validate_refname
($refname);
586 evaluate_path_info
();
588 # path to the current git repository
590 $git_dir = "$projectroot/$project" if $project;
594 "blame" => \
&git_blame
,
595 "blame_incremental" => \
&git_blame_incremental
,
596 "blame_data" => \
&git_blame_data
,
597 "blobdiff" => \
&git_blobdiff
,
598 "blobdiff_plain" => \
&git_blobdiff_plain
,
599 "blob" => \
&git_blob
,
600 "blob_plain" => \
&git_blob_plain
,
601 "commitdiff" => \
&git_commitdiff
,
602 "commitdiff_plain" => \
&git_commitdiff_plain
,
603 "commit" => \
&git_commit
,
604 "forks" => \
&git_forks
,
605 "heads" => \
&git_heads
,
606 "history" => \
&git_history
,
609 "atom" => \
&git_atom
,
610 "search" => \
&git_search
,
611 "search_help" => \
&git_search_help
,
612 "shortlog" => \
&git_shortlog
,
613 "summary" => \
&git_summary
,
615 "tags" => \
&git_tags
,
616 "tree" => \
&git_tree
,
617 "snapshot" => \
&git_snapshot
,
618 "object" => \
&git_object
,
619 # those below don't need $project
620 "opml" => \
&git_opml
,
621 "project_list" => \
&git_project_list
,
622 "project_index" => \
&git_project_index
,
625 if (!defined $action) {
627 $action = git_get_type
($hash);
628 } elsif (defined $hash_base && defined $file_name) {
629 $action = git_get_type
("$hash_base:$file_name");
630 } elsif (defined $project) {
633 $action = 'project_list';
636 if (!defined($actions{$action})) {
637 die_error
(400, "Unknown action");
639 if ($action !~ m/^(opml|project_list|project_index)$/ &&
641 die_error
(400, "Project needed");
643 $actions{$action}->();
646 ## ======================================================================
651 # default is to use -absolute url() i.e. $my_uri
652 my $href = $params{-full
} ?
$my_url : $my_uri;
654 # XXX: Warning: If you touch this, check the search form for updating,
665 hash_parent_base
=> "hpb",
670 snapshot_format
=> "sf",
671 extra_options
=> "opt",
672 search_use_regexp
=> "sr",
674 my %mapping = @mapping;
676 $params{'project'} = $project unless exists $params{'project'};
678 if ($params{-replay
}) {
679 while (my ($name, $symbol) = each %mapping) {
680 if (!exists $params{$name}) {
681 # to allow for multivalued params we use arrayref form
682 $params{$name} = [ $cgi->param($symbol) ];
687 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
689 # use PATH_INFO for project name
690 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
691 delete $params{'project'};
693 # Summary just uses the project path URL
694 if (defined $params{'action'} && $params{'action'} eq 'summary') {
695 delete $params{'action'};
699 # now encode the parameters explicitly
701 for (my $i = 0; $i < @mapping; $i += 2) {
702 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
703 if (defined $params{$name}) {
704 if (ref($params{$name}) eq "ARRAY") {
705 foreach my $par (@
{$params{$name}}) {
706 push @result, $symbol . "=" . esc_param
($par);
709 push @result, $symbol . "=" . esc_param
($params{$name});
713 $href .= "?" . join(';', @result) if $params{-partial_query
} or scalar @result;
719 ## ======================================================================
720 ## validation, quoting/unquoting and escaping
722 sub validate_pathname
{
723 my $input = shift || return undef;
725 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
726 # at the beginning, at the end, and between slashes.
727 # also this catches doubled slashes
728 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
732 if ($input =~ m!\0!) {
738 sub validate_refname
{
739 my $input = shift || return undef;
741 # textual hashes are O.K.
742 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
745 # it must be correct pathname
746 $input = validate_pathname
($input)
748 # restrictions on ref name according to git-check-ref-format
749 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
755 # decode sequences of octets in utf8 into Perl's internal form,
756 # which is utf-8 with utf8 flag set if needed. gitweb writes out
757 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
760 if (utf8
::valid
($str)) {
764 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
768 # quote unsafe chars, but keep the slash, even when it's not
769 # correct, but quoted slashes look too horrible in bookmarks
772 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
778 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
781 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
787 # replace invalid utf8 character with SUBSTITUTION sequence
792 $str = to_utf8
($str);
793 $str = $cgi->escapeHTML($str);
794 if ($opts{'-nbsp'}) {
795 $str =~ s/ / /g;
797 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
801 # quote control characters and escape filename to HTML
806 $str = to_utf8
($str);
807 $str = $cgi->escapeHTML($str);
808 if ($opts{'-nbsp'}) {
809 $str =~ s/ / /g;
811 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
815 # Make control characters "printable", using character escape codes (CEC)
819 my %es = ( # character escape codes, aka escape sequences
820 "\t" => '\t', # tab (HT)
821 "\n" => '\n', # line feed (LF)
822 "\r" => '\r', # carrige return (CR)
823 "\f" => '\f', # form feed (FF)
824 "\b" => '\b', # backspace (BS)
825 "\a" => '\a', # alarm (bell) (BEL)
826 "\e" => '\e', # escape (ESC)
827 "\013" => '\v', # vertical tab (VT)
828 "\000" => '\0', # nul character (NUL)
830 my $chr = ( (exists $es{$cntrl})
832 : sprintf('\%2x', ord($cntrl)) );
833 if ($opts{-nohtml
}) {
836 return "<span class=\"cntrl\">$chr</span>";
840 # Alternatively use unicode control pictures codepoints,
841 # Unicode "printable representation" (PR)
846 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
847 if ($opts{-nohtml
}) {
850 return "<span class=\"cntrl\">$chr</span>";
854 # git may return quoted and escaped filenames
860 my %es = ( # character escape codes, aka escape sequences
861 't' => "\t", # tab (HT, TAB)
862 'n' => "\n", # newline (NL)
863 'r' => "\r", # return (CR)
864 'f' => "\f", # form feed (FF)
865 'b' => "\b", # backspace (BS)
866 'a' => "\a", # alarm (bell) (BEL)
867 'e' => "\e", # escape (ESC)
868 'v' => "\013", # vertical tab (VT)
871 if ($seq =~ m/^[0-7]{1,3}$/) {
872 # octal char sequence
873 return chr(oct($seq));
874 } elsif (exists $es{$seq}) {
875 # C escape sequence, aka character escape code
878 # quoted ordinary character
882 if ($str =~ m/^"(.*)"$/) {
885 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
890 # escape tabs (convert tabs to spaces)
894 while ((my $pos = index($line, "\t")) != -1) {
895 if (my $count = (8 - ($pos % 8))) {
896 my $spaces = ' ' x
$count;
897 $line =~ s/\t/$spaces/;
904 sub project_in_list
{
906 my @list = git_get_projects_list
();
907 return @list && scalar(grep { $_->{'path'} eq $project } @list);
910 ## ----------------------------------------------------------------------
911 ## HTML aware string manipulation
913 # Try to chop given string on a word boundary between position
914 # $len and $len+$add_len. If there is no word boundary there,
915 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
916 # (marking chopped part) would be longer than given string.
920 my $add_len = shift || 10;
921 my $where = shift || 'right'; # 'left' | 'center' | 'right'
923 # Make sure perl knows it is utf8 encoded so we don't
924 # cut in the middle of a utf8 multibyte char.
925 $str = to_utf8
($str);
927 # allow only $len chars, but don't cut a word if it would fit in $add_len
928 # if it doesn't fit, cut it if it's still longer than the dots we would add
929 # remove chopped character entities entirely
931 # when chopping in the middle, distribute $len into left and right part
932 # return early if chopping wouldn't make string shorter
933 if ($where eq 'center') {
934 return $str if ($len + 5 >= length($str)); # filler is length 5
937 return $str if ($len + 4 >= length($str)); # filler is length 4
940 # regexps: ending and beginning with word part up to $add_len
941 my $endre = qr/.{$len}\w{0,$add_len}/;
942 my $begre = qr/\w{0,$add_len}.{$len}/;
944 if ($where eq 'left') {
945 $str =~ m/^(.*?)($begre)$/;
946 my ($lead, $body) = ($1, $2);
947 if (length($lead) > 4) {
948 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
953 } elsif ($where eq 'center') {
954 $str =~ m/^($endre)(.*)$/;
955 my ($left, $str) = ($1, $2);
956 $str =~ m/^(.*?)($begre)$/;
957 my ($mid, $right) = ($1, $2);
958 if (length($mid) > 5) {
959 $left =~ s/&[^;]*$//;
960 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
963 return "$left$mid$right";
966 $str =~ m/^($endre)(.*)$/;
969 if (length($tail) > 4) {
970 $body =~ s/&[^;]*$//;
977 # takes the same arguments as chop_str, but also wraps a <span> around the
978 # result with a title attribute if it does get chopped. Additionally, the
979 # string is HTML-escaped.
980 sub chop_and_escape_str
{
983 my $chopped = chop_str
(@_);
984 if ($chopped eq $str) {
985 return esc_html
($chopped);
987 $str =~ s/([[:cntrl:]])/?/g;
988 return $cgi->span({-title
=>$str}, esc_html
($chopped));
992 ## ----------------------------------------------------------------------
993 ## functions returning short strings
995 # CSS class for given age value (in seconds)
1001 } elsif ($age < 60*60*2) {
1003 } elsif ($age < 60*60*24*2) {
1010 # convert age in seconds to "nn units ago" string
1015 if ($age > 60*60*24*365*2) {
1016 $age_str = (int $age/60/60/24/365);
1017 $age_str .= " years ago";
1018 } elsif ($age > 60*60*24*(365/12)*2) {
1019 $age_str = int $age/60/60/24/(365/12);
1020 $age_str .= " months ago";
1021 } elsif ($age > 60*60*24*7*2) {
1022 $age_str = int $age/60/60/24/7;
1023 $age_str .= " weeks ago";
1024 } elsif ($age > 60*60*24*2) {
1025 $age_str = int $age/60/60/24;
1026 $age_str .= " days ago";
1027 } elsif ($age > 60*60*2) {
1028 $age_str = int $age/60/60;
1029 $age_str .= " hours ago";
1030 } elsif ($age > 60*2) {
1031 $age_str = int $age/60;
1032 $age_str .= " min ago";
1033 } elsif ($age > 2) {
1034 $age_str = int $age;
1035 $age_str .= " sec ago";
1037 $age_str .= " right now";
1043 S_IFINVALID
=> 0030000,
1044 S_IFGITLINK
=> 0160000,
1047 # submodule/subproject, a commit object reference
1048 sub S_ISGITLINK
($) {
1051 return (($mode & S_IFMT
) == S_IFGITLINK
)
1054 # convert file mode in octal to symbolic file mode string
1056 my $mode = oct shift;
1058 if (S_ISGITLINK
($mode)) {
1059 return 'm---------';
1060 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1061 return 'drwxr-xr-x';
1062 } elsif (S_ISLNK
($mode)) {
1063 return 'lrwxrwxrwx';
1064 } elsif (S_ISREG
($mode)) {
1065 # git cares only about the executable bit
1066 if ($mode & S_IXUSR
) {
1067 return '-rwxr-xr-x';
1069 return '-rw-r--r--';
1072 return '----------';
1076 # convert file mode in octal to file type string
1080 if ($mode !~ m/^[0-7]+$/) {
1086 if (S_ISGITLINK
($mode)) {
1088 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1090 } elsif (S_ISLNK
($mode)) {
1092 } elsif (S_ISREG
($mode)) {
1099 # convert file mode in octal to file type description string
1100 sub file_type_long
{
1103 if ($mode !~ m/^[0-7]+$/) {
1109 if (S_ISGITLINK
($mode)) {
1111 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1113 } elsif (S_ISLNK
($mode)) {
1115 } elsif (S_ISREG
($mode)) {
1116 if ($mode & S_IXUSR
) {
1117 return "executable";
1127 ## ----------------------------------------------------------------------
1128 ## functions returning short HTML fragments, or transforming HTML fragments
1129 ## which don't belong to other sections
1131 # format line of commit message.
1132 sub format_log_line_html
{
1135 $line = esc_html
($line, -nbsp
=>1);
1136 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1139 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1140 -class => "text"}, $hash_text);
1141 $line =~ s/$hash_text/$link/;
1146 # format marker of refs pointing to given object
1148 # the destination action is chosen based on object type and current context:
1149 # - for annotated tags, we choose the tag view unless it's the current view
1150 # already, in which case we go to shortlog view
1151 # - for other refs, we keep the current view if we're in history, shortlog or
1152 # log view, and select shortlog otherwise
1153 sub format_ref_marker
{
1154 my ($refs, $id) = @_;
1157 if (defined $refs->{$id}) {
1158 foreach my $ref (@
{$refs->{$id}}) {
1159 # this code exploits the fact that non-lightweight tags are the
1160 # only indirect objects, and that they are the only objects for which
1161 # we want to use tag instead of shortlog as action
1162 my ($type, $name) = qw();
1163 my $indirect = ($ref =~ s/\^\{\}$//);
1164 # e.g. tags/v2.6.11 or heads/next
1165 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1174 $class .= " indirect" if $indirect;
1176 my $dest_action = "shortlog";
1179 $dest_action = "tag" unless $action eq "tag";
1180 } elsif ($action =~ /^(history|(short)?log)$/) {
1181 $dest_action = $action;
1185 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1188 my $link = $cgi->a({
1190 action
=>$dest_action,
1194 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1200 return ' <span class="refs">'. $markers . '</span>';
1206 # format, perhaps shortened and with markers, title line
1207 sub format_subject_html
{
1208 my ($long, $short, $href, $extra) = @_;
1209 $extra = '' unless defined($extra);
1211 if (length($short) < length($long)) {
1212 return $cgi->a({-href
=> $href, -class => "list subject",
1213 -title
=> to_utf8
($long)},
1214 esc_html
($short) . $extra);
1216 return $cgi->a({-href
=> $href, -class => "list subject"},
1217 esc_html
($long) . $extra);
1221 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1222 sub format_git_diff_header_line
{
1224 my $diffinfo = shift;
1225 my ($from, $to) = @_;
1227 if ($diffinfo->{'nparents'}) {
1229 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1230 if ($to->{'href'}) {
1231 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1232 esc_path
($to->{'file'}));
1233 } else { # file was deleted (no href)
1234 $line .= esc_path
($to->{'file'});
1238 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1239 if ($from->{'href'}) {
1240 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1241 'a/' . esc_path
($from->{'file'}));
1242 } else { # file was added (no href)
1243 $line .= 'a/' . esc_path
($from->{'file'});
1246 if ($to->{'href'}) {
1247 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1248 'b/' . esc_path
($to->{'file'}));
1249 } else { # file was deleted
1250 $line .= 'b/' . esc_path
($to->{'file'});
1254 return "<div class=\"diff header\">$line</div>\n";
1257 # format extended diff header line, before patch itself
1258 sub format_extended_diff_header_line
{
1260 my $diffinfo = shift;
1261 my ($from, $to) = @_;
1264 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1265 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1266 esc_path
($from->{'file'}));
1268 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1269 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1270 esc_path
($to->{'file'}));
1272 # match single <mode>
1273 if ($line =~ m/\s(\d{6})$/) {
1274 $line .= '<span class="info"> (' .
1275 file_type_long
($1) .
1279 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1280 # can match only for combined diff
1282 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1283 if ($from->{'href'}[$i]) {
1284 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1286 substr($diffinfo->{'from_id'}[$i],0,7));
1291 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1294 if ($to->{'href'}) {
1295 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1296 substr($diffinfo->{'to_id'},0,7));
1301 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1302 # can match only for ordinary diff
1303 my ($from_link, $to_link);
1304 if ($from->{'href'}) {
1305 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1306 substr($diffinfo->{'from_id'},0,7));
1308 $from_link = '0' x
7;
1310 if ($to->{'href'}) {
1311 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1312 substr($diffinfo->{'to_id'},0,7));
1316 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1317 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1320 return $line . "<br/>\n";
1323 # format from-file/to-file diff header
1324 sub format_diff_from_to_header
{
1325 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1330 #assert($line =~ m/^---/) if DEBUG;
1331 # no extra formatting for "^--- /dev/null"
1332 if (! $diffinfo->{'nparents'}) {
1333 # ordinary (single parent) diff
1334 if ($line =~ m!^--- "?a/!) {
1335 if ($from->{'href'}) {
1337 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1338 esc_path
($from->{'file'}));
1341 esc_path
($from->{'file'});
1344 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1347 # combined diff (merge commit)
1348 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1349 if ($from->{'href'}[$i]) {
1351 $cgi->a({-href
=>href
(action
=>"blobdiff",
1352 hash_parent
=>$diffinfo->{'from_id'}[$i],
1353 hash_parent_base
=>$parents[$i],
1354 file_parent
=>$diffinfo->{'from_prefix'}.$from->{'file'}[$i],
1355 hash
=>$diffinfo->{'to_id'},
1357 file_name
=>$diffinfo->{'to_prefix'}.$to->{'file'}),
1359 -title
=>"diff" . ($i+1)},
1362 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1363 esc_path
($from->{'file'}[$i]));
1365 $line = '--- /dev/null';
1367 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
1372 #assert($line =~ m/^\+\+\+/) if DEBUG;
1373 # no extra formatting for "^+++ /dev/null"
1374 if ($line =~ m!^\+\+\+ "?b/!) {
1375 if ($to->{'href'}) {
1377 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1378 esc_path
($to->{'file'}));
1381 esc_path
($to->{'file'});
1384 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
1389 # create note for patch simplified by combined diff
1390 sub format_diff_cc_simplified
{
1391 my ($diffinfo, @parents) = @_;
1394 $result .= "<div class=\"diff header\">" .
1396 if (!is_deleted
($diffinfo)) {
1397 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1399 hash
=>$diffinfo->{'to_id'},
1400 file_name
=>$diffinfo->{'to_prefix'}.$diffinfo->{'to_file'}),
1402 esc_path
($diffinfo->{'to_file'}));
1404 $result .= esc_path
($diffinfo->{'to_file'});
1406 $result .= "</div>\n" . # class="diff header"
1407 "<div class=\"diff nodifferences\">" .
1409 "</div>\n"; # class="diff nodifferences"
1414 # format patch (diff) line (not to be used for diff headers)
1415 sub format_diff_line
{
1417 my ($from, $to) = @_;
1418 my $diff_class = "";
1422 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1424 my $prefix = substr($line, 0, scalar @
{$from->{'href'}});
1425 if ($line =~ m/^\@{3}/) {
1426 $diff_class = " chunk_header";
1427 } elsif ($line =~ m/^\\/) {
1428 $diff_class = " incomplete";
1429 } elsif ($prefix =~ tr/+/+/) {
1430 $diff_class = " add";
1431 } elsif ($prefix =~ tr/-/-/) {
1432 $diff_class = " rem";
1435 # assume ordinary diff
1436 my $char = substr($line, 0, 1);
1438 $diff_class = " add";
1439 } elsif ($char eq '-') {
1440 $diff_class = " rem";
1441 } elsif ($char eq '@') {
1442 $diff_class = " chunk_header";
1443 } elsif ($char eq "\\") {
1444 $diff_class = " incomplete";
1447 $line = untabify
($line);
1448 if ($from && $to && $line =~ m/^\@{2} /) {
1449 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1450 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1452 $from_lines = 0 unless defined $from_lines;
1453 $to_lines = 0 unless defined $to_lines;
1455 if ($from->{'href'}) {
1456 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1457 -class=>"list"}, $from_text);
1459 if ($to->{'href'}) {
1460 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1461 -class=>"list"}, $to_text);
1463 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1464 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1465 return "<div class=\"diff$diff_class\">$line</div>\n";
1466 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1467 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1468 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1470 @from_text = split(' ', $ranges);
1471 for (my $i = 0; $i < @from_text; ++$i) {
1472 ($from_start[$i], $from_nlines[$i]) =
1473 (split(',', substr($from_text[$i], 1)), 0);
1476 $to_text = pop @from_text;
1477 $to_start = pop @from_start;
1478 $to_nlines = pop @from_nlines;
1480 $line = "<span class=\"chunk_info\">$prefix ";
1481 for (my $i = 0; $i < @from_text; ++$i) {
1482 if ($from->{'href'}[$i]) {
1483 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1484 -class=>"list"}, $from_text[$i]);
1486 $line .= $from_text[$i];
1490 if ($to->{'href'}) {
1491 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1492 -class=>"list"}, $to_text);
1496 $line .= " $prefix</span>" .
1497 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1498 return "<div class=\"diff$diff_class\">$line</div>\n";
1500 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1503 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1504 # linked. Pass the hash of the tree/commit to snapshot.
1505 sub format_snapshot_links
{
1507 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1508 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1509 my $num_fmts = @snapshot_fmts;
1510 if ($num_fmts > 1) {
1511 # A parenthesized list of links bearing format names.
1512 # e.g. "snapshot (_tar.gz_ _zip_)"
1513 return "snapshot (" . join(' ', map
1520 }, $known_snapshot_formats{$_}{'display'})
1521 , @snapshot_fmts) . ")";
1522 } elsif ($num_fmts == 1) {
1523 # A single "snapshot" link whose tooltip bears the format name.
1525 my ($fmt) = @snapshot_fmts;
1531 snapshot_format
=>$fmt
1533 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1535 } else { # $num_fmts == 0
1540 ## ......................................................................
1541 ## functions returning values to be passed, perhaps after some
1542 ## transformation, to other functions; e.g. returning arguments to href()
1544 # returns hash to be passed to href to generate gitweb URL
1545 # in -title key it returns description of link
1547 my $format = shift || 'Atom';
1548 my %res = (action
=> lc($format));
1550 # feed links are possible only for project views
1551 return unless (defined $project);
1552 # some views should link to OPML, or to generic project feed,
1553 # or don't have specific feed yet (so they should use generic)
1554 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1557 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1558 # from tag links; this also makes possible to detect branch links
1559 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1560 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1563 # find log type for feed description (title)
1565 if (defined $file_name) {
1566 $type = "history of $file_name";
1567 $type .= "/" if ($action eq 'tree');
1568 $type .= " on '$branch'" if (defined $branch);
1570 $type = "log of $branch" if (defined $branch);
1573 $res{-title
} = $type;
1574 $res{'hash'} = (defined $branch ?
"refs/heads/$branch" : undef);
1575 $res{'file_name'} = $file_name;
1580 ## ----------------------------------------------------------------------
1581 ## git utility subroutines, invoking git commands
1583 # returns path to the core git executable and the --git-dir parameter as list
1585 return $GIT, '--git-dir='.$git_dir;
1588 # quote the given arguments for passing them to the shell
1589 # quote_command("command", "arg 1", "arg with ' and ! characters")
1590 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1591 # Try to avoid using this function wherever possible.
1594 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1597 # get HEAD ref of given project as hash
1598 sub git_get_head_hash
{
1599 my $project = shift;
1600 my $o_git_dir = $git_dir;
1602 $git_dir = "$projectroot/$project";
1603 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1606 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1610 if (defined $o_git_dir) {
1611 $git_dir = $o_git_dir;
1616 # get type of given object
1620 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1622 close $fd or return;
1627 # repository configuration
1628 our $config_file = '';
1631 # store multiple values for single key as anonymous array reference
1632 # single values stored directly in the hash, not as [ <value> ]
1633 sub hash_set_multi
{
1634 my ($hash, $key, $value) = @_;
1636 if (!exists $hash->{$key}) {
1637 $hash->{$key} = $value;
1638 } elsif (!ref $hash->{$key}) {
1639 $hash->{$key} = [ $hash->{$key}, $value ];
1641 push @
{$hash->{$key}}, $value;
1645 # return hash of git project configuration
1646 # optionally limited to some section, e.g. 'gitweb'
1647 sub git_parse_project_config
{
1648 my $section_regexp = shift;
1653 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1656 while (my $keyval = <$fh>) {
1658 my ($key, $value) = split(/\n/, $keyval, 2);
1660 hash_set_multi
(\
%config, $key, $value)
1661 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1668 # convert config value to boolean, 'true' or 'false'
1669 # no value, number > 0, 'true' and 'yes' values are true
1670 # rest of values are treated as false (never as error)
1671 sub config_to_bool
{
1674 # strip leading and trailing whitespace
1678 return (!defined $val || # section.key
1679 ($val =~ /^\d+$/ && $val) || # section.key = 1
1680 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1683 # convert config value to simple decimal number
1684 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1685 # to be multiplied by 1024, 1048576, or 1073741824
1689 # strip leading and trailing whitespace
1693 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1695 # unknown unit is treated as 1
1696 return $num * ($unit eq 'g' ?
1073741824 :
1697 $unit eq 'm' ?
1048576 :
1698 $unit eq 'k' ?
1024 : 1);
1703 # convert config value to array reference, if needed
1704 sub config_to_multi
{
1707 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
1710 sub git_get_project_config
{
1711 my ($key, $type) = @_;
1714 return unless ($key);
1715 $key =~ s/^gitweb\.//;
1716 return if ($key =~ m/\W/);
1719 if (defined $type) {
1722 unless ($type eq 'bool' || $type eq 'int');
1726 if (!defined $config_file ||
1727 $config_file ne "$git_dir/config") {
1728 %config = git_parse_project_config
('gitweb');
1729 $config_file = "$git_dir/config";
1733 if (!defined $type) {
1734 return $config{"gitweb.$key"};
1735 } elsif ($type eq 'bool') {
1736 # backward compatibility: 'git config --bool' returns true/false
1737 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
1738 } elsif ($type eq 'int') {
1739 return config_to_int
($config{"gitweb.$key"});
1741 return $config{"gitweb.$key"};
1744 # get hash of given path at given ref
1745 sub git_get_hash_by_path
{
1747 my $path = shift || return undef;
1752 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1753 or die_error
(500, "Open git-ls-tree failed");
1755 close $fd or return undef;
1757 if (!defined $line) {
1758 # there is no tree or hash given by $path at $base
1762 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1763 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1764 if (defined $type && $type ne $2) {
1765 # type doesn't match
1771 # get path of entry with given hash at given tree-ish (ref)
1772 # used to get 'from' filename for combined diff (merge commit) for renames
1773 sub git_get_path_by_hash
{
1774 my $base = shift || return;
1775 my $hash = shift || return;
1779 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1781 while (my $line = <$fd>) {
1784 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1785 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1786 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1795 ## ......................................................................
1796 ## git utility functions, directly accessing git repository
1798 sub git_get_project_description
{
1801 $git_dir = "$projectroot/$path";
1802 open my $fd, "$git_dir/description"
1803 or return git_get_project_config
('description');
1806 if (defined $descr) {
1812 sub git_get_project_ctags
{
1816 $git_dir = "$projectroot/$path";
1817 foreach (<$git_dir/ctags/*>) {
1818 open CT
, $_ or next;
1822 my $ctag = $_; $ctag =~ s
#.*/##;
1823 $ctags->{$ctag} = $val;
1828 sub git_populate_project_tagcloud
{
1831 # First, merge different-cased tags; tags vote on casing
1833 foreach (keys %$ctags) {
1834 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
1835 if (not $ctags_lc{lc $_}->{topcount
}
1836 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
1837 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
1838 $ctags_lc{lc $_}->{topname
} = $_;
1843 if (eval { require HTML
::TagCloud
; 1; }) {
1844 $cloud = HTML
::TagCloud
->new;
1845 foreach (sort keys %ctags_lc) {
1846 # Pad the title with spaces so that the cloud looks
1848 my $title = $ctags_lc{$_}->{topname
};
1849 $title =~ s/ / /g;
1850 $title =~ s/^/ /g;
1851 $title =~ s/$/ /g;
1852 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count
});
1855 $cloud = \
%ctags_lc;
1860 sub git_show_project_tagcloud
{
1861 my ($cloud, $count) = @_;
1862 print STDERR
ref($cloud)."..\n";
1863 if (ref $cloud eq 'HTML::TagCloud') {
1864 return $cloud->html_and_css($count);
1866 my @tags = sort { $cloud->{$a}->{count
} <=> $cloud->{$b}->{count
} } keys %$cloud;
1867 return '<p align="center">' . join (', ', map {
1868 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1869 } splice(@tags, 0, $count)) . '</p>';
1873 sub git_get_project_url_list
{
1876 $git_dir = "$projectroot/$path";
1877 open my $fd, "$git_dir/cloneurl"
1878 or return wantarray ?
1879 @
{ config_to_multi
(git_get_project_config
('url')) } :
1880 config_to_multi
(git_get_project_config
('url'));
1881 my @git_project_url_list = map { chomp; $_ } <$fd>;
1884 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
1887 sub git_get_projects_list
{
1892 $filter =~ s/\.git$//;
1894 my ($check_forks) = gitweb_check_feature
('forks');
1896 if (-d
$projects_list) {
1897 # search in directory
1898 my $dir = $projects_list . ($filter ?
"/$filter" : '');
1899 # remove the trailing "/"
1901 my $pfxlen = length("$dir");
1902 my $pfxdepth = ($dir =~ tr!/!!);
1905 follow_fast
=> 1, # follow symbolic links
1906 follow_skip
=> 2, # ignore duplicates
1907 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1909 # skip project-list toplevel, if we get it.
1910 return if (m!^[/.]$!);
1911 # only directories can be git repositories
1912 return unless (-d
$_);
1913 # don't traverse too deep (Find is super slow on os x)
1914 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1915 $File::Find
::prune
= 1;
1919 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1920 # we check related file in $projectroot
1921 if (check_export_ok
("$projectroot/$filter/$subdir")) {
1922 push @list, { path
=> ($filter ?
"$filter/" : '') . $subdir };
1923 $File::Find
::prune
= 1;
1928 } elsif (-f
$projects_list) {
1929 # read from file(url-encoded):
1930 # 'git%2Fgit.git Linus+Torvalds'
1931 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1932 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1934 open my ($fd), $projects_list or return;
1936 while (my $line = <$fd>) {
1938 my ($path, $owner) = split ' ', $line;
1939 $path = unescape
($path);
1940 $owner = unescape
($owner);
1941 if (!defined $path) {
1944 if ($filter ne '') {
1945 # looking for forks;
1946 my $pfx = substr($path, 0, length($filter));
1947 if ($pfx ne $filter) {
1950 my $sfx = substr($path, length($filter));
1951 if ($sfx !~ /^\/.*\
.git
$/) {
1954 } elsif ($check_forks) {
1956 foreach my $filter (keys %paths) {
1957 # looking for forks;
1958 my $pfx = substr($path, 0, length($filter));
1959 if ($pfx ne $filter) {
1962 my $sfx = substr($path, length($filter));
1963 if ($sfx !~ /^\/.*\
.git
$/) {
1966 # is a fork, don't include it in
1971 if (check_export_ok
("$projectroot/$path")) {
1974 owner
=> to_utf8
($owner),
1977 (my $forks_path = $path) =~ s/\.git$//;
1978 $paths{$forks_path}++;
1986 our $gitweb_project_owner = undef;
1987 sub git_get_project_list_from_file
{
1989 return if (defined $gitweb_project_owner);
1991 $gitweb_project_owner = {};
1992 # read from file (url-encoded):
1993 # 'git%2Fgit.git Linus+Torvalds'
1994 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1995 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1996 if (-f
$projects_list) {
1997 open (my $fd , $projects_list);
1998 while (my $line = <$fd>) {
2000 my ($pr, $ow) = split ' ', $line;
2001 $pr = unescape
($pr);
2002 $ow = unescape
($ow);
2003 $gitweb_project_owner->{$pr} = to_utf8
($ow);
2009 sub git_get_project_owner
{
2010 my $project = shift;
2013 return undef unless $project;
2014 $git_dir = "$projectroot/$project";
2016 if (!defined $gitweb_project_owner) {
2017 git_get_project_list_from_file
();
2020 if (exists $gitweb_project_owner->{$project}) {
2021 $owner = $gitweb_project_owner->{$project};
2023 if (!defined $owner){
2024 $owner = git_get_project_config
('owner');
2026 if (!defined $owner) {
2027 $owner = get_file_owner
("$git_dir");
2033 sub git_get_last_activity
{
2037 $git_dir = "$projectroot/$path";
2038 open($fd, "-|", git_cmd
(), 'for-each-ref',
2039 '--format=%(committer)',
2040 '--sort=-committerdate',
2042 'refs/heads') or return;
2043 my $most_recent = <$fd>;
2044 close $fd or return;
2045 if (defined $most_recent &&
2046 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2048 my $age = time - $timestamp;
2049 return ($age, age_string
($age));
2051 return (undef, undef);
2054 sub git_get_references
{
2055 my $type = shift || "";
2057 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2058 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2059 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
2060 ($type ?
("--", "refs/$type") : ()) # use -- <pattern> if $type
2063 while (my $line = <$fd>) {
2065 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2066 if (defined $refs{$1}) {
2067 push @
{$refs{$1}}, $2;
2073 close $fd or return;
2077 sub git_get_rev_name_tags
{
2078 my $hash = shift || return undef;
2080 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
2082 my $name_rev = <$fd>;
2085 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2088 # catches also '$hash undefined' output
2093 ## ----------------------------------------------------------------------
2094 ## parse to hash functions
2098 my $tz = shift || "-0000";
2101 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2102 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2103 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2104 $date{'hour'} = $hour;
2105 $date{'minute'} = $min;
2106 $date{'mday'} = $mday;
2107 $date{'day'} = $days[$wday];
2108 $date{'month'} = $months[$mon];
2109 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2110 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2111 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2112 $mday, $months[$mon], $hour ,$min;
2113 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2114 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2116 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2117 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2118 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2119 $date{'hour_local'} = $hour;
2120 $date{'minute_local'} = $min;
2121 $date{'tz_local'} = $tz;
2122 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2123 1900+$year, $mon+1, $mday,
2124 $hour, $min, $sec, $tz);
2133 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2134 $tag{'id'} = $tag_id;
2135 while (my $line = <$fd>) {
2137 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2138 $tag{'object'} = $1;
2139 } elsif ($line =~ m/^type (.+)$/) {
2141 } elsif ($line =~ m/^tag (.+)$/) {
2143 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2144 $tag{'author'} = $1;
2147 } elsif ($line =~ m/--BEGIN/) {
2148 push @comment, $line;
2150 } elsif ($line eq "") {
2154 push @comment, <$fd>;
2155 $tag{'comment'} = \
@comment;
2156 close $fd or return;
2157 if (!defined $tag{'name'}) {
2163 sub parse_commit_text
{
2164 my ($commit_text, $withparents) = @_;
2165 my @commit_lines = split '\n', $commit_text;
2168 pop @commit_lines; # Remove '\0'
2170 if (! @commit_lines) {
2174 my $header = shift @commit_lines;
2175 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2178 ($co{'id'}, my @parents) = split ' ', $header;
2179 while (my $line = shift @commit_lines) {
2180 last if $line eq "\n";
2181 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2183 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2185 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2187 $co{'author_epoch'} = $2;
2188 $co{'author_tz'} = $3;
2189 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2190 $co{'author_name'} = $1;
2191 $co{'author_email'} = $2;
2193 $co{'author_name'} = $co{'author'};
2195 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2196 $co{'committer'} = $1;
2197 $co{'committer_epoch'} = $2;
2198 $co{'committer_tz'} = $3;
2199 $co{'committer_name'} = $co{'committer'};
2200 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2201 $co{'committer_name'} = $1;
2202 $co{'committer_email'} = $2;
2204 $co{'committer_name'} = $co{'committer'};
2208 if (!defined $co{'tree'}) {
2211 $co{'parents'} = \
@parents;
2212 $co{'parent'} = $parents[0];
2214 foreach my $title (@commit_lines) {
2217 $co{'title'} = chop_str
($title, 80, 5);
2218 # remove leading stuff of merges to make the interesting part visible
2219 if (length($title) > 50) {
2220 $title =~ s/^Automatic //;
2221 $title =~ s/^merge (of|with) /Merge ... /i;
2222 if (length($title) > 50) {
2223 $title =~ s/(http|rsync):\/\///;
2225 if (length($title) > 50) {
2226 $title =~ s/(master|www|rsync)\.//;
2228 if (length($title) > 50) {
2229 $title =~ s/kernel.org:?//;
2231 if (length($title) > 50) {
2232 $title =~ s/\/pub\/scm//;
2235 $co{'title_short'} = chop_str
($title, 50, 5);
2239 if (! defined $co{'title'} || $co{'title'} eq "") {
2240 $co{'title'} = $co{'title_short'} = '(no commit message)';
2242 # remove added spaces
2243 foreach my $line (@commit_lines) {
2246 $co{'comment'} = \
@commit_lines;
2248 my $age = time - $co{'committer_epoch'};
2250 $co{'age_string'} = age_string
($age);
2251 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2252 if ($age > 60*60*24*7*2) {
2253 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2254 $co{'age_string_age'} = $co{'age_string'};
2256 $co{'age_string_date'} = $co{'age_string'};
2257 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2263 my ($commit_id) = @_;
2268 open my $fd, "-|", git_cmd
(), "rev-list",
2274 or die_error
(500, "Open git-rev-list failed");
2275 %co = parse_commit_text
(<$fd>, 1);
2282 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2290 open my $fd, "-|", git_cmd
(), "rev-list",
2293 ("--max-count=" . $maxcount),
2294 ("--skip=" . $skip),
2298 ($filename ?
($filename) : ())
2299 or die_error
(500, "Open git-rev-list failed");
2300 while (my $line = <$fd>) {
2301 my %co = parse_commit_text
($line);
2306 return wantarray ?
@cos : \
@cos;
2309 # parse line of git-diff-tree "raw" output
2310 sub parse_difftree_raw_line
{
2314 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2315 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2316 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2317 $res{'from_mode'} = $1;
2318 $res{'to_mode'} = $2;
2319 $res{'from_id'} = $3;
2321 $res{'status'} = $5;
2322 $res{'similarity'} = $6;
2323 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2324 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2326 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2329 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2330 # combined diff (for merge commit)
2331 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2332 $res{'nparents'} = length($1);
2333 $res{'from_mode'} = [ split(' ', $2) ];
2334 $res{'to_mode'} = pop @
{$res{'from_mode'}};
2335 $res{'from_id'} = [ split(' ', $3) ];
2336 $res{'to_id'} = pop @
{$res{'from_id'}};
2337 $res{'status'} = [ split('', $4) ];
2338 $res{'to_file'} = unquote
($5);
2340 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2341 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2342 $res{'commit'} = $1;
2345 return wantarray ?
%res : \
%res;
2348 # wrapper: return parsed line of git-diff-tree "raw" output
2349 # (the argument might be raw line, or parsed info)
2350 sub parsed_difftree_line
{
2351 my $line_or_ref = shift;
2353 if (ref($line_or_ref) eq "HASH") {
2354 # pre-parsed (or generated by hand)
2355 return $line_or_ref;
2357 return parse_difftree_raw_line
($line_or_ref);
2361 # parse line of git-ls-tree output
2362 sub parse_ls_tree_line
($;%) {
2367 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2368 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2376 $res{'name'} = unquote
($4);
2379 return wantarray ?
%res : \
%res;
2382 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2383 sub parse_from_to_diffinfo
{
2384 my ($diffinfo, $from, $to, @parents) = @_;
2386 if ($diffinfo->{'nparents'}) {
2388 $from->{'file'} = [];
2389 $from->{'href'} = [];
2390 fill_from_file_info
($diffinfo, @parents)
2391 unless exists $diffinfo->{'from_file'};
2392 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2393 $from->{'file'}[$i] =
2394 defined $diffinfo->{'from_file'}[$i] ?
2395 $diffinfo->{'from_file'}[$i] :
2396 $diffinfo->{'to_file'};
2397 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2398 $from->{'href'}[$i] = href
(action
=>"blob",
2399 hash_base
=>$parents[$i],
2400 hash
=>$diffinfo->{'from_id'}[$i],
2401 file_name
=>$diffinfo->{'from_prefix'}.$from->{'file'}[$i]);
2403 $from->{'href'}[$i] = undef;
2407 # ordinary (not combined) diff
2408 $from->{'file'} = $diffinfo->{'from_file'};
2409 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2410 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2411 hash
=>$diffinfo->{'from_id'},
2412 file_name
=>$diffinfo->{'from_prefix'}.$from->{'file'});
2414 delete $from->{'href'};
2418 $to->{'file'} = $diffinfo->{'to_file'};
2419 if (!is_deleted
($diffinfo)) { # file exists in result
2420 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2421 hash
=>$diffinfo->{'to_id'},
2422 file_name
=>$diffinfo->{'to_prefix'}.$to->{'file'});
2424 delete $to->{'href'};
2428 ## ......................................................................
2429 ## parse to array of hashes functions
2431 sub git_get_heads_list
{
2435 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2436 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
2437 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2440 while (my $line = <$fd>) {
2444 my ($refinfo, $committerinfo) = split(/\0/, $line);
2445 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2446 my ($committer, $epoch, $tz) =
2447 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2448 $ref_item{'fullname'} = $name;
2449 $name =~ s!^refs/heads/!!;
2451 $ref_item{'name'} = $name;
2452 $ref_item{'id'} = $hash;
2453 $ref_item{'title'} = $title || '(no commit message)';
2454 $ref_item{'epoch'} = $epoch;
2456 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2458 $ref_item{'age'} = "unknown";
2461 push @headslist, \
%ref_item;
2465 return wantarray ?
@headslist : \
@headslist;
2468 sub git_get_tags_list
{
2472 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2473 ($limit ?
'--count='.($limit+1) : ()), '--sort=-creatordate',
2474 '--format=%(objectname) %(objecttype) %(refname) '.
2475 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2478 while (my $line = <$fd>) {
2482 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2483 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2484 my ($creator, $epoch, $tz) =
2485 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2486 $ref_item{'fullname'} = $name;
2487 $name =~ s!^refs/tags/!!;
2489 $ref_item{'type'} = $type;
2490 $ref_item{'id'} = $id;
2491 $ref_item{'name'} = $name;
2492 if ($type eq "tag") {
2493 $ref_item{'subject'} = $title;
2494 $ref_item{'reftype'} = $reftype;
2495 $ref_item{'refid'} = $refid;
2497 $ref_item{'reftype'} = $type;
2498 $ref_item{'refid'} = $id;
2501 if ($type eq "tag" || $type eq "commit") {
2502 $ref_item{'epoch'} = $epoch;
2504 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2506 $ref_item{'age'} = "unknown";
2510 push @tagslist, \
%ref_item;
2514 return wantarray ?
@tagslist : \
@tagslist;
2517 ## ----------------------------------------------------------------------
2518 ## filesystem-related functions
2520 sub get_file_owner
{
2523 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2524 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2525 if (!defined $gcos) {
2529 $owner =~ s/[,;].*$//;
2530 return to_utf8
($owner);
2533 ## ......................................................................
2534 ## mimetype related functions
2536 sub mimetype_guess_file
{
2537 my $filename = shift;
2538 my $mimemap = shift;
2539 -r
$mimemap or return undef;
2542 open(MIME
, $mimemap) or return undef;
2544 next if m/^#/; # skip comments
2545 my ($mime, $exts) = split(/\t+/);
2546 if (defined $exts) {
2547 my @exts = split(/\s+/, $exts);
2548 foreach my $ext (@exts) {
2549 $mimemap{$ext} = $mime;
2555 $filename =~ /\.([^.]*)$/;
2556 return $mimemap{$1};
2559 sub mimetype_guess
{
2560 my $filename = shift;
2562 $filename =~ /\./ or return undef;
2564 if ($mimetypes_file) {
2565 my $file = $mimetypes_file;
2566 if ($file !~ m!^/!) { # if it is relative path
2567 # it is relative to project
2568 $file = "$projectroot/$project/$file";
2570 $mime = mimetype_guess_file
($filename, $file);
2572 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2578 my $filename = shift;
2581 my $mime = mimetype_guess
($filename);
2582 $mime and return $mime;
2586 return $default_blob_plain_mimetype unless $fd;
2589 return 'text/plain';
2590 } elsif (! $filename) {
2591 return 'application/octet-stream';
2592 } elsif ($filename =~ m/\.png$/i) {
2594 } elsif ($filename =~ m/\.gif$/i) {
2596 } elsif ($filename =~ m/\.jpe?g$/i) {
2597 return 'image/jpeg';
2599 return 'application/octet-stream';
2603 sub blob_contenttype
{
2604 my ($fd, $file_name, $type) = @_;
2606 $type ||= blob_mimetype
($fd, $file_name);
2607 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2608 $type .= "; charset=$default_text_plain_charset";
2614 ## ======================================================================
2615 ## functions printing HTML: header, footer, error page
2617 sub git_header_html
{
2618 my $status = shift || "200 OK";
2619 my $expires = shift;
2621 my $title = "$site_name";
2622 if (defined $project) {
2623 $title .= " - " . to_utf8
($project);
2624 if (defined $action) {
2625 $title .= "/$action";
2626 if (defined $file_name) {
2627 $title .= " - " . esc_path
($file_name);
2628 if ($action eq "tree" && $file_name !~ m
|/$|) {
2635 # require explicit support from the UA if we are to send the page as
2636 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2637 # we have to do this because MSIE sometimes globs '*/*', pretending to
2638 # support xhtml+xml but choking when it gets what it asked for.
2639 if (defined $cgi->http('HTTP_ACCEPT') &&
2640 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2641 $cgi->Accept('application/xhtml+xml') != 0) {
2642 $content_type = 'application/xhtml+xml';
2644 $content_type = 'text/html';
2646 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2647 -status
=> $status, -expires
=> $expires);
2648 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
2650 <?xml version="1.0" encoding="utf-8"?>
2651 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2652 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2653 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2654 <!-- git core binaries version $git_version -->
2656 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2657 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2658 <meta name="robots" content="index, nofollow"/>
2659 <title>$title</title>
2660 <script type="text/javascript">/* <![CDATA[ */
2661 function fixBlameLinks() {
2662 var allLinks = document.getElementsByTagName("a");
2663 for (var i = 0; i < allLinks.length; i++) {
2664 var link = allLinks.item(i);
2665 if (link.className == 'blamelink')
2666 link.href = link.href.replace("a=blame", "a=blame_incremental");
2671 # print out each stylesheet that exist
2672 if (defined $stylesheet) {
2673 #provides backwards capability for those people who define style sheet in a config file
2674 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2676 foreach my $stylesheet (@stylesheets) {
2677 next unless $stylesheet;
2678 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2681 if (defined $project) {
2682 my %href_params = get_feed_info
();
2683 if (!exists $href_params{'-title'}) {
2684 $href_params{'-title'} = 'log';
2687 foreach my $format qw(RSS Atom) {
2688 my $type = lc($format);
2690 '-rel' => 'alternate',
2691 '-title' => "$project - $href_params{'-title'} - $format feed",
2692 '-type' => "application/$type+xml"
2695 $href_params{'action'} = $type;
2696 $link_attr{'-href'} = href
(%href_params);
2698 "rel=\"$link_attr{'-rel'}\" ".
2699 "title=\"$link_attr{'-title'}\" ".
2700 "href=\"$link_attr{'-href'}\" ".
2701 "type=\"$link_attr{'-type'}\" ".
2704 $href_params{'extra_options'} = '--no-merges';
2705 $link_attr{'-href'} = href
(%href_params);
2706 $link_attr{'-title'} .= ' (no merges)';
2708 "rel=\"$link_attr{'-rel'}\" ".
2709 "title=\"$link_attr{'-title'}\" ".
2710 "href=\"$link_attr{'-href'}\" ".
2711 "type=\"$link_attr{'-type'}\" ".
2716 printf('<link rel="alternate" title="%s projects list" '.
2717 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2718 $site_name, href
(project
=>undef, action
=>"project_index"));
2719 printf('<link rel="alternate" title="%s projects feeds" '.
2720 'href="%s" type="text/x-opml" />'."\n",
2721 $site_name, href
(project
=>undef, action
=>"opml"));
2723 if (defined $favicon) {
2724 print qq(<link rel
="shortcut icon" href
="$favicon" type
="image/png" />\n);
2727 if (defined $gitwebjs) {
2728 print qq(<script src
="$gitwebjs" type
="text/javascript"></script
>\n);
2732 "<body onload=\"GitAddLinks(); fixBlameLinks();\">\n";
2734 if (-f
$site_header) {
2735 open (my $fd, $site_header);
2740 print "<div class=\"page_header\">\n" .
2741 $cgi->a({-href
=> esc_url
($logo_url),
2742 -title
=> $logo_label},
2743 qq(<img src
="$logo" width
="72" height
="27" alt
="git" class="logo"/>));
2744 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2745 if (defined $project) {
2746 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2747 if (defined $action) {
2754 my ($have_search) = gitweb_check_feature
('search');
2755 if (defined $project && $have_search) {
2756 if (!defined $searchtext) {
2760 if (defined $hash_base) {
2761 $search_hash = $hash_base;
2762 } elsif (defined $hash) {
2763 $search_hash = $hash;
2765 $search_hash = "HEAD";
2767 my $action = $my_uri;
2768 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2769 if ($use_pathinfo) {
2770 $action .= "/".esc_url
($project);
2772 print $cgi->startform(-method
=> "get", -action
=> $action) .
2773 "<div class=\"search\">\n" .
2775 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2776 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2777 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2778 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2779 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2780 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2782 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2783 "<span title=\"Extended regular expression\">" .
2784 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2785 -checked
=> $search_use_regexp) .
2788 $cgi->end_form() . "\n";
2792 sub git_footer_html
{
2793 my $feed_class = 'rss_logo';
2795 print "<div class=\"page_footer\">\n";
2796 if (defined $project) {
2797 my $descr = git_get_project_description
($project);
2798 if (defined $descr) {
2799 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2802 my %href_params = get_feed_info
();
2803 if (!%href_params) {
2804 $feed_class .= ' generic';
2806 $href_params{'-title'} ||= 'log';
2808 foreach my $format qw(RSS Atom) {
2809 $href_params{'action'} = lc($format);
2810 print $cgi->a({-href
=> href
(%href_params),
2811 -title
=> "$href_params{'-title'} $format feed",
2812 -class => $feed_class}, $format)."\n";
2816 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2817 -class => $feed_class}, "OPML") . " ";
2818 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2819 -class => $feed_class}, "TXT") . "\n";
2821 print "</div>\n"; # class="page_footer"
2823 if (-f
$site_footer) {
2824 open (my $fd, $site_footer);
2833 # die_error(<http_status_code>, <error_message>)
2834 # Example: die_error(404, 'Hash not found')
2835 # By convention, use the following status codes (as defined in RFC 2616):
2836 # 400: Invalid or missing CGI parameters, or
2837 # requested object exists but has wrong type.
2838 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2839 # this server or project.
2840 # 404: Requested object/revision/project doesn't exist.
2841 # 500: The server isn't configured properly, or
2842 # an internal error occurred (e.g. failed assertions caused by bugs), or
2843 # an unknown error occurred (e.g. the git binary died unexpectedly).
2845 my $status = shift || 500;
2846 my $error = shift || "Internal server error";
2848 my %http_responses = (400 => '400 Bad Request',
2849 403 => '403 Forbidden',
2850 404 => '404 Not Found',
2851 500 => '500 Internal Server Error');
2852 git_header_html
($http_responses{$status});
2854 <div class="page_body">
2864 ## ----------------------------------------------------------------------
2865 ## functions printing or outputting HTML: navigation
2867 sub git_print_page_nav
{
2868 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2869 $extra = '' if !defined $extra; # pager or formats
2871 my @navs = qw(summary shortlog log commit commitdiff tree);
2873 @navs = grep { $_ ne $suppress } @navs;
2876 my %arg = map { $_ => {action
=>$_} } @navs;
2877 if (defined $head) {
2878 for (qw(commit commitdiff)) {
2879 $arg{$_}{'hash'} = $head;
2881 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2882 for (qw(shortlog log)) {
2883 $arg{$_}{'hash'} = $head;
2888 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2889 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2891 my @actions = gitweb_check_feature
('actions');
2893 my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
2894 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
2896 $link =~ s
#%n#$project#g;
2897 $link =~ s
#%f#$git_dir#g;
2898 $treehead ?
$link =~ s
#%h#$treehead#g : $link =~ s#%h##g;
2899 $treebase ?
$link =~ s
#%b#$treebase#g : $link =~ s#%b##g;
2900 $arg{$label}{'_href'} = $link;
2903 print "<div class=\"page_nav\">\n" .
2905 map { $_ eq $current ?
2906 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
2908 print "<br/>\n$extra<br/>\n" .
2912 sub format_paging_nav
{
2913 my ($action, $hash, $head, $page, $has_next_link) = @_;
2917 if ($hash ne $head || $page) {
2918 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2920 $paging_nav .= "HEAD";
2924 $paging_nav .= " ⋅ " .
2925 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2926 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2928 $paging_nav .= " ⋅ prev";
2931 if ($has_next_link) {
2932 $paging_nav .= " ⋅ " .
2933 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2934 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2936 $paging_nav .= " ⋅ next";
2942 ## ......................................................................
2943 ## functions printing or outputting HTML: div
2945 sub git_print_header_div
{
2946 my ($action, $title, $hash, $hash_base) = @_;
2949 $args{'action'} = $action;
2950 $args{'hash'} = $hash if $hash;
2951 $args{'hash_base'} = $hash_base if $hash_base;
2953 print "<div class=\"header\">\n" .
2954 $cgi->a({-href
=> href
(%args), -class => "title"},
2955 $title ?
$title : $action) .
2959 #sub git_print_authorship (\%) {
2960 sub git_print_authorship
{
2963 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
2964 print "<div class=\"author_date\">" .
2965 esc_html
($co->{'author_name'}) .
2967 if ($ad{'hour_local'} < 6) {
2968 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2969 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2971 printf(" (%02d:%02d %s)",
2972 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2977 sub git_print_page_path
{
2983 print "<div class=\"page_path\">";
2984 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
2985 -title
=> 'tree root'}, to_utf8
("[$project]"));
2987 if (defined $name) {
2988 my @dirname = split '/', $name;
2989 my $basename = pop @dirname;
2992 foreach my $dir (@dirname) {
2993 $fullname .= ($fullname ?
'/' : '') . $dir;
2994 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
2996 -title
=> $fullname}, esc_path
($dir));
2999 if (defined $type && $type eq 'blob') {
3000 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
3002 -title
=> $name}, esc_path
($basename));
3003 } elsif (defined $type && $type eq 'tree') {
3004 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
3006 -title
=> $name}, esc_path
($basename));
3009 print esc_path
($basename);
3012 print "<br/></div>\n";
3015 # sub git_print_log (\@;%) {
3016 sub git_print_log
($;%) {
3020 if ($opts{'-remove_title'}) {
3021 # remove title, i.e. first line of log
3024 # remove leading empty lines
3025 while (defined $log->[0] && $log->[0] eq "") {
3032 foreach my $line (@
$log) {
3033 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3036 if (! $opts{'-remove_signoff'}) {
3037 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
3040 # remove signoff lines
3047 # print only one empty line
3048 # do not print empty line after signoff
3050 next if ($empty || $signoff);
3056 print format_log_line_html
($line) . "<br/>\n";
3059 if ($opts{'-final_empty_line'}) {
3060 # end with single empty line
3061 print "<br/>\n" unless $empty;
3065 # return link target (what link points to)
3066 sub git_get_link_target
{
3071 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
3075 $link_target = <$fd>;
3080 return $link_target;
3083 # given link target, and the directory (basedir) the link is in,
3084 # return target of link relative to top directory (top tree);
3085 # return undef if it is not possible (including absolute links).
3086 sub normalize_link_target
{
3087 my ($link_target, $basedir, $hash_base) = @_;
3089 # we can normalize symlink target only if $hash_base is provided
3090 return unless $hash_base;
3092 # absolute symlinks (beginning with '/') cannot be normalized
3093 return if (substr($link_target, 0, 1) eq '/');
3095 # normalize link target to path from top (root) tree (dir)
3098 $path = $basedir . '/' . $link_target;
3100 # we are in top (root) tree (dir)
3101 $path = $link_target;
3104 # remove //, /./, and /../
3106 foreach my $part (split('/', $path)) {
3107 # discard '.' and ''
3108 next if (!$part || $part eq '.');
3110 if ($part eq '..') {
3114 # link leads outside repository (outside top dir)
3118 push @path_parts, $part;
3121 $path = join('/', @path_parts);
3126 # print tree entry (row of git_tree), but without encompassing <tr> element
3127 sub git_print_tree_entry
{
3128 my ($t, $basedir, $hash_base, $have_blame) = @_;
3131 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3133 # The format of a table row is: mode list link. Where mode is
3134 # the mode of the entry, list is the name of the entry, an href,
3135 # and link is the action links of the entry.
3137 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3138 if ($t->{'type'} eq "blob") {
3139 print "<td class=\"list\">" .
3140 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3141 file_name
=>"$basedir$t->{'name'}", %base_key),
3142 -class => "list"}, esc_path
($t->{'name'}));
3143 if (S_ISLNK
(oct $t->{'mode'})) {
3144 my $link_target = git_get_link_target
($t->{'hash'});
3146 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3147 if (defined $norm_target) {
3149 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3150 file_name
=>$norm_target),
3151 -title
=> $norm_target}, esc_path
($link_target));
3153 print " -> " . esc_path
($link_target);
3158 print "<td class=\"link\">";
3159 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3160 file_name
=>"$basedir$t->{'name'}", %base_key)},
3164 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3165 file_name
=>"$basedir$t->{'name'}", %base_key), -class => "blamelink"},
3168 if (defined $hash_base) {
3170 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3171 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3175 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3176 file_name
=>"$basedir$t->{'name'}")},
3180 } elsif ($t->{'type'} eq "tree") {
3181 print "<td class=\"list\">";
3182 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3183 file_name
=>"$basedir$t->{'name'}", %base_key)},
3184 esc_path
($t->{'name'}));
3186 print "<td class=\"link\">";
3187 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3188 file_name
=>"$basedir$t->{'name'}", %base_key)},
3190 if (defined $hash_base) {
3192 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3193 file_name
=>"$basedir$t->{'name'}")},
3198 # unknown object: we can only present history for it
3199 # (this includes 'commit' object, i.e. submodule support)
3200 print "<td class=\"list\">" .
3201 esc_path
($t->{'name'}) .
3203 print "<td class=\"link\">";
3204 if (defined $hash_base) {
3205 print $cgi->a({-href
=> href
(action
=>"history",
3206 hash_base
=>$hash_base,
3207 file_name
=>"$basedir$t->{'name'}")},
3214 ## ......................................................................
3215 ## functions printing large fragments of HTML
3217 # get pre-image filenames for merge (combined) diff
3218 sub fill_from_file_info
{
3219 my ($diff, @parents) = @_;
3221 $diff->{'from_file'} = [ ];
3222 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3223 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3224 if ($diff->{'status'}[$i] eq 'R' ||
3225 $diff->{'status'}[$i] eq 'C') {
3226 $diff->{'from_file'}[$i] =
3227 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3234 # is current raw difftree line of file deletion
3236 my $diffinfo = shift;
3238 return $diffinfo->{'to_id'} eq ('0' x
40);
3241 # does patch correspond to [previous] difftree raw line
3242 # $diffinfo - hashref of parsed raw diff format
3243 # $patchinfo - hashref of parsed patch diff format
3244 # (the same keys as in $diffinfo)
3245 sub is_patch_split
{
3246 my ($diffinfo, $patchinfo) = @_;
3248 return defined $diffinfo && defined $patchinfo
3249 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3253 sub git_difftree_body
{
3254 my ($difftree, $from_prefix, $to_prefix, $hash, @parents) = @_;
3255 my ($parent) = $parents[0];
3256 my ($have_blame) = gitweb_check_feature
('blame');
3258 $from_prefix = !defined $from_prefix ?
'' : $from_prefix.'/';
3259 $to_prefix = !defined $to_prefix ?
'' : $to_prefix . '/';
3261 print "<div class=\"list_head\">\n";
3262 if ($#{$difftree} > 10) {
3263 print(($#{$difftree} + 1) . " files changed:\n");
3267 print "<table class=\"" .
3268 (@parents > 1 ?
"combined " : "") .
3271 # header only for combined diff in 'commitdiff' view
3272 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
3275 print "<thead><tr>\n" .
3276 "<th></th><th></th>\n"; # filename, patchN link
3277 for (my $i = 0; $i < @parents; $i++) {
3278 my $par = $parents[$i];
3280 $cgi->a({-href
=> href
(action
=>"commitdiff",
3281 hash
=>$hash, hash_parent
=>$par),
3282 -title
=> 'commitdiff to parent number ' .
3283 ($i+1) . ': ' . substr($par,0,7)},
3287 print "</tr></thead>\n<tbody>\n";
3292 foreach my $line (@
{$difftree}) {
3293 my $diff = parsed_difftree_line
($line);
3296 print "<tr class=\"dark\">\n";
3298 print "<tr class=\"light\">\n";
3302 if (exists $diff->{'nparents'}) { # combined diff
3304 fill_from_file_info
($diff, @parents)
3305 unless exists $diff->{'from_file'};
3307 if (!is_deleted
($diff)) {
3308 # file exists in the result (child) commit
3310 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3311 file_name
=>$to_prefix.$diff->{'to_file'},
3313 -class => "list"}, esc_path
($diff->{'to_file'})) .
3317 esc_path
($diff->{'to_file'}) .
3321 if ($action eq 'commitdiff') {
3324 print "<td class=\"link\">" .
3325 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3330 my $has_history = 0;
3331 my $not_deleted = 0;
3332 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3333 my $hash_parent = $parents[$i];
3334 my $from_hash = $diff->{'from_id'}[$i];
3335 my $from_path = $diff->{'from_file'}[$i];
3336 my $status = $diff->{'status'}[$i];
3338 $has_history ||= ($status ne 'A');
3339 $not_deleted ||= ($status ne 'D');
3341 if ($status eq 'A') {
3342 print "<td class=\"link\" align=\"right\"> | </td>\n";
3343 } elsif ($status eq 'D') {
3344 print "<td class=\"link\">" .
3345 $cgi->a({-href
=> href
(action
=>"blob",
3348 file_name
=>$from_prefix.$from_path)},
3352 if ($diff->{'to_id'} eq $from_hash) {
3353 print "<td class=\"link nochange\">";
3355 print "<td class=\"link\">";
3357 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3358 hash
=>$diff->{'to_id'},
3359 hash_parent
=>$from_hash,
3361 hash_parent_base
=>$hash_parent,
3362 file_name
=>$to_prefix.$diff->{'to_file'},
3363 file_parent
=>$from_prefix.$from_path)},
3369 print "<td class=\"link\">";
3371 print $cgi->a({-href
=> href
(action
=>"blob",
3372 hash
=>$diff->{'to_id'},
3373 file_name
=>$to_prefix.$diff->{'to_file'},
3376 print " | " if ($has_history);
3379 print $cgi->a({-href
=> href
(action
=>"history",
3380 file_name
=>$to_prefix.$diff->{'to_file'},
3387 next; # instead of 'else' clause, to avoid extra indent
3389 # else ordinary diff
3391 my ($to_mode_oct, $to_mode_str, $to_file_type);
3392 my ($from_mode_oct, $from_mode_str, $from_file_type);
3393 if ($diff->{'to_mode'} ne ('0' x
6)) {
3394 $to_mode_oct = oct $diff->{'to_mode'};
3395 if (S_ISREG
($to_mode_oct)) { # only for regular file
3396 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3398 $to_file_type = file_type
($diff->{'to_mode'});
3400 if ($diff->{'from_mode'} ne ('0' x
6)) {
3401 $from_mode_oct = oct $diff->{'from_mode'};
3402 if (S_ISREG
($to_mode_oct)) { # only for regular file
3403 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3405 $from_file_type = file_type
($diff->{'from_mode'});
3408 if ($diff->{'status'} eq "A") { # created
3409 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3410 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3411 $mode_chng .= "]</span>";
3413 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3414 hash_base
=>$hash, file_name
=>$to_prefix.$diff->{'file'}),
3415 -class => "list"}, esc_path
($diff->{'file'}));
3417 print "<td>$mode_chng</td>\n";
3418 print "<td class=\"link\">";
3419 if ($action eq 'commitdiff') {
3422 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3425 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3426 hash_base
=>$hash, file_name
=>$to_prefix.$diff->{'file'})},
3430 } elsif ($diff->{'status'} eq "D") { # deleted
3431 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3433 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3434 hash_base
=>$parent, file_name
=>$from_prefix.$diff->{'file'}),
3435 -class => "list"}, esc_path
($diff->{'file'}));
3437 print "<td>$mode_chng</td>\n";
3438 print "<td class=\"link\">";
3439 if ($action eq 'commitdiff') {
3442 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3445 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3446 hash_base
=>$parent, file_name
=>$from_prefix.$diff->{'file'})},
3449 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3450 file_name
=>$from_prefix.$diff->{'file'}), -class => "blamelink"},
3453 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3454 file_name
=>$from_prefix.$diff->{'file'})},
3458 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3459 my $mode_chnge = "";
3460 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3461 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3462 if ($from_file_type ne $to_file_type) {
3463 $mode_chnge .= " from $from_file_type to $to_file_type";
3465 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3466 if ($from_mode_str && $to_mode_str) {
3467 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3468 } elsif ($to_mode_str) {
3469 $mode_chnge .= " mode: $to_mode_str";
3472 $mode_chnge .= "]</span>\n";
3475 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3476 hash_base
=>$hash, file_name
=>$to_prefix.$diff->{'file'}),
3477 -class => "list"}, esc_path
($diff->{'file'}));
3479 print "<td>$mode_chnge</td>\n";
3480 print "<td class=\"link\">";
3481 if ($action eq 'commitdiff') {
3484 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3486 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3487 # "commit" view and modified file (not onlu mode changed)
3488 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3489 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3490 hash_base
=>$hash, hash_parent_base
=>$parent,
3491 file_name
=>$to_prefix.$diff->{'file'},
3492 file_parent
=>$from_prefix.$diff->{'file'})},
3496 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3497 hash_base
=>$hash, file_name
=>$to_prefix.$diff->{'file'})},
3500 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3501 file_name
=>$diff->{'file'}), -class => "blamelink"},
3504 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3505 file_name
=>$to_prefix.$diff->{'file'})},
3509 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3510 my %status_name = ('R' => 'moved', 'C' => 'copied');
3511 my $nstatus = $status_name{$diff->{'status'}};
3513 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3514 # mode also for directories, so we cannot use $to_mode_str
3515 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3518 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3519 hash
=>$diff->{'to_id'}, file_name
=>$to_prefix.$diff->{'to_file'}),
3520 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3521 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3522 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3523 hash
=>$diff->{'from_id'}, file_name
=>$from_prefix.$diff->{'from_file'}),
3524 -class => "list"}, esc_path
($diff->{'from_file'})) .
3525 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3526 "<td class=\"link\">";
3527 if ($action eq 'commitdiff') {
3530 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3532 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3533 # "commit" view and modified file (not only pure rename or copy)
3534 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3535 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3536 hash_base
=>$hash, hash_parent_base
=>$parent,
3537 file_name
=>$to_prefix.$diff->{'to_file'}, file_parent
=>$from_prefix.$diff->{'from_file'})},
3541 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3542 hash_base
=>$parent, file_name
=>$to_prefix.$diff->{'to_file'})},
3545 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3546 file_name
=>$diff->{'to_file'}), -class => "blamelink"},
3549 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3550 file_name
=>$to_prefix.$diff->{'to_file'})},
3554 } # we should not encounter Unmerged (U) or Unknown (X) status
3557 print "</tbody>" if $has_header;
3561 sub git_patchset_body
{
3562 my ($fd, $difftree, $from_prefix, $to_prefix, $hash, @hash_parents) = @_;
3563 my ($hash_parent) = $hash_parents[0];
3565 my $is_combined = (@hash_parents > 1);
3567 my $patch_number = 0;
3573 $from_prefix = !defined $from_prefix ?
'' : $from_prefix.'/';
3574 $to_prefix = !defined $to_prefix ?
'' : $to_prefix . '/';
3576 print "<div class=\"patchset\">\n";
3578 # skip to first patch
3579 while ($patch_line = <$fd>) {
3582 last if ($patch_line =~ m/^diff /);
3586 while ($patch_line) {
3588 # parse "git diff" header line
3589 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3590 # $1 is from_name, which we do not use
3591 $to_name = unquote
($2);
3592 $to_name =~ s!^b/!!;
3593 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3594 # $1 is 'cc' or 'combined', which we do not use
3595 $to_name = unquote
($2);
3600 # check if current patch belong to current raw line
3601 # and parse raw git-diff line if needed
3602 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3603 # this is continuation of a split patch
3604 print "<div class=\"patch cont\">\n";
3605 $diffinfo->{'from_prefix'} = $from_prefix;
3606 $diffinfo->{'to_prefix'} = $to_prefix;
3608 # advance raw git-diff output if needed
3609 $patch_idx++ if defined $diffinfo;
3611 # read and prepare patch information
3612 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3614 # compact combined diff output can have some patches skipped
3615 # find which patch (using pathname of result) we are at now;
3617 while ($to_name ne $diffinfo->{'to_file'}) {
3618 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3619 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3620 "</div>\n"; # class="patch"
3625 last if $patch_idx > $#$difftree;
3626 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3630 $diffinfo->{'from_prefix'} = $from_prefix;
3631 $diffinfo->{'to_prefix'} = $to_prefix;
3633 # modifies %from, %to hashes
3634 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3635 # this is first patch for raw difftree line with $patch_idx index
3636 # we index @$difftree array from 0, but number patches from 1
3637 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3641 #assert($patch_line =~ m/^diff /) if DEBUG;
3642 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3644 # print "git diff" header
3645 print format_git_diff_header_line
($patch_line, $diffinfo,
3648 # print extended diff header
3649 print "<div class=\"diff extended_header\">\n";
3651 while ($patch_line = <$fd>) {
3654 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3656 print format_extended_diff_header_line
($patch_line, $diffinfo,
3659 print "</div>\n"; # class="diff extended_header"
3661 # from-file/to-file diff header
3662 if (! $patch_line) {
3663 print "</div>\n"; # class="patch"
3666 next PATCH
if ($patch_line =~ m/^diff /);
3667 #assert($patch_line =~ m/^---/) if DEBUG;
3669 my $last_patch_line = $patch_line;
3670 $patch_line = <$fd>;
3672 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3674 print format_diff_from_to_header
($last_patch_line, $patch_line,
3675 $diffinfo, \
%from, \
%to,
3680 while ($patch_line = <$fd>) {
3683 next PATCH
if ($patch_line =~ m/^diff /);
3685 print format_diff_line
($patch_line, \
%from, \
%to);
3689 print "</div>\n"; # class="patch"
3692 # for compact combined (--cc) format, with chunk and patch simpliciaction
3693 # patchset might be empty, but there might be unprocessed raw lines
3694 for (++$patch_idx if $patch_number > 0;
3695 $patch_idx < @
$difftree;
3697 # read and prepare patch information
3698 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3700 # generate anchor for "patch" links in difftree / whatchanged part
3701 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3702 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3703 "</div>\n"; # class="patch"
3708 if ($patch_number == 0) {
3709 if (@hash_parents > 1) {
3710 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3712 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3716 print "</div>\n"; # class="patchset"
3719 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3721 # fills project list info (age, description, owner, forks) for each
3722 # project in the list, removing invalid projects from returned list
3723 # NOTE: modifies $projlist, but does not remove entries from it
3724 sub fill_project_list_info
{
3725 my ($projlist, $check_forks) = @_;
3728 my $show_ctags = gitweb_check_feature
('ctags');
3730 foreach my $pr (@
$projlist) {
3731 my (@activity) = git_get_last_activity
($pr->{'path'});
3732 unless (@activity) {
3735 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3736 if (!defined $pr->{'descr'}) {
3737 my $descr = git_get_project_description
($pr->{'path'}) || "";
3738 $descr = to_utf8
($descr);
3739 $pr->{'descr_long'} = $descr;
3740 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3742 if (!defined $pr->{'owner'}) {
3743 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3746 my $pname = $pr->{'path'};
3747 if (($pname =~ s/\.git$//) &&
3748 ($pname !~ /\/$/) &&
3749 (-d
"$projectroot/$pname")) {
3750 $pr->{'forks'} = "-d $projectroot/$pname";
3755 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
3756 push @projects, $pr;
3762 # print 'sort by' <th> element, generating 'sort by $name' replay link
3763 # if that order is not selected
3765 my ($name, $order, $header) = @_;
3766 $header ||= ucfirst($name);
3768 if ($order eq $name) {
3769 print "<th>$header</th>\n";
3772 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3773 -class => "header"}, $header) .
3778 sub git_project_list_body
{
3779 # actually uses global variable $project
3780 my ($projlist, $order, $from, $to, $extra, $no_header, $cache_lifetime) = @_;
3782 my ($check_forks) = gitweb_check_feature
('forks');
3785 use POSIX
qw(:fcntl_h);
3786 use Storable
qw(store_fd retrieve);
3788 my $cache_file = "$cache_dir/$projlist_cache_name";
3794 if ($cache_lifetime && -f
$cache_file) {
3795 $cache_mtime = stat($cache_file)->mtime;
3797 if (defined $cache_mtime && # caching is on and $cache_file exists
3798 $cache_mtime + $cache_lifetime*60 > $now &&
3799 (my $dump = retrieve
($cache_file))) {
3800 $stale = $now - $cache_mtime;
3803 if (defined $cache_mtime) {
3804 # Postpone timeout by two minutes so that we get
3805 # enough time to do our job, or to be more exact
3806 # make cache expire after two minutes from now.
3807 my $time = $now - $cache_lifetime*60 + 120;
3808 utime $time, $time, $cache_file;
3810 @projects = fill_project_list_info
($projlist, $check_forks);
3811 if ($cache_lifetime &&
3812 (-d
$cache_dir || mkdir($cache_dir, 0700)) &&
3813 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, 0600)) {
3814 store_fd
(\
@projects, $fd);
3816 rename "$cache_file.lock", $cache_file;
3820 $order ||= $default_projects_order;
3821 $from = 0 unless defined $from;
3822 $to = $#projects if (!defined $to || $#projects < $to);
3825 project
=> { key
=> 'path', type
=> 'str' },
3826 descr
=> { key
=> 'descr_long', type
=> 'str' },
3827 owner
=> { key
=> 'owner', type
=> 'str' },
3828 age
=> { key
=> 'age', type
=> 'num' }
3830 my $oi = $order_info{$order};
3831 if ($oi->{'type'} eq 'str') {
3832 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3834 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3837 if ($cache_lifetime && $stale > 0) {
3838 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n";
3841 my $show_ctags = gitweb_check_feature
('ctags');
3844 foreach my $p (@projects) {
3845 foreach my $ct (keys %{$p->{'ctags'}}) {
3846 $ctags{$ct} += $p->{'ctags'}->{$ct};
3849 my $cloud = git_populate_project_tagcloud
(\
%ctags);
3850 print git_show_project_tagcloud
($cloud, 64);
3853 print "<table class=\"project_list\">\n";
3854 unless ($no_header) {
3857 print "<th></th>\n";
3859 print_sort_th
('project', $order, 'Project');
3860 print_sort_th
('descr', $order, 'Description');
3861 print_sort_th
('owner', $order, 'Owner');
3862 print_sort_th
('age', $order, 'Last Change');
3863 print "<th></th>\n" . # for links
3867 my $tagfilter = $cgi->param('by_tag');
3868 for (my $i = $from; $i <= $to; $i++) {
3869 my $pr = $projects[$i];
3871 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3874 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s
#\.git$#/#;
3875 $forkbase="^$forkbase" if $forkbase;
3876 next if not $tagfilter and $pr->{'path'} =~ m
#$forkbase.*/.*#; # regexp-safe
3880 print "<tr class=\"dark\">\n";
3882 print "<tr class=\"light\">\n";
3887 if ($pr->{'forks'}) {
3888 print "<!-- $pr->{'forks'} -->\n";
3889 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3893 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3894 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3895 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3896 -class => "list", -title
=> $pr->{'descr_long'}},
3897 esc_html
($pr->{'descr'})) . "</td>\n" .
3898 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3899 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3900 (defined $pr->{'age_string'} ?
$pr->{'age_string'} : "No commits") . "</td>\n" .
3901 "<td class=\"link\">" .
3902 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3903 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3904 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3905 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3906 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3910 if (defined $extra) {
3913 print "<td></td>\n";
3915 print "<td colspan=\"5\">$extra</td>\n" .
3921 sub git_shortlog_body
{
3922 # uses global variable $project
3923 my ($commitlist, $from, $to, $refs, $extra) = @_;
3925 $from = 0 unless defined $from;
3926 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3928 print "<table class=\"shortlog\">\n";
3930 for (my $i = $from; $i <= $to; $i++) {
3931 my %co = %{$commitlist->[$i]};
3932 my $commit = $co{'id'};
3933 my $ref = format_ref_marker
($refs, $commit);
3935 print "<tr class=\"dark\">\n";
3937 print "<tr class=\"light\">\n";
3940 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3941 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3942 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3943 "<td><i>" . $author . "</i></td>\n" .
3945 print format_subject_html
($co{'title'}, $co{'title_short'},
3946 href
(action
=>"commit", hash
=>$commit), $ref);
3948 "<td class=\"link\">" .
3949 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3950 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3951 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3952 my $snapshot_links = format_snapshot_links
($commit);
3953 if (defined $snapshot_links) {
3954 print " | " . $snapshot_links;
3959 if (defined $extra) {
3961 "<td colspan=\"4\">$extra</td>\n" .
3967 sub git_history_body
{
3968 # Warning: assumes constant type (blob or tree) during history
3969 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3971 $from = 0 unless defined $from;
3972 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3974 print "<table class=\"history\">\n";
3976 for (my $i = $from; $i <= $to; $i++) {
3977 my %co = %{$commitlist->[$i]};
3981 my $commit = $co{'id'};
3983 my $ref = format_ref_marker
($refs, $commit);
3986 print "<tr class=\"dark\">\n";
3988 print "<tr class=\"light\">\n";
3991 # shortlog uses chop_str($co{'author_name'}, 10)
3992 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
3993 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3994 "<td><i>" . $author . "</i></td>\n" .
3996 # originally git_history used chop_str($co{'title'}, 50)
3997 print format_subject_html
($co{'title'}, $co{'title_short'},
3998 href
(action
=>"commit", hash
=>$commit), $ref);
4000 "<td class=\"link\">" .
4001 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
4002 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
4004 if ($ftype eq 'blob') {
4005 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
4006 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
4007 if (defined $blob_current && defined $blob_parent &&
4008 $blob_current ne $blob_parent) {
4010 $cgi->a({-href
=> href
(action
=>"blobdiff",
4011 hash
=>$blob_current, hash_parent
=>$blob_parent,
4012 hash_base
=>$hash_base, hash_parent_base
=>$commit,
4013 file_name
=>$file_name)},
4020 if (defined $extra) {
4022 "<td colspan=\"4\">$extra</td>\n" .
4029 # uses global variable $project
4030 my ($taglist, $from, $to, $extra) = @_;
4031 $from = 0 unless defined $from;
4032 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4034 print "<table class=\"tags\">\n";
4036 for (my $i = $from; $i <= $to; $i++) {
4037 my $entry = $taglist->[$i];
4039 my $comment = $tag{'subject'};
4041 if (defined $comment) {
4042 $comment_short = chop_str
($comment, 30, 5);
4045 print "<tr class=\"dark\">\n";
4047 print "<tr class=\"light\">\n";
4050 if (defined $tag{'age'}) {
4051 print "<td><i>$tag{'age'}</i></td>\n";
4053 print "<td></td>\n";
4056 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
4057 -class => "list name"}, esc_html
($tag{'name'})) .
4060 if (defined $comment) {
4061 print format_subject_html
($comment, $comment_short,
4062 href
(action
=>"tag", hash
=>$tag{'id'}));
4065 "<td class=\"selflink\">";
4066 if ($tag{'type'} eq "tag") {
4067 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
4072 "<td class=\"link\">" . " | " .
4073 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
4074 if ($tag{'reftype'} eq "commit") {
4075 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
4076 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
4077 } elsif ($tag{'reftype'} eq "blob") {
4078 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
4083 if (defined $extra) {
4085 "<td colspan=\"5\">$extra</td>\n" .
4091 sub git_heads_body
{
4092 # uses global variable $project
4093 my ($headlist, $head, $from, $to, $extra) = @_;
4094 $from = 0 unless defined $from;
4095 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4097 print "<table class=\"heads\">\n";
4099 for (my $i = $from; $i <= $to; $i++) {
4100 my $entry = $headlist->[$i];
4102 my $curr = $ref{'id'} eq $head;
4104 print "<tr class=\"dark\">\n";
4106 print "<tr class=\"light\">\n";
4109 print "<td><i>$ref{'age'}</i></td>\n" .
4110 ($curr ?
"<td class=\"current_head\">" : "<td>") .
4111 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
4112 -class => "list name"},esc_html
($ref{'name'})) .
4114 "<td class=\"link\">" .
4115 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
4116 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
4117 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
4121 if (defined $extra) {
4123 "<td colspan=\"3\">$extra</td>\n" .
4129 sub git_search_grep_body
{
4130 my ($commitlist, $from, $to, $extra) = @_;
4131 $from = 0 unless defined $from;
4132 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4134 print "<table class=\"commit_search\">\n";
4136 for (my $i = $from; $i <= $to; $i++) {
4137 my %co = %{$commitlist->[$i]};
4141 my $commit = $co{'id'};
4143 print "<tr class=\"dark\">\n";
4145 print "<tr class=\"light\">\n";
4148 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
4149 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4150 "<td><i>" . $author . "</i></td>\n" .
4152 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
4153 -class => "list subject"},
4154 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
4155 my $comment = $co{'comment'};
4156 foreach my $line (@
$comment) {
4157 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4158 my ($lead, $match, $trail) = ($1, $2, $3);
4159 $match = chop_str
($match, 70, 5, 'center');
4160 my $contextlen = int((80 - length($match))/2);
4161 $contextlen = 30 if ($contextlen > 30);
4162 $lead = chop_str
($lead, $contextlen, 10, 'left');
4163 $trail = chop_str
($trail, $contextlen, 10, 'right');
4165 $lead = esc_html
($lead);
4166 $match = esc_html
($match);
4167 $trail = esc_html
($trail);
4169 print "$lead<span class=\"match\">$match</span>$trail<br />";
4173 "<td class=\"link\">" .
4174 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4176 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4178 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4182 if (defined $extra) {
4184 "<td colspan=\"3\">$extra</td>\n" .
4190 ## ======================================================================
4191 ## ======================================================================
4194 sub git_project_list
{
4195 my $order = $cgi->param('o');
4196 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4197 die_error
(400, "Unknown order parameter");
4200 my @list = git_get_projects_list
();
4202 die_error
(404, "No projects found");
4206 if (-f
$home_text) {
4207 print "<div class=\"index_include\">\n";
4208 open (my $fd, $home_text);
4213 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, $projlist_cache_lifetime);
4218 my $order = $cgi->param('o');
4219 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4220 die_error
(400, "Unknown order parameter");
4223 my @list = git_get_projects_list
($project);
4225 die_error
(404, "No forks found");
4229 git_print_page_nav
('','');
4230 git_print_header_div
('summary', "$project forks");
4231 git_project_list_body
(\
@list, $order);
4235 sub git_project_index
{
4236 my @projects = git_get_projects_list
($project);
4239 -type
=> 'text/plain',
4240 -charset
=> 'utf-8',
4241 -content_disposition
=> 'inline; filename="index.aux"');
4243 foreach my $pr (@projects) {
4244 if (!exists $pr->{'owner'}) {
4245 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4248 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4249 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4250 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4251 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4255 print "$path $owner\n";
4260 my $descr = git_get_project_description
($project) || "none";
4261 my %co = parse_commit
("HEAD");
4262 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4263 my $head = $co{'id'};
4265 my $owner = git_get_project_owner
($project);
4267 my $refs = git_get_references
();
4268 # These get_*_list functions return one more to allow us to see if
4269 # there are more ...
4270 my @taglist = git_get_tags_list
(16);
4271 my @headlist = git_get_heads_list
(16);
4273 my ($check_forks) = gitweb_check_feature
('forks');
4276 @forklist = git_get_projects_list
($project);
4280 git_print_page_nav
('summary','', $head);
4282 print "<div class=\"title\"> </div>\n";
4283 print "<table class=\"projects_list\">\n" .
4284 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4285 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4286 if (defined $cd{'rfc2822'}) {
4287 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4290 # use per project git URL list in $projectroot/$project/cloneurl
4291 # or make project git URL from git base URL and project name
4292 my $url_tag = "URL";
4293 my @url_list = git_get_project_url_list
($project);
4294 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4295 foreach my $git_url (@url_list) {
4296 next unless $git_url;
4297 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4302 my $show_ctags = (gitweb_check_feature
('ctags'))[0];
4304 my $ctags = git_get_project_ctags
($project);
4305 my $cloud = git_populate_project_tagcloud
($ctags);
4306 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4307 print "</td>\n<td>" unless %$ctags;
4308 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4309 print "</td>\n<td>" if %$ctags;
4310 print git_show_project_tagcloud
($cloud, 48);
4316 if (-s
"$projectroot/$project/README.html") {
4317 if (open my $fd, "$projectroot/$project/README.html") {
4318 print "<div class=\"title\">readme</div>\n" .
4319 "<div class=\"readme\">\n";
4320 print $_ while (<$fd>);
4321 print "\n</div>\n"; # class="readme"
4326 # we need to request one more than 16 (0..15) to check if
4328 my @commitlist = $head ? parse_commits
($head, 17) : ();
4330 git_print_header_div
('shortlog');
4331 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4332 $#commitlist <= 15 ?
undef :
4333 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4337 git_print_header_div
('tags');
4338 git_tags_body
(\
@taglist, 0, 15,
4339 $#taglist <= 15 ?
undef :
4340 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4344 git_print_header_div
('heads');
4345 git_heads_body
(\
@headlist, $head, 0, 15,
4346 $#headlist <= 15 ?
undef :
4347 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4351 git_print_header_div
('forks');
4352 git_project_list_body
(\
@forklist, 'age', 0, 15,
4353 $#forklist <= 15 ?
undef :
4354 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4362 my $head = git_get_head_hash
($project);
4364 git_print_page_nav
('','', $head,undef,$head);
4365 my %tag = parse_tag
($hash);
4368 die_error
(404, "Unknown tag object");
4371 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4372 print "<div class=\"title_text\">\n" .
4373 "<table class=\"object_header\">\n" .
4375 "<td>object</td>\n" .
4376 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4377 $tag{'object'}) . "</td>\n" .
4378 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4379 $tag{'type'}) . "</td>\n" .
4381 if (defined($tag{'author'})) {
4382 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4383 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4384 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4385 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4388 print "</table>\n\n" .
4390 print "<div class=\"page_body\">";
4391 my $comment = $tag{'comment'};
4392 foreach my $line (@
$comment) {
4394 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4400 sub git_blame_data
{
4404 my ($have_blame) = gitweb_check_feature
('blame');
4406 die_error
('403 Permission denied', "Permission denied");
4408 die_error
('404 Not Found', "File name not defined") if (!$file_name);
4409 $hash_base ||= git_get_head_hash
($project);
4410 die_error
(undef, "Couldn't find base commit") unless ($hash_base);
4411 my %co = parse_commit
($hash_base)
4412 or die_error
(undef, "Reading commit failed");
4413 if (!defined $hash) {
4414 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4415 or die_error
(undef, "Error looking up file");
4417 $ftype = git_get_type
($hash);
4418 if ($ftype !~ "blob") {
4419 die_error
("400 Bad Request", "Object is not a blob");
4421 open ($fd, "-|", git_cmd
(), "blame", '--incremental', $hash_base, '--',
4423 or die_error
(undef, "Open git-blame --incremental failed");
4425 print $cgi->header(-type
=>"text/plain", -charset
=> 'utf-8',
4426 -status
=> "200 OK");
4429 if (/^([0-9a-f]{40}) ([0-9]+) ([0-9]+) ([0-9]+)/ or
4430 /^author-time |^author |^filename /) {
4435 close $fd or print "Reading blame data failed\n";
4438 sub git_blame_common
{
4444 gitweb_check_feature
('blame')
4445 or die_error
(403, "Blame view not allowed");
4447 die_error
(400, "No file name given") unless $file_name;
4448 $hash_base ||= git_get_head_hash
($project);
4449 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4450 my %co = parse_commit
($hash_base)
4451 or die_error
(404, "Commit not found");
4452 if (!defined $hash) {
4453 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4454 or die_error
(404, "Error looking up file");
4456 $ftype = git_get_type
($hash);
4457 if ($ftype !~ "blob") {
4458 die_error
(400, "Object is not a blob");
4460 if ($type eq 'incremental') {
4461 open ($fd, "-|", git_cmd
(), 'cat-file', 'blob', $hash)
4462 or die_error
(undef, "Open git-cat-file failed");
4464 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4465 $file_name, $hash_base)
4466 or die_error
(500, "Open git-blame failed");
4470 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4473 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4476 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name), -class => "blamelink"},
4478 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4479 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4480 git_print_page_path
($file_name, $ftype, $hash_base);
4481 my @rev_color = (qw(light2 dark2));
4482 my $num_colors = scalar(@rev_color);
4483 my $current_color = 0;
4487 <div class="page_body">
4488 <table class="blame">
4489 <tr><th>Commit <a href="javascript:extra_blame_columns()" id="columns_expander">[+]</a></th>
4490 <th class="extra_column">Author</th>
4491 <th class="extra_column">Date</th>
4499 if ($type eq 'incremental') {
4500 # Empty stage with just the file contents
4502 print "<tr id=\"l$linenr\" class=\"light2\">";
4503 print '<td class="sha1"><a href=""></a></td>';
4504 print "<td class=\"extra_column\"></td>";
4505 print "<td class=\"extra_column\"></td>";
4506 print "<td class=\"linenr\"><a class=\"linenr\" href=\"\">$linenr</a></td><td class=\"pre\">" . esc_html
($_) . "</td>\n";
4511 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4512 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4513 if (!exists $metainfo{$full_rev}) {
4514 $metainfo{$full_rev} = {};
4516 my $meta = $metainfo{$full_rev};
4519 if (/^(\S+) (.*)$/) {
4525 my $rev = substr($full_rev, 0, 8);
4526 my $author = $meta->{'author'};
4527 my %date = parse_date
($meta->{'author-time'},
4528 $meta->{'author-tz'});
4529 my $date = $date{'iso-tz'};
4531 $current_color = ++$current_color % $num_colors;
4533 print "<tr class=\"$rev_color[$current_color]\">\n";
4535 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
4536 print "<td class=\"sha1\"";
4537 print " title=\"". esc_html
($author) . ", $date\"";
4539 print $cgi->a({-href
=> href
(action
=>"commit",
4541 file_name
=>$file_name)},
4544 print "<td class=\"extra_column\" $rowspan>". esc_html
($author) . "</td>";
4545 print "<td class=\"extra_column\" $rowspan>". $date . "</td>";
4547 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4548 or die_error
(500, "Open git-rev-parse failed");
4549 my $parent_commit = <$dd>;
4551 chomp($parent_commit);
4552 my $blamed = href
(action
=> 'blame',
4553 file_name
=> $meta->{'filename'},
4554 hash_base
=> $parent_commit);
4555 print "<td class=\"linenr\">";
4556 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4558 -class => "linenr" },
4561 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4568 or print "Reading blob failed\n";
4570 if ($type eq 'incremental') {
4571 print "<script type=\"text/javascript\">\n";
4572 print "startBlame(\"" . href
(action
=>"blame_data", hash_base
=>$hash_base, file_name
=>$file_name) . "\", \"" .
4573 href
(-partial_query
=>1) . "\");\n";
4574 print "</script>\n";
4580 sub git_blame_incremental
{
4581 git_blame_common
('incremental');
4585 git_blame_common
('oneshot');
4589 my $head = git_get_head_hash
($project);
4591 git_print_page_nav
('','', $head,undef,$head);
4592 git_print_header_div
('summary', $project);
4594 my @tagslist = git_get_tags_list
();
4596 git_tags_body
(\
@tagslist);
4602 my $head = git_get_head_hash
($project);
4604 git_print_page_nav
('','', $head,undef,$head);
4605 git_print_header_div
('summary', $project);
4607 my @headslist = git_get_heads_list
();
4609 git_heads_body
(\
@headslist, $head);
4614 sub git_blob_plain
{
4618 if (!defined $hash) {
4619 if (defined $file_name) {
4620 my $base = $hash_base || git_get_head_hash
($project);
4621 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4622 or die_error
(404, "Cannot find file");
4624 die_error
(400, "No file name defined");
4626 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4627 # blobs defined by non-textual hash id's can be cached
4631 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4632 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4634 # content-type (can include charset)
4635 $type = blob_contenttype
($fd, $file_name, $type);
4637 # "save as" filename, even when no $file_name is given
4638 my $save_as = "$hash";
4639 if (defined $file_name) {
4640 $save_as = $file_name;
4641 } elsif ($type =~ m/^text\//) {
4647 -expires
=> $expires,
4648 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4650 binmode STDOUT
, ':raw';
4652 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4660 if (!defined $hash) {
4661 if (defined $file_name) {
4662 my $base = $hash_base || git_get_head_hash
($project);
4663 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4664 or die_error
(404, "Cannot find file");
4666 die_error
(400, "No file name defined");
4668 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4669 # blobs defined by non-textual hash id's can be cached
4673 my ($have_blame) = gitweb_check_feature
('blame');
4674 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4675 or die_error
(500, "Couldn't cat $file_name, $hash");
4676 my $mimetype = blob_mimetype
($fd, $file_name);
4677 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4679 return git_blob_plain
($mimetype);
4681 # we can have blame only for text/* mimetype
4682 $have_blame &&= ($mimetype =~ m!^text/!);
4684 git_header_html
(undef, $expires);
4685 my $formats_nav = '';
4686 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4687 if (defined $file_name) {
4690 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1,
4691 -class => "blamelink")},
4696 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4699 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4702 $cgi->a({-href
=> href
(action
=>"blob",
4703 hash_base
=>"HEAD", file_name
=>$file_name)},
4707 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4710 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4711 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4713 print "<div class=\"page_nav\">\n" .
4714 "<br/><br/></div>\n" .
4715 "<div class=\"title\">$hash</div>\n";
4717 git_print_page_path
($file_name, "blob", $hash_base);
4718 print "<div class=\"page_body\">\n";
4719 if ($mimetype =~ m!^image/!) {
4720 print qq!<img type
="$mimetype"!;
4722 print qq! alt
="$file_name" title
="$file_name"!;
4725 href(action=>"blob_plain
", hash=>$hash,
4726 hash_base=>$hash_base, file_name=>$file_name) .
4730 while (my $line = <$fd>) {
4733 $line = untabify
($line);
4734 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4735 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4739 or print "Reading blob failed.\n";
4745 if (!defined $hash_base) {
4746 $hash_base = "HEAD";
4748 if (!defined $hash) {
4749 if (defined $file_name) {
4750 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4756 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4757 or die_error
(500, "Open git-ls-tree failed");
4758 my @entries = map { chomp; $_ } <$fd>;
4759 close $fd or die_error
(404, "Reading tree failed");
4762 my $refs = git_get_references
();
4763 my $ref = format_ref_marker
($refs, $hash_base);
4766 my ($have_blame) = gitweb_check_feature
('blame');
4767 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4769 if (defined $file_name) {
4771 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4773 $cgi->a({-href
=> href
(action
=>"tree",
4774 hash_base
=>"HEAD", file_name
=>$file_name)},
4777 my $snapshot_links = format_snapshot_links
($hash);
4778 if (defined $snapshot_links) {
4779 # FIXME: Should be available when we have no hash base as well.
4780 push @views_nav, $snapshot_links;
4782 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4783 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4786 print "<div class=\"page_nav\">\n";
4787 print "<br/><br/></div>\n";
4788 print "<div class=\"title\">$hash</div>\n";
4790 if (defined $file_name) {
4791 $basedir = $file_name;
4792 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4796 git_print_page_path
($file_name, 'tree', $hash_base);
4797 print "<div class=\"page_body\">\n";
4798 print "<table class=\"tree\">\n";
4800 # '..' (top directory) link if possible
4801 if (defined $hash_base &&
4802 defined $file_name && $file_name =~ m![^/]+$!) {
4804 print "<tr class=\"dark\">\n";
4806 print "<tr class=\"light\">\n";
4810 my $up = $file_name;
4811 $up =~ s!/?[^/]+$!!;
4812 undef $up unless $up;
4813 # based on git_print_tree_entry
4814 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4815 print '<td class="list">';
4816 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4820 print "<td class=\"link\"></td>\n";
4824 foreach my $line (@entries) {
4825 my %t = parse_ls_tree_line
($line, -z
=> 1);
4828 print "<tr class=\"dark\">\n";
4830 print "<tr class=\"light\">\n";
4834 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4838 print "</table>\n" .
4844 my @supported_fmts = gitweb_check_feature
('snapshot');
4845 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4847 my $format = $cgi->param('sf');
4848 if (!@supported_fmts) {
4849 die_error
(403, "Snapshots not allowed");
4851 # default to first supported snapshot format
4852 $format ||= $supported_fmts[0];
4853 if ($format !~ m/^[a-z0-9]+$/) {
4854 die_error
(400, "Invalid snapshot format parameter");
4855 } elsif (!exists($known_snapshot_formats{$format})) {
4856 die_error
(400, "Unknown snapshot format");
4857 } elsif (!grep($_ eq $format, @supported_fmts)) {
4858 die_error
(403, "Unsupported snapshot format");
4861 if (!defined $hash) {
4862 $hash = git_get_head_hash
($project);
4865 my $name = $project;
4866 $name =~ s
,([^/])/*\
.git
$,$1,;
4867 $name = basename
($name);
4868 my $filename = to_utf8
($name);
4869 $name =~ s/\047/\047\\\047\047/g;
4871 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4872 $cmd = quote_command
(
4873 git_cmd
(), 'archive',
4874 "--format=$known_snapshot_formats{$format}{'format'}",
4875 "--prefix=$name/", $hash);
4876 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4877 $cmd .= ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}});
4881 -type
=> $known_snapshot_formats{$format}{'type'},
4882 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4883 -status
=> '200 OK');
4885 open my $fd, "-|", $cmd
4886 or die_error
(500, "Execute git-archive failed");
4887 binmode STDOUT
, ':raw';
4889 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4894 my $head = git_get_head_hash
($project);
4895 if (!defined $hash) {
4898 if (!defined $page) {
4901 my $refs = git_get_references
();
4903 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4905 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4908 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4911 my %co = parse_commit
($hash);
4913 git_print_header_div
('summary', $project);
4914 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4916 my $to = ($#commitlist >= 99) ?
(99) : ($#commitlist);
4917 for (my $i = 0; $i <= $to; $i++) {
4918 my %co = %{$commitlist[$i]};
4920 my $commit = $co{'id'};
4921 my $ref = format_ref_marker
($refs, $commit);
4922 my %ad = parse_date
($co{'author_epoch'});
4923 git_print_header_div
('commit',
4924 "<span class=\"age\">$co{'age_string'}</span>" .
4925 esc_html
($co{'title'}) . $ref,
4927 print "<div class=\"title_text\">\n" .
4928 "<div class=\"log_link\">\n" .
4929 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4931 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4933 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4936 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4939 print "<div class=\"log_body\">\n";
4940 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4943 if ($#commitlist >= 100) {
4944 print "<div class=\"page_nav\">\n";
4945 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4946 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4953 $hash ||= $hash_base || "HEAD";
4954 my %co = parse_commit
($hash)
4955 or die_error
(404, "Unknown commit object");
4956 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4957 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4959 my $parent = $co{'parent'};
4960 my $parents = $co{'parents'}; # listref
4962 # we need to prepare $formats_nav before any parameter munging
4964 if (!defined $parent) {
4966 $formats_nav .= '(initial)';
4967 } elsif (@
$parents == 1) {
4968 # single parent commit
4971 $cgi->a({-href
=> href
(action
=>"commit",
4973 esc_html
(substr($parent, 0, 7))) .
4980 $cgi->a({-href
=> href
(action
=>"commit",
4982 esc_html
(substr($_, 0, 7)));
4987 if (!defined $parent) {
4991 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4993 (@
$parents <= 1 ?
$parent : '-c'),
4995 or die_error
(500, "Open git-diff-tree failed");
4996 @difftree = map { chomp; $_ } <$fd>;
4997 close $fd or die_error
(404, "Reading git-diff-tree failed");
4999 # non-textual hash id's can be cached
5001 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5004 my $refs = git_get_references
();
5005 my $ref = format_ref_marker
($refs, $co{'id'});
5007 git_header_html
(undef, $expires);
5008 git_print_page_nav
('commit', '',
5009 $hash, $co{'tree'}, $hash,
5012 if (defined $co{'parent'}) {
5013 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
5015 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
5017 print "<div class=\"title_text\">\n" .
5018 "<table class=\"object_header\">\n";
5019 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
5021 "<td></td><td> $ad{'rfc2822'}";
5022 if ($ad{'hour_local'} < 6) {
5023 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5024 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5026 printf(" (%02d:%02d %s)",
5027 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5031 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
5032 print "<tr><td></td><td> $cd{'rfc2822'}" .
5033 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5035 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5038 "<td class=\"sha1\">" .
5039 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
5040 class => "list"}, $co{'tree'}) .
5042 "<td class=\"link\">" .
5043 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
5045 my $snapshot_links = format_snapshot_links
($hash);
5046 if (defined $snapshot_links) {
5047 print " | " . $snapshot_links;
5052 foreach my $par (@
$parents) {
5055 "<td class=\"sha1\">" .
5056 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
5057 class => "list"}, $par) .
5059 "<td class=\"link\">" .
5060 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
5062 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
5069 print "<div class=\"page_body\">\n";
5070 git_print_log
($co{'comment'});
5073 git_difftree_body
(\
@difftree, undef, undef, $hash, @
$parents);
5079 # object is defined by:
5080 # - hash or hash_base alone
5081 # - hash_base and file_name
5084 # - hash or hash_base alone
5085 if ($hash || ($hash_base && !defined $file_name)) {
5086 my $object_id = $hash || $hash_base;
5088 open my $fd, "-|", quote_command
(
5089 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5090 or die_error
(404, "Object does not exist");
5094 or die_error
(404, "Object does not exist");
5096 # - hash_base and file_name
5097 } elsif ($hash_base && defined $file_name) {
5098 $file_name =~ s
,/+$,,;
5100 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
5101 or die_error
(404, "Base object does not exist");
5103 # here errors should not hapen
5104 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
5105 or die_error
(500, "Open git-ls-tree failed");
5109 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5110 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5111 die_error
(404, "File or directory for given base does not exist");
5116 die_error
(400, "Not enough information to find object");
5119 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
5120 hash
=>$hash, hash_base
=>$hash_base,
5121 file_name
=>$file_name),
5122 -status
=> '302 Found');
5126 my $format = shift || 'html';
5131 my $expires = '+1d';
5134 $file_parent ||= $file_name;
5136 # non-textual hash id's can be cached
5137 if (defined $hash && $hash !~ m/^[0-9a-fA-F]{40}$/) {
5139 } elsif (defined $hash_parent && $hash_parent !~ m/^[0-9a-fA-F]{40}$/) {
5141 } elsif (defined $hash_base && $hash_base !~ m/^[0-9a-fA-F]{40}$/) {
5143 } elsif (defined $hash_parent_base && $hash_parent_base !~ m/^[0-9a-fA-F]{40}$/) {
5147 # if hash parameter is missing, read it from the commit.
5148 if (defined $hash_base && defined $file_name && !defined $hash) {
5149 $hash = git_get_hash_by_path
($hash_base, $file_name);
5152 if (defined $hash_parent_base && defined $file_parent && !defined $hash_parent) {
5153 $hash_parent = git_get_hash_by_path
($hash_parent_base, $file_parent);
5156 if (!defined $hash || ! defined $hash_parent) {
5157 die_error
(404, "Missing one of the blob diff parameters");
5160 if (defined $hash_base && defined $file_name) {
5161 $to = $hash_base . ':' . $file_name;
5166 if (defined $hash_parent_base && defined $file_parent) {
5167 $from = $hash_parent_base . ':' . $file_parent;
5169 $from = $hash_parent;
5172 # fake git-diff-tree raw output
5173 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5174 $diffinfo{'from_id'} = $hash_parent;
5175 $diffinfo{'to_id'} = $hash;
5176 if (defined $file_name) {
5177 $diffinfo{'status'} = '2';
5178 $diffinfo{'from_file'} = $file_parent;
5179 $diffinfo{'to_file'} = $file_name;
5180 } else { # no filename given
5181 $diffinfo{'status'} = '2';
5182 $diffinfo{'from_file'} = $hash_parent;
5183 $diffinfo{'to_file'} = $hash;
5187 open $fd, "-|", git_cmd
(), "diff", @diff_opts, '-p', "--full-index",
5188 ($format eq 'html' ?
"--raw" : ()), $from, $to, "--"
5189 or die_error
(500, "Open git-diff failed");
5192 if ($format eq 'html') {
5194 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5196 git_header_html
(undef, $expires);
5197 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5198 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5199 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5201 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5202 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5204 if (defined $file_name) {
5205 git_print_page_path
($file_name, "blob", $hash_base);
5207 print "<div class=\"page_path\"></div>\n";
5210 } elsif ($format eq 'plain') {
5211 my $patch_file_name = $file_name || $hash;
5213 -type
=> 'text/plain',
5214 -charset
=> 'utf-8',
5215 -expires
=> $expires,
5216 -content_disposition
=> 'inline; filename="' . "$patch_file_name" . '.patch"');
5218 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5221 die_error
(400, "Unknown blobdiff format");
5225 if ($format eq 'html') {
5226 print "<div class=\"page_body\">\n";
5228 git_patchset_body
($fd, [ \
%diffinfo ], undef, undef, $hash_base, $hash_parent_base);
5231 print "</div>\n"; # class="page_body"
5235 while (my $line = <$fd>) {
5236 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5237 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5241 last if $line =~ m!^\+\+\+!;
5249 sub git_blobdiff_plain
{
5250 git_blobdiff
('plain');
5253 sub git_commitdiff
{
5254 my $format = shift || 'html';
5255 $hash ||= $hash_base || "HEAD";
5256 my %co = parse_commit
($hash)
5257 or die_error
(404, "Unknown commit object");
5259 # choose format for commitdiff for merge
5260 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
5261 $hash_parent = '--cc';
5263 # we need to prepare $formats_nav before almost any parameter munging
5265 if ($format eq 'html') {
5267 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5270 if (defined $hash_parent &&
5271 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5272 # commitdiff with two commits given
5273 my $hash_parent_short = $hash_parent;
5274 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5275 $hash_parent_short = substr($hash_parent, 0, 7);
5279 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
5280 if ($co{'parents'}[$i] eq $hash_parent) {
5281 $formats_nav .= ' parent ' . ($i+1);
5285 $formats_nav .= ': ' .
5286 $cgi->a({-href
=> href
(action
=>"commitdiff",
5287 hash
=>$hash_parent)},
5288 esc_html
($hash_parent_short)) .
5290 } elsif (!$co{'parent'}) {
5292 $formats_nav .= ' (initial)';
5293 } elsif (scalar @
{$co{'parents'}} == 1) {
5294 # single parent commit
5297 $cgi->a({-href
=> href
(action
=>"commitdiff",
5298 hash
=>$co{'parent'})},
5299 esc_html
(substr($co{'parent'}, 0, 7))) .
5303 if ($hash_parent eq '--cc') {
5304 $formats_nav .= ' | ' .
5305 $cgi->a({-href
=> href
(action
=>"commitdiff",
5306 hash
=>$hash, hash_parent
=>'-c')},
5308 } else { # $hash_parent eq '-c'
5309 $formats_nav .= ' | ' .
5310 $cgi->a({-href
=> href
(action
=>"commitdiff",
5311 hash
=>$hash, hash_parent
=>'--cc')},
5317 $cgi->a({-href
=> href
(action
=>"commitdiff",
5319 esc_html
(substr($_, 0, 7)));
5320 } @
{$co{'parents'}} ) .
5325 my $hash_parent_param = $hash_parent;
5326 if (!defined $hash_parent_param) {
5327 # --cc for multiple parents, --root for parentless
5328 $hash_parent_param =
5329 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
5335 if ($format eq 'html') {
5336 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5337 "--no-commit-id", "--patch-with-raw", "--full-index",
5338 $hash_parent_param, $hash, "--"
5339 or die_error
(500, "Open git-diff-tree failed");
5341 while (my $line = <$fd>) {
5343 # empty line ends raw part of diff-tree output
5345 push @difftree, scalar parse_difftree_raw_line
($line);
5348 } elsif ($format eq 'plain') {
5349 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5350 '-p', $hash_parent_param, $hash, "--"
5351 or die_error
(500, "Open git-diff-tree failed");
5354 die_error
(400, "Unknown commitdiff format");
5357 # non-textual hash id's can be cached
5359 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5363 # write commit message
5364 if ($format eq 'html') {
5365 my $refs = git_get_references
();
5366 my $ref = format_ref_marker
($refs, $co{'id'});
5368 git_header_html
(undef, $expires);
5369 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5370 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5371 git_print_authorship
(\
%co);
5372 print "<div class=\"page_body\">\n";
5373 if (@
{$co{'comment'}} > 1) {
5374 print "<div class=\"log\">\n";
5375 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5376 print "</div>\n"; # class="log"
5379 } elsif ($format eq 'plain') {
5380 my $refs = git_get_references
("tags");
5381 my $tagname = git_get_rev_name_tags
($hash);
5382 my $filename = basename
($project) . "-$hash.patch";
5385 -type
=> 'text/plain',
5386 -charset
=> 'utf-8',
5387 -expires
=> $expires,
5388 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5389 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5390 print "From: " . to_utf8
($co{'author'}) . "\n";
5391 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5392 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5394 print "X-Git-Tag: $tagname\n" if $tagname;
5395 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5397 foreach my $line (@
{$co{'comment'}}) {
5398 print to_utf8
($line) . "\n";
5404 if ($format eq 'html') {
5405 my $use_parents = !defined $hash_parent ||
5406 $hash_parent eq '-c' || $hash_parent eq '--cc';
5407 git_difftree_body
(\
@difftree, undef, undef, $hash,
5408 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5411 git_patchset_body
($fd, \
@difftree, undef, undef, $hash,
5412 $use_parents ? @
{$co{'parents'}} : $hash_parent);
5414 print "</div>\n"; # class="page_body"
5417 } elsif ($format eq 'plain') {
5421 or print "Reading git-diff-tree failed\n";
5425 sub git_commitdiff_plain
{
5426 git_commitdiff
('plain');
5430 if (!defined $hash_base) {
5431 $hash_base = git_get_head_hash
($project);
5433 if (!defined $page) {
5437 my %co = parse_commit
($hash_base)
5438 or die_error
(404, "Unknown commit object");
5440 my $refs = git_get_references
();
5441 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5443 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5444 $file_name, "--full-history")
5445 or die_error
(404, "No such file or directory on given branch");
5447 if (!defined $hash && defined $file_name) {
5448 # some commits could have deleted file in question,
5449 # and not have it in tree, but one of them has to have it
5450 for (my $i = 0; $i <= @commitlist; $i++) {
5451 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5452 last if defined $hash;
5455 if (defined $hash) {
5456 $ftype = git_get_type
($hash);
5458 if (!defined $ftype) {
5459 die_error
(500, "Unknown type of object");
5462 my $paging_nav = '';
5465 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5466 file_name
=>$file_name)},
5468 $paging_nav .= " ⋅ " .
5469 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5470 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5472 $paging_nav .= "first";
5473 $paging_nav .= " ⋅ prev";
5476 if ($#commitlist >= 100) {
5478 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5479 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5480 $paging_nav .= " ⋅ $next_link";
5482 $paging_nav .= " ⋅ next";
5486 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5487 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5488 git_print_page_path
($file_name, $ftype, $hash_base);
5490 git_history_body
(\
@commitlist, 0, 99,
5491 $refs, $hash_base, $ftype, $next_link);
5497 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5498 if (!defined $searchtext) {
5499 die_error
(400, "Text field is empty");
5501 if (!defined $hash) {
5502 $hash = git_get_head_hash
($project);
5504 my %co = parse_commit
($hash);
5506 die_error
(404, "Unknown commit object");
5508 if (!defined $page) {
5512 $searchtype ||= 'commit';
5513 if ($searchtype eq 'pickaxe') {
5514 # pickaxe may take all resources of your box and run for several minutes
5515 # with every query - so decide by yourself how public you make this feature
5516 gitweb_check_feature
('pickaxe')
5517 or die_error
(403, "Pickaxe is disabled");
5519 if ($searchtype eq 'grep') {
5520 gitweb_check_feature
('grep')
5521 or die_error
(403, "Grep is disabled");
5526 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5528 if ($searchtype eq 'commit') {
5529 $greptype = "--grep=";
5530 } elsif ($searchtype eq 'author') {
5531 $greptype = "--author=";
5532 } elsif ($searchtype eq 'committer') {
5533 $greptype = "--committer=";
5535 $greptype .= $searchtext;
5536 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5537 $greptype, '--regexp-ignore-case',
5538 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
5540 my $paging_nav = '';
5543 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5544 searchtext
=>$searchtext,
5545 searchtype
=>$searchtype)},
5547 $paging_nav .= " ⋅ " .
5548 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5549 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5551 $paging_nav .= "first";
5552 $paging_nav .= " ⋅ prev";
5555 if ($#commitlist >= 100) {
5557 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5558 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5559 $paging_nav .= " ⋅ $next_link";
5561 $paging_nav .= " ⋅ next";
5564 if ($#commitlist >= 100) {
5567 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5568 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5569 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5572 if ($searchtype eq 'pickaxe') {
5573 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5574 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5576 print "<table class=\"pickaxe search\">\n";
5579 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5580 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5581 ($search_use_regexp ?
'--pickaxe-regex' : ());
5584 while (my $line = <$fd>) {
5588 my %set = parse_difftree_raw_line
($line);
5589 if (defined $set{'commit'}) {
5590 # finish previous commit
5593 "<td class=\"link\">" .
5594 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5596 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5602 print "<tr class=\"dark\">\n";
5604 print "<tr class=\"light\">\n";
5607 %co = parse_commit
($set{'commit'});
5608 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5609 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5610 "<td><i>$author</i></td>\n" .
5612 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5613 -class => "list subject"},
5614 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5615 } elsif (defined $set{'to_id'}) {
5616 next if ($set{'to_id'} =~ m/^0{40}$/);
5618 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5619 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5621 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5627 # finish last commit (warning: repetition!)
5630 "<td class=\"link\">" .
5631 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5633 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5641 if ($searchtype eq 'grep') {
5642 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5643 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5645 print "<table class=\"grep_search\">\n";
5649 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5650 $search_use_regexp ?
('-E', '-i') : '-F',
5651 $searchtext, $co{'tree'};
5653 while (my $line = <$fd>) {
5655 my ($file, $lno, $ltext, $binary);
5656 last if ($matches++ > 1000);
5657 if ($line =~ /^Binary file (.+) matches$/) {
5661 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5663 if ($file ne $lastfile) {
5664 $lastfile and print "</td></tr>\n";
5666 print "<tr class=\"dark\">\n";
5668 print "<tr class=\"light\">\n";
5670 print "<td class=\"list\">".
5671 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5672 file_name
=>"$file"),
5673 -class => "list"}, esc_path
($file));
5674 print "</td><td>\n";
5678 print "<div class=\"binary\">Binary file</div>\n";
5680 $ltext = untabify
($ltext);
5681 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5682 $ltext = esc_html
($1, -nbsp
=>1);
5683 $ltext .= '<span class="match">';
5684 $ltext .= esc_html
($2, -nbsp
=>1);
5685 $ltext .= '</span>';
5686 $ltext .= esc_html
($3, -nbsp
=>1);
5688 $ltext = esc_html
($ltext, -nbsp
=>1);
5690 print "<div class=\"pre\">" .
5691 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5692 file_name
=>"$file").'#l'.$lno,
5693 -class => "linenr"}, sprintf('%4i', $lno))
5694 . ' ' . $ltext . "</div>\n";
5698 print "</td></tr>\n";
5699 if ($matches > 1000) {
5700 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5703 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5712 sub git_search_help
{
5714 git_print_page_nav
('','', $hash,$hash,$hash);
5716 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5717 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5718 the pattern entered is recognized as the POSIX extended
5719 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5722 <dt><b>commit</b></dt>
5723 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5725 my ($have_grep) = gitweb_check_feature
('grep');
5728 <dt><b>grep</b></dt>
5729 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5730 a different one) are searched for the given pattern. On large trees, this search can take
5731 a while and put some strain on the server, so please use it with some consideration. Note that
5732 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5733 case-sensitive.</dd>
5737 <dt><b>author</b></dt>
5738 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5739 <dt><b>committer</b></dt>
5740 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5742 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5743 if ($have_pickaxe) {
5745 <dt><b>pickaxe</b></dt>
5746 <dd>All commits that caused the string to appear or disappear from any file (changes that
5747 added, removed or "modified" the string) will be listed. This search can take a while and
5748 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5749 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5757 my $head = git_get_head_hash
($project);
5758 if (!defined $hash) {
5761 if (!defined $page) {
5764 my $refs = git_get_references
();
5766 my @commitlist = parse_commits
($hash, 101, (100 * $page));
5768 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5770 if ($#commitlist >= 100) {
5772 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5773 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5777 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5778 git_print_header_div
('summary', $project);
5780 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5785 ## ......................................................................
5786 ## feeds (RSS, Atom; OPML)
5789 my $format = shift || 'atom';
5790 my ($have_blame) = gitweb_check_feature
('blame');
5792 # Atom: http://www.atomenabled.org/developers/syndication/
5793 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5794 if ($format ne 'rss' && $format ne 'atom') {
5795 die_error
(400, "Unknown web feed format");
5798 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5799 my $head = $hash || 'HEAD';
5800 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5804 my $content_type = "application/$format+xml";
5805 if (defined $cgi->http('HTTP_ACCEPT') &&
5806 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5807 # browser (feed reader) prefers text/xml
5808 $content_type = 'text/xml';
5810 if (defined($commitlist[0])) {
5811 %latest_commit = %{$commitlist[0]};
5812 %latest_date = parse_date
($latest_commit{'author_epoch'});
5814 -type
=> $content_type,
5815 -charset
=> 'utf-8',
5816 -last_modified
=> $latest_date{'rfc2822'});
5819 -type
=> $content_type,
5820 -charset
=> 'utf-8');
5823 # Optimization: skip generating the body if client asks only
5824 # for Last-Modified date.
5825 return if ($cgi->request_method() eq 'HEAD');
5828 my $title = "$site_name - $project/$action";
5829 my $feed_type = 'log';
5830 if (defined $hash) {
5831 $title .= " - '$hash'";
5832 $feed_type = 'branch log';
5833 if (defined $file_name) {
5834 $title .= " :: $file_name";
5835 $feed_type = 'history';
5837 } elsif (defined $file_name) {
5838 $title .= " - $file_name";
5839 $feed_type = 'history';
5841 $title .= " $feed_type";
5842 my $descr = git_get_project_description
($project);
5843 if (defined $descr) {
5844 $descr = esc_html
($descr);
5846 $descr = "$project " .
5847 ($format eq 'rss' ?
'RSS' : 'Atom') .
5850 my $owner = git_get_project_owner
($project);
5851 $owner = esc_html
($owner);
5855 if (defined $file_name) {
5856 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5857 } elsif (defined $hash) {
5858 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5860 $alt_url = href
(-full
=>1, action
=>"summary");
5862 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
5863 if ($format eq 'rss') {
5865 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5868 print "<title>$title</title>\n" .
5869 "<link>$alt_url</link>\n" .
5870 "<description>$descr</description>\n" .
5871 "<language>en</language>\n";
5872 } elsif ($format eq 'atom') {
5874 <feed xmlns="http://www.w3.org/2005/Atom">
5876 print "<title>$title</title>\n" .
5877 "<subtitle>$descr</subtitle>\n" .
5878 '<link rel="alternate" type="text/html" href="' .
5879 $alt_url . '" />' . "\n" .
5880 '<link rel="self" type="' . $content_type . '" href="' .
5881 $cgi->self_url() . '" />' . "\n" .
5882 "<id>" . href
(-full
=>1) . "</id>\n" .
5883 # use project owner for feed author
5884 "<author><name>$owner</name></author>\n";
5885 if (defined $favicon) {
5886 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5888 if (defined $logo_url) {
5889 # not twice as wide as tall: 72 x 27 pixels
5890 print "<logo>" . esc_url
($logo) . "</logo>\n";
5892 if (! %latest_date) {
5893 # dummy date to keep the feed valid until commits trickle in:
5894 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5896 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5901 for (my $i = 0; $i <= $#commitlist; $i++) {
5902 my %co = %{$commitlist[$i]};
5903 my $commit = $co{'id'};
5904 # we read 150, we always show 30 and the ones more recent than 48 hours
5905 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5908 my %cd = parse_date
($co{'author_epoch'});
5910 # get list of changed files
5911 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5912 $co{'parent'} || "--root",
5913 $co{'id'}, "--", (defined $file_name ?
$file_name : ())
5915 my @difftree = map { chomp; $_ } <$fd>;
5919 # print element (entry, item)
5920 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5921 if ($format eq 'rss') {
5923 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5924 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5925 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5926 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5927 "<link>$co_url</link>\n" .
5928 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5929 "<content:encoded>" .
5931 } elsif ($format eq 'atom') {
5933 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5934 "<updated>$cd{'iso-8601'}</updated>\n" .
5936 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5937 if ($co{'author_email'}) {
5938 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5940 print "</author>\n" .
5941 # use committer for contributor
5943 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5944 if ($co{'committer_email'}) {
5945 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5947 print "</contributor>\n" .
5948 "<published>$cd{'iso-8601'}</published>\n" .
5949 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5950 "<id>$co_url</id>\n" .
5951 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5952 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5954 my $comment = $co{'comment'};
5956 foreach my $line (@
$comment) {
5957 $line = esc_html
($line);
5960 print "</pre><ul>\n";
5961 foreach my $difftree_line (@difftree) {
5962 my %difftree = parse_difftree_raw_line
($difftree_line);
5963 next if !$difftree{'from_id'};
5965 my $file = $difftree{'file'} || $difftree{'to_file'};
5969 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5970 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5971 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5972 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5973 -title
=> "diff"}, 'D');
5975 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5976 file_name
=>$file, hash_base
=>$commit), -class => "blamelink",
5977 -title
=> "blame"}, 'B');
5979 # if this is not a feed of a file history
5980 if (!defined $file_name || $file_name ne $file) {
5981 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5982 file_name
=>$file, hash
=>$commit),
5983 -title
=> "history"}, 'H');
5985 $file = esc_path
($file);
5989 if ($format eq 'rss') {
5990 print "</ul>]]>\n" .
5991 "</content:encoded>\n" .
5993 } elsif ($format eq 'atom') {
5994 print "</ul>\n</div>\n" .
6001 if ($format eq 'rss') {
6002 print "</channel>\n</rss>\n";
6003 } elsif ($format eq 'atom') {
6017 my @list = git_get_projects_list
();
6019 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
6021 <?xml version="1.0" encoding="utf-8"?>
6022 <opml version="1.0">
6024 <title>$site_name OPML Export</title>
6027 <outline text="git RSS feeds">
6030 foreach my $pr (@list) {
6032 my $head = git_get_head_hash
($proj{'path'});
6033 if (!defined $head) {
6036 $git_dir = "$projectroot/$proj{'path'}";
6037 my %co = parse_commit
($head);
6042 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
6043 my $rss = "$my_url?p=$proj{'path'};a=rss";
6044 my $html = "$my_url?p=$proj{'path'};a=summary";
6045 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";