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;
73 # URI of GIT logo (72x27 size)
74 our $logo = "++GITWEB_LOGO++";
75 # URI of GIT favicon, assumed to be image/png type
76 our $favicon = "++GITWEB_FAVICON++";
78 # URI and label (title) of GIT logo link
79 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
80 #our $logo_label = "git documentation";
81 our $logo_url = "http://git.or.cz/";
82 our $logo_label = "git homepage";
84 # source of projects list
85 our $projects_list = "++GITWEB_LIST++";
87 # the width (in characters) of the projects list "Description" column
88 our $projects_list_description_width = 25;
90 # default order of projects list
91 # valid values are none, project, descr, owner, and age
92 our $default_projects_order = "project";
94 # show repository only if this file exists
95 # (only effective if this variable evaluates to true)
96 our $export_ok = "++GITWEB_EXPORT_OK++";
98 # only allow viewing of repositories also shown on the overview page
99 our $strict_export = "++GITWEB_STRICT_EXPORT++";
101 # list of git base URLs used for URL to where fetch project from,
102 # i.e. full URL is "$git_base_url/$project"
103 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
105 # default blob_plain mimetype and default charset for text/plain blob
106 our $default_blob_plain_mimetype = 'text/plain';
107 our $default_text_plain_charset = undef;
109 # file to use for guessing MIME types before trying /etc/mime.types
110 # (relative to the current git repository)
111 our $mimetypes_file = undef;
113 # assume this charset if line contains non-UTF-8 characters;
114 # it should be valid encoding (see Encoding::Supported(3pm) for list),
115 # for which encoding all byte sequences are valid, for example
116 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
117 # could be even 'utf-8' for the old behavior)
118 our $fallback_encoding = 'latin1';
120 # rename detection options for git-diff and git-diff-tree
121 # - default is '-M', with the cost proportional to
122 # (number of removed files) * (number of new files).
123 # - more costly is '-C' (which implies '-M'), with the cost proportional to
124 # (number of changed files + number of removed files) * (number of new files)
125 # - even more costly is '-C', '--find-copies-harder' with cost
126 # (number of files in the original tree) * (number of new files)
127 # - one might want to include '-B' option, e.g. '-B', '-M'
128 our @diff_opts = ('-M'); # taken from git_commit
130 # information about snapshot formats that gitweb is capable of serving
131 our %known_snapshot_formats = (
133 # 'display' => display name,
134 # 'type' => mime type,
135 # 'suffix' => filename suffix,
136 # 'format' => --format for git-archive,
137 # 'compressor' => [compressor command and arguments]
138 # (array reference, optional)}
141 'display' => 'tar.gz',
142 'type' => 'application/x-gzip',
143 'suffix' => '.tar.gz',
145 'compressor' => ['gzip']},
148 'display' => 'tar.bz2',
149 'type' => 'application/x-bzip2',
150 'suffix' => '.tar.bz2',
152 'compressor' => ['bzip2']},
156 'type' => 'application/x-zip',
161 # Aliases so we understand old gitweb.snapshot values in repository
163 our %known_snapshot_format_aliases = (
167 # backward compatibility: legacy gitweb config support
168 'x-gzip' => undef, 'gz' => undef,
169 'x-bzip2' => undef, 'bz2' => undef,
170 'x-zip' => undef, '' => undef,
173 # You define site-wide feature defaults here; override them with
174 # $GITWEB_CONFIG as necessary.
177 # 'sub' => feature-sub (subroutine),
178 # 'override' => allow-override (boolean),
179 # 'default' => [ default options...] (array reference)}
181 # if feature is overridable (it means that allow-override has true value),
182 # then feature-sub will be called with default options as parameters;
183 # return value of feature-sub indicates if to enable specified feature
185 # if there is no 'sub' key (no feature-sub), then feature cannot be
188 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
190 # Enable the 'blame' blob view, showing the last commit that modified
191 # each line in the file. This can be very CPU-intensive.
193 # To enable system wide have in $GITWEB_CONFIG
194 # $feature{'blame'}{'default'} = [1];
195 # To have project specific config enable override in $GITWEB_CONFIG
196 # $feature{'blame'}{'override'} = 1;
197 # and in project config gitweb.blame = 0|1;
199 'sub' => \&feature_blame,
203 # Enable the 'snapshot' link, providing a compressed archive of any
204 # tree. This can potentially generate high traffic if you have large
207 # Value is a list of formats defined in %known_snapshot_formats that
209 # To disable system wide have in $GITWEB_CONFIG
210 # $feature{'snapshot'}{'default'} = [];
211 # To have project specific config enable override in $GITWEB_CONFIG
212 # $feature{'snapshot'}{'override'} = 1;
213 # and in project config, a comma-separated list of formats or "none"
214 # to disable. Example: gitweb.snapshot = tbz2,zip;
216 'sub' => \&feature_snapshot,
218 'default' => ['tgz']},
220 # Enable text search, which will list the commits which match author,
221 # committer or commit text to a given string. Enabled by default.
222 # Project specific override is not supported.
227 # Enable grep search, which will list the files in currently selected
228 # tree containing the given string. Enabled by default. This can be
229 # potentially CPU-intensive, of course.
231 # To enable system wide have in $GITWEB_CONFIG
232 # $feature{'grep'}{'default'} = [1];
233 # To have project specific config enable override in $GITWEB_CONFIG
234 # $feature{'grep'}{'override'} = 1;
235 # and in project config gitweb.grep = 0|1;
240 # Enable the pickaxe search, which will list the commits that modified
241 # a given string in a file. This can be practical and quite faster
242 # alternative to 'blame', but still potentially CPU-intensive.
244 # To enable system wide have in $GITWEB_CONFIG
245 # $feature{'pickaxe'}{'default'} = [1];
246 # To have project specific config enable override in $GITWEB_CONFIG
247 # $feature{'pickaxe'}{'override'} = 1;
248 # and in project config gitweb.pickaxe = 0|1;
250 'sub' => \&feature_pickaxe,
254 # Make gitweb use an alternative format of the URLs which can be
255 # more readable and natural-looking: project name is embedded
256 # directly in the path and the query string contains other
257 # auxiliary information. All gitweb installations recognize
258 # URL in either format; this configures in which formats gitweb
261 # To enable system wide have in $GITWEB_CONFIG
262 # $feature{'pathinfo'}{'default'} = [1];
263 # Project specific override is not supported.
265 # Note that you will need to change the default location of CSS,
266 # favicon, logo and possibly other files to an absolute URL. Also,
267 # if gitweb.cgi serves as your indexfile, you will need to force
268 # $my_uri to contain the script name in your $GITWEB_CONFIG.
273 # Make gitweb consider projects in project root subdirectories
274 # to be forks of existing projects. Given project $projname.git,
275 # projects matching $projname/*.git will not be shown in the main
276 # projects list, instead a '+' mark will be added to $projname
277 # there and a 'forks' view will be enabled for the project, listing
278 # all the forks. If project list is taken from a file, forks have
279 # to be listed after the main project.
281 # To enable system wide have in $GITWEB_CONFIG
282 # $feature{'forks'}{'default'} = [1];
283 # Project specific override is not supported.
288 # Insert custom links to the action bar of all project pages.
289 # This enables you mainly to link to third-party scripts integrating
290 # into gitweb; e.g. git-browser for graphical history representation
291 # or custom web-based repository administration interface.
293 # The 'default' value consists of a list of triplets in the form
294 # (label, link, position) where position is the label after which
295 # to inster the link and link is a format string where %n expands
296 # to the project name, %f to the project path within the filesystem,
297 # %h to the current hash (h gitweb parameter) and %b to the current
298 # hash base (hb gitweb parameter).
300 # To enable system wide have in $GITWEB_CONFIG e.g.
301 # $feature{'actions'}{'default'} = [('graphiclog',
302 # '/git-browser/by-commit.html?r=%n', 'summary')];
303 # Project specific override is not supported.
308 # Allow gitweb scan project content tags described in ctags/
309 # of project repository, and display the popular Web 2.0-ish
310 # "tag cloud" near the project list. Note that this is something
311 # COMPLETELY different from the normal Git tags.
313 # gitweb by itself can show existing tags, but it does not handle
314 # tagging itself; you need an external application for that.
315 # For an example script, check Girocco's cgi/tagproj.cgi.
316 # You may want to install the HTML::TagCloud Perl module to get
317 # a pretty tag cloud instead of just a list of tags.
319 # To enable system wide have in $GITWEB_CONFIG
320 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
321 # Project specific override is not supported.
327 sub gitweb_check_feature {
329 return unless exists $feature{$name};
330 my ($sub, $override, @defaults) = (
331 $feature{$name}{'sub'},
332 $feature{$name}{'override'},
333 @{$feature{$name}{'default'}});
334 if (!$override) { return @defaults; }
336 warn "feature $name is not overrideable";
339 return $sub->(@defaults);
343 my ($val) = git_get_project_config('blame', '--bool');
345 if ($val eq 'true') {
347 } elsif ($val eq 'false') {
354 sub feature_snapshot {
357 my ($val) = git_get_project_config('snapshot');
360 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
367 my ($val) = git_get_project_config('grep', '--bool');
369 if ($val eq 'true') {
371 } elsif ($val eq 'false') {
378 sub feature_pickaxe {
379 my ($val) = git_get_project_config('pickaxe', '--bool');
381 if ($val eq 'true') {
383 } elsif ($val eq 'false') {
390 # checking HEAD file with -e is fragile if the repository was
391 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
393 sub check_head_link {
395 my $headfile = "$dir/HEAD";
396 return ((-e $headfile) ||
397 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
400 sub check_export_ok {
402 return (check_head_link($dir) &&
403 (!$export_ok || -e "$dir/$export_ok"));
406 # process alternate names for backward compatibility
407 # filter out unsupported (unknown) snapshot formats
408 sub filter_snapshot_fmts {
412 exists $known_snapshot_format_aliases{$_} ?
413 $known_snapshot_format_aliases{$_} : $_} @fmts;
414 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
418 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
419 if (-e $GITWEB_CONFIG) {
422 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
423 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
426 # version of the core git binary
427 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
429 $projects_list ||= $projectroot;
431 # ======================================================================
432 # input validation and dispatch
434 # input parameters can be collected from a variety of sources (presently, CGI
435 # and PATH_INFO), so we define an %input_params hash that collects them all
436 # together during validation: this allows subsequent uses (e.g. href()) to be
437 # agnostic of the parameter origin
439 my %input_params = ();
441 # input parameters are stored with the long parameter name as key. This will
442 # also be used in the href subroutine to convert parameters to their CGI
443 # equivalent, and since the href() usage is the most frequent one, we store
444 # the name -> CGI key mapping here, instead of the reverse.
446 # XXX: Warning: If you touch this, check the search form for updating,
449 my @cgi_param_mapping = (
457 hash_parent_base => "hpb",
462 snapshot_format => "sf",
463 extra_options => "opt",
464 search_use_regexp => "sr",
466 my %cgi_param_mapping = @cgi_param_mapping;
468 # we will also need to know the possible actions, for validation
470 "blame" => \&git_blame,
471 "blobdiff" => \&git_blobdiff,
472 "blobdiff_plain" => \&git_blobdiff_plain,
473 "blob" => \&git_blob,
474 "blob_plain" => \&git_blob_plain,
475 "commitdiff" => \&git_commitdiff,
476 "commitdiff_plain" => \&git_commitdiff_plain,
477 "commit" => \&git_commit,
478 "forks" => \&git_forks,
479 "heads" => \&git_heads,
480 "history" => \&git_history,
483 "atom" => \&git_atom,
484 "search" => \&git_search,
485 "search_help" => \&git_search_help,
486 "shortlog" => \&git_shortlog,
487 "summary" => \&git_summary,
489 "tags" => \&git_tags,
490 "tree" => \&git_tree,
491 "snapshot" => \&git_snapshot,
492 "object" => \&git_object,
493 # those below don't need $project
494 "opml" => \&git_opml,
495 "project_list" => \&git_project_list,
496 "project_index" => \&git_project_index,
499 # finally, we have the hash of allowed extra_options for the commands that
501 my %allowed_options = (
502 "--no-merges" => [ qw(rss atom log shortlog history) ],
505 # fill %input_params with the CGI parameters. All values except for 'opt'
506 # should be single values, but opt can be an array. We should probably
507 # build an array of parameters that can be multi-valued, but since for the time
508 # being it's only this one, we just single it out
509 while (my ($name, $symbol) = each %cgi_param_mapping) {
510 if ($symbol eq 'opt') {
511 $input_params{$name} = [ $cgi->param($symbol) ];
513 $input_params{$name} = $cgi->param($symbol);
517 # now read PATH_INFO and update the parameter list for missing parameters
518 sub evaluate_path_info {
519 return if defined $input_params{'project'};
520 return if !$path_info;
521 $path_info =~ s,^/+,,;
522 return if !$path_info;
524 # find which part of PATH_INFO is project
525 my $project = $path_info;
527 while ($project && !check_head_link("$projectroot/$project")) {
528 $project =~ s,/*[^/]*$,,;
530 return unless $project;
531 $input_params{'project'} = $project;
533 # do not change any parameters if an action is given using the query string
534 return if $input_params{'action'};
535 $path_info =~ s,^\Q$project\E/*,,;
537 # next, check if we have an action
538 my $action = $path_info;
540 if (exists $actions{$action}) {
541 $path_info =~ s,^$action/*,,;
542 $input_params{'action'} = $action;
545 # list of actions that want hash_base instead of hash, but can have no
546 # pathname (f) parameter
553 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
554 my ($parentrefname, $parentpathname, $refname, $pathname) =
555 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
557 # first, analyze the 'current' part
558 if (defined $pathname) {
559 # we got "branch:filename" or "branch:dir/"
560 # we could use git_get_type(branch:pathname), but:
561 # - it needs $git_dir
562 # - it does a git() call
563 # - the convention of terminating directories with a slash
564 # makes it superfluous
565 # - embedding the action in the PATH_INFO would make it even
567 $pathname =~ s,^/+,,;
568 if (!$pathname || substr($pathname, -1) eq "/") {
569 $input_params{'action'} ||= "tree";
572 # the default action depends on whether we had parent info
574 if ($parentrefname) {
575 $input_params{'action'} ||= "blobdiff_plain";
577 $input_params{'action'} ||= "blob_plain";
580 $input_params{'hash_base'} ||= $refname;
581 $input_params{'file_name'} ||= $pathname;
582 } elsif (defined $refname) {
583 # we got "branch". In this case we have to choose if we have to
584 # set hash or hash_base.
586 # Most of the actions without a pathname only want hash to be
587 # set, except for the ones specified in @wants_base that want
588 # hash_base instead. It should also be noted that hand-crafted
589 # links having 'history' as an action and no pathname or hash
590 # set will fail, but that happens regardless of PATH_INFO.
591 $input_params{'action'} ||= "shortlog";
592 if (grep { $_ eq $input_params{'action'} } @wants_base) {
593 $input_params{'hash_base'} ||= $refname;
595 $input_params{'hash'} ||= $refname;
599 # next, handle the 'parent' part, if present
600 if (defined $parentrefname) {
601 # a missing pathspec defaults to the 'current' filename, allowing e.g.
602 # someproject/blobdiff/oldrev..newrev:/filename
603 if ($parentpathname) {
604 $parentpathname =~ s,^/+,,;
605 $parentpathname =~ s,/$,,;
606 $input_params{'file_parent'} ||= $parentpathname;
608 $input_params{'file_parent'} ||= $input_params{'file_name'};
610 # we assume that hash_parent_base is wanted if a path was specified,
611 # or if the action wants hash_base instead of hash
612 if (defined $input_params{'file_parent'} ||
613 grep { $_ eq $input_params{'action'} } @wants_base) {
614 $input_params{'hash_parent_base'} ||= $parentrefname;
616 $input_params{'hash_parent'} ||= $parentrefname;
620 evaluate_path_info();
622 our $action = $input_params{'action'};
623 if (defined $action) {
624 if (!validate_action($action)) {
625 die_error(400, "Invalid action parameter");
629 # parameters which are pathnames
630 our $project = $input_params{'project'};
631 if (defined $project) {
632 if (!validate_project($project)) {
634 die_error(404, "No such project");
638 our $file_name = $input_params{'file_name'};
639 if (defined $file_name) {
640 if (!validate_pathname($file_name)) {
641 die_error(400, "Invalid file parameter");
645 our $file_parent = $input_params{'file_parent'};
646 if (defined $file_parent) {
647 if (!validate_pathname($file_parent)) {
648 die_error(400, "Invalid file parent parameter");
652 # parameters which are refnames
653 our $hash = $input_params{'hash'};
655 if (!validate_refname($hash)) {
656 die_error(400, "Invalid hash parameter");
660 our $hash_parent = $input_params{'hash_parent'};
661 if (defined $hash_parent) {
662 if (!validate_refname($hash_parent)) {
663 die_error(400, "Invalid hash parent parameter");
667 our $hash_base = $input_params{'hash_base'};
668 if (defined $hash_base) {
669 if (!validate_refname($hash_base)) {
670 die_error(400, "Invalid hash base parameter");
674 our @extra_options = @{$input_params{'extra_options'}};
675 # @extra_options is always defined, since it can only be (currently) set from
676 # CGI, and $cgi->param() returns the empty array in array context if the param
678 foreach my $opt (@extra_options) {
679 if (not exists $allowed_options{$opt}) {
680 die_error(400, "Invalid option parameter");
682 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
683 die_error(400, "Invalid option parameter for this action");
687 our $hash_parent_base = $input_params{'hash_parent_base'};
688 if (defined $hash_parent_base) {
689 if (!validate_refname($hash_parent_base)) {
690 die_error(400, "Invalid hash parent base parameter");
695 our $page = $input_params{'page'};
697 if ($page =~ m/[^0-9]/) {
698 die_error(400, "Invalid page parameter");
702 our $searchtype = $input_params{'searchtype'};
703 if (defined $searchtype) {
704 if ($searchtype =~ m/[^a-z]/) {
705 die_error(400, "Invalid searchtype parameter");
709 our $search_use_regexp = $input_params{'search_use_regexp'};
711 our $searchtext = $input_params{'searchtext'};
713 if (defined $searchtext) {
714 if (length($searchtext) < 2) {
715 die_error(403, "At least two characters are required for search parameter");
717 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
720 # path to the current git repository
722 $git_dir = "$projectroot/$project" if $project;
725 if (!defined $action) {
727 $action = git_get_type($hash);
728 } elsif (defined $hash_base && defined $file_name) {
729 $action = git_get_type("$hash_base:$file_name");
730 } elsif (defined $project) {
733 $action = 'project_list';
736 if (!defined($actions{$action})) {
737 die_error(400, "Unknown action");
739 if ($action !~ m/^(opml|project_list|project_index)$/ &&
741 die_error(400, "Project needed");
743 $actions{$action}->();
746 ## ======================================================================
751 # default is to use -absolute url() i.e. $my_uri
752 my $href = $params{-full} ? $my_url : $my_uri;
754 $params{'project'} = $project unless exists $params{'project'};
756 if ($params{-replay}) {
757 while (my ($name, $symbol) = each %cgi_param_mapping) {
758 if (!exists $params{$name}) {
759 $params{$name} = $input_params{$name};
764 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
766 # try to put as many parameters as possible in PATH_INFO:
769 # - hash_parent or hash_parent_base:/file_parent
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 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
790 # stripping nonexistent or useless pieces
791 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
792 || $params{'hash_parent'} || $params{'hash'});
793 if (defined $params{'hash_base'}) {
794 if (defined $params{'hash_parent_base'}) {
795 $href .= esc_url($params{'hash_parent_base'});
796 # skip the file_parent if it's the same as the file_name
797 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
798 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
799 $href .= ":/".esc_url($params{'file_parent'});
800 delete $params{'file_parent'};
803 delete $params{'hash_parent'};
804 delete $params{'hash_parent_base'};
805 } elsif (defined $params{'hash_parent'}) {
806 $href .= esc_url($params{'hash_parent'}). "..";
807 delete $params{'hash_parent'};
810 $href .= esc_url($params{'hash_base'});
811 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
812 $href .= ":/".esc_url($params{'file_name'});
813 delete $params{'file_name'};
815 delete $params{'hash'};
816 delete $params{'hash_base'};
817 } elsif (defined $params{'hash'}) {
818 $href .= esc_url($params{'hash'});
819 delete $params{'hash'};
823 # now encode the parameters explicitly
825 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
826 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
827 if (defined $params{$name}) {
828 if (ref($params{$name}) eq "ARRAY") {
829 foreach my $par (@{$params{$name}}) {
830 push @result, $symbol . "=" . esc_param($par);
833 push @result, $symbol . "=" . esc_param($params{$name});
837 $href .= "?" . join(';', @result) if scalar @result;
843 ## ======================================================================
844 ## validation, quoting/unquoting and escaping
846 sub validate_action {
847 my $input = shift || return undef;
848 return undef unless exists $actions{$input};
852 sub validate_project {
853 my $input = shift || return undef;
854 if (!validate_pathname($input) ||
855 !(-d "$projectroot/$input") ||
856 !check_export_ok("$projectroot/$input") ||
857 ($strict_export && !project_in_list($input))) {
864 sub validate_pathname {
865 my $input = shift || return undef;
867 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
868 # at the beginning, at the end, and between slashes.
869 # also this catches doubled slashes
870 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
874 if ($input =~ m!\0!) {
880 sub validate_refname {
881 my $input = shift || return undef;
883 # textual hashes are O.K.
884 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
887 # it must be correct pathname
888 $input = validate_pathname($input)
890 # restrictions on ref name according to git-check-ref-format
891 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
897 # decode sequences of octets in utf8 into Perl's internal form,
898 # which is utf-8 with utf8 flag set if needed. gitweb writes out
899 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
902 if (utf8::valid($str)) {
906 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
910 # quote unsafe chars, but keep the slash, even when it's not
911 # correct, but quoted slashes look too horrible in bookmarks
914 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
920 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
923 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
929 # replace invalid utf8 character with SUBSTITUTION sequence
934 $str = to_utf8($str);
935 $str = $cgi->escapeHTML($str);
936 if ($opts{'-nbsp'}) {
937 $str =~ s/ / /g;
939 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
943 # quote control characters and escape filename to HTML
948 $str = to_utf8($str);
949 $str = $cgi->escapeHTML($str);
950 if ($opts{'-nbsp'}) {
951 $str =~ s/ / /g;
953 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
957 # Make control characters "printable", using character escape codes (CEC)
961 my %es = ( # character escape codes, aka escape sequences
962 "\t" => '\t', # tab (HT)
963 "\n" => '\n', # line feed (LF)
964 "\r" => '\r', # carrige return (CR)
965 "\f" => '\f', # form feed (FF)
966 "\b" => '\b', # backspace (BS)
967 "\a" => '\a', # alarm (bell) (BEL)
968 "\e" => '\e', # escape (ESC)
969 "\013" => '\v', # vertical tab (VT)
970 "\000" => '\0', # nul character (NUL)
972 my $chr = ( (exists $es{$cntrl})
974 : sprintf('\%2x', ord($cntrl)) );
975 if ($opts{-nohtml}) {
978 return "<span class=\"cntrl\">$chr</span>";
982 # Alternatively use unicode control pictures codepoints,
983 # Unicode "printable representation" (PR)
988 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
989 if ($opts{-nohtml}) {
992 return "<span class=\"cntrl\">$chr</span>";
996 # git may return quoted and escaped filenames
1002 my %es = ( # character escape codes, aka escape sequences
1003 't' => "\t", # tab (HT, TAB)
1004 'n' => "\n", # newline (NL)
1005 'r' => "\r", # return (CR)
1006 'f' => "\f", # form feed (FF)
1007 'b' => "\b", # backspace (BS)
1008 'a' => "\a", # alarm (bell) (BEL)
1009 'e' => "\e", # escape (ESC)
1010 'v' => "\013", # vertical tab (VT)
1013 if ($seq =~ m/^[0-7]{1,3}$/) {
1014 # octal char sequence
1015 return chr(oct($seq));
1016 } elsif (exists $es{$seq}) {
1017 # C escape sequence, aka character escape code
1020 # quoted ordinary character
1024 if ($str =~ m/^"(.*)"$/) {
1027 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1032 # escape tabs (convert tabs to spaces)
1036 while ((my $pos = index($line, "\t")) != -1) {
1037 if (my $count = (8 - ($pos % 8))) {
1038 my $spaces = ' ' x $count;
1039 $line =~ s/\t/$spaces/;
1046 sub project_in_list {
1047 my $project = shift;
1048 my @list = git_get_projects_list();
1049 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1052 ## ----------------------------------------------------------------------
1053 ## HTML aware string manipulation
1055 # Try to chop given string on a word boundary between position
1056 # $len and $len+$add_len. If there is no word boundary there,
1057 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1058 # (marking chopped part) would be longer than given string.
1062 my $add_len = shift || 10;
1063 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1065 # Make sure perl knows it is utf8 encoded so we don't
1066 # cut in the middle of a utf8 multibyte char.
1067 $str = to_utf8($str);
1069 # allow only $len chars, but don't cut a word if it would fit in $add_len
1070 # if it doesn't fit, cut it if it's still longer than the dots we would add
1071 # remove chopped character entities entirely
1073 # when chopping in the middle, distribute $len into left and right part
1074 # return early if chopping wouldn't make string shorter
1075 if ($where eq 'center') {
1076 return $str if ($len + 5 >= length($str)); # filler is length 5
1079 return $str if ($len + 4 >= length($str)); # filler is length 4
1082 # regexps: ending and beginning with word part up to $add_len
1083 my $endre = qr/.{$len}\w{0,$add_len}/;
1084 my $begre = qr/\w{0,$add_len}.{$len}/;
1086 if ($where eq 'left') {
1087 $str =~ m/^(.*?)($begre)$/;
1088 my ($lead, $body) = ($1, $2);
1089 if (length($lead) > 4) {
1090 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1093 return "$lead$body";
1095 } elsif ($where eq 'center') {
1096 $str =~ m/^($endre)(.*)$/;
1097 my ($left, $str) = ($1, $2);
1098 $str =~ m/^(.*?)($begre)$/;
1099 my ($mid, $right) = ($1, $2);
1100 if (length($mid) > 5) {
1101 $left =~ s/&[^;]*$//;
1102 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1105 return "$left$mid$right";
1108 $str =~ m/^($endre)(.*)$/;
1111 if (length($tail) > 4) {
1112 $body =~ s/&[^;]*$//;
1115 return "$body$tail";
1119 # takes the same arguments as chop_str, but also wraps a <span> around the
1120 # result with a title attribute if it does get chopped. Additionally, the
1121 # string is HTML-escaped.
1122 sub chop_and_escape_str {
1125 my $chopped = chop_str(@_);
1126 if ($chopped eq $str) {
1127 return esc_html($chopped);
1129 $str =~ s/([[:cntrl:]])/?/g;
1130 return $cgi->span({-title=>$str}, esc_html($chopped));
1134 ## ----------------------------------------------------------------------
1135 ## functions returning short strings
1137 # CSS class for given age value (in seconds)
1141 if (!defined $age) {
1143 } elsif ($age < 60*60*2) {
1145 } elsif ($age < 60*60*24*2) {
1152 # convert age in seconds to "nn units ago" string
1157 if ($age > 60*60*24*365*2) {
1158 $age_str = (int $age/60/60/24/365);
1159 $age_str .= " years ago";
1160 } elsif ($age > 60*60*24*(365/12)*2) {
1161 $age_str = int $age/60/60/24/(365/12);
1162 $age_str .= " months ago";
1163 } elsif ($age > 60*60*24*7*2) {
1164 $age_str = int $age/60/60/24/7;
1165 $age_str .= " weeks ago";
1166 } elsif ($age > 60*60*24*2) {
1167 $age_str = int $age/60/60/24;
1168 $age_str .= " days ago";
1169 } elsif ($age > 60*60*2) {
1170 $age_str = int $age/60/60;
1171 $age_str .= " hours ago";
1172 } elsif ($age > 60*2) {
1173 $age_str = int $age/60;
1174 $age_str .= " min ago";
1175 } elsif ($age > 2) {
1176 $age_str = int $age;
1177 $age_str .= " sec ago";
1179 $age_str .= " right now";
1185 S_IFINVALID => 0030000,
1186 S_IFGITLINK => 0160000,
1189 # submodule/subproject, a commit object reference
1190 sub S_ISGITLINK($) {
1193 return (($mode & S_IFMT) == S_IFGITLINK)
1196 # convert file mode in octal to symbolic file mode string
1198 my $mode = oct shift;
1200 if (S_ISGITLINK($mode)) {
1201 return 'm---------';
1202 } elsif (S_ISDIR($mode & S_IFMT)) {
1203 return 'drwxr-xr-x';
1204 } elsif (S_ISLNK($mode)) {
1205 return 'lrwxrwxrwx';
1206 } elsif (S_ISREG($mode)) {
1207 # git cares only about the executable bit
1208 if ($mode & S_IXUSR) {
1209 return '-rwxr-xr-x';
1211 return '-rw-r--r--';
1214 return '----------';
1218 # convert file mode in octal to file type string
1222 if ($mode !~ m/^[0-7]+$/) {
1228 if (S_ISGITLINK($mode)) {
1230 } elsif (S_ISDIR($mode & S_IFMT)) {
1232 } elsif (S_ISLNK($mode)) {
1234 } elsif (S_ISREG($mode)) {
1241 # convert file mode in octal to file type description string
1242 sub file_type_long {
1245 if ($mode !~ m/^[0-7]+$/) {
1251 if (S_ISGITLINK($mode)) {
1253 } elsif (S_ISDIR($mode & S_IFMT)) {
1255 } elsif (S_ISLNK($mode)) {
1257 } elsif (S_ISREG($mode)) {
1258 if ($mode & S_IXUSR) {
1259 return "executable";
1269 ## ----------------------------------------------------------------------
1270 ## functions returning short HTML fragments, or transforming HTML fragments
1271 ## which don't belong to other sections
1273 # format line of commit message.
1274 sub format_log_line_html {
1277 $line = esc_html($line, -nbsp=>1);
1278 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1281 $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1282 -class => "text"}, $hash_text);
1283 $line =~ s/$hash_text/$link/;
1288 # format marker of refs pointing to given object
1290 # the destination action is chosen based on object type and current context:
1291 # - for annotated tags, we choose the tag view unless it's the current view
1292 # already, in which case we go to shortlog view
1293 # - for other refs, we keep the current view if we're in history, shortlog or
1294 # log view, and select shortlog otherwise
1295 sub format_ref_marker {
1296 my ($refs, $id) = @_;
1299 if (defined $refs->{$id}) {
1300 foreach my $ref (@{$refs->{$id}}) {
1301 # this code exploits the fact that non-lightweight tags are the
1302 # only indirect objects, and that they are the only objects for which
1303 # we want to use tag instead of shortlog as action
1304 my ($type, $name) = qw();
1305 my $indirect = ($ref =~ s/\^\{\}$//);
1306 # e.g. tags/v2.6.11 or heads/next
1307 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1316 $class .= " indirect" if $indirect;
1318 my $dest_action = "shortlog";
1321 $dest_action = "tag" unless $action eq "tag";
1322 } elsif ($action =~ /^(history|(short)?log)$/) {
1323 $dest_action = $action;
1327 $dest .= "refs/" unless $ref =~ m!^refs/!;
1330 my $link = $cgi->a({
1332 action=>$dest_action,
1336 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1342 return ' <span class="refs">'. $markers . '</span>';
1348 # format, perhaps shortened and with markers, title line
1349 sub format_subject_html {
1350 my ($long, $short, $href, $extra) = @_;
1351 $extra = '' unless defined($extra);
1353 if (length($short) < length($long)) {
1354 return $cgi->a({-href => $href, -class => "list subject",
1355 -title => to_utf8($long)},
1356 esc_html($short) . $extra);
1358 return $cgi->a({-href => $href, -class => "list subject"},
1359 esc_html($long) . $extra);
1363 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1364 sub format_git_diff_header_line {
1366 my $diffinfo = shift;
1367 my ($from, $to) = @_;
1369 if ($diffinfo->{'nparents'}) {
1371 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1372 if ($to->{'href'}) {
1373 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1374 esc_path($to->{'file'}));
1375 } else { # file was deleted (no href)
1376 $line .= esc_path($to->{'file'});
1380 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1381 if ($from->{'href'}) {
1382 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1383 'a/' . esc_path($from->{'file'}));
1384 } else { # file was added (no href)
1385 $line .= 'a/' . esc_path($from->{'file'});
1388 if ($to->{'href'}) {
1389 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1390 'b/' . esc_path($to->{'file'}));
1391 } else { # file was deleted
1392 $line .= 'b/' . esc_path($to->{'file'});
1396 return "<div class=\"diff header\">$line</div>\n";
1399 # format extended diff header line, before patch itself
1400 sub format_extended_diff_header_line {
1402 my $diffinfo = shift;
1403 my ($from, $to) = @_;
1406 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1407 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1408 esc_path($from->{'file'}));
1410 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1411 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1412 esc_path($to->{'file'}));
1414 # match single <mode>
1415 if ($line =~ m/\s(\d{6})$/) {
1416 $line .= '<span class="info"> (' .
1417 file_type_long($1) .
1421 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1422 # can match only for combined diff
1424 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1425 if ($from->{'href'}[$i]) {
1426 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1428 substr($diffinfo->{'from_id'}[$i],0,7));
1433 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1436 if ($to->{'href'}) {
1437 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1438 substr($diffinfo->{'to_id'},0,7));
1443 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1444 # can match only for ordinary diff
1445 my ($from_link, $to_link);
1446 if ($from->{'href'}) {
1447 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1448 substr($diffinfo->{'from_id'},0,7));
1450 $from_link = '0' x 7;
1452 if ($to->{'href'}) {
1453 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1454 substr($diffinfo->{'to_id'},0,7));
1458 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1459 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1462 return $line . "<br/>\n";
1465 # format from-file/to-file diff header
1466 sub format_diff_from_to_header {
1467 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1472 #assert($line =~ m/^---/) if DEBUG;
1473 # no extra formatting for "^--- /dev/null"
1474 if (! $diffinfo->{'nparents'}) {
1475 # ordinary (single parent) diff
1476 if ($line =~ m!^--- "?a/!) {
1477 if ($from->{'href'}) {
1479 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1480 esc_path($from->{'file'}));
1483 esc_path($from->{'file'});
1486 $result .= qq!<div class="diff from_file">$line</div>\n!;
1489 # combined diff (merge commit)
1490 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1491 if ($from->{'href'}[$i]) {
1493 $cgi->a({-href=>href(action=>"blobdiff",
1494 hash_parent=>$diffinfo->{'from_id'}[$i],
1495 hash_parent_base=>$parents[$i],
1496 file_parent=>$from->{'file'}[$i],
1497 hash=>$diffinfo->{'to_id'},
1499 file_name=>$to->{'file'}),
1501 -title=>"diff" . ($i+1)},
1504 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1505 esc_path($from->{'file'}[$i]));
1507 $line = '--- /dev/null';
1509 $result .= qq!<div class="diff from_file">$line</div>\n!;
1514 #assert($line =~ m/^\+\+\+/) if DEBUG;
1515 # no extra formatting for "^+++ /dev/null"
1516 if ($line =~ m!^\+\+\+ "?b/!) {
1517 if ($to->{'href'}) {
1519 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1520 esc_path($to->{'file'}));
1523 esc_path($to->{'file'});
1526 $result .= qq!<div class="diff to_file">$line</div>\n!;
1531 # create note for patch simplified by combined diff
1532 sub format_diff_cc_simplified {
1533 my ($diffinfo, @parents) = @_;
1536 $result .= "<div class=\"diff header\">" .
1538 if (!is_deleted($diffinfo)) {
1539 $result .= $cgi->a({-href => href(action=>"blob",
1541 hash=>$diffinfo->{'to_id'},
1542 file_name=>$diffinfo->{'to_file'}),
1544 esc_path($diffinfo->{'to_file'}));
1546 $result .= esc_path($diffinfo->{'to_file'});
1548 $result .= "</div>\n" . # class="diff header"
1549 "<div class=\"diff nodifferences\">" .
1551 "</div>\n"; # class="diff nodifferences"
1556 # format patch (diff) line (not to be used for diff headers)
1557 sub format_diff_line {
1559 my ($from, $to) = @_;
1560 my $diff_class = "";
1564 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1566 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1567 if ($line =~ m/^\@{3}/) {
1568 $diff_class = " chunk_header";
1569 } elsif ($line =~ m/^\\/) {
1570 $diff_class = " incomplete";
1571 } elsif ($prefix =~ tr/+/+/) {
1572 $diff_class = " add";
1573 } elsif ($prefix =~ tr/-/-/) {
1574 $diff_class = " rem";
1577 # assume ordinary diff
1578 my $char = substr($line, 0, 1);
1580 $diff_class = " add";
1581 } elsif ($char eq '-') {
1582 $diff_class = " rem";
1583 } elsif ($char eq '@') {
1584 $diff_class = " chunk_header";
1585 } elsif ($char eq "\\") {
1586 $diff_class = " incomplete";
1589 $line = untabify($line);
1590 if ($from && $to && $line =~ m/^\@{2} /) {
1591 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1592 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1594 $from_lines = 0 unless defined $from_lines;
1595 $to_lines = 0 unless defined $to_lines;
1597 if ($from->{'href'}) {
1598 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1599 -class=>"list"}, $from_text);
1601 if ($to->{'href'}) {
1602 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1603 -class=>"list"}, $to_text);
1605 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1606 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1607 return "<div class=\"diff$diff_class\">$line</div>\n";
1608 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1609 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1610 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1612 @from_text = split(' ', $ranges);
1613 for (my $i = 0; $i < @from_text; ++$i) {
1614 ($from_start[$i], $from_nlines[$i]) =
1615 (split(',', substr($from_text[$i], 1)), 0);
1618 $to_text = pop @from_text;
1619 $to_start = pop @from_start;
1620 $to_nlines = pop @from_nlines;
1622 $line = "<span class=\"chunk_info\">$prefix ";
1623 for (my $i = 0; $i < @from_text; ++$i) {
1624 if ($from->{'href'}[$i]) {
1625 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1626 -class=>"list"}, $from_text[$i]);
1628 $line .= $from_text[$i];
1632 if ($to->{'href'}) {
1633 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1634 -class=>"list"}, $to_text);
1638 $line .= " $prefix</span>" .
1639 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1640 return "<div class=\"diff$diff_class\">$line</div>\n";
1642 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1645 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1646 # linked. Pass the hash of the tree/commit to snapshot.
1647 sub format_snapshot_links {
1649 my @snapshot_fmts = gitweb_check_feature('snapshot');
1650 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1651 my $num_fmts = @snapshot_fmts;
1652 if ($num_fmts > 1) {
1653 # A parenthesized list of links bearing format names.
1654 # e.g. "snapshot (_tar.gz_ _zip_)"
1655 return "snapshot (" . join(' ', map
1662 }, $known_snapshot_formats{$_}{'display'})
1663 , @snapshot_fmts) . ")";
1664 } elsif ($num_fmts == 1) {
1665 # A single "snapshot" link whose tooltip bears the format name.
1667 my ($fmt) = @snapshot_fmts;
1673 snapshot_format=>$fmt
1675 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1677 } else { # $num_fmts == 0
1682 ## ......................................................................
1683 ## functions returning values to be passed, perhaps after some
1684 ## transformation, to other functions; e.g. returning arguments to href()
1686 # returns hash to be passed to href to generate gitweb URL
1687 # in -title key it returns description of link
1689 my $format = shift || 'Atom';
1690 my %res = (action => lc($format));
1692 # feed links are possible only for project views
1693 return unless (defined $project);
1694 # some views should link to OPML, or to generic project feed,
1695 # or don't have specific feed yet (so they should use generic)
1696 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1699 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1700 # from tag links; this also makes possible to detect branch links
1701 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1702 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1705 # find log type for feed description (title)
1707 if (defined $file_name) {
1708 $type = "history of $file_name";
1709 $type .= "/" if ($action eq 'tree');
1710 $type .= " on '$branch'" if (defined $branch);
1712 $type = "log of $branch" if (defined $branch);
1715 $res{-title} = $type;
1716 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1717 $res{'file_name'} = $file_name;
1722 ## ----------------------------------------------------------------------
1723 ## git utility subroutines, invoking git commands
1725 # returns path to the core git executable and the --git-dir parameter as list
1727 return $GIT, '--git-dir='.$git_dir;
1730 # quote the given arguments for passing them to the shell
1731 # quote_command("command", "arg 1", "arg with ' and ! characters")
1732 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1733 # Try to avoid using this function wherever possible.
1736 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1739 # get HEAD ref of given project as hash
1740 sub git_get_head_hash {
1741 my $project = shift;
1742 my $o_git_dir = $git_dir;
1744 $git_dir = "$projectroot/$project";
1745 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1748 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1752 if (defined $o_git_dir) {
1753 $git_dir = $o_git_dir;
1758 # get type of given object
1762 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1764 close $fd or return;
1769 # repository configuration
1770 our $config_file = '';
1773 # store multiple values for single key as anonymous array reference
1774 # single values stored directly in the hash, not as [ <value> ]
1775 sub hash_set_multi {
1776 my ($hash, $key, $value) = @_;
1778 if (!exists $hash->{$key}) {
1779 $hash->{$key} = $value;
1780 } elsif (!ref $hash->{$key}) {
1781 $hash->{$key} = [ $hash->{$key}, $value ];
1783 push @{$hash->{$key}}, $value;
1787 # return hash of git project configuration
1788 # optionally limited to some section, e.g. 'gitweb'
1789 sub git_parse_project_config {
1790 my $section_regexp = shift;
1795 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1798 while (my $keyval = <$fh>) {
1800 my ($key, $value) = split(/\n/, $keyval, 2);
1802 hash_set_multi(\%config, $key, $value)
1803 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1810 # convert config value to boolean, 'true' or 'false'
1811 # no value, number > 0, 'true' and 'yes' values are true
1812 # rest of values are treated as false (never as error)
1813 sub config_to_bool {
1816 # strip leading and trailing whitespace
1820 return (!defined $val || # section.key
1821 ($val =~ /^\d+$/ && $val) || # section.key = 1
1822 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1825 # convert config value to simple decimal number
1826 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1827 # to be multiplied by 1024, 1048576, or 1073741824
1831 # strip leading and trailing whitespace
1835 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1837 # unknown unit is treated as 1
1838 return $num * ($unit eq 'g' ? 1073741824 :
1839 $unit eq 'm' ? 1048576 :
1840 $unit eq 'k' ? 1024 : 1);
1845 # convert config value to array reference, if needed
1846 sub config_to_multi {
1849 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1852 sub git_get_project_config {
1853 my ($key, $type) = @_;
1856 return unless ($key);
1857 $key =~ s/^gitweb\.//;
1858 return if ($key =~ m/\W/);
1861 if (defined $type) {
1864 unless ($type eq 'bool' || $type eq 'int');
1868 if (!defined $config_file ||
1869 $config_file ne "$git_dir/config") {
1870 %config = git_parse_project_config('gitweb');
1871 $config_file = "$git_dir/config";
1875 if (!defined $type) {
1876 return $config{"gitweb.$key"};
1877 } elsif ($type eq 'bool') {
1878 # backward compatibility: 'git config --bool' returns true/false
1879 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1880 } elsif ($type eq 'int') {
1881 return config_to_int($config{"gitweb.$key"});
1883 return $config{"gitweb.$key"};
1886 # get hash of given path at given ref
1887 sub git_get_hash_by_path {
1889 my $path = shift || return undef;
1894 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1895 or die_error(500, "Open git-ls-tree failed");
1897 close $fd or return undef;
1899 if (!defined $line) {
1900 # there is no tree or hash given by $path at $base
1904 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1905 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1906 if (defined $type && $type ne $2) {
1907 # type doesn't match
1913 # get path of entry with given hash at given tree-ish (ref)
1914 # used to get 'from' filename for combined diff (merge commit) for renames
1915 sub git_get_path_by_hash {
1916 my $base = shift || return;
1917 my $hash = shift || return;
1921 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1923 while (my $line = <$fd>) {
1926 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1927 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1928 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1937 ## ......................................................................
1938 ## git utility functions, directly accessing git repository
1940 sub git_get_project_description {
1943 $git_dir = "$projectroot/$path";
1944 open my $fd, "$git_dir/description"
1945 or return git_get_project_config('description');
1948 if (defined $descr) {
1954 sub git_get_project_ctags {
1958 $git_dir = "$projectroot/$path";
1959 unless (opendir D, "$git_dir/ctags") {
1962 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
1963 open CT, $_ or next;
1967 my $ctag = $_; $ctag =~ s#.*/##;
1968 $ctags->{$ctag} = $val;
1974 sub git_populate_project_tagcloud {
1977 # First, merge different-cased tags; tags vote on casing
1979 foreach (keys %$ctags) {
1980 $ctags_lc{lc $_}->{count} += $ctags->{$_};
1981 if (not $ctags_lc{lc $_}->{topcount}
1982 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
1983 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
1984 $ctags_lc{lc $_}->{topname} = $_;
1989 if (eval { require HTML::TagCloud; 1; }) {
1990 $cloud = HTML::TagCloud->new;
1991 foreach (sort keys %ctags_lc) {
1992 # Pad the title with spaces so that the cloud looks
1994 my $title = $ctags_lc{$_}->{topname};
1995 $title =~ s/ / /g;
1996 $title =~ s/^/ /g;
1997 $title =~ s/$/ /g;
1998 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2001 $cloud = \%ctags_lc;
2006 sub git_show_project_tagcloud {
2007 my ($cloud, $count) = @_;
2008 print STDERR ref($cloud)."..\n";
2009 if (ref $cloud eq 'HTML::TagCloud') {
2010 return $cloud->html_and_css($count);
2012 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2013 return '<p align="center">' . join (', ', map {
2014 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2015 } splice(@tags, 0, $count)) . '</p>';
2019 sub git_get_project_url_list {
2022 $git_dir = "$projectroot/$path";
2023 open my $fd, "$git_dir/cloneurl"
2024 or return wantarray ?
2025 @{ config_to_multi(git_get_project_config('url')) } :
2026 config_to_multi(git_get_project_config('url'));
2027 my @git_project_url_list = map { chomp; $_ } <$fd>;
2030 return wantarray ? @git_project_url_list : \@git_project_url_list;
2033 sub git_get_projects_list {
2038 $filter =~ s/\.git$//;
2040 my ($check_forks) = gitweb_check_feature('forks');
2042 if (-d $projects_list) {
2043 # search in directory
2044 my $dir = $projects_list . ($filter ? "/$filter" : '');
2045 # remove the trailing "/"
2047 my $pfxlen = length("$dir");
2048 my $pfxdepth = ($dir =~ tr!/!!);
2051 follow_fast => 1, # follow symbolic links
2052 follow_skip => 2, # ignore duplicates
2053 dangling_symlinks => 0, # ignore dangling symlinks, silently
2055 # skip project-list toplevel, if we get it.
2056 return if (m!^[/.]$!);
2057 # only directories can be git repositories
2058 return unless (-d $_);
2059 # don't traverse too deep (Find is super slow on os x)
2060 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2061 $File::Find::prune = 1;
2065 my $subdir = substr($File::Find::name, $pfxlen + 1);
2066 # we check related file in $projectroot
2067 if (check_export_ok("$projectroot/$filter/$subdir")) {
2068 push @list, { path => ($filter ? "$filter/" : '') . $subdir };
2069 $File::Find::prune = 1;
2074 } elsif (-f $projects_list) {
2075 # read from file(url-encoded):
2076 # 'git%2Fgit.git Linus+Torvalds'
2077 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2078 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2080 open my ($fd), $projects_list or return;
2082 while (my $line = <$fd>) {
2084 my ($path, $owner) = split ' ', $line;
2085 $path = unescape($path);
2086 $owner = unescape($owner);
2087 if (!defined $path) {
2090 if ($filter ne '') {
2091 # looking for forks;
2092 my $pfx = substr($path, 0, length($filter));
2093 if ($pfx ne $filter) {
2096 my $sfx = substr($path, length($filter));
2097 if ($sfx !~ /^\/.*\.git$/) {
2100 } elsif ($check_forks) {
2102 foreach my $filter (keys %paths) {
2103 # looking for forks;
2104 my $pfx = substr($path, 0, length($filter));
2105 if ($pfx ne $filter) {
2108 my $sfx = substr($path, length($filter));
2109 if ($sfx !~ /^\/.*\.git$/) {
2112 # is a fork, don't include it in
2117 if (check_export_ok("$projectroot/$path")) {
2120 owner => to_utf8($owner),
2123 (my $forks_path = $path) =~ s/\.git$//;
2124 $paths{$forks_path}++;
2132 our $gitweb_project_owner = undef;
2133 sub git_get_project_list_from_file {
2135 return if (defined $gitweb_project_owner);
2137 $gitweb_project_owner = {};
2138 # read from file (url-encoded):
2139 # 'git%2Fgit.git Linus+Torvalds'
2140 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2141 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2142 if (-f $projects_list) {
2143 open (my $fd , $projects_list);
2144 while (my $line = <$fd>) {
2146 my ($pr, $ow) = split ' ', $line;
2147 $pr = unescape($pr);
2148 $ow = unescape($ow);
2149 $gitweb_project_owner->{$pr} = to_utf8($ow);
2155 sub git_get_project_owner {
2156 my $project = shift;
2159 return undef unless $project;
2160 $git_dir = "$projectroot/$project";
2162 if (!defined $gitweb_project_owner) {
2163 git_get_project_list_from_file();
2166 if (exists $gitweb_project_owner->{$project}) {
2167 $owner = $gitweb_project_owner->{$project};
2169 if (!defined $owner){
2170 $owner = git_get_project_config('owner');
2172 if (!defined $owner) {
2173 $owner = get_file_owner("$git_dir");
2179 sub git_get_last_activity {
2183 $git_dir = "$projectroot/$path";
2184 open($fd, "-|", git_cmd(), 'for-each-ref',
2185 '--format=%(committer)',
2186 '--sort=-committerdate',
2188 'refs/heads') or return;
2189 my $most_recent = <$fd>;
2190 close $fd or return;
2191 if (defined $most_recent &&
2192 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2194 my $age = time - $timestamp;
2195 return ($age, age_string($age));
2197 return (undef, undef);
2200 sub git_get_references {
2201 my $type = shift || "";
2203 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2204 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2205 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2206 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2209 while (my $line = <$fd>) {
2211 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2212 if (defined $refs{$1}) {
2213 push @{$refs{$1}}, $2;
2219 close $fd or return;
2223 sub git_get_rev_name_tags {
2224 my $hash = shift || return undef;
2226 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2228 my $name_rev = <$fd>;
2231 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2234 # catches also '$hash undefined' output
2239 ## ----------------------------------------------------------------------
2240 ## parse to hash functions
2244 my $tz = shift || "-0000";
2247 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2248 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2249 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2250 $date{'hour'} = $hour;
2251 $date{'minute'} = $min;
2252 $date{'mday'} = $mday;
2253 $date{'day'} = $days[$wday];
2254 $date{'month'} = $months[$mon];
2255 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2256 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2257 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2258 $mday, $months[$mon], $hour ,$min;
2259 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2260 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2262 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2263 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2264 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2265 $date{'hour_local'} = $hour;
2266 $date{'minute_local'} = $min;
2267 $date{'tz_local'} = $tz;
2268 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2269 1900+$year, $mon+1, $mday,
2270 $hour, $min, $sec, $tz);
2279 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2280 $tag{'id'} = $tag_id;
2281 while (my $line = <$fd>) {
2283 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2284 $tag{'object'} = $1;
2285 } elsif ($line =~ m/^type (.+)$/) {
2287 } elsif ($line =~ m/^tag (.+)$/) {
2289 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2290 $tag{'author'} = $1;
2293 } elsif ($line =~ m/--BEGIN/) {
2294 push @comment, $line;
2296 } elsif ($line eq "") {
2300 push @comment, <$fd>;
2301 $tag{'comment'} = \@comment;
2302 close $fd or return;
2303 if (!defined $tag{'name'}) {
2309 sub parse_commit_text {
2310 my ($commit_text, $withparents) = @_;
2311 my @commit_lines = split '\n', $commit_text;
2314 pop @commit_lines; # Remove '\0'
2316 if (! @commit_lines) {
2320 my $header = shift @commit_lines;
2321 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2324 ($co{'id'}, my @parents) = split ' ', $header;
2325 while (my $line = shift @commit_lines) {
2326 last if $line eq "\n";
2327 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2329 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2331 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2333 $co{'author_epoch'} = $2;
2334 $co{'author_tz'} = $3;
2335 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2336 $co{'author_name'} = $1;
2337 $co{'author_email'} = $2;
2339 $co{'author_name'} = $co{'author'};
2341 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2342 $co{'committer'} = $1;
2343 $co{'committer_epoch'} = $2;
2344 $co{'committer_tz'} = $3;
2345 $co{'committer_name'} = $co{'committer'};
2346 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2347 $co{'committer_name'} = $1;
2348 $co{'committer_email'} = $2;
2350 $co{'committer_name'} = $co{'committer'};
2354 if (!defined $co{'tree'}) {
2357 $co{'parents'} = \@parents;
2358 $co{'parent'} = $parents[0];
2360 foreach my $title (@commit_lines) {
2363 $co{'title'} = chop_str($title, 80, 5);
2364 # remove leading stuff of merges to make the interesting part visible
2365 if (length($title) > 50) {
2366 $title =~ s/^Automatic //;
2367 $title =~ s/^merge (of|with) /Merge ... /i;
2368 if (length($title) > 50) {
2369 $title =~ s/(http|rsync):\/\///;
2371 if (length($title) > 50) {
2372 $title =~ s/(master|www|rsync)\.//;
2374 if (length($title) > 50) {
2375 $title =~ s/kernel.org:?//;
2377 if (length($title) > 50) {
2378 $title =~ s/\/pub\/scm//;
2381 $co{'title_short'} = chop_str($title, 50, 5);
2385 if (! defined $co{'title'} || $co{'title'} eq "") {
2386 $co{'title'} = $co{'title_short'} = '(no commit message)';
2388 # remove added spaces
2389 foreach my $line (@commit_lines) {
2392 $co{'comment'} = \@commit_lines;
2394 my $age = time - $co{'committer_epoch'};
2396 $co{'age_string'} = age_string($age);
2397 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2398 if ($age > 60*60*24*7*2) {
2399 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2400 $co{'age_string_age'} = $co{'age_string'};
2402 $co{'age_string_date'} = $co{'age_string'};
2403 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2409 my ($commit_id) = @_;
2414 open my $fd, "-|", git_cmd(), "rev-list",
2420 or die_error(500, "Open git-rev-list failed");
2421 %co = parse_commit_text(<$fd>, 1);
2428 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2436 open my $fd, "-|", git_cmd(), "rev-list",
2439 ("--max-count=" . $maxcount),
2440 ("--skip=" . $skip),
2444 ($filename ? ($filename) : ())
2445 or die_error(500, "Open git-rev-list failed");
2446 while (my $line = <$fd>) {
2447 my %co = parse_commit_text($line);
2452 return wantarray ? @cos : \@cos;
2455 # parse line of git-diff-tree "raw" output
2456 sub parse_difftree_raw_line {
2460 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2461 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2462 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2463 $res{'from_mode'} = $1;
2464 $res{'to_mode'} = $2;
2465 $res{'from_id'} = $3;
2467 $res{'status'} = $5;
2468 $res{'similarity'} = $6;
2469 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2470 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2472 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2475 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2476 # combined diff (for merge commit)
2477 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2478 $res{'nparents'} = length($1);
2479 $res{'from_mode'} = [ split(' ', $2) ];
2480 $res{'to_mode'} = pop @{$res{'from_mode'}};
2481 $res{'from_id'} = [ split(' ', $3) ];
2482 $res{'to_id'} = pop @{$res{'from_id'}};
2483 $res{'status'} = [ split('', $4) ];
2484 $res{'to_file'} = unquote($5);
2486 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2487 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2488 $res{'commit'} = $1;
2491 return wantarray ? %res : \%res;
2494 # wrapper: return parsed line of git-diff-tree "raw" output
2495 # (the argument might be raw line, or parsed info)
2496 sub parsed_difftree_line {
2497 my $line_or_ref = shift;
2499 if (ref($line_or_ref) eq "HASH") {
2500 # pre-parsed (or generated by hand)
2501 return $line_or_ref;
2503 return parse_difftree_raw_line($line_or_ref);
2507 # parse line of git-ls-tree output
2508 sub parse_ls_tree_line ($;%) {
2513 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2514 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2522 $res{'name'} = unquote($4);
2525 return wantarray ? %res : \%res;
2528 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2529 sub parse_from_to_diffinfo {
2530 my ($diffinfo, $from, $to, @parents) = @_;
2532 if ($diffinfo->{'nparents'}) {
2534 $from->{'file'} = [];
2535 $from->{'href'} = [];
2536 fill_from_file_info($diffinfo, @parents)
2537 unless exists $diffinfo->{'from_file'};
2538 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2539 $from->{'file'}[$i] =
2540 defined $diffinfo->{'from_file'}[$i] ?
2541 $diffinfo->{'from_file'}[$i] :
2542 $diffinfo->{'to_file'};
2543 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2544 $from->{'href'}[$i] = href(action=>"blob",
2545 hash_base=>$parents[$i],
2546 hash=>$diffinfo->{'from_id'}[$i],
2547 file_name=>$from->{'file'}[$i]);
2549 $from->{'href'}[$i] = undef;
2553 # ordinary (not combined) diff
2554 $from->{'file'} = $diffinfo->{'from_file'};
2555 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2556 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2557 hash=>$diffinfo->{'from_id'},
2558 file_name=>$from->{'file'});
2560 delete $from->{'href'};
2564 $to->{'file'} = $diffinfo->{'to_file'};
2565 if (!is_deleted($diffinfo)) { # file exists in result
2566 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2567 hash=>$diffinfo->{'to_id'},
2568 file_name=>$to->{'file'});
2570 delete $to->{'href'};
2574 ## ......................................................................
2575 ## parse to array of hashes functions
2577 sub git_get_heads_list {
2581 open my $fd, '-|', git_cmd(), 'for-each-ref',
2582 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2583 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2586 while (my $line = <$fd>) {
2590 my ($refinfo, $committerinfo) = split(/\0/, $line);
2591 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2592 my ($committer, $epoch, $tz) =
2593 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2594 $ref_item{'fullname'} = $name;
2595 $name =~ s!^refs/heads/!!;
2597 $ref_item{'name'} = $name;
2598 $ref_item{'id'} = $hash;
2599 $ref_item{'title'} = $title || '(no commit message)';
2600 $ref_item{'epoch'} = $epoch;
2602 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2604 $ref_item{'age'} = "unknown";
2607 push @headslist, \%ref_item;
2611 return wantarray ? @headslist : \@headslist;
2614 sub git_get_tags_list {
2618 open my $fd, '-|', git_cmd(), 'for-each-ref',
2619 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2620 '--format=%(objectname) %(objecttype) %(refname) '.
2621 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2624 while (my $line = <$fd>) {
2628 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2629 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2630 my ($creator, $epoch, $tz) =
2631 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2632 $ref_item{'fullname'} = $name;
2633 $name =~ s!^refs/tags/!!;
2635 $ref_item{'type'} = $type;
2636 $ref_item{'id'} = $id;
2637 $ref_item{'name'} = $name;
2638 if ($type eq "tag") {
2639 $ref_item{'subject'} = $title;
2640 $ref_item{'reftype'} = $reftype;
2641 $ref_item{'refid'} = $refid;
2643 $ref_item{'reftype'} = $type;
2644 $ref_item{'refid'} = $id;
2647 if ($type eq "tag" || $type eq "commit") {
2648 $ref_item{'epoch'} = $epoch;
2650 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2652 $ref_item{'age'} = "unknown";
2656 push @tagslist, \%ref_item;
2660 return wantarray ? @tagslist : \@tagslist;
2663 ## ----------------------------------------------------------------------
2664 ## filesystem-related functions
2666 sub get_file_owner {
2669 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2670 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2671 if (!defined $gcos) {
2675 $owner =~ s/[,;].*$//;
2676 return to_utf8($owner);
2679 ## ......................................................................
2680 ## mimetype related functions
2682 sub mimetype_guess_file {
2683 my $filename = shift;
2684 my $mimemap = shift;
2685 -r $mimemap or return undef;
2688 open(MIME, $mimemap) or return undef;
2690 next if m/^#/; # skip comments
2691 my ($mime, $exts) = split(/\t+/);
2692 if (defined $exts) {
2693 my @exts = split(/\s+/, $exts);
2694 foreach my $ext (@exts) {
2695 $mimemap{$ext} = $mime;
2701 $filename =~ /\.([^.]*)$/;
2702 return $mimemap{$1};
2705 sub mimetype_guess {
2706 my $filename = shift;
2708 $filename =~ /\./ or return undef;
2710 if ($mimetypes_file) {
2711 my $file = $mimetypes_file;
2712 if ($file !~ m!^/!) { # if it is relative path
2713 # it is relative to project
2714 $file = "$projectroot/$project/$file";
2716 $mime = mimetype_guess_file($filename, $file);
2718 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2724 my $filename = shift;
2727 my $mime = mimetype_guess($filename);
2728 $mime and return $mime;
2732 return $default_blob_plain_mimetype unless $fd;
2735 return 'text/plain';
2736 } elsif (! $filename) {
2737 return 'application/octet-stream';
2738 } elsif ($filename =~ m/\.png$/i) {
2740 } elsif ($filename =~ m/\.gif$/i) {
2742 } elsif ($filename =~ m/\.jpe?g$/i) {
2743 return 'image/jpeg';
2745 return 'application/octet-stream';
2749 sub blob_contenttype {
2750 my ($fd, $file_name, $type) = @_;
2752 $type ||= blob_mimetype($fd, $file_name);
2753 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2754 $type .= "; charset=$default_text_plain_charset";
2760 ## ======================================================================
2761 ## functions printing HTML: header, footer, error page
2763 sub git_header_html {
2764 my $status = shift || "200 OK";
2765 my $expires = shift;
2767 my $title = "$site_name";
2768 if (defined $project) {
2769 $title .= " - " . to_utf8($project);
2770 if (defined $action) {
2771 $title .= "/$action";
2772 if (defined $file_name) {
2773 $title .= " - " . esc_path($file_name);
2774 if ($action eq "tree" && $file_name !~ m|/$|) {
2781 # require explicit support from the UA if we are to send the page as
2782 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2783 # we have to do this because MSIE sometimes globs '*/*', pretending to
2784 # support xhtml+xml but choking when it gets what it asked for.
2785 if (defined $cgi->http('HTTP_ACCEPT') &&
2786 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2787 $cgi->Accept('application/xhtml+xml') != 0) {
2788 $content_type = 'application/xhtml+xml';
2790 $content_type = 'text/html';
2792 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2793 -status=> $status, -expires => $expires);
2794 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2796 <?xml version="1.0" encoding="utf-8"?>
2797 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2798 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2799 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2800 <!-- git core binaries version $git_version -->
2802 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2803 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2804 <meta name="robots" content="index, nofollow"/>
2805 <title>$title</title>
2807 # print out each stylesheet that exist
2808 if (defined $stylesheet) {
2809 #provides backwards capability for those people who define style sheet in a config file
2810 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2812 foreach my $stylesheet (@stylesheets) {
2813 next unless $stylesheet;
2814 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2817 if (defined $project) {
2818 my %href_params = get_feed_info();
2819 if (!exists $href_params{'-title'}) {
2820 $href_params{'-title'} = 'log';
2823 foreach my $format qw(RSS Atom) {
2824 my $type = lc($format);
2826 '-rel' => 'alternate',
2827 '-title' => "$project - $href_params{'-title'} - $format feed",
2828 '-type' => "application/$type+xml"
2831 $href_params{'action'} = $type;
2832 $link_attr{'-href'} = href(%href_params);
2834 "rel=\"$link_attr{'-rel'}\" ".
2835 "title=\"$link_attr{'-title'}\" ".
2836 "href=\"$link_attr{'-href'}\" ".
2837 "type=\"$link_attr{'-type'}\" ".
2840 $href_params{'extra_options'} = '--no-merges';
2841 $link_attr{'-href'} = href(%href_params);
2842 $link_attr{'-title'} .= ' (no merges)';
2844 "rel=\"$link_attr{'-rel'}\" ".
2845 "title=\"$link_attr{'-title'}\" ".
2846 "href=\"$link_attr{'-href'}\" ".
2847 "type=\"$link_attr{'-type'}\" ".
2852 printf('<link rel="alternate" title="%s projects list" '.
2853 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2854 $site_name, href(project=>undef, action=>"project_index"));
2855 printf('<link rel="alternate" title="%s projects feeds" '.
2856 'href="%s" type="text/x-opml" />'."\n",
2857 $site_name, href(project=>undef, action=>"opml"));
2859 if (defined $favicon) {
2860 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2866 if (-f $site_header) {
2867 open (my $fd, $site_header);
2872 print "<div class=\"page_header\">\n" .
2873 $cgi->a({-href => esc_url($logo_url),
2874 -title => $logo_label},
2875 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2876 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2877 if (defined $project) {
2878 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2879 if (defined $action) {
2886 my ($have_search) = gitweb_check_feature('search');
2887 if (defined $project && $have_search) {
2888 if (!defined $searchtext) {
2892 if (defined $hash_base) {
2893 $search_hash = $hash_base;
2894 } elsif (defined $hash) {
2895 $search_hash = $hash;
2897 $search_hash = "HEAD";
2899 my $action = $my_uri;
2900 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2901 if ($use_pathinfo) {
2902 $action .= "/".esc_url($project);
2904 print $cgi->startform(-method => "get", -action => $action) .
2905 "<div class=\"search\">\n" .
2907 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2908 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2909 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2910 $cgi->popup_menu(-name => 'st', -default => 'commit',
2911 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2912 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2914 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2915 "<span title=\"Extended regular expression\">" .
2916 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2917 -checked => $search_use_regexp) .
2920 $cgi->end_form() . "\n";
2924 sub git_footer_html {
2925 my $feed_class = 'rss_logo';
2927 print "<div class=\"page_footer\">\n";
2928 if (defined $project) {
2929 my $descr = git_get_project_description($project);
2930 if (defined $descr) {
2931 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2934 my %href_params = get_feed_info();
2935 if (!%href_params) {
2936 $feed_class .= ' generic';
2938 $href_params{'-title'} ||= 'log';
2940 foreach my $format qw(RSS Atom) {
2941 $href_params{'action'} = lc($format);
2942 print $cgi->a({-href => href(%href_params),
2943 -title => "$href_params{'-title'} $format feed",
2944 -class => $feed_class}, $format)."\n";
2948 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2949 -class => $feed_class}, "OPML") . " ";
2950 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2951 -class => $feed_class}, "TXT") . "\n";
2953 print "</div>\n"; # class="page_footer"
2955 if (-f $site_footer) {
2956 open (my $fd, $site_footer);
2965 # die_error(<http_status_code>, <error_message>)
2966 # Example: die_error(404, 'Hash not found')
2967 # By convention, use the following status codes (as defined in RFC 2616):
2968 # 400: Invalid or missing CGI parameters, or
2969 # requested object exists but has wrong type.
2970 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2971 # this server or project.
2972 # 404: Requested object/revision/project doesn't exist.
2973 # 500: The server isn't configured properly, or
2974 # an internal error occurred (e.g. failed assertions caused by bugs), or
2975 # an unknown error occurred (e.g. the git binary died unexpectedly).
2977 my $status = shift || 500;
2978 my $error = shift || "Internal server error";
2980 my %http_responses = (400 => '400 Bad Request',
2981 403 => '403 Forbidden',
2982 404 => '404 Not Found',
2983 500 => '500 Internal Server Error');
2984 git_header_html($http_responses{$status});
2986 <div class="page_body">
2996 ## ----------------------------------------------------------------------
2997 ## functions printing or outputting HTML: navigation
2999 sub git_print_page_nav {
3000 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3001 $extra = '' if !defined $extra; # pager or formats
3003 my @navs = qw(summary shortlog log commit commitdiff tree);
3005 @navs = grep { $_ ne $suppress } @navs;
3008 my %arg = map { $_ => {action=>$_} } @navs;
3009 if (defined $head) {
3010 for (qw(commit commitdiff)) {
3011 $arg{$_}{'hash'} = $head;
3013 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3014 for (qw(shortlog log)) {
3015 $arg{$_}{'hash'} = $head;
3020 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3021 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3023 my @actions = gitweb_check_feature('actions');
3025 my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
3026 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3028 $link =~ s#%n#$project#g;
3029 $link =~ s#%f#$git_dir#g;
3030 $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
3031 $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
3032 $arg{$label}{'_href'} = $link;
3035 print "<div class=\"page_nav\">\n" .
3037 map { $_ eq $current ?
3038 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3040 print "<br/>\n$extra<br/>\n" .
3044 sub format_paging_nav {
3045 my ($action, $hash, $head, $page, $has_next_link) = @_;
3049 if ($hash ne $head || $page) {
3050 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3052 $paging_nav .= "HEAD";
3056 $paging_nav .= " ⋅ " .
3057 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3058 -accesskey => "p", -title => "Alt-p"}, "prev");
3060 $paging_nav .= " ⋅ prev";
3063 if ($has_next_link) {
3064 $paging_nav .= " ⋅ " .
3065 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3066 -accesskey => "n", -title => "Alt-n"}, "next");
3068 $paging_nav .= " ⋅ next";
3074 ## ......................................................................
3075 ## functions printing or outputting HTML: div
3077 sub git_print_header_div {
3078 my ($action, $title, $hash, $hash_base) = @_;
3081 $args{'action'} = $action;
3082 $args{'hash'} = $hash if $hash;
3083 $args{'hash_base'} = $hash_base if $hash_base;
3085 print "<div class=\"header\">\n" .
3086 $cgi->a({-href => href(%args), -class => "title"},
3087 $title ? $title : $action) .
3091 #sub git_print_authorship (\%) {
3092 sub git_print_authorship {
3095 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3096 print "<div class=\"author_date\">" .
3097 esc_html($co->{'author_name'}) .
3099 if ($ad{'hour_local'} < 6) {
3100 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3101 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3103 printf(" (%02d:%02d %s)",
3104 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3109 sub git_print_page_path {
3115 print "<div class=\"page_path\">";
3116 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3117 -title => 'tree root'}, to_utf8("[$project]"));
3119 if (defined $name) {
3120 my @dirname = split '/', $name;
3121 my $basename = pop @dirname;
3124 foreach my $dir (@dirname) {
3125 $fullname .= ($fullname ? '/' : '') . $dir;
3126 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3128 -title => $fullname}, esc_path($dir));
3131 if (defined $type && $type eq 'blob') {
3132 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3134 -title => $name}, esc_path($basename));
3135 } elsif (defined $type && $type eq 'tree') {
3136 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3138 -title => $name}, esc_path($basename));
3141 print esc_path($basename);
3144 print "<br/></div>\n";
3147 # sub git_print_log (\@;%) {
3148 sub git_print_log ($;%) {
3152 if ($opts{'-remove_title'}) {
3153 # remove title, i.e. first line of log
3156 # remove leading empty lines
3157 while (defined $log->[0] && $log->[0] eq "") {
3164 foreach my $line (@$log) {
3165 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3168 if (! $opts{'-remove_signoff'}) {
3169 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3172 # remove signoff lines
3179 # print only one empty line
3180 # do not print empty line after signoff
3182 next if ($empty || $signoff);
3188 print format_log_line_html($line) . "<br/>\n";
3191 if ($opts{'-final_empty_line'}) {
3192 # end with single empty line
3193 print "<br/>\n" unless $empty;
3197 # return link target (what link points to)
3198 sub git_get_link_target {
3203 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3207 $link_target = <$fd>;
3212 return $link_target;
3215 # given link target, and the directory (basedir) the link is in,
3216 # return target of link relative to top directory (top tree);
3217 # return undef if it is not possible (including absolute links).
3218 sub normalize_link_target {
3219 my ($link_target, $basedir, $hash_base) = @_;
3221 # we can normalize symlink target only if $hash_base is provided
3222 return unless $hash_base;
3224 # absolute symlinks (beginning with '/') cannot be normalized
3225 return if (substr($link_target, 0, 1) eq '/');
3227 # normalize link target to path from top (root) tree (dir)
3230 $path = $basedir . '/' . $link_target;
3232 # we are in top (root) tree (dir)
3233 $path = $link_target;
3236 # remove //, /./, and /../
3238 foreach my $part (split('/', $path)) {
3239 # discard '.' and ''
3240 next if (!$part || $part eq '.');
3242 if ($part eq '..') {
3246 # link leads outside repository (outside top dir)
3250 push @path_parts, $part;
3253 $path = join('/', @path_parts);
3258 # print tree entry (row of git_tree), but without encompassing <tr> element
3259 sub git_print_tree_entry {
3260 my ($t, $basedir, $hash_base, $have_blame) = @_;
3263 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3265 # The format of a table row is: mode list link. Where mode is
3266 # the mode of the entry, list is the name of the entry, an href,
3267 # and link is the action links of the entry.
3269 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3270 if ($t->{'type'} eq "blob") {
3271 print "<td class=\"list\">" .
3272 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3273 file_name=>"$basedir$t->{'name'}", %base_key),
3274 -class => "list"}, esc_path($t->{'name'}));
3275 if (S_ISLNK(oct $t->{'mode'})) {
3276 my $link_target = git_get_link_target($t->{'hash'});
3278 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3279 if (defined $norm_target) {
3281 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3282 file_name=>$norm_target),
3283 -title => $norm_target}, esc_path($link_target));
3285 print " -> " . esc_path($link_target);
3290 print "<td class=\"link\">";
3291 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3292 file_name=>"$basedir$t->{'name'}", %base_key)},
3296 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3297 file_name=>"$basedir$t->{'name'}", %base_key)},
3300 if (defined $hash_base) {
3302 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3303 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3307 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3308 file_name=>"$basedir$t->{'name'}")},
3312 } elsif ($t->{'type'} eq "tree") {
3313 print "<td class=\"list\">";
3314 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3315 file_name=>"$basedir$t->{'name'}", %base_key)},
3316 esc_path($t->{'name'}));
3318 print "<td class=\"link\">";
3319 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3320 file_name=>"$basedir$t->{'name'}", %base_key)},
3322 if (defined $hash_base) {
3324 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3325 file_name=>"$basedir$t->{'name'}")},
3330 # unknown object: we can only present history for it
3331 # (this includes 'commit' object, i.e. submodule support)
3332 print "<td class=\"list\">" .
3333 esc_path($t->{'name'}) .
3335 print "<td class=\"link\">";
3336 if (defined $hash_base) {
3337 print $cgi->a({-href => href(action=>"history",
3338 hash_base=>$hash_base,
3339 file_name=>"$basedir$t->{'name'}")},
3346 ## ......................................................................
3347 ## functions printing large fragments of HTML
3349 # get pre-image filenames for merge (combined) diff
3350 sub fill_from_file_info {
3351 my ($diff, @parents) = @_;
3353 $diff->{'from_file'} = [ ];
3354 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3355 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3356 if ($diff->{'status'}[$i] eq 'R' ||
3357 $diff->{'status'}[$i] eq 'C') {
3358 $diff->{'from_file'}[$i] =
3359 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3366 # is current raw difftree line of file deletion
3368 my $diffinfo = shift;
3370 return $diffinfo->{'to_id'} eq ('0' x 40);
3373 # does patch correspond to [previous] difftree raw line
3374 # $diffinfo - hashref of parsed raw diff format
3375 # $patchinfo - hashref of parsed patch diff format
3376 # (the same keys as in $diffinfo)
3377 sub is_patch_split {
3378 my ($diffinfo, $patchinfo) = @_;
3380 return defined $diffinfo && defined $patchinfo
3381 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3385 sub git_difftree_body {
3386 my ($difftree, $hash, @parents) = @_;
3387 my ($parent) = $parents[0];
3388 my ($have_blame) = gitweb_check_feature('blame');
3389 print "<div class=\"list_head\">\n";
3390 if ($#{$difftree} > 10) {
3391 print(($#{$difftree} + 1) . " files changed:\n");
3395 print "<table class=\"" .
3396 (@parents > 1 ? "combined " : "") .
3399 # header only for combined diff in 'commitdiff' view
3400 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3403 print "<thead><tr>\n" .
3404 "<th></th><th></th>\n"; # filename, patchN link
3405 for (my $i = 0; $i < @parents; $i++) {
3406 my $par = $parents[$i];
3408 $cgi->a({-href => href(action=>"commitdiff",
3409 hash=>$hash, hash_parent=>$par),
3410 -title => 'commitdiff to parent number ' .
3411 ($i+1) . ': ' . substr($par,0,7)},
3415 print "</tr></thead>\n<tbody>\n";
3420 foreach my $line (@{$difftree}) {
3421 my $diff = parsed_difftree_line($line);
3424 print "<tr class=\"dark\">\n";
3426 print "<tr class=\"light\">\n";
3430 if (exists $diff->{'nparents'}) { # combined diff
3432 fill_from_file_info($diff, @parents)
3433 unless exists $diff->{'from_file'};
3435 if (!is_deleted($diff)) {
3436 # file exists in the result (child) commit
3438 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3439 file_name=>$diff->{'to_file'},
3441 -class => "list"}, esc_path($diff->{'to_file'})) .
3445 esc_path($diff->{'to_file'}) .
3449 if ($action eq 'commitdiff') {
3452 print "<td class=\"link\">" .
3453 $cgi->a({-href => "#patch$patchno"}, "patch") .
3458 my $has_history = 0;
3459 my $not_deleted = 0;
3460 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3461 my $hash_parent = $parents[$i];
3462 my $from_hash = $diff->{'from_id'}[$i];
3463 my $from_path = $diff->{'from_file'}[$i];
3464 my $status = $diff->{'status'}[$i];
3466 $has_history ||= ($status ne 'A');
3467 $not_deleted ||= ($status ne 'D');
3469 if ($status eq 'A') {
3470 print "<td class=\"link\" align=\"right\"> | </td>\n";
3471 } elsif ($status eq 'D') {
3472 print "<td class=\"link\">" .
3473 $cgi->a({-href => href(action=>"blob",
3476 file_name=>$from_path)},
3480 if ($diff->{'to_id'} eq $from_hash) {
3481 print "<td class=\"link nochange\">";
3483 print "<td class=\"link\">";
3485 print $cgi->a({-href => href(action=>"blobdiff",
3486 hash=>$diff->{'to_id'},
3487 hash_parent=>$from_hash,
3489 hash_parent_base=>$hash_parent,
3490 file_name=>$diff->{'to_file'},
3491 file_parent=>$from_path)},
3497 print "<td class=\"link\">";
3499 print $cgi->a({-href => href(action=>"blob",
3500 hash=>$diff->{'to_id'},
3501 file_name=>$diff->{'to_file'},
3504 print " | " if ($has_history);
3507 print $cgi->a({-href => href(action=>"history",
3508 file_name=>$diff->{'to_file'},
3515 next; # instead of 'else' clause, to avoid extra indent
3517 # else ordinary diff
3519 my ($to_mode_oct, $to_mode_str, $to_file_type);
3520 my ($from_mode_oct, $from_mode_str, $from_file_type);
3521 if ($diff->{'to_mode'} ne ('0' x 6)) {
3522 $to_mode_oct = oct $diff->{'to_mode'};
3523 if (S_ISREG($to_mode_oct)) { # only for regular file
3524 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3526 $to_file_type = file_type($diff->{'to_mode'});
3528 if ($diff->{'from_mode'} ne ('0' x 6)) {
3529 $from_mode_oct = oct $diff->{'from_mode'};
3530 if (S_ISREG($to_mode_oct)) { # only for regular file
3531 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3533 $from_file_type = file_type($diff->{'from_mode'});
3536 if ($diff->{'status'} eq "A") { # created
3537 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3538 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3539 $mode_chng .= "]</span>";
3541 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3542 hash_base=>$hash, file_name=>$diff->{'file'}),
3543 -class => "list"}, esc_path($diff->{'file'}));
3545 print "<td>$mode_chng</td>\n";
3546 print "<td class=\"link\">";
3547 if ($action eq 'commitdiff') {
3550 print $cgi->a({-href => "#patch$patchno"}, "patch");
3553 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3554 hash_base=>$hash, file_name=>$diff->{'file'})},
3558 } elsif ($diff->{'status'} eq "D") { # deleted
3559 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3561 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3562 hash_base=>$parent, file_name=>$diff->{'file'}),
3563 -class => "list"}, esc_path($diff->{'file'}));
3565 print "<td>$mode_chng</td>\n";
3566 print "<td class=\"link\">";
3567 if ($action eq 'commitdiff') {
3570 print $cgi->a({-href => "#patch$patchno"}, "patch");
3573 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3574 hash_base=>$parent, file_name=>$diff->{'file'})},
3577 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3578 file_name=>$diff->{'file'})},
3581 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3582 file_name=>$diff->{'file'})},
3586 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3587 my $mode_chnge = "";
3588 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3589 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3590 if ($from_file_type ne $to_file_type) {
3591 $mode_chnge .= " from $from_file_type to $to_file_type";
3593 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3594 if ($from_mode_str && $to_mode_str) {
3595 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3596 } elsif ($to_mode_str) {
3597 $mode_chnge .= " mode: $to_mode_str";
3600 $mode_chnge .= "]</span>\n";
3603 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3604 hash_base=>$hash, file_name=>$diff->{'file'}),
3605 -class => "list"}, esc_path($diff->{'file'}));
3607 print "<td>$mode_chnge</td>\n";
3608 print "<td class=\"link\">";
3609 if ($action eq 'commitdiff') {
3612 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3614 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3615 # "commit" view and modified file (not onlu mode changed)
3616 print $cgi->a({-href => href(action=>"blobdiff",
3617 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3618 hash_base=>$hash, hash_parent_base=>$parent,
3619 file_name=>$diff->{'file'})},
3623 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3624 hash_base=>$hash, file_name=>$diff->{'file'})},
3627 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3628 file_name=>$diff->{'file'})},
3631 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3632 file_name=>$diff->{'file'})},
3636 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3637 my %status_name = ('R' => 'moved', 'C' => 'copied');
3638 my $nstatus = $status_name{$diff->{'status'}};
3640 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3641 # mode also for directories, so we cannot use $to_mode_str
3642 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3645 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3646 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3647 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3648 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3649 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3650 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3651 -class => "list"}, esc_path($diff->{'from_file'})) .
3652 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3653 "<td class=\"link\">";
3654 if ($action eq 'commitdiff') {
3657 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3659 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3660 # "commit" view and modified file (not only pure rename or copy)
3661 print $cgi->a({-href => href(action=>"blobdiff",
3662 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3663 hash_base=>$hash, hash_parent_base=>$parent,
3664 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3668 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3669 hash_base=>$parent, file_name=>$diff->{'to_file'})},
3672 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3673 file_name=>$diff->{'to_file'})},
3676 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3677 file_name=>$diff->{'to_file'})},
3681 } # we should not encounter Unmerged (U) or Unknown (X) status
3684 print "</tbody>" if $has_header;
3688 sub git_patchset_body {
3689 my ($fd, $difftree, $hash, @hash_parents) = @_;
3690 my ($hash_parent) = $hash_parents[0];
3692 my $is_combined = (@hash_parents > 1);
3694 my $patch_number = 0;
3700 print "<div class=\"patchset\">\n";
3702 # skip to first patch
3703 while ($patch_line = <$fd>) {
3706 last if ($patch_line =~ m/^diff /);
3710 while ($patch_line) {
3712 # parse "git diff" header line
3713 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3714 # $1 is from_name, which we do not use
3715 $to_name = unquote($2);
3716 $to_name =~ s!^b/!!;
3717 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3718 # $1 is 'cc' or 'combined', which we do not use
3719 $to_name = unquote($2);
3724 # check if current patch belong to current raw line
3725 # and parse raw git-diff line if needed
3726 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3727 # this is continuation of a split patch
3728 print "<div class=\"patch cont\">\n";
3730 # advance raw git-diff output if needed
3731 $patch_idx++ if defined $diffinfo;
3733 # read and prepare patch information
3734 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3736 # compact combined diff output can have some patches skipped
3737 # find which patch (using pathname of result) we are at now;
3739 while ($to_name ne $diffinfo->{'to_file'}) {
3740 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3741 format_diff_cc_simplified($diffinfo, @hash_parents) .
3742 "</div>\n"; # class="patch"
3747 last if $patch_idx > $#$difftree;
3748 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3752 # modifies %from, %to hashes
3753 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3755 # this is first patch for raw difftree line with $patch_idx index
3756 # we index @$difftree array from 0, but number patches from 1
3757 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3761 #assert($patch_line =~ m/^diff /) if DEBUG;
3762 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3764 # print "git diff" header
3765 print format_git_diff_header_line($patch_line, $diffinfo,
3768 # print extended diff header
3769 print "<div class=\"diff extended_header\">\n";
3771 while ($patch_line = <$fd>) {
3774 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3776 print format_extended_diff_header_line($patch_line, $diffinfo,
3779 print "</div>\n"; # class="diff extended_header"
3781 # from-file/to-file diff header
3782 if (! $patch_line) {
3783 print "</div>\n"; # class="patch"
3786 next PATCH if ($patch_line =~ m/^diff /);
3787 #assert($patch_line =~ m/^---/) if DEBUG;
3789 my $last_patch_line = $patch_line;
3790 $patch_line = <$fd>;
3792 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3794 print format_diff_from_to_header($last_patch_line, $patch_line,
3795 $diffinfo, \%from, \%to,
3800 while ($patch_line = <$fd>) {
3803 next PATCH if ($patch_line =~ m/^diff /);
3805 print format_diff_line($patch_line, \%from, \%to);
3809 print "</div>\n"; # class="patch"
3812 # for compact combined (--cc) format, with chunk and patch simpliciaction
3813 # patchset might be empty, but there might be unprocessed raw lines
3814 for (++$patch_idx if $patch_number > 0;
3815 $patch_idx < @$difftree;
3817 # read and prepare patch information
3818 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3820 # generate anchor for "patch" links in difftree / whatchanged part
3821 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3822 format_diff_cc_simplified($diffinfo, @hash_parents) .
3823 "</div>\n"; # class="patch"
3828 if ($patch_number == 0) {
3829 if (@hash_parents > 1) {
3830 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3832 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3836 print "</div>\n"; # class="patchset"
3839 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3841 # fills project list info (age, description, owner, forks) for each
3842 # project in the list, removing invalid projects from returned list
3843 # NOTE: modifies $projlist, but does not remove entries from it
3844 sub fill_project_list_info {
3845 my ($projlist, $check_forks) = @_;
3848 my $show_ctags = gitweb_check_feature('ctags');
3850 foreach my $pr (@$projlist) {
3851 my (@activity) = git_get_last_activity($pr->{'path'});
3852 unless (@activity) {
3855 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3856 if (!defined $pr->{'descr'}) {
3857 my $descr = git_get_project_description($pr->{'path'}) || "";
3858 $descr = to_utf8($descr);
3859 $pr->{'descr_long'} = $descr;
3860 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3862 if (!defined $pr->{'owner'}) {
3863 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3866 my $pname = $pr->{'path'};
3867 if (($pname =~ s/\.git$//) &&
3868 ($pname !~ /\/$/) &&
3869 (-d "$projectroot/$pname")) {
3870 $pr->{'forks'} = "-d $projectroot/$pname";
3875 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3876 push @projects, $pr;
3882 # print 'sort by' <th> element, generating 'sort by $name' replay link
3883 # if that order is not selected
3885 my ($name, $order, $header) = @_;
3886 $header ||= ucfirst($name);
3888 if ($order eq $name) {
3889 print "<th>$header</th>\n";
3892 $cgi->a({-href => href(-replay=>1, order=>$name),
3893 -class => "header"}, $header) .
3898 sub git_project_list_body {
3899 # actually uses global variable $project
3900 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3902 my ($check_forks) = gitweb_check_feature('forks');
3903 my @projects = fill_project_list_info($projlist, $check_forks);
3905 $order ||= $default_projects_order;
3906 $from = 0 unless defined $from;
3907 $to = $#projects if (!defined $to || $#projects < $to);
3910 project => { key => 'path', type => 'str' },
3911 descr => { key => 'descr_long', type => 'str' },
3912 owner => { key => 'owner', type => 'str' },
3913 age => { key => 'age', type => 'num' }
3915 my $oi = $order_info{$order};
3916 if ($oi->{'type'} eq 'str') {
3917 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3919 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3922 my $show_ctags = gitweb_check_feature('ctags');
3925 foreach my $p (@projects) {
3926 foreach my $ct (keys %{$p->{'ctags'}}) {
3927 $ctags{$ct} += $p->{'ctags'}->{$ct};
3930 my $cloud = git_populate_project_tagcloud(\%ctags);
3931 print git_show_project_tagcloud($cloud, 64);
3934 print "<table class=\"project_list\">\n";
3935 unless ($no_header) {
3938 print "<th></th>\n";
3940 print_sort_th('project', $order, 'Project');
3941 print_sort_th('descr', $order, 'Description');
3942 print_sort_th('owner', $order, 'Owner');
3943 print_sort_th('age', $order, 'Last Change');
3944 print "<th></th>\n" . # for links
3948 my $tagfilter = $cgi->param('by_tag');
3949 for (my $i = $from; $i <= $to; $i++) {
3950 my $pr = $projects[$i];
3952 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3953 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3954 and not $pr->{'descr_long'} =~ /$searchtext/;
3955 # Weed out forks or non-matching entries of search
3957 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3958 $forkbase="^$forkbase" if $forkbase;
3959 next if not $searchtext and not $tagfilter and $show_ctags
3960 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3964 print "<tr class=\"dark\">\n";
3966 print "<tr class=\"light\">\n";
3971 if ($pr->{'forks'}) {
3972 print "<!-- $pr->{'forks'} -->\n";
3973 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3977 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3978 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3979 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3980 -class => "list", -title => $pr->{'descr_long'}},
3981 esc_html($pr->{'descr'})) . "</td>\n" .
3982 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3983 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3984 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3985 "<td class=\"link\">" .
3986 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
3987 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3988 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3989 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3990 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3994 if (defined $extra) {
3997 print "<td></td>\n";
3999 print "<td colspan=\"5\">$extra</td>\n" .
4005 sub git_shortlog_body {
4006 # uses global variable $project
4007 my ($commitlist, $from, $to, $refs, $extra) = @_;
4009 $from = 0 unless defined $from;
4010 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4012 print "<table class=\"shortlog\">\n";
4014 for (my $i = $from; $i <= $to; $i++) {
4015 my %co = %{$commitlist->[$i]};
4016 my $commit = $co{'id'};
4017 my $ref = format_ref_marker($refs, $commit);
4019 print "<tr class=\"dark\">\n";
4021 print "<tr class=\"light\">\n";
4024 my $author = chop_and_escape_str($co{'author_name'}, 10);
4025 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4026 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4027 "<td><i>" . $author . "</i></td>\n" .
4029 print format_subject_html($co{'title'}, $co{'title_short'},
4030 href(action=>"commit", hash=>$commit), $ref);
4032 "<td class=\"link\">" .
4033 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4034 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4035 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4036 my $snapshot_links = format_snapshot_links($commit);
4037 if (defined $snapshot_links) {
4038 print " | " . $snapshot_links;
4043 if (defined $extra) {
4045 "<td colspan=\"4\">$extra</td>\n" .
4051 sub git_history_body {
4052 # Warning: assumes constant type (blob or tree) during history
4053 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4055 $from = 0 unless defined $from;
4056 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4058 print "<table class=\"history\">\n";
4060 for (my $i = $from; $i <= $to; $i++) {
4061 my %co = %{$commitlist->[$i]};
4065 my $commit = $co{'id'};
4067 my $ref = format_ref_marker($refs, $commit);
4070 print "<tr class=\"dark\">\n";
4072 print "<tr class=\"light\">\n";
4075 # shortlog uses chop_str($co{'author_name'}, 10)
4076 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
4077 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4078 "<td><i>" . $author . "</i></td>\n" .
4080 # originally git_history used chop_str($co{'title'}, 50)
4081 print format_subject_html($co{'title'}, $co{'title_short'},
4082 href(action=>"commit", hash=>$commit), $ref);
4084 "<td class=\"link\">" .
4085 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4086 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4088 if ($ftype eq 'blob') {
4089 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4090 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4091 if (defined $blob_current && defined $blob_parent &&
4092 $blob_current ne $blob_parent) {
4094 $cgi->a({-href => href(action=>"blobdiff",
4095 hash=>$blob_current, hash_parent=>$blob_parent,
4096 hash_base=>$hash_base, hash_parent_base=>$commit,
4097 file_name=>$file_name)},
4104 if (defined $extra) {
4106 "<td colspan=\"4\">$extra</td>\n" .
4113 # uses global variable $project
4114 my ($taglist, $from, $to, $extra) = @_;
4115 $from = 0 unless defined $from;
4116 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4118 print "<table class=\"tags\">\n";
4120 for (my $i = $from; $i <= $to; $i++) {
4121 my $entry = $taglist->[$i];
4123 my $comment = $tag{'subject'};
4125 if (defined $comment) {
4126 $comment_short = chop_str($comment, 30, 5);
4129 print "<tr class=\"dark\">\n";
4131 print "<tr class=\"light\">\n";
4134 if (defined $tag{'age'}) {
4135 print "<td><i>$tag{'age'}</i></td>\n";
4137 print "<td></td>\n";
4140 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4141 -class => "list name"}, esc_html($tag{'name'})) .
4144 if (defined $comment) {
4145 print format_subject_html($comment, $comment_short,
4146 href(action=>"tag", hash=>$tag{'id'}));
4149 "<td class=\"selflink\">";
4150 if ($tag{'type'} eq "tag") {
4151 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4156 "<td class=\"link\">" . " | " .
4157 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4158 if ($tag{'reftype'} eq "commit") {
4159 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4160 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4161 } elsif ($tag{'reftype'} eq "blob") {
4162 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4167 if (defined $extra) {
4169 "<td colspan=\"5\">$extra</td>\n" .
4175 sub git_heads_body {
4176 # uses global variable $project
4177 my ($headlist, $head, $from, $to, $extra) = @_;
4178 $from = 0 unless defined $from;
4179 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4181 print "<table class=\"heads\">\n";
4183 for (my $i = $from; $i <= $to; $i++) {
4184 my $entry = $headlist->[$i];
4186 my $curr = $ref{'id'} eq $head;
4188 print "<tr class=\"dark\">\n";
4190 print "<tr class=\"light\">\n";
4193 print "<td><i>$ref{'age'}</i></td>\n" .
4194 ($curr ? "<td class=\"current_head\">" : "<td>") .
4195 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4196 -class => "list name"},esc_html($ref{'name'})) .
4198 "<td class=\"link\">" .
4199 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4200 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4201 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4205 if (defined $extra) {
4207 "<td colspan=\"3\">$extra</td>\n" .
4213 sub git_search_grep_body {
4214 my ($commitlist, $from, $to, $extra) = @_;
4215 $from = 0 unless defined $from;
4216 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4218 print "<table class=\"commit_search\">\n";
4220 for (my $i = $from; $i <= $to; $i++) {
4221 my %co = %{$commitlist->[$i]};
4225 my $commit = $co{'id'};
4227 print "<tr class=\"dark\">\n";
4229 print "<tr class=\"light\">\n";
4232 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4233 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4234 "<td><i>" . $author . "</i></td>\n" .
4236 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4237 -class => "list subject"},
4238 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4239 my $comment = $co{'comment'};
4240 foreach my $line (@$comment) {
4241 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4242 my ($lead, $match, $trail) = ($1, $2, $3);
4243 $match = chop_str($match, 70, 5, 'center');
4244 my $contextlen = int((80 - length($match))/2);
4245 $contextlen = 30 if ($contextlen > 30);
4246 $lead = chop_str($lead, $contextlen, 10, 'left');
4247 $trail = chop_str($trail, $contextlen, 10, 'right');
4249 $lead = esc_html($lead);
4250 $match = esc_html($match);
4251 $trail = esc_html($trail);
4253 print "$lead<span class=\"match\">$match</span>$trail<br />";
4257 "<td class=\"link\">" .
4258 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4260 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4262 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4266 if (defined $extra) {
4268 "<td colspan=\"3\">$extra</td>\n" .
4274 ## ======================================================================
4275 ## ======================================================================
4278 sub git_project_list {
4279 my $order = $input_params{'order'};
4280 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4281 die_error(400, "Unknown order parameter");
4284 my @list = git_get_projects_list();
4286 die_error(404, "No projects found");
4290 if (-f $home_text) {
4291 print "<div class=\"index_include\">\n";
4292 open (my $fd, $home_text);
4297 print $cgi->startform(-method => "get") .
4298 "<p class=\"projsearch\">Search:\n" .
4299 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4301 $cgi->end_form() . "\n";
4302 git_project_list_body(\@list, $order);
4307 my $order = $input_params{'order'};
4308 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4309 die_error(400, "Unknown order parameter");
4312 my @list = git_get_projects_list($project);
4314 die_error(404, "No forks found");
4318 git_print_page_nav('','');
4319 git_print_header_div('summary', "$project forks");
4320 git_project_list_body(\@list, $order);
4324 sub git_project_index {
4325 my @projects = git_get_projects_list($project);
4328 -type => 'text/plain',
4329 -charset => 'utf-8',
4330 -content_disposition => 'inline; filename="index.aux"');
4332 foreach my $pr (@projects) {
4333 if (!exists $pr->{'owner'}) {
4334 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4337 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4338 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4339 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4340 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4344 print "$path $owner\n";
4349 my $descr = git_get_project_description($project) || "none";
4350 my %co = parse_commit("HEAD");
4351 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4352 my $head = $co{'id'};
4354 my $owner = git_get_project_owner($project);
4356 my $refs = git_get_references();
4357 # These get_*_list functions return one more to allow us to see if
4358 # there are more ...
4359 my @taglist = git_get_tags_list(16);
4360 my @headlist = git_get_heads_list(16);
4362 my ($check_forks) = gitweb_check_feature('forks');
4365 @forklist = git_get_projects_list($project);
4369 git_print_page_nav('summary','', $head);
4371 print "<div class=\"title\"> </div>\n";
4372 print "<table class=\"projects_list\">\n" .
4373 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4374 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4375 if (defined $cd{'rfc2822'}) {
4376 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4379 # use per project git URL list in $projectroot/$project/cloneurl
4380 # or make project git URL from git base URL and project name
4381 my $url_tag = "URL";
4382 my @url_list = git_get_project_url_list($project);
4383 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4384 foreach my $git_url (@url_list) {
4385 next unless $git_url;
4386 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4391 my $show_ctags = (gitweb_check_feature('ctags'))[0];
4393 my $ctags = git_get_project_ctags($project);
4394 my $cloud = git_populate_project_tagcloud($ctags);
4395 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4396 print "</td>\n<td>" unless %$ctags;
4397 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4398 print "</td>\n<td>" if %$ctags;
4399 print git_show_project_tagcloud($cloud, 48);
4405 if (-s "$projectroot/$project/README.html") {
4406 if (open my $fd, "$projectroot/$project/README.html") {
4407 print "<div class=\"title\">readme</div>\n" .
4408 "<div class=\"readme\">\n";
4409 print $_ while (<$fd>);
4410 print "\n</div>\n"; # class="readme"
4415 # we need to request one more than 16 (0..15) to check if
4417 my @commitlist = $head ? parse_commits($head, 17) : ();
4419 git_print_header_div('shortlog');
4420 git_shortlog_body(\@commitlist, 0, 15, $refs,
4421 $#commitlist <= 15 ? undef :
4422 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4426 git_print_header_div('tags');
4427 git_tags_body(\@taglist, 0, 15,
4428 $#taglist <= 15 ? undef :
4429 $cgi->a({-href => href(action=>"tags")}, "..."));
4433 git_print_header_div('heads');
4434 git_heads_body(\@headlist, $head, 0, 15,
4435 $#headlist <= 15 ? undef :
4436 $cgi->a({-href => href(action=>"heads")}, "..."));
4440 git_print_header_div('forks');
4441 git_project_list_body(\@forklist, 'age', 0, 15,
4442 $#forklist <= 15 ? undef :
4443 $cgi->a({-href => href(action=>"forks")}, "..."),
4451 my $head = git_get_head_hash($project);
4453 git_print_page_nav('','', $head,undef,$head);
4454 my %tag = parse_tag($hash);
4457 die_error(404, "Unknown tag object");
4460 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4461 print "<div class=\"title_text\">\n" .
4462 "<table class=\"object_header\">\n" .
4464 "<td>object</td>\n" .
4465 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4466 $tag{'object'}) . "</td>\n" .
4467 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4468 $tag{'type'}) . "</td>\n" .
4470 if (defined($tag{'author'})) {
4471 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4472 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4473 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4474 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4477 print "</table>\n\n" .
4479 print "<div class=\"page_body\">";
4480 my $comment = $tag{'comment'};
4481 foreach my $line (@$comment) {
4483 print esc_html($line, -nbsp=>1) . "<br/>\n";
4493 gitweb_check_feature('blame')
4494 or die_error(403, "Blame view not allowed");
4496 die_error(400, "No file name given") unless $file_name;
4497 $hash_base ||= git_get_head_hash($project);
4498 die_error(404, "Couldn't find base commit") unless ($hash_base);
4499 my %co = parse_commit($hash_base)
4500 or die_error(404, "Commit not found");
4501 if (!defined $hash) {
4502 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4503 or die_error(404, "Error looking up file");
4505 $ftype = git_get_type($hash);
4506 if ($ftype !~ "blob") {
4507 die_error(400, "Object is not a blob");
4509 open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4510 $file_name, $hash_base)
4511 or die_error(500, "Open git-blame failed");
4514 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4517 $cgi->a({-href => href(action=>"history", -replay=>1)},
4520 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4522 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4523 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4524 git_print_page_path($file_name, $ftype, $hash_base);
4525 my @rev_color = (qw(light2 dark2));
4526 my $num_colors = scalar(@rev_color);
4527 my $current_color = 0;
4530 <div class="page_body">
4531 <table class="blame">
4532 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4537 last unless defined $_;
4538 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4539 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4540 if (!exists $metainfo{$full_rev}) {
4541 $metainfo{$full_rev} = {};
4543 my $meta = $metainfo{$full_rev};
4546 if (/^(\S+) (.*)$/) {
4552 my $rev = substr($full_rev, 0, 8);
4553 my $author = $meta->{'author'};
4554 my %date = parse_date($meta->{'author-time'},
4555 $meta->{'author-tz'});
4556 my $date = $date{'iso-tz'};
4558 $current_color = ++$current_color % $num_colors;
4560 print "<tr class=\"$rev_color[$current_color]\">\n";
4562 print "<td class=\"sha1\"";
4563 print " title=\"". esc_html($author) . ", $date\"";
4564 print " rowspan=\"$group_size\"" if ($group_size > 1);
4566 print $cgi->a({-href => href(action=>"commit",
4568 file_name=>$file_name)},
4572 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4573 or die_error(500, "Open git-rev-parse failed");
4574 my $parent_commit = <$dd>;
4576 chomp($parent_commit);
4577 my $blamed = href(action => 'blame',
4578 file_name => $meta->{'filename'},
4579 hash_base => $parent_commit);
4580 print "<td class=\"linenr\">";
4581 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4583 -class => "linenr" },
4586 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4592 or print "Reading blob failed\n";
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 @tagslist = git_get_tags_list();
4604 git_tags_body(\@tagslist);
4610 my $head = git_get_head_hash($project);
4612 git_print_page_nav('','', $head,undef,$head);
4613 git_print_header_div('summary', $project);
4615 my @headslist = git_get_heads_list();
4617 git_heads_body(\@headslist, $head);
4622 sub git_blob_plain {
4626 if (!defined $hash) {
4627 if (defined $file_name) {
4628 my $base = $hash_base || git_get_head_hash($project);
4629 $hash = git_get_hash_by_path($base, $file_name, "blob")
4630 or die_error(404, "Cannot find file");
4632 die_error(400, "No file name defined");
4634 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4635 # blobs defined by non-textual hash id's can be cached
4639 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4640 or die_error(500, "Open git-cat-file blob '$hash' failed");
4642 # content-type (can include charset)
4643 $type = blob_contenttype($fd, $file_name, $type);
4645 # "save as" filename, even when no $file_name is given
4646 my $save_as = "$hash";
4647 if (defined $file_name) {
4648 $save_as = $file_name;
4649 } elsif ($type =~ m/^text\//) {
4655 -expires => $expires,
4656 -content_disposition => 'inline; filename="' . $save_as . '"');
4658 binmode STDOUT, ':raw';
4660 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4668 if (!defined $hash) {
4669 if (defined $file_name) {
4670 my $base = $hash_base || git_get_head_hash($project);
4671 $hash = git_get_hash_by_path($base, $file_name, "blob")
4672 or die_error(404, "Cannot find file");
4674 die_error(400, "No file name defined");
4676 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4677 # blobs defined by non-textual hash id's can be cached
4681 my ($have_blame) = gitweb_check_feature('blame');
4682 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4683 or die_error(500, "Couldn't cat $file_name, $hash");
4684 my $mimetype = blob_mimetype($fd, $file_name);
4685 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4687 return git_blob_plain($mimetype);
4689 # we can have blame only for text/* mimetype
4690 $have_blame &&= ($mimetype =~ m!^text/!);
4692 git_header_html(undef, $expires);
4693 my $formats_nav = '';
4694 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4695 if (defined $file_name) {
4698 $cgi->a({-href => href(action=>"blame", -replay=>1)},
4703 $cgi->a({-href => href(action=>"history", -replay=>1)},
4706 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4709 $cgi->a({-href => href(action=>"blob",
4710 hash_base=>"HEAD", file_name=>$file_name)},
4714 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4717 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4718 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4720 print "<div class=\"page_nav\">\n" .
4721 "<br/><br/></div>\n" .
4722 "<div class=\"title\">$hash</div>\n";
4724 git_print_page_path($file_name, "blob", $hash_base);
4725 print "<div class=\"page_body\">\n";
4726 if ($mimetype =~ m!^image/!) {
4727 print qq!<img type="$mimetype"!;
4729 print qq! alt="$file_name" title="$file_name"!;
4732 href(action=>"blob_plain", hash=>$hash,
4733 hash_base=>$hash_base, file_name=>$file_name) .
4737 while (my $line = <$fd>) {
4740 $line = untabify($line);
4741 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4742 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4746 or print "Reading blob failed.\n";
4752 if (!defined $hash_base) {
4753 $hash_base = "HEAD";
4755 if (!defined $hash) {
4756 if (defined $file_name) {
4757 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4762 die_error(404, "No such tree") unless defined($hash);
4764 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4765 or die_error(500, "Open git-ls-tree failed");
4766 my @entries = map { chomp; $_ } <$fd>;
4767 close $fd or die_error(404, "Reading tree failed");
4770 my $refs = git_get_references();
4771 my $ref = format_ref_marker($refs, $hash_base);
4774 my ($have_blame) = gitweb_check_feature('blame');
4775 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4777 if (defined $file_name) {
4779 $cgi->a({-href => href(action=>"history", -replay=>1)},
4781 $cgi->a({-href => href(action=>"tree",
4782 hash_base=>"HEAD", file_name=>$file_name)},
4785 my $snapshot_links = format_snapshot_links($hash);
4786 if (defined $snapshot_links) {
4787 # FIXME: Should be available when we have no hash base as well.
4788 push @views_nav, $snapshot_links;
4790 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4791 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4794 print "<div class=\"page_nav\">\n";
4795 print "<br/><br/></div>\n";
4796 print "<div class=\"title\">$hash</div>\n";
4798 if (defined $file_name) {
4799 $basedir = $file_name;
4800 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4803 git_print_page_path($file_name, 'tree', $hash_base);
4805 print "<div class=\"page_body\">\n";
4806 print "<table class=\"tree\">\n";
4808 # '..' (top directory) link if possible
4809 if (defined $hash_base &&
4810 defined $file_name && $file_name =~ m![^/]+$!) {
4812 print "<tr class=\"dark\">\n";
4814 print "<tr class=\"light\">\n";
4818 my $up = $file_name;
4819 $up =~ s!/?[^/]+$!!;
4820 undef $up unless $up;
4821 # based on git_print_tree_entry
4822 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4823 print '<td class="list">';
4824 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4828 print "<td class=\"link\"></td>\n";
4832 foreach my $line (@entries) {
4833 my %t = parse_ls_tree_line($line, -z => 1);
4836 print "<tr class=\"dark\">\n";
4838 print "<tr class=\"light\">\n";
4842 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4846 print "</table>\n" .
4852 my @supported_fmts = gitweb_check_feature('snapshot');
4853 @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4855 my $format = $input_params{'snapshot_format'};
4856 if (!@supported_fmts) {
4857 die_error(403, "Snapshots not allowed");
4859 # default to first supported snapshot format
4860 $format ||= $supported_fmts[0];
4861 if ($format !~ m/^[a-z0-9]+$/) {
4862 die_error(400, "Invalid snapshot format parameter");
4863 } elsif (!exists($known_snapshot_formats{$format})) {
4864 die_error(400, "Unknown snapshot format");
4865 } elsif (!grep($_ eq $format, @supported_fmts)) {
4866 die_error(403, "Unsupported snapshot format");
4869 if (!defined $hash) {
4870 $hash = git_get_head_hash($project);
4873 my $name = $project;
4874 $name =~ s,([^/])/*\.git$,$1,;
4875 $name = basename($name);
4876 my $filename = to_utf8($name);
4877 $name =~ s/\047/\047\\\047\047/g;
4879 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4880 $cmd = quote_command(
4881 git_cmd(), 'archive',
4882 "--format=$known_snapshot_formats{$format}{'format'}",
4883 "--prefix=$name/", $hash);
4884 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4885 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4889 -type => $known_snapshot_formats{$format}{'type'},
4890 -content_disposition => 'inline; filename="' . "$filename" . '"',
4891 -status => '200 OK');
4893 open my $fd, "-|", $cmd
4894 or die_error(500, "Execute git-archive failed");
4895 binmode STDOUT, ':raw';
4897 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4902 my $head = git_get_head_hash($project);
4903 if (!defined $hash) {
4906 if (!defined $page) {
4909 my $refs = git_get_references();
4911 my @commitlist = parse_commits($hash, 101, (100 * $page));
4913 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4916 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4919 my %co = parse_commit($hash);
4921 git_print_header_div('summary', $project);
4922 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4924 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4925 for (my $i = 0; $i <= $to; $i++) {
4926 my %co = %{$commitlist[$i]};
4928 my $commit = $co{'id'};
4929 my $ref = format_ref_marker($refs, $commit);
4930 my %ad = parse_date($co{'author_epoch'});
4931 git_print_header_div('commit',
4932 "<span class=\"age\">$co{'age_string'}</span>" .
4933 esc_html($co{'title'}) . $ref,
4935 print "<div class=\"title_text\">\n" .
4936 "<div class=\"log_link\">\n" .
4937 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4939 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4941 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4944 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4947 print "<div class=\"log_body\">\n";
4948 git_print_log($co{'comment'}, -final_empty_line=> 1);
4951 if ($#commitlist >= 100) {
4952 print "<div class=\"page_nav\">\n";
4953 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4954 -accesskey => "n", -title => "Alt-n"}, "next");
4961 $hash ||= $hash_base || "HEAD";
4962 my %co = parse_commit($hash)
4963 or die_error(404, "Unknown commit object");
4964 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4965 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4967 my $parent = $co{'parent'};
4968 my $parents = $co{'parents'}; # listref
4970 # we need to prepare $formats_nav before any parameter munging
4972 if (!defined $parent) {
4974 $formats_nav .= '(initial)';
4975 } elsif (@$parents == 1) {
4976 # single parent commit
4979 $cgi->a({-href => href(action=>"commit",
4981 esc_html(substr($parent, 0, 7))) .
4988 $cgi->a({-href => href(action=>"commit",
4990 esc_html(substr($_, 0, 7)));
4995 if (!defined $parent) {
4999 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5001 (@$parents <= 1 ? $parent : '-c'),
5003 or die_error(500, "Open git-diff-tree failed");
5004 @difftree = map { chomp; $_ } <$fd>;
5005 close $fd or die_error(404, "Reading git-diff-tree failed");
5007 # non-textual hash id's can be cached
5009 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5012 my $refs = git_get_references();
5013 my $ref = format_ref_marker($refs, $co{'id'});
5015 git_header_html(undef, $expires);
5016 git_print_page_nav('commit', '',
5017 $hash, $co{'tree'}, $hash,
5020 if (defined $co{'parent'}) {
5021 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5023 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5025 print "<div class=\"title_text\">\n" .
5026 "<table class=\"object_header\">\n";
5027 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
5029 "<td></td><td> $ad{'rfc2822'}";
5030 if ($ad{'hour_local'} < 6) {
5031 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5032 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5034 printf(" (%02d:%02d %s)",
5035 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
5039 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
5040 print "<tr><td></td><td> $cd{'rfc2822'}" .
5041 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5043 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5046 "<td class=\"sha1\">" .
5047 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5048 class => "list"}, $co{'tree'}) .
5050 "<td class=\"link\">" .
5051 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5053 my $snapshot_links = format_snapshot_links($hash);
5054 if (defined $snapshot_links) {
5055 print " | " . $snapshot_links;
5060 foreach my $par (@$parents) {
5063 "<td class=\"sha1\">" .
5064 $cgi->a({-href => href(action=>"commit", hash=>$par),
5065 class => "list"}, $par) .
5067 "<td class=\"link\">" .
5068 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5070 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5077 print "<div class=\"page_body\">\n";
5078 git_print_log($co{'comment'});
5081 git_difftree_body(\@difftree, $hash, @$parents);
5087 # object is defined by:
5088 # - hash or hash_base alone
5089 # - hash_base and file_name
5092 # - hash or hash_base alone
5093 if ($hash || ($hash_base && !defined $file_name)) {
5094 my $object_id = $hash || $hash_base;
5096 open my $fd, "-|", quote_command(
5097 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5098 or die_error(404, "Object does not exist");
5102 or die_error(404, "Object does not exist");
5104 # - hash_base and file_name
5105 } elsif ($hash_base && defined $file_name) {
5106 $file_name =~ s,/+$,,;
5108 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5109 or die_error(404, "Base object does not exist");
5111 # here errors should not hapen
5112 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5113 or die_error(500, "Open git-ls-tree failed");
5117 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5118 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5119 die_error(404, "File or directory for given base does not exist");
5124 die_error(400, "Not enough information to find object");
5127 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5128 hash=>$hash, hash_base=>$hash_base,
5129 file_name=>$file_name),
5130 -status => '302 Found');
5134 my $format = shift || 'html';
5141 # preparing $fd and %diffinfo for git_patchset_body
5143 if (defined $hash_base && defined $hash_parent_base) {
5144 if (defined $file_name) {
5146 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5147 $hash_parent_base, $hash_base,
5148 "--", (defined $file_parent ? $file_parent : ()), $file_name
5149 or die_error(500, "Open git-diff-tree failed");
5150 @difftree = map { chomp; $_ } <$fd>;
5152 or die_error(404, "Reading git-diff-tree failed");
5154 or die_error(404, "Blob diff not found");
5156 } elsif (defined $hash &&
5157 $hash =~ /[0-9a-fA-F]{40}/) {
5158 # try to find filename from $hash
5160 # read filtered raw output
5161 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5162 $hash_parent_base, $hash_base, "--"
5163 or die_error(500, "Open git-diff-tree failed");
5165 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5167 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5168 map { chomp; $_ } <$fd>;
5170 or die_error(404, "Reading git-diff-tree failed");
5172 or die_error(404, "Blob diff not found");
5175 die_error(400, "Missing one of the blob diff parameters");
5178 if (@difftree > 1) {
5179 die_error(400, "Ambiguous blob diff specification");
5182 %diffinfo = parse_difftree_raw_line($difftree[0]);
5183 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5184 $file_name ||= $diffinfo{'to_file'};
5186 $hash_parent ||= $diffinfo{'from_id'};
5187 $hash ||= $diffinfo{'to_id'};
5189 # non-textual hash id's can be cached
5190 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5191 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5196 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5197 '-p', ($format eq 'html' ? "--full-index" : ()),
5198 $hash_parent_base, $hash_base,
5199 "--", (defined $file_parent ? $file_parent : ()), $file_name
5200 or die_error(500, "Open git-diff-tree failed");
5203 # old/legacy style URI
5204 if (!%diffinfo && # if new style URI failed
5205 defined $hash && defined $hash_parent) {
5206 # fake git-diff-tree raw output
5207 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5208 $diffinfo{'from_id'} = $hash_parent;
5209 $diffinfo{'to_id'} = $hash;
5210 if (defined $file_name) {
5211 if (defined $file_parent) {
5212 $diffinfo{'status'} = '2';
5213 $diffinfo{'from_file'} = $file_parent;
5214 $diffinfo{'to_file'} = $file_name;
5215 } else { # assume not renamed
5216 $diffinfo{'status'} = '1';
5217 $diffinfo{'from_file'} = $file_name;
5218 $diffinfo{'to_file'} = $file_name;
5220 } else { # no filename given
5221 $diffinfo{'status'} = '2';
5222 $diffinfo{'from_file'} = $hash_parent;
5223 $diffinfo{'to_file'} = $hash;
5226 # non-textual hash id's can be cached
5227 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5228 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5233 open $fd, "-|", git_cmd(), "diff", @diff_opts,
5234 '-p', ($format eq 'html' ? "--full-index" : ()),
5235 $hash_parent, $hash, "--"
5236 or die_error(500, "Open git-diff failed");
5238 die_error(400, "Missing one of the blob diff parameters")
5243 if ($format eq 'html') {
5245 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5247 git_header_html(undef, $expires);
5248 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5249 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5250 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5252 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5253 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5255 if (defined $file_name) {
5256 git_print_page_path($file_name, "blob", $hash_base);
5258 print "<div class=\"page_path\"></div>\n";
5261 } elsif ($format eq 'plain') {
5263 -type => 'text/plain',
5264 -charset => 'utf-8',
5265 -expires => $expires,
5266 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5268 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5271 die_error(400, "Unknown blobdiff format");
5275 if ($format eq 'html') {
5276 print "<div class=\"page_body\">\n";
5278 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5281 print "</div>\n"; # class="page_body"
5285 while (my $line = <$fd>) {
5286 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5287 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5291 last if $line =~ m!^\+\+\+!;
5299 sub git_blobdiff_plain {
5300 git_blobdiff('plain');
5303 sub git_commitdiff {
5304 my $format = shift || 'html';
5305 $hash ||= $hash_base || "HEAD";
5306 my %co = parse_commit($hash)
5307 or die_error(404, "Unknown commit object");
5309 # choose format for commitdiff for merge
5310 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5311 $hash_parent = '--cc';
5313 # we need to prepare $formats_nav before almost any parameter munging
5315 if ($format eq 'html') {
5317 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5320 if (defined $hash_parent &&
5321 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5322 # commitdiff with two commits given
5323 my $hash_parent_short = $hash_parent;
5324 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5325 $hash_parent_short = substr($hash_parent, 0, 7);
5329 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5330 if ($co{'parents'}[$i] eq $hash_parent) {
5331 $formats_nav .= ' parent ' . ($i+1);
5335 $formats_nav .= ': ' .
5336 $cgi->a({-href => href(action=>"commitdiff",
5337 hash=>$hash_parent)},
5338 esc_html($hash_parent_short)) .
5340 } elsif (!$co{'parent'}) {
5342 $formats_nav .= ' (initial)';
5343 } elsif (scalar @{$co{'parents'}} == 1) {
5344 # single parent commit
5347 $cgi->a({-href => href(action=>"commitdiff",
5348 hash=>$co{'parent'})},
5349 esc_html(substr($co{'parent'}, 0, 7))) .
5353 if ($hash_parent eq '--cc') {
5354 $formats_nav .= ' | ' .
5355 $cgi->a({-href => href(action=>"commitdiff",
5356 hash=>$hash, hash_parent=>'-c')},
5358 } else { # $hash_parent eq '-c'
5359 $formats_nav .= ' | ' .
5360 $cgi->a({-href => href(action=>"commitdiff",
5361 hash=>$hash, hash_parent=>'--cc')},
5367 $cgi->a({-href => href(action=>"commitdiff",
5369 esc_html(substr($_, 0, 7)));
5370 } @{$co{'parents'}} ) .
5375 my $hash_parent_param = $hash_parent;
5376 if (!defined $hash_parent_param) {
5377 # --cc for multiple parents, --root for parentless
5378 $hash_parent_param =
5379 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5385 if ($format eq 'html') {
5386 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5387 "--no-commit-id", "--patch-with-raw", "--full-index",
5388 $hash_parent_param, $hash, "--"
5389 or die_error(500, "Open git-diff-tree failed");
5391 while (my $line = <$fd>) {
5393 # empty line ends raw part of diff-tree output
5395 push @difftree, scalar parse_difftree_raw_line($line);
5398 } elsif ($format eq 'plain') {
5399 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5400 '-p', $hash_parent_param, $hash, "--"
5401 or die_error(500, "Open git-diff-tree failed");
5404 die_error(400, "Unknown commitdiff format");
5407 # non-textual hash id's can be cached
5409 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5413 # write commit message
5414 if ($format eq 'html') {
5415 my $refs = git_get_references();
5416 my $ref = format_ref_marker($refs, $co{'id'});
5418 git_header_html(undef, $expires);
5419 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5420 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5421 git_print_authorship(\%co);
5422 print "<div class=\"page_body\">\n";
5423 if (@{$co{'comment'}} > 1) {
5424 print "<div class=\"log\">\n";
5425 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5426 print "</div>\n"; # class="log"
5429 } elsif ($format eq 'plain') {
5430 my $refs = git_get_references("tags");
5431 my $tagname = git_get_rev_name_tags($hash);
5432 my $filename = basename($project) . "-$hash.patch";
5435 -type => 'text/plain',
5436 -charset => 'utf-8',
5437 -expires => $expires,
5438 -content_disposition => 'inline; filename="' . "$filename" . '"');
5439 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5440 print "From: " . to_utf8($co{'author'}) . "\n";
5441 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5442 print "Subject: " . to_utf8($co{'title'}) . "\n";
5444 print "X-Git-Tag: $tagname\n" if $tagname;
5445 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5447 foreach my $line (@{$co{'comment'}}) {
5448 print to_utf8($line) . "\n";
5454 if ($format eq 'html') {
5455 my $use_parents = !defined $hash_parent ||
5456 $hash_parent eq '-c' || $hash_parent eq '--cc';
5457 git_difftree_body(\@difftree, $hash,
5458 $use_parents ? @{$co{'parents'}} : $hash_parent);
5461 git_patchset_body($fd, \@difftree, $hash,
5462 $use_parents ? @{$co{'parents'}} : $hash_parent);
5464 print "</div>\n"; # class="page_body"
5467 } elsif ($format eq 'plain') {
5471 or print "Reading git-diff-tree failed\n";
5475 sub git_commitdiff_plain {
5476 git_commitdiff('plain');
5480 if (!defined $hash_base) {
5481 $hash_base = git_get_head_hash($project);
5483 if (!defined $page) {
5487 my %co = parse_commit($hash_base)
5488 or die_error(404, "Unknown commit object");
5490 my $refs = git_get_references();
5491 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5493 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5494 $file_name, "--full-history")
5495 or die_error(404, "No such file or directory on given branch");
5497 if (!defined $hash && defined $file_name) {
5498 # some commits could have deleted file in question,
5499 # and not have it in tree, but one of them has to have it
5500 for (my $i = 0; $i <= @commitlist; $i++) {
5501 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5502 last if defined $hash;
5505 if (defined $hash) {
5506 $ftype = git_get_type($hash);
5508 if (!defined $ftype) {
5509 die_error(500, "Unknown type of object");
5512 my $paging_nav = '';
5515 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5516 file_name=>$file_name)},
5518 $paging_nav .= " ⋅ " .
5519 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5520 -accesskey => "p", -title => "Alt-p"}, "prev");
5522 $paging_nav .= "first";
5523 $paging_nav .= " ⋅ prev";
5526 if ($#commitlist >= 100) {
5528 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5529 -accesskey => "n", -title => "Alt-n"}, "next");
5530 $paging_nav .= " ⋅ $next_link";
5532 $paging_nav .= " ⋅ next";
5536 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5537 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5538 git_print_page_path($file_name, $ftype, $hash_base);
5540 git_history_body(\@commitlist, 0, 99,
5541 $refs, $hash_base, $ftype, $next_link);
5547 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5548 if (!defined $searchtext) {
5549 die_error(400, "Text field is empty");
5551 if (!defined $hash) {
5552 $hash = git_get_head_hash($project);
5554 my %co = parse_commit($hash);
5556 die_error(404, "Unknown commit object");
5558 if (!defined $page) {
5562 $searchtype ||= 'commit';
5563 if ($searchtype eq 'pickaxe') {
5564 # pickaxe may take all resources of your box and run for several minutes
5565 # with every query - so decide by yourself how public you make this feature
5566 gitweb_check_feature('pickaxe')
5567 or die_error(403, "Pickaxe is disabled");
5569 if ($searchtype eq 'grep') {
5570 gitweb_check_feature('grep')
5571 or die_error(403, "Grep is disabled");
5576 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5578 if ($searchtype eq 'commit') {
5579 $greptype = "--grep=";
5580 } elsif ($searchtype eq 'author') {
5581 $greptype = "--author=";
5582 } elsif ($searchtype eq 'committer') {
5583 $greptype = "--committer=";
5585 $greptype .= $searchtext;
5586 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5587 $greptype, '--regexp-ignore-case',
5588 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5590 my $paging_nav = '';
5593 $cgi->a({-href => href(action=>"search", hash=>$hash,
5594 searchtext=>$searchtext,
5595 searchtype=>$searchtype)},
5597 $paging_nav .= " ⋅ " .
5598 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5599 -accesskey => "p", -title => "Alt-p"}, "prev");
5601 $paging_nav .= "first";
5602 $paging_nav .= " ⋅ prev";
5605 if ($#commitlist >= 100) {
5607 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5608 -accesskey => "n", -title => "Alt-n"}, "next");
5609 $paging_nav .= " ⋅ $next_link";
5611 $paging_nav .= " ⋅ next";
5614 if ($#commitlist >= 100) {
5617 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5618 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5619 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5622 if ($searchtype eq 'pickaxe') {
5623 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5624 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5626 print "<table class=\"pickaxe search\">\n";
5629 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5630 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5631 ($search_use_regexp ? '--pickaxe-regex' : ());
5634 while (my $line = <$fd>) {
5638 my %set = parse_difftree_raw_line($line);
5639 if (defined $set{'commit'}) {
5640 # finish previous commit
5643 "<td class=\"link\">" .
5644 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5646 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5652 print "<tr class=\"dark\">\n";
5654 print "<tr class=\"light\">\n";
5657 %co = parse_commit($set{'commit'});
5658 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5659 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5660 "<td><i>$author</i></td>\n" .
5662 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5663 -class => "list subject"},
5664 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5665 } elsif (defined $set{'to_id'}) {
5666 next if ($set{'to_id'} =~ m/^0{40}$/);
5668 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5669 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5671 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5677 # finish last commit (warning: repetition!)
5680 "<td class=\"link\">" .
5681 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5683 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5691 if ($searchtype eq 'grep') {
5692 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5693 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5695 print "<table class=\"grep_search\">\n";
5699 open my $fd, "-|", git_cmd(), 'grep', '-n',
5700 $search_use_regexp ? ('-E', '-i') : '-F',
5701 $searchtext, $co{'tree'};
5703 while (my $line = <$fd>) {
5705 my ($file, $lno, $ltext, $binary);
5706 last if ($matches++ > 1000);
5707 if ($line =~ /^Binary file (.+) matches$/) {
5711 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5713 if ($file ne $lastfile) {
5714 $lastfile and print "</td></tr>\n";
5716 print "<tr class=\"dark\">\n";
5718 print "<tr class=\"light\">\n";
5720 print "<td class=\"list\">".
5721 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5722 file_name=>"$file"),
5723 -class => "list"}, esc_path($file));
5724 print "</td><td>\n";
5728 print "<div class=\"binary\">Binary file</div>\n";
5730 $ltext = untabify($ltext);
5731 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5732 $ltext = esc_html($1, -nbsp=>1);
5733 $ltext .= '<span class="match">';
5734 $ltext .= esc_html($2, -nbsp=>1);
5735 $ltext .= '</span>';
5736 $ltext .= esc_html($3, -nbsp=>1);
5738 $ltext = esc_html($ltext, -nbsp=>1);
5740 print "<div class=\"pre\">" .
5741 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5742 file_name=>"$file").'#l'.$lno,
5743 -class => "linenr"}, sprintf('%4i', $lno))
5744 . ' ' . $ltext . "</div>\n";
5748 print "</td></tr>\n";
5749 if ($matches > 1000) {
5750 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5753 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5762 sub git_search_help {
5764 git_print_page_nav('','', $hash,$hash,$hash);
5766 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5767 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5768 the pattern entered is recognized as the POSIX extended
5769 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5772 <dt><b>commit</b></dt>
5773 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5775 my ($have_grep) = gitweb_check_feature('grep');
5778 <dt><b>grep</b></dt>
5779 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5780 a different one) are searched for the given pattern. On large trees, this search can take
5781 a while and put some strain on the server, so please use it with some consideration. Note that
5782 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5783 case-sensitive.</dd>
5787 <dt><b>author</b></dt>
5788 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5789 <dt><b>committer</b></dt>
5790 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5792 my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5793 if ($have_pickaxe) {
5795 <dt><b>pickaxe</b></dt>
5796 <dd>All commits that caused the string to appear or disappear from any file (changes that
5797 added, removed or "modified" the string) will be listed. This search can take a while and
5798 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5799 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5807 my $head = git_get_head_hash($project);
5808 if (!defined $hash) {
5811 if (!defined $page) {
5814 my $refs = git_get_references();
5816 my $commit_hash = $hash;
5817 if (defined $hash_parent) {
5818 $commit_hash = "$hash_parent..$hash";
5820 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5822 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5824 if ($#commitlist >= 100) {
5826 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5827 -accesskey => "n", -title => "Alt-n"}, "next");
5831 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5832 git_print_header_div('summary', $project);
5834 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5839 ## ......................................................................
5840 ## feeds (RSS, Atom; OPML)
5843 my $format = shift || 'atom';
5844 my ($have_blame) = gitweb_check_feature('blame');
5846 # Atom: http://www.atomenabled.org/developers/syndication/
5847 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5848 if ($format ne 'rss' && $format ne 'atom') {
5849 die_error(400, "Unknown web feed format");
5852 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5853 my $head = $hash || 'HEAD';
5854 my @commitlist = parse_commits($head, 150, 0, $file_name);
5858 my $content_type = "application/$format+xml";
5859 if (defined $cgi->http('HTTP_ACCEPT') &&
5860 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5861 # browser (feed reader) prefers text/xml
5862 $content_type = 'text/xml';
5864 if (defined($commitlist[0])) {
5865 %latest_commit = %{$commitlist[0]};
5866 %latest_date = parse_date($latest_commit{'author_epoch'});
5868 -type => $content_type,
5869 -charset => 'utf-8',
5870 -last_modified => $latest_date{'rfc2822'});
5873 -type => $content_type,
5874 -charset => 'utf-8');
5877 # Optimization: skip generating the body if client asks only
5878 # for Last-Modified date.
5879 return if ($cgi->request_method() eq 'HEAD');
5882 my $title = "$site_name - $project/$action";
5883 my $feed_type = 'log';
5884 if (defined $hash) {
5885 $title .= " - '$hash'";
5886 $feed_type = 'branch log';
5887 if (defined $file_name) {
5888 $title .= " :: $file_name";
5889 $feed_type = 'history';
5891 } elsif (defined $file_name) {
5892 $title .= " - $file_name";
5893 $feed_type = 'history';
5895 $title .= " $feed_type";
5896 my $descr = git_get_project_description($project);
5897 if (defined $descr) {
5898 $descr = esc_html($descr);
5900 $descr = "$project " .
5901 ($format eq 'rss' ? 'RSS' : 'Atom') .
5904 my $owner = git_get_project_owner($project);
5905 $owner = esc_html($owner);
5909 if (defined $file_name) {
5910 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5911 } elsif (defined $hash) {
5912 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5914 $alt_url = href(-full=>1, action=>"summary");
5916 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5917 if ($format eq 'rss') {
5919 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5922 print "<title>$title</title>\n" .
5923 "<link>$alt_url</link>\n" .
5924 "<description>$descr</description>\n" .
5925 "<language>en</language>\n";
5926 } elsif ($format eq 'atom') {
5928 <feed xmlns="http://www.w3.org/2005/Atom">
5930 print "<title>$title</title>\n" .
5931 "<subtitle>$descr</subtitle>\n" .
5932 '<link rel="alternate" type="text/html" href="' .
5933 $alt_url . '" />' . "\n" .
5934 '<link rel="self" type="' . $content_type . '" href="' .
5935 $cgi->self_url() . '" />' . "\n" .
5936 "<id>" . href(-full=>1) . "</id>\n" .
5937 # use project owner for feed author
5938 "<author><name>$owner</name></author>\n";
5939 if (defined $favicon) {
5940 print "<icon>" . esc_url($favicon) . "</icon>\n";
5942 if (defined $logo_url) {
5943 # not twice as wide as tall: 72 x 27 pixels
5944 print "<logo>" . esc_url($logo) . "</logo>\n";
5946 if (! %latest_date) {
5947 # dummy date to keep the feed valid until commits trickle in:
5948 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5950 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5955 for (my $i = 0; $i <= $#commitlist; $i++) {
5956 my %co = %{$commitlist[$i]};
5957 my $commit = $co{'id'};
5958 # we read 150, we always show 30 and the ones more recent than 48 hours
5959 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5962 my %cd = parse_date($co{'author_epoch'});
5964 # get list of changed files
5965 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5966 $co{'parent'} || "--root",
5967 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5969 my @difftree = map { chomp; $_ } <$fd>;
5973 # print element (entry, item)
5974 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5975 if ($format eq 'rss') {
5977 "<title>" . esc_html($co{'title'}) . "</title>\n" .
5978 "<author>" . esc_html($co{'author'}) . "</author>\n" .
5979 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5980 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5981 "<link>$co_url</link>\n" .
5982 "<description>" . esc_html($co{'title'}) . "</description>\n" .
5983 "<content:encoded>" .
5985 } elsif ($format eq 'atom') {
5987 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5988 "<updated>$cd{'iso-8601'}</updated>\n" .
5990 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
5991 if ($co{'author_email'}) {
5992 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
5994 print "</author>\n" .
5995 # use committer for contributor
5997 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5998 if ($co{'committer_email'}) {
5999 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6001 print "</contributor>\n" .
6002 "<published>$cd{'iso-8601'}</published>\n" .
6003 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6004 "<id>$co_url</id>\n" .
6005 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6006 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6008 my $comment = $co{'comment'};
6010 foreach my $line (@$comment) {
6011 $line = esc_html($line);
6014 print "</pre><ul>\n";
6015 foreach my $difftree_line (@difftree) {
6016 my %difftree = parse_difftree_raw_line($difftree_line);
6017 next if !$difftree{'from_id'};
6019 my $file = $difftree{'file'} || $difftree{'to_file'};
6023 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6024 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6025 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6026 file_name=>$file, file_parent=>$difftree{'from_file'}),
6027 -title => "diff"}, 'D');
6029 print $cgi->a({-href => href(-full=>1, action=>"blame",
6030 file_name=>$file, hash_base=>$commit),
6031 -title => "blame"}, 'B');
6033 # if this is not a feed of a file history
6034 if (!defined $file_name || $file_name ne $file) {
6035 print $cgi->a({-href => href(-full=>1, action=>"history",
6036 file_name=>$file, hash=>$commit),
6037 -title => "history"}, 'H');
6039 $file = esc_path($file);
6043 if ($format eq 'rss') {
6044 print "</ul>]]>\n" .
6045 "</content:encoded>\n" .
6047 } elsif ($format eq 'atom') {
6048 print "</ul>\n</div>\n" .
6055 if ($format eq 'rss') {
6056 print "</channel>\n</rss>\n";
6057 } elsif ($format eq 'atom') {
6071 my @list = git_get_projects_list();
6073 print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
6075 <?xml version="1.0" encoding="utf-8"?>
6076 <opml version="1.0">
6078 <title>$site_name OPML Export</title>
6081 <outline text="git RSS feeds">
6084 foreach my $pr (@list) {
6086 my $head = git_get_head_hash($proj{'path'});
6087 if (!defined $head) {
6090 $git_dir = "$projectroot/$proj{'path'}";
6091 my %co = parse_commit($head);
6096 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6097 my $rss = "$my_url?p=$proj{'path'};a=rss";
6098 my $html = "$my_url?p=$proj{'path'};a=summary";
6099 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";