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
554 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
555 my ($parentrefname, $parentpathname, $refname, $pathname) =
556 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
558 # first, analyze the 'current' part
559 if (defined $pathname) {
560 # we got "branch:filename" or "branch:dir/"
561 # we could use git_get_type(branch:pathname), but:
562 # - it needs $git_dir
563 # - it does a git() call
564 # - the convention of terminating directories with a slash
565 # makes it superfluous
566 # - embedding the action in the PATH_INFO would make it even
568 $pathname =~ s
,^/+,,;
569 if (!$pathname || substr($pathname, -1) eq "/") {
570 $input_params{'action'} ||= "tree";
573 # the default action depends on whether we had parent info
575 if ($parentrefname) {
576 $input_params{'action'} ||= "blobdiff_plain";
578 $input_params{'action'} ||= "blob_plain";
581 $input_params{'hash_base'} ||= $refname;
582 $input_params{'file_name'} ||= $pathname;
583 } elsif (defined $refname) {
584 # we got "branch". In this case we have to choose if we have to
585 # set hash or hash_base.
587 # Most of the actions without a pathname only want hash to be
588 # set, except for the ones specified in @wants_base that want
589 # hash_base instead. It should also be noted that hand-crafted
590 # links having 'history' as an action and no pathname or hash
591 # set will fail, but that happens regardless of PATH_INFO.
592 $input_params{'action'} ||= "shortlog";
593 if (grep { $_ eq $input_params{'action'} } @wants_base) {
594 $input_params{'hash_base'} ||= $refname;
596 $input_params{'hash'} ||= $refname;
600 # next, handle the 'parent' part, if present
601 if (defined $parentrefname) {
602 # a missing pathspec defaults to the 'current' filename, allowing e.g.
603 # someproject/blobdiff/oldrev..newrev:/filename
604 if ($parentpathname) {
605 $parentpathname =~ s
,^/+,,;
606 $parentpathname =~ s
,/$,,;
607 $input_params{'file_parent'} ||= $parentpathname;
609 $input_params{'file_parent'} ||= $input_params{'file_name'};
611 # we assume that hash_parent_base is wanted if a path was specified,
612 # or if the action wants hash_base instead of hash
613 if (defined $input_params{'file_parent'} ||
614 grep { $_ eq $input_params{'action'} } @wants_base) {
615 $input_params{'hash_parent_base'} ||= $parentrefname;
617 $input_params{'hash_parent'} ||= $parentrefname;
621 evaluate_path_info
();
623 our $action = $input_params{'action'};
624 if (defined $action) {
625 if (!validate_action
($action)) {
626 die_error
(400, "Invalid action parameter");
630 # parameters which are pathnames
631 our $project = $input_params{'project'};
632 if (defined $project) {
633 if (!validate_project
($project)) {
635 die_error
(404, "No such project");
639 our $file_name = $input_params{'file_name'};
640 if (defined $file_name) {
641 if (!validate_pathname
($file_name)) {
642 die_error
(400, "Invalid file parameter");
646 our $file_parent = $input_params{'file_parent'};
647 if (defined $file_parent) {
648 if (!validate_pathname
($file_parent)) {
649 die_error
(400, "Invalid file parent parameter");
653 # parameters which are refnames
654 our $hash = $input_params{'hash'};
656 if (!validate_refname
($hash)) {
657 die_error
(400, "Invalid hash parameter");
661 our $hash_parent = $input_params{'hash_parent'};
662 if (defined $hash_parent) {
663 if (!validate_refname
($hash_parent)) {
664 die_error
(400, "Invalid hash parent parameter");
668 our $hash_base = $input_params{'hash_base'};
669 if (defined $hash_base) {
670 if (!validate_refname
($hash_base)) {
671 die_error
(400, "Invalid hash base parameter");
675 our @extra_options = @{$input_params{'extra_options'}};
676 # @extra_options is always defined, since it can only be (currently) set from
677 # CGI, and $cgi->param() returns the empty array in array context if the param
679 foreach my $opt (@extra_options) {
680 if (not exists $allowed_options{$opt}) {
681 die_error
(400, "Invalid option parameter");
683 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
684 die_error
(400, "Invalid option parameter for this action");
688 our $hash_parent_base = $input_params{'hash_parent_base'};
689 if (defined $hash_parent_base) {
690 if (!validate_refname
($hash_parent_base)) {
691 die_error
(400, "Invalid hash parent base parameter");
696 our $page = $input_params{'page'};
698 if ($page =~ m/[^0-9]/) {
699 die_error
(400, "Invalid page parameter");
703 our $searchtype = $input_params{'searchtype'};
704 if (defined $searchtype) {
705 if ($searchtype =~ m/[^a-z]/) {
706 die_error
(400, "Invalid searchtype parameter");
710 our $search_use_regexp = $input_params{'search_use_regexp'};
712 our $searchtext = $input_params{'searchtext'};
714 if (defined $searchtext) {
715 if (length($searchtext) < 2) {
716 die_error
(403, "At least two characters are required for search parameter");
718 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
721 # path to the current git repository
723 $git_dir = "$projectroot/$project" if $project;
726 if (!defined $action) {
728 $action = git_get_type
($hash);
729 } elsif (defined $hash_base && defined $file_name) {
730 $action = git_get_type
("$hash_base:$file_name");
731 } elsif (defined $project) {
734 $action = 'project_list';
737 if (!defined($actions{$action})) {
738 die_error
(400, "Unknown action");
740 if ($action !~ m/^(opml|project_list|project_index)$/ &&
742 die_error
(400, "Project needed");
744 $actions{$action}->();
747 ## ======================================================================
752 # default is to use -absolute url() i.e. $my_uri
753 my $href = $params{-full
} ? $my_url : $my_uri;
755 $params{'project'} = $project unless exists $params{'project'};
757 if ($params{-replay
}) {
758 while (my ($name, $symbol) = each %cgi_param_mapping) {
759 if (!exists $params{$name}) {
760 $params{$name} = $input_params{$name};
765 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
767 # try to put as many parameters as possible in PATH_INFO:
770 # - hash or hash_base:/filename
772 # When the script is the root DirectoryIndex for the domain,
773 # $href here would be something like http://gitweb.example.com/
774 # Thus, we strip any trailing / from $href, to spare us double
775 # slashes in the final URL
778 # Then add the project name, if present
779 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
780 delete $params{'project'};
782 # Summary just uses the project path URL, any other action is
784 if (defined $params{'action'}) {
785 $href .= "/".esc_url
($params{'action'}) unless $params{'action'} eq 'summary';
786 delete $params{'action'};
789 # Finally, we put either hash_base:/file_name or hash
790 if (defined $params{'hash_base'}) {
791 $href .= "/".esc_url
($params{'hash_base'});
792 if (defined $params{'file_name'}) {
793 $href .= ":/".esc_url
($params{'file_name'});
794 delete $params{'file_name'};
796 delete $params{'hash'};
797 delete $params{'hash_base'};
798 } elsif (defined $params{'hash'}) {
799 $href .= "/".esc_url
($params{'hash'});
800 delete $params{'hash'};
804 # now encode the parameters explicitly
806 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
807 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
808 if (defined $params{$name}) {
809 if (ref($params{$name}) eq "ARRAY") {
810 foreach my $par (@{$params{$name}}) {
811 push @result, $symbol . "=" . esc_param
($par);
814 push @result, $symbol . "=" . esc_param
($params{$name});
818 $href .= "?" . join(';', @result) if scalar @result;
824 ## ======================================================================
825 ## validation, quoting/unquoting and escaping
827 sub validate_action
{
828 my $input = shift || return undef;
829 return undef unless exists $actions{$input};
833 sub validate_project
{
834 my $input = shift || return undef;
835 if (!validate_pathname
($input) ||
836 !(-d
"$projectroot/$input") ||
837 !check_head_link
("$projectroot/$input") ||
838 ($export_ok && !(-e
"$projectroot/$input/$export_ok")) ||
839 ($strict_export && !project_in_list
($input))) {
846 sub validate_pathname
{
847 my $input = shift || return undef;
849 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
850 # at the beginning, at the end, and between slashes.
851 # also this catches doubled slashes
852 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
856 if ($input =~ m!\0!) {
862 sub validate_refname
{
863 my $input = shift || return undef;
865 # textual hashes are O.K.
866 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
869 # it must be correct pathname
870 $input = validate_pathname
($input)
872 # restrictions on ref name according to git-check-ref-format
873 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
879 # decode sequences of octets in utf8 into Perl's internal form,
880 # which is utf-8 with utf8 flag set if needed. gitweb writes out
881 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
884 if (utf8
::valid
($str)) {
888 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
892 # quote unsafe chars, but keep the slash, even when it's not
893 # correct, but quoted slashes look too horrible in bookmarks
896 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
902 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
905 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
911 # replace invalid utf8 character with SUBSTITUTION sequence
916 $str = to_utf8
($str);
917 $str = $cgi->escapeHTML($str);
918 if ($opts{'-nbsp'}) {
919 $str =~ s/ / /g;
921 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
925 # quote control characters and escape filename to HTML
930 $str = to_utf8
($str);
931 $str = $cgi->escapeHTML($str);
932 if ($opts{'-nbsp'}) {
933 $str =~ s/ / /g;
935 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
939 # Make control characters "printable", using character escape codes (CEC)
943 my %es = ( # character escape codes, aka escape sequences
944 "\t" => '\t', # tab (HT)
945 "\n" => '\n', # line feed (LF)
946 "\r" => '\r', # carrige return (CR)
947 "\f" => '\f', # form feed (FF)
948 "\b" => '\b', # backspace (BS)
949 "\a" => '\a', # alarm (bell) (BEL)
950 "\e" => '\e', # escape (ESC)
951 "\013" => '\v', # vertical tab (VT)
952 "\000" => '\0', # nul character (NUL)
954 my $chr = ( (exists $es{$cntrl})
956 : sprintf('\%2x', ord($cntrl)) );
957 if ($opts{-nohtml
}) {
960 return "<span class=\"cntrl\">$chr</span>";
964 # Alternatively use unicode control pictures codepoints,
965 # Unicode "printable representation" (PR)
970 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
971 if ($opts{-nohtml
}) {
974 return "<span class=\"cntrl\">$chr</span>";
978 # git may return quoted and escaped filenames
984 my %es = ( # character escape codes, aka escape sequences
985 't' => "\t", # tab (HT, TAB)
986 'n' => "\n", # newline (NL)
987 'r' => "\r", # return (CR)
988 'f' => "\f", # form feed (FF)
989 'b' => "\b", # backspace (BS)
990 'a' => "\a", # alarm (bell) (BEL)
991 'e' => "\e", # escape (ESC)
992 'v' => "\013", # vertical tab (VT)
995 if ($seq =~ m/^[0-7]{1,3}$/) {
996 # octal char sequence
997 return chr(oct($seq));
998 } elsif (exists $es{$seq}) {
999 # C escape sequence, aka character escape code
1002 # quoted ordinary character
1006 if ($str =~ m/^"(.*)"$/) {
1009 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1014 # escape tabs (convert tabs to spaces)
1018 while ((my $pos = index($line, "\t")) != -1) {
1019 if (my $count = (8 - ($pos % 8))) {
1020 my $spaces = ' ' x
$count;
1021 $line =~ s/\t/$spaces/;
1028 sub project_in_list
{
1029 my $project = shift;
1030 my @list = git_get_projects_list
();
1031 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1034 ## ----------------------------------------------------------------------
1035 ## HTML aware string manipulation
1037 # Try to chop given string on a word boundary between position
1038 # $len and $len+$add_len. If there is no word boundary there,
1039 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1040 # (marking chopped part) would be longer than given string.
1044 my $add_len = shift || 10;
1045 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1047 # Make sure perl knows it is utf8 encoded so we don't
1048 # cut in the middle of a utf8 multibyte char.
1049 $str = to_utf8
($str);
1051 # allow only $len chars, but don't cut a word if it would fit in $add_len
1052 # if it doesn't fit, cut it if it's still longer than the dots we would add
1053 # remove chopped character entities entirely
1055 # when chopping in the middle, distribute $len into left and right part
1056 # return early if chopping wouldn't make string shorter
1057 if ($where eq 'center') {
1058 return $str if ($len + 5 >= length($str)); # filler is length 5
1061 return $str if ($len + 4 >= length($str)); # filler is length 4
1064 # regexps: ending and beginning with word part up to $add_len
1065 my $endre = qr/.{$len}\w{0,$add_len}/;
1066 my $begre = qr/\w{0,$add_len}.{$len}/;
1068 if ($where eq 'left') {
1069 $str =~ m/^(.*?)($begre)$/;
1070 my ($lead, $body) = ($1, $2);
1071 if (length($lead) > 4) {
1072 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1075 return "$lead$body";
1077 } elsif ($where eq 'center') {
1078 $str =~ m/^($endre)(.*)$/;
1079 my ($left, $str) = ($1, $2);
1080 $str =~ m/^(.*?)($begre)$/;
1081 my ($mid, $right) = ($1, $2);
1082 if (length($mid) > 5) {
1083 $left =~ s/&[^;]*$//;
1084 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1087 return "$left$mid$right";
1090 $str =~ m/^($endre)(.*)$/;
1093 if (length($tail) > 4) {
1094 $body =~ s/&[^;]*$//;
1097 return "$body$tail";
1101 # takes the same arguments as chop_str, but also wraps a <span> around the
1102 # result with a title attribute if it does get chopped. Additionally, the
1103 # string is HTML-escaped.
1104 sub chop_and_escape_str
{
1107 my $chopped = chop_str
(@_);
1108 if ($chopped eq $str) {
1109 return esc_html
($chopped);
1111 $str =~ s/([[:cntrl:]])/?/g;
1112 return $cgi->span({-title
=>$str}, esc_html
($chopped));
1116 ## ----------------------------------------------------------------------
1117 ## functions returning short strings
1119 # CSS class for given age value (in seconds)
1123 if (!defined $age) {
1125 } elsif ($age < 60*60*2) {
1127 } elsif ($age < 60*60*24*2) {
1134 # convert age in seconds to "nn units ago" string
1139 if ($age > 60*60*24*365*2) {
1140 $age_str = (int $age/60/60/24/365);
1141 $age_str .= " years ago";
1142 } elsif ($age > 60*60*24*(365/12)*2) {
1143 $age_str = int $age/60/60/24/(365/12);
1144 $age_str .= " months ago";
1145 } elsif ($age > 60*60*24*7*2) {
1146 $age_str = int $age/60/60/24/7;
1147 $age_str .= " weeks ago";
1148 } elsif ($age > 60*60*24*2) {
1149 $age_str = int $age/60/60/24;
1150 $age_str .= " days ago";
1151 } elsif ($age > 60*60*2) {
1152 $age_str = int $age/60/60;
1153 $age_str .= " hours ago";
1154 } elsif ($age > 60*2) {
1155 $age_str = int $age/60;
1156 $age_str .= " min ago";
1157 } elsif ($age > 2) {
1158 $age_str = int $age;
1159 $age_str .= " sec ago";
1161 $age_str .= " right now";
1167 S_IFINVALID
=> 0030000,
1168 S_IFGITLINK
=> 0160000,
1171 # submodule/subproject, a commit object reference
1172 sub S_ISGITLINK
($) {
1175 return (($mode & S_IFMT
) == S_IFGITLINK
)
1178 # convert file mode in octal to symbolic file mode string
1180 my $mode = oct shift;
1182 if (S_ISGITLINK
($mode)) {
1183 return 'm---------';
1184 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1185 return 'drwxr-xr-x';
1186 } elsif (S_ISLNK
($mode)) {
1187 return 'lrwxrwxrwx';
1188 } elsif (S_ISREG
($mode)) {
1189 # git cares only about the executable bit
1190 if ($mode & S_IXUSR
) {
1191 return '-rwxr-xr-x';
1193 return '-rw-r--r--';
1196 return '----------';
1200 # convert file mode in octal to file type string
1204 if ($mode !~ m/^[0-7]+$/) {
1210 if (S_ISGITLINK
($mode)) {
1212 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1214 } elsif (S_ISLNK
($mode)) {
1216 } elsif (S_ISREG
($mode)) {
1223 # convert file mode in octal to file type description string
1224 sub file_type_long
{
1227 if ($mode !~ m/^[0-7]+$/) {
1233 if (S_ISGITLINK
($mode)) {
1235 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1237 } elsif (S_ISLNK
($mode)) {
1239 } elsif (S_ISREG
($mode)) {
1240 if ($mode & S_IXUSR
) {
1241 return "executable";
1251 ## ----------------------------------------------------------------------
1252 ## functions returning short HTML fragments, or transforming HTML fragments
1253 ## which don't belong to other sections
1255 # format line of commit message.
1256 sub format_log_line_html
{
1259 $line = esc_html
($line, -nbsp
=>1);
1260 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1263 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1264 -class => "text"}, $hash_text);
1265 $line =~ s/$hash_text/$link/;
1270 # format marker of refs pointing to given object
1272 # the destination action is chosen based on object type and current context:
1273 # - for annotated tags, we choose the tag view unless it's the current view
1274 # already, in which case we go to shortlog view
1275 # - for other refs, we keep the current view if we're in history, shortlog or
1276 # log view, and select shortlog otherwise
1277 sub format_ref_marker
{
1278 my ($refs, $id) = @_;
1281 if (defined $refs->{$id}) {
1282 foreach my $ref (@{$refs->{$id}}) {
1283 # this code exploits the fact that non-lightweight tags are the
1284 # only indirect objects, and that they are the only objects for which
1285 # we want to use tag instead of shortlog as action
1286 my ($type, $name) = qw();
1287 my $indirect = ($ref =~ s/\^\{\}$//);
1288 # e.g. tags/v2.6.11 or heads/next
1289 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1298 $class .= " indirect" if $indirect;
1300 my $dest_action = "shortlog";
1303 $dest_action = "tag" unless $action eq "tag";
1304 } elsif ($action =~ /^(history|(short)?log)$/) {
1305 $dest_action = $action;
1309 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1312 my $link = $cgi->a({
1314 action
=>$dest_action,
1318 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1324 return ' <span class="refs">'. $markers . '</span>';
1330 # format, perhaps shortened and with markers, title line
1331 sub format_subject_html
{
1332 my ($long, $short, $href, $extra) = @_;
1333 $extra = '' unless defined($extra);
1335 if (length($short) < length($long)) {
1336 return $cgi->a({-href
=> $href, -class => "list subject",
1337 -title
=> to_utf8
($long)},
1338 esc_html
($short) . $extra);
1340 return $cgi->a({-href
=> $href, -class => "list subject"},
1341 esc_html
($long) . $extra);
1345 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1346 sub format_git_diff_header_line
{
1348 my $diffinfo = shift;
1349 my ($from, $to) = @_;
1351 if ($diffinfo->{'nparents'}) {
1353 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1354 if ($to->{'href'}) {
1355 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1356 esc_path
($to->{'file'}));
1357 } else { # file was deleted (no href)
1358 $line .= esc_path
($to->{'file'});
1362 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1363 if ($from->{'href'}) {
1364 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1365 'a/' . esc_path
($from->{'file'}));
1366 } else { # file was added (no href)
1367 $line .= 'a/' . esc_path
($from->{'file'});
1370 if ($to->{'href'}) {
1371 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1372 'b/' . esc_path
($to->{'file'}));
1373 } else { # file was deleted
1374 $line .= 'b/' . esc_path
($to->{'file'});
1378 return "<div class=\"diff header\">$line</div>\n";
1381 # format extended diff header line, before patch itself
1382 sub format_extended_diff_header_line
{
1384 my $diffinfo = shift;
1385 my ($from, $to) = @_;
1388 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1389 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1390 esc_path
($from->{'file'}));
1392 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1393 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1394 esc_path
($to->{'file'}));
1396 # match single <mode>
1397 if ($line =~ m/\s(\d{6})$/) {
1398 $line .= '<span class="info"> (' .
1399 file_type_long
($1) .
1403 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1404 # can match only for combined diff
1406 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1407 if ($from->{'href'}[$i]) {
1408 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1410 substr($diffinfo->{'from_id'}[$i],0,7));
1415 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1418 if ($to->{'href'}) {
1419 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1420 substr($diffinfo->{'to_id'},0,7));
1425 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1426 # can match only for ordinary diff
1427 my ($from_link, $to_link);
1428 if ($from->{'href'}) {
1429 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1430 substr($diffinfo->{'from_id'},0,7));
1432 $from_link = '0' x
7;
1434 if ($to->{'href'}) {
1435 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1436 substr($diffinfo->{'to_id'},0,7));
1440 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1441 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1444 return $line . "<br/>\n";
1447 # format from-file/to-file diff header
1448 sub format_diff_from_to_header
{
1449 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1454 #assert($line =~ m/^---/) if DEBUG;
1455 # no extra formatting for "^--- /dev/null"
1456 if (! $diffinfo->{'nparents'}) {
1457 # ordinary (single parent) diff
1458 if ($line =~ m!^--- "?a/!) {
1459 if ($from->{'href'}) {
1461 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1462 esc_path
($from->{'file'}));
1465 esc_path
($from->{'file'});
1468 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1471 # combined diff (merge commit)
1472 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1473 if ($from->{'href'}[$i]) {
1475 $cgi->a({-href
=>href
(action
=>"blobdiff",
1476 hash_parent
=>$diffinfo->{'from_id'}[$i],
1477 hash_parent_base
=>$parents[$i],
1478 file_parent
=>$from->{'file'}[$i],
1479 hash
=>$diffinfo->{'to_id'},
1481 file_name
=>$to->{'file'}),
1483 -title
=>"diff" . ($i+1)},
1486 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1487 esc_path
($from->{'file'}[$i]));
1489 $line = '--- /dev/null';
1491 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1496 #assert($line =~ m/^\+\+\+/) if DEBUG;
1497 # no extra formatting for "^+++ /dev/null"
1498 if ($line =~ m!^\+\+\+ "?b/!) {
1499 if ($to->{'href'}) {
1501 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1502 esc_path
($to->{'file'}));
1505 esc_path
($to->{'file'});
1508 $result .= qq
!<div
class="diff to_file">$line</div
>\n!;
1513 # create note for patch simplified by combined diff
1514 sub format_diff_cc_simplified
{
1515 my ($diffinfo, @parents) = @_;
1518 $result .= "<div class=\"diff header\">" .
1520 if (!is_deleted
($diffinfo)) {
1521 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1523 hash
=>$diffinfo->{'to_id'},
1524 file_name
=>$diffinfo->{'to_file'}),
1526 esc_path
($diffinfo->{'to_file'}));
1528 $result .= esc_path
($diffinfo->{'to_file'});
1530 $result .= "</div>\n" . # class="diff header"
1531 "<div class=\"diff nodifferences\">" .
1533 "</div>\n"; # class="diff nodifferences"
1538 # format patch (diff) line (not to be used for diff headers)
1539 sub format_diff_line
{
1541 my ($from, $to) = @_;
1542 my $diff_class = "";
1546 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1548 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1549 if ($line =~ m/^\@{3}/) {
1550 $diff_class = " chunk_header";
1551 } elsif ($line =~ m/^\\/) {
1552 $diff_class = " incomplete";
1553 } elsif ($prefix =~ tr/+/+/) {
1554 $diff_class = " add";
1555 } elsif ($prefix =~ tr/-/-/) {
1556 $diff_class = " rem";
1559 # assume ordinary diff
1560 my $char = substr($line, 0, 1);
1562 $diff_class = " add";
1563 } elsif ($char eq '-') {
1564 $diff_class = " rem";
1565 } elsif ($char eq '@') {
1566 $diff_class = " chunk_header";
1567 } elsif ($char eq "\\") {
1568 $diff_class = " incomplete";
1571 $line = untabify
($line);
1572 if ($from && $to && $line =~ m/^\@{2} /) {
1573 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1574 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1576 $from_lines = 0 unless defined $from_lines;
1577 $to_lines = 0 unless defined $to_lines;
1579 if ($from->{'href'}) {
1580 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1581 -class=>"list"}, $from_text);
1583 if ($to->{'href'}) {
1584 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1585 -class=>"list"}, $to_text);
1587 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1588 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1589 return "<div class=\"diff$diff_class\">$line</div>\n";
1590 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1591 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1592 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1594 @from_text = split(' ', $ranges);
1595 for (my $i = 0; $i < @from_text; ++$i) {
1596 ($from_start[$i], $from_nlines[$i]) =
1597 (split(',', substr($from_text[$i], 1)), 0);
1600 $to_text = pop @from_text;
1601 $to_start = pop @from_start;
1602 $to_nlines = pop @from_nlines;
1604 $line = "<span class=\"chunk_info\">$prefix ";
1605 for (my $i = 0; $i < @from_text; ++$i) {
1606 if ($from->{'href'}[$i]) {
1607 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1608 -class=>"list"}, $from_text[$i]);
1610 $line .= $from_text[$i];
1614 if ($to->{'href'}) {
1615 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1616 -class=>"list"}, $to_text);
1620 $line .= " $prefix</span>" .
1621 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1622 return "<div class=\"diff$diff_class\">$line</div>\n";
1624 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1627 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1628 # linked. Pass the hash of the tree/commit to snapshot.
1629 sub format_snapshot_links
{
1631 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1632 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1633 my $num_fmts = @snapshot_fmts;
1634 if ($num_fmts > 1) {
1635 # A parenthesized list of links bearing format names.
1636 # e.g. "snapshot (_tar.gz_ _zip_)"
1637 return "snapshot (" . join(' ', map
1644 }, $known_snapshot_formats{$_}{'display'})
1645 , @snapshot_fmts) . ")";
1646 } elsif ($num_fmts == 1) {
1647 # A single "snapshot" link whose tooltip bears the format name.
1649 my ($fmt) = @snapshot_fmts;
1655 snapshot_format
=>$fmt
1657 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1659 } else { # $num_fmts == 0
1664 ## ......................................................................
1665 ## functions returning values to be passed, perhaps after some
1666 ## transformation, to other functions; e.g. returning arguments to href()
1668 # returns hash to be passed to href to generate gitweb URL
1669 # in -title key it returns description of link
1671 my $format = shift || 'Atom';
1672 my %res = (action
=> lc($format));
1674 # feed links are possible only for project views
1675 return unless (defined $project);
1676 # some views should link to OPML, or to generic project feed,
1677 # or don't have specific feed yet (so they should use generic)
1678 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1681 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1682 # from tag links; this also makes possible to detect branch links
1683 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1684 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1687 # find log type for feed description (title)
1689 if (defined $file_name) {
1690 $type = "history of $file_name";
1691 $type .= "/" if ($action eq 'tree');
1692 $type .= " on '$branch'" if (defined $branch);
1694 $type = "log of $branch" if (defined $branch);
1697 $res{-title
} = $type;
1698 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1699 $res{'file_name'} = $file_name;
1704 ## ----------------------------------------------------------------------
1705 ## git utility subroutines, invoking git commands
1707 # returns path to the core git executable and the --git-dir parameter as list
1709 return $GIT, '--git-dir='.$git_dir;
1712 # quote the given arguments for passing them to the shell
1713 # quote_command("command", "arg 1", "arg with ' and ! characters")
1714 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1715 # Try to avoid using this function wherever possible.
1718 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1721 # get HEAD ref of given project as hash
1722 sub git_get_head_hash
{
1723 my $project = shift;
1724 my $o_git_dir = $git_dir;
1726 $git_dir = "$projectroot/$project";
1727 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1730 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1734 if (defined $o_git_dir) {
1735 $git_dir = $o_git_dir;
1740 # get type of given object
1744 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1746 close $fd or return;
1751 # repository configuration
1752 our $config_file = '';
1755 # store multiple values for single key as anonymous array reference
1756 # single values stored directly in the hash, not as [ <value> ]
1757 sub hash_set_multi
{
1758 my ($hash, $key, $value) = @_;
1760 if (!exists $hash->{$key}) {
1761 $hash->{$key} = $value;
1762 } elsif (!ref $hash->{$key}) {
1763 $hash->{$key} = [ $hash->{$key}, $value ];
1765 push @{$hash->{$key}}, $value;
1769 # return hash of git project configuration
1770 # optionally limited to some section, e.g. 'gitweb'
1771 sub git_parse_project_config
{
1772 my $section_regexp = shift;
1777 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1780 while (my $keyval = <$fh>) {
1782 my ($key, $value) = split(/\n/, $keyval, 2);
1784 hash_set_multi
(\
%config, $key, $value)
1785 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1792 # convert config value to boolean, 'true' or 'false'
1793 # no value, number > 0, 'true' and 'yes' values are true
1794 # rest of values are treated as false (never as error)
1795 sub config_to_bool
{
1798 # strip leading and trailing whitespace
1802 return (!defined $val || # section.key
1803 ($val =~ /^\d+$/ && $val) || # section.key = 1
1804 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1807 # convert config value to simple decimal number
1808 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1809 # to be multiplied by 1024, 1048576, or 1073741824
1813 # strip leading and trailing whitespace
1817 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1819 # unknown unit is treated as 1
1820 return $num * ($unit eq 'g' ? 1073741824 :
1821 $unit eq 'm' ? 1048576 :
1822 $unit eq 'k' ? 1024 : 1);
1827 # convert config value to array reference, if needed
1828 sub config_to_multi
{
1831 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1834 sub git_get_project_config
{
1835 my ($key, $type) = @_;
1838 return unless ($key);
1839 $key =~ s/^gitweb\.//;
1840 return if ($key =~ m/\W/);
1843 if (defined $type) {
1846 unless ($type eq 'bool' || $type eq 'int');
1850 if (!defined $config_file ||
1851 $config_file ne "$git_dir/config") {
1852 %config = git_parse_project_config
('gitweb');
1853 $config_file = "$git_dir/config";
1857 if (!defined $type) {
1858 return $config{"gitweb.$key"};
1859 } elsif ($type eq 'bool') {
1860 # backward compatibility: 'git config --bool' returns true/false
1861 return config_to_bool
($config{"gitweb.$key"}) ? 'true' : 'false';
1862 } elsif ($type eq 'int') {
1863 return config_to_int
($config{"gitweb.$key"});
1865 return $config{"gitweb.$key"};
1868 # get hash of given path at given ref
1869 sub git_get_hash_by_path
{
1871 my $path = shift || return undef;
1876 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1877 or die_error
(500, "Open git-ls-tree failed");
1879 close $fd or return undef;
1881 if (!defined $line) {
1882 # there is no tree or hash given by $path at $base
1886 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1887 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1888 if (defined $type && $type ne $2) {
1889 # type doesn't match
1895 # get path of entry with given hash at given tree-ish (ref)
1896 # used to get 'from' filename for combined diff (merge commit) for renames
1897 sub git_get_path_by_hash
{
1898 my $base = shift || return;
1899 my $hash = shift || return;
1903 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1905 while (my $line = <$fd>) {
1908 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1909 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1910 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1919 ## ......................................................................
1920 ## git utility functions, directly accessing git repository
1922 sub git_get_project_description
{
1925 $git_dir = "$projectroot/$path";
1926 open my $fd, "$git_dir/description"
1927 or return git_get_project_config
('description');
1930 if (defined $descr) {
1936 sub git_get_project_ctags
{
1940 $git_dir = "$projectroot/$path";
1941 unless (opendir D
, "$git_dir/ctags") {
1944 foreach (grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir(D
)) {
1945 open CT
, $_ or next;
1949 my $ctag = $_; $ctag =~ s
#.*/##;
1950 $ctags->{$ctag} = $val;
1956 sub git_populate_project_tagcloud
{
1959 # First, merge different-cased tags; tags vote on casing
1961 foreach (keys %$ctags) {
1962 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
1963 if (not $ctags_lc{lc $_}->{topcount
}
1964 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
1965 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
1966 $ctags_lc{lc $_}->{topname
} = $_;
1971 if (eval { require HTML
::TagCloud
; 1; }) {
1972 $cloud = HTML
::TagCloud-
>new;
1973 foreach (sort keys %ctags_lc) {
1974 # Pad the title with spaces so that the cloud looks
1976 my $title = $ctags_lc{$_}->{topname
};
1977 $title =~ s/ / /g;
1978 $title =~ s/^/ /g;
1979 $title =~ s/$/ /g;
1980 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count
});
1983 $cloud = \
%ctags_lc;
1988 sub git_show_project_tagcloud
{
1989 my ($cloud, $count) = @_;
1990 print STDERR
ref($cloud)."..\n";
1991 if (ref $cloud eq 'HTML::TagCloud') {
1992 return $cloud->html_and_css($count);
1994 my @tags = sort { $cloud->{$a}->{count
} <=> $cloud->{$b}->{count
} } keys %$cloud;
1995 return '<p align="center">' . join (', ', map {
1996 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1997 } splice(@tags, 0, $count)) . '</p>';
2001 sub git_get_project_url_list
{
2004 $git_dir = "$projectroot/$path";
2005 open my $fd, "$git_dir/cloneurl"
2006 or return wantarray ?
2007 @{ config_to_multi
(git_get_project_config
('url')) } :
2008 config_to_multi
(git_get_project_config
('url'));
2009 my @git_project_url_list = map { chomp; $_ } <$fd>;
2012 return wantarray ? @git_project_url_list : \
@git_project_url_list;
2015 sub git_get_projects_list
{
2020 $filter =~ s/\.git$//;
2022 my ($check_forks) = gitweb_check_feature
('forks');
2024 if (-d
$projects_list) {
2025 # search in directory
2026 my $dir = $projects_list . ($filter ? "/$filter" : '');
2027 # remove the trailing "/"
2029 my $pfxlen = length("$dir");
2030 my $pfxdepth = ($dir =~ tr!/!!);
2033 follow_fast
=> 1, # follow symbolic links
2034 follow_skip
=> 2, # ignore duplicates
2035 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
2037 # skip project-list toplevel, if we get it.
2038 return if (m!^[/.]$!);
2039 # only directories can be git repositories
2040 return unless (-d
$_);
2041 # don't traverse too deep (Find is super slow on os x)
2042 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2043 $File::Find
::prune
= 1;
2047 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
2048 # we check related file in $projectroot
2049 if (check_export_ok
("$projectroot/$filter/$subdir")) {
2050 push @list, { path
=> ($filter ? "$filter/" : '') . $subdir };
2051 $File::Find
::prune
= 1;
2056 } elsif (-f
$projects_list) {
2057 # read from file(url-encoded):
2058 # 'git%2Fgit.git Linus+Torvalds'
2059 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2060 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2062 open my ($fd), $projects_list or return;
2064 while (my $line = <$fd>) {
2066 my ($path, $owner) = split ' ', $line;
2067 $path = unescape
($path);
2068 $owner = unescape
($owner);
2069 if (!defined $path) {
2072 if ($filter ne '') {
2073 # looking for forks;
2074 my $pfx = substr($path, 0, length($filter));
2075 if ($pfx ne $filter) {
2078 my $sfx = substr($path, length($filter));
2079 if ($sfx !~ /^\/.*\
.git
$/) {
2082 } elsif ($check_forks) {
2084 foreach my $filter (keys %paths) {
2085 # looking for forks;
2086 my $pfx = substr($path, 0, length($filter));
2087 if ($pfx ne $filter) {
2090 my $sfx = substr($path, length($filter));
2091 if ($sfx !~ /^\/.*\
.git
$/) {
2094 # is a fork, don't include it in
2099 if (check_export_ok
("$projectroot/$path")) {
2102 owner
=> to_utf8
($owner),
2105 (my $forks_path = $path) =~ s/\.git$//;
2106 $paths{$forks_path}++;
2114 our $gitweb_project_owner = undef;
2115 sub git_get_project_list_from_file
{
2117 return if (defined $gitweb_project_owner);
2119 $gitweb_project_owner = {};
2120 # read from file (url-encoded):
2121 # 'git%2Fgit.git Linus+Torvalds'
2122 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2123 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2124 if (-f
$projects_list) {
2125 open (my $fd , $projects_list);
2126 while (my $line = <$fd>) {
2128 my ($pr, $ow) = split ' ', $line;
2129 $pr = unescape
($pr);
2130 $ow = unescape
($ow);
2131 $gitweb_project_owner->{$pr} = to_utf8
($ow);
2137 sub git_get_project_owner
{
2138 my $project = shift;
2141 return undef unless $project;
2142 $git_dir = "$projectroot/$project";
2144 if (!defined $gitweb_project_owner) {
2145 git_get_project_list_from_file
();
2148 if (exists $gitweb_project_owner->{$project}) {
2149 $owner = $gitweb_project_owner->{$project};
2151 if (!defined $owner){
2152 $owner = git_get_project_config
('owner');
2154 if (!defined $owner) {
2155 $owner = get_file_owner
("$git_dir");
2161 sub git_get_last_activity
{
2165 $git_dir = "$projectroot/$path";
2166 open($fd, "-|", git_cmd
(), 'for-each-ref',
2167 '--format=%(committer)',
2168 '--sort=-committerdate',
2170 'refs/heads') or return;
2171 my $most_recent = <$fd>;
2172 close $fd or return;
2173 if (defined $most_recent &&
2174 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2176 my $age = time - $timestamp;
2177 return ($age, age_string
($age));
2179 return (undef, undef);
2182 sub git_get_references
{
2183 my $type = shift || "";
2185 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2186 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2187 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
2188 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2191 while (my $line = <$fd>) {
2193 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2194 if (defined $refs{$1}) {
2195 push @{$refs{$1}}, $2;
2201 close $fd or return;
2205 sub git_get_rev_name_tags
{
2206 my $hash = shift || return undef;
2208 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
2210 my $name_rev = <$fd>;
2213 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2216 # catches also '$hash undefined' output
2221 ## ----------------------------------------------------------------------
2222 ## parse to hash functions
2226 my $tz = shift || "-0000";
2229 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2230 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2231 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2232 $date{'hour'} = $hour;
2233 $date{'minute'} = $min;
2234 $date{'mday'} = $mday;
2235 $date{'day'} = $days[$wday];
2236 $date{'month'} = $months[$mon];
2237 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2238 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2239 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2240 $mday, $months[$mon], $hour ,$min;
2241 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2242 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2244 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2245 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2246 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2247 $date{'hour_local'} = $hour;
2248 $date{'minute_local'} = $min;
2249 $date{'tz_local'} = $tz;
2250 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2251 1900+$year, $mon+1, $mday,
2252 $hour, $min, $sec, $tz);
2261 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2262 $tag{'id'} = $tag_id;
2263 while (my $line = <$fd>) {
2265 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2266 $tag{'object'} = $1;
2267 } elsif ($line =~ m/^type (.+)$/) {
2269 } elsif ($line =~ m/^tag (.+)$/) {
2271 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2272 $tag{'author'} = $1;
2275 } elsif ($line =~ m/--BEGIN/) {
2276 push @comment, $line;
2278 } elsif ($line eq "") {
2282 push @comment, <$fd>;
2283 $tag{'comment'} = \
@comment;
2284 close $fd or return;
2285 if (!defined $tag{'name'}) {
2291 sub parse_commit_text
{
2292 my ($commit_text, $withparents) = @_;
2293 my @commit_lines = split '\n', $commit_text;
2296 pop @commit_lines; # Remove '\0'
2298 if (! @commit_lines) {
2302 my $header = shift @commit_lines;
2303 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2306 ($co{'id'}, my @parents) = split ' ', $header;
2307 while (my $line = shift @commit_lines) {
2308 last if $line eq "\n";
2309 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2311 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2313 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2315 $co{'author_epoch'} = $2;
2316 $co{'author_tz'} = $3;
2317 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2318 $co{'author_name'} = $1;
2319 $co{'author_email'} = $2;
2321 $co{'author_name'} = $co{'author'};
2323 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2324 $co{'committer'} = $1;
2325 $co{'committer_epoch'} = $2;
2326 $co{'committer_tz'} = $3;
2327 $co{'committer_name'} = $co{'committer'};
2328 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2329 $co{'committer_name'} = $1;
2330 $co{'committer_email'} = $2;
2332 $co{'committer_name'} = $co{'committer'};
2336 if (!defined $co{'tree'}) {
2339 $co{'parents'} = \
@parents;
2340 $co{'parent'} = $parents[0];
2342 foreach my $title (@commit_lines) {
2345 $co{'title'} = chop_str
($title, 80, 5);
2346 # remove leading stuff of merges to make the interesting part visible
2347 if (length($title) > 50) {
2348 $title =~ s/^Automatic //;
2349 $title =~ s/^merge (of|with) /Merge ... /i;
2350 if (length($title) > 50) {
2351 $title =~ s/(http|rsync):\/\///;
2353 if (length($title) > 50) {
2354 $title =~ s/(master|www|rsync)\.//;
2356 if (length($title) > 50) {
2357 $title =~ s/kernel.org:?//;
2359 if (length($title) > 50) {
2360 $title =~ s/\/pub\/scm//;
2363 $co{'title_short'} = chop_str
($title, 50, 5);
2367 if (! defined $co{'title'} || $co{'title'} eq "") {
2368 $co{'title'} = $co{'title_short'} = '(no commit message)';
2370 # remove added spaces
2371 foreach my $line (@commit_lines) {
2374 $co{'comment'} = \
@commit_lines;
2376 my $age = time - $co{'committer_epoch'};
2378 $co{'age_string'} = age_string
($age);
2379 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2380 if ($age > 60*60*24*7*2) {
2381 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2382 $co{'age_string_age'} = $co{'age_string'};
2384 $co{'age_string_date'} = $co{'age_string'};
2385 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2391 my ($commit_id) = @_;
2396 open my $fd, "-|", git_cmd
(), "rev-list",
2402 or die_error
(500, "Open git-rev-list failed");
2403 %co = parse_commit_text
(<$fd>, 1);
2410 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2418 open my $fd, "-|", git_cmd
(), "rev-list",
2421 ("--max-count=" . $maxcount),
2422 ("--skip=" . $skip),
2426 ($filename ? ($filename) : ())
2427 or die_error
(500, "Open git-rev-list failed");
2428 while (my $line = <$fd>) {
2429 my %co = parse_commit_text
($line);
2434 return wantarray ? @cos : \
@cos;
2437 # parse line of git-diff-tree "raw" output
2438 sub parse_difftree_raw_line
{
2442 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2443 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2444 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2445 $res{'from_mode'} = $1;
2446 $res{'to_mode'} = $2;
2447 $res{'from_id'} = $3;
2449 $res{'status'} = $5;
2450 $res{'similarity'} = $6;
2451 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2452 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2454 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2457 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2458 # combined diff (for merge commit)
2459 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2460 $res{'nparents'} = length($1);
2461 $res{'from_mode'} = [ split(' ', $2) ];
2462 $res{'to_mode'} = pop @{$res{'from_mode'}};
2463 $res{'from_id'} = [ split(' ', $3) ];
2464 $res{'to_id'} = pop @{$res{'from_id'}};
2465 $res{'status'} = [ split('', $4) ];
2466 $res{'to_file'} = unquote
($5);
2468 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2469 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2470 $res{'commit'} = $1;
2473 return wantarray ? %res : \
%res;
2476 # wrapper: return parsed line of git-diff-tree "raw" output
2477 # (the argument might be raw line, or parsed info)
2478 sub parsed_difftree_line
{
2479 my $line_or_ref = shift;
2481 if (ref($line_or_ref) eq "HASH") {
2482 # pre-parsed (or generated by hand)
2483 return $line_or_ref;
2485 return parse_difftree_raw_line
($line_or_ref);
2489 # parse line of git-ls-tree output
2490 sub parse_ls_tree_line
($;%) {
2495 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2496 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2504 $res{'name'} = unquote
($4);
2507 return wantarray ? %res : \
%res;
2510 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2511 sub parse_from_to_diffinfo
{
2512 my ($diffinfo, $from, $to, @parents) = @_;
2514 if ($diffinfo->{'nparents'}) {
2516 $from->{'file'} = [];
2517 $from->{'href'} = [];
2518 fill_from_file_info
($diffinfo, @parents)
2519 unless exists $diffinfo->{'from_file'};
2520 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2521 $from->{'file'}[$i] =
2522 defined $diffinfo->{'from_file'}[$i] ?
2523 $diffinfo->{'from_file'}[$i] :
2524 $diffinfo->{'to_file'};
2525 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2526 $from->{'href'}[$i] = href
(action
=>"blob",
2527 hash_base
=>$parents[$i],
2528 hash
=>$diffinfo->{'from_id'}[$i],
2529 file_name
=>$from->{'file'}[$i]);
2531 $from->{'href'}[$i] = undef;
2535 # ordinary (not combined) diff
2536 $from->{'file'} = $diffinfo->{'from_file'};
2537 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2538 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2539 hash
=>$diffinfo->{'from_id'},
2540 file_name
=>$from->{'file'});
2542 delete $from->{'href'};
2546 $to->{'file'} = $diffinfo->{'to_file'};
2547 if (!is_deleted
($diffinfo)) { # file exists in result
2548 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2549 hash
=>$diffinfo->{'to_id'},
2550 file_name
=>$to->{'file'});
2552 delete $to->{'href'};
2556 ## ......................................................................
2557 ## parse to array of hashes functions
2559 sub git_get_heads_list
{
2563 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2564 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2565 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2568 while (my $line = <$fd>) {
2572 my ($refinfo, $committerinfo) = split(/\0/, $line);
2573 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2574 my ($committer, $epoch, $tz) =
2575 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2576 $ref_item{'fullname'} = $name;
2577 $name =~ s!^refs/heads/!!;
2579 $ref_item{'name'} = $name;
2580 $ref_item{'id'} = $hash;
2581 $ref_item{'title'} = $title || '(no commit message)';
2582 $ref_item{'epoch'} = $epoch;
2584 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2586 $ref_item{'age'} = "unknown";
2589 push @headslist, \
%ref_item;
2593 return wantarray ? @headslist : \
@headslist;
2596 sub git_get_tags_list
{
2600 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2601 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2602 '--format=%(objectname) %(objecttype) %(refname) '.
2603 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2606 while (my $line = <$fd>) {
2610 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2611 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2612 my ($creator, $epoch, $tz) =
2613 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2614 $ref_item{'fullname'} = $name;
2615 $name =~ s!^refs/tags/!!;
2617 $ref_item{'type'} = $type;
2618 $ref_item{'id'} = $id;
2619 $ref_item{'name'} = $name;
2620 if ($type eq "tag") {
2621 $ref_item{'subject'} = $title;
2622 $ref_item{'reftype'} = $reftype;
2623 $ref_item{'refid'} = $refid;
2625 $ref_item{'reftype'} = $type;
2626 $ref_item{'refid'} = $id;
2629 if ($type eq "tag" || $type eq "commit") {
2630 $ref_item{'epoch'} = $epoch;
2632 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2634 $ref_item{'age'} = "unknown";
2638 push @tagslist, \
%ref_item;
2642 return wantarray ? @tagslist : \
@tagslist;
2645 ## ----------------------------------------------------------------------
2646 ## filesystem-related functions
2648 sub get_file_owner
{
2651 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2652 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2653 if (!defined $gcos) {
2657 $owner =~ s/[,;].*$//;
2658 return to_utf8
($owner);
2661 ## ......................................................................
2662 ## mimetype related functions
2664 sub mimetype_guess_file
{
2665 my $filename = shift;
2666 my $mimemap = shift;
2667 -r
$mimemap or return undef;
2670 open(MIME
, $mimemap) or return undef;
2672 next if m/^#/; # skip comments
2673 my ($mime, $exts) = split(/\t+/);
2674 if (defined $exts) {
2675 my @exts = split(/\s+/, $exts);
2676 foreach my $ext (@exts) {
2677 $mimemap{$ext} = $mime;
2683 $filename =~ /\.([^.]*)$/;
2684 return $mimemap{$1};
2687 sub mimetype_guess
{
2688 my $filename = shift;
2690 $filename =~ /\./ or return undef;
2692 if ($mimetypes_file) {
2693 my $file = $mimetypes_file;
2694 if ($file !~ m!^/!) { # if it is relative path
2695 # it is relative to project
2696 $file = "$projectroot/$project/$file";
2698 $mime = mimetype_guess_file
($filename, $file);
2700 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2706 my $filename = shift;
2709 my $mime = mimetype_guess
($filename);
2710 $mime and return $mime;
2714 return $default_blob_plain_mimetype unless $fd;
2717 return 'text/plain';
2718 } elsif (! $filename) {
2719 return 'application/octet-stream';
2720 } elsif ($filename =~ m/\.png$/i) {
2722 } elsif ($filename =~ m/\.gif$/i) {
2724 } elsif ($filename =~ m/\.jpe?g$/i) {
2725 return 'image/jpeg';
2727 return 'application/octet-stream';
2731 sub blob_contenttype
{
2732 my ($fd, $file_name, $type) = @_;
2734 $type ||= blob_mimetype
($fd, $file_name);
2735 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2736 $type .= "; charset=$default_text_plain_charset";
2742 ## ======================================================================
2743 ## functions printing HTML: header, footer, error page
2745 sub git_header_html
{
2746 my $status = shift || "200 OK";
2747 my $expires = shift;
2749 my $title = "$site_name";
2750 if (defined $project) {
2751 $title .= " - " . to_utf8
($project);
2752 if (defined $action) {
2753 $title .= "/$action";
2754 if (defined $file_name) {
2755 $title .= " - " . esc_path
($file_name);
2756 if ($action eq "tree" && $file_name !~ m
|/$|) {
2763 # require explicit support from the UA if we are to send the page as
2764 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2765 # we have to do this because MSIE sometimes globs '*/*', pretending to
2766 # support xhtml+xml but choking when it gets what it asked for.
2767 if (defined $cgi->http('HTTP_ACCEPT') &&
2768 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2769 $cgi->Accept('application/xhtml+xml') != 0) {
2770 $content_type = 'application/xhtml+xml';
2772 $content_type = 'text/html';
2774 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2775 -status
=> $status, -expires
=> $expires);
2776 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2778 <?xml version="1.0" encoding="utf-8"?>
2779 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2780 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2781 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2782 <!-- git core binaries version $git_version -->
2784 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2785 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2786 <meta name="robots" content="index, nofollow"/>
2787 <title>$title</title>
2789 # print out each stylesheet that exist
2790 if (defined $stylesheet) {
2791 #provides backwards capability for those people who define style sheet in a config file
2792 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2794 foreach my $stylesheet (@stylesheets) {
2795 next unless $stylesheet;
2796 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2799 if (defined $project) {
2800 my %href_params = get_feed_info
();
2801 if (!exists $href_params{'-title'}) {
2802 $href_params{'-title'} = 'log';
2805 foreach my $format qw(RSS Atom) {
2806 my $type = lc($format);
2808 '-rel' => 'alternate',
2809 '-title' => "$project - $href_params{'-title'} - $format feed",
2810 '-type' => "application/$type+xml"
2813 $href_params{'action'} = $type;
2814 $link_attr{'-href'} = href
(%href_params);
2816 "rel=\"$link_attr{'-rel'}\" ".
2817 "title=\"$link_attr{'-title'}\" ".
2818 "href=\"$link_attr{'-href'}\" ".
2819 "type=\"$link_attr{'-type'}\" ".
2822 $href_params{'extra_options'} = '--no-merges';
2823 $link_attr{'-href'} = href
(%href_params);
2824 $link_attr{'-title'} .= ' (no merges)';
2826 "rel=\"$link_attr{'-rel'}\" ".
2827 "title=\"$link_attr{'-title'}\" ".
2828 "href=\"$link_attr{'-href'}\" ".
2829 "type=\"$link_attr{'-type'}\" ".
2834 printf('<link rel="alternate" title="%s projects list" '.
2835 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2836 $site_name, href
(project
=>undef, action
=>"project_index"));
2837 printf('<link rel="alternate" title="%s projects feeds" '.
2838 'href="%s" type="text/x-opml" />'."\n",
2839 $site_name, href
(project
=>undef, action
=>"opml"));
2841 if (defined $favicon) {
2842 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2848 if (-f
$site_header) {
2849 open (my $fd, $site_header);
2854 print "<div class=\"page_header\">\n" .
2855 $cgi->a({-href
=> esc_url
($logo_url),
2856 -title
=> $logo_label},
2857 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2858 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2859 if (defined $project) {
2860 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2861 if (defined $action) {
2868 my ($have_search) = gitweb_check_feature
('search');
2869 if (defined $project && $have_search) {
2870 if (!defined $searchtext) {
2874 if (defined $hash_base) {
2875 $search_hash = $hash_base;
2876 } elsif (defined $hash) {
2877 $search_hash = $hash;
2879 $search_hash = "HEAD";
2881 my $action = $my_uri;
2882 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2883 if ($use_pathinfo) {
2884 $action .= "/".esc_url
($project);
2886 print $cgi->startform(-method => "get", -action
=> $action) .
2887 "<div class=\"search\">\n" .
2889 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2890 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2891 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2892 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2893 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2894 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2896 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2897 "<span title=\"Extended regular expression\">" .
2898 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2899 -checked
=> $search_use_regexp) .
2902 $cgi->end_form() . "\n";
2906 sub git_footer_html
{
2907 my $feed_class = 'rss_logo';
2909 print "<div class=\"page_footer\">\n";
2910 if (defined $project) {
2911 my $descr = git_get_project_description
($project);
2912 if (defined $descr) {
2913 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2916 my %href_params = get_feed_info
();
2917 if (!%href_params) {
2918 $feed_class .= ' generic';
2920 $href_params{'-title'} ||= 'log';
2922 foreach my $format qw(RSS Atom) {
2923 $href_params{'action'} = lc($format);
2924 print $cgi->a({-href
=> href
(%href_params),
2925 -title
=> "$href_params{'-title'} $format feed",
2926 -class => $feed_class}, $format)."\n";
2930 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2931 -class => $feed_class}, "OPML") . " ";
2932 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2933 -class => $feed_class}, "TXT") . "\n";
2935 print "</div>\n"; # class="page_footer"
2937 if (-f
$site_footer) {
2938 open (my $fd, $site_footer);
2947 # die_error(<http_status_code>, <error_message>)
2948 # Example: die_error(404, 'Hash not found')
2949 # By convention, use the following status codes (as defined in RFC 2616):
2950 # 400: Invalid or missing CGI parameters, or
2951 # requested object exists but has wrong type.
2952 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2953 # this server or project.
2954 # 404: Requested object/revision/project doesn't exist.
2955 # 500: The server isn't configured properly, or
2956 # an internal error occurred (e.g. failed assertions caused by bugs), or
2957 # an unknown error occurred (e.g. the git binary died unexpectedly).
2959 my $status = shift || 500;
2960 my $error = shift || "Internal server error";
2962 my %http_responses = (400 => '400 Bad Request',
2963 403 => '403 Forbidden',
2964 404 => '404 Not Found',
2965 500 => '500 Internal Server Error');
2966 git_header_html
($http_responses{$status});
2968 <div class="page_body">
2978 ## ----------------------------------------------------------------------
2979 ## functions printing or outputting HTML: navigation
2981 sub git_print_page_nav
{
2982 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2983 $extra = '' if !defined $extra; # pager or formats
2985 my @navs = qw(summary shortlog log commit commitdiff tree);
2987 @navs = grep { $_ ne $suppress } @navs;
2990 my %arg = map { $_ => {action
=>$_} } @navs;
2991 if (defined $head) {
2992 for (qw(commit commitdiff)) {
2993 $arg{$_}{'hash'} = $head;
2995 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2996 for (qw(shortlog log)) {
2997 $arg{$_}{'hash'} = $head;
3002 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3003 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3005 my @actions = gitweb_check_feature
('actions');
3008 'n' => $project, # project name
3009 'f' => $git_dir, # project path within filesystem
3010 'h' => $treehead || '', # current hash ('h' parameter)
3011 'b' => $treebase || '', # hash base ('hb' parameter)
3014 my ($label, $link, $pos) = splice(@actions,0,3);
3016 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3018 $link =~ s/%([%nfhb])/$repl{$1}/g;
3019 $arg{$label}{'_href'} = $link;
3022 print "<div class=\"page_nav\">\n" .
3024 map { $_ eq $current ?
3025 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ? $arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
3027 print "<br/>\n$extra<br/>\n" .
3031 sub format_paging_nav
{
3032 my ($action, $hash, $head, $page, $has_next_link) = @_;
3036 if ($hash ne $head || $page) {
3037 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
3039 $paging_nav .= "HEAD";
3043 $paging_nav .= " ⋅ " .
3044 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
3045 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
3047 $paging_nav .= " ⋅ prev";
3050 if ($has_next_link) {
3051 $paging_nav .= " ⋅ " .
3052 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
3053 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
3055 $paging_nav .= " ⋅ next";
3061 ## ......................................................................
3062 ## functions printing or outputting HTML: div
3064 sub git_print_header_div
{
3065 my ($action, $title, $hash, $hash_base) = @_;
3068 $args{'action'} = $action;
3069 $args{'hash'} = $hash if $hash;
3070 $args{'hash_base'} = $hash_base if $hash_base;
3072 print "<div class=\"header\">\n" .
3073 $cgi->a({-href
=> href
(%args), -class => "title"},
3074 $title ? $title : $action) .
3078 #sub git_print_authorship (\%) {
3079 sub git_print_authorship
{
3082 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
3083 print "<div class=\"author_date\">" .
3084 esc_html
($co->{'author_name'}) .
3086 if ($ad{'hour_local'} < 6) {
3087 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3088 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3090 printf(" (%02d:%02d %s)",
3091 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3096 sub git_print_page_path
{
3102 print "<div class=\"page_path\">";
3103 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
3104 -title
=> 'tree root'}, to_utf8
("[$project]"));
3106 if (defined $name) {
3107 my @dirname = split '/', $name;
3108 my $basename = pop @dirname;
3111 foreach my $dir (@dirname) {
3112 $fullname .= ($fullname ? '/' : '') . $dir;
3113 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
3115 -title
=> $fullname}, esc_path
($dir));
3118 if (defined $type && $type eq 'blob') {
3119 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
3121 -title
=> $name}, esc_path
($basename));
3122 } elsif (defined $type && $type eq 'tree') {
3123 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
3125 -title
=> $name}, esc_path
($basename));
3128 print esc_path
($basename);
3131 print "<br/></div>\n";
3134 # sub git_print_log (\@;%) {
3135 sub git_print_log
($;%) {
3139 if ($opts{'-remove_title'}) {
3140 # remove title, i.e. first line of log
3143 # remove leading empty lines
3144 while (defined $log->[0] && $log->[0] eq "") {
3151 foreach my $line (@$log) {
3152 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3155 if (! $opts{'-remove_signoff'}) {
3156 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
3159 # remove signoff lines
3166 # print only one empty line
3167 # do not print empty line after signoff
3169 next if ($empty || $signoff);
3175 print format_log_line_html
($line) . "<br/>\n";
3178 if ($opts{'-final_empty_line'}) {
3179 # end with single empty line
3180 print "<br/>\n" unless $empty;
3184 # return link target (what link points to)
3185 sub git_get_link_target
{
3190 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
3194 $link_target = <$fd>;
3199 return $link_target;
3202 # given link target, and the directory (basedir) the link is in,
3203 # return target of link relative to top directory (top tree);
3204 # return undef if it is not possible (including absolute links).
3205 sub normalize_link_target
{
3206 my ($link_target, $basedir, $hash_base) = @_;
3208 # we can normalize symlink target only if $hash_base is provided
3209 return unless $hash_base;
3211 # absolute symlinks (beginning with '/') cannot be normalized
3212 return if (substr($link_target, 0, 1) eq '/');
3214 # normalize link target to path from top (root) tree (dir)
3217 $path = $basedir . '/' . $link_target;
3219 # we are in top (root) tree (dir)
3220 $path = $link_target;
3223 # remove //, /./, and /../
3225 foreach my $part (split('/', $path)) {
3226 # discard '.' and ''
3227 next if (!$part || $part eq '.');
3229 if ($part eq '..') {
3233 # link leads outside repository (outside top dir)
3237 push @path_parts, $part;
3240 $path = join('/', @path_parts);
3245 # print tree entry (row of git_tree), but without encompassing <tr> element
3246 sub git_print_tree_entry
{
3247 my ($t, $basedir, $hash_base, $have_blame) = @_;
3250 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3252 # The format of a table row is: mode list link. Where mode is
3253 # the mode of the entry, list is the name of the entry, an href,
3254 # and link is the action links of the entry.
3256 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3257 if ($t->{'type'} eq "blob") {
3258 print "<td class=\"list\">" .
3259 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3260 file_name
=>"$basedir$t->{'name'}", %base_key),
3261 -class => "list"}, esc_path
($t->{'name'}));
3262 if (S_ISLNK
(oct $t->{'mode'})) {
3263 my $link_target = git_get_link_target
($t->{'hash'});
3265 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3266 if (defined $norm_target) {
3268 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3269 file_name
=>$norm_target),
3270 -title
=> $norm_target}, esc_path
($link_target));
3272 print " -> " . esc_path
($link_target);
3277 print "<td class=\"link\">";
3278 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3279 file_name
=>"$basedir$t->{'name'}", %base_key)},
3283 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3284 file_name
=>"$basedir$t->{'name'}", %base_key)},
3287 if (defined $hash_base) {
3289 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3290 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3294 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3295 file_name
=>"$basedir$t->{'name'}")},
3299 } elsif ($t->{'type'} eq "tree") {
3300 print "<td class=\"list\">";
3301 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3302 file_name
=>"$basedir$t->{'name'}", %base_key)},
3303 esc_path
($t->{'name'}));
3305 print "<td class=\"link\">";
3306 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3307 file_name
=>"$basedir$t->{'name'}", %base_key)},
3309 if (defined $hash_base) {
3311 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3312 file_name
=>"$basedir$t->{'name'}")},
3317 # unknown object: we can only present history for it
3318 # (this includes 'commit' object, i.e. submodule support)
3319 print "<td class=\"list\">" .
3320 esc_path
($t->{'name'}) .
3322 print "<td class=\"link\">";
3323 if (defined $hash_base) {
3324 print $cgi->a({-href
=> href
(action
=>"history",
3325 hash_base
=>$hash_base,
3326 file_name
=>"$basedir$t->{'name'}")},
3333 ## ......................................................................
3334 ## functions printing large fragments of HTML
3336 # get pre-image filenames for merge (combined) diff
3337 sub fill_from_file_info
{
3338 my ($diff, @parents) = @_;
3340 $diff->{'from_file'} = [ ];
3341 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3342 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3343 if ($diff->{'status'}[$i] eq 'R' ||
3344 $diff->{'status'}[$i] eq 'C') {
3345 $diff->{'from_file'}[$i] =
3346 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3353 # is current raw difftree line of file deletion
3355 my $diffinfo = shift;
3357 return $diffinfo->{'to_id'} eq ('0' x
40);
3360 # does patch correspond to [previous] difftree raw line
3361 # $diffinfo - hashref of parsed raw diff format
3362 # $patchinfo - hashref of parsed patch diff format
3363 # (the same keys as in $diffinfo)
3364 sub is_patch_split
{
3365 my ($diffinfo, $patchinfo) = @_;
3367 return defined $diffinfo && defined $patchinfo
3368 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3372 sub git_difftree_body
{
3373 my ($difftree, $hash, @parents) = @_;
3374 my ($parent) = $parents[0];
3375 my ($have_blame) = gitweb_check_feature
('blame');
3376 print "<div class=\"list_head\">\n";
3377 if ($#{$difftree} > 10) {
3378 print(($#{$difftree} + 1) . " files changed:\n");
3382 print "<table class=\"" .
3383 (@parents > 1 ? "combined " : "") .
3386 # header only for combined diff in 'commitdiff' view
3387 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3390 print "<thead><tr>\n" .
3391 "<th></th><th></th>\n"; # filename, patchN link
3392 for (my $i = 0; $i < @parents; $i++) {
3393 my $par = $parents[$i];
3395 $cgi->a({-href
=> href
(action
=>"commitdiff",
3396 hash
=>$hash, hash_parent
=>$par),
3397 -title
=> 'commitdiff to parent number ' .
3398 ($i+1) . ': ' . substr($par,0,7)},
3402 print "</tr></thead>\n<tbody>\n";
3407 foreach my $line (@{$difftree}) {
3408 my $diff = parsed_difftree_line
($line);
3411 print "<tr class=\"dark\">\n";
3413 print "<tr class=\"light\">\n";
3417 if (exists $diff->{'nparents'}) { # combined diff
3419 fill_from_file_info
($diff, @parents)
3420 unless exists $diff->{'from_file'};
3422 if (!is_deleted
($diff)) {
3423 # file exists in the result (child) commit
3425 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3426 file_name
=>$diff->{'to_file'},
3428 -class => "list"}, esc_path
($diff->{'to_file'})) .
3432 esc_path
($diff->{'to_file'}) .
3436 if ($action eq 'commitdiff') {
3439 print "<td class=\"link\">" .
3440 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3445 my $has_history = 0;
3446 my $not_deleted = 0;
3447 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3448 my $hash_parent = $parents[$i];
3449 my $from_hash = $diff->{'from_id'}[$i];
3450 my $from_path = $diff->{'from_file'}[$i];
3451 my $status = $diff->{'status'}[$i];
3453 $has_history ||= ($status ne 'A');
3454 $not_deleted ||= ($status ne 'D');
3456 if ($status eq 'A') {
3457 print "<td class=\"link\" align=\"right\"> | </td>\n";
3458 } elsif ($status eq 'D') {
3459 print "<td class=\"link\">" .
3460 $cgi->a({-href
=> href
(action
=>"blob",
3463 file_name
=>$from_path)},
3467 if ($diff->{'to_id'} eq $from_hash) {
3468 print "<td class=\"link nochange\">";
3470 print "<td class=\"link\">";
3472 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3473 hash
=>$diff->{'to_id'},
3474 hash_parent
=>$from_hash,
3476 hash_parent_base
=>$hash_parent,
3477 file_name
=>$diff->{'to_file'},
3478 file_parent
=>$from_path)},
3484 print "<td class=\"link\">";
3486 print $cgi->a({-href
=> href
(action
=>"blob",
3487 hash
=>$diff->{'to_id'},
3488 file_name
=>$diff->{'to_file'},
3491 print " | " if ($has_history);
3494 print $cgi->a({-href
=> href
(action
=>"history",
3495 file_name
=>$diff->{'to_file'},
3502 next; # instead of 'else' clause, to avoid extra indent
3504 # else ordinary diff
3506 my ($to_mode_oct, $to_mode_str, $to_file_type);
3507 my ($from_mode_oct, $from_mode_str, $from_file_type);
3508 if ($diff->{'to_mode'} ne ('0' x
6)) {
3509 $to_mode_oct = oct $diff->{'to_mode'};
3510 if (S_ISREG
($to_mode_oct)) { # only for regular file
3511 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3513 $to_file_type = file_type
($diff->{'to_mode'});
3515 if ($diff->{'from_mode'} ne ('0' x
6)) {
3516 $from_mode_oct = oct $diff->{'from_mode'};
3517 if (S_ISREG
($to_mode_oct)) { # only for regular file
3518 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3520 $from_file_type = file_type
($diff->{'from_mode'});
3523 if ($diff->{'status'} eq "A") { # created
3524 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3525 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3526 $mode_chng .= "]</span>";
3528 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3529 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3530 -class => "list"}, esc_path
($diff->{'file'}));
3532 print "<td>$mode_chng</td>\n";
3533 print "<td class=\"link\">";
3534 if ($action eq 'commitdiff') {
3537 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3540 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3541 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3545 } elsif ($diff->{'status'} eq "D") { # deleted
3546 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3548 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3549 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3550 -class => "list"}, esc_path
($diff->{'file'}));
3552 print "<td>$mode_chng</td>\n";
3553 print "<td class=\"link\">";
3554 if ($action eq 'commitdiff') {
3557 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3560 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3561 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3564 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3565 file_name
=>$diff->{'file'})},
3568 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3569 file_name
=>$diff->{'file'})},
3573 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3574 my $mode_chnge = "";
3575 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3576 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3577 if ($from_file_type ne $to_file_type) {
3578 $mode_chnge .= " from $from_file_type to $to_file_type";
3580 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3581 if ($from_mode_str && $to_mode_str) {
3582 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3583 } elsif ($to_mode_str) {
3584 $mode_chnge .= " mode: $to_mode_str";
3587 $mode_chnge .= "]</span>\n";
3590 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3591 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3592 -class => "list"}, esc_path
($diff->{'file'}));
3594 print "<td>$mode_chnge</td>\n";
3595 print "<td class=\"link\">";
3596 if ($action eq 'commitdiff') {
3599 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3601 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3602 # "commit" view and modified file (not onlu mode changed)
3603 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3604 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3605 hash_base
=>$hash, hash_parent_base
=>$parent,
3606 file_name
=>$diff->{'file'})},
3610 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3611 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3614 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3615 file_name
=>$diff->{'file'})},
3618 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3619 file_name
=>$diff->{'file'})},
3623 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3624 my %status_name = ('R' => 'moved', 'C' => 'copied');
3625 my $nstatus = $status_name{$diff->{'status'}};
3627 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3628 # mode also for directories, so we cannot use $to_mode_str
3629 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3632 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3633 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3634 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3635 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3636 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3637 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3638 -class => "list"}, esc_path
($diff->{'from_file'})) .
3639 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3640 "<td class=\"link\">";
3641 if ($action eq 'commitdiff') {
3644 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3646 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3647 # "commit" view and modified file (not only pure rename or copy)
3648 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3649 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3650 hash_base
=>$hash, hash_parent_base
=>$parent,
3651 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3655 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3656 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3659 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3660 file_name
=>$diff->{'to_file'})},
3663 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3664 file_name
=>$diff->{'to_file'})},
3668 } # we should not encounter Unmerged (U) or Unknown (X) status
3671 print "</tbody>" if $has_header;
3675 sub git_patchset_body
{
3676 my ($fd, $difftree, $hash, @hash_parents) = @_;
3677 my ($hash_parent) = $hash_parents[0];
3679 my $is_combined = (@hash_parents > 1);
3681 my $patch_number = 0;
3687 print "<div class=\"patchset\">\n";
3689 # skip to first patch
3690 while ($patch_line = <$fd>) {
3693 last if ($patch_line =~ m/^diff /);
3697 while ($patch_line) {
3699 # parse "git diff" header line
3700 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3701 # $1 is from_name, which we do not use
3702 $to_name = unquote
($2);
3703 $to_name =~ s!^b/!!;
3704 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3705 # $1 is 'cc' or 'combined', which we do not use
3706 $to_name = unquote
($2);
3711 # check if current patch belong to current raw line
3712 # and parse raw git-diff line if needed
3713 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3714 # this is continuation of a split patch
3715 print "<div class=\"patch cont\">\n";
3717 # advance raw git-diff output if needed
3718 $patch_idx++ if defined $diffinfo;
3720 # read and prepare patch information
3721 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3723 # compact combined diff output can have some patches skipped
3724 # find which patch (using pathname of result) we are at now;
3726 while ($to_name ne $diffinfo->{'to_file'}) {
3727 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3728 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3729 "</div>\n"; # class="patch"
3734 last if $patch_idx > $#$difftree;
3735 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3739 # modifies %from, %to hashes
3740 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3742 # this is first patch for raw difftree line with $patch_idx index
3743 # we index @$difftree array from 0, but number patches from 1
3744 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3748 #assert($patch_line =~ m/^diff /) if DEBUG;
3749 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3751 # print "git diff" header
3752 print format_git_diff_header_line
($patch_line, $diffinfo,
3755 # print extended diff header
3756 print "<div class=\"diff extended_header\">\n";
3758 while ($patch_line = <$fd>) {
3761 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3763 print format_extended_diff_header_line
($patch_line, $diffinfo,
3766 print "</div>\n"; # class="diff extended_header"
3768 # from-file/to-file diff header
3769 if (! $patch_line) {
3770 print "</div>\n"; # class="patch"
3773 next PATCH
if ($patch_line =~ m/^diff /);
3774 #assert($patch_line =~ m/^---/) if DEBUG;
3776 my $last_patch_line = $patch_line;
3777 $patch_line = <$fd>;
3779 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3781 print format_diff_from_to_header
($last_patch_line, $patch_line,
3782 $diffinfo, \
%from, \
%to,
3787 while ($patch_line = <$fd>) {
3790 next PATCH
if ($patch_line =~ m/^diff /);
3792 print format_diff_line
($patch_line, \
%from, \
%to);
3796 print "</div>\n"; # class="patch"
3799 # for compact combined (--cc) format, with chunk and patch simpliciaction
3800 # patchset might be empty, but there might be unprocessed raw lines
3801 for (++$patch_idx if $patch_number > 0;
3802 $patch_idx < @$difftree;
3804 # read and prepare patch information
3805 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3807 # generate anchor for "patch" links in difftree / whatchanged part
3808 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3809 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3810 "</div>\n"; # class="patch"
3815 if ($patch_number == 0) {
3816 if (@hash_parents > 1) {
3817 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3819 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3823 print "</div>\n"; # class="patchset"
3826 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3828 # fills project list info (age, description, owner, forks) for each
3829 # project in the list, removing invalid projects from returned list
3830 # NOTE: modifies $projlist, but does not remove entries from it
3831 sub fill_project_list_info
{
3832 my ($projlist, $check_forks) = @_;
3835 my $show_ctags = gitweb_check_feature
('ctags');
3837 foreach my $pr (@$projlist) {
3838 my (@activity) = git_get_last_activity
($pr->{'path'});
3839 unless (@activity) {
3842 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3843 if (!defined $pr->{'descr'}) {
3844 my $descr = git_get_project_description
($pr->{'path'}) || "";
3845 $descr = to_utf8
($descr);
3846 $pr->{'descr_long'} = $descr;
3847 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3849 if (!defined $pr->{'owner'}) {
3850 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3853 my $pname = $pr->{'path'};
3854 if (($pname =~ s/\.git$//) &&
3855 ($pname !~ /\/$/) &&
3856 (-d
"$projectroot/$pname")) {
3857 $pr->{'forks'} = "-d $projectroot/$pname";
3862 $show_ctags and $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
3863 push @projects, $pr;
3869 # print 'sort by' <th> element, generating 'sort by $name' replay link
3870 # if that order is not selected
3872 my ($name, $order, $header) = @_;
3873 $header ||= ucfirst($name);
3875 if ($order eq $name) {
3876 print "<th>$header</th>\n";
3879 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3880 -class => "header"}, $header) .
3885 sub git_project_list_body
{
3886 # actually uses global variable $project
3887 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3889 my ($check_forks) = gitweb_check_feature
('forks');
3890 my @projects = fill_project_list_info
($projlist, $check_forks);
3892 $order ||= $default_projects_order;
3893 $from = 0 unless defined $from;
3894 $to = $#projects if (!defined $to || $#projects < $to);
3897 project
=> { key
=> 'path', type
=> 'str' },
3898 descr
=> { key
=> 'descr_long', type
=> 'str' },
3899 owner
=> { key
=> 'owner', type
=> 'str' },
3900 age
=> { key
=> 'age', type
=> 'num' }
3902 my $oi = $order_info{$order};
3903 if ($oi->{'type'} eq 'str') {
3904 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3906 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3909 my $show_ctags = gitweb_check_feature
('ctags');
3912 foreach my $p (@projects) {
3913 foreach my $ct (keys %{$p->{'ctags'}}) {
3914 $ctags{$ct} += $p->{'ctags'}->{$ct};
3917 my $cloud = git_populate_project_tagcloud
(\
%ctags);
3918 print git_show_project_tagcloud
($cloud, 64);
3921 print "<table class=\"project_list\">\n";
3922 unless ($no_header) {
3925 print "<th></th>\n";
3927 print_sort_th
('project', $order, 'Project');
3928 print_sort_th
('descr', $order, 'Description');
3929 print_sort_th
('owner', $order, 'Owner');
3930 print_sort_th
('age', $order, 'Last Change');
3931 print "<th></th>\n" . # for links
3935 my $tagfilter = $cgi->param('by_tag');
3936 for (my $i = $from; $i <= $to; $i++) {
3937 my $pr = $projects[$i];
3939 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3940 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3941 and not $pr->{'descr_long'} =~ /$searchtext/;
3942 # Weed out forks or non-matching entries of search
3944 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s
#\.git$#/#;
3945 $forkbase="^$forkbase" if $forkbase;
3946 next if not $searchtext and not $tagfilter and $show_ctags
3947 and $pr->{'path'} =~ m
#$forkbase.*/.*#; # regexp-safe
3951 print "<tr class=\"dark\">\n";
3953 print "<tr class=\"light\">\n";
3958 if ($pr->{'forks'}) {
3959 print "<!-- $pr->{'forks'} -->\n";
3960 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3964 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3965 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3966 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3967 -class => "list", -title
=> $pr->{'descr_long'}},
3968 esc_html
($pr->{'descr'})) . "</td>\n" .
3969 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3970 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3971 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3972 "<td class=\"link\">" .
3973 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3974 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3975 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3976 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3977 ($pr->{'forks'} ? " | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3981 if (defined $extra) {
3984 print "<td></td>\n";
3986 print "<td colspan=\"5\">$extra</td>\n" .
3992 sub git_shortlog_body
{
3993 # uses global variable $project
3994 my ($commitlist, $from, $to, $refs, $extra) = @_;
3996 $from = 0 unless defined $from;
3997 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3999 print "<table class=\"shortlog\">\n";
4001 for (my $i = $from; $i <= $to; $i++) {
4002 my %co = %{$commitlist->[$i]};
4003 my $commit = $co{'id'};
4004 my $ref = format_ref_marker
($refs, $commit);
4006 print "<tr class=\"dark\">\n";
4008 print "<tr class=\"light\">\n";
4011 my $author = chop_and_escape_str
($co{'author_name'}, 10);
4012 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4013 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4014 "<td><i>" . $author . "</i></td>\n" .
4016 print format_subject_html
($co{'title'}, $co{'title_short'},
4017 href
(action
=>"commit", hash
=>$commit), $ref);
4019 "<td class=\"link\">" .
4020 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
4021 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
4022 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
4023 my $snapshot_links = format_snapshot_links
($commit);
4024 if (defined $snapshot_links) {
4025 print " | " . $snapshot_links;
4030 if (defined $extra) {
4032 "<td colspan=\"4\">$extra</td>\n" .
4038 sub git_history_body
{
4039 # Warning: assumes constant type (blob or tree) during history
4040 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4042 $from = 0 unless defined $from;
4043 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4045 print "<table class=\"history\">\n";
4047 for (my $i = $from; $i <= $to; $i++) {
4048 my %co = %{$commitlist->[$i]};
4052 my $commit = $co{'id'};
4054 my $ref = format_ref_marker
($refs, $commit);
4057 print "<tr class=\"dark\">\n";
4059 print "<tr class=\"light\">\n";
4062 # shortlog uses chop_str($co{'author_name'}, 10)
4063 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
4064 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4065 "<td><i>" . $author . "</i></td>\n" .
4067 # originally git_history used chop_str($co{'title'}, 50)
4068 print format_subject_html
($co{'title'}, $co{'title_short'},
4069 href
(action
=>"commit", hash
=>$commit), $ref);
4071 "<td class=\"link\">" .
4072 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
4073 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
4075 if ($ftype eq 'blob') {
4076 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
4077 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
4078 if (defined $blob_current && defined $blob_parent &&
4079 $blob_current ne $blob_parent) {
4081 $cgi->a({-href
=> href
(action
=>"blobdiff",
4082 hash
=>$blob_current, hash_parent
=>$blob_parent,
4083 hash_base
=>$hash_base, hash_parent_base
=>$commit,
4084 file_name
=>$file_name)},
4091 if (defined $extra) {
4093 "<td colspan=\"4\">$extra</td>\n" .
4100 # uses global variable $project
4101 my ($taglist, $from, $to, $extra) = @_;
4102 $from = 0 unless defined $from;
4103 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4105 print "<table class=\"tags\">\n";
4107 for (my $i = $from; $i <= $to; $i++) {
4108 my $entry = $taglist->[$i];
4110 my $comment = $tag{'subject'};
4112 if (defined $comment) {
4113 $comment_short = chop_str
($comment, 30, 5);
4116 print "<tr class=\"dark\">\n";
4118 print "<tr class=\"light\">\n";
4121 if (defined $tag{'age'}) {
4122 print "<td><i>$tag{'age'}</i></td>\n";
4124 print "<td></td>\n";
4127 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
4128 -class => "list name"}, esc_html
($tag{'name'})) .
4131 if (defined $comment) {
4132 print format_subject_html
($comment, $comment_short,
4133 href
(action
=>"tag", hash
=>$tag{'id'}));
4136 "<td class=\"selflink\">";
4137 if ($tag{'type'} eq "tag") {
4138 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
4143 "<td class=\"link\">" . " | " .
4144 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
4145 if ($tag{'reftype'} eq "commit") {
4146 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
4147 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
4148 } elsif ($tag{'reftype'} eq "blob") {
4149 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
4154 if (defined $extra) {
4156 "<td colspan=\"5\">$extra</td>\n" .
4162 sub git_heads_body
{
4163 # uses global variable $project
4164 my ($headlist, $head, $from, $to, $extra) = @_;
4165 $from = 0 unless defined $from;
4166 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4168 print "<table class=\"heads\">\n";
4170 for (my $i = $from; $i <= $to; $i++) {
4171 my $entry = $headlist->[$i];
4173 my $curr = $ref{'id'} eq $head;
4175 print "<tr class=\"dark\">\n";
4177 print "<tr class=\"light\">\n";
4180 print "<td><i>$ref{'age'}</i></td>\n" .
4181 ($curr ? "<td class=\"current_head\">" : "<td>") .
4182 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
4183 -class => "list name"},esc_html
($ref{'name'})) .
4185 "<td class=\"link\">" .
4186 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
4187 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
4188 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
4192 if (defined $extra) {
4194 "<td colspan=\"3\">$extra</td>\n" .
4200 sub git_search_grep_body
{
4201 my ($commitlist, $from, $to, $extra) = @_;
4202 $from = 0 unless defined $from;
4203 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4205 print "<table class=\"commit_search\">\n";
4207 for (my $i = $from; $i <= $to; $i++) {
4208 my %co = %{$commitlist->[$i]};
4212 my $commit = $co{'id'};
4214 print "<tr class=\"dark\">\n";
4216 print "<tr class=\"light\">\n";
4219 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
4220 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4221 "<td><i>" . $author . "</i></td>\n" .
4223 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
4224 -class => "list subject"},
4225 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
4226 my $comment = $co{'comment'};
4227 foreach my $line (@$comment) {
4228 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4229 my ($lead, $match, $trail) = ($1, $2, $3);
4230 $match = chop_str
($match, 70, 5, 'center');
4231 my $contextlen = int((80 - length($match))/2);
4232 $contextlen = 30 if ($contextlen > 30);
4233 $lead = chop_str
($lead, $contextlen, 10, 'left');
4234 $trail = chop_str
($trail, $contextlen, 10, 'right');
4236 $lead = esc_html
($lead);
4237 $match = esc_html
($match);
4238 $trail = esc_html
($trail);
4240 print "$lead<span class=\"match\">$match</span>$trail<br />";
4244 "<td class=\"link\">" .
4245 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4247 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4249 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4253 if (defined $extra) {
4255 "<td colspan=\"3\">$extra</td>\n" .
4261 ## ======================================================================
4262 ## ======================================================================
4265 sub git_project_list
{
4266 my $order = $input_params{'order'};
4267 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4268 die_error
(400, "Unknown order parameter");
4271 my @list = git_get_projects_list
();
4273 die_error
(404, "No projects found");
4277 if (-f
$home_text) {
4278 print "<div class=\"index_include\">\n";
4279 open (my $fd, $home_text);
4284 print $cgi->startform(-method => "get") .
4285 "<p class=\"projsearch\">Search:\n" .
4286 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
4288 $cgi->end_form() . "\n";
4289 git_project_list_body
(\
@list, $order);
4294 my $order = $input_params{'order'};
4295 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4296 die_error
(400, "Unknown order parameter");
4299 my @list = git_get_projects_list
($project);
4301 die_error
(404, "No forks found");
4305 git_print_page_nav
('','');
4306 git_print_header_div
('summary', "$project forks");
4307 git_project_list_body
(\
@list, $order);
4311 sub git_project_index
{
4312 my @projects = git_get_projects_list
($project);
4315 -type
=> 'text/plain',
4316 -charset
=> 'utf-8',
4317 -content_disposition
=> 'inline; filename="index.aux"');
4319 foreach my $pr (@projects) {
4320 if (!exists $pr->{'owner'}) {
4321 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4324 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4325 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4326 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4327 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4331 print "$path $owner\n";
4336 my $descr = git_get_project_description
($project) || "none";
4337 my %co = parse_commit
("HEAD");
4338 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4339 my $head = $co{'id'};
4341 my $owner = git_get_project_owner
($project);
4343 my $refs = git_get_references
();
4344 # These get_*_list functions return one more to allow us to see if
4345 # there are more ...
4346 my @taglist = git_get_tags_list
(16);
4347 my @headlist = git_get_heads_list
(16);
4349 my ($check_forks) = gitweb_check_feature
('forks');
4352 @forklist = git_get_projects_list
($project);
4356 git_print_page_nav
('summary','', $head);
4358 print "<div class=\"title\"> </div>\n";
4359 print "<table class=\"projects_list\">\n" .
4360 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4361 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4362 if (defined $cd{'rfc2822'}) {
4363 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4366 # use per project git URL list in $projectroot/$project/cloneurl
4367 # or make project git URL from git base URL and project name
4368 my $url_tag = "URL";
4369 my @url_list = git_get_project_url_list
($project);
4370 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4371 foreach my $git_url (@url_list) {
4372 next unless $git_url;
4373 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4378 my $show_ctags = (gitweb_check_feature
('ctags'))[0];
4380 my $ctags = git_get_project_ctags
($project);
4381 my $cloud = git_populate_project_tagcloud
($ctags);
4382 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4383 print "</td>\n<td>" unless %$ctags;
4384 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4385 print "</td>\n<td>" if %$ctags;
4386 print git_show_project_tagcloud
($cloud, 48);
4392 if (-s
"$projectroot/$project/README.html") {
4393 if (open my $fd, "$projectroot/$project/README.html") {
4394 print "<div class=\"title\">readme</div>\n" .
4395 "<div class=\"readme\">\n";
4396 print $_ while (<$fd>);
4397 print "\n</div>\n"; # class="readme"
4402 # we need to request one more than 16 (0..15) to check if
4404 my @commitlist = $head ? parse_commits
($head, 17) : ();
4406 git_print_header_div
('shortlog');
4407 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4408 $#commitlist <= 15 ? undef :
4409 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4413 git_print_header_div
('tags');
4414 git_tags_body
(\
@taglist, 0, 15,
4415 $#taglist <= 15 ? undef :
4416 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4420 git_print_header_div
('heads');
4421 git_heads_body
(\
@headlist, $head, 0, 15,
4422 $#headlist <= 15 ? undef :
4423 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4427 git_print_header_div
('forks');
4428 git_project_list_body
(\
@forklist, 'age', 0, 15,
4429 $#forklist <= 15 ? undef :
4430 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4438 my $head = git_get_head_hash
($project);
4440 git_print_page_nav
('','', $head,undef,$head);
4441 my %tag = parse_tag
($hash);
4444 die_error
(404, "Unknown tag object");
4447 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4448 print "<div class=\"title_text\">\n" .
4449 "<table class=\"object_header\">\n" .
4451 "<td>object</td>\n" .
4452 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4453 $tag{'object'}) . "</td>\n" .
4454 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4455 $tag{'type'}) . "</td>\n" .
4457 if (defined($tag{'author'})) {
4458 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4459 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4460 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4461 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4464 print "</table>\n\n" .
4466 print "<div class=\"page_body\">";
4467 my $comment = $tag{'comment'};
4468 foreach my $line (@$comment) {
4470 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4480 gitweb_check_feature
('blame')
4481 or die_error
(403, "Blame view not allowed");
4483 die_error
(400, "No file name given") unless $file_name;
4484 $hash_base ||= git_get_head_hash
($project);
4485 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4486 my %co = parse_commit
($hash_base)
4487 or die_error
(404, "Commit not found");
4488 if (!defined $hash) {
4489 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4490 or die_error
(404, "Error looking up file");
4492 $ftype = git_get_type
($hash);
4493 if ($ftype !~ "blob") {
4494 die_error
(400, "Object is not a blob");
4496 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4497 $file_name, $hash_base)
4498 or die_error
(500, "Open git-blame failed");
4501 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4504 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4507 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4509 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4510 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4511 git_print_page_path
($file_name, $ftype, $hash_base);
4512 my @rev_color = (qw(light2 dark2));
4513 my $num_colors = scalar(@rev_color);
4514 my $current_color = 0;
4517 <div class="page_body">
4518 <table class="blame">
4519 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4524 last unless defined $_;
4525 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4526 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4527 if (!exists $metainfo{$full_rev}) {
4528 $metainfo{$full_rev} = {};
4530 my $meta = $metainfo{$full_rev};
4533 if (/^(\S+) (.*)$/) {
4539 my $rev = substr($full_rev, 0, 8);
4540 my $author = $meta->{'author'};
4541 my %date = parse_date
($meta->{'author-time'},
4542 $meta->{'author-tz'});
4543 my $date = $date{'iso-tz'};
4545 $current_color = ++$current_color % $num_colors;
4547 print "<tr class=\"$rev_color[$current_color]\">\n";
4549 print "<td class=\"sha1\"";
4550 print " title=\"". esc_html
($author) . ", $date\"";
4551 print " rowspan=\"$group_size\"" if ($group_size > 1);
4553 print $cgi->a({-href
=> href
(action
=>"commit",
4555 file_name
=>$file_name)},
4559 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4560 or die_error
(500, "Open git-rev-parse failed");
4561 my $parent_commit = <$dd>;
4563 chomp($parent_commit);
4564 my $blamed = href
(action
=> 'blame',
4565 file_name
=> $meta->{'filename'},
4566 hash_base
=> $parent_commit);
4567 print "<td class=\"linenr\">";
4568 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4570 -class => "linenr" },
4573 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4579 or print "Reading blob failed\n";
4584 my $head = git_get_head_hash
($project);
4586 git_print_page_nav
('','', $head,undef,$head);
4587 git_print_header_div
('summary', $project);
4589 my @tagslist = git_get_tags_list
();
4591 git_tags_body
(\
@tagslist);
4597 my $head = git_get_head_hash
($project);
4599 git_print_page_nav
('','', $head,undef,$head);
4600 git_print_header_div
('summary', $project);
4602 my @headslist = git_get_heads_list
();
4604 git_heads_body
(\
@headslist, $head);
4609 sub git_blob_plain
{
4613 if (!defined $hash) {
4614 if (defined $file_name) {
4615 my $base = $hash_base || git_get_head_hash
($project);
4616 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4617 or die_error
(404, "Cannot find file");
4619 die_error
(400, "No file name defined");
4621 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4622 # blobs defined by non-textual hash id's can be cached
4626 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4627 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4629 # content-type (can include charset)
4630 $type = blob_contenttype
($fd, $file_name, $type);
4632 # "save as" filename, even when no $file_name is given
4633 my $save_as = "$hash";
4634 if (defined $file_name) {
4635 $save_as = $file_name;
4636 } elsif ($type =~ m/^text\//) {
4642 -expires
=> $expires,
4643 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4645 binmode STDOUT
, ':raw';
4647 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4655 if (!defined $hash) {
4656 if (defined $file_name) {
4657 my $base = $hash_base || git_get_head_hash
($project);
4658 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4659 or die_error
(404, "Cannot find file");
4661 die_error
(400, "No file name defined");
4663 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4664 # blobs defined by non-textual hash id's can be cached
4668 my ($have_blame) = gitweb_check_feature
('blame');
4669 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4670 or die_error
(500, "Couldn't cat $file_name, $hash");
4671 my $mimetype = blob_mimetype
($fd, $file_name);
4672 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4674 return git_blob_plain
($mimetype);
4676 # we can have blame only for text/* mimetype
4677 $have_blame &&= ($mimetype =~ m!^text/!);
4679 git_header_html
(undef, $expires);
4680 my $formats_nav = '';
4681 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4682 if (defined $file_name) {
4685 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4690 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4693 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4696 $cgi->a({-href
=> href
(action
=>"blob",
4697 hash_base
=>"HEAD", file_name
=>$file_name)},
4701 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4704 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4705 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4707 print "<div class=\"page_nav\">\n" .
4708 "<br/><br/></div>\n" .
4709 "<div class=\"title\">$hash</div>\n";
4711 git_print_page_path
($file_name, "blob", $hash_base);
4712 print "<div class=\"page_body\">\n";
4713 if ($mimetype =~ m!^image/!) {
4714 print qq
!<img type
="$mimetype"!;
4716 print qq
! alt
="$file_name" title
="$file_name"!;
4719 href(action=>"blob_plain
", hash=>$hash,
4720 hash_base=>$hash_base, file_name=>$file_name) .
4724 while (my $line = <$fd>) {
4727 $line = untabify
($line);
4728 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4729 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4733 or print "Reading blob failed.\n";
4739 if (!defined $hash_base) {
4740 $hash_base = "HEAD";
4742 if (!defined $hash) {
4743 if (defined $file_name) {
4744 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4749 die_error
(404, "No such tree") unless defined($hash);
4751 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4752 or die_error
(500, "Open git-ls-tree failed");
4753 my @entries = map { chomp; $_ } <$fd>;
4754 close $fd or die_error
(404, "Reading tree failed");
4757 my $refs = git_get_references
();
4758 my $ref = format_ref_marker
($refs, $hash_base);
4761 my ($have_blame) = gitweb_check_feature
('blame');
4762 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4764 if (defined $file_name) {
4766 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4768 $cgi->a({-href
=> href
(action
=>"tree",
4769 hash_base
=>"HEAD", file_name
=>$file_name)},
4772 my $snapshot_links = format_snapshot_links
($hash);
4773 if (defined $snapshot_links) {
4774 # FIXME: Should be available when we have no hash base as well.
4775 push @views_nav, $snapshot_links;
4777 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4778 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4781 print "<div class=\"page_nav\">\n";
4782 print "<br/><br/></div>\n";
4783 print "<div class=\"title\">$hash</div>\n";
4785 if (defined $file_name) {
4786 $basedir = $file_name;
4787 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4790 git_print_page_path
($file_name, 'tree', $hash_base);
4792 print "<div class=\"page_body\">\n";
4793 print "<table class=\"tree\">\n";
4795 # '..' (top directory) link if possible
4796 if (defined $hash_base &&
4797 defined $file_name && $file_name =~ m![^/]+$!) {
4799 print "<tr class=\"dark\">\n";
4801 print "<tr class=\"light\">\n";
4805 my $up = $file_name;
4806 $up =~ s!/?[^/]+$!!;
4807 undef $up unless $up;
4808 # based on git_print_tree_entry
4809 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4810 print '<td class="list">';
4811 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4815 print "<td class=\"link\"></td>\n";
4819 foreach my $line (@entries) {
4820 my %t = parse_ls_tree_line
($line, -z
=> 1);
4823 print "<tr class=\"dark\">\n";
4825 print "<tr class=\"light\">\n";
4829 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4833 print "</table>\n" .
4839 my @supported_fmts = gitweb_check_feature
('snapshot');
4840 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4842 my $format = $input_params{'snapshot_format'};
4843 if (!@supported_fmts) {
4844 die_error
(403, "Snapshots not allowed");
4846 # default to first supported snapshot format
4847 $format ||= $supported_fmts[0];
4848 if ($format !~ m/^[a-z0-9]+$/) {
4849 die_error
(400, "Invalid snapshot format parameter");
4850 } elsif (!exists($known_snapshot_formats{$format})) {
4851 die_error
(400, "Unknown snapshot format");
4852 } elsif (!grep($_ eq $format, @supported_fmts)) {
4853 die_error
(403, "Unsupported snapshot format");
4856 if (!defined $hash) {
4857 $hash = git_get_head_hash
($project);
4860 my $name = $project;
4861 $name =~ s
,([^/])/*\
.git
$,$1,;
4862 $name = basename
($name);
4863 my $filename = to_utf8
($name);
4864 $name =~ s/\047/\047\\\047\047/g;
4866 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4867 $cmd = quote_command
(
4868 git_cmd
(), 'archive',
4869 "--format=$known_snapshot_formats{$format}{'format'}",
4870 "--prefix=$name/", $hash);
4871 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4872 $cmd .= ' | ' . quote_command
(@{$known_snapshot_formats{$format}{'compressor'}});
4876 -type
=> $known_snapshot_formats{$format}{'type'},
4877 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4878 -status
=> '200 OK');
4880 open my $fd, "-|", $cmd
4881 or die_error
(500, "Execute git-archive failed");
4882 binmode STDOUT
, ':raw';
4884 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4889 my $head = git_get_head_hash
($project);
4890 if (!defined $hash) {
4893 if (!defined $page) {
4896 my $refs = git_get_references
();
4898 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4900 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4903 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4906 my %co = parse_commit
($hash);
4908 git_print_header_div
('summary', $project);
4909 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4911 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4912 for (my $i = 0; $i <= $to; $i++) {
4913 my %co = %{$commitlist[$i]};
4915 my $commit = $co{'id'};
4916 my $ref = format_ref_marker
($refs, $commit);
4917 my %ad = parse_date
($co{'author_epoch'});
4918 git_print_header_div
('commit',
4919 "<span class=\"age\">$co{'age_string'}</span>" .
4920 esc_html
($co{'title'}) . $ref,
4922 print "<div class=\"title_text\">\n" .
4923 "<div class=\"log_link\">\n" .
4924 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4926 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4928 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4931 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4934 print "<div class=\"log_body\">\n";
4935 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4938 if ($#commitlist >= 100) {
4939 print "<div class=\"page_nav\">\n";
4940 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4941 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4948 $hash ||= $hash_base || "HEAD";
4949 my %co = parse_commit
($hash)
4950 or die_error
(404, "Unknown commit object");
4951 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4952 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4954 my $parent = $co{'parent'};
4955 my $parents = $co{'parents'}; # listref
4957 # we need to prepare $formats_nav before any parameter munging
4959 if (!defined $parent) {
4961 $formats_nav .= '(initial)';
4962 } elsif (@$parents == 1) {
4963 # single parent commit
4966 $cgi->a({-href
=> href
(action
=>"commit",
4968 esc_html
(substr($parent, 0, 7))) .
4975 $cgi->a({-href
=> href
(action
=>"commit",
4977 esc_html
(substr($_, 0, 7)));
4982 if (!defined $parent) {
4986 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4988 (@$parents <= 1 ? $parent : '-c'),
4990 or die_error
(500, "Open git-diff-tree failed");
4991 @difftree = map { chomp; $_ } <$fd>;
4992 close $fd or die_error
(404, "Reading git-diff-tree failed");
4994 # non-textual hash id's can be cached
4996 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4999 my $refs = git_get_references
();
5000 my $ref = format_ref_marker
($refs, $co{'id'});
5002 git_header_html
(undef, $expires);
5003 git_print_page_nav
('commit', '',
5004 $hash, $co{'tree'}, $hash,
5007 if (defined $co{'parent'}) {
5008 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
5010 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
5012 print "<div class=\"title_text\">\n" .
5013 "<table class=\"object_header\">\n";
5014 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
5016 "<td></td><td> $ad{'rfc2822'}";
5017 if ($ad{'hour_local'} < 6) {
5018 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5019 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5021 printf(" (%02d:%02d %s)",
5022 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5026 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
5027 print "<tr><td></td><td> $cd{'rfc2822'}" .
5028 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5030 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5033 "<td class=\"sha1\">" .
5034 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
5035 class => "list"}, $co{'tree'}) .
5037 "<td class=\"link\">" .
5038 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
5040 my $snapshot_links = format_snapshot_links
($hash);
5041 if (defined $snapshot_links) {
5042 print " | " . $snapshot_links;
5047 foreach my $par (@$parents) {
5050 "<td class=\"sha1\">" .
5051 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
5052 class => "list"}, $par) .
5054 "<td class=\"link\">" .
5055 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
5057 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
5064 print "<div class=\"page_body\">\n";
5065 git_print_log
($co{'comment'});
5068 git_difftree_body
(\
@difftree, $hash, @$parents);
5074 # object is defined by:
5075 # - hash or hash_base alone
5076 # - hash_base and file_name
5079 # - hash or hash_base alone
5080 if ($hash || ($hash_base && !defined $file_name)) {
5081 my $object_id = $hash || $hash_base;
5083 open my $fd, "-|", quote_command
(
5084 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5085 or die_error
(404, "Object does not exist");
5089 or die_error
(404, "Object does not exist");
5091 # - hash_base and file_name
5092 } elsif ($hash_base && defined $file_name) {
5093 $file_name =~ s
,/+$,,;
5095 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
5096 or die_error
(404, "Base object does not exist");
5098 # here errors should not hapen
5099 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
5100 or die_error
(500, "Open git-ls-tree failed");
5104 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5105 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5106 die_error
(404, "File or directory for given base does not exist");
5111 die_error
(400, "Not enough information to find object");
5114 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
5115 hash
=>$hash, hash_base
=>$hash_base,
5116 file_name
=>$file_name),
5117 -status
=> '302 Found');
5121 my $format = shift || 'html';
5128 # preparing $fd and %diffinfo for git_patchset_body
5130 if (defined $hash_base && defined $hash_parent_base) {
5131 if (defined $file_name) {
5133 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5134 $hash_parent_base, $hash_base,
5135 "--", (defined $file_parent ? $file_parent : ()), $file_name
5136 or die_error
(500, "Open git-diff-tree failed");
5137 @difftree = map { chomp; $_ } <$fd>;
5139 or die_error
(404, "Reading git-diff-tree failed");
5141 or die_error
(404, "Blob diff not found");
5143 } elsif (defined $hash &&
5144 $hash =~ /[0-9a-fA-F]{40}/) {
5145 # try to find filename from $hash
5147 # read filtered raw output
5148 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5149 $hash_parent_base, $hash_base, "--"
5150 or die_error
(500, "Open git-diff-tree failed");
5152 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5154 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5155 map { chomp; $_ } <$fd>;
5157 or die_error
(404, "Reading git-diff-tree failed");
5159 or die_error
(404, "Blob diff not found");
5162 die_error
(400, "Missing one of the blob diff parameters");
5165 if (@difftree > 1) {
5166 die_error
(400, "Ambiguous blob diff specification");
5169 %diffinfo = parse_difftree_raw_line
($difftree[0]);
5170 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5171 $file_name ||= $diffinfo{'to_file'};
5173 $hash_parent ||= $diffinfo{'from_id'};
5174 $hash ||= $diffinfo{'to_id'};
5176 # non-textual hash id's can be cached
5177 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5178 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5183 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5184 '-p', ($format eq 'html' ? "--full-index" : ()),
5185 $hash_parent_base, $hash_base,
5186 "--", (defined $file_parent ? $file_parent : ()), $file_name
5187 or die_error
(500, "Open git-diff-tree failed");
5190 # old/legacy style URI
5191 if (!%diffinfo && # if new style URI failed
5192 defined $hash && defined $hash_parent) {
5193 # fake git-diff-tree raw output
5194 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5195 $diffinfo{'from_id'} = $hash_parent;
5196 $diffinfo{'to_id'} = $hash;
5197 if (defined $file_name) {
5198 if (defined $file_parent) {
5199 $diffinfo{'status'} = '2';
5200 $diffinfo{'from_file'} = $file_parent;
5201 $diffinfo{'to_file'} = $file_name;
5202 } else { # assume not renamed
5203 $diffinfo{'status'} = '1';
5204 $diffinfo{'from_file'} = $file_name;
5205 $diffinfo{'to_file'} = $file_name;
5207 } else { # no filename given
5208 $diffinfo{'status'} = '2';
5209 $diffinfo{'from_file'} = $hash_parent;
5210 $diffinfo{'to_file'} = $hash;
5213 # non-textual hash id's can be cached
5214 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5215 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5220 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
5221 '-p', ($format eq 'html' ? "--full-index" : ()),
5222 $hash_parent, $hash, "--"
5223 or die_error
(500, "Open git-diff failed");
5225 die_error
(400, "Missing one of the blob diff parameters")
5230 if ($format eq 'html') {
5232 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
5234 git_header_html
(undef, $expires);
5235 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
5236 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5237 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5239 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5240 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5242 if (defined $file_name) {
5243 git_print_page_path
($file_name, "blob", $hash_base);
5245 print "<div class=\"page_path\"></div>\n";
5248 } elsif ($format eq 'plain') {
5250 -type
=> 'text/plain',
5251 -charset
=> 'utf-8',
5252 -expires
=> $expires,
5253 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
5255 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5258 die_error
(400, "Unknown blobdiff format");
5262 if ($format eq 'html') {
5263 print "<div class=\"page_body\">\n";
5265 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5268 print "</div>\n"; # class="page_body"
5272 while (my $line = <$fd>) {
5273 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5274 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5278 last if $line =~ m!^\+\+\+!;
5286 sub git_blobdiff_plain
{
5287 git_blobdiff
('plain');
5290 sub git_commitdiff
{
5291 my $format = shift || 'html';
5292 $hash ||= $hash_base || "HEAD";
5293 my %co = parse_commit
($hash)
5294 or die_error
(404, "Unknown commit object");
5296 # choose format for commitdiff for merge
5297 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5298 $hash_parent = '--cc';
5300 # we need to prepare $formats_nav before almost any parameter munging
5302 if ($format eq 'html') {
5304 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5307 if (defined $hash_parent &&
5308 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5309 # commitdiff with two commits given
5310 my $hash_parent_short = $hash_parent;
5311 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5312 $hash_parent_short = substr($hash_parent, 0, 7);
5316 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5317 if ($co{'parents'}[$i] eq $hash_parent) {
5318 $formats_nav .= ' parent ' . ($i+1);
5322 $formats_nav .= ': ' .
5323 $cgi->a({-href
=> href
(action
=>"commitdiff",
5324 hash
=>$hash_parent)},
5325 esc_html
($hash_parent_short)) .
5327 } elsif (!$co{'parent'}) {
5329 $formats_nav .= ' (initial)';
5330 } elsif (scalar @{$co{'parents'}} == 1) {
5331 # single parent commit
5334 $cgi->a({-href
=> href
(action
=>"commitdiff",
5335 hash
=>$co{'parent'})},
5336 esc_html
(substr($co{'parent'}, 0, 7))) .
5340 if ($hash_parent eq '--cc') {
5341 $formats_nav .= ' | ' .
5342 $cgi->a({-href
=> href
(action
=>"commitdiff",
5343 hash
=>$hash, hash_parent
=>'-c')},
5345 } else { # $hash_parent eq '-c'
5346 $formats_nav .= ' | ' .
5347 $cgi->a({-href
=> href
(action
=>"commitdiff",
5348 hash
=>$hash, hash_parent
=>'--cc')},
5354 $cgi->a({-href
=> href
(action
=>"commitdiff",
5356 esc_html
(substr($_, 0, 7)));
5357 } @{$co{'parents'}} ) .
5362 my $hash_parent_param = $hash_parent;
5363 if (!defined $hash_parent_param) {
5364 # --cc for multiple parents, --root for parentless
5365 $hash_parent_param =
5366 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5372 if ($format eq 'html') {
5373 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5374 "--no-commit-id", "--patch-with-raw", "--full-index",
5375 $hash_parent_param, $hash, "--"
5376 or die_error
(500, "Open git-diff-tree failed");
5378 while (my $line = <$fd>) {
5380 # empty line ends raw part of diff-tree output
5382 push @difftree, scalar parse_difftree_raw_line
($line);
5385 } elsif ($format eq 'plain') {
5386 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5387 '-p', $hash_parent_param, $hash, "--"
5388 or die_error
(500, "Open git-diff-tree failed");
5391 die_error
(400, "Unknown commitdiff format");
5394 # non-textual hash id's can be cached
5396 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5400 # write commit message
5401 if ($format eq 'html') {
5402 my $refs = git_get_references
();
5403 my $ref = format_ref_marker
($refs, $co{'id'});
5405 git_header_html
(undef, $expires);
5406 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5407 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5408 git_print_authorship
(\
%co);
5409 print "<div class=\"page_body\">\n";
5410 if (@{$co{'comment'}} > 1) {
5411 print "<div class=\"log\">\n";
5412 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5413 print "</div>\n"; # class="log"
5416 } elsif ($format eq 'plain') {
5417 my $refs = git_get_references
("tags");
5418 my $tagname = git_get_rev_name_tags
($hash);
5419 my $filename = basename
($project) . "-$hash.patch";
5422 -type
=> 'text/plain',
5423 -charset
=> 'utf-8',
5424 -expires
=> $expires,
5425 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5426 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5427 print "From: " . to_utf8
($co{'author'}) . "\n";
5428 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5429 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5431 print "X-Git-Tag: $tagname\n" if $tagname;
5432 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5434 foreach my $line (@{$co{'comment'}}) {
5435 print to_utf8
($line) . "\n";
5441 if ($format eq 'html') {
5442 my $use_parents = !defined $hash_parent ||
5443 $hash_parent eq '-c' || $hash_parent eq '--cc';
5444 git_difftree_body
(\
@difftree, $hash,
5445 $use_parents ? @{$co{'parents'}} : $hash_parent);
5448 git_patchset_body
($fd, \
@difftree, $hash,
5449 $use_parents ? @{$co{'parents'}} : $hash_parent);
5451 print "</div>\n"; # class="page_body"
5454 } elsif ($format eq 'plain') {
5458 or print "Reading git-diff-tree failed\n";
5462 sub git_commitdiff_plain
{
5463 git_commitdiff
('plain');
5467 if (!defined $hash_base) {
5468 $hash_base = git_get_head_hash
($project);
5470 if (!defined $page) {
5474 my %co = parse_commit
($hash_base)
5475 or die_error
(404, "Unknown commit object");
5477 my $refs = git_get_references
();
5478 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5480 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5481 $file_name, "--full-history")
5482 or die_error
(404, "No such file or directory on given branch");
5484 if (!defined $hash && defined $file_name) {
5485 # some commits could have deleted file in question,
5486 # and not have it in tree, but one of them has to have it
5487 for (my $i = 0; $i <= @commitlist; $i++) {
5488 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5489 last if defined $hash;
5492 if (defined $hash) {
5493 $ftype = git_get_type
($hash);
5495 if (!defined $ftype) {
5496 die_error
(500, "Unknown type of object");
5499 my $paging_nav = '';
5502 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5503 file_name
=>$file_name)},
5505 $paging_nav .= " ⋅ " .
5506 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5507 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5509 $paging_nav .= "first";
5510 $paging_nav .= " ⋅ prev";
5513 if ($#commitlist >= 100) {
5515 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5516 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5517 $paging_nav .= " ⋅ $next_link";
5519 $paging_nav .= " ⋅ next";
5523 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5524 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5525 git_print_page_path
($file_name, $ftype, $hash_base);
5527 git_history_body
(\
@commitlist, 0, 99,
5528 $refs, $hash_base, $ftype, $next_link);
5534 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5535 if (!defined $searchtext) {
5536 die_error
(400, "Text field is empty");
5538 if (!defined $hash) {
5539 $hash = git_get_head_hash
($project);
5541 my %co = parse_commit
($hash);
5543 die_error
(404, "Unknown commit object");
5545 if (!defined $page) {
5549 $searchtype ||= 'commit';
5550 if ($searchtype eq 'pickaxe') {
5551 # pickaxe may take all resources of your box and run for several minutes
5552 # with every query - so decide by yourself how public you make this feature
5553 gitweb_check_feature
('pickaxe')
5554 or die_error
(403, "Pickaxe is disabled");
5556 if ($searchtype eq 'grep') {
5557 gitweb_check_feature
('grep')
5558 or die_error
(403, "Grep is disabled");
5563 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5565 if ($searchtype eq 'commit') {
5566 $greptype = "--grep=";
5567 } elsif ($searchtype eq 'author') {
5568 $greptype = "--author=";
5569 } elsif ($searchtype eq 'committer') {
5570 $greptype = "--committer=";
5572 $greptype .= $searchtext;
5573 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5574 $greptype, '--regexp-ignore-case',
5575 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5577 my $paging_nav = '';
5580 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5581 searchtext
=>$searchtext,
5582 searchtype
=>$searchtype)},
5584 $paging_nav .= " ⋅ " .
5585 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5586 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5588 $paging_nav .= "first";
5589 $paging_nav .= " ⋅ prev";
5592 if ($#commitlist >= 100) {
5594 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5595 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5596 $paging_nav .= " ⋅ $next_link";
5598 $paging_nav .= " ⋅ next";
5601 if ($#commitlist >= 100) {
5604 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5605 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5606 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5609 if ($searchtype eq 'pickaxe') {
5610 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5611 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5613 print "<table class=\"pickaxe search\">\n";
5616 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5617 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5618 ($search_use_regexp ? '--pickaxe-regex' : ());
5621 while (my $line = <$fd>) {
5625 my %set = parse_difftree_raw_line
($line);
5626 if (defined $set{'commit'}) {
5627 # finish previous commit
5630 "<td class=\"link\">" .
5631 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5633 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5639 print "<tr class=\"dark\">\n";
5641 print "<tr class=\"light\">\n";
5644 %co = parse_commit
($set{'commit'});
5645 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5646 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5647 "<td><i>$author</i></td>\n" .
5649 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5650 -class => "list subject"},
5651 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5652 } elsif (defined $set{'to_id'}) {
5653 next if ($set{'to_id'} =~ m/^0{40}$/);
5655 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5656 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5658 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5664 # finish last commit (warning: repetition!)
5667 "<td class=\"link\">" .
5668 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5670 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5678 if ($searchtype eq 'grep') {
5679 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5680 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5682 print "<table class=\"grep_search\">\n";
5686 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5687 $search_use_regexp ? ('-E', '-i') : '-F',
5688 $searchtext, $co{'tree'};
5690 while (my $line = <$fd>) {
5692 my ($file, $lno, $ltext, $binary);
5693 last if ($matches++ > 1000);
5694 if ($line =~ /^Binary file (.+) matches$/) {
5698 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5700 if ($file ne $lastfile) {
5701 $lastfile and print "</td></tr>\n";
5703 print "<tr class=\"dark\">\n";
5705 print "<tr class=\"light\">\n";
5707 print "<td class=\"list\">".
5708 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5709 file_name
=>"$file"),
5710 -class => "list"}, esc_path
($file));
5711 print "</td><td>\n";
5715 print "<div class=\"binary\">Binary file</div>\n";
5717 $ltext = untabify
($ltext);
5718 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5719 $ltext = esc_html
($1, -nbsp
=>1);
5720 $ltext .= '<span class="match">';
5721 $ltext .= esc_html
($2, -nbsp
=>1);
5722 $ltext .= '</span>';
5723 $ltext .= esc_html
($3, -nbsp
=>1);
5725 $ltext = esc_html
($ltext, -nbsp
=>1);
5727 print "<div class=\"pre\">" .
5728 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5729 file_name
=>"$file").'#l'.$lno,
5730 -class => "linenr"}, sprintf('%4i', $lno))
5731 . ' ' . $ltext . "</div>\n";
5735 print "</td></tr>\n";
5736 if ($matches > 1000) {
5737 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5740 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5749 sub git_search_help
{
5751 git_print_page_nav
('','', $hash,$hash,$hash);
5753 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5754 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5755 the pattern entered is recognized as the POSIX extended
5756 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5759 <dt><b>commit</b></dt>
5760 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5762 my ($have_grep) = gitweb_check_feature
('grep');
5765 <dt><b>grep</b></dt>
5766 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5767 a different one) are searched for the given pattern. On large trees, this search can take
5768 a while and put some strain on the server, so please use it with some consideration. Note that
5769 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5770 case-sensitive.</dd>
5774 <dt><b>author</b></dt>
5775 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5776 <dt><b>committer</b></dt>
5777 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5779 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5780 if ($have_pickaxe) {
5782 <dt><b>pickaxe</b></dt>
5783 <dd>All commits that caused the string to appear or disappear from any file (changes that
5784 added, removed or "modified" the string) will be listed. This search can take a while and
5785 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5786 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5794 my $head = git_get_head_hash
($project);
5795 if (!defined $hash) {
5798 if (!defined $page) {
5801 my $refs = git_get_references
();
5803 my $commit_hash = $hash;
5804 if (defined $hash_parent) {
5805 $commit_hash = "$hash_parent..$hash";
5807 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5809 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5811 if ($#commitlist >= 100) {
5813 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5814 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5818 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5819 git_print_header_div
('summary', $project);
5821 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5826 ## ......................................................................
5827 ## feeds (RSS, Atom; OPML)
5830 my $format = shift || 'atom';
5831 my ($have_blame) = gitweb_check_feature
('blame');
5833 # Atom: http://www.atomenabled.org/developers/syndication/
5834 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5835 if ($format ne 'rss' && $format ne 'atom') {
5836 die_error
(400, "Unknown web feed format");
5839 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5840 my $head = $hash || 'HEAD';
5841 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5845 my $content_type = "application/$format+xml";
5846 if (defined $cgi->http('HTTP_ACCEPT') &&
5847 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5848 # browser (feed reader) prefers text/xml
5849 $content_type = 'text/xml';
5851 if (defined($commitlist[0])) {
5852 %latest_commit = %{$commitlist[0]};
5853 %latest_date = parse_date
($latest_commit{'author_epoch'});
5855 -type
=> $content_type,
5856 -charset
=> 'utf-8',
5857 -last_modified
=> $latest_date{'rfc2822'});
5860 -type
=> $content_type,
5861 -charset
=> 'utf-8');
5864 # Optimization: skip generating the body if client asks only
5865 # for Last-Modified date.
5866 return if ($cgi->request_method() eq 'HEAD');
5869 my $title = "$site_name - $project/$action";
5870 my $feed_type = 'log';
5871 if (defined $hash) {
5872 $title .= " - '$hash'";
5873 $feed_type = 'branch log';
5874 if (defined $file_name) {
5875 $title .= " :: $file_name";
5876 $feed_type = 'history';
5878 } elsif (defined $file_name) {
5879 $title .= " - $file_name";
5880 $feed_type = 'history';
5882 $title .= " $feed_type";
5883 my $descr = git_get_project_description
($project);
5884 if (defined $descr) {
5885 $descr = esc_html
($descr);
5887 $descr = "$project " .
5888 ($format eq 'rss' ? 'RSS' : 'Atom') .
5891 my $owner = git_get_project_owner
($project);
5892 $owner = esc_html
($owner);
5896 if (defined $file_name) {
5897 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5898 } elsif (defined $hash) {
5899 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5901 $alt_url = href
(-full
=>1, action
=>"summary");
5903 print qq
!<?xml version
="1.0" encoding
="utf-8"?>\n!;
5904 if ($format eq 'rss') {
5906 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5909 print "<title>$title</title>\n" .
5910 "<link>$alt_url</link>\n" .
5911 "<description>$descr</description>\n" .
5912 "<language>en</language>\n";
5913 } elsif ($format eq 'atom') {
5915 <feed xmlns="http://www.w3.org/2005/Atom">
5917 print "<title>$title</title>\n" .
5918 "<subtitle>$descr</subtitle>\n" .
5919 '<link rel="alternate" type="text/html" href="' .
5920 $alt_url . '" />' . "\n" .
5921 '<link rel="self" type="' . $content_type . '" href="' .
5922 $cgi->self_url() . '" />' . "\n" .
5923 "<id>" . href
(-full
=>1) . "</id>\n" .
5924 # use project owner for feed author
5925 "<author><name>$owner</name></author>\n";
5926 if (defined $favicon) {
5927 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5929 if (defined $logo_url) {
5930 # not twice as wide as tall: 72 x 27 pixels
5931 print "<logo>" . esc_url
($logo) . "</logo>\n";
5933 if (! %latest_date) {
5934 # dummy date to keep the feed valid until commits trickle in:
5935 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5937 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5942 for (my $i = 0; $i <= $#commitlist; $i++) {
5943 my %co = %{$commitlist[$i]};
5944 my $commit = $co{'id'};
5945 # we read 150, we always show 30 and the ones more recent than 48 hours
5946 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5949 my %cd = parse_date
($co{'author_epoch'});
5951 # get list of changed files
5952 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5953 $co{'parent'} || "--root",
5954 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5956 my @difftree = map { chomp; $_ } <$fd>;
5960 # print element (entry, item)
5961 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5962 if ($format eq 'rss') {
5964 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5965 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5966 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5967 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5968 "<link>$co_url</link>\n" .
5969 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5970 "<content:encoded>" .
5972 } elsif ($format eq 'atom') {
5974 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5975 "<updated>$cd{'iso-8601'}</updated>\n" .
5977 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5978 if ($co{'author_email'}) {
5979 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5981 print "</author>\n" .
5982 # use committer for contributor
5984 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5985 if ($co{'committer_email'}) {
5986 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5988 print "</contributor>\n" .
5989 "<published>$cd{'iso-8601'}</published>\n" .
5990 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5991 "<id>$co_url</id>\n" .
5992 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5993 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5995 my $comment = $co{'comment'};
5997 foreach my $line (@$comment) {
5998 $line = esc_html
($line);
6001 print "</pre><ul>\n";
6002 foreach my $difftree_line (@difftree) {
6003 my %difftree = parse_difftree_raw_line
($difftree_line);
6004 next if !$difftree{'from_id'};
6006 my $file = $difftree{'file'} || $difftree{'to_file'};
6010 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
6011 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
6012 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
6013 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
6014 -title
=> "diff"}, 'D');
6016 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
6017 file_name
=>$file, hash_base
=>$commit),
6018 -title
=> "blame"}, 'B');
6020 # if this is not a feed of a file history
6021 if (!defined $file_name || $file_name ne $file) {
6022 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
6023 file_name
=>$file, hash
=>$commit),
6024 -title
=> "history"}, 'H');
6026 $file = esc_path
($file);
6030 if ($format eq 'rss') {
6031 print "</ul>]]>\n" .
6032 "</content:encoded>\n" .
6034 } elsif ($format eq 'atom') {
6035 print "</ul>\n</div>\n" .
6042 if ($format eq 'rss') {
6043 print "</channel>\n</rss>\n";
6044 } elsif ($format eq 'atom') {
6058 my @list = git_get_projects_list
();
6060 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
6062 <?xml version="1.0" encoding="utf-8"?>
6063 <opml version="1.0">
6065 <title>$site_name OPML Export</title>
6068 <outline text="git RSS feeds">
6071 foreach my $pr (@list) {
6073 my $head = git_get_head_hash
($proj{'path'});
6074 if (!defined $head) {
6077 $git_dir = "$projectroot/$proj{'path'}";
6078 my %co = parse_commit
($head);
6083 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
6084 my $rss = "$my_url?p=$proj{'path'};a=rss";
6085 my $html = "$my_url?p=$proj{'path'};a=summary";
6086 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";