3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
12 use CGI
qw(:standard :escapeHTML -nosticky);
13 use CGI
::Util
qw(unescape);
14 use CGI
::Carp
qw(fatalsToBrowser);
18 use File
::Basename
qw(basename);
19 binmode STDOUT
, ':utf8';
22 CGI-
>compile() if $ENV{'MOD_PERL'};
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute
=> 1);
30 # if we're called with PATH_INFO, we have to strip that
31 # from the URL to find our real URL
32 # we make $path_info global because it's also used later on
33 my $path_info = $ENV{"PATH_INFO"};
35 $my_url =~ s
,\Q
$path_info\E
$,,;
36 $my_uri =~ s
,\Q
$path_info\E
$,,;
39 # core git executable to use
40 # this can just be "git" if your webserver has a sensible PATH
41 our $GIT = "++GIT_BINDIR++/git";
43 # absolute fs-path which will be prepended to the project path
44 #our $projectroot = "/pub/scm";
45 our $projectroot = "++GITWEB_PROJECTROOT++";
47 # fs traversing limit for getting project list
48 # the number is relative to the projectroot
49 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
51 # target of the home link on top of all pages
52 our $home_link = $my_uri || "/";
54 # string of the home link on top of all pages
55 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
57 # name of your site or organization to appear in page titles
58 # replace this with something more descriptive for clearer bookmarks
59 our $site_name = "++GITWEB_SITENAME++"
60 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
62 # filename of html text to include at top of each page
63 our $site_header = "++GITWEB_SITE_HEADER++";
64 # html text to include at home page
65 our $home_text = "++GITWEB_HOMETEXT++";
66 # filename of html text to include at bottom of each page
67 our $site_footer = "++GITWEB_SITE_FOOTER++";
70 our @stylesheets = ("++GITWEB_CSS++");
71 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
72 our $stylesheet = undef;
74 # URI of GIT logo (72x27 size)
75 our $logo = "++GITWEB_LOGO++";
76 # URI of GIT favicon, assumed to be image/png type
77 our $favicon = "++GITWEB_FAVICON++";
79 # URI and label (title) of GIT logo link
80 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
81 #our $logo_label = "git documentation";
82 our $logo_url = "http://git.or.cz/";
83 our $logo_label = "git homepage";
85 # source of projects list
86 our $projects_list = "++GITWEB_LIST++";
88 # the width (in characters) of the projects list "Description" column
89 our $projects_list_description_width = 25;
91 # default order of projects list
92 # valid values are none, project, descr, owner, and age
93 our $default_projects_order = "project";
95 # show repository only if this file exists
96 # (only effective if this variable evaluates to true)
97 our $export_ok = "++GITWEB_EXPORT_OK++";
99 # only allow viewing of repositories also shown on the overview page
100 our $strict_export = "++GITWEB_STRICT_EXPORT++";
102 # list of git base URLs used for URL to where fetch project from,
103 # i.e. full URL is "$git_base_url/$project"
104 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
106 # default blob_plain mimetype and default charset for text/plain blob
107 our $default_blob_plain_mimetype = 'text/plain';
108 our $default_text_plain_charset = undef;
110 # file to use for guessing MIME types before trying /etc/mime.types
111 # (relative to the current git repository)
112 our $mimetypes_file = undef;
114 # assume this charset if line contains non-UTF-8 characters;
115 # it should be valid encoding (see Encoding::Supported(3pm) for list),
116 # for which encoding all byte sequences are valid, for example
117 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
118 # could be even 'utf-8' for the old behavior)
119 our $fallback_encoding = 'latin1';
121 # rename detection options for git-diff and git-diff-tree
122 # - default is '-M', with the cost proportional to
123 # (number of removed files) * (number of new files).
124 # - more costly is '-C' (which implies '-M'), with the cost proportional to
125 # (number of changed files + number of removed files) * (number of new files)
126 # - even more costly is '-C', '--find-copies-harder' with cost
127 # (number of files in the original tree) * (number of new files)
128 # - one might want to include '-B' option, e.g. '-B', '-M'
129 our @diff_opts = ('-M'); # taken from git_commit
131 # information about snapshot formats that gitweb is capable of serving
132 our %known_snapshot_formats = (
134 # 'display' => display name,
135 # 'type' => mime type,
136 # 'suffix' => filename suffix,
137 # 'format' => --format for git-archive,
138 # 'compressor' => [compressor command and arguments]
139 # (array reference, optional)}
142 'display' => 'tar.gz',
143 'type' => 'application/x-gzip',
144 'suffix' => '.tar.gz',
146 'compressor' => ['gzip']},
149 'display' => 'tar.bz2',
150 'type' => 'application/x-bzip2',
151 'suffix' => '.tar.bz2',
153 'compressor' => ['bzip2']},
157 'type' => 'application/x-zip',
162 # Aliases so we understand old gitweb.snapshot values in repository
164 our %known_snapshot_format_aliases = (
168 # backward compatibility: legacy gitweb config support
169 'x-gzip' => undef, 'gz' => undef,
170 'x-bzip2' => undef, 'bz2' => undef,
171 'x-zip' => undef, '' => undef,
174 # You define site-wide feature defaults here; override them with
175 # $GITWEB_CONFIG as necessary.
178 # 'sub' => feature-sub (subroutine),
179 # 'override' => allow-override (boolean),
180 # 'default' => [ default options...] (array reference)}
182 # if feature is overridable (it means that allow-override has true value),
183 # then feature-sub will be called with default options as parameters;
184 # return value of feature-sub indicates if to enable specified feature
186 # if there is no 'sub' key (no feature-sub), then feature cannot be
189 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
191 # Enable the 'blame' blob view, showing the last commit that modified
192 # each line in the file. This can be very CPU-intensive.
194 # To enable system wide have in $GITWEB_CONFIG
195 # $feature{'blame'}{'default'} = [1];
196 # To have project specific config enable override in $GITWEB_CONFIG
197 # $feature{'blame'}{'override'} = 1;
198 # and in project config gitweb.blame = 0|1;
200 'sub' => \
&feature_blame
,
204 # Enable the 'snapshot' link, providing a compressed archive of any
205 # tree. This can potentially generate high traffic if you have large
208 # Value is a list of formats defined in %known_snapshot_formats that
210 # To disable system wide have in $GITWEB_CONFIG
211 # $feature{'snapshot'}{'default'} = [];
212 # To have project specific config enable override in $GITWEB_CONFIG
213 # $feature{'snapshot'}{'override'} = 1;
214 # and in project config, a comma-separated list of formats or "none"
215 # to disable. Example: gitweb.snapshot = tbz2,zip;
217 'sub' => \
&feature_snapshot
,
219 'default' => ['tgz']},
221 # Enable text search, which will list the commits which match author,
222 # committer or commit text to a given string. Enabled by default.
223 # Project specific override is not supported.
228 # Enable grep search, which will list the files in currently selected
229 # tree containing the given string. Enabled by default. This can be
230 # potentially CPU-intensive, of course.
232 # To enable system wide have in $GITWEB_CONFIG
233 # $feature{'grep'}{'default'} = [1];
234 # To have project specific config enable override in $GITWEB_CONFIG
235 # $feature{'grep'}{'override'} = 1;
236 # and in project config gitweb.grep = 0|1;
241 # Enable the pickaxe search, which will list the commits that modified
242 # a given string in a file. This can be practical and quite faster
243 # alternative to 'blame', but still potentially CPU-intensive.
245 # To enable system wide have in $GITWEB_CONFIG
246 # $feature{'pickaxe'}{'default'} = [1];
247 # To have project specific config enable override in $GITWEB_CONFIG
248 # $feature{'pickaxe'}{'override'} = 1;
249 # and in project config gitweb.pickaxe = 0|1;
251 'sub' => \
&feature_pickaxe
,
255 # Make gitweb use an alternative format of the URLs which can be
256 # more readable and natural-looking: project name is embedded
257 # directly in the path and the query string contains other
258 # auxiliary information. All gitweb installations recognize
259 # URL in either format; this configures in which formats gitweb
262 # To enable system wide have in $GITWEB_CONFIG
263 # $feature{'pathinfo'}{'default'} = [1];
264 # Project specific override is not supported.
266 # Note that you will need to change the default location of CSS,
267 # favicon, logo and possibly other files to an absolute URL. Also,
268 # if gitweb.cgi serves as your indexfile, you will need to force
269 # $my_uri to contain the script name in your $GITWEB_CONFIG.
274 # Make gitweb consider projects in project root subdirectories
275 # to be forks of existing projects. Given project $projname.git,
276 # projects matching $projname/*.git will not be shown in the main
277 # projects list, instead a '+' mark will be added to $projname
278 # there and a 'forks' view will be enabled for the project, listing
279 # all the forks. If project list is taken from a file, forks have
280 # to be listed after the main project.
282 # To enable system wide have in $GITWEB_CONFIG
283 # $feature{'forks'}{'default'} = [1];
284 # Project specific override is not supported.
289 # Insert custom links to the action bar of all project pages.
290 # This enables you mainly to link to third-party scripts integrating
291 # into gitweb; e.g. git-browser for graphical history representation
292 # or custom web-based repository administration interface.
294 # The 'default' value consists of a list of triplets in the form
295 # (label, link, position) where position is the label after which
296 # to insert the link and link is a format string where %n expands
297 # to the project name, %f to the project path within the filesystem,
298 # %h to the current hash (h gitweb parameter) and %b to the current
299 # hash base (hb gitweb parameter); %% expands to %.
301 # To enable system wide have in $GITWEB_CONFIG e.g.
302 # $feature{'actions'}{'default'} = [('graphiclog',
303 # '/git-browser/by-commit.html?r=%n', 'summary')];
304 # Project specific override is not supported.
309 # Allow gitweb scan project content tags described in ctags/
310 # of project repository, and display the popular Web 2.0-ish
311 # "tag cloud" near the project list. Note that this is something
312 # COMPLETELY different from the normal Git tags.
314 # gitweb by itself can show existing tags, but it does not handle
315 # tagging itself; you need an external application for that.
316 # For an example script, check Girocco's cgi/tagproj.cgi.
317 # You may want to install the HTML::TagCloud Perl module to get
318 # a pretty tag cloud instead of just a list of tags.
320 # To enable system wide have in $GITWEB_CONFIG
321 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
322 # Project specific override is not supported.
328 sub gitweb_check_feature
{
330 return unless exists $feature{$name};
331 my ($sub, $override, @defaults) = (
332 $feature{$name}{'sub'},
333 $feature{$name}{'override'},
334 @{$feature{$name}{'default'}});
335 if (!$override) { return @defaults; }
337 warn "feature $name is not overrideable";
340 return $sub->(@defaults);
344 my ($val) = git_get_project_config
('blame', '--bool');
346 if ($val eq 'true') {
348 } elsif ($val eq 'false') {
355 sub feature_snapshot
{
358 my ($val) = git_get_project_config
('snapshot');
361 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
368 my ($val) = git_get_project_config
('grep', '--bool');
370 if ($val eq 'true') {
372 } elsif ($val eq 'false') {
379 sub feature_pickaxe
{
380 my ($val) = git_get_project_config
('pickaxe', '--bool');
382 if ($val eq 'true') {
384 } elsif ($val eq 'false') {
391 # checking HEAD file with -e is fragile if the repository was
392 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
394 sub check_head_link
{
396 my $headfile = "$dir/HEAD";
397 return ((-e
$headfile) ||
398 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
401 sub check_export_ok
{
403 return (check_head_link
($dir) &&
404 (!$export_ok || -e
"$dir/$export_ok"));
407 # process alternate names for backward compatibility
408 # filter out unsupported (unknown) snapshot formats
409 sub filter_snapshot_fmts
{
413 exists $known_snapshot_format_aliases{$_} ?
414 $known_snapshot_format_aliases{$_} : $_} @fmts;
415 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
419 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
420 if (-e
$GITWEB_CONFIG) {
423 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
424 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
427 # version of the core git binary
428 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
430 $projects_list ||= $projectroot;
432 # ======================================================================
433 # input validation and dispatch
435 # input parameters can be collected from a variety of sources (presently, CGI
436 # and PATH_INFO), so we define an %input_params hash that collects them all
437 # together during validation: this allows subsequent uses (e.g. href()) to be
438 # agnostic of the parameter origin
440 my %input_params = ();
442 # input parameters are stored with the long parameter name as key. This will
443 # also be used in the href subroutine to convert parameters to their CGI
444 # equivalent, and since the href() usage is the most frequent one, we store
445 # the name -> CGI key mapping here, instead of the reverse.
447 # XXX: Warning: If you touch this, check the search form for updating,
450 my @cgi_param_mapping = (
458 hash_parent_base
=> "hpb",
463 snapshot_format
=> "sf",
464 extra_options
=> "opt",
465 search_use_regexp
=> "sr",
467 my %cgi_param_mapping = @cgi_param_mapping;
469 # we will also need to know the possible actions, for validation
471 "blame" => \
&git_blame
,
472 "blobdiff" => \
&git_blobdiff
,
473 "blobdiff_plain" => \
&git_blobdiff_plain
,
474 "blob" => \
&git_blob
,
475 "blob_plain" => \
&git_blob_plain
,
476 "commitdiff" => \
&git_commitdiff
,
477 "commitdiff_plain" => \
&git_commitdiff_plain
,
478 "commit" => \
&git_commit
,
479 "forks" => \
&git_forks
,
480 "heads" => \
&git_heads
,
481 "history" => \
&git_history
,
484 "atom" => \
&git_atom
,
485 "search" => \
&git_search
,
486 "search_help" => \
&git_search_help
,
487 "shortlog" => \
&git_shortlog
,
488 "summary" => \
&git_summary
,
490 "tags" => \
&git_tags
,
491 "tree" => \
&git_tree
,
492 "snapshot" => \
&git_snapshot
,
493 "object" => \
&git_object
,
494 # those below don't need $project
495 "opml" => \
&git_opml
,
496 "project_list" => \
&git_project_list
,
497 "project_index" => \
&git_project_index
,
500 # finally, we have the hash of allowed extra_options for the commands that
502 my %allowed_options = (
503 "--no-merges" => [ qw(rss atom log shortlog history) ],
506 # fill %input_params with the CGI parameters. All values except for 'opt'
507 # should be single values, but opt can be an array. We should probably
508 # build an array of parameters that can be multi-valued, but since for the time
509 # being it's only this one, we just single it out
510 while (my ($name, $symbol) = each %cgi_param_mapping) {
511 if ($symbol eq 'opt') {
512 $input_params{$name} = [ $cgi->param($symbol) ];
514 $input_params{$name} = $cgi->param($symbol);
518 # now read PATH_INFO and update the parameter list for missing parameters
519 sub evaluate_path_info
{
520 return if defined $input_params{'project'};
521 return if !$path_info;
522 $path_info =~ s
,^/+,,;
523 return if !$path_info;
525 # find which part of PATH_INFO is project
526 my $project = $path_info;
528 while ($project && !check_head_link
("$projectroot/$project")) {
529 $project =~ s
,/*[^/]*$,,;
531 return unless $project;
532 $input_params{'project'} = $project;
534 # do not change any parameters if an action is given using the query string
535 return if $input_params{'action'};
536 $path_info =~ s
,^\Q
$project\E
/*,,;
538 # next, check if we have an action
539 my $action = $path_info;
541 if (exists $actions{$action}) {
542 $path_info =~ s
,^$action/*,,;
543 $input_params{'action'} = $action;
546 # list of actions that want hash_base instead of hash, but can have no
547 # pathname (f) parameter
553 my ($refname, $pathname) = split(/:/, $path_info, 2);
554 if (defined $pathname) {
555 # we got "branch:filename" or "branch:dir/"
556 # we could use git_get_type(branch:pathname), but:
557 # - it needs $git_dir
558 # - it does a git() call
559 # - the convention of terminating directories with a slash
560 # makes it superfluous
561 # - embedding the action in the PATH_INFO would make it even
563 $pathname =~ s
,^/+,,;
564 if (!$pathname || substr($pathname, -1) eq "/") {
565 $input_params{'action'} ||= "tree";
568 $input_params{'action'} ||= "blob_plain";
570 $input_params{'hash_base'} ||= $refname;
571 $input_params{'file_name'} ||= $pathname;
572 } elsif (defined $refname) {
573 # we got "branch". In this case we have to choose if we have to
574 # set hash or hash_base.
576 # Most of the actions without a pathname only want hash to be
577 # set, except for the ones specified in @wants_base that want
578 # hash_base instead. It should also be noted that hand-crafted
579 # links having 'history' as an action and no pathname or hash
580 # set will fail, but that happens regardless of PATH_INFO.
581 $input_params{'action'} ||= "shortlog";
582 if (grep { $_ eq $input_params{'action'} } @wants_base) {
583 $input_params{'hash_base'} ||= $refname;
585 $input_params{'hash'} ||= $refname;
589 evaluate_path_info
();
591 our $action = $input_params{'action'};
592 if (defined $action) {
593 if (!validate_action
($action)) {
594 die_error
(400, "Invalid action parameter");
598 # parameters which are pathnames
599 our $project = $input_params{'project'};
600 if (defined $project) {
601 if (!validate_project
($project)) {
603 die_error
(404, "No such project");
607 our $file_name = $input_params{'file_name'};
608 if (defined $file_name) {
609 if (!validate_pathname
($file_name)) {
610 die_error
(400, "Invalid file parameter");
614 our $file_parent = $input_params{'file_parent'};
615 if (defined $file_parent) {
616 if (!validate_pathname
($file_parent)) {
617 die_error
(400, "Invalid file parent parameter");
621 # parameters which are refnames
622 our $hash = $input_params{'hash'};
624 if (!validate_refname
($hash)) {
625 die_error
(400, "Invalid hash parameter");
629 our $hash_parent = $input_params{'hash_parent'};
630 if (defined $hash_parent) {
631 if (!validate_refname
($hash_parent)) {
632 die_error
(400, "Invalid hash parent parameter");
636 our $hash_base = $input_params{'hash_base'};
637 if (defined $hash_base) {
638 if (!validate_refname
($hash_base)) {
639 die_error
(400, "Invalid hash base parameter");
643 our @extra_options = @{$input_params{'extra_options'}};
644 # @extra_options is always defined, since it can only be (currently) set from
645 # CGI, and $cgi->param() returns the empty array in array context if the param
647 foreach my $opt (@extra_options) {
648 if (not exists $allowed_options{$opt}) {
649 die_error
(400, "Invalid option parameter");
651 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
652 die_error
(400, "Invalid option parameter for this action");
656 our $hash_parent_base = $input_params{'hash_parent_base'};
657 if (defined $hash_parent_base) {
658 if (!validate_refname
($hash_parent_base)) {
659 die_error
(400, "Invalid hash parent base parameter");
664 our $page = $input_params{'page'};
666 if ($page =~ m/[^0-9]/) {
667 die_error
(400, "Invalid page parameter");
671 our $searchtype = $input_params{'searchtype'};
672 if (defined $searchtype) {
673 if ($searchtype =~ m/[^a-z]/) {
674 die_error
(400, "Invalid searchtype parameter");
678 our $search_use_regexp = $input_params{'search_use_regexp'};
680 our $searchtext = $input_params{'searchtext'};
682 if (defined $searchtext) {
683 if (length($searchtext) < 2) {
684 die_error
(403, "At least two characters are required for search parameter");
686 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
689 # path to the current git repository
691 $git_dir = "$projectroot/$project" if $project;
694 if (!defined $action) {
696 $action = git_get_type
($hash);
697 } elsif (defined $hash_base && defined $file_name) {
698 $action = git_get_type
("$hash_base:$file_name");
699 } elsif (defined $project) {
702 $action = 'project_list';
705 if (!defined($actions{$action})) {
706 die_error
(400, "Unknown action");
708 if ($action !~ m/^(opml|project_list|project_index)$/ &&
710 die_error
(400, "Project needed");
712 $actions{$action}->();
715 ## ======================================================================
720 # default is to use -absolute url() i.e. $my_uri
721 my $href = $params{-full
} ? $my_url : $my_uri;
723 $params{'project'} = $project unless exists $params{'project'};
725 if ($params{-replay
}) {
726 while (my ($name, $symbol) = each %cgi_param_mapping) {
727 if (!exists $params{$name}) {
728 $params{$name} = $input_params{$name};
733 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
735 # use PATH_INFO for project name
736 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
737 delete $params{'project'};
739 # Summary just uses the project path URL
740 if (defined $params{'action'} && $params{'action'} eq 'summary') {
741 delete $params{'action'};
745 # now encode the parameters explicitly
747 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
748 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
749 if (defined $params{$name}) {
750 if (ref($params{$name}) eq "ARRAY") {
751 foreach my $par (@{$params{$name}}) {
752 push @result, $symbol . "=" . esc_param
($par);
755 push @result, $symbol . "=" . esc_param
($params{$name});
759 $href .= "?" . join(';', @result) if scalar @result;
765 ## ======================================================================
766 ## validation, quoting/unquoting and escaping
768 sub validate_action
{
769 my $input = shift || return undef;
770 return undef unless exists $actions{$input};
774 sub validate_project
{
775 my $input = shift || return undef;
776 if (!validate_pathname
($input) ||
777 !(-d
"$projectroot/$input") ||
778 !check_head_link
("$projectroot/$input") ||
779 ($export_ok && !(-e
"$projectroot/$input/$export_ok")) ||
780 ($strict_export && !project_in_list
($input))) {
787 sub validate_pathname
{
788 my $input = shift || return undef;
790 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
791 # at the beginning, at the end, and between slashes.
792 # also this catches doubled slashes
793 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
797 if ($input =~ m!\0!) {
803 sub validate_refname
{
804 my $input = shift || return undef;
806 # textual hashes are O.K.
807 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
810 # it must be correct pathname
811 $input = validate_pathname
($input)
813 # restrictions on ref name according to git-check-ref-format
814 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
820 # decode sequences of octets in utf8 into Perl's internal form,
821 # which is utf-8 with utf8 flag set if needed. gitweb writes out
822 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
825 if (utf8
::valid
($str)) {
829 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
833 # quote unsafe chars, but keep the slash, even when it's not
834 # correct, but quoted slashes look too horrible in bookmarks
837 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
843 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
846 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
852 # replace invalid utf8 character with SUBSTITUTION sequence
857 $str = to_utf8
($str);
858 $str = $cgi->escapeHTML($str);
859 if ($opts{'-nbsp'}) {
860 $str =~ s/ / /g;
862 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
866 # quote control characters and escape filename to HTML
871 $str = to_utf8
($str);
872 $str = $cgi->escapeHTML($str);
873 if ($opts{'-nbsp'}) {
874 $str =~ s/ / /g;
876 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
880 # Make control characters "printable", using character escape codes (CEC)
884 my %es = ( # character escape codes, aka escape sequences
885 "\t" => '\t', # tab (HT)
886 "\n" => '\n', # line feed (LF)
887 "\r" => '\r', # carrige return (CR)
888 "\f" => '\f', # form feed (FF)
889 "\b" => '\b', # backspace (BS)
890 "\a" => '\a', # alarm (bell) (BEL)
891 "\e" => '\e', # escape (ESC)
892 "\013" => '\v', # vertical tab (VT)
893 "\000" => '\0', # nul character (NUL)
895 my $chr = ( (exists $es{$cntrl})
897 : sprintf('\%2x', ord($cntrl)) );
898 if ($opts{-nohtml
}) {
901 return "<span class=\"cntrl\">$chr</span>";
905 # Alternatively use unicode control pictures codepoints,
906 # Unicode "printable representation" (PR)
911 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
912 if ($opts{-nohtml
}) {
915 return "<span class=\"cntrl\">$chr</span>";
919 # git may return quoted and escaped filenames
925 my %es = ( # character escape codes, aka escape sequences
926 't' => "\t", # tab (HT, TAB)
927 'n' => "\n", # newline (NL)
928 'r' => "\r", # return (CR)
929 'f' => "\f", # form feed (FF)
930 'b' => "\b", # backspace (BS)
931 'a' => "\a", # alarm (bell) (BEL)
932 'e' => "\e", # escape (ESC)
933 'v' => "\013", # vertical tab (VT)
936 if ($seq =~ m/^[0-7]{1,3}$/) {
937 # octal char sequence
938 return chr(oct($seq));
939 } elsif (exists $es{$seq}) {
940 # C escape sequence, aka character escape code
943 # quoted ordinary character
947 if ($str =~ m/^"(.*)"$/) {
950 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
955 # escape tabs (convert tabs to spaces)
959 while ((my $pos = index($line, "\t")) != -1) {
960 if (my $count = (8 - ($pos % 8))) {
961 my $spaces = ' ' x
$count;
962 $line =~ s/\t/$spaces/;
969 sub project_in_list
{
971 my @list = git_get_projects_list
();
972 return @list && scalar(grep { $_->{'path'} eq $project } @list);
975 ## ----------------------------------------------------------------------
976 ## HTML aware string manipulation
978 # Try to chop given string on a word boundary between position
979 # $len and $len+$add_len. If there is no word boundary there,
980 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
981 # (marking chopped part) would be longer than given string.
985 my $add_len = shift || 10;
986 my $where = shift || 'right'; # 'left' | 'center' | 'right'
988 # Make sure perl knows it is utf8 encoded so we don't
989 # cut in the middle of a utf8 multibyte char.
990 $str = to_utf8
($str);
992 # allow only $len chars, but don't cut a word if it would fit in $add_len
993 # if it doesn't fit, cut it if it's still longer than the dots we would add
994 # remove chopped character entities entirely
996 # when chopping in the middle, distribute $len into left and right part
997 # return early if chopping wouldn't make string shorter
998 if ($where eq 'center') {
999 return $str if ($len + 5 >= length($str)); # filler is length 5
1002 return $str if ($len + 4 >= length($str)); # filler is length 4
1005 # regexps: ending and beginning with word part up to $add_len
1006 my $endre = qr/.{$len}\w{0,$add_len}/;
1007 my $begre = qr/\w{0,$add_len}.{$len}/;
1009 if ($where eq 'left') {
1010 $str =~ m/^(.*?)($begre)$/;
1011 my ($lead, $body) = ($1, $2);
1012 if (length($lead) > 4) {
1013 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1016 return "$lead$body";
1018 } elsif ($where eq 'center') {
1019 $str =~ m/^($endre)(.*)$/;
1020 my ($left, $str) = ($1, $2);
1021 $str =~ m/^(.*?)($begre)$/;
1022 my ($mid, $right) = ($1, $2);
1023 if (length($mid) > 5) {
1024 $left =~ s/&[^;]*$//;
1025 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1028 return "$left$mid$right";
1031 $str =~ m/^($endre)(.*)$/;
1034 if (length($tail) > 4) {
1035 $body =~ s/&[^;]*$//;
1038 return "$body$tail";
1042 # takes the same arguments as chop_str, but also wraps a <span> around the
1043 # result with a title attribute if it does get chopped. Additionally, the
1044 # string is HTML-escaped.
1045 sub chop_and_escape_str
{
1048 my $chopped = chop_str
(@_);
1049 if ($chopped eq $str) {
1050 return esc_html
($chopped);
1052 $str =~ s/([[:cntrl:]])/?/g;
1053 return $cgi->span({-title
=>$str}, esc_html
($chopped));
1057 ## ----------------------------------------------------------------------
1058 ## functions returning short strings
1060 # CSS class for given age value (in seconds)
1064 if (!defined $age) {
1066 } elsif ($age < 60*60*2) {
1068 } elsif ($age < 60*60*24*2) {
1075 # convert age in seconds to "nn units ago" string
1080 if ($age > 60*60*24*365*2) {
1081 $age_str = (int $age/60/60/24/365);
1082 $age_str .= " years ago";
1083 } elsif ($age > 60*60*24*(365/12)*2) {
1084 $age_str = int $age/60/60/24/(365/12);
1085 $age_str .= " months ago";
1086 } elsif ($age > 60*60*24*7*2) {
1087 $age_str = int $age/60/60/24/7;
1088 $age_str .= " weeks ago";
1089 } elsif ($age > 60*60*24*2) {
1090 $age_str = int $age/60/60/24;
1091 $age_str .= " days ago";
1092 } elsif ($age > 60*60*2) {
1093 $age_str = int $age/60/60;
1094 $age_str .= " hours ago";
1095 } elsif ($age > 60*2) {
1096 $age_str = int $age/60;
1097 $age_str .= " min ago";
1098 } elsif ($age > 2) {
1099 $age_str = int $age;
1100 $age_str .= " sec ago";
1102 $age_str .= " right now";
1108 S_IFINVALID
=> 0030000,
1109 S_IFGITLINK
=> 0160000,
1112 # submodule/subproject, a commit object reference
1113 sub S_ISGITLINK
($) {
1116 return (($mode & S_IFMT
) == S_IFGITLINK
)
1119 # convert file mode in octal to symbolic file mode string
1121 my $mode = oct shift;
1123 if (S_ISGITLINK
($mode)) {
1124 return 'm---------';
1125 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1126 return 'drwxr-xr-x';
1127 } elsif (S_ISLNK
($mode)) {
1128 return 'lrwxrwxrwx';
1129 } elsif (S_ISREG
($mode)) {
1130 # git cares only about the executable bit
1131 if ($mode & S_IXUSR
) {
1132 return '-rwxr-xr-x';
1134 return '-rw-r--r--';
1137 return '----------';
1141 # convert file mode in octal to file type string
1145 if ($mode !~ m/^[0-7]+$/) {
1151 if (S_ISGITLINK
($mode)) {
1153 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1155 } elsif (S_ISLNK
($mode)) {
1157 } elsif (S_ISREG
($mode)) {
1164 # convert file mode in octal to file type description string
1165 sub file_type_long
{
1168 if ($mode !~ m/^[0-7]+$/) {
1174 if (S_ISGITLINK
($mode)) {
1176 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1178 } elsif (S_ISLNK
($mode)) {
1180 } elsif (S_ISREG
($mode)) {
1181 if ($mode & S_IXUSR
) {
1182 return "executable";
1192 ## ----------------------------------------------------------------------
1193 ## functions returning short HTML fragments, or transforming HTML fragments
1194 ## which don't belong to other sections
1196 # format line of commit message.
1197 sub format_log_line_html
{
1200 $line = esc_html
($line, -nbsp
=>1);
1201 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1204 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1205 -class => "text"}, $hash_text);
1206 $line =~ s/$hash_text/$link/;
1211 # format marker of refs pointing to given object
1213 # the destination action is chosen based on object type and current context:
1214 # - for annotated tags, we choose the tag view unless it's the current view
1215 # already, in which case we go to shortlog view
1216 # - for other refs, we keep the current view if we're in history, shortlog or
1217 # log view, and select shortlog otherwise
1218 sub format_ref_marker
{
1219 my ($refs, $id) = @_;
1222 if (defined $refs->{$id}) {
1223 foreach my $ref (@{$refs->{$id}}) {
1224 # this code exploits the fact that non-lightweight tags are the
1225 # only indirect objects, and that they are the only objects for which
1226 # we want to use tag instead of shortlog as action
1227 my ($type, $name) = qw();
1228 my $indirect = ($ref =~ s/\^\{\}$//);
1229 # e.g. tags/v2.6.11 or heads/next
1230 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1239 $class .= " indirect" if $indirect;
1241 my $dest_action = "shortlog";
1244 $dest_action = "tag" unless $action eq "tag";
1245 } elsif ($action =~ /^(history|(short)?log)$/) {
1246 $dest_action = $action;
1250 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1253 my $link = $cgi->a({
1255 action
=>$dest_action,
1259 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1265 return ' <span class="refs">'. $markers . '</span>';
1271 # format, perhaps shortened and with markers, title line
1272 sub format_subject_html
{
1273 my ($long, $short, $href, $extra) = @_;
1274 $extra = '' unless defined($extra);
1276 if (length($short) < length($long)) {
1277 return $cgi->a({-href
=> $href, -class => "list subject",
1278 -title
=> to_utf8
($long)},
1279 esc_html
($short) . $extra);
1281 return $cgi->a({-href
=> $href, -class => "list subject"},
1282 esc_html
($long) . $extra);
1286 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1287 sub format_git_diff_header_line
{
1289 my $diffinfo = shift;
1290 my ($from, $to) = @_;
1292 if ($diffinfo->{'nparents'}) {
1294 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1295 if ($to->{'href'}) {
1296 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1297 esc_path
($to->{'file'}));
1298 } else { # file was deleted (no href)
1299 $line .= esc_path
($to->{'file'});
1303 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1304 if ($from->{'href'}) {
1305 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1306 'a/' . esc_path
($from->{'file'}));
1307 } else { # file was added (no href)
1308 $line .= 'a/' . esc_path
($from->{'file'});
1311 if ($to->{'href'}) {
1312 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1313 'b/' . esc_path
($to->{'file'}));
1314 } else { # file was deleted
1315 $line .= 'b/' . esc_path
($to->{'file'});
1319 return "<div class=\"diff header\">$line</div>\n";
1322 # format extended diff header line, before patch itself
1323 sub format_extended_diff_header_line
{
1325 my $diffinfo = shift;
1326 my ($from, $to) = @_;
1329 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1330 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1331 esc_path
($from->{'file'}));
1333 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1334 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1335 esc_path
($to->{'file'}));
1337 # match single <mode>
1338 if ($line =~ m/\s(\d{6})$/) {
1339 $line .= '<span class="info"> (' .
1340 file_type_long
($1) .
1344 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1345 # can match only for combined diff
1347 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1348 if ($from->{'href'}[$i]) {
1349 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1351 substr($diffinfo->{'from_id'}[$i],0,7));
1356 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1359 if ($to->{'href'}) {
1360 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1361 substr($diffinfo->{'to_id'},0,7));
1366 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1367 # can match only for ordinary diff
1368 my ($from_link, $to_link);
1369 if ($from->{'href'}) {
1370 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1371 substr($diffinfo->{'from_id'},0,7));
1373 $from_link = '0' x
7;
1375 if ($to->{'href'}) {
1376 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1377 substr($diffinfo->{'to_id'},0,7));
1381 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1382 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1385 return $line . "<br/>\n";
1388 # format from-file/to-file diff header
1389 sub format_diff_from_to_header
{
1390 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1395 #assert($line =~ m/^---/) if DEBUG;
1396 # no extra formatting for "^--- /dev/null"
1397 if (! $diffinfo->{'nparents'}) {
1398 # ordinary (single parent) diff
1399 if ($line =~ m!^--- "?a/!) {
1400 if ($from->{'href'}) {
1402 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1403 esc_path
($from->{'file'}));
1406 esc_path
($from->{'file'});
1409 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1412 # combined diff (merge commit)
1413 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1414 if ($from->{'href'}[$i]) {
1416 $cgi->a({-href
=>href
(action
=>"blobdiff",
1417 hash_parent
=>$diffinfo->{'from_id'}[$i],
1418 hash_parent_base
=>$parents[$i],
1419 file_parent
=>$from->{'file'}[$i],
1420 hash
=>$diffinfo->{'to_id'},
1422 file_name
=>$to->{'file'}),
1424 -title
=>"diff" . ($i+1)},
1427 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1428 esc_path
($from->{'file'}[$i]));
1430 $line = '--- /dev/null';
1432 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1437 #assert($line =~ m/^\+\+\+/) if DEBUG;
1438 # no extra formatting for "^+++ /dev/null"
1439 if ($line =~ m!^\+\+\+ "?b/!) {
1440 if ($to->{'href'}) {
1442 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1443 esc_path
($to->{'file'}));
1446 esc_path
($to->{'file'});
1449 $result .= qq
!<div
class="diff to_file">$line</div
>\n!;
1454 # create note for patch simplified by combined diff
1455 sub format_diff_cc_simplified
{
1456 my ($diffinfo, @parents) = @_;
1459 $result .= "<div class=\"diff header\">" .
1461 if (!is_deleted
($diffinfo)) {
1462 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1464 hash
=>$diffinfo->{'to_id'},
1465 file_name
=>$diffinfo->{'to_file'}),
1467 esc_path
($diffinfo->{'to_file'}));
1469 $result .= esc_path
($diffinfo->{'to_file'});
1471 $result .= "</div>\n" . # class="diff header"
1472 "<div class=\"diff nodifferences\">" .
1474 "</div>\n"; # class="diff nodifferences"
1479 # format patch (diff) line (not to be used for diff headers)
1480 sub format_diff_line
{
1482 my ($from, $to) = @_;
1483 my $diff_class = "";
1487 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1489 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1490 if ($line =~ m/^\@{3}/) {
1491 $diff_class = " chunk_header";
1492 } elsif ($line =~ m/^\\/) {
1493 $diff_class = " incomplete";
1494 } elsif ($prefix =~ tr/+/+/) {
1495 $diff_class = " add";
1496 } elsif ($prefix =~ tr/-/-/) {
1497 $diff_class = " rem";
1500 # assume ordinary diff
1501 my $char = substr($line, 0, 1);
1503 $diff_class = " add";
1504 } elsif ($char eq '-') {
1505 $diff_class = " rem";
1506 } elsif ($char eq '@') {
1507 $diff_class = " chunk_header";
1508 } elsif ($char eq "\\") {
1509 $diff_class = " incomplete";
1512 $line = untabify
($line);
1513 if ($from && $to && $line =~ m/^\@{2} /) {
1514 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1515 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1517 $from_lines = 0 unless defined $from_lines;
1518 $to_lines = 0 unless defined $to_lines;
1520 if ($from->{'href'}) {
1521 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1522 -class=>"list"}, $from_text);
1524 if ($to->{'href'}) {
1525 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1526 -class=>"list"}, $to_text);
1528 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1529 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1530 return "<div class=\"diff$diff_class\">$line</div>\n";
1531 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1532 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1533 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1535 @from_text = split(' ', $ranges);
1536 for (my $i = 0; $i < @from_text; ++$i) {
1537 ($from_start[$i], $from_nlines[$i]) =
1538 (split(',', substr($from_text[$i], 1)), 0);
1541 $to_text = pop @from_text;
1542 $to_start = pop @from_start;
1543 $to_nlines = pop @from_nlines;
1545 $line = "<span class=\"chunk_info\">$prefix ";
1546 for (my $i = 0; $i < @from_text; ++$i) {
1547 if ($from->{'href'}[$i]) {
1548 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1549 -class=>"list"}, $from_text[$i]);
1551 $line .= $from_text[$i];
1555 if ($to->{'href'}) {
1556 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1557 -class=>"list"}, $to_text);
1561 $line .= " $prefix</span>" .
1562 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1563 return "<div class=\"diff$diff_class\">$line</div>\n";
1565 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1568 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1569 # linked. Pass the hash of the tree/commit to snapshot.
1570 sub format_snapshot_links
{
1572 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1573 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1574 my $num_fmts = @snapshot_fmts;
1575 if ($num_fmts > 1) {
1576 # A parenthesized list of links bearing format names.
1577 # e.g. "snapshot (_tar.gz_ _zip_)"
1578 return "snapshot (" . join(' ', map
1585 }, $known_snapshot_formats{$_}{'display'})
1586 , @snapshot_fmts) . ")";
1587 } elsif ($num_fmts == 1) {
1588 # A single "snapshot" link whose tooltip bears the format name.
1590 my ($fmt) = @snapshot_fmts;
1596 snapshot_format
=>$fmt
1598 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1600 } else { # $num_fmts == 0
1605 ## ......................................................................
1606 ## functions returning values to be passed, perhaps after some
1607 ## transformation, to other functions; e.g. returning arguments to href()
1609 # returns hash to be passed to href to generate gitweb URL
1610 # in -title key it returns description of link
1612 my $format = shift || 'Atom';
1613 my %res = (action
=> lc($format));
1615 # feed links are possible only for project views
1616 return unless (defined $project);
1617 # some views should link to OPML, or to generic project feed,
1618 # or don't have specific feed yet (so they should use generic)
1619 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1622 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1623 # from tag links; this also makes possible to detect branch links
1624 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1625 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1628 # find log type for feed description (title)
1630 if (defined $file_name) {
1631 $type = "history of $file_name";
1632 $type .= "/" if ($action eq 'tree');
1633 $type .= " on '$branch'" if (defined $branch);
1635 $type = "log of $branch" if (defined $branch);
1638 $res{-title
} = $type;
1639 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1640 $res{'file_name'} = $file_name;
1645 ## ----------------------------------------------------------------------
1646 ## git utility subroutines, invoking git commands
1648 # returns path to the core git executable and the --git-dir parameter as list
1650 return $GIT, '--git-dir='.$git_dir;
1653 # quote the given arguments for passing them to the shell
1654 # quote_command("command", "arg 1", "arg with ' and ! characters")
1655 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1656 # Try to avoid using this function wherever possible.
1659 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1662 # get HEAD ref of given project as hash
1663 sub git_get_head_hash
{
1664 my $project = shift;
1665 my $o_git_dir = $git_dir;
1667 $git_dir = "$projectroot/$project";
1668 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1671 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1675 if (defined $o_git_dir) {
1676 $git_dir = $o_git_dir;
1681 # get type of given object
1685 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1687 close $fd or return;
1692 # repository configuration
1693 our $config_file = '';
1696 # store multiple values for single key as anonymous array reference
1697 # single values stored directly in the hash, not as [ <value> ]
1698 sub hash_set_multi
{
1699 my ($hash, $key, $value) = @_;
1701 if (!exists $hash->{$key}) {
1702 $hash->{$key} = $value;
1703 } elsif (!ref $hash->{$key}) {
1704 $hash->{$key} = [ $hash->{$key}, $value ];
1706 push @{$hash->{$key}}, $value;
1710 # return hash of git project configuration
1711 # optionally limited to some section, e.g. 'gitweb'
1712 sub git_parse_project_config
{
1713 my $section_regexp = shift;
1718 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1721 while (my $keyval = <$fh>) {
1723 my ($key, $value) = split(/\n/, $keyval, 2);
1725 hash_set_multi
(\
%config, $key, $value)
1726 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1733 # convert config value to boolean, 'true' or 'false'
1734 # no value, number > 0, 'true' and 'yes' values are true
1735 # rest of values are treated as false (never as error)
1736 sub config_to_bool
{
1739 # strip leading and trailing whitespace
1743 return (!defined $val || # section.key
1744 ($val =~ /^\d+$/ && $val) || # section.key = 1
1745 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1748 # convert config value to simple decimal number
1749 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1750 # to be multiplied by 1024, 1048576, or 1073741824
1754 # strip leading and trailing whitespace
1758 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1760 # unknown unit is treated as 1
1761 return $num * ($unit eq 'g' ? 1073741824 :
1762 $unit eq 'm' ? 1048576 :
1763 $unit eq 'k' ? 1024 : 1);
1768 # convert config value to array reference, if needed
1769 sub config_to_multi
{
1772 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1775 sub git_get_project_config
{
1776 my ($key, $type) = @_;
1779 return unless ($key);
1780 $key =~ s/^gitweb\.//;
1781 return if ($key =~ m/\W/);
1784 if (defined $type) {
1787 unless ($type eq 'bool' || $type eq 'int');
1791 if (!defined $config_file ||
1792 $config_file ne "$git_dir/config") {
1793 %config = git_parse_project_config
('gitweb');
1794 $config_file = "$git_dir/config";
1798 if (!defined $type) {
1799 return $config{"gitweb.$key"};
1800 } elsif ($type eq 'bool') {
1801 # backward compatibility: 'git config --bool' returns true/false
1802 return config_to_bool
($config{"gitweb.$key"}) ? 'true' : 'false';
1803 } elsif ($type eq 'int') {
1804 return config_to_int
($config{"gitweb.$key"});
1806 return $config{"gitweb.$key"};
1809 # get hash of given path at given ref
1810 sub git_get_hash_by_path
{
1812 my $path = shift || return undef;
1817 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1818 or die_error
(500, "Open git-ls-tree failed");
1820 close $fd or return undef;
1822 if (!defined $line) {
1823 # there is no tree or hash given by $path at $base
1827 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1828 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1829 if (defined $type && $type ne $2) {
1830 # type doesn't match
1836 # get path of entry with given hash at given tree-ish (ref)
1837 # used to get 'from' filename for combined diff (merge commit) for renames
1838 sub git_get_path_by_hash
{
1839 my $base = shift || return;
1840 my $hash = shift || return;
1844 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1846 while (my $line = <$fd>) {
1849 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1850 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1851 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1860 ## ......................................................................
1861 ## git utility functions, directly accessing git repository
1863 sub git_get_project_description
{
1866 $git_dir = "$projectroot/$path";
1867 open my $fd, "$git_dir/description"
1868 or return git_get_project_config
('description');
1871 if (defined $descr) {
1877 sub git_get_project_ctags
{
1881 $git_dir = "$projectroot/$path";
1882 unless (opendir D
, "$git_dir/ctags") {
1885 foreach (grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir(D
)) {
1886 open CT
, $_ or next;
1890 my $ctag = $_; $ctag =~ s
#.*/##;
1891 $ctags->{$ctag} = $val;
1897 sub git_populate_project_tagcloud
{
1900 # First, merge different-cased tags; tags vote on casing
1902 foreach (keys %$ctags) {
1903 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
1904 if (not $ctags_lc{lc $_}->{topcount
}
1905 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
1906 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
1907 $ctags_lc{lc $_}->{topname
} = $_;
1912 if (eval { require HTML
::TagCloud
; 1; }) {
1913 $cloud = HTML
::TagCloud-
>new;
1914 foreach (sort keys %ctags_lc) {
1915 # Pad the title with spaces so that the cloud looks
1917 my $title = $ctags_lc{$_}->{topname
};
1918 $title =~ s/ / /g;
1919 $title =~ s/^/ /g;
1920 $title =~ s/$/ /g;
1921 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count
});
1924 $cloud = \
%ctags_lc;
1929 sub git_show_project_tagcloud
{
1930 my ($cloud, $count) = @_;
1931 print STDERR
ref($cloud)."..\n";
1932 if (ref $cloud eq 'HTML::TagCloud') {
1933 return $cloud->html_and_css($count);
1935 my @tags = sort { $cloud->{$a}->{count
} <=> $cloud->{$b}->{count
} } keys %$cloud;
1936 return '<p align="center">' . join (', ', map {
1937 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1938 } splice(@tags, 0, $count)) . '</p>';
1942 sub git_get_project_url_list
{
1945 $git_dir = "$projectroot/$path";
1946 open my $fd, "$git_dir/cloneurl"
1947 or return wantarray ?
1948 @{ config_to_multi
(git_get_project_config
('url')) } :
1949 config_to_multi
(git_get_project_config
('url'));
1950 my @git_project_url_list = map { chomp; $_ } <$fd>;
1953 return wantarray ? @git_project_url_list : \
@git_project_url_list;
1956 sub git_get_projects_list
{
1961 $filter =~ s/\.git$//;
1963 my ($check_forks) = gitweb_check_feature
('forks');
1965 if (-d
$projects_list) {
1966 # search in directory
1967 my $dir = $projects_list . ($filter ? "/$filter" : '');
1968 # remove the trailing "/"
1970 my $pfxlen = length("$dir");
1971 my $pfxdepth = ($dir =~ tr!/!!);
1974 follow_fast
=> 1, # follow symbolic links
1975 follow_skip
=> 2, # ignore duplicates
1976 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1978 # skip project-list toplevel, if we get it.
1979 return if (m!^[/.]$!);
1980 # only directories can be git repositories
1981 return unless (-d
$_);
1982 # don't traverse too deep (Find is super slow on os x)
1983 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1984 $File::Find
::prune
= 1;
1988 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1989 # we check related file in $projectroot
1990 if (check_export_ok
("$projectroot/$filter/$subdir")) {
1991 push @list, { path
=> ($filter ? "$filter/" : '') . $subdir };
1992 $File::Find
::prune
= 1;
1997 } elsif (-f
$projects_list) {
1998 # read from file(url-encoded):
1999 # 'git%2Fgit.git Linus+Torvalds'
2000 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2001 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2003 open my ($fd), $projects_list or return;
2005 while (my $line = <$fd>) {
2007 my ($path, $owner) = split ' ', $line;
2008 $path = unescape
($path);
2009 $owner = unescape
($owner);
2010 if (!defined $path) {
2013 if ($filter ne '') {
2014 # looking for forks;
2015 my $pfx = substr($path, 0, length($filter));
2016 if ($pfx ne $filter) {
2019 my $sfx = substr($path, length($filter));
2020 if ($sfx !~ /^\/.*\
.git
$/) {
2023 } elsif ($check_forks) {
2025 foreach my $filter (keys %paths) {
2026 # looking for forks;
2027 my $pfx = substr($path, 0, length($filter));
2028 if ($pfx ne $filter) {
2031 my $sfx = substr($path, length($filter));
2032 if ($sfx !~ /^\/.*\
.git
$/) {
2035 # is a fork, don't include it in
2040 if (check_export_ok
("$projectroot/$path")) {
2043 owner
=> to_utf8
($owner),
2046 (my $forks_path = $path) =~ s/\.git$//;
2047 $paths{$forks_path}++;
2055 our $gitweb_project_owner = undef;
2056 sub git_get_project_list_from_file
{
2058 return if (defined $gitweb_project_owner);
2060 $gitweb_project_owner = {};
2061 # read from file (url-encoded):
2062 # 'git%2Fgit.git Linus+Torvalds'
2063 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2064 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2065 if (-f
$projects_list) {
2066 open (my $fd , $projects_list);
2067 while (my $line = <$fd>) {
2069 my ($pr, $ow) = split ' ', $line;
2070 $pr = unescape
($pr);
2071 $ow = unescape
($ow);
2072 $gitweb_project_owner->{$pr} = to_utf8
($ow);
2078 sub git_get_project_owner
{
2079 my $project = shift;
2082 return undef unless $project;
2083 $git_dir = "$projectroot/$project";
2085 if (!defined $gitweb_project_owner) {
2086 git_get_project_list_from_file
();
2089 if (exists $gitweb_project_owner->{$project}) {
2090 $owner = $gitweb_project_owner->{$project};
2092 if (!defined $owner){
2093 $owner = git_get_project_config
('owner');
2095 if (!defined $owner) {
2096 $owner = get_file_owner
("$git_dir");
2102 sub git_get_last_activity
{
2106 $git_dir = "$projectroot/$path";
2107 open($fd, "-|", git_cmd
(), 'for-each-ref',
2108 '--format=%(committer)',
2109 '--sort=-committerdate',
2111 'refs/heads') or return;
2112 my $most_recent = <$fd>;
2113 close $fd or return;
2114 if (defined $most_recent &&
2115 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2117 my $age = time - $timestamp;
2118 return ($age, age_string
($age));
2120 return (undef, undef);
2123 sub git_get_references
{
2124 my $type = shift || "";
2126 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2127 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2128 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
2129 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2132 while (my $line = <$fd>) {
2134 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2135 if (defined $refs{$1}) {
2136 push @{$refs{$1}}, $2;
2142 close $fd or return;
2146 sub git_get_rev_name_tags
{
2147 my $hash = shift || return undef;
2149 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
2151 my $name_rev = <$fd>;
2154 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2157 # catches also '$hash undefined' output
2162 ## ----------------------------------------------------------------------
2163 ## parse to hash functions
2167 my $tz = shift || "-0000";
2170 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2171 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2172 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2173 $date{'hour'} = $hour;
2174 $date{'minute'} = $min;
2175 $date{'mday'} = $mday;
2176 $date{'day'} = $days[$wday];
2177 $date{'month'} = $months[$mon];
2178 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2179 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2180 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2181 $mday, $months[$mon], $hour ,$min;
2182 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2183 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2185 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2186 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2187 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2188 $date{'hour_local'} = $hour;
2189 $date{'minute_local'} = $min;
2190 $date{'tz_local'} = $tz;
2191 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2192 1900+$year, $mon+1, $mday,
2193 $hour, $min, $sec, $tz);
2202 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2203 $tag{'id'} = $tag_id;
2204 while (my $line = <$fd>) {
2206 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2207 $tag{'object'} = $1;
2208 } elsif ($line =~ m/^type (.+)$/) {
2210 } elsif ($line =~ m/^tag (.+)$/) {
2212 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2213 $tag{'author'} = $1;
2216 } elsif ($line =~ m/--BEGIN/) {
2217 push @comment, $line;
2219 } elsif ($line eq "") {
2223 push @comment, <$fd>;
2224 $tag{'comment'} = \
@comment;
2225 close $fd or return;
2226 if (!defined $tag{'name'}) {
2232 sub parse_commit_text
{
2233 my ($commit_text, $withparents) = @_;
2234 my @commit_lines = split '\n', $commit_text;
2237 pop @commit_lines; # Remove '\0'
2239 if (! @commit_lines) {
2243 my $header = shift @commit_lines;
2244 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2247 ($co{'id'}, my @parents) = split ' ', $header;
2248 while (my $line = shift @commit_lines) {
2249 last if $line eq "\n";
2250 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2252 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2254 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2256 $co{'author_epoch'} = $2;
2257 $co{'author_tz'} = $3;
2258 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2259 $co{'author_name'} = $1;
2260 $co{'author_email'} = $2;
2262 $co{'author_name'} = $co{'author'};
2264 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2265 $co{'committer'} = $1;
2266 $co{'committer_epoch'} = $2;
2267 $co{'committer_tz'} = $3;
2268 $co{'committer_name'} = $co{'committer'};
2269 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2270 $co{'committer_name'} = $1;
2271 $co{'committer_email'} = $2;
2273 $co{'committer_name'} = $co{'committer'};
2277 if (!defined $co{'tree'}) {
2280 $co{'parents'} = \
@parents;
2281 $co{'parent'} = $parents[0];
2283 foreach my $title (@commit_lines) {
2286 $co{'title'} = chop_str
($title, 80, 5);
2287 # remove leading stuff of merges to make the interesting part visible
2288 if (length($title) > 50) {
2289 $title =~ s/^Automatic //;
2290 $title =~ s/^merge (of|with) /Merge ... /i;
2291 if (length($title) > 50) {
2292 $title =~ s/(http|rsync):\/\///;
2294 if (length($title) > 50) {
2295 $title =~ s/(master|www|rsync)\.//;
2297 if (length($title) > 50) {
2298 $title =~ s/kernel.org:?//;
2300 if (length($title) > 50) {
2301 $title =~ s/\/pub\/scm//;
2304 $co{'title_short'} = chop_str
($title, 50, 5);
2308 if (! defined $co{'title'} || $co{'title'} eq "") {
2309 $co{'title'} = $co{'title_short'} = '(no commit message)';
2311 # remove added spaces
2312 foreach my $line (@commit_lines) {
2315 $co{'comment'} = \
@commit_lines;
2317 my $age = time - $co{'committer_epoch'};
2319 $co{'age_string'} = age_string
($age);
2320 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2321 if ($age > 60*60*24*7*2) {
2322 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2323 $co{'age_string_age'} = $co{'age_string'};
2325 $co{'age_string_date'} = $co{'age_string'};
2326 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2332 my ($commit_id) = @_;
2337 open my $fd, "-|", git_cmd
(), "rev-list",
2343 or die_error
(500, "Open git-rev-list failed");
2344 %co = parse_commit_text
(<$fd>, 1);
2351 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2359 open my $fd, "-|", git_cmd
(), "rev-list",
2362 ("--max-count=" . $maxcount),
2363 ("--skip=" . $skip),
2367 ($filename ? ($filename) : ())
2368 or die_error
(500, "Open git-rev-list failed");
2369 while (my $line = <$fd>) {
2370 my %co = parse_commit_text
($line);
2375 return wantarray ? @cos : \
@cos;
2378 # parse line of git-diff-tree "raw" output
2379 sub parse_difftree_raw_line
{
2383 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2384 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2385 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2386 $res{'from_mode'} = $1;
2387 $res{'to_mode'} = $2;
2388 $res{'from_id'} = $3;
2390 $res{'status'} = $5;
2391 $res{'similarity'} = $6;
2392 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2393 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2395 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2398 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2399 # combined diff (for merge commit)
2400 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2401 $res{'nparents'} = length($1);
2402 $res{'from_mode'} = [ split(' ', $2) ];
2403 $res{'to_mode'} = pop @{$res{'from_mode'}};
2404 $res{'from_id'} = [ split(' ', $3) ];
2405 $res{'to_id'} = pop @{$res{'from_id'}};
2406 $res{'status'} = [ split('', $4) ];
2407 $res{'to_file'} = unquote
($5);
2409 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2410 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2411 $res{'commit'} = $1;
2414 return wantarray ? %res : \
%res;
2417 # wrapper: return parsed line of git-diff-tree "raw" output
2418 # (the argument might be raw line, or parsed info)
2419 sub parsed_difftree_line
{
2420 my $line_or_ref = shift;
2422 if (ref($line_or_ref) eq "HASH") {
2423 # pre-parsed (or generated by hand)
2424 return $line_or_ref;
2426 return parse_difftree_raw_line
($line_or_ref);
2430 # parse line of git-ls-tree output
2431 sub parse_ls_tree_line
($;%) {
2436 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2437 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2445 $res{'name'} = unquote
($4);
2448 return wantarray ? %res : \
%res;
2451 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2452 sub parse_from_to_diffinfo
{
2453 my ($diffinfo, $from, $to, @parents) = @_;
2455 if ($diffinfo->{'nparents'}) {
2457 $from->{'file'} = [];
2458 $from->{'href'} = [];
2459 fill_from_file_info
($diffinfo, @parents)
2460 unless exists $diffinfo->{'from_file'};
2461 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2462 $from->{'file'}[$i] =
2463 defined $diffinfo->{'from_file'}[$i] ?
2464 $diffinfo->{'from_file'}[$i] :
2465 $diffinfo->{'to_file'};
2466 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2467 $from->{'href'}[$i] = href
(action
=>"blob",
2468 hash_base
=>$parents[$i],
2469 hash
=>$diffinfo->{'from_id'}[$i],
2470 file_name
=>$from->{'file'}[$i]);
2472 $from->{'href'}[$i] = undef;
2476 # ordinary (not combined) diff
2477 $from->{'file'} = $diffinfo->{'from_file'};
2478 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2479 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2480 hash
=>$diffinfo->{'from_id'},
2481 file_name
=>$from->{'file'});
2483 delete $from->{'href'};
2487 $to->{'file'} = $diffinfo->{'to_file'};
2488 if (!is_deleted
($diffinfo)) { # file exists in result
2489 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2490 hash
=>$diffinfo->{'to_id'},
2491 file_name
=>$to->{'file'});
2493 delete $to->{'href'};
2497 ## ......................................................................
2498 ## parse to array of hashes functions
2500 sub git_get_heads_list
{
2504 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2505 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2506 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2509 while (my $line = <$fd>) {
2513 my ($refinfo, $committerinfo) = split(/\0/, $line);
2514 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2515 my ($committer, $epoch, $tz) =
2516 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2517 $ref_item{'fullname'} = $name;
2518 $name =~ s!^refs/heads/!!;
2520 $ref_item{'name'} = $name;
2521 $ref_item{'id'} = $hash;
2522 $ref_item{'title'} = $title || '(no commit message)';
2523 $ref_item{'epoch'} = $epoch;
2525 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2527 $ref_item{'age'} = "unknown";
2530 push @headslist, \
%ref_item;
2534 return wantarray ? @headslist : \
@headslist;
2537 sub git_get_tags_list
{
2541 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2542 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2543 '--format=%(objectname) %(objecttype) %(refname) '.
2544 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2547 while (my $line = <$fd>) {
2551 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2552 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2553 my ($creator, $epoch, $tz) =
2554 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2555 $ref_item{'fullname'} = $name;
2556 $name =~ s!^refs/tags/!!;
2558 $ref_item{'type'} = $type;
2559 $ref_item{'id'} = $id;
2560 $ref_item{'name'} = $name;
2561 if ($type eq "tag") {
2562 $ref_item{'subject'} = $title;
2563 $ref_item{'reftype'} = $reftype;
2564 $ref_item{'refid'} = $refid;
2566 $ref_item{'reftype'} = $type;
2567 $ref_item{'refid'} = $id;
2570 if ($type eq "tag" || $type eq "commit") {
2571 $ref_item{'epoch'} = $epoch;
2573 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2575 $ref_item{'age'} = "unknown";
2579 push @tagslist, \
%ref_item;
2583 return wantarray ? @tagslist : \
@tagslist;
2586 ## ----------------------------------------------------------------------
2587 ## filesystem-related functions
2589 sub get_file_owner
{
2592 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2593 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2594 if (!defined $gcos) {
2598 $owner =~ s/[,;].*$//;
2599 return to_utf8
($owner);
2602 ## ......................................................................
2603 ## mimetype related functions
2605 sub mimetype_guess_file
{
2606 my $filename = shift;
2607 my $mimemap = shift;
2608 -r
$mimemap or return undef;
2611 open(MIME
, $mimemap) or return undef;
2613 next if m/^#/; # skip comments
2614 my ($mime, $exts) = split(/\t+/);
2615 if (defined $exts) {
2616 my @exts = split(/\s+/, $exts);
2617 foreach my $ext (@exts) {
2618 $mimemap{$ext} = $mime;
2624 $filename =~ /\.([^.]*)$/;
2625 return $mimemap{$1};
2628 sub mimetype_guess
{
2629 my $filename = shift;
2631 $filename =~ /\./ or return undef;
2633 if ($mimetypes_file) {
2634 my $file = $mimetypes_file;
2635 if ($file !~ m!^/!) { # if it is relative path
2636 # it is relative to project
2637 $file = "$projectroot/$project/$file";
2639 $mime = mimetype_guess_file
($filename, $file);
2641 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2647 my $filename = shift;
2650 my $mime = mimetype_guess
($filename);
2651 $mime and return $mime;
2655 return $default_blob_plain_mimetype unless $fd;
2658 return 'text/plain';
2659 } elsif (! $filename) {
2660 return 'application/octet-stream';
2661 } elsif ($filename =~ m/\.png$/i) {
2663 } elsif ($filename =~ m/\.gif$/i) {
2665 } elsif ($filename =~ m/\.jpe?g$/i) {
2666 return 'image/jpeg';
2668 return 'application/octet-stream';
2672 sub blob_contenttype
{
2673 my ($fd, $file_name, $type) = @_;
2675 $type ||= blob_mimetype
($fd, $file_name);
2676 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2677 $type .= "; charset=$default_text_plain_charset";
2683 ## ======================================================================
2684 ## functions printing HTML: header, footer, error page
2686 sub git_header_html
{
2687 my $status = shift || "200 OK";
2688 my $expires = shift;
2690 my $title = "$site_name";
2691 if (defined $project) {
2692 $title .= " - " . to_utf8
($project);
2693 if (defined $action) {
2694 $title .= "/$action";
2695 if (defined $file_name) {
2696 $title .= " - " . esc_path
($file_name);
2697 if ($action eq "tree" && $file_name !~ m
|/$|) {
2704 # require explicit support from the UA if we are to send the page as
2705 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2706 # we have to do this because MSIE sometimes globs '*/*', pretending to
2707 # support xhtml+xml but choking when it gets what it asked for.
2708 if (defined $cgi->http('HTTP_ACCEPT') &&
2709 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2710 $cgi->Accept('application/xhtml+xml') != 0) {
2711 $content_type = 'application/xhtml+xml';
2713 $content_type = 'text/html';
2715 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2716 -status
=> $status, -expires
=> $expires);
2717 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2719 <?xml version="1.0" encoding="utf-8"?>
2720 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2721 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2722 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2723 <!-- git core binaries version $git_version -->
2725 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2726 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2727 <meta name="robots" content="index, nofollow"/>
2728 <title>$title</title>
2730 # print out each stylesheet that exist
2731 if (defined $stylesheet) {
2732 #provides backwards capability for those people who define style sheet in a config file
2733 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2735 foreach my $stylesheet (@stylesheets) {
2736 next unless $stylesheet;
2737 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2740 if (defined $project) {
2741 my %href_params = get_feed_info
();
2742 if (!exists $href_params{'-title'}) {
2743 $href_params{'-title'} = 'log';
2746 foreach my $format qw(RSS Atom) {
2747 my $type = lc($format);
2749 '-rel' => 'alternate',
2750 '-title' => "$project - $href_params{'-title'} - $format feed",
2751 '-type' => "application/$type+xml"
2754 $href_params{'action'} = $type;
2755 $link_attr{'-href'} = href
(%href_params);
2757 "rel=\"$link_attr{'-rel'}\" ".
2758 "title=\"$link_attr{'-title'}\" ".
2759 "href=\"$link_attr{'-href'}\" ".
2760 "type=\"$link_attr{'-type'}\" ".
2763 $href_params{'extra_options'} = '--no-merges';
2764 $link_attr{'-href'} = href
(%href_params);
2765 $link_attr{'-title'} .= ' (no merges)';
2767 "rel=\"$link_attr{'-rel'}\" ".
2768 "title=\"$link_attr{'-title'}\" ".
2769 "href=\"$link_attr{'-href'}\" ".
2770 "type=\"$link_attr{'-type'}\" ".
2775 printf('<link rel="alternate" title="%s projects list" '.
2776 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2777 $site_name, href
(project
=>undef, action
=>"project_index"));
2778 printf('<link rel="alternate" title="%s projects feeds" '.
2779 'href="%s" type="text/x-opml" />'."\n",
2780 $site_name, href
(project
=>undef, action
=>"opml"));
2782 if (defined $favicon) {
2783 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2789 if (-f
$site_header) {
2790 open (my $fd, $site_header);
2795 print "<div class=\"page_header\">\n" .
2796 $cgi->a({-href
=> esc_url
($logo_url),
2797 -title
=> $logo_label},
2798 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2799 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2800 if (defined $project) {
2801 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2802 if (defined $action) {
2809 my ($have_search) = gitweb_check_feature
('search');
2810 if (defined $project && $have_search) {
2811 if (!defined $searchtext) {
2815 if (defined $hash_base) {
2816 $search_hash = $hash_base;
2817 } elsif (defined $hash) {
2818 $search_hash = $hash;
2820 $search_hash = "HEAD";
2822 my $action = $my_uri;
2823 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2824 if ($use_pathinfo) {
2825 $action .= "/".esc_url
($project);
2827 print $cgi->startform(-method => "get", -action
=> $action) .
2828 "<div class=\"search\">\n" .
2830 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2831 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2832 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2833 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2834 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2835 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2837 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2838 "<span title=\"Extended regular expression\">" .
2839 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2840 -checked
=> $search_use_regexp) .
2843 $cgi->end_form() . "\n";
2847 sub git_footer_html
{
2848 my $feed_class = 'rss_logo';
2850 print "<div class=\"page_footer\">\n";
2851 if (defined $project) {
2852 my $descr = git_get_project_description
($project);
2853 if (defined $descr) {
2854 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2857 my %href_params = get_feed_info
();
2858 if (!%href_params) {
2859 $feed_class .= ' generic';
2861 $href_params{'-title'} ||= 'log';
2863 foreach my $format qw(RSS Atom) {
2864 $href_params{'action'} = lc($format);
2865 print $cgi->a({-href
=> href
(%href_params),
2866 -title
=> "$href_params{'-title'} $format feed",
2867 -class => $feed_class}, $format)."\n";
2871 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2872 -class => $feed_class}, "OPML") . " ";
2873 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2874 -class => $feed_class}, "TXT") . "\n";
2876 print "</div>\n"; # class="page_footer"
2878 if (-f
$site_footer) {
2879 open (my $fd, $site_footer);
2888 # die_error(<http_status_code>, <error_message>)
2889 # Example: die_error(404, 'Hash not found')
2890 # By convention, use the following status codes (as defined in RFC 2616):
2891 # 400: Invalid or missing CGI parameters, or
2892 # requested object exists but has wrong type.
2893 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2894 # this server or project.
2895 # 404: Requested object/revision/project doesn't exist.
2896 # 500: The server isn't configured properly, or
2897 # an internal error occurred (e.g. failed assertions caused by bugs), or
2898 # an unknown error occurred (e.g. the git binary died unexpectedly).
2900 my $status = shift || 500;
2901 my $error = shift || "Internal server error";
2903 my %http_responses = (400 => '400 Bad Request',
2904 403 => '403 Forbidden',
2905 404 => '404 Not Found',
2906 500 => '500 Internal Server Error');
2907 git_header_html
($http_responses{$status});
2909 <div class="page_body">
2919 ## ----------------------------------------------------------------------
2920 ## functions printing or outputting HTML: navigation
2922 sub git_print_page_nav
{
2923 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2924 $extra = '' if !defined $extra; # pager or formats
2926 my @navs = qw(summary shortlog log commit commitdiff tree);
2928 @navs = grep { $_ ne $suppress } @navs;
2931 my %arg = map { $_ => {action
=>$_} } @navs;
2932 if (defined $head) {
2933 for (qw(commit commitdiff)) {
2934 $arg{$_}{'hash'} = $head;
2936 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2937 for (qw(shortlog log)) {
2938 $arg{$_}{'hash'} = $head;
2943 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2944 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2946 my @actions = gitweb_check_feature
('actions');
2949 'n' => $project, # project name
2950 'f' => $git_dir, # project path within filesystem
2951 'h' => $treehead || '', # current hash ('h' parameter)
2952 'b' => $treebase || '', # hash base ('hb' parameter)
2955 my ($label, $link, $pos) = splice(@actions,0,3);
2957 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2959 $link =~ s/%([%nfhb])/$repl{$1}/g;
2960 $arg{$label}{'_href'} = $link;
2963 print "<div class=\"page_nav\">\n" .
2965 map { $_ eq $current ?
2966 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ? $arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
2968 print "<br/>\n$extra<br/>\n" .
2972 sub format_paging_nav
{
2973 my ($action, $hash, $head, $page, $has_next_link) = @_;
2977 if ($hash ne $head || $page) {
2978 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2980 $paging_nav .= "HEAD";
2984 $paging_nav .= " ⋅ " .
2985 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2986 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2988 $paging_nav .= " ⋅ prev";
2991 if ($has_next_link) {
2992 $paging_nav .= " ⋅ " .
2993 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2994 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2996 $paging_nav .= " ⋅ next";
3002 ## ......................................................................
3003 ## functions printing or outputting HTML: div
3005 sub git_print_header_div
{
3006 my ($action, $title, $hash, $hash_base) = @_;
3009 $args{'action'} = $action;
3010 $args{'hash'} = $hash if $hash;
3011 $args{'hash_base'} = $hash_base if $hash_base;
3013 print "<div class=\"header\">\n" .
3014 $cgi->a({-href
=> href
(%args), -class => "title"},
3015 $title ? $title : $action) .
3019 #sub git_print_authorship (\%) {
3020 sub git_print_authorship
{
3023 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
3024 print "<div class=\"author_date\">" .
3025 esc_html
($co->{'author_name'}) .
3027 if ($ad{'hour_local'} < 6) {
3028 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3029 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3031 printf(" (%02d:%02d %s)",
3032 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3037 sub git_print_page_path
{
3043 print "<div class=\"page_path\">";
3044 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
3045 -title
=> 'tree root'}, to_utf8
("[$project]"));
3047 if (defined $name) {
3048 my @dirname = split '/', $name;
3049 my $basename = pop @dirname;
3052 foreach my $dir (@dirname) {
3053 $fullname .= ($fullname ? '/' : '') . $dir;
3054 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
3056 -title
=> $fullname}, esc_path
($dir));
3059 if (defined $type && $type eq 'blob') {
3060 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
3062 -title
=> $name}, esc_path
($basename));
3063 } elsif (defined $type && $type eq 'tree') {
3064 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
3066 -title
=> $name}, esc_path
($basename));
3069 print esc_path
($basename);
3072 print "<br/></div>\n";
3075 # sub git_print_log (\@;%) {
3076 sub git_print_log
($;%) {
3080 if ($opts{'-remove_title'}) {
3081 # remove title, i.e. first line of log
3084 # remove leading empty lines
3085 while (defined $log->[0] && $log->[0] eq "") {
3092 foreach my $line (@$log) {
3093 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3096 if (! $opts{'-remove_signoff'}) {
3097 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
3100 # remove signoff lines
3107 # print only one empty line
3108 # do not print empty line after signoff
3110 next if ($empty || $signoff);
3116 print format_log_line_html
($line) . "<br/>\n";
3119 if ($opts{'-final_empty_line'}) {
3120 # end with single empty line
3121 print "<br/>\n" unless $empty;
3125 # return link target (what link points to)
3126 sub git_get_link_target
{
3131 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
3135 $link_target = <$fd>;
3140 return $link_target;
3143 # given link target, and the directory (basedir) the link is in,
3144 # return target of link relative to top directory (top tree);
3145 # return undef if it is not possible (including absolute links).
3146 sub normalize_link_target
{
3147 my ($link_target, $basedir, $hash_base) = @_;
3149 # we can normalize symlink target only if $hash_base is provided
3150 return unless $hash_base;
3152 # absolute symlinks (beginning with '/') cannot be normalized
3153 return if (substr($link_target, 0, 1) eq '/');
3155 # normalize link target to path from top (root) tree (dir)
3158 $path = $basedir . '/' . $link_target;
3160 # we are in top (root) tree (dir)
3161 $path = $link_target;
3164 # remove //, /./, and /../
3166 foreach my $part (split('/', $path)) {
3167 # discard '.' and ''
3168 next if (!$part || $part eq '.');
3170 if ($part eq '..') {
3174 # link leads outside repository (outside top dir)
3178 push @path_parts, $part;
3181 $path = join('/', @path_parts);
3186 # print tree entry (row of git_tree), but without encompassing <tr> element
3187 sub git_print_tree_entry
{
3188 my ($t, $basedir, $hash_base, $have_blame) = @_;
3191 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3193 # The format of a table row is: mode list link. Where mode is
3194 # the mode of the entry, list is the name of the entry, an href,
3195 # and link is the action links of the entry.
3197 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3198 if ($t->{'type'} eq "blob") {
3199 print "<td class=\"list\">" .
3200 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3201 file_name
=>"$basedir$t->{'name'}", %base_key),
3202 -class => "list"}, esc_path
($t->{'name'}));
3203 if (S_ISLNK
(oct $t->{'mode'})) {
3204 my $link_target = git_get_link_target
($t->{'hash'});
3206 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3207 if (defined $norm_target) {
3209 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3210 file_name
=>$norm_target),
3211 -title
=> $norm_target}, esc_path
($link_target));
3213 print " -> " . esc_path
($link_target);
3218 print "<td class=\"link\">";
3219 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3220 file_name
=>"$basedir$t->{'name'}", %base_key)},
3224 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3225 file_name
=>"$basedir$t->{'name'}", %base_key)},
3228 if (defined $hash_base) {
3230 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3231 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3235 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3236 file_name
=>"$basedir$t->{'name'}")},
3240 } elsif ($t->{'type'} eq "tree") {
3241 print "<td class=\"list\">";
3242 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3243 file_name
=>"$basedir$t->{'name'}", %base_key)},
3244 esc_path
($t->{'name'}));
3246 print "<td class=\"link\">";
3247 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3248 file_name
=>"$basedir$t->{'name'}", %base_key)},
3250 if (defined $hash_base) {
3252 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3253 file_name
=>"$basedir$t->{'name'}")},
3258 # unknown object: we can only present history for it
3259 # (this includes 'commit' object, i.e. submodule support)
3260 print "<td class=\"list\">" .
3261 esc_path
($t->{'name'}) .
3263 print "<td class=\"link\">";
3264 if (defined $hash_base) {
3265 print $cgi->a({-href
=> href
(action
=>"history",
3266 hash_base
=>$hash_base,
3267 file_name
=>"$basedir$t->{'name'}")},
3274 ## ......................................................................
3275 ## functions printing large fragments of HTML
3277 # get pre-image filenames for merge (combined) diff
3278 sub fill_from_file_info
{
3279 my ($diff, @parents) = @_;
3281 $diff->{'from_file'} = [ ];
3282 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3283 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3284 if ($diff->{'status'}[$i] eq 'R' ||
3285 $diff->{'status'}[$i] eq 'C') {
3286 $diff->{'from_file'}[$i] =
3287 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3294 # is current raw difftree line of file deletion
3296 my $diffinfo = shift;
3298 return $diffinfo->{'to_id'} eq ('0' x
40);
3301 # does patch correspond to [previous] difftree raw line
3302 # $diffinfo - hashref of parsed raw diff format
3303 # $patchinfo - hashref of parsed patch diff format
3304 # (the same keys as in $diffinfo)
3305 sub is_patch_split
{
3306 my ($diffinfo, $patchinfo) = @_;
3308 return defined $diffinfo && defined $patchinfo
3309 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3313 sub git_difftree_body
{
3314 my ($difftree, $hash, @parents) = @_;
3315 my ($parent) = $parents[0];
3316 my ($have_blame) = gitweb_check_feature
('blame');
3317 print "<div class=\"list_head\">\n";
3318 if ($#{$difftree} > 10) {
3319 print(($#{$difftree} + 1) . " files changed:\n");
3323 print "<table class=\"" .
3324 (@parents > 1 ? "combined " : "") .
3327 # header only for combined diff in 'commitdiff' view
3328 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3331 print "<thead><tr>\n" .
3332 "<th></th><th></th>\n"; # filename, patchN link
3333 for (my $i = 0; $i < @parents; $i++) {
3334 my $par = $parents[$i];
3336 $cgi->a({-href
=> href
(action
=>"commitdiff",
3337 hash
=>$hash, hash_parent
=>$par),
3338 -title
=> 'commitdiff to parent number ' .
3339 ($i+1) . ': ' . substr($par,0,7)},
3343 print "</tr></thead>\n<tbody>\n";
3348 foreach my $line (@{$difftree}) {
3349 my $diff = parsed_difftree_line
($line);
3352 print "<tr class=\"dark\">\n";
3354 print "<tr class=\"light\">\n";
3358 if (exists $diff->{'nparents'}) { # combined diff
3360 fill_from_file_info
($diff, @parents)
3361 unless exists $diff->{'from_file'};
3363 if (!is_deleted
($diff)) {
3364 # file exists in the result (child) commit
3366 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3367 file_name
=>$diff->{'to_file'},
3369 -class => "list"}, esc_path
($diff->{'to_file'})) .
3373 esc_path
($diff->{'to_file'}) .
3377 if ($action eq 'commitdiff') {
3380 print "<td class=\"link\">" .
3381 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3386 my $has_history = 0;
3387 my $not_deleted = 0;
3388 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3389 my $hash_parent = $parents[$i];
3390 my $from_hash = $diff->{'from_id'}[$i];
3391 my $from_path = $diff->{'from_file'}[$i];
3392 my $status = $diff->{'status'}[$i];
3394 $has_history ||= ($status ne 'A');
3395 $not_deleted ||= ($status ne 'D');
3397 if ($status eq 'A') {
3398 print "<td class=\"link\" align=\"right\"> | </td>\n";
3399 } elsif ($status eq 'D') {
3400 print "<td class=\"link\">" .
3401 $cgi->a({-href
=> href
(action
=>"blob",
3404 file_name
=>$from_path)},
3408 if ($diff->{'to_id'} eq $from_hash) {
3409 print "<td class=\"link nochange\">";
3411 print "<td class=\"link\">";
3413 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3414 hash
=>$diff->{'to_id'},
3415 hash_parent
=>$from_hash,
3417 hash_parent_base
=>$hash_parent,
3418 file_name
=>$diff->{'to_file'},
3419 file_parent
=>$from_path)},
3425 print "<td class=\"link\">";
3427 print $cgi->a({-href
=> href
(action
=>"blob",
3428 hash
=>$diff->{'to_id'},
3429 file_name
=>$diff->{'to_file'},
3432 print " | " if ($has_history);
3435 print $cgi->a({-href
=> href
(action
=>"history",
3436 file_name
=>$diff->{'to_file'},
3443 next; # instead of 'else' clause, to avoid extra indent
3445 # else ordinary diff
3447 my ($to_mode_oct, $to_mode_str, $to_file_type);
3448 my ($from_mode_oct, $from_mode_str, $from_file_type);
3449 if ($diff->{'to_mode'} ne ('0' x
6)) {
3450 $to_mode_oct = oct $diff->{'to_mode'};
3451 if (S_ISREG
($to_mode_oct)) { # only for regular file
3452 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3454 $to_file_type = file_type
($diff->{'to_mode'});
3456 if ($diff->{'from_mode'} ne ('0' x
6)) {
3457 $from_mode_oct = oct $diff->{'from_mode'};
3458 if (S_ISREG
($to_mode_oct)) { # only for regular file
3459 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3461 $from_file_type = file_type
($diff->{'from_mode'});
3464 if ($diff->{'status'} eq "A") { # created
3465 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3466 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3467 $mode_chng .= "]</span>";
3469 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3470 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3471 -class => "list"}, esc_path
($diff->{'file'}));
3473 print "<td>$mode_chng</td>\n";
3474 print "<td class=\"link\">";
3475 if ($action eq 'commitdiff') {
3478 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3481 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3482 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3486 } elsif ($diff->{'status'} eq "D") { # deleted
3487 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3489 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3490 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3491 -class => "list"}, esc_path
($diff->{'file'}));
3493 print "<td>$mode_chng</td>\n";
3494 print "<td class=\"link\">";
3495 if ($action eq 'commitdiff') {
3498 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3501 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3502 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3505 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3506 file_name
=>$diff->{'file'})},
3509 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3510 file_name
=>$diff->{'file'})},
3514 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3515 my $mode_chnge = "";
3516 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3517 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3518 if ($from_file_type ne $to_file_type) {
3519 $mode_chnge .= " from $from_file_type to $to_file_type";
3521 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3522 if ($from_mode_str && $to_mode_str) {
3523 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3524 } elsif ($to_mode_str) {
3525 $mode_chnge .= " mode: $to_mode_str";
3528 $mode_chnge .= "]</span>\n";
3531 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3532 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3533 -class => "list"}, esc_path
($diff->{'file'}));
3535 print "<td>$mode_chnge</td>\n";
3536 print "<td class=\"link\">";
3537 if ($action eq 'commitdiff') {
3540 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3542 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3543 # "commit" view and modified file (not onlu mode changed)
3544 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3545 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3546 hash_base
=>$hash, hash_parent_base
=>$parent,
3547 file_name
=>$diff->{'file'})},
3551 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3552 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3555 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3556 file_name
=>$diff->{'file'})},
3559 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3560 file_name
=>$diff->{'file'})},
3564 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3565 my %status_name = ('R' => 'moved', 'C' => 'copied');
3566 my $nstatus = $status_name{$diff->{'status'}};
3568 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3569 # mode also for directories, so we cannot use $to_mode_str
3570 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3573 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3574 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3575 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3576 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3577 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3578 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3579 -class => "list"}, esc_path
($diff->{'from_file'})) .
3580 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3581 "<td class=\"link\">";
3582 if ($action eq 'commitdiff') {
3585 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3587 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3588 # "commit" view and modified file (not only pure rename or copy)
3589 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3590 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3591 hash_base
=>$hash, hash_parent_base
=>$parent,
3592 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3596 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3597 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3600 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3601 file_name
=>$diff->{'to_file'})},
3604 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3605 file_name
=>$diff->{'to_file'})},
3609 } # we should not encounter Unmerged (U) or Unknown (X) status
3612 print "</tbody>" if $has_header;
3616 sub git_patchset_body
{
3617 my ($fd, $difftree, $hash, @hash_parents) = @_;
3618 my ($hash_parent) = $hash_parents[0];
3620 my $is_combined = (@hash_parents > 1);
3622 my $patch_number = 0;
3628 print "<div class=\"patchset\">\n";
3630 # skip to first patch
3631 while ($patch_line = <$fd>) {
3634 last if ($patch_line =~ m/^diff /);
3638 while ($patch_line) {
3640 # parse "git diff" header line
3641 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3642 # $1 is from_name, which we do not use
3643 $to_name = unquote
($2);
3644 $to_name =~ s!^b/!!;
3645 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3646 # $1 is 'cc' or 'combined', which we do not use
3647 $to_name = unquote
($2);
3652 # check if current patch belong to current raw line
3653 # and parse raw git-diff line if needed
3654 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3655 # this is continuation of a split patch
3656 print "<div class=\"patch cont\">\n";
3658 # advance raw git-diff output if needed
3659 $patch_idx++ if defined $diffinfo;
3661 # read and prepare patch information
3662 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3664 # compact combined diff output can have some patches skipped
3665 # find which patch (using pathname of result) we are at now;
3667 while ($to_name ne $diffinfo->{'to_file'}) {
3668 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3669 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3670 "</div>\n"; # class="patch"
3675 last if $patch_idx > $#$difftree;
3676 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3680 # modifies %from, %to hashes
3681 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3683 # this is first patch for raw difftree line with $patch_idx index
3684 # we index @$difftree array from 0, but number patches from 1
3685 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3689 #assert($patch_line =~ m/^diff /) if DEBUG;
3690 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3692 # print "git diff" header
3693 print format_git_diff_header_line
($patch_line, $diffinfo,
3696 # print extended diff header
3697 print "<div class=\"diff extended_header\">\n";
3699 while ($patch_line = <$fd>) {
3702 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3704 print format_extended_diff_header_line
($patch_line, $diffinfo,
3707 print "</div>\n"; # class="diff extended_header"
3709 # from-file/to-file diff header
3710 if (! $patch_line) {
3711 print "</div>\n"; # class="patch"
3714 next PATCH
if ($patch_line =~ m/^diff /);
3715 #assert($patch_line =~ m/^---/) if DEBUG;
3717 my $last_patch_line = $patch_line;
3718 $patch_line = <$fd>;
3720 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3722 print format_diff_from_to_header
($last_patch_line, $patch_line,
3723 $diffinfo, \
%from, \
%to,
3728 while ($patch_line = <$fd>) {
3731 next PATCH
if ($patch_line =~ m/^diff /);
3733 print format_diff_line
($patch_line, \
%from, \
%to);
3737 print "</div>\n"; # class="patch"
3740 # for compact combined (--cc) format, with chunk and patch simpliciaction
3741 # patchset might be empty, but there might be unprocessed raw lines
3742 for (++$patch_idx if $patch_number > 0;
3743 $patch_idx < @$difftree;
3745 # read and prepare patch information
3746 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3748 # generate anchor for "patch" links in difftree / whatchanged part
3749 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3750 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3751 "</div>\n"; # class="patch"
3756 if ($patch_number == 0) {
3757 if (@hash_parents > 1) {
3758 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3760 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3764 print "</div>\n"; # class="patchset"
3767 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3769 # fills project list info (age, description, owner, forks) for each
3770 # project in the list, removing invalid projects from returned list
3771 # NOTE: modifies $projlist, but does not remove entries from it
3772 sub fill_project_list_info
{
3773 my ($projlist, $check_forks) = @_;
3776 my $show_ctags = gitweb_check_feature
('ctags');
3778 foreach my $pr (@$projlist) {
3779 my (@activity) = git_get_last_activity
($pr->{'path'});
3780 unless (@activity) {
3783 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3784 if (!defined $pr->{'descr'}) {
3785 my $descr = git_get_project_description
($pr->{'path'}) || "";
3786 $descr = to_utf8
($descr);
3787 $pr->{'descr_long'} = $descr;
3788 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3790 if (!defined $pr->{'owner'}) {
3791 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3794 my $pname = $pr->{'path'};
3795 if (($pname =~ s/\.git$//) &&
3796 ($pname !~ /\/$/) &&
3797 (-d
"$projectroot/$pname")) {
3798 $pr->{'forks'} = "-d $projectroot/$pname";
3803 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
3804 push @projects, $pr;
3810 # print 'sort by' <th> element, generating 'sort by $name' replay link
3811 # if that order is not selected
3813 my ($name, $order, $header) = @_;
3814 $header ||= ucfirst($name);
3816 if ($order eq $name) {
3817 print "<th>$header</th>\n";
3820 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3821 -class => "header"}, $header) .
3826 sub git_project_list_body
{
3827 # actually uses global variable $project
3828 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3830 my ($check_forks) = gitweb_check_feature
('forks');
3831 my @projects = fill_project_list_info
($projlist, $check_forks);
3833 $order ||= $default_projects_order;
3834 $from = 0 unless defined $from;
3835 $to = $#projects if (!defined $to || $#projects < $to);
3838 project
=> { key
=> 'path', type
=> 'str' },
3839 descr
=> { key
=> 'descr_long', type
=> 'str' },
3840 owner
=> { key
=> 'owner', type
=> 'str' },
3841 age
=> { key
=> 'age', type
=> 'num' }
3843 my $oi = $order_info{$order};
3844 if ($oi->{'type'} eq 'str') {
3845 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3847 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3850 my $show_ctags = gitweb_check_feature
('ctags');
3853 foreach my $p (@projects) {
3854 foreach my $ct (keys %{$p->{'ctags'}}) {
3855 $ctags{$ct} += $p->{'ctags'}->{$ct};
3858 my $cloud = git_populate_project_tagcloud
(\
%ctags);
3859 print git_show_project_tagcloud
($cloud, 64);
3862 print "<table class=\"project_list\">\n";
3863 unless ($no_header) {
3866 print "<th></th>\n";
3868 print_sort_th
('project', $order, 'Project');
3869 print_sort_th
('descr', $order, 'Description');
3870 print_sort_th
('owner', $order, 'Owner');
3871 print_sort_th
('age', $order, 'Last Change');
3872 print "<th></th>\n" . # for links
3876 my $tagfilter = $cgi->param('by_tag');
3877 for (my $i = $from; $i <= $to; $i++) {
3878 my $pr = $projects[$i];
3880 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3881 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3882 and not $pr->{'descr_long'} =~ /$searchtext/;
3883 # Weed out forks or non-matching entries of search
3885 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s
#\.git$#/#;
3886 $forkbase="^$forkbase" if $forkbase;
3887 next if not $searchtext and not $tagfilter and $show_ctags
3888 and $pr->{'path'} =~ m
#$forkbase.*/.*#; # regexp-safe
3892 print "<tr class=\"dark\">\n";
3894 print "<tr class=\"light\">\n";
3899 if ($pr->{'forks'}) {
3900 print "<!-- $pr->{'forks'} -->\n";
3901 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3905 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3906 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3907 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3908 -class => "list", -title
=> $pr->{'descr_long'}},
3909 esc_html
($pr->{'descr'})) . "</td>\n" .
3910 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3911 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3912 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3913 "<td class=\"link\">" .
3914 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3915 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3916 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3917 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3918 ($pr->{'forks'} ? " | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3922 if (defined $extra) {
3925 print "<td></td>\n";
3927 print "<td colspan=\"5\">$extra</td>\n" .
3933 sub git_shortlog_body
{
3934 # uses global variable $project
3935 my ($commitlist, $from, $to, $refs, $extra) = @_;
3937 $from = 0 unless defined $from;
3938 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3940 print "<table class=\"shortlog\">\n";
3942 for (my $i = $from; $i <= $to; $i++) {
3943 my %co = %{$commitlist->[$i]};
3944 my $commit = $co{'id'};
3945 my $ref = format_ref_marker
($refs, $commit);
3947 print "<tr class=\"dark\">\n";
3949 print "<tr class=\"light\">\n";
3952 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3953 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3954 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3955 "<td><i>" . $author . "</i></td>\n" .
3957 print format_subject_html
($co{'title'}, $co{'title_short'},
3958 href
(action
=>"commit", hash
=>$commit), $ref);
3960 "<td class=\"link\">" .
3961 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3962 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3963 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3964 my $snapshot_links = format_snapshot_links
($commit);
3965 if (defined $snapshot_links) {
3966 print " | " . $snapshot_links;
3971 if (defined $extra) {
3973 "<td colspan=\"4\">$extra</td>\n" .
3979 sub git_history_body
{
3980 # Warning: assumes constant type (blob or tree) during history
3981 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3983 $from = 0 unless defined $from;
3984 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3986 print "<table class=\"history\">\n";
3988 for (my $i = $from; $i <= $to; $i++) {
3989 my %co = %{$commitlist->[$i]};
3993 my $commit = $co{'id'};
3995 my $ref = format_ref_marker
($refs, $commit);
3998 print "<tr class=\"dark\">\n";
4000 print "<tr class=\"light\">\n";
4003 # shortlog uses chop_str($co{'author_name'}, 10)
4004 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
4005 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4006 "<td><i>" . $author . "</i></td>\n" .
4008 # originally git_history used chop_str($co{'title'}, 50)
4009 print format_subject_html
($co{'title'}, $co{'title_short'},
4010 href
(action
=>"commit", hash
=>$commit), $ref);
4012 "<td class=\"link\">" .
4013 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
4014 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
4016 if ($ftype eq 'blob') {
4017 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
4018 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
4019 if (defined $blob_current && defined $blob_parent &&
4020 $blob_current ne $blob_parent) {
4022 $cgi->a({-href
=> href
(action
=>"blobdiff",
4023 hash
=>$blob_current, hash_parent
=>$blob_parent,
4024 hash_base
=>$hash_base, hash_parent_base
=>$commit,
4025 file_name
=>$file_name)},
4032 if (defined $extra) {
4034 "<td colspan=\"4\">$extra</td>\n" .
4041 # uses global variable $project
4042 my ($taglist, $from, $to, $extra) = @_;
4043 $from = 0 unless defined $from;
4044 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4046 print "<table class=\"tags\">\n";
4048 for (my $i = $from; $i <= $to; $i++) {
4049 my $entry = $taglist->[$i];
4051 my $comment = $tag{'subject'};
4053 if (defined $comment) {
4054 $comment_short = chop_str
($comment, 30, 5);
4057 print "<tr class=\"dark\">\n";
4059 print "<tr class=\"light\">\n";
4062 if (defined $tag{'age'}) {
4063 print "<td><i>$tag{'age'}</i></td>\n";
4065 print "<td></td>\n";
4068 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
4069 -class => "list name"}, esc_html
($tag{'name'})) .
4072 if (defined $comment) {
4073 print format_subject_html
($comment, $comment_short,
4074 href
(action
=>"tag", hash
=>$tag{'id'}));
4077 "<td class=\"selflink\">";
4078 if ($tag{'type'} eq "tag") {
4079 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
4084 "<td class=\"link\">" . " | " .
4085 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
4086 if ($tag{'reftype'} eq "commit") {
4087 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
4088 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
4089 } elsif ($tag{'reftype'} eq "blob") {
4090 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
4095 if (defined $extra) {
4097 "<td colspan=\"5\">$extra</td>\n" .
4103 sub git_heads_body
{
4104 # uses global variable $project
4105 my ($headlist, $head, $from, $to, $extra) = @_;
4106 $from = 0 unless defined $from;
4107 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4109 print "<table class=\"heads\">\n";
4111 for (my $i = $from; $i <= $to; $i++) {
4112 my $entry = $headlist->[$i];
4114 my $curr = $ref{'id'} eq $head;
4116 print "<tr class=\"dark\">\n";
4118 print "<tr class=\"light\">\n";
4121 print "<td><i>$ref{'age'}</i></td>\n" .
4122 ($curr ? "<td class=\"current_head\">" : "<td>") .
4123 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
4124 -class => "list name"},esc_html
($ref{'name'})) .
4126 "<td class=\"link\">" .
4127 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
4128 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
4129 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
4133 if (defined $extra) {
4135 "<td colspan=\"3\">$extra</td>\n" .
4141 sub git_search_grep_body
{
4142 my ($commitlist, $from, $to, $extra) = @_;
4143 $from = 0 unless defined $from;
4144 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4146 print "<table class=\"commit_search\">\n";
4148 for (my $i = $from; $i <= $to; $i++) {
4149 my %co = %{$commitlist->[$i]};
4153 my $commit = $co{'id'};
4155 print "<tr class=\"dark\">\n";
4157 print "<tr class=\"light\">\n";
4160 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
4161 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4162 "<td><i>" . $author . "</i></td>\n" .
4164 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
4165 -class => "list subject"},
4166 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
4167 my $comment = $co{'comment'};
4168 foreach my $line (@$comment) {
4169 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4170 my ($lead, $match, $trail) = ($1, $2, $3);
4171 $match = chop_str
($match, 70, 5, 'center');
4172 my $contextlen = int((80 - length($match))/2);
4173 $contextlen = 30 if ($contextlen > 30);
4174 $lead = chop_str
($lead, $contextlen, 10, 'left');
4175 $trail = chop_str
($trail, $contextlen, 10, 'right');
4177 $lead = esc_html
($lead);
4178 $match = esc_html
($match);
4179 $trail = esc_html
($trail);
4181 print "$lead<span class=\"match\">$match</span>$trail<br />";
4185 "<td class=\"link\">" .
4186 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4188 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4190 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4194 if (defined $extra) {
4196 "<td colspan=\"3\">$extra</td>\n" .
4202 ## ======================================================================
4203 ## ======================================================================
4206 sub git_project_list
{
4207 my $order = $input_params{'order'};
4208 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4209 die_error
(400, "Unknown order parameter");
4212 my @list = git_get_projects_list
();
4214 die_error
(404, "No projects found");
4218 if (-f
$home_text) {
4219 print "<div class=\"index_include\">\n";
4220 open (my $fd, $home_text);
4225 print $cgi->startform(-method => "get") .
4226 "<p class=\"projsearch\">Search:\n" .
4227 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
4229 $cgi->end_form() . "\n";
4230 git_project_list_body
(\
@list, $order);
4235 my $order = $input_params{'order'};
4236 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4237 die_error
(400, "Unknown order parameter");
4240 my @list = git_get_projects_list
($project);
4242 die_error
(404, "No forks found");
4246 git_print_page_nav
('','');
4247 git_print_header_div
('summary', "$project forks");
4248 git_project_list_body
(\
@list, $order);
4252 sub git_project_index
{
4253 my @projects = git_get_projects_list
($project);
4256 -type
=> 'text/plain',
4257 -charset
=> 'utf-8',
4258 -content_disposition
=> 'inline; filename="index.aux"');
4260 foreach my $pr (@projects) {
4261 if (!exists $pr->{'owner'}) {
4262 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4265 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4266 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4267 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4268 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4272 print "$path $owner\n";
4277 my $descr = git_get_project_description
($project) || "none";
4278 my %co = parse_commit
("HEAD");
4279 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4280 my $head = $co{'id'};
4282 my $owner = git_get_project_owner
($project);
4284 my $refs = git_get_references
();
4285 # These get_*_list functions return one more to allow us to see if
4286 # there are more ...
4287 my @taglist = git_get_tags_list
(16);
4288 my @headlist = git_get_heads_list
(16);
4290 my ($check_forks) = gitweb_check_feature
('forks');
4293 @forklist = git_get_projects_list
($project);
4297 git_print_page_nav
('summary','', $head);
4299 print "<div class=\"title\"> </div>\n";
4300 print "<table class=\"projects_list\">\n" .
4301 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4302 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4303 if (defined $cd{'rfc2822'}) {
4304 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4307 # use per project git URL list in $projectroot/$project/cloneurl
4308 # or make project git URL from git base URL and project name
4309 my $url_tag = "URL";
4310 my @url_list = git_get_project_url_list
($project);
4311 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4312 foreach my $git_url (@url_list) {
4313 next unless $git_url;
4314 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4319 my $show_ctags = (gitweb_check_feature
('ctags'))[0];
4321 my $ctags = git_get_project_ctags
($project);
4322 my $cloud = git_populate_project_tagcloud
($ctags);
4323 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4324 print "</td>\n<td>" unless %$ctags;
4325 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4326 print "</td>\n<td>" if %$ctags;
4327 print git_show_project_tagcloud
($cloud, 48);
4333 if (-s
"$projectroot/$project/README.html") {
4334 if (open my $fd, "$projectroot/$project/README.html") {
4335 print "<div class=\"title\">readme</div>\n" .
4336 "<div class=\"readme\">\n";
4337 print $_ while (<$fd>);
4338 print "\n</div>\n"; # class="readme"
4343 # we need to request one more than 16 (0..15) to check if
4345 my @commitlist = $head ? parse_commits
($head, 17) : ();
4347 git_print_header_div
('shortlog');
4348 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4349 $#commitlist <= 15 ? undef :
4350 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4354 git_print_header_div
('tags');
4355 git_tags_body
(\
@taglist, 0, 15,
4356 $#taglist <= 15 ? undef :
4357 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4361 git_print_header_div
('heads');
4362 git_heads_body
(\
@headlist, $head, 0, 15,
4363 $#headlist <= 15 ? undef :
4364 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4368 git_print_header_div
('forks');
4369 git_project_list_body
(\
@forklist, 'age', 0, 15,
4370 $#forklist <= 15 ? undef :
4371 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4379 my $head = git_get_head_hash
($project);
4381 git_print_page_nav
('','', $head,undef,$head);
4382 my %tag = parse_tag
($hash);
4385 die_error
(404, "Unknown tag object");
4388 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4389 print "<div class=\"title_text\">\n" .
4390 "<table class=\"object_header\">\n" .
4392 "<td>object</td>\n" .
4393 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4394 $tag{'object'}) . "</td>\n" .
4395 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4396 $tag{'type'}) . "</td>\n" .
4398 if (defined($tag{'author'})) {
4399 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4400 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4401 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4402 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4405 print "</table>\n\n" .
4407 print "<div class=\"page_body\">";
4408 my $comment = $tag{'comment'};
4409 foreach my $line (@$comment) {
4411 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4421 gitweb_check_feature
('blame')
4422 or die_error
(403, "Blame view not allowed");
4424 die_error
(400, "No file name given") unless $file_name;
4425 $hash_base ||= git_get_head_hash
($project);
4426 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4427 my %co = parse_commit
($hash_base)
4428 or die_error
(404, "Commit not found");
4429 if (!defined $hash) {
4430 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4431 or die_error
(404, "Error looking up file");
4433 $ftype = git_get_type
($hash);
4434 if ($ftype !~ "blob") {
4435 die_error
(400, "Object is not a blob");
4437 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4438 $file_name, $hash_base)
4439 or die_error
(500, "Open git-blame failed");
4442 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4445 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4448 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4450 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4451 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4452 git_print_page_path
($file_name, $ftype, $hash_base);
4453 my @rev_color = (qw(light2 dark2));
4454 my $num_colors = scalar(@rev_color);
4455 my $current_color = 0;
4458 <div class="page_body">
4459 <table class="blame">
4460 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4465 last unless defined $_;
4466 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4467 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4468 if (!exists $metainfo{$full_rev}) {
4469 $metainfo{$full_rev} = {};
4471 my $meta = $metainfo{$full_rev};
4474 if (/^(\S+) (.*)$/) {
4480 my $rev = substr($full_rev, 0, 8);
4481 my $author = $meta->{'author'};
4482 my %date = parse_date
($meta->{'author-time'},
4483 $meta->{'author-tz'});
4484 my $date = $date{'iso-tz'};
4486 $current_color = ++$current_color % $num_colors;
4488 print "<tr class=\"$rev_color[$current_color]\">\n";
4490 print "<td class=\"sha1\"";
4491 print " title=\"". esc_html
($author) . ", $date\"";
4492 print " rowspan=\"$group_size\"" if ($group_size > 1);
4494 print $cgi->a({-href
=> href
(action
=>"commit",
4496 file_name
=>$file_name)},
4500 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4501 or die_error
(500, "Open git-rev-parse failed");
4502 my $parent_commit = <$dd>;
4504 chomp($parent_commit);
4505 my $blamed = href
(action
=> 'blame',
4506 file_name
=> $meta->{'filename'},
4507 hash_base
=> $parent_commit);
4508 print "<td class=\"linenr\">";
4509 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4511 -class => "linenr" },
4514 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4520 or print "Reading blob failed\n";
4525 my $head = git_get_head_hash
($project);
4527 git_print_page_nav
('','', $head,undef,$head);
4528 git_print_header_div
('summary', $project);
4530 my @tagslist = git_get_tags_list
();
4532 git_tags_body
(\
@tagslist);
4538 my $head = git_get_head_hash
($project);
4540 git_print_page_nav
('','', $head,undef,$head);
4541 git_print_header_div
('summary', $project);
4543 my @headslist = git_get_heads_list
();
4545 git_heads_body
(\
@headslist, $head);
4550 sub git_blob_plain
{
4554 if (!defined $hash) {
4555 if (defined $file_name) {
4556 my $base = $hash_base || git_get_head_hash
($project);
4557 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4558 or die_error
(404, "Cannot find file");
4560 die_error
(400, "No file name defined");
4562 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4563 # blobs defined by non-textual hash id's can be cached
4567 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4568 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4570 # content-type (can include charset)
4571 $type = blob_contenttype
($fd, $file_name, $type);
4573 # "save as" filename, even when no $file_name is given
4574 my $save_as = "$hash";
4575 if (defined $file_name) {
4576 $save_as = $file_name;
4577 } elsif ($type =~ m/^text\//) {
4583 -expires
=> $expires,
4584 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4586 binmode STDOUT
, ':raw';
4588 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4596 if (!defined $hash) {
4597 if (defined $file_name) {
4598 my $base = $hash_base || git_get_head_hash
($project);
4599 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4600 or die_error
(404, "Cannot find file");
4602 die_error
(400, "No file name defined");
4604 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4605 # blobs defined by non-textual hash id's can be cached
4609 my ($have_blame) = gitweb_check_feature
('blame');
4610 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4611 or die_error
(500, "Couldn't cat $file_name, $hash");
4612 my $mimetype = blob_mimetype
($fd, $file_name);
4613 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4615 return git_blob_plain
($mimetype);
4617 # we can have blame only for text/* mimetype
4618 $have_blame &&= ($mimetype =~ m!^text/!);
4620 git_header_html
(undef, $expires);
4621 my $formats_nav = '';
4622 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4623 if (defined $file_name) {
4626 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4631 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4634 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4637 $cgi->a({-href
=> href
(action
=>"blob",
4638 hash_base
=>"HEAD", file_name
=>$file_name)},
4642 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4645 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4646 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4648 print "<div class=\"page_nav\">\n" .
4649 "<br/><br/></div>\n" .
4650 "<div class=\"title\">$hash</div>\n";
4652 git_print_page_path
($file_name, "blob", $hash_base);
4653 print "<div class=\"page_body\">\n";
4654 if ($mimetype =~ m!^image/!) {
4655 print qq
!<img type
="$mimetype"!;
4657 print qq
! alt
="$file_name" title
="$file_name"!;
4660 href(action=>"blob_plain
", hash=>$hash,
4661 hash_base=>$hash_base, file_name=>$file_name) .
4665 while (my $line = <$fd>) {
4668 $line = untabify
($line);
4669 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4670 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4674 or print "Reading blob failed.\n";
4680 if (!defined $hash_base) {
4681 $hash_base = "HEAD";
4683 if (!defined $hash) {
4684 if (defined $file_name) {
4685 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4690 die_error
(404, "No such tree") unless defined($hash);
4692 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4693 or die_error
(500, "Open git-ls-tree failed");
4694 my @entries = map { chomp; $_ } <$fd>;
4695 close $fd or die_error
(404, "Reading tree failed");
4698 my $refs = git_get_references
();
4699 my $ref = format_ref_marker
($refs, $hash_base);
4702 my ($have_blame) = gitweb_check_feature
('blame');
4703 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4705 if (defined $file_name) {
4707 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4709 $cgi->a({-href
=> href
(action
=>"tree",
4710 hash_base
=>"HEAD", file_name
=>$file_name)},
4713 my $snapshot_links = format_snapshot_links
($hash);
4714 if (defined $snapshot_links) {
4715 # FIXME: Should be available when we have no hash base as well.
4716 push @views_nav, $snapshot_links;
4718 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4719 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4722 print "<div class=\"page_nav\">\n";
4723 print "<br/><br/></div>\n";
4724 print "<div class=\"title\">$hash</div>\n";
4726 if (defined $file_name) {
4727 $basedir = $file_name;
4728 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4731 git_print_page_path
($file_name, 'tree', $hash_base);
4733 print "<div class=\"page_body\">\n";
4734 print "<table class=\"tree\">\n";
4736 # '..' (top directory) link if possible
4737 if (defined $hash_base &&
4738 defined $file_name && $file_name =~ m![^/]+$!) {
4740 print "<tr class=\"dark\">\n";
4742 print "<tr class=\"light\">\n";
4746 my $up = $file_name;
4747 $up =~ s!/?[^/]+$!!;
4748 undef $up unless $up;
4749 # based on git_print_tree_entry
4750 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4751 print '<td class="list">';
4752 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4756 print "<td class=\"link\"></td>\n";
4760 foreach my $line (@entries) {
4761 my %t = parse_ls_tree_line
($line, -z
=> 1);
4764 print "<tr class=\"dark\">\n";
4766 print "<tr class=\"light\">\n";
4770 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4774 print "</table>\n" .
4780 my @supported_fmts = gitweb_check_feature
('snapshot');
4781 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4783 my $format = $input_params{'snapshot_format'};
4784 if (!@supported_fmts) {
4785 die_error
(403, "Snapshots not allowed");
4787 # default to first supported snapshot format
4788 $format ||= $supported_fmts[0];
4789 if ($format !~ m/^[a-z0-9]+$/) {
4790 die_error
(400, "Invalid snapshot format parameter");
4791 } elsif (!exists($known_snapshot_formats{$format})) {
4792 die_error
(400, "Unknown snapshot format");
4793 } elsif (!grep($_ eq $format, @supported_fmts)) {
4794 die_error
(403, "Unsupported snapshot format");
4797 if (!defined $hash) {
4798 $hash = git_get_head_hash
($project);
4801 my $name = $project;
4802 $name =~ s
,([^/])/*\
.git
$,$1,;
4803 $name = basename
($name);
4804 my $filename = to_utf8
($name);
4805 $name =~ s/\047/\047\\\047\047/g;
4807 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4808 $cmd = quote_command
(
4809 git_cmd
(), 'archive',
4810 "--format=$known_snapshot_formats{$format}{'format'}",
4811 "--prefix=$name/", $hash);
4812 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4813 $cmd .= ' | ' . quote_command
(@{$known_snapshot_formats{$format}{'compressor'}});
4817 -type
=> $known_snapshot_formats{$format}{'type'},
4818 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4819 -status
=> '200 OK');
4821 open my $fd, "-|", $cmd
4822 or die_error
(500, "Execute git-archive failed");
4823 binmode STDOUT
, ':raw';
4825 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4830 my $head = git_get_head_hash
($project);
4831 if (!defined $hash) {
4834 if (!defined $page) {
4837 my $refs = git_get_references
();
4839 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4841 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4844 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4847 my %co = parse_commit
($hash);
4849 git_print_header_div
('summary', $project);
4850 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4852 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4853 for (my $i = 0; $i <= $to; $i++) {
4854 my %co = %{$commitlist[$i]};
4856 my $commit = $co{'id'};
4857 my $ref = format_ref_marker
($refs, $commit);
4858 my %ad = parse_date
($co{'author_epoch'});
4859 git_print_header_div
('commit',
4860 "<span class=\"age\">$co{'age_string'}</span>" .
4861 esc_html
($co{'title'}) . $ref,
4863 print "<div class=\"title_text\">\n" .
4864 "<div class=\"log_link\">\n" .
4865 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4867 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4869 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4872 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4875 print "<div class=\"log_body\">\n";
4876 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4879 if ($#commitlist >= 100) {
4880 print "<div class=\"page_nav\">\n";
4881 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4882 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4889 $hash ||= $hash_base || "HEAD";
4890 my %co = parse_commit
($hash)
4891 or die_error
(404, "Unknown commit object");
4892 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4893 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4895 my $parent = $co{'parent'};
4896 my $parents = $co{'parents'}; # listref
4898 # we need to prepare $formats_nav before any parameter munging
4900 if (!defined $parent) {
4902 $formats_nav .= '(initial)';
4903 } elsif (@$parents == 1) {
4904 # single parent commit
4907 $cgi->a({-href
=> href
(action
=>"commit",
4909 esc_html
(substr($parent, 0, 7))) .
4916 $cgi->a({-href
=> href
(action
=>"commit",
4918 esc_html
(substr($_, 0, 7)));
4923 if (!defined $parent) {
4927 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4929 (@$parents <= 1 ? $parent : '-c'),
4931 or die_error
(500, "Open git-diff-tree failed");
4932 @difftree = map { chomp; $_ } <$fd>;
4933 close $fd or die_error
(404, "Reading git-diff-tree failed");
4935 # non-textual hash id's can be cached
4937 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4940 my $refs = git_get_references
();
4941 my $ref = format_ref_marker
($refs, $co{'id'});
4943 git_header_html
(undef, $expires);
4944 git_print_page_nav
('commit', '',
4945 $hash, $co{'tree'}, $hash,
4948 if (defined $co{'parent'}) {
4949 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4951 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4953 print "<div class=\"title_text\">\n" .
4954 "<table class=\"object_header\">\n";
4955 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4957 "<td></td><td> $ad{'rfc2822'}";
4958 if ($ad{'hour_local'} < 6) {
4959 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4960 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4962 printf(" (%02d:%02d %s)",
4963 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4967 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4968 print "<tr><td></td><td> $cd{'rfc2822'}" .
4969 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4971 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4974 "<td class=\"sha1\">" .
4975 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4976 class => "list"}, $co{'tree'}) .
4978 "<td class=\"link\">" .
4979 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4981 my $snapshot_links = format_snapshot_links
($hash);
4982 if (defined $snapshot_links) {
4983 print " | " . $snapshot_links;
4988 foreach my $par (@$parents) {
4991 "<td class=\"sha1\">" .
4992 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4993 class => "list"}, $par) .
4995 "<td class=\"link\">" .
4996 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4998 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
5005 print "<div class=\"page_body\">\n";
5006 git_print_log
($co{'comment'});
5009 git_difftree_body
(\
@difftree, $hash, @$parents);
5015 # object is defined by:
5016 # - hash or hash_base alone
5017 # - hash_base and file_name
5020 # - hash or hash_base alone
5021 if ($hash || ($hash_base && !defined $file_name)) {
5022 my $object_id = $hash || $hash_base;
5024 open my $fd, "-|", quote_command
(
5025 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5026 or die_error
(404, "Object does not exist");
5030 or die_error
(404, "Object does not exist");
5032 # - hash_base and file_name
5033 } elsif ($hash_base && defined $file_name) {
5034 $file_name =~ s
,/+$,,;
5036 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
5037 or die_error
(404, "Base object does not exist");
5039 # here errors should not hapen
5040 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
5041 or die_error
(500, "Open git-ls-tree failed");
5045 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5046 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5047 die_error
(404, "File or directory for given base does not exist");
5052 die_error
(400, "Not enough information to find object");
5055 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
5056 hash
=>$hash, hash_base
=>$hash_base,
5057 file_name
=>$file_name),
5058 -status
=> '302 Found');
5062 my $format = shift || 'html';
5069 # preparing $fd and %diffinfo for git_patchset_body
5071 if (defined $hash_base && defined $hash_parent_base) {
5072 if (defined $file_name) {
5074 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5075 $hash_parent_base, $hash_base,
5076 "--", (defined $file_parent ? $file_parent : ()), $file_name
5077 or die_error
(500, "Open git-diff-tree failed");
5078 @difftree = map { chomp; $_ } <$fd>;
5080 or die_error
(404, "Reading git-diff-tree failed");
5082 or die_error
(404, "Blob diff not found");
5084 } elsif (defined $hash &&
5085 $hash =~ /[0-9a-fA-F]{40}/) {
5086 # try to find filename from $hash
5088 # read filtered raw output
5089 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5090 $hash_parent_base, $hash_base, "--"
5091 or die_error
(500, "Open git-diff-tree failed");
5093 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5095 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5096 map { chomp; $_ } <$fd>;
5098 or die_error
(404, "Reading git-diff-tree failed");
5100 or die_error
(404, "Blob diff not found");
5103 die_error
(400, "Missing one of the blob diff parameters");
5106 if (@difftree > 1) {
5107 die_error
(400, "Ambiguous blob diff specification");
5110 %diffinfo = parse_difftree_raw_line
($difftree[0]);
5111 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5112 $file_name ||= $diffinfo{'to_file'};
5114 $hash_parent ||= $diffinfo{'from_id'};
5115 $hash ||= $diffinfo{'to_id'};
5117 # non-textual hash id's can be cached
5118 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5119 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5124 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5125 '-p', ($format eq 'html' ? "--full-index" : ()),
5126 $hash_parent_base, $hash_base,
5127 "--", (defined $file_parent ? $file_parent : ()), $file_name
5128 or die_error
(500, "Open git-diff-tree failed");
5131 # old/legacy style URI
5132 if (!%diffinfo && # if new style URI failed
5133 defined $hash && defined $hash_parent) {
5134 # fake git-diff-tree raw output
5135 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5136 $diffinfo{'from_id'} = $hash_parent;
5137 $diffinfo{'to_id'} = $hash;
5138 if (defined $file_name) {
5139 if (defined $file_parent) {
5140 $diffinfo{'status'} = '2';
5141 $diffinfo{'from_file'} = $file_parent;
5142 $diffinfo{'to_file'} = $file_name;
5143 } else { # assume not renamed
5144 $diffinfo{'status'} = '1';
5145 $diffinfo{'from_file'} = $file_name;
5146 $diffinfo{'to_file'} = $file_name;
5148 } else { # no filename given
5149 $diffinfo{'status'} = '2';
5150 $diffinfo{'from_file'} = $hash_parent;
5151 $diffinfo{'to_file'} = $hash;
5154 # non-textual hash id's can be cached
5155 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5156 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5161 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
5162 '-p', ($format eq 'html' ? "--full-index" : ()),
5163 $hash_parent, $hash, "--"
5164 or die_error
(500, "Open git-diff failed");
5166 die_error
(400, "Missing one of the blob diff parameters")
5171 if ($format eq 'html') {
5173 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5175 git_header_html
(undef, $expires);
5176 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5177 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5178 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5180 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5181 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5183 if (defined $file_name) {
5184 git_print_page_path
($file_name, "blob", $hash_base);
5186 print "<div class=\"page_path\"></div>\n";
5189 } elsif ($format eq 'plain') {
5191 -type
=> 'text/plain',
5192 -charset
=> 'utf-8',
5193 -expires
=> $expires,
5194 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
5196 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5199 die_error
(400, "Unknown blobdiff format");
5203 if ($format eq 'html') {
5204 print "<div class=\"page_body\">\n";
5206 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5209 print "</div>\n"; # class="page_body"
5213 while (my $line = <$fd>) {
5214 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5215 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5219 last if $line =~ m!^\+\+\+!;
5227 sub git_blobdiff_plain
{
5228 git_blobdiff
('plain');
5231 sub git_commitdiff
{
5232 my $format = shift || 'html';
5233 $hash ||= $hash_base || "HEAD";
5234 my %co = parse_commit
($hash)
5235 or die_error
(404, "Unknown commit object");
5237 # choose format for commitdiff for merge
5238 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5239 $hash_parent = '--cc';
5241 # we need to prepare $formats_nav before almost any parameter munging
5243 if ($format eq 'html') {
5245 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5248 if (defined $hash_parent &&
5249 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5250 # commitdiff with two commits given
5251 my $hash_parent_short = $hash_parent;
5252 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5253 $hash_parent_short = substr($hash_parent, 0, 7);
5257 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5258 if ($co{'parents'}[$i] eq $hash_parent) {
5259 $formats_nav .= ' parent ' . ($i+1);
5263 $formats_nav .= ': ' .
5264 $cgi->a({-href
=> href
(action
=>"commitdiff",
5265 hash
=>$hash_parent)},
5266 esc_html
($hash_parent_short)) .
5268 } elsif (!$co{'parent'}) {
5270 $formats_nav .= ' (initial)';
5271 } elsif (scalar @{$co{'parents'}} == 1) {
5272 # single parent commit
5275 $cgi->a({-href
=> href
(action
=>"commitdiff",
5276 hash
=>$co{'parent'})},
5277 esc_html
(substr($co{'parent'}, 0, 7))) .
5281 if ($hash_parent eq '--cc') {
5282 $formats_nav .= ' | ' .
5283 $cgi->a({-href
=> href
(action
=>"commitdiff",
5284 hash
=>$hash, hash_parent
=>'-c')},
5286 } else { # $hash_parent eq '-c'
5287 $formats_nav .= ' | ' .
5288 $cgi->a({-href
=> href
(action
=>"commitdiff",
5289 hash
=>$hash, hash_parent
=>'--cc')},
5295 $cgi->a({-href
=> href
(action
=>"commitdiff",
5297 esc_html
(substr($_, 0, 7)));
5298 } @{$co{'parents'}} ) .
5303 my $hash_parent_param = $hash_parent;
5304 if (!defined $hash_parent_param) {
5305 # --cc for multiple parents, --root for parentless
5306 $hash_parent_param =
5307 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5313 if ($format eq 'html') {
5314 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5315 "--no-commit-id", "--patch-with-raw", "--full-index",
5316 $hash_parent_param, $hash, "--"
5317 or die_error
(500, "Open git-diff-tree failed");
5319 while (my $line = <$fd>) {
5321 # empty line ends raw part of diff-tree output
5323 push @difftree, scalar parse_difftree_raw_line
($line);
5326 } elsif ($format eq 'plain') {
5327 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5328 '-p', $hash_parent_param, $hash, "--"
5329 or die_error
(500, "Open git-diff-tree failed");
5332 die_error
(400, "Unknown commitdiff format");
5335 # non-textual hash id's can be cached
5337 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5341 # write commit message
5342 if ($format eq 'html') {
5343 my $refs = git_get_references
();
5344 my $ref = format_ref_marker
($refs, $co{'id'});
5346 git_header_html
(undef, $expires);
5347 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5348 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5349 git_print_authorship
(\
%co);
5350 print "<div class=\"page_body\">\n";
5351 if (@{$co{'comment'}} > 1) {
5352 print "<div class=\"log\">\n";
5353 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5354 print "</div>\n"; # class="log"
5357 } elsif ($format eq 'plain') {
5358 my $refs = git_get_references
("tags");
5359 my $tagname = git_get_rev_name_tags
($hash);
5360 my $filename = basename
($project) . "-$hash.patch";
5363 -type
=> 'text/plain',
5364 -charset
=> 'utf-8',
5365 -expires
=> $expires,
5366 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5367 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5368 print "From: " . to_utf8
($co{'author'}) . "\n";
5369 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5370 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5372 print "X-Git-Tag: $tagname\n" if $tagname;
5373 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5375 foreach my $line (@{$co{'comment'}}) {
5376 print to_utf8
($line) . "\n";
5382 if ($format eq 'html') {
5383 my $use_parents = !defined $hash_parent ||
5384 $hash_parent eq '-c' || $hash_parent eq '--cc';
5385 git_difftree_body
(\
@difftree, $hash,
5386 $use_parents ? @{$co{'parents'}} : $hash_parent);
5389 git_patchset_body
($fd, \
@difftree, $hash,
5390 $use_parents ? @{$co{'parents'}} : $hash_parent);
5392 print "</div>\n"; # class="page_body"
5395 } elsif ($format eq 'plain') {
5399 or print "Reading git-diff-tree failed\n";
5403 sub git_commitdiff_plain
{
5404 git_commitdiff
('plain');
5408 if (!defined $hash_base) {
5409 $hash_base = git_get_head_hash
($project);
5411 if (!defined $page) {
5415 my %co = parse_commit
($hash_base)
5416 or die_error
(404, "Unknown commit object");
5418 my $refs = git_get_references
();
5419 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5421 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5422 $file_name, "--full-history")
5423 or die_error
(404, "No such file or directory on given branch");
5425 if (!defined $hash && defined $file_name) {
5426 # some commits could have deleted file in question,
5427 # and not have it in tree, but one of them has to have it
5428 for (my $i = 0; $i <= @commitlist; $i++) {
5429 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5430 last if defined $hash;
5433 if (defined $hash) {
5434 $ftype = git_get_type
($hash);
5436 if (!defined $ftype) {
5437 die_error
(500, "Unknown type of object");
5440 my $paging_nav = '';
5443 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5444 file_name
=>$file_name)},
5446 $paging_nav .= " ⋅ " .
5447 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5448 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5450 $paging_nav .= "first";
5451 $paging_nav .= " ⋅ prev";
5454 if ($#commitlist >= 100) {
5456 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5457 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5458 $paging_nav .= " ⋅ $next_link";
5460 $paging_nav .= " ⋅ next";
5464 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5465 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5466 git_print_page_path
($file_name, $ftype, $hash_base);
5468 git_history_body
(\
@commitlist, 0, 99,
5469 $refs, $hash_base, $ftype, $next_link);
5475 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5476 if (!defined $searchtext) {
5477 die_error
(400, "Text field is empty");
5479 if (!defined $hash) {
5480 $hash = git_get_head_hash
($project);
5482 my %co = parse_commit
($hash);
5484 die_error
(404, "Unknown commit object");
5486 if (!defined $page) {
5490 $searchtype ||= 'commit';
5491 if ($searchtype eq 'pickaxe') {
5492 # pickaxe may take all resources of your box and run for several minutes
5493 # with every query - so decide by yourself how public you make this feature
5494 gitweb_check_feature
('pickaxe')
5495 or die_error
(403, "Pickaxe is disabled");
5497 if ($searchtype eq 'grep') {
5498 gitweb_check_feature
('grep')
5499 or die_error
(403, "Grep is disabled");
5504 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5506 if ($searchtype eq 'commit') {
5507 $greptype = "--grep=";
5508 } elsif ($searchtype eq 'author') {
5509 $greptype = "--author=";
5510 } elsif ($searchtype eq 'committer') {
5511 $greptype = "--committer=";
5513 $greptype .= $searchtext;
5514 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5515 $greptype, '--regexp-ignore-case',
5516 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5518 my $paging_nav = '';
5521 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5522 searchtext
=>$searchtext,
5523 searchtype
=>$searchtype)},
5525 $paging_nav .= " ⋅ " .
5526 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5527 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5529 $paging_nav .= "first";
5530 $paging_nav .= " ⋅ prev";
5533 if ($#commitlist >= 100) {
5535 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5536 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5537 $paging_nav .= " ⋅ $next_link";
5539 $paging_nav .= " ⋅ next";
5542 if ($#commitlist >= 100) {
5545 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5546 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5547 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5550 if ($searchtype eq 'pickaxe') {
5551 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5552 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5554 print "<table class=\"pickaxe search\">\n";
5557 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5558 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5559 ($search_use_regexp ? '--pickaxe-regex' : ());
5562 while (my $line = <$fd>) {
5566 my %set = parse_difftree_raw_line
($line);
5567 if (defined $set{'commit'}) {
5568 # finish previous commit
5571 "<td class=\"link\">" .
5572 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5574 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5580 print "<tr class=\"dark\">\n";
5582 print "<tr class=\"light\">\n";
5585 %co = parse_commit
($set{'commit'});
5586 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5587 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5588 "<td><i>$author</i></td>\n" .
5590 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5591 -class => "list subject"},
5592 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5593 } elsif (defined $set{'to_id'}) {
5594 next if ($set{'to_id'} =~ m/^0{40}$/);
5596 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5597 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5599 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5605 # finish last commit (warning: repetition!)
5608 "<td class=\"link\">" .
5609 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5611 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5619 if ($searchtype eq 'grep') {
5620 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5621 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5623 print "<table class=\"grep_search\">\n";
5627 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5628 $search_use_regexp ? ('-E', '-i') : '-F',
5629 $searchtext, $co{'tree'};
5631 while (my $line = <$fd>) {
5633 my ($file, $lno, $ltext, $binary);
5634 last if ($matches++ > 1000);
5635 if ($line =~ /^Binary file (.+) matches$/) {
5639 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5641 if ($file ne $lastfile) {
5642 $lastfile and print "</td></tr>\n";
5644 print "<tr class=\"dark\">\n";
5646 print "<tr class=\"light\">\n";
5648 print "<td class=\"list\">".
5649 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5650 file_name
=>"$file"),
5651 -class => "list"}, esc_path
($file));
5652 print "</td><td>\n";
5656 print "<div class=\"binary\">Binary file</div>\n";
5658 $ltext = untabify
($ltext);
5659 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5660 $ltext = esc_html
($1, -nbsp
=>1);
5661 $ltext .= '<span class="match">';
5662 $ltext .= esc_html
($2, -nbsp
=>1);
5663 $ltext .= '</span>';
5664 $ltext .= esc_html
($3, -nbsp
=>1);
5666 $ltext = esc_html
($ltext, -nbsp
=>1);
5668 print "<div class=\"pre\">" .
5669 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5670 file_name
=>"$file").'#l'.$lno,
5671 -class => "linenr"}, sprintf('%4i', $lno))
5672 . ' ' . $ltext . "</div>\n";
5676 print "</td></tr>\n";
5677 if ($matches > 1000) {
5678 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5681 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5690 sub git_search_help
{
5692 git_print_page_nav
('','', $hash,$hash,$hash);
5694 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5695 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5696 the pattern entered is recognized as the POSIX extended
5697 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5700 <dt><b>commit</b></dt>
5701 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5703 my ($have_grep) = gitweb_check_feature
('grep');
5706 <dt><b>grep</b></dt>
5707 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5708 a different one) are searched for the given pattern. On large trees, this search can take
5709 a while and put some strain on the server, so please use it with some consideration. Note that
5710 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5711 case-sensitive.</dd>
5715 <dt><b>author</b></dt>
5716 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5717 <dt><b>committer</b></dt>
5718 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5720 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5721 if ($have_pickaxe) {
5723 <dt><b>pickaxe</b></dt>
5724 <dd>All commits that caused the string to appear or disappear from any file (changes that
5725 added, removed or "modified" the string) will be listed. This search can take a while and
5726 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5727 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5735 my $head = git_get_head_hash
($project);
5736 if (!defined $hash) {
5739 if (!defined $page) {
5742 my $refs = git_get_references
();
5744 my $commit_hash = $hash;
5745 if (defined $hash_parent) {
5746 $commit_hash = "$hash_parent..$hash";
5748 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5750 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5752 if ($#commitlist >= 100) {
5754 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5755 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5759 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5760 git_print_header_div
('summary', $project);
5762 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5767 ## ......................................................................
5768 ## feeds (RSS, Atom; OPML)
5771 my $format = shift || 'atom';
5772 my ($have_blame) = gitweb_check_feature
('blame');
5774 # Atom: http://www.atomenabled.org/developers/syndication/
5775 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5776 if ($format ne 'rss' && $format ne 'atom') {
5777 die_error
(400, "Unknown web feed format");
5780 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5781 my $head = $hash || 'HEAD';
5782 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5786 my $content_type = "application/$format+xml";
5787 if (defined $cgi->http('HTTP_ACCEPT') &&
5788 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5789 # browser (feed reader) prefers text/xml
5790 $content_type = 'text/xml';
5792 if (defined($commitlist[0])) {
5793 %latest_commit = %{$commitlist[0]};
5794 %latest_date = parse_date
($latest_commit{'author_epoch'});
5796 -type
=> $content_type,
5797 -charset
=> 'utf-8',
5798 -last_modified
=> $latest_date{'rfc2822'});
5801 -type
=> $content_type,
5802 -charset
=> 'utf-8');
5805 # Optimization: skip generating the body if client asks only
5806 # for Last-Modified date.
5807 return if ($cgi->request_method() eq 'HEAD');
5810 my $title = "$site_name - $project/$action";
5811 my $feed_type = 'log';
5812 if (defined $hash) {
5813 $title .= " - '$hash'";
5814 $feed_type = 'branch log';
5815 if (defined $file_name) {
5816 $title .= " :: $file_name";
5817 $feed_type = 'history';
5819 } elsif (defined $file_name) {
5820 $title .= " - $file_name";
5821 $feed_type = 'history';
5823 $title .= " $feed_type";
5824 my $descr = git_get_project_description
($project);
5825 if (defined $descr) {
5826 $descr = esc_html
($descr);
5828 $descr = "$project " .
5829 ($format eq 'rss' ? 'RSS' : 'Atom') .
5832 my $owner = git_get_project_owner
($project);
5833 $owner = esc_html
($owner);
5837 if (defined $file_name) {
5838 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5839 } elsif (defined $hash) {
5840 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5842 $alt_url = href
(-full
=>1, action
=>"summary");
5844 print qq
!<?xml version
="1.0" encoding
="utf-8"?>\n!;
5845 if ($format eq 'rss') {
5847 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5850 print "<title>$title</title>\n" .
5851 "<link>$alt_url</link>\n" .
5852 "<description>$descr</description>\n" .
5853 "<language>en</language>\n";
5854 } elsif ($format eq 'atom') {
5856 <feed xmlns="http://www.w3.org/2005/Atom">
5858 print "<title>$title</title>\n" .
5859 "<subtitle>$descr</subtitle>\n" .
5860 '<link rel="alternate" type="text/html" href="' .
5861 $alt_url . '" />' . "\n" .
5862 '<link rel="self" type="' . $content_type . '" href="' .
5863 $cgi->self_url() . '" />' . "\n" .
5864 "<id>" . href
(-full
=>1) . "</id>\n" .
5865 # use project owner for feed author
5866 "<author><name>$owner</name></author>\n";
5867 if (defined $favicon) {
5868 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5870 if (defined $logo_url) {
5871 # not twice as wide as tall: 72 x 27 pixels
5872 print "<logo>" . esc_url
($logo) . "</logo>\n";
5874 if (! %latest_date) {
5875 # dummy date to keep the feed valid until commits trickle in:
5876 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5878 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5883 for (my $i = 0; $i <= $#commitlist; $i++) {
5884 my %co = %{$commitlist[$i]};
5885 my $commit = $co{'id'};
5886 # we read 150, we always show 30 and the ones more recent than 48 hours
5887 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5890 my %cd = parse_date
($co{'author_epoch'});
5892 # get list of changed files
5893 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5894 $co{'parent'} || "--root",
5895 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5897 my @difftree = map { chomp; $_ } <$fd>;
5901 # print element (entry, item)
5902 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5903 if ($format eq 'rss') {
5905 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5906 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5907 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5908 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5909 "<link>$co_url</link>\n" .
5910 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5911 "<content:encoded>" .
5913 } elsif ($format eq 'atom') {
5915 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5916 "<updated>$cd{'iso-8601'}</updated>\n" .
5918 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5919 if ($co{'author_email'}) {
5920 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5922 print "</author>\n" .
5923 # use committer for contributor
5925 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5926 if ($co{'committer_email'}) {
5927 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5929 print "</contributor>\n" .
5930 "<published>$cd{'iso-8601'}</published>\n" .
5931 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5932 "<id>$co_url</id>\n" .
5933 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5934 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5936 my $comment = $co{'comment'};
5938 foreach my $line (@$comment) {
5939 $line = esc_html
($line);
5942 print "</pre><ul>\n";
5943 foreach my $difftree_line (@difftree) {
5944 my %difftree = parse_difftree_raw_line
($difftree_line);
5945 next if !$difftree{'from_id'};
5947 my $file = $difftree{'file'} || $difftree{'to_file'};
5951 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5952 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5953 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5954 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5955 -title
=> "diff"}, 'D');
5957 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5958 file_name
=>$file, hash_base
=>$commit),
5959 -title
=> "blame"}, 'B');
5961 # if this is not a feed of a file history
5962 if (!defined $file_name || $file_name ne $file) {
5963 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5964 file_name
=>$file, hash
=>$commit),
5965 -title
=> "history"}, 'H');
5967 $file = esc_path
($file);
5971 if ($format eq 'rss') {
5972 print "</ul>]]>\n" .
5973 "</content:encoded>\n" .
5975 } elsif ($format eq 'atom') {
5976 print "</ul>\n</div>\n" .
5983 if ($format eq 'rss') {
5984 print "</channel>\n</rss>\n";
5985 } elsif ($format eq 'atom') {
5999 my @list = git_get_projects_list
();
6001 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
6003 <?xml version="1.0" encoding="utf-8"?>
6004 <opml version="1.0">
6006 <title>$site_name OPML Export</title>
6009 <outline text="git RSS feeds">
6012 foreach my $pr (@list) {
6014 my $head = git_get_head_hash
($proj{'path'});
6015 if (!defined $head) {
6018 $git_dir = "$projectroot/$proj{'path'}";
6019 my %co = parse_commit
($head);
6024 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
6025 my $rss = "$my_url?p=$proj{'path'};a=rss";
6026 my $html = "$my_url?p=$proj{'path'};a=summary";
6027 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";