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 # try to put as many parameters as possible in PATH_INFO:
738 # - hash or hash_base:/filename
740 # When the script is the root DirectoryIndex for the domain,
741 # $href here would be something like http://gitweb.example.com/
742 # Thus, we strip any trailing / from $href, to spare us double
743 # slashes in the final URL
746 # Then add the project name, if present
747 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
748 delete $params{'project'};
750 # Summary just uses the project path URL, any other action is
752 if (defined $params{'action'}) {
753 $href .= "/".esc_url
($params{'action'}) unless $params{'action'} eq 'summary';
754 delete $params{'action'};
757 # Finally, we put either hash_base:/file_name or hash
758 if (defined $params{'hash_base'}) {
759 $href .= "/".esc_url
($params{'hash_base'});
760 if (defined $params{'file_name'}) {
761 $href .= ":/".esc_url
($params{'file_name'});
762 delete $params{'file_name'};
764 delete $params{'hash'};
765 delete $params{'hash_base'};
766 } elsif (defined $params{'hash'}) {
767 $href .= "/".esc_url
($params{'hash'});
768 delete $params{'hash'};
772 # now encode the parameters explicitly
774 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
775 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
776 if (defined $params{$name}) {
777 if (ref($params{$name}) eq "ARRAY") {
778 foreach my $par (@{$params{$name}}) {
779 push @result, $symbol . "=" . esc_param
($par);
782 push @result, $symbol . "=" . esc_param
($params{$name});
786 $href .= "?" . join(';', @result) if scalar @result;
792 ## ======================================================================
793 ## validation, quoting/unquoting and escaping
795 sub validate_action
{
796 my $input = shift || return undef;
797 return undef unless exists $actions{$input};
801 sub validate_project
{
802 my $input = shift || return undef;
803 if (!validate_pathname
($input) ||
804 !(-d
"$projectroot/$input") ||
805 !check_head_link
("$projectroot/$input") ||
806 ($export_ok && !(-e
"$projectroot/$input/$export_ok")) ||
807 ($strict_export && !project_in_list
($input))) {
814 sub validate_pathname
{
815 my $input = shift || return undef;
817 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
818 # at the beginning, at the end, and between slashes.
819 # also this catches doubled slashes
820 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
824 if ($input =~ m!\0!) {
830 sub validate_refname
{
831 my $input = shift || return undef;
833 # textual hashes are O.K.
834 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
837 # it must be correct pathname
838 $input = validate_pathname
($input)
840 # restrictions on ref name according to git-check-ref-format
841 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
847 # decode sequences of octets in utf8 into Perl's internal form,
848 # which is utf-8 with utf8 flag set if needed. gitweb writes out
849 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
852 if (utf8
::valid
($str)) {
856 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
860 # quote unsafe chars, but keep the slash, even when it's not
861 # correct, but quoted slashes look too horrible in bookmarks
864 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
870 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
873 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
879 # replace invalid utf8 character with SUBSTITUTION sequence
884 $str = to_utf8
($str);
885 $str = $cgi->escapeHTML($str);
886 if ($opts{'-nbsp'}) {
887 $str =~ s/ / /g;
889 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
893 # quote control characters and escape filename to HTML
898 $str = to_utf8
($str);
899 $str = $cgi->escapeHTML($str);
900 if ($opts{'-nbsp'}) {
901 $str =~ s/ / /g;
903 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
907 # Make control characters "printable", using character escape codes (CEC)
911 my %es = ( # character escape codes, aka escape sequences
912 "\t" => '\t', # tab (HT)
913 "\n" => '\n', # line feed (LF)
914 "\r" => '\r', # carrige return (CR)
915 "\f" => '\f', # form feed (FF)
916 "\b" => '\b', # backspace (BS)
917 "\a" => '\a', # alarm (bell) (BEL)
918 "\e" => '\e', # escape (ESC)
919 "\013" => '\v', # vertical tab (VT)
920 "\000" => '\0', # nul character (NUL)
922 my $chr = ( (exists $es{$cntrl})
924 : sprintf('\%2x', ord($cntrl)) );
925 if ($opts{-nohtml
}) {
928 return "<span class=\"cntrl\">$chr</span>";
932 # Alternatively use unicode control pictures codepoints,
933 # Unicode "printable representation" (PR)
938 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
939 if ($opts{-nohtml
}) {
942 return "<span class=\"cntrl\">$chr</span>";
946 # git may return quoted and escaped filenames
952 my %es = ( # character escape codes, aka escape sequences
953 't' => "\t", # tab (HT, TAB)
954 'n' => "\n", # newline (NL)
955 'r' => "\r", # return (CR)
956 'f' => "\f", # form feed (FF)
957 'b' => "\b", # backspace (BS)
958 'a' => "\a", # alarm (bell) (BEL)
959 'e' => "\e", # escape (ESC)
960 'v' => "\013", # vertical tab (VT)
963 if ($seq =~ m/^[0-7]{1,3}$/) {
964 # octal char sequence
965 return chr(oct($seq));
966 } elsif (exists $es{$seq}) {
967 # C escape sequence, aka character escape code
970 # quoted ordinary character
974 if ($str =~ m/^"(.*)"$/) {
977 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
982 # escape tabs (convert tabs to spaces)
986 while ((my $pos = index($line, "\t")) != -1) {
987 if (my $count = (8 - ($pos % 8))) {
988 my $spaces = ' ' x
$count;
989 $line =~ s/\t/$spaces/;
996 sub project_in_list
{
998 my @list = git_get_projects_list
();
999 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1002 ## ----------------------------------------------------------------------
1003 ## HTML aware string manipulation
1005 # Try to chop given string on a word boundary between position
1006 # $len and $len+$add_len. If there is no word boundary there,
1007 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1008 # (marking chopped part) would be longer than given string.
1012 my $add_len = shift || 10;
1013 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1015 # Make sure perl knows it is utf8 encoded so we don't
1016 # cut in the middle of a utf8 multibyte char.
1017 $str = to_utf8
($str);
1019 # allow only $len chars, but don't cut a word if it would fit in $add_len
1020 # if it doesn't fit, cut it if it's still longer than the dots we would add
1021 # remove chopped character entities entirely
1023 # when chopping in the middle, distribute $len into left and right part
1024 # return early if chopping wouldn't make string shorter
1025 if ($where eq 'center') {
1026 return $str if ($len + 5 >= length($str)); # filler is length 5
1029 return $str if ($len + 4 >= length($str)); # filler is length 4
1032 # regexps: ending and beginning with word part up to $add_len
1033 my $endre = qr/.{$len}\w{0,$add_len}/;
1034 my $begre = qr/\w{0,$add_len}.{$len}/;
1036 if ($where eq 'left') {
1037 $str =~ m/^(.*?)($begre)$/;
1038 my ($lead, $body) = ($1, $2);
1039 if (length($lead) > 4) {
1040 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1043 return "$lead$body";
1045 } elsif ($where eq 'center') {
1046 $str =~ m/^($endre)(.*)$/;
1047 my ($left, $str) = ($1, $2);
1048 $str =~ m/^(.*?)($begre)$/;
1049 my ($mid, $right) = ($1, $2);
1050 if (length($mid) > 5) {
1051 $left =~ s/&[^;]*$//;
1052 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1055 return "$left$mid$right";
1058 $str =~ m/^($endre)(.*)$/;
1061 if (length($tail) > 4) {
1062 $body =~ s/&[^;]*$//;
1065 return "$body$tail";
1069 # takes the same arguments as chop_str, but also wraps a <span> around the
1070 # result with a title attribute if it does get chopped. Additionally, the
1071 # string is HTML-escaped.
1072 sub chop_and_escape_str
{
1075 my $chopped = chop_str
(@_);
1076 if ($chopped eq $str) {
1077 return esc_html
($chopped);
1079 $str =~ s/([[:cntrl:]])/?/g;
1080 return $cgi->span({-title
=>$str}, esc_html
($chopped));
1084 ## ----------------------------------------------------------------------
1085 ## functions returning short strings
1087 # CSS class for given age value (in seconds)
1091 if (!defined $age) {
1093 } elsif ($age < 60*60*2) {
1095 } elsif ($age < 60*60*24*2) {
1102 # convert age in seconds to "nn units ago" string
1107 if ($age > 60*60*24*365*2) {
1108 $age_str = (int $age/60/60/24/365);
1109 $age_str .= " years ago";
1110 } elsif ($age > 60*60*24*(365/12)*2) {
1111 $age_str = int $age/60/60/24/(365/12);
1112 $age_str .= " months ago";
1113 } elsif ($age > 60*60*24*7*2) {
1114 $age_str = int $age/60/60/24/7;
1115 $age_str .= " weeks ago";
1116 } elsif ($age > 60*60*24*2) {
1117 $age_str = int $age/60/60/24;
1118 $age_str .= " days ago";
1119 } elsif ($age > 60*60*2) {
1120 $age_str = int $age/60/60;
1121 $age_str .= " hours ago";
1122 } elsif ($age > 60*2) {
1123 $age_str = int $age/60;
1124 $age_str .= " min ago";
1125 } elsif ($age > 2) {
1126 $age_str = int $age;
1127 $age_str .= " sec ago";
1129 $age_str .= " right now";
1135 S_IFINVALID
=> 0030000,
1136 S_IFGITLINK
=> 0160000,
1139 # submodule/subproject, a commit object reference
1140 sub S_ISGITLINK
($) {
1143 return (($mode & S_IFMT
) == S_IFGITLINK
)
1146 # convert file mode in octal to symbolic file mode string
1148 my $mode = oct shift;
1150 if (S_ISGITLINK
($mode)) {
1151 return 'm---------';
1152 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1153 return 'drwxr-xr-x';
1154 } elsif (S_ISLNK
($mode)) {
1155 return 'lrwxrwxrwx';
1156 } elsif (S_ISREG
($mode)) {
1157 # git cares only about the executable bit
1158 if ($mode & S_IXUSR
) {
1159 return '-rwxr-xr-x';
1161 return '-rw-r--r--';
1164 return '----------';
1168 # convert file mode in octal to file type string
1172 if ($mode !~ m/^[0-7]+$/) {
1178 if (S_ISGITLINK
($mode)) {
1180 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1182 } elsif (S_ISLNK
($mode)) {
1184 } elsif (S_ISREG
($mode)) {
1191 # convert file mode in octal to file type description string
1192 sub file_type_long
{
1195 if ($mode !~ m/^[0-7]+$/) {
1201 if (S_ISGITLINK
($mode)) {
1203 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1205 } elsif (S_ISLNK
($mode)) {
1207 } elsif (S_ISREG
($mode)) {
1208 if ($mode & S_IXUSR
) {
1209 return "executable";
1219 ## ----------------------------------------------------------------------
1220 ## functions returning short HTML fragments, or transforming HTML fragments
1221 ## which don't belong to other sections
1223 # format line of commit message.
1224 sub format_log_line_html
{
1227 $line = esc_html
($line, -nbsp
=>1);
1228 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1231 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1232 -class => "text"}, $hash_text);
1233 $line =~ s/$hash_text/$link/;
1238 # format marker of refs pointing to given object
1240 # the destination action is chosen based on object type and current context:
1241 # - for annotated tags, we choose the tag view unless it's the current view
1242 # already, in which case we go to shortlog view
1243 # - for other refs, we keep the current view if we're in history, shortlog or
1244 # log view, and select shortlog otherwise
1245 sub format_ref_marker
{
1246 my ($refs, $id) = @_;
1249 if (defined $refs->{$id}) {
1250 foreach my $ref (@{$refs->{$id}}) {
1251 # this code exploits the fact that non-lightweight tags are the
1252 # only indirect objects, and that they are the only objects for which
1253 # we want to use tag instead of shortlog as action
1254 my ($type, $name) = qw();
1255 my $indirect = ($ref =~ s/\^\{\}$//);
1256 # e.g. tags/v2.6.11 or heads/next
1257 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1266 $class .= " indirect" if $indirect;
1268 my $dest_action = "shortlog";
1271 $dest_action = "tag" unless $action eq "tag";
1272 } elsif ($action =~ /^(history|(short)?log)$/) {
1273 $dest_action = $action;
1277 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1280 my $link = $cgi->a({
1282 action
=>$dest_action,
1286 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1292 return ' <span class="refs">'. $markers . '</span>';
1298 # format, perhaps shortened and with markers, title line
1299 sub format_subject_html
{
1300 my ($long, $short, $href, $extra) = @_;
1301 $extra = '' unless defined($extra);
1303 if (length($short) < length($long)) {
1304 return $cgi->a({-href
=> $href, -class => "list subject",
1305 -title
=> to_utf8
($long)},
1306 esc_html
($short) . $extra);
1308 return $cgi->a({-href
=> $href, -class => "list subject"},
1309 esc_html
($long) . $extra);
1313 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1314 sub format_git_diff_header_line
{
1316 my $diffinfo = shift;
1317 my ($from, $to) = @_;
1319 if ($diffinfo->{'nparents'}) {
1321 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1322 if ($to->{'href'}) {
1323 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1324 esc_path
($to->{'file'}));
1325 } else { # file was deleted (no href)
1326 $line .= esc_path
($to->{'file'});
1330 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1331 if ($from->{'href'}) {
1332 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1333 'a/' . esc_path
($from->{'file'}));
1334 } else { # file was added (no href)
1335 $line .= 'a/' . esc_path
($from->{'file'});
1338 if ($to->{'href'}) {
1339 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1340 'b/' . esc_path
($to->{'file'}));
1341 } else { # file was deleted
1342 $line .= 'b/' . esc_path
($to->{'file'});
1346 return "<div class=\"diff header\">$line</div>\n";
1349 # format extended diff header line, before patch itself
1350 sub format_extended_diff_header_line
{
1352 my $diffinfo = shift;
1353 my ($from, $to) = @_;
1356 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1357 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1358 esc_path
($from->{'file'}));
1360 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1361 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1362 esc_path
($to->{'file'}));
1364 # match single <mode>
1365 if ($line =~ m/\s(\d{6})$/) {
1366 $line .= '<span class="info"> (' .
1367 file_type_long
($1) .
1371 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1372 # can match only for combined diff
1374 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1375 if ($from->{'href'}[$i]) {
1376 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1378 substr($diffinfo->{'from_id'}[$i],0,7));
1383 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1386 if ($to->{'href'}) {
1387 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1388 substr($diffinfo->{'to_id'},0,7));
1393 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1394 # can match only for ordinary diff
1395 my ($from_link, $to_link);
1396 if ($from->{'href'}) {
1397 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1398 substr($diffinfo->{'from_id'},0,7));
1400 $from_link = '0' x
7;
1402 if ($to->{'href'}) {
1403 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1404 substr($diffinfo->{'to_id'},0,7));
1408 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1409 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1412 return $line . "<br/>\n";
1415 # format from-file/to-file diff header
1416 sub format_diff_from_to_header
{
1417 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1422 #assert($line =~ m/^---/) if DEBUG;
1423 # no extra formatting for "^--- /dev/null"
1424 if (! $diffinfo->{'nparents'}) {
1425 # ordinary (single parent) diff
1426 if ($line =~ m!^--- "?a/!) {
1427 if ($from->{'href'}) {
1429 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1430 esc_path
($from->{'file'}));
1433 esc_path
($from->{'file'});
1436 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1439 # combined diff (merge commit)
1440 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1441 if ($from->{'href'}[$i]) {
1443 $cgi->a({-href
=>href
(action
=>"blobdiff",
1444 hash_parent
=>$diffinfo->{'from_id'}[$i],
1445 hash_parent_base
=>$parents[$i],
1446 file_parent
=>$from->{'file'}[$i],
1447 hash
=>$diffinfo->{'to_id'},
1449 file_name
=>$to->{'file'}),
1451 -title
=>"diff" . ($i+1)},
1454 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1455 esc_path
($from->{'file'}[$i]));
1457 $line = '--- /dev/null';
1459 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1464 #assert($line =~ m/^\+\+\+/) if DEBUG;
1465 # no extra formatting for "^+++ /dev/null"
1466 if ($line =~ m!^\+\+\+ "?b/!) {
1467 if ($to->{'href'}) {
1469 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1470 esc_path
($to->{'file'}));
1473 esc_path
($to->{'file'});
1476 $result .= qq
!<div
class="diff to_file">$line</div
>\n!;
1481 # create note for patch simplified by combined diff
1482 sub format_diff_cc_simplified
{
1483 my ($diffinfo, @parents) = @_;
1486 $result .= "<div class=\"diff header\">" .
1488 if (!is_deleted
($diffinfo)) {
1489 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1491 hash
=>$diffinfo->{'to_id'},
1492 file_name
=>$diffinfo->{'to_file'}),
1494 esc_path
($diffinfo->{'to_file'}));
1496 $result .= esc_path
($diffinfo->{'to_file'});
1498 $result .= "</div>\n" . # class="diff header"
1499 "<div class=\"diff nodifferences\">" .
1501 "</div>\n"; # class="diff nodifferences"
1506 # format patch (diff) line (not to be used for diff headers)
1507 sub format_diff_line
{
1509 my ($from, $to) = @_;
1510 my $diff_class = "";
1514 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1516 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1517 if ($line =~ m/^\@{3}/) {
1518 $diff_class = " chunk_header";
1519 } elsif ($line =~ m/^\\/) {
1520 $diff_class = " incomplete";
1521 } elsif ($prefix =~ tr/+/+/) {
1522 $diff_class = " add";
1523 } elsif ($prefix =~ tr/-/-/) {
1524 $diff_class = " rem";
1527 # assume ordinary diff
1528 my $char = substr($line, 0, 1);
1530 $diff_class = " add";
1531 } elsif ($char eq '-') {
1532 $diff_class = " rem";
1533 } elsif ($char eq '@') {
1534 $diff_class = " chunk_header";
1535 } elsif ($char eq "\\") {
1536 $diff_class = " incomplete";
1539 $line = untabify
($line);
1540 if ($from && $to && $line =~ m/^\@{2} /) {
1541 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1542 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1544 $from_lines = 0 unless defined $from_lines;
1545 $to_lines = 0 unless defined $to_lines;
1547 if ($from->{'href'}) {
1548 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1549 -class=>"list"}, $from_text);
1551 if ($to->{'href'}) {
1552 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1553 -class=>"list"}, $to_text);
1555 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1556 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1557 return "<div class=\"diff$diff_class\">$line</div>\n";
1558 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1559 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1560 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1562 @from_text = split(' ', $ranges);
1563 for (my $i = 0; $i < @from_text; ++$i) {
1564 ($from_start[$i], $from_nlines[$i]) =
1565 (split(',', substr($from_text[$i], 1)), 0);
1568 $to_text = pop @from_text;
1569 $to_start = pop @from_start;
1570 $to_nlines = pop @from_nlines;
1572 $line = "<span class=\"chunk_info\">$prefix ";
1573 for (my $i = 0; $i < @from_text; ++$i) {
1574 if ($from->{'href'}[$i]) {
1575 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1576 -class=>"list"}, $from_text[$i]);
1578 $line .= $from_text[$i];
1582 if ($to->{'href'}) {
1583 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1584 -class=>"list"}, $to_text);
1588 $line .= " $prefix</span>" .
1589 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1590 return "<div class=\"diff$diff_class\">$line</div>\n";
1592 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1595 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1596 # linked. Pass the hash of the tree/commit to snapshot.
1597 sub format_snapshot_links
{
1599 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1600 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1601 my $num_fmts = @snapshot_fmts;
1602 if ($num_fmts > 1) {
1603 # A parenthesized list of links bearing format names.
1604 # e.g. "snapshot (_tar.gz_ _zip_)"
1605 return "snapshot (" . join(' ', map
1612 }, $known_snapshot_formats{$_}{'display'})
1613 , @snapshot_fmts) . ")";
1614 } elsif ($num_fmts == 1) {
1615 # A single "snapshot" link whose tooltip bears the format name.
1617 my ($fmt) = @snapshot_fmts;
1623 snapshot_format
=>$fmt
1625 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1627 } else { # $num_fmts == 0
1632 ## ......................................................................
1633 ## functions returning values to be passed, perhaps after some
1634 ## transformation, to other functions; e.g. returning arguments to href()
1636 # returns hash to be passed to href to generate gitweb URL
1637 # in -title key it returns description of link
1639 my $format = shift || 'Atom';
1640 my %res = (action
=> lc($format));
1642 # feed links are possible only for project views
1643 return unless (defined $project);
1644 # some views should link to OPML, or to generic project feed,
1645 # or don't have specific feed yet (so they should use generic)
1646 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1649 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1650 # from tag links; this also makes possible to detect branch links
1651 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1652 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1655 # find log type for feed description (title)
1657 if (defined $file_name) {
1658 $type = "history of $file_name";
1659 $type .= "/" if ($action eq 'tree');
1660 $type .= " on '$branch'" if (defined $branch);
1662 $type = "log of $branch" if (defined $branch);
1665 $res{-title
} = $type;
1666 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1667 $res{'file_name'} = $file_name;
1672 ## ----------------------------------------------------------------------
1673 ## git utility subroutines, invoking git commands
1675 # returns path to the core git executable and the --git-dir parameter as list
1677 return $GIT, '--git-dir='.$git_dir;
1680 # quote the given arguments for passing them to the shell
1681 # quote_command("command", "arg 1", "arg with ' and ! characters")
1682 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1683 # Try to avoid using this function wherever possible.
1686 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1689 # get HEAD ref of given project as hash
1690 sub git_get_head_hash
{
1691 my $project = shift;
1692 my $o_git_dir = $git_dir;
1694 $git_dir = "$projectroot/$project";
1695 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1698 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1702 if (defined $o_git_dir) {
1703 $git_dir = $o_git_dir;
1708 # get type of given object
1712 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1714 close $fd or return;
1719 # repository configuration
1720 our $config_file = '';
1723 # store multiple values for single key as anonymous array reference
1724 # single values stored directly in the hash, not as [ <value> ]
1725 sub hash_set_multi
{
1726 my ($hash, $key, $value) = @_;
1728 if (!exists $hash->{$key}) {
1729 $hash->{$key} = $value;
1730 } elsif (!ref $hash->{$key}) {
1731 $hash->{$key} = [ $hash->{$key}, $value ];
1733 push @{$hash->{$key}}, $value;
1737 # return hash of git project configuration
1738 # optionally limited to some section, e.g. 'gitweb'
1739 sub git_parse_project_config
{
1740 my $section_regexp = shift;
1745 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1748 while (my $keyval = <$fh>) {
1750 my ($key, $value) = split(/\n/, $keyval, 2);
1752 hash_set_multi
(\
%config, $key, $value)
1753 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1760 # convert config value to boolean, 'true' or 'false'
1761 # no value, number > 0, 'true' and 'yes' values are true
1762 # rest of values are treated as false (never as error)
1763 sub config_to_bool
{
1766 # strip leading and trailing whitespace
1770 return (!defined $val || # section.key
1771 ($val =~ /^\d+$/ && $val) || # section.key = 1
1772 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1775 # convert config value to simple decimal number
1776 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1777 # to be multiplied by 1024, 1048576, or 1073741824
1781 # strip leading and trailing whitespace
1785 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1787 # unknown unit is treated as 1
1788 return $num * ($unit eq 'g' ? 1073741824 :
1789 $unit eq 'm' ? 1048576 :
1790 $unit eq 'k' ? 1024 : 1);
1795 # convert config value to array reference, if needed
1796 sub config_to_multi
{
1799 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1802 sub git_get_project_config
{
1803 my ($key, $type) = @_;
1806 return unless ($key);
1807 $key =~ s/^gitweb\.//;
1808 return if ($key =~ m/\W/);
1811 if (defined $type) {
1814 unless ($type eq 'bool' || $type eq 'int');
1818 if (!defined $config_file ||
1819 $config_file ne "$git_dir/config") {
1820 %config = git_parse_project_config
('gitweb');
1821 $config_file = "$git_dir/config";
1825 if (!defined $type) {
1826 return $config{"gitweb.$key"};
1827 } elsif ($type eq 'bool') {
1828 # backward compatibility: 'git config --bool' returns true/false
1829 return config_to_bool
($config{"gitweb.$key"}) ? 'true' : 'false';
1830 } elsif ($type eq 'int') {
1831 return config_to_int
($config{"gitweb.$key"});
1833 return $config{"gitweb.$key"};
1836 # get hash of given path at given ref
1837 sub git_get_hash_by_path
{
1839 my $path = shift || return undef;
1844 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1845 or die_error
(500, "Open git-ls-tree failed");
1847 close $fd or return undef;
1849 if (!defined $line) {
1850 # there is no tree or hash given by $path at $base
1854 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1855 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1856 if (defined $type && $type ne $2) {
1857 # type doesn't match
1863 # get path of entry with given hash at given tree-ish (ref)
1864 # used to get 'from' filename for combined diff (merge commit) for renames
1865 sub git_get_path_by_hash
{
1866 my $base = shift || return;
1867 my $hash = shift || return;
1871 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1873 while (my $line = <$fd>) {
1876 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1877 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1878 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1887 ## ......................................................................
1888 ## git utility functions, directly accessing git repository
1890 sub git_get_project_description
{
1893 $git_dir = "$projectroot/$path";
1894 open my $fd, "$git_dir/description"
1895 or return git_get_project_config
('description');
1898 if (defined $descr) {
1904 sub git_get_project_ctags
{
1908 $git_dir = "$projectroot/$path";
1909 unless (opendir D
, "$git_dir/ctags") {
1912 foreach (grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir(D
)) {
1913 open CT
, $_ or next;
1917 my $ctag = $_; $ctag =~ s
#.*/##;
1918 $ctags->{$ctag} = $val;
1924 sub git_populate_project_tagcloud
{
1927 # First, merge different-cased tags; tags vote on casing
1929 foreach (keys %$ctags) {
1930 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
1931 if (not $ctags_lc{lc $_}->{topcount
}
1932 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
1933 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
1934 $ctags_lc{lc $_}->{topname
} = $_;
1939 if (eval { require HTML
::TagCloud
; 1; }) {
1940 $cloud = HTML
::TagCloud-
>new;
1941 foreach (sort keys %ctags_lc) {
1942 # Pad the title with spaces so that the cloud looks
1944 my $title = $ctags_lc{$_}->{topname
};
1945 $title =~ s/ / /g;
1946 $title =~ s/^/ /g;
1947 $title =~ s/$/ /g;
1948 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count
});
1951 $cloud = \
%ctags_lc;
1956 sub git_show_project_tagcloud
{
1957 my ($cloud, $count) = @_;
1958 print STDERR
ref($cloud)."..\n";
1959 if (ref $cloud eq 'HTML::TagCloud') {
1960 return $cloud->html_and_css($count);
1962 my @tags = sort { $cloud->{$a}->{count
} <=> $cloud->{$b}->{count
} } keys %$cloud;
1963 return '<p align="center">' . join (', ', map {
1964 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1965 } splice(@tags, 0, $count)) . '</p>';
1969 sub git_get_project_url_list
{
1972 $git_dir = "$projectroot/$path";
1973 open my $fd, "$git_dir/cloneurl"
1974 or return wantarray ?
1975 @{ config_to_multi
(git_get_project_config
('url')) } :
1976 config_to_multi
(git_get_project_config
('url'));
1977 my @git_project_url_list = map { chomp; $_ } <$fd>;
1980 return wantarray ? @git_project_url_list : \
@git_project_url_list;
1983 sub git_get_projects_list
{
1988 $filter =~ s/\.git$//;
1990 my ($check_forks) = gitweb_check_feature
('forks');
1992 if (-d
$projects_list) {
1993 # search in directory
1994 my $dir = $projects_list . ($filter ? "/$filter" : '');
1995 # remove the trailing "/"
1997 my $pfxlen = length("$dir");
1998 my $pfxdepth = ($dir =~ tr!/!!);
2001 follow_fast
=> 1, # follow symbolic links
2002 follow_skip
=> 2, # ignore duplicates
2003 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
2005 # skip project-list toplevel, if we get it.
2006 return if (m!^[/.]$!);
2007 # only directories can be git repositories
2008 return unless (-d
$_);
2009 # don't traverse too deep (Find is super slow on os x)
2010 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2011 $File::Find
::prune
= 1;
2015 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
2016 # we check related file in $projectroot
2017 if (check_export_ok
("$projectroot/$filter/$subdir")) {
2018 push @list, { path
=> ($filter ? "$filter/" : '') . $subdir };
2019 $File::Find
::prune
= 1;
2024 } elsif (-f
$projects_list) {
2025 # read from file(url-encoded):
2026 # 'git%2Fgit.git Linus+Torvalds'
2027 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2028 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2030 open my ($fd), $projects_list or return;
2032 while (my $line = <$fd>) {
2034 my ($path, $owner) = split ' ', $line;
2035 $path = unescape
($path);
2036 $owner = unescape
($owner);
2037 if (!defined $path) {
2040 if ($filter ne '') {
2041 # looking for forks;
2042 my $pfx = substr($path, 0, length($filter));
2043 if ($pfx ne $filter) {
2046 my $sfx = substr($path, length($filter));
2047 if ($sfx !~ /^\/.*\
.git
$/) {
2050 } elsif ($check_forks) {
2052 foreach my $filter (keys %paths) {
2053 # looking for forks;
2054 my $pfx = substr($path, 0, length($filter));
2055 if ($pfx ne $filter) {
2058 my $sfx = substr($path, length($filter));
2059 if ($sfx !~ /^\/.*\
.git
$/) {
2062 # is a fork, don't include it in
2067 if (check_export_ok
("$projectroot/$path")) {
2070 owner
=> to_utf8
($owner),
2073 (my $forks_path = $path) =~ s/\.git$//;
2074 $paths{$forks_path}++;
2082 our $gitweb_project_owner = undef;
2083 sub git_get_project_list_from_file
{
2085 return if (defined $gitweb_project_owner);
2087 $gitweb_project_owner = {};
2088 # read from file (url-encoded):
2089 # 'git%2Fgit.git Linus+Torvalds'
2090 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2091 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2092 if (-f
$projects_list) {
2093 open (my $fd , $projects_list);
2094 while (my $line = <$fd>) {
2096 my ($pr, $ow) = split ' ', $line;
2097 $pr = unescape
($pr);
2098 $ow = unescape
($ow);
2099 $gitweb_project_owner->{$pr} = to_utf8
($ow);
2105 sub git_get_project_owner
{
2106 my $project = shift;
2109 return undef unless $project;
2110 $git_dir = "$projectroot/$project";
2112 if (!defined $gitweb_project_owner) {
2113 git_get_project_list_from_file
();
2116 if (exists $gitweb_project_owner->{$project}) {
2117 $owner = $gitweb_project_owner->{$project};
2119 if (!defined $owner){
2120 $owner = git_get_project_config
('owner');
2122 if (!defined $owner) {
2123 $owner = get_file_owner
("$git_dir");
2129 sub git_get_last_activity
{
2133 $git_dir = "$projectroot/$path";
2134 open($fd, "-|", git_cmd
(), 'for-each-ref',
2135 '--format=%(committer)',
2136 '--sort=-committerdate',
2138 'refs/heads') or return;
2139 my $most_recent = <$fd>;
2140 close $fd or return;
2141 if (defined $most_recent &&
2142 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2144 my $age = time - $timestamp;
2145 return ($age, age_string
($age));
2147 return (undef, undef);
2150 sub git_get_references
{
2151 my $type = shift || "";
2153 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2154 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2155 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
2156 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2159 while (my $line = <$fd>) {
2161 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2162 if (defined $refs{$1}) {
2163 push @{$refs{$1}}, $2;
2169 close $fd or return;
2173 sub git_get_rev_name_tags
{
2174 my $hash = shift || return undef;
2176 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
2178 my $name_rev = <$fd>;
2181 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2184 # catches also '$hash undefined' output
2189 ## ----------------------------------------------------------------------
2190 ## parse to hash functions
2194 my $tz = shift || "-0000";
2197 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2198 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2199 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2200 $date{'hour'} = $hour;
2201 $date{'minute'} = $min;
2202 $date{'mday'} = $mday;
2203 $date{'day'} = $days[$wday];
2204 $date{'month'} = $months[$mon];
2205 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2206 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2207 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2208 $mday, $months[$mon], $hour ,$min;
2209 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2210 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2212 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2213 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2214 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2215 $date{'hour_local'} = $hour;
2216 $date{'minute_local'} = $min;
2217 $date{'tz_local'} = $tz;
2218 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2219 1900+$year, $mon+1, $mday,
2220 $hour, $min, $sec, $tz);
2229 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2230 $tag{'id'} = $tag_id;
2231 while (my $line = <$fd>) {
2233 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2234 $tag{'object'} = $1;
2235 } elsif ($line =~ m/^type (.+)$/) {
2237 } elsif ($line =~ m/^tag (.+)$/) {
2239 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2240 $tag{'author'} = $1;
2243 } elsif ($line =~ m/--BEGIN/) {
2244 push @comment, $line;
2246 } elsif ($line eq "") {
2250 push @comment, <$fd>;
2251 $tag{'comment'} = \
@comment;
2252 close $fd or return;
2253 if (!defined $tag{'name'}) {
2259 sub parse_commit_text
{
2260 my ($commit_text, $withparents) = @_;
2261 my @commit_lines = split '\n', $commit_text;
2264 pop @commit_lines; # Remove '\0'
2266 if (! @commit_lines) {
2270 my $header = shift @commit_lines;
2271 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2274 ($co{'id'}, my @parents) = split ' ', $header;
2275 while (my $line = shift @commit_lines) {
2276 last if $line eq "\n";
2277 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2279 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2281 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2283 $co{'author_epoch'} = $2;
2284 $co{'author_tz'} = $3;
2285 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2286 $co{'author_name'} = $1;
2287 $co{'author_email'} = $2;
2289 $co{'author_name'} = $co{'author'};
2291 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2292 $co{'committer'} = $1;
2293 $co{'committer_epoch'} = $2;
2294 $co{'committer_tz'} = $3;
2295 $co{'committer_name'} = $co{'committer'};
2296 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2297 $co{'committer_name'} = $1;
2298 $co{'committer_email'} = $2;
2300 $co{'committer_name'} = $co{'committer'};
2304 if (!defined $co{'tree'}) {
2307 $co{'parents'} = \
@parents;
2308 $co{'parent'} = $parents[0];
2310 foreach my $title (@commit_lines) {
2313 $co{'title'} = chop_str
($title, 80, 5);
2314 # remove leading stuff of merges to make the interesting part visible
2315 if (length($title) > 50) {
2316 $title =~ s/^Automatic //;
2317 $title =~ s/^merge (of|with) /Merge ... /i;
2318 if (length($title) > 50) {
2319 $title =~ s/(http|rsync):\/\///;
2321 if (length($title) > 50) {
2322 $title =~ s/(master|www|rsync)\.//;
2324 if (length($title) > 50) {
2325 $title =~ s/kernel.org:?//;
2327 if (length($title) > 50) {
2328 $title =~ s/\/pub\/scm//;
2331 $co{'title_short'} = chop_str
($title, 50, 5);
2335 if (! defined $co{'title'} || $co{'title'} eq "") {
2336 $co{'title'} = $co{'title_short'} = '(no commit message)';
2338 # remove added spaces
2339 foreach my $line (@commit_lines) {
2342 $co{'comment'} = \
@commit_lines;
2344 my $age = time - $co{'committer_epoch'};
2346 $co{'age_string'} = age_string
($age);
2347 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2348 if ($age > 60*60*24*7*2) {
2349 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2350 $co{'age_string_age'} = $co{'age_string'};
2352 $co{'age_string_date'} = $co{'age_string'};
2353 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2359 my ($commit_id) = @_;
2364 open my $fd, "-|", git_cmd
(), "rev-list",
2370 or die_error
(500, "Open git-rev-list failed");
2371 %co = parse_commit_text
(<$fd>, 1);
2378 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2386 open my $fd, "-|", git_cmd
(), "rev-list",
2389 ("--max-count=" . $maxcount),
2390 ("--skip=" . $skip),
2394 ($filename ? ($filename) : ())
2395 or die_error
(500, "Open git-rev-list failed");
2396 while (my $line = <$fd>) {
2397 my %co = parse_commit_text
($line);
2402 return wantarray ? @cos : \
@cos;
2405 # parse line of git-diff-tree "raw" output
2406 sub parse_difftree_raw_line
{
2410 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2411 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2412 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2413 $res{'from_mode'} = $1;
2414 $res{'to_mode'} = $2;
2415 $res{'from_id'} = $3;
2417 $res{'status'} = $5;
2418 $res{'similarity'} = $6;
2419 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2420 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2422 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2425 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2426 # combined diff (for merge commit)
2427 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2428 $res{'nparents'} = length($1);
2429 $res{'from_mode'} = [ split(' ', $2) ];
2430 $res{'to_mode'} = pop @{$res{'from_mode'}};
2431 $res{'from_id'} = [ split(' ', $3) ];
2432 $res{'to_id'} = pop @{$res{'from_id'}};
2433 $res{'status'} = [ split('', $4) ];
2434 $res{'to_file'} = unquote
($5);
2436 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2437 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2438 $res{'commit'} = $1;
2441 return wantarray ? %res : \
%res;
2444 # wrapper: return parsed line of git-diff-tree "raw" output
2445 # (the argument might be raw line, or parsed info)
2446 sub parsed_difftree_line
{
2447 my $line_or_ref = shift;
2449 if (ref($line_or_ref) eq "HASH") {
2450 # pre-parsed (or generated by hand)
2451 return $line_or_ref;
2453 return parse_difftree_raw_line
($line_or_ref);
2457 # parse line of git-ls-tree output
2458 sub parse_ls_tree_line
($;%) {
2463 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2464 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2472 $res{'name'} = unquote
($4);
2475 return wantarray ? %res : \
%res;
2478 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2479 sub parse_from_to_diffinfo
{
2480 my ($diffinfo, $from, $to, @parents) = @_;
2482 if ($diffinfo->{'nparents'}) {
2484 $from->{'file'} = [];
2485 $from->{'href'} = [];
2486 fill_from_file_info
($diffinfo, @parents)
2487 unless exists $diffinfo->{'from_file'};
2488 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2489 $from->{'file'}[$i] =
2490 defined $diffinfo->{'from_file'}[$i] ?
2491 $diffinfo->{'from_file'}[$i] :
2492 $diffinfo->{'to_file'};
2493 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2494 $from->{'href'}[$i] = href
(action
=>"blob",
2495 hash_base
=>$parents[$i],
2496 hash
=>$diffinfo->{'from_id'}[$i],
2497 file_name
=>$from->{'file'}[$i]);
2499 $from->{'href'}[$i] = undef;
2503 # ordinary (not combined) diff
2504 $from->{'file'} = $diffinfo->{'from_file'};
2505 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2506 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2507 hash
=>$diffinfo->{'from_id'},
2508 file_name
=>$from->{'file'});
2510 delete $from->{'href'};
2514 $to->{'file'} = $diffinfo->{'to_file'};
2515 if (!is_deleted
($diffinfo)) { # file exists in result
2516 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2517 hash
=>$diffinfo->{'to_id'},
2518 file_name
=>$to->{'file'});
2520 delete $to->{'href'};
2524 ## ......................................................................
2525 ## parse to array of hashes functions
2527 sub git_get_heads_list
{
2531 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2532 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2533 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2536 while (my $line = <$fd>) {
2540 my ($refinfo, $committerinfo) = split(/\0/, $line);
2541 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2542 my ($committer, $epoch, $tz) =
2543 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2544 $ref_item{'fullname'} = $name;
2545 $name =~ s!^refs/heads/!!;
2547 $ref_item{'name'} = $name;
2548 $ref_item{'id'} = $hash;
2549 $ref_item{'title'} = $title || '(no commit message)';
2550 $ref_item{'epoch'} = $epoch;
2552 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2554 $ref_item{'age'} = "unknown";
2557 push @headslist, \
%ref_item;
2561 return wantarray ? @headslist : \
@headslist;
2564 sub git_get_tags_list
{
2568 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2569 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2570 '--format=%(objectname) %(objecttype) %(refname) '.
2571 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2574 while (my $line = <$fd>) {
2578 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2579 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2580 my ($creator, $epoch, $tz) =
2581 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2582 $ref_item{'fullname'} = $name;
2583 $name =~ s!^refs/tags/!!;
2585 $ref_item{'type'} = $type;
2586 $ref_item{'id'} = $id;
2587 $ref_item{'name'} = $name;
2588 if ($type eq "tag") {
2589 $ref_item{'subject'} = $title;
2590 $ref_item{'reftype'} = $reftype;
2591 $ref_item{'refid'} = $refid;
2593 $ref_item{'reftype'} = $type;
2594 $ref_item{'refid'} = $id;
2597 if ($type eq "tag" || $type eq "commit") {
2598 $ref_item{'epoch'} = $epoch;
2600 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2602 $ref_item{'age'} = "unknown";
2606 push @tagslist, \
%ref_item;
2610 return wantarray ? @tagslist : \
@tagslist;
2613 ## ----------------------------------------------------------------------
2614 ## filesystem-related functions
2616 sub get_file_owner
{
2619 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2620 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2621 if (!defined $gcos) {
2625 $owner =~ s/[,;].*$//;
2626 return to_utf8
($owner);
2629 ## ......................................................................
2630 ## mimetype related functions
2632 sub mimetype_guess_file
{
2633 my $filename = shift;
2634 my $mimemap = shift;
2635 -r
$mimemap or return undef;
2638 open(MIME
, $mimemap) or return undef;
2640 next if m/^#/; # skip comments
2641 my ($mime, $exts) = split(/\t+/);
2642 if (defined $exts) {
2643 my @exts = split(/\s+/, $exts);
2644 foreach my $ext (@exts) {
2645 $mimemap{$ext} = $mime;
2651 $filename =~ /\.([^.]*)$/;
2652 return $mimemap{$1};
2655 sub mimetype_guess
{
2656 my $filename = shift;
2658 $filename =~ /\./ or return undef;
2660 if ($mimetypes_file) {
2661 my $file = $mimetypes_file;
2662 if ($file !~ m!^/!) { # if it is relative path
2663 # it is relative to project
2664 $file = "$projectroot/$project/$file";
2666 $mime = mimetype_guess_file
($filename, $file);
2668 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2674 my $filename = shift;
2677 my $mime = mimetype_guess
($filename);
2678 $mime and return $mime;
2682 return $default_blob_plain_mimetype unless $fd;
2685 return 'text/plain';
2686 } elsif (! $filename) {
2687 return 'application/octet-stream';
2688 } elsif ($filename =~ m/\.png$/i) {
2690 } elsif ($filename =~ m/\.gif$/i) {
2692 } elsif ($filename =~ m/\.jpe?g$/i) {
2693 return 'image/jpeg';
2695 return 'application/octet-stream';
2699 sub blob_contenttype
{
2700 my ($fd, $file_name, $type) = @_;
2702 $type ||= blob_mimetype
($fd, $file_name);
2703 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2704 $type .= "; charset=$default_text_plain_charset";
2710 ## ======================================================================
2711 ## functions printing HTML: header, footer, error page
2713 sub git_header_html
{
2714 my $status = shift || "200 OK";
2715 my $expires = shift;
2717 my $title = "$site_name";
2718 if (defined $project) {
2719 $title .= " - " . to_utf8
($project);
2720 if (defined $action) {
2721 $title .= "/$action";
2722 if (defined $file_name) {
2723 $title .= " - " . esc_path
($file_name);
2724 if ($action eq "tree" && $file_name !~ m
|/$|) {
2731 # require explicit support from the UA if we are to send the page as
2732 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2733 # we have to do this because MSIE sometimes globs '*/*', pretending to
2734 # support xhtml+xml but choking when it gets what it asked for.
2735 if (defined $cgi->http('HTTP_ACCEPT') &&
2736 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2737 $cgi->Accept('application/xhtml+xml') != 0) {
2738 $content_type = 'application/xhtml+xml';
2740 $content_type = 'text/html';
2742 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2743 -status
=> $status, -expires
=> $expires);
2744 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2746 <?xml version="1.0" encoding="utf-8"?>
2747 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2748 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2749 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2750 <!-- git core binaries version $git_version -->
2752 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2753 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2754 <meta name="robots" content="index, nofollow"/>
2755 <title>$title</title>
2757 # print out each stylesheet that exist
2758 if (defined $stylesheet) {
2759 #provides backwards capability for those people who define style sheet in a config file
2760 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2762 foreach my $stylesheet (@stylesheets) {
2763 next unless $stylesheet;
2764 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2767 if (defined $project) {
2768 my %href_params = get_feed_info
();
2769 if (!exists $href_params{'-title'}) {
2770 $href_params{'-title'} = 'log';
2773 foreach my $format qw(RSS Atom) {
2774 my $type = lc($format);
2776 '-rel' => 'alternate',
2777 '-title' => "$project - $href_params{'-title'} - $format feed",
2778 '-type' => "application/$type+xml"
2781 $href_params{'action'} = $type;
2782 $link_attr{'-href'} = href
(%href_params);
2784 "rel=\"$link_attr{'-rel'}\" ".
2785 "title=\"$link_attr{'-title'}\" ".
2786 "href=\"$link_attr{'-href'}\" ".
2787 "type=\"$link_attr{'-type'}\" ".
2790 $href_params{'extra_options'} = '--no-merges';
2791 $link_attr{'-href'} = href
(%href_params);
2792 $link_attr{'-title'} .= ' (no merges)';
2794 "rel=\"$link_attr{'-rel'}\" ".
2795 "title=\"$link_attr{'-title'}\" ".
2796 "href=\"$link_attr{'-href'}\" ".
2797 "type=\"$link_attr{'-type'}\" ".
2802 printf('<link rel="alternate" title="%s projects list" '.
2803 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2804 $site_name, href
(project
=>undef, action
=>"project_index"));
2805 printf('<link rel="alternate" title="%s projects feeds" '.
2806 'href="%s" type="text/x-opml" />'."\n",
2807 $site_name, href
(project
=>undef, action
=>"opml"));
2809 if (defined $favicon) {
2810 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2816 if (-f
$site_header) {
2817 open (my $fd, $site_header);
2822 print "<div class=\"page_header\">\n" .
2823 $cgi->a({-href
=> esc_url
($logo_url),
2824 -title
=> $logo_label},
2825 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2826 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2827 if (defined $project) {
2828 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2829 if (defined $action) {
2836 my ($have_search) = gitweb_check_feature
('search');
2837 if (defined $project && $have_search) {
2838 if (!defined $searchtext) {
2842 if (defined $hash_base) {
2843 $search_hash = $hash_base;
2844 } elsif (defined $hash) {
2845 $search_hash = $hash;
2847 $search_hash = "HEAD";
2849 my $action = $my_uri;
2850 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2851 if ($use_pathinfo) {
2852 $action .= "/".esc_url
($project);
2854 print $cgi->startform(-method => "get", -action
=> $action) .
2855 "<div class=\"search\">\n" .
2857 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2858 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2859 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2860 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2861 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2862 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2864 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2865 "<span title=\"Extended regular expression\">" .
2866 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2867 -checked
=> $search_use_regexp) .
2870 $cgi->end_form() . "\n";
2874 sub git_footer_html
{
2875 my $feed_class = 'rss_logo';
2877 print "<div class=\"page_footer\">\n";
2878 if (defined $project) {
2879 my $descr = git_get_project_description
($project);
2880 if (defined $descr) {
2881 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2884 my %href_params = get_feed_info
();
2885 if (!%href_params) {
2886 $feed_class .= ' generic';
2888 $href_params{'-title'} ||= 'log';
2890 foreach my $format qw(RSS Atom) {
2891 $href_params{'action'} = lc($format);
2892 print $cgi->a({-href
=> href
(%href_params),
2893 -title
=> "$href_params{'-title'} $format feed",
2894 -class => $feed_class}, $format)."\n";
2898 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2899 -class => $feed_class}, "OPML") . " ";
2900 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2901 -class => $feed_class}, "TXT") . "\n";
2903 print "</div>\n"; # class="page_footer"
2905 if (-f
$site_footer) {
2906 open (my $fd, $site_footer);
2915 # die_error(<http_status_code>, <error_message>)
2916 # Example: die_error(404, 'Hash not found')
2917 # By convention, use the following status codes (as defined in RFC 2616):
2918 # 400: Invalid or missing CGI parameters, or
2919 # requested object exists but has wrong type.
2920 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2921 # this server or project.
2922 # 404: Requested object/revision/project doesn't exist.
2923 # 500: The server isn't configured properly, or
2924 # an internal error occurred (e.g. failed assertions caused by bugs), or
2925 # an unknown error occurred (e.g. the git binary died unexpectedly).
2927 my $status = shift || 500;
2928 my $error = shift || "Internal server error";
2930 my %http_responses = (400 => '400 Bad Request',
2931 403 => '403 Forbidden',
2932 404 => '404 Not Found',
2933 500 => '500 Internal Server Error');
2934 git_header_html
($http_responses{$status});
2936 <div class="page_body">
2946 ## ----------------------------------------------------------------------
2947 ## functions printing or outputting HTML: navigation
2949 sub git_print_page_nav
{
2950 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2951 $extra = '' if !defined $extra; # pager or formats
2953 my @navs = qw(summary shortlog log commit commitdiff tree);
2955 @navs = grep { $_ ne $suppress } @navs;
2958 my %arg = map { $_ => {action
=>$_} } @navs;
2959 if (defined $head) {
2960 for (qw(commit commitdiff)) {
2961 $arg{$_}{'hash'} = $head;
2963 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2964 for (qw(shortlog log)) {
2965 $arg{$_}{'hash'} = $head;
2970 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2971 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2973 my @actions = gitweb_check_feature
('actions');
2976 'n' => $project, # project name
2977 'f' => $git_dir, # project path within filesystem
2978 'h' => $treehead || '', # current hash ('h' parameter)
2979 'b' => $treebase || '', # hash base ('hb' parameter)
2982 my ($label, $link, $pos) = splice(@actions,0,3);
2984 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2986 $link =~ s/%([%nfhb])/$repl{$1}/g;
2987 $arg{$label}{'_href'} = $link;
2990 print "<div class=\"page_nav\">\n" .
2992 map { $_ eq $current ?
2993 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ? $arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
2995 print "<br/>\n$extra<br/>\n" .
2999 sub format_paging_nav
{
3000 my ($action, $hash, $head, $page, $has_next_link) = @_;
3004 if ($hash ne $head || $page) {
3005 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
3007 $paging_nav .= "HEAD";
3011 $paging_nav .= " ⋅ " .
3012 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
3013 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
3015 $paging_nav .= " ⋅ prev";
3018 if ($has_next_link) {
3019 $paging_nav .= " ⋅ " .
3020 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
3021 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
3023 $paging_nav .= " ⋅ next";
3029 ## ......................................................................
3030 ## functions printing or outputting HTML: div
3032 sub git_print_header_div
{
3033 my ($action, $title, $hash, $hash_base) = @_;
3036 $args{'action'} = $action;
3037 $args{'hash'} = $hash if $hash;
3038 $args{'hash_base'} = $hash_base if $hash_base;
3040 print "<div class=\"header\">\n" .
3041 $cgi->a({-href
=> href
(%args), -class => "title"},
3042 $title ? $title : $action) .
3046 #sub git_print_authorship (\%) {
3047 sub git_print_authorship
{
3050 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
3051 print "<div class=\"author_date\">" .
3052 esc_html
($co->{'author_name'}) .
3054 if ($ad{'hour_local'} < 6) {
3055 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3056 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3058 printf(" (%02d:%02d %s)",
3059 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3064 sub git_print_page_path
{
3070 print "<div class=\"page_path\">";
3071 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
3072 -title
=> 'tree root'}, to_utf8
("[$project]"));
3074 if (defined $name) {
3075 my @dirname = split '/', $name;
3076 my $basename = pop @dirname;
3079 foreach my $dir (@dirname) {
3080 $fullname .= ($fullname ? '/' : '') . $dir;
3081 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
3083 -title
=> $fullname}, esc_path
($dir));
3086 if (defined $type && $type eq 'blob') {
3087 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
3089 -title
=> $name}, esc_path
($basename));
3090 } elsif (defined $type && $type eq 'tree') {
3091 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
3093 -title
=> $name}, esc_path
($basename));
3096 print esc_path
($basename);
3099 print "<br/></div>\n";
3102 # sub git_print_log (\@;%) {
3103 sub git_print_log
($;%) {
3107 if ($opts{'-remove_title'}) {
3108 # remove title, i.e. first line of log
3111 # remove leading empty lines
3112 while (defined $log->[0] && $log->[0] eq "") {
3119 foreach my $line (@$log) {
3120 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3123 if (! $opts{'-remove_signoff'}) {
3124 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
3127 # remove signoff lines
3134 # print only one empty line
3135 # do not print empty line after signoff
3137 next if ($empty || $signoff);
3143 print format_log_line_html
($line) . "<br/>\n";
3146 if ($opts{'-final_empty_line'}) {
3147 # end with single empty line
3148 print "<br/>\n" unless $empty;
3152 # return link target (what link points to)
3153 sub git_get_link_target
{
3158 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
3162 $link_target = <$fd>;
3167 return $link_target;
3170 # given link target, and the directory (basedir) the link is in,
3171 # return target of link relative to top directory (top tree);
3172 # return undef if it is not possible (including absolute links).
3173 sub normalize_link_target
{
3174 my ($link_target, $basedir, $hash_base) = @_;
3176 # we can normalize symlink target only if $hash_base is provided
3177 return unless $hash_base;
3179 # absolute symlinks (beginning with '/') cannot be normalized
3180 return if (substr($link_target, 0, 1) eq '/');
3182 # normalize link target to path from top (root) tree (dir)
3185 $path = $basedir . '/' . $link_target;
3187 # we are in top (root) tree (dir)
3188 $path = $link_target;
3191 # remove //, /./, and /../
3193 foreach my $part (split('/', $path)) {
3194 # discard '.' and ''
3195 next if (!$part || $part eq '.');
3197 if ($part eq '..') {
3201 # link leads outside repository (outside top dir)
3205 push @path_parts, $part;
3208 $path = join('/', @path_parts);
3213 # print tree entry (row of git_tree), but without encompassing <tr> element
3214 sub git_print_tree_entry
{
3215 my ($t, $basedir, $hash_base, $have_blame) = @_;
3218 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3220 # The format of a table row is: mode list link. Where mode is
3221 # the mode of the entry, list is the name of the entry, an href,
3222 # and link is the action links of the entry.
3224 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3225 if ($t->{'type'} eq "blob") {
3226 print "<td class=\"list\">" .
3227 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3228 file_name
=>"$basedir$t->{'name'}", %base_key),
3229 -class => "list"}, esc_path
($t->{'name'}));
3230 if (S_ISLNK
(oct $t->{'mode'})) {
3231 my $link_target = git_get_link_target
($t->{'hash'});
3233 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3234 if (defined $norm_target) {
3236 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3237 file_name
=>$norm_target),
3238 -title
=> $norm_target}, esc_path
($link_target));
3240 print " -> " . esc_path
($link_target);
3245 print "<td class=\"link\">";
3246 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3247 file_name
=>"$basedir$t->{'name'}", %base_key)},
3251 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3252 file_name
=>"$basedir$t->{'name'}", %base_key)},
3255 if (defined $hash_base) {
3257 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3258 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3262 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3263 file_name
=>"$basedir$t->{'name'}")},
3267 } elsif ($t->{'type'} eq "tree") {
3268 print "<td class=\"list\">";
3269 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3270 file_name
=>"$basedir$t->{'name'}", %base_key)},
3271 esc_path
($t->{'name'}));
3273 print "<td class=\"link\">";
3274 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3275 file_name
=>"$basedir$t->{'name'}", %base_key)},
3277 if (defined $hash_base) {
3279 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3280 file_name
=>"$basedir$t->{'name'}")},
3285 # unknown object: we can only present history for it
3286 # (this includes 'commit' object, i.e. submodule support)
3287 print "<td class=\"list\">" .
3288 esc_path
($t->{'name'}) .
3290 print "<td class=\"link\">";
3291 if (defined $hash_base) {
3292 print $cgi->a({-href
=> href
(action
=>"history",
3293 hash_base
=>$hash_base,
3294 file_name
=>"$basedir$t->{'name'}")},
3301 ## ......................................................................
3302 ## functions printing large fragments of HTML
3304 # get pre-image filenames for merge (combined) diff
3305 sub fill_from_file_info
{
3306 my ($diff, @parents) = @_;
3308 $diff->{'from_file'} = [ ];
3309 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3310 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3311 if ($diff->{'status'}[$i] eq 'R' ||
3312 $diff->{'status'}[$i] eq 'C') {
3313 $diff->{'from_file'}[$i] =
3314 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3321 # is current raw difftree line of file deletion
3323 my $diffinfo = shift;
3325 return $diffinfo->{'to_id'} eq ('0' x
40);
3328 # does patch correspond to [previous] difftree raw line
3329 # $diffinfo - hashref of parsed raw diff format
3330 # $patchinfo - hashref of parsed patch diff format
3331 # (the same keys as in $diffinfo)
3332 sub is_patch_split
{
3333 my ($diffinfo, $patchinfo) = @_;
3335 return defined $diffinfo && defined $patchinfo
3336 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3340 sub git_difftree_body
{
3341 my ($difftree, $hash, @parents) = @_;
3342 my ($parent) = $parents[0];
3343 my ($have_blame) = gitweb_check_feature
('blame');
3344 print "<div class=\"list_head\">\n";
3345 if ($#{$difftree} > 10) {
3346 print(($#{$difftree} + 1) . " files changed:\n");
3350 print "<table class=\"" .
3351 (@parents > 1 ? "combined " : "") .
3354 # header only for combined diff in 'commitdiff' view
3355 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3358 print "<thead><tr>\n" .
3359 "<th></th><th></th>\n"; # filename, patchN link
3360 for (my $i = 0; $i < @parents; $i++) {
3361 my $par = $parents[$i];
3363 $cgi->a({-href
=> href
(action
=>"commitdiff",
3364 hash
=>$hash, hash_parent
=>$par),
3365 -title
=> 'commitdiff to parent number ' .
3366 ($i+1) . ': ' . substr($par,0,7)},
3370 print "</tr></thead>\n<tbody>\n";
3375 foreach my $line (@{$difftree}) {
3376 my $diff = parsed_difftree_line
($line);
3379 print "<tr class=\"dark\">\n";
3381 print "<tr class=\"light\">\n";
3385 if (exists $diff->{'nparents'}) { # combined diff
3387 fill_from_file_info
($diff, @parents)
3388 unless exists $diff->{'from_file'};
3390 if (!is_deleted
($diff)) {
3391 # file exists in the result (child) commit
3393 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3394 file_name
=>$diff->{'to_file'},
3396 -class => "list"}, esc_path
($diff->{'to_file'})) .
3400 esc_path
($diff->{'to_file'}) .
3404 if ($action eq 'commitdiff') {
3407 print "<td class=\"link\">" .
3408 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3413 my $has_history = 0;
3414 my $not_deleted = 0;
3415 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3416 my $hash_parent = $parents[$i];
3417 my $from_hash = $diff->{'from_id'}[$i];
3418 my $from_path = $diff->{'from_file'}[$i];
3419 my $status = $diff->{'status'}[$i];
3421 $has_history ||= ($status ne 'A');
3422 $not_deleted ||= ($status ne 'D');
3424 if ($status eq 'A') {
3425 print "<td class=\"link\" align=\"right\"> | </td>\n";
3426 } elsif ($status eq 'D') {
3427 print "<td class=\"link\">" .
3428 $cgi->a({-href
=> href
(action
=>"blob",
3431 file_name
=>$from_path)},
3435 if ($diff->{'to_id'} eq $from_hash) {
3436 print "<td class=\"link nochange\">";
3438 print "<td class=\"link\">";
3440 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3441 hash
=>$diff->{'to_id'},
3442 hash_parent
=>$from_hash,
3444 hash_parent_base
=>$hash_parent,
3445 file_name
=>$diff->{'to_file'},
3446 file_parent
=>$from_path)},
3452 print "<td class=\"link\">";
3454 print $cgi->a({-href
=> href
(action
=>"blob",
3455 hash
=>$diff->{'to_id'},
3456 file_name
=>$diff->{'to_file'},
3459 print " | " if ($has_history);
3462 print $cgi->a({-href
=> href
(action
=>"history",
3463 file_name
=>$diff->{'to_file'},
3470 next; # instead of 'else' clause, to avoid extra indent
3472 # else ordinary diff
3474 my ($to_mode_oct, $to_mode_str, $to_file_type);
3475 my ($from_mode_oct, $from_mode_str, $from_file_type);
3476 if ($diff->{'to_mode'} ne ('0' x
6)) {
3477 $to_mode_oct = oct $diff->{'to_mode'};
3478 if (S_ISREG
($to_mode_oct)) { # only for regular file
3479 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3481 $to_file_type = file_type
($diff->{'to_mode'});
3483 if ($diff->{'from_mode'} ne ('0' x
6)) {
3484 $from_mode_oct = oct $diff->{'from_mode'};
3485 if (S_ISREG
($to_mode_oct)) { # only for regular file
3486 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3488 $from_file_type = file_type
($diff->{'from_mode'});
3491 if ($diff->{'status'} eq "A") { # created
3492 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3493 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3494 $mode_chng .= "]</span>";
3496 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3497 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3498 -class => "list"}, esc_path
($diff->{'file'}));
3500 print "<td>$mode_chng</td>\n";
3501 print "<td class=\"link\">";
3502 if ($action eq 'commitdiff') {
3505 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3508 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3509 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3513 } elsif ($diff->{'status'} eq "D") { # deleted
3514 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3516 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3517 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3518 -class => "list"}, esc_path
($diff->{'file'}));
3520 print "<td>$mode_chng</td>\n";
3521 print "<td class=\"link\">";
3522 if ($action eq 'commitdiff') {
3525 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3528 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3529 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3532 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3533 file_name
=>$diff->{'file'})},
3536 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3537 file_name
=>$diff->{'file'})},
3541 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3542 my $mode_chnge = "";
3543 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3544 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3545 if ($from_file_type ne $to_file_type) {
3546 $mode_chnge .= " from $from_file_type to $to_file_type";
3548 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3549 if ($from_mode_str && $to_mode_str) {
3550 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3551 } elsif ($to_mode_str) {
3552 $mode_chnge .= " mode: $to_mode_str";
3555 $mode_chnge .= "]</span>\n";
3558 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3559 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3560 -class => "list"}, esc_path
($diff->{'file'}));
3562 print "<td>$mode_chnge</td>\n";
3563 print "<td class=\"link\">";
3564 if ($action eq 'commitdiff') {
3567 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3569 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3570 # "commit" view and modified file (not onlu mode changed)
3571 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3572 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3573 hash_base
=>$hash, hash_parent_base
=>$parent,
3574 file_name
=>$diff->{'file'})},
3578 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3579 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3582 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3583 file_name
=>$diff->{'file'})},
3586 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3587 file_name
=>$diff->{'file'})},
3591 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3592 my %status_name = ('R' => 'moved', 'C' => 'copied');
3593 my $nstatus = $status_name{$diff->{'status'}};
3595 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3596 # mode also for directories, so we cannot use $to_mode_str
3597 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3600 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3601 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3602 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3603 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3604 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3605 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3606 -class => "list"}, esc_path
($diff->{'from_file'})) .
3607 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3608 "<td class=\"link\">";
3609 if ($action eq 'commitdiff') {
3612 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3614 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3615 # "commit" view and modified file (not only pure rename or copy)
3616 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3617 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3618 hash_base
=>$hash, hash_parent_base
=>$parent,
3619 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3623 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3624 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3627 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3628 file_name
=>$diff->{'to_file'})},
3631 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3632 file_name
=>$diff->{'to_file'})},
3636 } # we should not encounter Unmerged (U) or Unknown (X) status
3639 print "</tbody>" if $has_header;
3643 sub git_patchset_body
{
3644 my ($fd, $difftree, $hash, @hash_parents) = @_;
3645 my ($hash_parent) = $hash_parents[0];
3647 my $is_combined = (@hash_parents > 1);
3649 my $patch_number = 0;
3655 print "<div class=\"patchset\">\n";
3657 # skip to first patch
3658 while ($patch_line = <$fd>) {
3661 last if ($patch_line =~ m/^diff /);
3665 while ($patch_line) {
3667 # parse "git diff" header line
3668 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3669 # $1 is from_name, which we do not use
3670 $to_name = unquote
($2);
3671 $to_name =~ s!^b/!!;
3672 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3673 # $1 is 'cc' or 'combined', which we do not use
3674 $to_name = unquote
($2);
3679 # check if current patch belong to current raw line
3680 # and parse raw git-diff line if needed
3681 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3682 # this is continuation of a split patch
3683 print "<div class=\"patch cont\">\n";
3685 # advance raw git-diff output if needed
3686 $patch_idx++ if defined $diffinfo;
3688 # read and prepare patch information
3689 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3691 # compact combined diff output can have some patches skipped
3692 # find which patch (using pathname of result) we are at now;
3694 while ($to_name ne $diffinfo->{'to_file'}) {
3695 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3696 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3697 "</div>\n"; # class="patch"
3702 last if $patch_idx > $#$difftree;
3703 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3707 # modifies %from, %to hashes
3708 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3710 # this is first patch for raw difftree line with $patch_idx index
3711 # we index @$difftree array from 0, but number patches from 1
3712 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3716 #assert($patch_line =~ m/^diff /) if DEBUG;
3717 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3719 # print "git diff" header
3720 print format_git_diff_header_line
($patch_line, $diffinfo,
3723 # print extended diff header
3724 print "<div class=\"diff extended_header\">\n";
3726 while ($patch_line = <$fd>) {
3729 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3731 print format_extended_diff_header_line
($patch_line, $diffinfo,
3734 print "</div>\n"; # class="diff extended_header"
3736 # from-file/to-file diff header
3737 if (! $patch_line) {
3738 print "</div>\n"; # class="patch"
3741 next PATCH
if ($patch_line =~ m/^diff /);
3742 #assert($patch_line =~ m/^---/) if DEBUG;
3744 my $last_patch_line = $patch_line;
3745 $patch_line = <$fd>;
3747 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3749 print format_diff_from_to_header
($last_patch_line, $patch_line,
3750 $diffinfo, \
%from, \
%to,
3755 while ($patch_line = <$fd>) {
3758 next PATCH
if ($patch_line =~ m/^diff /);
3760 print format_diff_line
($patch_line, \
%from, \
%to);
3764 print "</div>\n"; # class="patch"
3767 # for compact combined (--cc) format, with chunk and patch simpliciaction
3768 # patchset might be empty, but there might be unprocessed raw lines
3769 for (++$patch_idx if $patch_number > 0;
3770 $patch_idx < @$difftree;
3772 # read and prepare patch information
3773 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3775 # generate anchor for "patch" links in difftree / whatchanged part
3776 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3777 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3778 "</div>\n"; # class="patch"
3783 if ($patch_number == 0) {
3784 if (@hash_parents > 1) {
3785 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3787 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3791 print "</div>\n"; # class="patchset"
3794 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3796 # fills project list info (age, description, owner, forks) for each
3797 # project in the list, removing invalid projects from returned list
3798 # NOTE: modifies $projlist, but does not remove entries from it
3799 sub fill_project_list_info
{
3800 my ($projlist, $check_forks) = @_;
3803 my $show_ctags = gitweb_check_feature
('ctags');
3805 foreach my $pr (@$projlist) {
3806 my (@activity) = git_get_last_activity
($pr->{'path'});
3807 unless (@activity) {
3810 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3811 if (!defined $pr->{'descr'}) {
3812 my $descr = git_get_project_description
($pr->{'path'}) || "";
3813 $descr = to_utf8
($descr);
3814 $pr->{'descr_long'} = $descr;
3815 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3817 if (!defined $pr->{'owner'}) {
3818 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3821 my $pname = $pr->{'path'};
3822 if (($pname =~ s/\.git$//) &&
3823 ($pname !~ /\/$/) &&
3824 (-d
"$projectroot/$pname")) {
3825 $pr->{'forks'} = "-d $projectroot/$pname";
3830 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
3831 push @projects, $pr;
3837 # print 'sort by' <th> element, generating 'sort by $name' replay link
3838 # if that order is not selected
3840 my ($name, $order, $header) = @_;
3841 $header ||= ucfirst($name);
3843 if ($order eq $name) {
3844 print "<th>$header</th>\n";
3847 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3848 -class => "header"}, $header) .
3853 sub git_project_list_body
{
3854 # actually uses global variable $project
3855 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3857 my ($check_forks) = gitweb_check_feature
('forks');
3858 my @projects = fill_project_list_info
($projlist, $check_forks);
3860 $order ||= $default_projects_order;
3861 $from = 0 unless defined $from;
3862 $to = $#projects if (!defined $to || $#projects < $to);
3865 project
=> { key
=> 'path', type
=> 'str' },
3866 descr
=> { key
=> 'descr_long', type
=> 'str' },
3867 owner
=> { key
=> 'owner', type
=> 'str' },
3868 age
=> { key
=> 'age', type
=> 'num' }
3870 my $oi = $order_info{$order};
3871 if ($oi->{'type'} eq 'str') {
3872 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3874 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3877 my $show_ctags = gitweb_check_feature
('ctags');
3880 foreach my $p (@projects) {
3881 foreach my $ct (keys %{$p->{'ctags'}}) {
3882 $ctags{$ct} += $p->{'ctags'}->{$ct};
3885 my $cloud = git_populate_project_tagcloud
(\
%ctags);
3886 print git_show_project_tagcloud
($cloud, 64);
3889 print "<table class=\"project_list\">\n";
3890 unless ($no_header) {
3893 print "<th></th>\n";
3895 print_sort_th
('project', $order, 'Project');
3896 print_sort_th
('descr', $order, 'Description');
3897 print_sort_th
('owner', $order, 'Owner');
3898 print_sort_th
('age', $order, 'Last Change');
3899 print "<th></th>\n" . # for links
3903 my $tagfilter = $cgi->param('by_tag');
3904 for (my $i = $from; $i <= $to; $i++) {
3905 my $pr = $projects[$i];
3907 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3908 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3909 and not $pr->{'descr_long'} =~ /$searchtext/;
3910 # Weed out forks or non-matching entries of search
3912 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s
#\.git$#/#;
3913 $forkbase="^$forkbase" if $forkbase;
3914 next if not $searchtext and not $tagfilter and $show_ctags
3915 and $pr->{'path'} =~ m
#$forkbase.*/.*#; # regexp-safe
3919 print "<tr class=\"dark\">\n";
3921 print "<tr class=\"light\">\n";
3926 if ($pr->{'forks'}) {
3927 print "<!-- $pr->{'forks'} -->\n";
3928 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3932 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3933 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3934 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3935 -class => "list", -title
=> $pr->{'descr_long'}},
3936 esc_html
($pr->{'descr'})) . "</td>\n" .
3937 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3938 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3939 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3940 "<td class=\"link\">" .
3941 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3942 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3943 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3944 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3945 ($pr->{'forks'} ? " | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3949 if (defined $extra) {
3952 print "<td></td>\n";
3954 print "<td colspan=\"5\">$extra</td>\n" .
3960 sub git_shortlog_body
{
3961 # uses global variable $project
3962 my ($commitlist, $from, $to, $refs, $extra) = @_;
3964 $from = 0 unless defined $from;
3965 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3967 print "<table class=\"shortlog\">\n";
3969 for (my $i = $from; $i <= $to; $i++) {
3970 my %co = %{$commitlist->[$i]};
3971 my $commit = $co{'id'};
3972 my $ref = format_ref_marker
($refs, $commit);
3974 print "<tr class=\"dark\">\n";
3976 print "<tr class=\"light\">\n";
3979 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3980 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3981 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3982 "<td><i>" . $author . "</i></td>\n" .
3984 print format_subject_html
($co{'title'}, $co{'title_short'},
3985 href
(action
=>"commit", hash
=>$commit), $ref);
3987 "<td class=\"link\">" .
3988 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3989 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3990 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3991 my $snapshot_links = format_snapshot_links
($commit);
3992 if (defined $snapshot_links) {
3993 print " | " . $snapshot_links;
3998 if (defined $extra) {
4000 "<td colspan=\"4\">$extra</td>\n" .
4006 sub git_history_body
{
4007 # Warning: assumes constant type (blob or tree) during history
4008 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4010 $from = 0 unless defined $from;
4011 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4013 print "<table class=\"history\">\n";
4015 for (my $i = $from; $i <= $to; $i++) {
4016 my %co = %{$commitlist->[$i]};
4020 my $commit = $co{'id'};
4022 my $ref = format_ref_marker
($refs, $commit);
4025 print "<tr class=\"dark\">\n";
4027 print "<tr class=\"light\">\n";
4030 # shortlog uses chop_str($co{'author_name'}, 10)
4031 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
4032 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4033 "<td><i>" . $author . "</i></td>\n" .
4035 # originally git_history used chop_str($co{'title'}, 50)
4036 print format_subject_html
($co{'title'}, $co{'title_short'},
4037 href
(action
=>"commit", hash
=>$commit), $ref);
4039 "<td class=\"link\">" .
4040 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
4041 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
4043 if ($ftype eq 'blob') {
4044 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
4045 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
4046 if (defined $blob_current && defined $blob_parent &&
4047 $blob_current ne $blob_parent) {
4049 $cgi->a({-href
=> href
(action
=>"blobdiff",
4050 hash
=>$blob_current, hash_parent
=>$blob_parent,
4051 hash_base
=>$hash_base, hash_parent_base
=>$commit,
4052 file_name
=>$file_name)},
4059 if (defined $extra) {
4061 "<td colspan=\"4\">$extra</td>\n" .
4068 # uses global variable $project
4069 my ($taglist, $from, $to, $extra) = @_;
4070 $from = 0 unless defined $from;
4071 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4073 print "<table class=\"tags\">\n";
4075 for (my $i = $from; $i <= $to; $i++) {
4076 my $entry = $taglist->[$i];
4078 my $comment = $tag{'subject'};
4080 if (defined $comment) {
4081 $comment_short = chop_str
($comment, 30, 5);
4084 print "<tr class=\"dark\">\n";
4086 print "<tr class=\"light\">\n";
4089 if (defined $tag{'age'}) {
4090 print "<td><i>$tag{'age'}</i></td>\n";
4092 print "<td></td>\n";
4095 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
4096 -class => "list name"}, esc_html
($tag{'name'})) .
4099 if (defined $comment) {
4100 print format_subject_html
($comment, $comment_short,
4101 href
(action
=>"tag", hash
=>$tag{'id'}));
4104 "<td class=\"selflink\">";
4105 if ($tag{'type'} eq "tag") {
4106 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
4111 "<td class=\"link\">" . " | " .
4112 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
4113 if ($tag{'reftype'} eq "commit") {
4114 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
4115 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
4116 } elsif ($tag{'reftype'} eq "blob") {
4117 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
4122 if (defined $extra) {
4124 "<td colspan=\"5\">$extra</td>\n" .
4130 sub git_heads_body
{
4131 # uses global variable $project
4132 my ($headlist, $head, $from, $to, $extra) = @_;
4133 $from = 0 unless defined $from;
4134 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4136 print "<table class=\"heads\">\n";
4138 for (my $i = $from; $i <= $to; $i++) {
4139 my $entry = $headlist->[$i];
4141 my $curr = $ref{'id'} eq $head;
4143 print "<tr class=\"dark\">\n";
4145 print "<tr class=\"light\">\n";
4148 print "<td><i>$ref{'age'}</i></td>\n" .
4149 ($curr ? "<td class=\"current_head\">" : "<td>") .
4150 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
4151 -class => "list name"},esc_html
($ref{'name'})) .
4153 "<td class=\"link\">" .
4154 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
4155 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
4156 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
4160 if (defined $extra) {
4162 "<td colspan=\"3\">$extra</td>\n" .
4168 sub git_search_grep_body
{
4169 my ($commitlist, $from, $to, $extra) = @_;
4170 $from = 0 unless defined $from;
4171 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4173 print "<table class=\"commit_search\">\n";
4175 for (my $i = $from; $i <= $to; $i++) {
4176 my %co = %{$commitlist->[$i]};
4180 my $commit = $co{'id'};
4182 print "<tr class=\"dark\">\n";
4184 print "<tr class=\"light\">\n";
4187 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
4188 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4189 "<td><i>" . $author . "</i></td>\n" .
4191 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
4192 -class => "list subject"},
4193 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
4194 my $comment = $co{'comment'};
4195 foreach my $line (@$comment) {
4196 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4197 my ($lead, $match, $trail) = ($1, $2, $3);
4198 $match = chop_str
($match, 70, 5, 'center');
4199 my $contextlen = int((80 - length($match))/2);
4200 $contextlen = 30 if ($contextlen > 30);
4201 $lead = chop_str
($lead, $contextlen, 10, 'left');
4202 $trail = chop_str
($trail, $contextlen, 10, 'right');
4204 $lead = esc_html
($lead);
4205 $match = esc_html
($match);
4206 $trail = esc_html
($trail);
4208 print "$lead<span class=\"match\">$match</span>$trail<br />";
4212 "<td class=\"link\">" .
4213 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4215 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4217 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4221 if (defined $extra) {
4223 "<td colspan=\"3\">$extra</td>\n" .
4229 ## ======================================================================
4230 ## ======================================================================
4233 sub git_project_list
{
4234 my $order = $input_params{'order'};
4235 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4236 die_error
(400, "Unknown order parameter");
4239 my @list = git_get_projects_list
();
4241 die_error
(404, "No projects found");
4245 if (-f
$home_text) {
4246 print "<div class=\"index_include\">\n";
4247 open (my $fd, $home_text);
4252 print $cgi->startform(-method => "get") .
4253 "<p class=\"projsearch\">Search:\n" .
4254 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
4256 $cgi->end_form() . "\n";
4257 git_project_list_body
(\
@list, $order);
4262 my $order = $input_params{'order'};
4263 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4264 die_error
(400, "Unknown order parameter");
4267 my @list = git_get_projects_list
($project);
4269 die_error
(404, "No forks found");
4273 git_print_page_nav
('','');
4274 git_print_header_div
('summary', "$project forks");
4275 git_project_list_body
(\
@list, $order);
4279 sub git_project_index
{
4280 my @projects = git_get_projects_list
($project);
4283 -type
=> 'text/plain',
4284 -charset
=> 'utf-8',
4285 -content_disposition
=> 'inline; filename="index.aux"');
4287 foreach my $pr (@projects) {
4288 if (!exists $pr->{'owner'}) {
4289 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4292 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4293 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4294 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4295 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4299 print "$path $owner\n";
4304 my $descr = git_get_project_description
($project) || "none";
4305 my %co = parse_commit
("HEAD");
4306 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4307 my $head = $co{'id'};
4309 my $owner = git_get_project_owner
($project);
4311 my $refs = git_get_references
();
4312 # These get_*_list functions return one more to allow us to see if
4313 # there are more ...
4314 my @taglist = git_get_tags_list
(16);
4315 my @headlist = git_get_heads_list
(16);
4317 my ($check_forks) = gitweb_check_feature
('forks');
4320 @forklist = git_get_projects_list
($project);
4324 git_print_page_nav
('summary','', $head);
4326 print "<div class=\"title\"> </div>\n";
4327 print "<table class=\"projects_list\">\n" .
4328 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4329 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4330 if (defined $cd{'rfc2822'}) {
4331 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4334 # use per project git URL list in $projectroot/$project/cloneurl
4335 # or make project git URL from git base URL and project name
4336 my $url_tag = "URL";
4337 my @url_list = git_get_project_url_list
($project);
4338 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4339 foreach my $git_url (@url_list) {
4340 next unless $git_url;
4341 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4346 my $show_ctags = (gitweb_check_feature
('ctags'))[0];
4348 my $ctags = git_get_project_ctags
($project);
4349 my $cloud = git_populate_project_tagcloud
($ctags);
4350 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4351 print "</td>\n<td>" unless %$ctags;
4352 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4353 print "</td>\n<td>" if %$ctags;
4354 print git_show_project_tagcloud
($cloud, 48);
4360 if (-s
"$projectroot/$project/README.html") {
4361 if (open my $fd, "$projectroot/$project/README.html") {
4362 print "<div class=\"title\">readme</div>\n" .
4363 "<div class=\"readme\">\n";
4364 print $_ while (<$fd>);
4365 print "\n</div>\n"; # class="readme"
4370 # we need to request one more than 16 (0..15) to check if
4372 my @commitlist = $head ? parse_commits
($head, 17) : ();
4374 git_print_header_div
('shortlog');
4375 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4376 $#commitlist <= 15 ? undef :
4377 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4381 git_print_header_div
('tags');
4382 git_tags_body
(\
@taglist, 0, 15,
4383 $#taglist <= 15 ? undef :
4384 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4388 git_print_header_div
('heads');
4389 git_heads_body
(\
@headlist, $head, 0, 15,
4390 $#headlist <= 15 ? undef :
4391 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4395 git_print_header_div
('forks');
4396 git_project_list_body
(\
@forklist, 'age', 0, 15,
4397 $#forklist <= 15 ? undef :
4398 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4406 my $head = git_get_head_hash
($project);
4408 git_print_page_nav
('','', $head,undef,$head);
4409 my %tag = parse_tag
($hash);
4412 die_error
(404, "Unknown tag object");
4415 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4416 print "<div class=\"title_text\">\n" .
4417 "<table class=\"object_header\">\n" .
4419 "<td>object</td>\n" .
4420 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4421 $tag{'object'}) . "</td>\n" .
4422 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4423 $tag{'type'}) . "</td>\n" .
4425 if (defined($tag{'author'})) {
4426 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4427 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4428 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4429 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4432 print "</table>\n\n" .
4434 print "<div class=\"page_body\">";
4435 my $comment = $tag{'comment'};
4436 foreach my $line (@$comment) {
4438 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4448 gitweb_check_feature
('blame')
4449 or die_error
(403, "Blame view not allowed");
4451 die_error
(400, "No file name given") unless $file_name;
4452 $hash_base ||= git_get_head_hash
($project);
4453 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4454 my %co = parse_commit
($hash_base)
4455 or die_error
(404, "Commit not found");
4456 if (!defined $hash) {
4457 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4458 or die_error
(404, "Error looking up file");
4460 $ftype = git_get_type
($hash);
4461 if ($ftype !~ "blob") {
4462 die_error
(400, "Object is not a blob");
4464 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4465 $file_name, $hash_base)
4466 or die_error
(500, "Open git-blame failed");
4469 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4472 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4475 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4477 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4478 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4479 git_print_page_path
($file_name, $ftype, $hash_base);
4480 my @rev_color = (qw(light2 dark2));
4481 my $num_colors = scalar(@rev_color);
4482 my $current_color = 0;
4485 <div class="page_body">
4486 <table class="blame">
4487 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4492 last unless defined $_;
4493 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4494 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4495 if (!exists $metainfo{$full_rev}) {
4496 $metainfo{$full_rev} = {};
4498 my $meta = $metainfo{$full_rev};
4501 if (/^(\S+) (.*)$/) {
4507 my $rev = substr($full_rev, 0, 8);
4508 my $author = $meta->{'author'};
4509 my %date = parse_date
($meta->{'author-time'},
4510 $meta->{'author-tz'});
4511 my $date = $date{'iso-tz'};
4513 $current_color = ++$current_color % $num_colors;
4515 print "<tr class=\"$rev_color[$current_color]\">\n";
4517 print "<td class=\"sha1\"";
4518 print " title=\"". esc_html
($author) . ", $date\"";
4519 print " rowspan=\"$group_size\"" if ($group_size > 1);
4521 print $cgi->a({-href
=> href
(action
=>"commit",
4523 file_name
=>$file_name)},
4527 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4528 or die_error
(500, "Open git-rev-parse failed");
4529 my $parent_commit = <$dd>;
4531 chomp($parent_commit);
4532 my $blamed = href
(action
=> 'blame',
4533 file_name
=> $meta->{'filename'},
4534 hash_base
=> $parent_commit);
4535 print "<td class=\"linenr\">";
4536 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4538 -class => "linenr" },
4541 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4547 or print "Reading blob failed\n";
4552 my $head = git_get_head_hash
($project);
4554 git_print_page_nav
('','', $head,undef,$head);
4555 git_print_header_div
('summary', $project);
4557 my @tagslist = git_get_tags_list
();
4559 git_tags_body
(\
@tagslist);
4565 my $head = git_get_head_hash
($project);
4567 git_print_page_nav
('','', $head,undef,$head);
4568 git_print_header_div
('summary', $project);
4570 my @headslist = git_get_heads_list
();
4572 git_heads_body
(\
@headslist, $head);
4577 sub git_blob_plain
{
4581 if (!defined $hash) {
4582 if (defined $file_name) {
4583 my $base = $hash_base || git_get_head_hash
($project);
4584 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4585 or die_error
(404, "Cannot find file");
4587 die_error
(400, "No file name defined");
4589 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4590 # blobs defined by non-textual hash id's can be cached
4594 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4595 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4597 # content-type (can include charset)
4598 $type = blob_contenttype
($fd, $file_name, $type);
4600 # "save as" filename, even when no $file_name is given
4601 my $save_as = "$hash";
4602 if (defined $file_name) {
4603 $save_as = $file_name;
4604 } elsif ($type =~ m/^text\//) {
4610 -expires
=> $expires,
4611 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4613 binmode STDOUT
, ':raw';
4615 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4623 if (!defined $hash) {
4624 if (defined $file_name) {
4625 my $base = $hash_base || git_get_head_hash
($project);
4626 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4627 or die_error
(404, "Cannot find file");
4629 die_error
(400, "No file name defined");
4631 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4632 # blobs defined by non-textual hash id's can be cached
4636 my ($have_blame) = gitweb_check_feature
('blame');
4637 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4638 or die_error
(500, "Couldn't cat $file_name, $hash");
4639 my $mimetype = blob_mimetype
($fd, $file_name);
4640 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4642 return git_blob_plain
($mimetype);
4644 # we can have blame only for text/* mimetype
4645 $have_blame &&= ($mimetype =~ m!^text/!);
4647 git_header_html
(undef, $expires);
4648 my $formats_nav = '';
4649 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4650 if (defined $file_name) {
4653 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4658 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4661 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4664 $cgi->a({-href
=> href
(action
=>"blob",
4665 hash_base
=>"HEAD", file_name
=>$file_name)},
4669 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4672 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4673 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4675 print "<div class=\"page_nav\">\n" .
4676 "<br/><br/></div>\n" .
4677 "<div class=\"title\">$hash</div>\n";
4679 git_print_page_path
($file_name, "blob", $hash_base);
4680 print "<div class=\"page_body\">\n";
4681 if ($mimetype =~ m!^image/!) {
4682 print qq
!<img type
="$mimetype"!;
4684 print qq
! alt
="$file_name" title
="$file_name"!;
4687 href(action=>"blob_plain
", hash=>$hash,
4688 hash_base=>$hash_base, file_name=>$file_name) .
4692 while (my $line = <$fd>) {
4695 $line = untabify
($line);
4696 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4697 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4701 or print "Reading blob failed.\n";
4707 if (!defined $hash_base) {
4708 $hash_base = "HEAD";
4710 if (!defined $hash) {
4711 if (defined $file_name) {
4712 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4717 die_error
(404, "No such tree") unless defined($hash);
4719 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4720 or die_error
(500, "Open git-ls-tree failed");
4721 my @entries = map { chomp; $_ } <$fd>;
4722 close $fd or die_error
(404, "Reading tree failed");
4725 my $refs = git_get_references
();
4726 my $ref = format_ref_marker
($refs, $hash_base);
4729 my ($have_blame) = gitweb_check_feature
('blame');
4730 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4732 if (defined $file_name) {
4734 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4736 $cgi->a({-href
=> href
(action
=>"tree",
4737 hash_base
=>"HEAD", file_name
=>$file_name)},
4740 my $snapshot_links = format_snapshot_links
($hash);
4741 if (defined $snapshot_links) {
4742 # FIXME: Should be available when we have no hash base as well.
4743 push @views_nav, $snapshot_links;
4745 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4746 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4749 print "<div class=\"page_nav\">\n";
4750 print "<br/><br/></div>\n";
4751 print "<div class=\"title\">$hash</div>\n";
4753 if (defined $file_name) {
4754 $basedir = $file_name;
4755 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4758 git_print_page_path
($file_name, 'tree', $hash_base);
4760 print "<div class=\"page_body\">\n";
4761 print "<table class=\"tree\">\n";
4763 # '..' (top directory) link if possible
4764 if (defined $hash_base &&
4765 defined $file_name && $file_name =~ m![^/]+$!) {
4767 print "<tr class=\"dark\">\n";
4769 print "<tr class=\"light\">\n";
4773 my $up = $file_name;
4774 $up =~ s!/?[^/]+$!!;
4775 undef $up unless $up;
4776 # based on git_print_tree_entry
4777 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4778 print '<td class="list">';
4779 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4783 print "<td class=\"link\"></td>\n";
4787 foreach my $line (@entries) {
4788 my %t = parse_ls_tree_line
($line, -z
=> 1);
4791 print "<tr class=\"dark\">\n";
4793 print "<tr class=\"light\">\n";
4797 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4801 print "</table>\n" .
4807 my @supported_fmts = gitweb_check_feature
('snapshot');
4808 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4810 my $format = $input_params{'snapshot_format'};
4811 if (!@supported_fmts) {
4812 die_error
(403, "Snapshots not allowed");
4814 # default to first supported snapshot format
4815 $format ||= $supported_fmts[0];
4816 if ($format !~ m/^[a-z0-9]+$/) {
4817 die_error
(400, "Invalid snapshot format parameter");
4818 } elsif (!exists($known_snapshot_formats{$format})) {
4819 die_error
(400, "Unknown snapshot format");
4820 } elsif (!grep($_ eq $format, @supported_fmts)) {
4821 die_error
(403, "Unsupported snapshot format");
4824 if (!defined $hash) {
4825 $hash = git_get_head_hash
($project);
4828 my $name = $project;
4829 $name =~ s
,([^/])/*\
.git
$,$1,;
4830 $name = basename
($name);
4831 my $filename = to_utf8
($name);
4832 $name =~ s/\047/\047\\\047\047/g;
4834 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4835 $cmd = quote_command
(
4836 git_cmd
(), 'archive',
4837 "--format=$known_snapshot_formats{$format}{'format'}",
4838 "--prefix=$name/", $hash);
4839 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4840 $cmd .= ' | ' . quote_command
(@{$known_snapshot_formats{$format}{'compressor'}});
4844 -type
=> $known_snapshot_formats{$format}{'type'},
4845 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4846 -status
=> '200 OK');
4848 open my $fd, "-|", $cmd
4849 or die_error
(500, "Execute git-archive failed");
4850 binmode STDOUT
, ':raw';
4852 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4857 my $head = git_get_head_hash
($project);
4858 if (!defined $hash) {
4861 if (!defined $page) {
4864 my $refs = git_get_references
();
4866 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4868 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4871 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4874 my %co = parse_commit
($hash);
4876 git_print_header_div
('summary', $project);
4877 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4879 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4880 for (my $i = 0; $i <= $to; $i++) {
4881 my %co = %{$commitlist[$i]};
4883 my $commit = $co{'id'};
4884 my $ref = format_ref_marker
($refs, $commit);
4885 my %ad = parse_date
($co{'author_epoch'});
4886 git_print_header_div
('commit',
4887 "<span class=\"age\">$co{'age_string'}</span>" .
4888 esc_html
($co{'title'}) . $ref,
4890 print "<div class=\"title_text\">\n" .
4891 "<div class=\"log_link\">\n" .
4892 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4894 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4896 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4899 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4902 print "<div class=\"log_body\">\n";
4903 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4906 if ($#commitlist >= 100) {
4907 print "<div class=\"page_nav\">\n";
4908 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4909 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4916 $hash ||= $hash_base || "HEAD";
4917 my %co = parse_commit
($hash)
4918 or die_error
(404, "Unknown commit object");
4919 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4920 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4922 my $parent = $co{'parent'};
4923 my $parents = $co{'parents'}; # listref
4925 # we need to prepare $formats_nav before any parameter munging
4927 if (!defined $parent) {
4929 $formats_nav .= '(initial)';
4930 } elsif (@$parents == 1) {
4931 # single parent commit
4934 $cgi->a({-href
=> href
(action
=>"commit",
4936 esc_html
(substr($parent, 0, 7))) .
4943 $cgi->a({-href
=> href
(action
=>"commit",
4945 esc_html
(substr($_, 0, 7)));
4950 if (!defined $parent) {
4954 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4956 (@$parents <= 1 ? $parent : '-c'),
4958 or die_error
(500, "Open git-diff-tree failed");
4959 @difftree = map { chomp; $_ } <$fd>;
4960 close $fd or die_error
(404, "Reading git-diff-tree failed");
4962 # non-textual hash id's can be cached
4964 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4967 my $refs = git_get_references
();
4968 my $ref = format_ref_marker
($refs, $co{'id'});
4970 git_header_html
(undef, $expires);
4971 git_print_page_nav
('commit', '',
4972 $hash, $co{'tree'}, $hash,
4975 if (defined $co{'parent'}) {
4976 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4978 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4980 print "<div class=\"title_text\">\n" .
4981 "<table class=\"object_header\">\n";
4982 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4984 "<td></td><td> $ad{'rfc2822'}";
4985 if ($ad{'hour_local'} < 6) {
4986 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4987 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4989 printf(" (%02d:%02d %s)",
4990 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4994 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4995 print "<tr><td></td><td> $cd{'rfc2822'}" .
4996 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4998 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5001 "<td class=\"sha1\">" .
5002 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
5003 class => "list"}, $co{'tree'}) .
5005 "<td class=\"link\">" .
5006 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
5008 my $snapshot_links = format_snapshot_links
($hash);
5009 if (defined $snapshot_links) {
5010 print " | " . $snapshot_links;
5015 foreach my $par (@$parents) {
5018 "<td class=\"sha1\">" .
5019 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
5020 class => "list"}, $par) .
5022 "<td class=\"link\">" .
5023 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
5025 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
5032 print "<div class=\"page_body\">\n";
5033 git_print_log
($co{'comment'});
5036 git_difftree_body
(\
@difftree, $hash, @$parents);
5042 # object is defined by:
5043 # - hash or hash_base alone
5044 # - hash_base and file_name
5047 # - hash or hash_base alone
5048 if ($hash || ($hash_base && !defined $file_name)) {
5049 my $object_id = $hash || $hash_base;
5051 open my $fd, "-|", quote_command
(
5052 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5053 or die_error
(404, "Object does not exist");
5057 or die_error
(404, "Object does not exist");
5059 # - hash_base and file_name
5060 } elsif ($hash_base && defined $file_name) {
5061 $file_name =~ s
,/+$,,;
5063 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
5064 or die_error
(404, "Base object does not exist");
5066 # here errors should not hapen
5067 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
5068 or die_error
(500, "Open git-ls-tree failed");
5072 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5073 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5074 die_error
(404, "File or directory for given base does not exist");
5079 die_error
(400, "Not enough information to find object");
5082 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
5083 hash
=>$hash, hash_base
=>$hash_base,
5084 file_name
=>$file_name),
5085 -status
=> '302 Found');
5089 my $format = shift || 'html';
5096 # preparing $fd and %diffinfo for git_patchset_body
5098 if (defined $hash_base && defined $hash_parent_base) {
5099 if (defined $file_name) {
5101 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5102 $hash_parent_base, $hash_base,
5103 "--", (defined $file_parent ? $file_parent : ()), $file_name
5104 or die_error
(500, "Open git-diff-tree failed");
5105 @difftree = map { chomp; $_ } <$fd>;
5107 or die_error
(404, "Reading git-diff-tree failed");
5109 or die_error
(404, "Blob diff not found");
5111 } elsif (defined $hash &&
5112 $hash =~ /[0-9a-fA-F]{40}/) {
5113 # try to find filename from $hash
5115 # read filtered raw output
5116 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5117 $hash_parent_base, $hash_base, "--"
5118 or die_error
(500, "Open git-diff-tree failed");
5120 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5122 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5123 map { chomp; $_ } <$fd>;
5125 or die_error
(404, "Reading git-diff-tree failed");
5127 or die_error
(404, "Blob diff not found");
5130 die_error
(400, "Missing one of the blob diff parameters");
5133 if (@difftree > 1) {
5134 die_error
(400, "Ambiguous blob diff specification");
5137 %diffinfo = parse_difftree_raw_line
($difftree[0]);
5138 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5139 $file_name ||= $diffinfo{'to_file'};
5141 $hash_parent ||= $diffinfo{'from_id'};
5142 $hash ||= $diffinfo{'to_id'};
5144 # non-textual hash id's can be cached
5145 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5146 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5151 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5152 '-p', ($format eq 'html' ? "--full-index" : ()),
5153 $hash_parent_base, $hash_base,
5154 "--", (defined $file_parent ? $file_parent : ()), $file_name
5155 or die_error
(500, "Open git-diff-tree failed");
5158 # old/legacy style URI
5159 if (!%diffinfo && # if new style URI failed
5160 defined $hash && defined $hash_parent) {
5161 # fake git-diff-tree raw output
5162 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5163 $diffinfo{'from_id'} = $hash_parent;
5164 $diffinfo{'to_id'} = $hash;
5165 if (defined $file_name) {
5166 if (defined $file_parent) {
5167 $diffinfo{'status'} = '2';
5168 $diffinfo{'from_file'} = $file_parent;
5169 $diffinfo{'to_file'} = $file_name;
5170 } else { # assume not renamed
5171 $diffinfo{'status'} = '1';
5172 $diffinfo{'from_file'} = $file_name;
5173 $diffinfo{'to_file'} = $file_name;
5175 } else { # no filename given
5176 $diffinfo{'status'} = '2';
5177 $diffinfo{'from_file'} = $hash_parent;
5178 $diffinfo{'to_file'} = $hash;
5181 # non-textual hash id's can be cached
5182 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5183 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5188 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
5189 '-p', ($format eq 'html' ? "--full-index" : ()),
5190 $hash_parent, $hash, "--"
5191 or die_error
(500, "Open git-diff failed");
5193 die_error
(400, "Missing one of the blob diff parameters")
5198 if ($format eq 'html') {
5200 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5202 git_header_html
(undef, $expires);
5203 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5204 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5205 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5207 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5208 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5210 if (defined $file_name) {
5211 git_print_page_path
($file_name, "blob", $hash_base);
5213 print "<div class=\"page_path\"></div>\n";
5216 } elsif ($format eq 'plain') {
5218 -type
=> 'text/plain',
5219 -charset
=> 'utf-8',
5220 -expires
=> $expires,
5221 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
5223 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5226 die_error
(400, "Unknown blobdiff format");
5230 if ($format eq 'html') {
5231 print "<div class=\"page_body\">\n";
5233 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5236 print "</div>\n"; # class="page_body"
5240 while (my $line = <$fd>) {
5241 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5242 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5246 last if $line =~ m!^\+\+\+!;
5254 sub git_blobdiff_plain
{
5255 git_blobdiff
('plain');
5258 sub git_commitdiff
{
5259 my $format = shift || 'html';
5260 $hash ||= $hash_base || "HEAD";
5261 my %co = parse_commit
($hash)
5262 or die_error
(404, "Unknown commit object");
5264 # choose format for commitdiff for merge
5265 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5266 $hash_parent = '--cc';
5268 # we need to prepare $formats_nav before almost any parameter munging
5270 if ($format eq 'html') {
5272 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5275 if (defined $hash_parent &&
5276 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5277 # commitdiff with two commits given
5278 my $hash_parent_short = $hash_parent;
5279 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5280 $hash_parent_short = substr($hash_parent, 0, 7);
5284 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5285 if ($co{'parents'}[$i] eq $hash_parent) {
5286 $formats_nav .= ' parent ' . ($i+1);
5290 $formats_nav .= ': ' .
5291 $cgi->a({-href
=> href
(action
=>"commitdiff",
5292 hash
=>$hash_parent)},
5293 esc_html
($hash_parent_short)) .
5295 } elsif (!$co{'parent'}) {
5297 $formats_nav .= ' (initial)';
5298 } elsif (scalar @{$co{'parents'}} == 1) {
5299 # single parent commit
5302 $cgi->a({-href
=> href
(action
=>"commitdiff",
5303 hash
=>$co{'parent'})},
5304 esc_html
(substr($co{'parent'}, 0, 7))) .
5308 if ($hash_parent eq '--cc') {
5309 $formats_nav .= ' | ' .
5310 $cgi->a({-href
=> href
(action
=>"commitdiff",
5311 hash
=>$hash, hash_parent
=>'-c')},
5313 } else { # $hash_parent eq '-c'
5314 $formats_nav .= ' | ' .
5315 $cgi->a({-href
=> href
(action
=>"commitdiff",
5316 hash
=>$hash, hash_parent
=>'--cc')},
5322 $cgi->a({-href
=> href
(action
=>"commitdiff",
5324 esc_html
(substr($_, 0, 7)));
5325 } @{$co{'parents'}} ) .
5330 my $hash_parent_param = $hash_parent;
5331 if (!defined $hash_parent_param) {
5332 # --cc for multiple parents, --root for parentless
5333 $hash_parent_param =
5334 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5340 if ($format eq 'html') {
5341 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5342 "--no-commit-id", "--patch-with-raw", "--full-index",
5343 $hash_parent_param, $hash, "--"
5344 or die_error
(500, "Open git-diff-tree failed");
5346 while (my $line = <$fd>) {
5348 # empty line ends raw part of diff-tree output
5350 push @difftree, scalar parse_difftree_raw_line
($line);
5353 } elsif ($format eq 'plain') {
5354 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5355 '-p', $hash_parent_param, $hash, "--"
5356 or die_error
(500, "Open git-diff-tree failed");
5359 die_error
(400, "Unknown commitdiff format");
5362 # non-textual hash id's can be cached
5364 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5368 # write commit message
5369 if ($format eq 'html') {
5370 my $refs = git_get_references
();
5371 my $ref = format_ref_marker
($refs, $co{'id'});
5373 git_header_html
(undef, $expires);
5374 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5375 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5376 git_print_authorship
(\
%co);
5377 print "<div class=\"page_body\">\n";
5378 if (@{$co{'comment'}} > 1) {
5379 print "<div class=\"log\">\n";
5380 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5381 print "</div>\n"; # class="log"
5384 } elsif ($format eq 'plain') {
5385 my $refs = git_get_references
("tags");
5386 my $tagname = git_get_rev_name_tags
($hash);
5387 my $filename = basename
($project) . "-$hash.patch";
5390 -type
=> 'text/plain',
5391 -charset
=> 'utf-8',
5392 -expires
=> $expires,
5393 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5394 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5395 print "From: " . to_utf8
($co{'author'}) . "\n";
5396 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5397 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5399 print "X-Git-Tag: $tagname\n" if $tagname;
5400 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5402 foreach my $line (@{$co{'comment'}}) {
5403 print to_utf8
($line) . "\n";
5409 if ($format eq 'html') {
5410 my $use_parents = !defined $hash_parent ||
5411 $hash_parent eq '-c' || $hash_parent eq '--cc';
5412 git_difftree_body
(\
@difftree, $hash,
5413 $use_parents ? @{$co{'parents'}} : $hash_parent);
5416 git_patchset_body
($fd, \
@difftree, $hash,
5417 $use_parents ? @{$co{'parents'}} : $hash_parent);
5419 print "</div>\n"; # class="page_body"
5422 } elsif ($format eq 'plain') {
5426 or print "Reading git-diff-tree failed\n";
5430 sub git_commitdiff_plain
{
5431 git_commitdiff
('plain');
5435 if (!defined $hash_base) {
5436 $hash_base = git_get_head_hash
($project);
5438 if (!defined $page) {
5442 my %co = parse_commit
($hash_base)
5443 or die_error
(404, "Unknown commit object");
5445 my $refs = git_get_references
();
5446 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5448 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5449 $file_name, "--full-history")
5450 or die_error
(404, "No such file or directory on given branch");
5452 if (!defined $hash && defined $file_name) {
5453 # some commits could have deleted file in question,
5454 # and not have it in tree, but one of them has to have it
5455 for (my $i = 0; $i <= @commitlist; $i++) {
5456 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5457 last if defined $hash;
5460 if (defined $hash) {
5461 $ftype = git_get_type
($hash);
5463 if (!defined $ftype) {
5464 die_error
(500, "Unknown type of object");
5467 my $paging_nav = '';
5470 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5471 file_name
=>$file_name)},
5473 $paging_nav .= " ⋅ " .
5474 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5475 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5477 $paging_nav .= "first";
5478 $paging_nav .= " ⋅ prev";
5481 if ($#commitlist >= 100) {
5483 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5484 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5485 $paging_nav .= " ⋅ $next_link";
5487 $paging_nav .= " ⋅ next";
5491 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5492 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5493 git_print_page_path
($file_name, $ftype, $hash_base);
5495 git_history_body
(\
@commitlist, 0, 99,
5496 $refs, $hash_base, $ftype, $next_link);
5502 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5503 if (!defined $searchtext) {
5504 die_error
(400, "Text field is empty");
5506 if (!defined $hash) {
5507 $hash = git_get_head_hash
($project);
5509 my %co = parse_commit
($hash);
5511 die_error
(404, "Unknown commit object");
5513 if (!defined $page) {
5517 $searchtype ||= 'commit';
5518 if ($searchtype eq 'pickaxe') {
5519 # pickaxe may take all resources of your box and run for several minutes
5520 # with every query - so decide by yourself how public you make this feature
5521 gitweb_check_feature
('pickaxe')
5522 or die_error
(403, "Pickaxe is disabled");
5524 if ($searchtype eq 'grep') {
5525 gitweb_check_feature
('grep')
5526 or die_error
(403, "Grep is disabled");
5531 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5533 if ($searchtype eq 'commit') {
5534 $greptype = "--grep=";
5535 } elsif ($searchtype eq 'author') {
5536 $greptype = "--author=";
5537 } elsif ($searchtype eq 'committer') {
5538 $greptype = "--committer=";
5540 $greptype .= $searchtext;
5541 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5542 $greptype, '--regexp-ignore-case',
5543 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5545 my $paging_nav = '';
5548 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5549 searchtext
=>$searchtext,
5550 searchtype
=>$searchtype)},
5552 $paging_nav .= " ⋅ " .
5553 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5554 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5556 $paging_nav .= "first";
5557 $paging_nav .= " ⋅ prev";
5560 if ($#commitlist >= 100) {
5562 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5563 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5564 $paging_nav .= " ⋅ $next_link";
5566 $paging_nav .= " ⋅ next";
5569 if ($#commitlist >= 100) {
5572 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5573 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5574 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5577 if ($searchtype eq 'pickaxe') {
5578 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5579 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5581 print "<table class=\"pickaxe search\">\n";
5584 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5585 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5586 ($search_use_regexp ? '--pickaxe-regex' : ());
5589 while (my $line = <$fd>) {
5593 my %set = parse_difftree_raw_line
($line);
5594 if (defined $set{'commit'}) {
5595 # finish previous commit
5598 "<td class=\"link\">" .
5599 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5601 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5607 print "<tr class=\"dark\">\n";
5609 print "<tr class=\"light\">\n";
5612 %co = parse_commit
($set{'commit'});
5613 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5614 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5615 "<td><i>$author</i></td>\n" .
5617 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5618 -class => "list subject"},
5619 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5620 } elsif (defined $set{'to_id'}) {
5621 next if ($set{'to_id'} =~ m/^0{40}$/);
5623 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5624 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5626 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5632 # finish last commit (warning: repetition!)
5635 "<td class=\"link\">" .
5636 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5638 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5646 if ($searchtype eq 'grep') {
5647 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5648 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5650 print "<table class=\"grep_search\">\n";
5654 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5655 $search_use_regexp ? ('-E', '-i') : '-F',
5656 $searchtext, $co{'tree'};
5658 while (my $line = <$fd>) {
5660 my ($file, $lno, $ltext, $binary);
5661 last if ($matches++ > 1000);
5662 if ($line =~ /^Binary file (.+) matches$/) {
5666 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5668 if ($file ne $lastfile) {
5669 $lastfile and print "</td></tr>\n";
5671 print "<tr class=\"dark\">\n";
5673 print "<tr class=\"light\">\n";
5675 print "<td class=\"list\">".
5676 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5677 file_name
=>"$file"),
5678 -class => "list"}, esc_path
($file));
5679 print "</td><td>\n";
5683 print "<div class=\"binary\">Binary file</div>\n";
5685 $ltext = untabify
($ltext);
5686 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5687 $ltext = esc_html
($1, -nbsp
=>1);
5688 $ltext .= '<span class="match">';
5689 $ltext .= esc_html
($2, -nbsp
=>1);
5690 $ltext .= '</span>';
5691 $ltext .= esc_html
($3, -nbsp
=>1);
5693 $ltext = esc_html
($ltext, -nbsp
=>1);
5695 print "<div class=\"pre\">" .
5696 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5697 file_name
=>"$file").'#l'.$lno,
5698 -class => "linenr"}, sprintf('%4i', $lno))
5699 . ' ' . $ltext . "</div>\n";
5703 print "</td></tr>\n";
5704 if ($matches > 1000) {
5705 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5708 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5717 sub git_search_help
{
5719 git_print_page_nav
('','', $hash,$hash,$hash);
5721 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5722 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5723 the pattern entered is recognized as the POSIX extended
5724 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5727 <dt><b>commit</b></dt>
5728 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5730 my ($have_grep) = gitweb_check_feature
('grep');
5733 <dt><b>grep</b></dt>
5734 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5735 a different one) are searched for the given pattern. On large trees, this search can take
5736 a while and put some strain on the server, so please use it with some consideration. Note that
5737 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5738 case-sensitive.</dd>
5742 <dt><b>author</b></dt>
5743 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5744 <dt><b>committer</b></dt>
5745 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5747 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5748 if ($have_pickaxe) {
5750 <dt><b>pickaxe</b></dt>
5751 <dd>All commits that caused the string to appear or disappear from any file (changes that
5752 added, removed or "modified" the string) will be listed. This search can take a while and
5753 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5754 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5762 my $head = git_get_head_hash
($project);
5763 if (!defined $hash) {
5766 if (!defined $page) {
5769 my $refs = git_get_references
();
5771 my $commit_hash = $hash;
5772 if (defined $hash_parent) {
5773 $commit_hash = "$hash_parent..$hash";
5775 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5777 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5779 if ($#commitlist >= 100) {
5781 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5782 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5786 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5787 git_print_header_div
('summary', $project);
5789 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5794 ## ......................................................................
5795 ## feeds (RSS, Atom; OPML)
5798 my $format = shift || 'atom';
5799 my ($have_blame) = gitweb_check_feature
('blame');
5801 # Atom: http://www.atomenabled.org/developers/syndication/
5802 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5803 if ($format ne 'rss' && $format ne 'atom') {
5804 die_error
(400, "Unknown web feed format");
5807 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5808 my $head = $hash || 'HEAD';
5809 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5813 my $content_type = "application/$format+xml";
5814 if (defined $cgi->http('HTTP_ACCEPT') &&
5815 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5816 # browser (feed reader) prefers text/xml
5817 $content_type = 'text/xml';
5819 if (defined($commitlist[0])) {
5820 %latest_commit = %{$commitlist[0]};
5821 %latest_date = parse_date
($latest_commit{'author_epoch'});
5823 -type
=> $content_type,
5824 -charset
=> 'utf-8',
5825 -last_modified
=> $latest_date{'rfc2822'});
5828 -type
=> $content_type,
5829 -charset
=> 'utf-8');
5832 # Optimization: skip generating the body if client asks only
5833 # for Last-Modified date.
5834 return if ($cgi->request_method() eq 'HEAD');
5837 my $title = "$site_name - $project/$action";
5838 my $feed_type = 'log';
5839 if (defined $hash) {
5840 $title .= " - '$hash'";
5841 $feed_type = 'branch log';
5842 if (defined $file_name) {
5843 $title .= " :: $file_name";
5844 $feed_type = 'history';
5846 } elsif (defined $file_name) {
5847 $title .= " - $file_name";
5848 $feed_type = 'history';
5850 $title .= " $feed_type";
5851 my $descr = git_get_project_description
($project);
5852 if (defined $descr) {
5853 $descr = esc_html
($descr);
5855 $descr = "$project " .
5856 ($format eq 'rss' ? 'RSS' : 'Atom') .
5859 my $owner = git_get_project_owner
($project);
5860 $owner = esc_html
($owner);
5864 if (defined $file_name) {
5865 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5866 } elsif (defined $hash) {
5867 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5869 $alt_url = href
(-full
=>1, action
=>"summary");
5871 print qq
!<?xml version
="1.0" encoding
="utf-8"?>\n!;
5872 if ($format eq 'rss') {
5874 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5877 print "<title>$title</title>\n" .
5878 "<link>$alt_url</link>\n" .
5879 "<description>$descr</description>\n" .
5880 "<language>en</language>\n";
5881 } elsif ($format eq 'atom') {
5883 <feed xmlns="http://www.w3.org/2005/Atom">
5885 print "<title>$title</title>\n" .
5886 "<subtitle>$descr</subtitle>\n" .
5887 '<link rel="alternate" type="text/html" href="' .
5888 $alt_url . '" />' . "\n" .
5889 '<link rel="self" type="' . $content_type . '" href="' .
5890 $cgi->self_url() . '" />' . "\n" .
5891 "<id>" . href
(-full
=>1) . "</id>\n" .
5892 # use project owner for feed author
5893 "<author><name>$owner</name></author>\n";
5894 if (defined $favicon) {
5895 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5897 if (defined $logo_url) {
5898 # not twice as wide as tall: 72 x 27 pixels
5899 print "<logo>" . esc_url
($logo) . "</logo>\n";
5901 if (! %latest_date) {
5902 # dummy date to keep the feed valid until commits trickle in:
5903 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5905 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5910 for (my $i = 0; $i <= $#commitlist; $i++) {
5911 my %co = %{$commitlist[$i]};
5912 my $commit = $co{'id'};
5913 # we read 150, we always show 30 and the ones more recent than 48 hours
5914 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5917 my %cd = parse_date
($co{'author_epoch'});
5919 # get list of changed files
5920 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5921 $co{'parent'} || "--root",
5922 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5924 my @difftree = map { chomp; $_ } <$fd>;
5928 # print element (entry, item)
5929 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5930 if ($format eq 'rss') {
5932 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5933 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5934 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5935 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5936 "<link>$co_url</link>\n" .
5937 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5938 "<content:encoded>" .
5940 } elsif ($format eq 'atom') {
5942 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5943 "<updated>$cd{'iso-8601'}</updated>\n" .
5945 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5946 if ($co{'author_email'}) {
5947 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5949 print "</author>\n" .
5950 # use committer for contributor
5952 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5953 if ($co{'committer_email'}) {
5954 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5956 print "</contributor>\n" .
5957 "<published>$cd{'iso-8601'}</published>\n" .
5958 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5959 "<id>$co_url</id>\n" .
5960 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5961 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5963 my $comment = $co{'comment'};
5965 foreach my $line (@$comment) {
5966 $line = esc_html
($line);
5969 print "</pre><ul>\n";
5970 foreach my $difftree_line (@difftree) {
5971 my %difftree = parse_difftree_raw_line
($difftree_line);
5972 next if !$difftree{'from_id'};
5974 my $file = $difftree{'file'} || $difftree{'to_file'};
5978 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5979 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5980 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5981 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5982 -title
=> "diff"}, 'D');
5984 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5985 file_name
=>$file, hash_base
=>$commit),
5986 -title
=> "blame"}, 'B');
5988 # if this is not a feed of a file history
5989 if (!defined $file_name || $file_name ne $file) {
5990 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5991 file_name
=>$file, hash
=>$commit),
5992 -title
=> "history"}, 'H');
5994 $file = esc_path
($file);
5998 if ($format eq 'rss') {
5999 print "</ul>]]>\n" .
6000 "</content:encoded>\n" .
6002 } elsif ($format eq 'atom') {
6003 print "</ul>\n</div>\n" .
6010 if ($format eq 'rss') {
6011 print "</channel>\n</rss>\n";
6012 } elsif ($format eq 'atom') {
6026 my @list = git_get_projects_list
();
6028 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
6030 <?xml version="1.0" encoding="utf-8"?>
6031 <opml version="1.0">
6033 <title>$site_name OPML Export</title>
6036 <outline text="git RSS feeds">
6039 foreach my $pr (@list) {
6041 my $head = git_get_head_hash
($proj{'path'});
6042 if (!defined $head) {
6045 $git_dir = "$projectroot/$proj{'path'}";
6046 my %co = parse_commit
($head);
6051 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
6052 my $rss = "$my_url?p=$proj{'path'};a=rss";
6053 my $html = "$my_url?p=$proj{'path'};a=summary";
6054 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";