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 if (my $path_info = $ENV{"PATH_INFO"}) {
33 $my_url =~ s
,\Q
$path_info\E
$,,;
34 $my_uri =~ s
,\Q
$path_info\E
$,,;
37 # core git executable to use
38 # this can just be "git" if your webserver has a sensible PATH
39 our $GIT = "++GIT_BINDIR++/git";
41 # absolute fs-path which will be prepended to the project path
42 #our $projectroot = "/pub/scm";
43 our $projectroot = "++GITWEB_PROJECTROOT++";
45 # fs traversing limit for getting project list
46 # the number is relative to the projectroot
47 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
49 # target of the home link on top of all pages
50 our $home_link = $my_uri || "/";
52 # string of the home link on top of all pages
53 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
55 # name of your site or organization to appear in page titles
56 # replace this with something more descriptive for clearer bookmarks
57 our $site_name = "++GITWEB_SITENAME++"
58 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
60 # filename of html text to include at top of each page
61 our $site_header = "++GITWEB_SITE_HEADER++";
62 # html text to include at home page
63 our $home_text = "++GITWEB_HOMETEXT++";
64 # filename of html text to include at bottom of each page
65 our $site_footer = "++GITWEB_SITE_FOOTER++";
68 our @stylesheets = ("++GITWEB_CSS++");
69 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
70 our $stylesheet = undef;
72 # URI of GIT logo (72x27 size)
73 our $logo = "++GITWEB_LOGO++";
74 # URI of GIT favicon, assumed to be image/png type
75 our $favicon = "++GITWEB_FAVICON++";
77 # URI and label (title) of GIT logo link
78 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
79 #our $logo_label = "git documentation";
80 our $logo_url = "http://git.or.cz/";
81 our $logo_label = "git homepage";
83 # source of projects list
84 our $projects_list = "++GITWEB_LIST++";
86 # the width (in characters) of the projects list "Description" column
87 our $projects_list_description_width = 25;
89 # default order of projects list
90 # valid values are none, project, descr, owner, and age
91 our $default_projects_order = "project";
93 # show repository only if this file exists
94 # (only effective if this variable evaluates to true)
95 our $export_ok = "++GITWEB_EXPORT_OK++";
97 # only allow viewing of repositories also shown on the overview page
98 our $strict_export = "++GITWEB_STRICT_EXPORT++";
100 # list of git base URLs used for URL to where fetch project from,
101 # i.e. full URL is "$git_base_url/$project"
102 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
104 # default blob_plain mimetype and default charset for text/plain blob
105 our $default_blob_plain_mimetype = 'text/plain';
106 our $default_text_plain_charset = undef;
108 # file to use for guessing MIME types before trying /etc/mime.types
109 # (relative to the current git repository)
110 our $mimetypes_file = undef;
112 # assume this charset if line contains non-UTF-8 characters;
113 # it should be valid encoding (see Encoding::Supported(3pm) for list),
114 # for which encoding all byte sequences are valid, for example
115 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
116 # could be even 'utf-8' for the old behavior)
117 our $fallback_encoding = 'latin1';
119 # rename detection options for git-diff and git-diff-tree
120 # - default is '-M', with the cost proportional to
121 # (number of removed files) * (number of new files).
122 # - more costly is '-C' (which implies '-M'), with the cost proportional to
123 # (number of changed files + number of removed files) * (number of new files)
124 # - even more costly is '-C', '--find-copies-harder' with cost
125 # (number of files in the original tree) * (number of new files)
126 # - one might want to include '-B' option, e.g. '-B', '-M'
127 our @diff_opts = ('-M'); # taken from git_commit
129 # information about snapshot formats that gitweb is capable of serving
130 our %known_snapshot_formats = (
132 # 'display' => display name,
133 # 'type' => mime type,
134 # 'suffix' => filename suffix,
135 # 'format' => --format for git-archive,
136 # 'compressor' => [compressor command and arguments]
137 # (array reference, optional)}
140 'display' => 'tar.gz',
141 'type' => 'application/x-gzip',
142 'suffix' => '.tar.gz',
144 'compressor' => ['gzip']},
147 'display' => 'tar.bz2',
148 'type' => 'application/x-bzip2',
149 'suffix' => '.tar.bz2',
151 'compressor' => ['bzip2']},
155 'type' => 'application/x-zip',
160 # Aliases so we understand old gitweb.snapshot values in repository
162 our %known_snapshot_format_aliases = (
166 # backward compatibility: legacy gitweb config support
167 'x-gzip' => undef, 'gz' => undef,
168 'x-bzip2' => undef, 'bz2' => undef,
169 'x-zip' => undef, '' => undef,
172 # You define site-wide feature defaults here; override them with
173 # $GITWEB_CONFIG as necessary.
176 # 'sub' => feature-sub (subroutine),
177 # 'override' => allow-override (boolean),
178 # 'default' => [ default options...] (array reference)}
180 # if feature is overridable (it means that allow-override has true value),
181 # then feature-sub will be called with default options as parameters;
182 # return value of feature-sub indicates if to enable specified feature
184 # if there is no 'sub' key (no feature-sub), then feature cannot be
187 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
189 # Enable the 'blame' blob view, showing the last commit that modified
190 # each line in the file. This can be very CPU-intensive.
192 # To enable system wide have in $GITWEB_CONFIG
193 # $feature{'blame'}{'default'} = [1];
194 # To have project specific config enable override in $GITWEB_CONFIG
195 # $feature{'blame'}{'override'} = 1;
196 # and in project config gitweb.blame = 0|1;
198 'sub' => \
&feature_blame
,
202 # Enable the 'snapshot' link, providing a compressed archive of any
203 # tree. This can potentially generate high traffic if you have large
206 # Value is a list of formats defined in %known_snapshot_formats that
208 # To disable system wide have in $GITWEB_CONFIG
209 # $feature{'snapshot'}{'default'} = [];
210 # To have project specific config enable override in $GITWEB_CONFIG
211 # $feature{'snapshot'}{'override'} = 1;
212 # and in project config, a comma-separated list of formats or "none"
213 # to disable. Example: gitweb.snapshot = tbz2,zip;
215 'sub' => \
&feature_snapshot
,
217 'default' => ['tgz']},
219 # Enable text search, which will list the commits which match author,
220 # committer or commit text to a given string. Enabled by default.
221 # Project specific override is not supported.
226 # Enable grep search, which will list the files in currently selected
227 # tree containing the given string. Enabled by default. This can be
228 # potentially CPU-intensive, of course.
230 # To enable system wide have in $GITWEB_CONFIG
231 # $feature{'grep'}{'default'} = [1];
232 # To have project specific config enable override in $GITWEB_CONFIG
233 # $feature{'grep'}{'override'} = 1;
234 # and in project config gitweb.grep = 0|1;
239 # Enable the pickaxe search, which will list the commits that modified
240 # a given string in a file. This can be practical and quite faster
241 # alternative to 'blame', but still potentially CPU-intensive.
243 # To enable system wide have in $GITWEB_CONFIG
244 # $feature{'pickaxe'}{'default'} = [1];
245 # To have project specific config enable override in $GITWEB_CONFIG
246 # $feature{'pickaxe'}{'override'} = 1;
247 # and in project config gitweb.pickaxe = 0|1;
249 'sub' => \
&feature_pickaxe
,
253 # Make gitweb use an alternative format of the URLs which can be
254 # more readable and natural-looking: project name is embedded
255 # directly in the path and the query string contains other
256 # auxiliary information. All gitweb installations recognize
257 # URL in either format; this configures in which formats gitweb
260 # To enable system wide have in $GITWEB_CONFIG
261 # $feature{'pathinfo'}{'default'} = [1];
262 # Project specific override is not supported.
264 # Note that you will need to change the default location of CSS,
265 # favicon, logo and possibly other files to an absolute URL. Also,
266 # if gitweb.cgi serves as your indexfile, you will need to force
267 # $my_uri to contain the script name in your $GITWEB_CONFIG.
272 # Make gitweb consider projects in project root subdirectories
273 # to be forks of existing projects. Given project $projname.git,
274 # projects matching $projname/*.git will not be shown in the main
275 # projects list, instead a '+' mark will be added to $projname
276 # there and a 'forks' view will be enabled for the project, listing
277 # all the forks. If project list is taken from a file, forks have
278 # to be listed after the main project.
280 # To enable system wide have in $GITWEB_CONFIG
281 # $feature{'forks'}{'default'} = [1];
282 # Project specific override is not supported.
287 # Insert custom links to the action bar of all project pages.
288 # This enables you mainly to link to third-party scripts integrating
289 # into gitweb; e.g. git-browser for graphical history representation
290 # or custom web-based repository administration interface.
292 # The 'default' value consists of a list of triplets in the form
293 # (label, link, position) where position is the label after which
294 # to inster the link and link is a format string where %n expands
295 # to the project name, %f to the project path within the filesystem,
296 # %h to the current hash (h gitweb parameter) and %b to the current
297 # hash base (hb gitweb parameter).
299 # To enable system wide have in $GITWEB_CONFIG e.g.
300 # $feature{'actions'}{'default'} = [('graphiclog',
301 # '/git-browser/by-commit.html?r=%n', 'summary')];
302 # Project specific override is not supported.
308 sub gitweb_check_feature
{
310 return unless exists $feature{$name};
311 my ($sub, $override, @defaults) = (
312 $feature{$name}{'sub'},
313 $feature{$name}{'override'},
314 @{$feature{$name}{'default'}});
315 if (!$override) { return @defaults; }
317 warn "feature $name is not overrideable";
320 return $sub->(@defaults);
324 my ($val) = git_get_project_config
('blame', '--bool');
326 if ($val eq 'true') {
328 } elsif ($val eq 'false') {
335 sub feature_snapshot
{
338 my ($val) = git_get_project_config
('snapshot');
341 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
348 my ($val) = git_get_project_config
('grep', '--bool');
350 if ($val eq 'true') {
352 } elsif ($val eq 'false') {
359 sub feature_pickaxe
{
360 my ($val) = git_get_project_config
('pickaxe', '--bool');
362 if ($val eq 'true') {
364 } elsif ($val eq 'false') {
371 # checking HEAD file with -e is fragile if the repository was
372 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
374 sub check_head_link
{
376 my $headfile = "$dir/HEAD";
377 return ((-e
$headfile) ||
378 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
381 sub check_export_ok
{
383 return (check_head_link
($dir) &&
384 (!$export_ok || -e
"$dir/$export_ok"));
387 # process alternate names for backward compatibility
388 # filter out unsupported (unknown) snapshot formats
389 sub filter_snapshot_fmts
{
393 exists $known_snapshot_format_aliases{$_} ?
394 $known_snapshot_format_aliases{$_} : $_} @fmts;
395 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
399 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
400 if (-e
$GITWEB_CONFIG) {
403 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
404 do $GITWEB_CONFIG_SYSTEM if -e
$GITWEB_CONFIG_SYSTEM;
407 # version of the core git binary
408 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
410 $projects_list ||= $projectroot;
412 # ======================================================================
413 # input validation and dispatch
414 our $action = $cgi->param('a');
415 if (defined $action) {
416 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
417 die_error
(400, "Invalid action parameter");
421 # parameters which are pathnames
422 our $project = $cgi->param('p');
423 if (defined $project) {
424 if (!validate_pathname
($project) ||
425 !(-d
"$projectroot/$project") ||
426 !check_head_link
("$projectroot/$project") ||
427 ($export_ok && !(-e
"$projectroot/$project/$export_ok")) ||
428 ($strict_export && !project_in_list
($project))) {
430 die_error
(404, "No such project");
434 our $file_name = $cgi->param('f');
435 if (defined $file_name) {
436 if (!validate_pathname
($file_name)) {
437 die_error
(400, "Invalid file parameter");
441 our $file_parent = $cgi->param('fp');
442 if (defined $file_parent) {
443 if (!validate_pathname
($file_parent)) {
444 die_error
(400, "Invalid file parent parameter");
448 # parameters which are refnames
449 our $hash = $cgi->param('h');
451 if (!validate_refname
($hash)) {
452 die_error
(400, "Invalid hash parameter");
456 our $hash_parent = $cgi->param('hp');
457 if (defined $hash_parent) {
458 if (!validate_refname
($hash_parent)) {
459 die_error
(400, "Invalid hash parent parameter");
463 our $hash_base = $cgi->param('hb');
464 if (defined $hash_base) {
465 if (!validate_refname
($hash_base)) {
466 die_error
(400, "Invalid hash base parameter");
470 my %allowed_options = (
471 "--no-merges" => [ qw(rss atom log shortlog history) ],
474 our @extra_options = $cgi->param('opt');
475 if (defined @extra_options) {
476 foreach my $opt (@extra_options) {
477 if (not exists $allowed_options{$opt}) {
478 die_error
(400, "Invalid option parameter");
480 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
481 die_error
(400, "Invalid option parameter for this action");
486 our $hash_parent_base = $cgi->param('hpb');
487 if (defined $hash_parent_base) {
488 if (!validate_refname
($hash_parent_base)) {
489 die_error
(400, "Invalid hash parent base parameter");
494 our $page = $cgi->param('pg');
496 if ($page =~ m/[^0-9]/) {
497 die_error
(400, "Invalid page parameter");
501 our $searchtype = $cgi->param('st');
502 if (defined $searchtype) {
503 if ($searchtype =~ m/[^a-z]/) {
504 die_error
(400, "Invalid searchtype parameter");
508 our $search_use_regexp = $cgi->param('sr');
510 our $searchtext = $cgi->param('s');
512 if (defined $searchtext) {
513 if (length($searchtext) < 2) {
514 die_error
(403, "At least two characters are required for search parameter");
516 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
519 # now read PATH_INFO and use it as alternative to parameters
520 sub evaluate_path_info
{
521 return if defined $project;
522 my $path_info = $ENV{"PATH_INFO"};
523 return if !$path_info;
524 $path_info =~ s
,^/+,,;
525 return if !$path_info;
526 # find which part of PATH_INFO is project
527 $project = $path_info;
529 while ($project && !check_head_link
("$projectroot/$project")) {
530 $project =~ s
,/*[^/]*$,,;
533 $project = validate_pathname
($project);
535 ($export_ok && !-e
"$projectroot/$project/$export_ok") ||
536 ($strict_export && !project_in_list
($project))) {
540 # do not change any parameters if an action is given using the query string
542 $path_info =~ s
,^\Q
$project\E
/*,,;
543 my ($refname, $pathname) = split(/:/, $path_info, 2);
544 if (defined $pathname) {
545 # we got "project.git/branch:filename" or "project.git/branch:dir/"
546 # we could use git_get_type(branch:pathname), but it needs $git_dir
547 $pathname =~ s
,^/+,,;
548 if (!$pathname || substr($pathname, -1) eq "/") {
552 $action ||= "blob_plain";
554 $hash_base ||= validate_refname
($refname);
555 $file_name ||= validate_pathname
($pathname);
556 } elsif (defined $refname) {
557 # we got "project.git/branch"
558 $action ||= "shortlog";
559 $hash ||= validate_refname
($refname);
562 evaluate_path_info
();
564 # path to the current git repository
566 $git_dir = "$projectroot/$project" if $project;
570 "blame" => \
&git_blame
,
571 "blobdiff" => \
&git_blobdiff
,
572 "blobdiff_plain" => \
&git_blobdiff_plain
,
573 "blob" => \
&git_blob
,
574 "blob_plain" => \
&git_blob_plain
,
575 "commitdiff" => \
&git_commitdiff
,
576 "commitdiff_plain" => \
&git_commitdiff_plain
,
577 "commit" => \
&git_commit
,
578 "forks" => \
&git_forks
,
579 "heads" => \
&git_heads
,
580 "history" => \
&git_history
,
583 "atom" => \
&git_atom
,
584 "search" => \
&git_search
,
585 "search_help" => \
&git_search_help
,
586 "shortlog" => \
&git_shortlog
,
587 "summary" => \
&git_summary
,
589 "tags" => \
&git_tags
,
590 "tree" => \
&git_tree
,
591 "snapshot" => \
&git_snapshot
,
592 "object" => \
&git_object
,
593 # those below don't need $project
594 "opml" => \
&git_opml
,
595 "project_list" => \
&git_project_list
,
596 "project_index" => \
&git_project_index
,
599 if (!defined $action) {
601 $action = git_get_type
($hash);
602 } elsif (defined $hash_base && defined $file_name) {
603 $action = git_get_type
("$hash_base:$file_name");
604 } elsif (defined $project) {
607 $action = 'project_list';
610 if (!defined($actions{$action})) {
611 die_error
(400, "Unknown action");
613 if ($action !~ m/^(opml|project_list|project_index)$/ &&
615 die_error
(400, "Project needed");
617 $actions{$action}->();
620 ## ======================================================================
625 # default is to use -absolute url() i.e. $my_uri
626 my $href = $params{-full
} ? $my_url : $my_uri;
628 # XXX: Warning: If you touch this, check the search form for updating,
639 hash_parent_base
=> "hpb",
644 snapshot_format
=> "sf",
645 extra_options
=> "opt",
646 search_use_regexp
=> "sr",
648 my %mapping = @mapping;
650 $params{'project'} = $project unless exists $params{'project'};
652 if ($params{-replay
}) {
653 while (my ($name, $symbol) = each %mapping) {
654 if (!exists $params{$name}) {
655 # to allow for multivalued params we use arrayref form
656 $params{$name} = [ $cgi->param($symbol) ];
661 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
663 # use PATH_INFO for project name
664 $href .= "/".esc_url
($params{'project'}) if defined $params{'project'};
665 delete $params{'project'};
667 # Summary just uses the project path URL
668 if (defined $params{'action'} && $params{'action'} eq 'summary') {
669 delete $params{'action'};
673 # now encode the parameters explicitly
675 for (my $i = 0; $i < @mapping; $i += 2) {
676 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
677 if (defined $params{$name}) {
678 if (ref($params{$name}) eq "ARRAY") {
679 foreach my $par (@{$params{$name}}) {
680 push @result, $symbol . "=" . esc_param
($par);
683 push @result, $symbol . "=" . esc_param
($params{$name});
687 $href .= "?" . join(';', @result) if scalar @result;
693 ## ======================================================================
694 ## validation, quoting/unquoting and escaping
696 sub validate_pathname
{
697 my $input = shift || return undef;
699 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
700 # at the beginning, at the end, and between slashes.
701 # also this catches doubled slashes
702 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
706 if ($input =~ m!\0!) {
712 sub validate_refname
{
713 my $input = shift || return undef;
715 # textual hashes are O.K.
716 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
719 # it must be correct pathname
720 $input = validate_pathname
($input)
722 # restrictions on ref name according to git-check-ref-format
723 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
729 # decode sequences of octets in utf8 into Perl's internal form,
730 # which is utf-8 with utf8 flag set if needed. gitweb writes out
731 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
734 if (utf8
::valid
($str)) {
738 return decode
($fallback_encoding, $str, Encode
::FB_DEFAULT
);
742 # quote unsafe chars, but keep the slash, even when it's not
743 # correct, but quoted slashes look too horrible in bookmarks
746 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf
("%%%02X", ord($1))/eg
;
752 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
755 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf
("%%%02X", ord($1))/eg
;
761 # replace invalid utf8 character with SUBSTITUTION sequence
766 $str = to_utf8
($str);
767 $str = $cgi->escapeHTML($str);
768 if ($opts{'-nbsp'}) {
769 $str =~ s/ / /g;
771 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
775 # quote control characters and escape filename to HTML
780 $str = to_utf8
($str);
781 $str = $cgi->escapeHTML($str);
782 if ($opts{'-nbsp'}) {
783 $str =~ s/ / /g;
785 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
789 # Make control characters "printable", using character escape codes (CEC)
793 my %es = ( # character escape codes, aka escape sequences
794 "\t" => '\t', # tab (HT)
795 "\n" => '\n', # line feed (LF)
796 "\r" => '\r', # carrige return (CR)
797 "\f" => '\f', # form feed (FF)
798 "\b" => '\b', # backspace (BS)
799 "\a" => '\a', # alarm (bell) (BEL)
800 "\e" => '\e', # escape (ESC)
801 "\013" => '\v', # vertical tab (VT)
802 "\000" => '\0', # nul character (NUL)
804 my $chr = ( (exists $es{$cntrl})
806 : sprintf('\%2x', ord($cntrl)) );
807 if ($opts{-nohtml
}) {
810 return "<span class=\"cntrl\">$chr</span>";
814 # Alternatively use unicode control pictures codepoints,
815 # Unicode "printable representation" (PR)
820 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
821 if ($opts{-nohtml
}) {
824 return "<span class=\"cntrl\">$chr</span>";
828 # git may return quoted and escaped filenames
834 my %es = ( # character escape codes, aka escape sequences
835 't' => "\t", # tab (HT, TAB)
836 'n' => "\n", # newline (NL)
837 'r' => "\r", # return (CR)
838 'f' => "\f", # form feed (FF)
839 'b' => "\b", # backspace (BS)
840 'a' => "\a", # alarm (bell) (BEL)
841 'e' => "\e", # escape (ESC)
842 'v' => "\013", # vertical tab (VT)
845 if ($seq =~ m/^[0-7]{1,3}$/) {
846 # octal char sequence
847 return chr(oct($seq));
848 } elsif (exists $es{$seq}) {
849 # C escape sequence, aka character escape code
852 # quoted ordinary character
856 if ($str =~ m/^"(.*)"$/) {
859 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
864 # escape tabs (convert tabs to spaces)
868 while ((my $pos = index($line, "\t")) != -1) {
869 if (my $count = (8 - ($pos % 8))) {
870 my $spaces = ' ' x
$count;
871 $line =~ s/\t/$spaces/;
878 sub project_in_list
{
880 my @list = git_get_projects_list
();
881 return @list && scalar(grep { $_->{'path'} eq $project } @list);
884 ## ----------------------------------------------------------------------
885 ## HTML aware string manipulation
887 # Try to chop given string on a word boundary between position
888 # $len and $len+$add_len. If there is no word boundary there,
889 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
890 # (marking chopped part) would be longer than given string.
894 my $add_len = shift || 10;
895 my $where = shift || 'right'; # 'left' | 'center' | 'right'
897 # Make sure perl knows it is utf8 encoded so we don't
898 # cut in the middle of a utf8 multibyte char.
899 $str = to_utf8
($str);
901 # allow only $len chars, but don't cut a word if it would fit in $add_len
902 # if it doesn't fit, cut it if it's still longer than the dots we would add
903 # remove chopped character entities entirely
905 # when chopping in the middle, distribute $len into left and right part
906 # return early if chopping wouldn't make string shorter
907 if ($where eq 'center') {
908 return $str if ($len + 5 >= length($str)); # filler is length 5
911 return $str if ($len + 4 >= length($str)); # filler is length 4
914 # regexps: ending and beginning with word part up to $add_len
915 my $endre = qr/.{$len}\w{0,$add_len}/;
916 my $begre = qr/\w{0,$add_len}.{$len}/;
918 if ($where eq 'left') {
919 $str =~ m/^(.*?)($begre)$/;
920 my ($lead, $body) = ($1, $2);
921 if (length($lead) > 4) {
922 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
927 } elsif ($where eq 'center') {
928 $str =~ m/^($endre)(.*)$/;
929 my ($left, $str) = ($1, $2);
930 $str =~ m/^(.*?)($begre)$/;
931 my ($mid, $right) = ($1, $2);
932 if (length($mid) > 5) {
933 $left =~ s/&[^;]*$//;
934 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
937 return "$left$mid$right";
940 $str =~ m/^($endre)(.*)$/;
943 if (length($tail) > 4) {
944 $body =~ s/&[^;]*$//;
951 # takes the same arguments as chop_str, but also wraps a <span> around the
952 # result with a title attribute if it does get chopped. Additionally, the
953 # string is HTML-escaped.
954 sub chop_and_escape_str
{
957 my $chopped = chop_str
(@_);
958 if ($chopped eq $str) {
959 return esc_html
($chopped);
961 $str =~ s/([[:cntrl:]])/?/g;
962 return $cgi->span({-title
=>$str}, esc_html
($chopped));
966 ## ----------------------------------------------------------------------
967 ## functions returning short strings
969 # CSS class for given age value (in seconds)
975 } elsif ($age < 60*60*2) {
977 } elsif ($age < 60*60*24*2) {
984 # convert age in seconds to "nn units ago" string
989 if ($age > 60*60*24*365*2) {
990 $age_str = (int $age/60/60/24/365);
991 $age_str .= " years ago";
992 } elsif ($age > 60*60*24*(365/12)*2) {
993 $age_str = int $age/60/60/24/(365/12);
994 $age_str .= " months ago";
995 } elsif ($age > 60*60*24*7*2) {
996 $age_str = int $age/60/60/24/7;
997 $age_str .= " weeks ago";
998 } elsif ($age > 60*60*24*2) {
999 $age_str = int $age/60/60/24;
1000 $age_str .= " days ago";
1001 } elsif ($age > 60*60*2) {
1002 $age_str = int $age/60/60;
1003 $age_str .= " hours ago";
1004 } elsif ($age > 60*2) {
1005 $age_str = int $age/60;
1006 $age_str .= " min ago";
1007 } elsif ($age > 2) {
1008 $age_str = int $age;
1009 $age_str .= " sec ago";
1011 $age_str .= " right now";
1017 S_IFINVALID
=> 0030000,
1018 S_IFGITLINK
=> 0160000,
1021 # submodule/subproject, a commit object reference
1022 sub S_ISGITLINK
($) {
1025 return (($mode & S_IFMT
) == S_IFGITLINK
)
1028 # convert file mode in octal to symbolic file mode string
1030 my $mode = oct shift;
1032 if (S_ISGITLINK
($mode)) {
1033 return 'm---------';
1034 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1035 return 'drwxr-xr-x';
1036 } elsif (S_ISLNK
($mode)) {
1037 return 'lrwxrwxrwx';
1038 } elsif (S_ISREG
($mode)) {
1039 # git cares only about the executable bit
1040 if ($mode & S_IXUSR
) {
1041 return '-rwxr-xr-x';
1043 return '-rw-r--r--';
1046 return '----------';
1050 # convert file mode in octal to file type string
1054 if ($mode !~ m/^[0-7]+$/) {
1060 if (S_ISGITLINK
($mode)) {
1062 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1064 } elsif (S_ISLNK
($mode)) {
1066 } elsif (S_ISREG
($mode)) {
1073 # convert file mode in octal to file type description string
1074 sub file_type_long
{
1077 if ($mode !~ m/^[0-7]+$/) {
1083 if (S_ISGITLINK
($mode)) {
1085 } elsif (S_ISDIR
($mode & S_IFMT
)) {
1087 } elsif (S_ISLNK
($mode)) {
1089 } elsif (S_ISREG
($mode)) {
1090 if ($mode & S_IXUSR
) {
1091 return "executable";
1101 ## ----------------------------------------------------------------------
1102 ## functions returning short HTML fragments, or transforming HTML fragments
1103 ## which don't belong to other sections
1105 # format line of commit message.
1106 sub format_log_line_html
{
1109 $line = esc_html
($line, -nbsp
=>1);
1110 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1113 $cgi->a({-href
=> href
(action
=>"object", hash
=>$hash_text),
1114 -class => "text"}, $hash_text);
1115 $line =~ s/$hash_text/$link/;
1120 # format marker of refs pointing to given object
1122 # the destination action is chosen based on object type and current context:
1123 # - for annotated tags, we choose the tag view unless it's the current view
1124 # already, in which case we go to shortlog view
1125 # - for other refs, we keep the current view if we're in history, shortlog or
1126 # log view, and select shortlog otherwise
1127 sub format_ref_marker
{
1128 my ($refs, $id) = @_;
1131 if (defined $refs->{$id}) {
1132 foreach my $ref (@{$refs->{$id}}) {
1133 # this code exploits the fact that non-lightweight tags are the
1134 # only indirect objects, and that they are the only objects for which
1135 # we want to use tag instead of shortlog as action
1136 my ($type, $name) = qw();
1137 my $indirect = ($ref =~ s/\^\{\}$//);
1138 # e.g. tags/v2.6.11 or heads/next
1139 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1148 $class .= " indirect" if $indirect;
1150 my $dest_action = "shortlog";
1153 $dest_action = "tag" unless $action eq "tag";
1154 } elsif ($action =~ /^(history|(short)?log)$/) {
1155 $dest_action = $action;
1159 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
1162 my $link = $cgi->a({
1164 action
=>$dest_action,
1168 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1174 return ' <span class="refs">'. $markers . '</span>';
1180 # format, perhaps shortened and with markers, title line
1181 sub format_subject_html
{
1182 my ($long, $short, $href, $extra) = @_;
1183 $extra = '' unless defined($extra);
1185 if (length($short) < length($long)) {
1186 return $cgi->a({-href
=> $href, -class => "list subject",
1187 -title
=> to_utf8
($long)},
1188 esc_html
($short) . $extra);
1190 return $cgi->a({-href
=> $href, -class => "list subject"},
1191 esc_html
($long) . $extra);
1195 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1196 sub format_git_diff_header_line
{
1198 my $diffinfo = shift;
1199 my ($from, $to) = @_;
1201 if ($diffinfo->{'nparents'}) {
1203 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1204 if ($to->{'href'}) {
1205 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1206 esc_path
($to->{'file'}));
1207 } else { # file was deleted (no href)
1208 $line .= esc_path
($to->{'file'});
1212 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1213 if ($from->{'href'}) {
1214 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
1215 'a/' . esc_path
($from->{'file'}));
1216 } else { # file was added (no href)
1217 $line .= 'a/' . esc_path
($from->{'file'});
1220 if ($to->{'href'}) {
1221 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
1222 'b/' . esc_path
($to->{'file'}));
1223 } else { # file was deleted
1224 $line .= 'b/' . esc_path
($to->{'file'});
1228 return "<div class=\"diff header\">$line</div>\n";
1231 # format extended diff header line, before patch itself
1232 sub format_extended_diff_header_line
{
1234 my $diffinfo = shift;
1235 my ($from, $to) = @_;
1238 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1239 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1240 esc_path
($from->{'file'}));
1242 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1243 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1244 esc_path
($to->{'file'}));
1246 # match single <mode>
1247 if ($line =~ m/\s(\d{6})$/) {
1248 $line .= '<span class="info"> (' .
1249 file_type_long
($1) .
1253 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1254 # can match only for combined diff
1256 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1257 if ($from->{'href'}[$i]) {
1258 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
1260 substr($diffinfo->{'from_id'}[$i],0,7));
1265 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1268 if ($to->{'href'}) {
1269 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1270 substr($diffinfo->{'to_id'},0,7));
1275 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1276 # can match only for ordinary diff
1277 my ($from_link, $to_link);
1278 if ($from->{'href'}) {
1279 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
1280 substr($diffinfo->{'from_id'},0,7));
1282 $from_link = '0' x
7;
1284 if ($to->{'href'}) {
1285 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
1286 substr($diffinfo->{'to_id'},0,7));
1290 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1291 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1294 return $line . "<br/>\n";
1297 # format from-file/to-file diff header
1298 sub format_diff_from_to_header
{
1299 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1304 #assert($line =~ m/^---/) if DEBUG;
1305 # no extra formatting for "^--- /dev/null"
1306 if (! $diffinfo->{'nparents'}) {
1307 # ordinary (single parent) diff
1308 if ($line =~ m!^--- "?a/!) {
1309 if ($from->{'href'}) {
1311 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
1312 esc_path
($from->{'file'}));
1315 esc_path
($from->{'file'});
1318 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1321 # combined diff (merge commit)
1322 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1323 if ($from->{'href'}[$i]) {
1325 $cgi->a({-href
=>href
(action
=>"blobdiff",
1326 hash_parent
=>$diffinfo->{'from_id'}[$i],
1327 hash_parent_base
=>$parents[$i],
1328 file_parent
=>$from->{'file'}[$i],
1329 hash
=>$diffinfo->{'to_id'},
1331 file_name
=>$to->{'file'}),
1333 -title
=>"diff" . ($i+1)},
1336 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
1337 esc_path
($from->{'file'}[$i]));
1339 $line = '--- /dev/null';
1341 $result .= qq
!<div
class="diff from_file">$line</div
>\n!;
1346 #assert($line =~ m/^\+\+\+/) if DEBUG;
1347 # no extra formatting for "^+++ /dev/null"
1348 if ($line =~ m!^\+\+\+ "?b/!) {
1349 if ($to->{'href'}) {
1351 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
1352 esc_path
($to->{'file'}));
1355 esc_path
($to->{'file'});
1358 $result .= qq
!<div
class="diff to_file">$line</div
>\n!;
1363 # create note for patch simplified by combined diff
1364 sub format_diff_cc_simplified
{
1365 my ($diffinfo, @parents) = @_;
1368 $result .= "<div class=\"diff header\">" .
1370 if (!is_deleted
($diffinfo)) {
1371 $result .= $cgi->a({-href
=> href
(action
=>"blob",
1373 hash
=>$diffinfo->{'to_id'},
1374 file_name
=>$diffinfo->{'to_file'}),
1376 esc_path
($diffinfo->{'to_file'}));
1378 $result .= esc_path
($diffinfo->{'to_file'});
1380 $result .= "</div>\n" . # class="diff header"
1381 "<div class=\"diff nodifferences\">" .
1383 "</div>\n"; # class="diff nodifferences"
1388 # format patch (diff) line (not to be used for diff headers)
1389 sub format_diff_line
{
1391 my ($from, $to) = @_;
1392 my $diff_class = "";
1396 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1398 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1399 if ($line =~ m/^\@{3}/) {
1400 $diff_class = " chunk_header";
1401 } elsif ($line =~ m/^\\/) {
1402 $diff_class = " incomplete";
1403 } elsif ($prefix =~ tr/+/+/) {
1404 $diff_class = " add";
1405 } elsif ($prefix =~ tr/-/-/) {
1406 $diff_class = " rem";
1409 # assume ordinary diff
1410 my $char = substr($line, 0, 1);
1412 $diff_class = " add";
1413 } elsif ($char eq '-') {
1414 $diff_class = " rem";
1415 } elsif ($char eq '@') {
1416 $diff_class = " chunk_header";
1417 } elsif ($char eq "\\") {
1418 $diff_class = " incomplete";
1421 $line = untabify
($line);
1422 if ($from && $to && $line =~ m/^\@{2} /) {
1423 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1424 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1426 $from_lines = 0 unless defined $from_lines;
1427 $to_lines = 0 unless defined $to_lines;
1429 if ($from->{'href'}) {
1430 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
1431 -class=>"list"}, $from_text);
1433 if ($to->{'href'}) {
1434 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1435 -class=>"list"}, $to_text);
1437 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1438 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1439 return "<div class=\"diff$diff_class\">$line</div>\n";
1440 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1441 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1442 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1444 @from_text = split(' ', $ranges);
1445 for (my $i = 0; $i < @from_text; ++$i) {
1446 ($from_start[$i], $from_nlines[$i]) =
1447 (split(',', substr($from_text[$i], 1)), 0);
1450 $to_text = pop @from_text;
1451 $to_start = pop @from_start;
1452 $to_nlines = pop @from_nlines;
1454 $line = "<span class=\"chunk_info\">$prefix ";
1455 for (my $i = 0; $i < @from_text; ++$i) {
1456 if ($from->{'href'}[$i]) {
1457 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
1458 -class=>"list"}, $from_text[$i]);
1460 $line .= $from_text[$i];
1464 if ($to->{'href'}) {
1465 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
1466 -class=>"list"}, $to_text);
1470 $line .= " $prefix</span>" .
1471 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
1472 return "<div class=\"diff$diff_class\">$line</div>\n";
1474 return "<div class=\"diff$diff_class\">" . esc_html
($line, -nbsp
=>1) . "</div>\n";
1477 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1478 # linked. Pass the hash of the tree/commit to snapshot.
1479 sub format_snapshot_links
{
1481 my @snapshot_fmts = gitweb_check_feature
('snapshot');
1482 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1483 my $num_fmts = @snapshot_fmts;
1484 if ($num_fmts > 1) {
1485 # A parenthesized list of links bearing format names.
1486 # e.g. "snapshot (_tar.gz_ _zip_)"
1487 return "snapshot (" . join(' ', map
1494 }, $known_snapshot_formats{$_}{'display'})
1495 , @snapshot_fmts) . ")";
1496 } elsif ($num_fmts == 1) {
1497 # A single "snapshot" link whose tooltip bears the format name.
1499 my ($fmt) = @snapshot_fmts;
1505 snapshot_format
=>$fmt
1507 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
1509 } else { # $num_fmts == 0
1514 ## ......................................................................
1515 ## functions returning values to be passed, perhaps after some
1516 ## transformation, to other functions; e.g. returning arguments to href()
1518 # returns hash to be passed to href to generate gitweb URL
1519 # in -title key it returns description of link
1521 my $format = shift || 'Atom';
1522 my %res = (action
=> lc($format));
1524 # feed links are possible only for project views
1525 return unless (defined $project);
1526 # some views should link to OPML, or to generic project feed,
1527 # or don't have specific feed yet (so they should use generic)
1528 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1531 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1532 # from tag links; this also makes possible to detect branch links
1533 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1534 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1537 # find log type for feed description (title)
1539 if (defined $file_name) {
1540 $type = "history of $file_name";
1541 $type .= "/" if ($action eq 'tree');
1542 $type .= " on '$branch'" if (defined $branch);
1544 $type = "log of $branch" if (defined $branch);
1547 $res{-title
} = $type;
1548 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1549 $res{'file_name'} = $file_name;
1554 ## ----------------------------------------------------------------------
1555 ## git utility subroutines, invoking git commands
1557 # returns path to the core git executable and the --git-dir parameter as list
1559 return $GIT, '--git-dir='.$git_dir;
1562 # quote the given arguments for passing them to the shell
1563 # quote_command("command", "arg 1", "arg with ' and ! characters")
1564 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1565 # Try to avoid using this function wherever possible.
1568 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1571 # get HEAD ref of given project as hash
1572 sub git_get_head_hash
{
1573 my $project = shift;
1574 my $o_git_dir = $git_dir;
1576 $git_dir = "$projectroot/$project";
1577 if (open my $fd, "-|", git_cmd
(), "rev-parse", "--verify", "HEAD") {
1580 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1584 if (defined $o_git_dir) {
1585 $git_dir = $o_git_dir;
1590 # get type of given object
1594 open my $fd, "-|", git_cmd
(), "cat-file", '-t', $hash or return;
1596 close $fd or return;
1601 # repository configuration
1602 our $config_file = '';
1605 # store multiple values for single key as anonymous array reference
1606 # single values stored directly in the hash, not as [ <value> ]
1607 sub hash_set_multi
{
1608 my ($hash, $key, $value) = @_;
1610 if (!exists $hash->{$key}) {
1611 $hash->{$key} = $value;
1612 } elsif (!ref $hash->{$key}) {
1613 $hash->{$key} = [ $hash->{$key}, $value ];
1615 push @{$hash->{$key}}, $value;
1619 # return hash of git project configuration
1620 # optionally limited to some section, e.g. 'gitweb'
1621 sub git_parse_project_config
{
1622 my $section_regexp = shift;
1627 open my $fh, "-|", git_cmd
(), "config", '-z', '-l',
1630 while (my $keyval = <$fh>) {
1632 my ($key, $value) = split(/\n/, $keyval, 2);
1634 hash_set_multi
(\
%config, $key, $value)
1635 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1642 # convert config value to boolean, 'true' or 'false'
1643 # no value, number > 0, 'true' and 'yes' values are true
1644 # rest of values are treated as false (never as error)
1645 sub config_to_bool
{
1648 # strip leading and trailing whitespace
1652 return (!defined $val || # section.key
1653 ($val =~ /^\d+$/ && $val) || # section.key = 1
1654 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1657 # convert config value to simple decimal number
1658 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1659 # to be multiplied by 1024, 1048576, or 1073741824
1663 # strip leading and trailing whitespace
1667 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1669 # unknown unit is treated as 1
1670 return $num * ($unit eq 'g' ? 1073741824 :
1671 $unit eq 'm' ? 1048576 :
1672 $unit eq 'k' ? 1024 : 1);
1677 # convert config value to array reference, if needed
1678 sub config_to_multi
{
1681 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1684 sub git_get_project_config
{
1685 my ($key, $type) = @_;
1688 return unless ($key);
1689 $key =~ s/^gitweb\.//;
1690 return if ($key =~ m/\W/);
1693 if (defined $type) {
1696 unless ($type eq 'bool' || $type eq 'int');
1700 if (!defined $config_file ||
1701 $config_file ne "$git_dir/config") {
1702 %config = git_parse_project_config
('gitweb');
1703 $config_file = "$git_dir/config";
1707 if (!defined $type) {
1708 return $config{"gitweb.$key"};
1709 } elsif ($type eq 'bool') {
1710 # backward compatibility: 'git config --bool' returns true/false
1711 return config_to_bool
($config{"gitweb.$key"}) ? 'true' : 'false';
1712 } elsif ($type eq 'int') {
1713 return config_to_int
($config{"gitweb.$key"});
1715 return $config{"gitweb.$key"};
1718 # get hash of given path at given ref
1719 sub git_get_hash_by_path
{
1721 my $path = shift || return undef;
1726 open my $fd, "-|", git_cmd
(), "ls-tree", $base, "--", $path
1727 or die_error
(500, "Open git-ls-tree failed");
1729 close $fd or return undef;
1731 if (!defined $line) {
1732 # there is no tree or hash given by $path at $base
1736 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1737 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1738 if (defined $type && $type ne $2) {
1739 # type doesn't match
1745 # get path of entry with given hash at given tree-ish (ref)
1746 # used to get 'from' filename for combined diff (merge commit) for renames
1747 sub git_get_path_by_hash
{
1748 my $base = shift || return;
1749 my $hash = shift || return;
1753 open my $fd, "-|", git_cmd
(), "ls-tree", '-r', '-t', '-z', $base
1755 while (my $line = <$fd>) {
1758 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1759 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1760 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1769 ## ......................................................................
1770 ## git utility functions, directly accessing git repository
1772 sub git_get_project_description
{
1775 $git_dir = "$projectroot/$path";
1776 open my $fd, "$git_dir/description"
1777 or return git_get_project_config
('description');
1780 if (defined $descr) {
1786 sub git_get_project_url_list
{
1789 $git_dir = "$projectroot/$path";
1790 open my $fd, "$git_dir/cloneurl"
1791 or return wantarray ?
1792 @{ config_to_multi
(git_get_project_config
('url')) } :
1793 config_to_multi
(git_get_project_config
('url'));
1794 my @git_project_url_list = map { chomp; $_ } <$fd>;
1797 return wantarray ? @git_project_url_list : \
@git_project_url_list;
1800 sub git_get_projects_list
{
1805 $filter =~ s/\.git$//;
1807 my ($check_forks) = gitweb_check_feature
('forks');
1809 if (-d
$projects_list) {
1810 # search in directory
1811 my $dir = $projects_list . ($filter ? "/$filter" : '');
1812 # remove the trailing "/"
1814 my $pfxlen = length("$dir");
1815 my $pfxdepth = ($dir =~ tr!/!!);
1818 follow_fast
=> 1, # follow symbolic links
1819 follow_skip
=> 2, # ignore duplicates
1820 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
1822 # skip project-list toplevel, if we get it.
1823 return if (m!^[/.]$!);
1824 # only directories can be git repositories
1825 return unless (-d
$_);
1826 # don't traverse too deep (Find is super slow on os x)
1827 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1828 $File::Find
::prune
= 1;
1832 my $subdir = substr($File::Find
::name
, $pfxlen + 1);
1833 # we check related file in $projectroot
1834 if ($check_forks and $subdir =~ m
#/.#) {
1835 $File::Find
::prune
= 1;
1836 } elsif (check_export_ok
("$projectroot/$filter/$subdir")) {
1837 push @list, { path
=> ($filter ? "$filter/" : '') . $subdir };
1838 $File::Find
::prune
= 1;
1843 } elsif (-f
$projects_list) {
1844 # read from file(url-encoded):
1845 # 'git%2Fgit.git Linus+Torvalds'
1846 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1847 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1849 open my ($fd), $projects_list or return;
1851 while (my $line = <$fd>) {
1853 my ($path, $owner) = split ' ', $line;
1854 $path = unescape
($path);
1855 $owner = unescape
($owner);
1856 if (!defined $path) {
1859 if ($filter ne '') {
1860 # looking for forks;
1861 my $pfx = substr($path, 0, length($filter));
1862 if ($pfx ne $filter) {
1865 my $sfx = substr($path, length($filter));
1866 if ($sfx !~ /^\/.*\
.git
$/) {
1869 } elsif ($check_forks) {
1871 foreach my $filter (keys %paths) {
1872 # looking for forks;
1873 my $pfx = substr($path, 0, length($filter));
1874 if ($pfx ne $filter) {
1877 my $sfx = substr($path, length($filter));
1878 if ($sfx !~ /^\/.*\
.git
$/) {
1881 # is a fork, don't include it in
1886 if (check_export_ok
("$projectroot/$path")) {
1889 owner
=> to_utf8
($owner),
1892 (my $forks_path = $path) =~ s/\.git$//;
1893 $paths{$forks_path}++;
1901 our $gitweb_project_owner = undef;
1902 sub git_get_project_list_from_file
{
1904 return if (defined $gitweb_project_owner);
1906 $gitweb_project_owner = {};
1907 # read from file (url-encoded):
1908 # 'git%2Fgit.git Linus+Torvalds'
1909 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1910 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1911 if (-f
$projects_list) {
1912 open (my $fd , $projects_list);
1913 while (my $line = <$fd>) {
1915 my ($pr, $ow) = split ' ', $line;
1916 $pr = unescape
($pr);
1917 $ow = unescape
($ow);
1918 $gitweb_project_owner->{$pr} = to_utf8
($ow);
1924 sub git_get_project_owner
{
1925 my $project = shift;
1928 return undef unless $project;
1929 $git_dir = "$projectroot/$project";
1931 if (!defined $gitweb_project_owner) {
1932 git_get_project_list_from_file
();
1935 if (exists $gitweb_project_owner->{$project}) {
1936 $owner = $gitweb_project_owner->{$project};
1938 if (!defined $owner){
1939 $owner = git_get_project_config
('owner');
1941 if (!defined $owner) {
1942 $owner = get_file_owner
("$git_dir");
1948 sub git_get_last_activity
{
1952 $git_dir = "$projectroot/$path";
1953 open($fd, "-|", git_cmd
(), 'for-each-ref',
1954 '--format=%(committer)',
1955 '--sort=-committerdate',
1957 'refs/heads') or return;
1958 my $most_recent = <$fd>;
1959 close $fd or return;
1960 if (defined $most_recent &&
1961 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
1963 my $age = time - $timestamp;
1964 return ($age, age_string
($age));
1966 return (undef, undef);
1969 sub git_get_references
{
1970 my $type = shift || "";
1972 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
1973 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
1974 open my $fd, "-|", git_cmd
(), "show-ref", "--dereference",
1975 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
1978 while (my $line = <$fd>) {
1980 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
1981 if (defined $refs{$1}) {
1982 push @{$refs{$1}}, $2;
1988 close $fd or return;
1992 sub git_get_rev_name_tags
{
1993 my $hash = shift || return undef;
1995 open my $fd, "-|", git_cmd
(), "name-rev", "--tags", $hash
1997 my $name_rev = <$fd>;
2000 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
2003 # catches also '$hash undefined' output
2008 ## ----------------------------------------------------------------------
2009 ## parse to hash functions
2013 my $tz = shift || "-0000";
2016 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2017 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2018 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2019 $date{'hour'} = $hour;
2020 $date{'minute'} = $min;
2021 $date{'mday'} = $mday;
2022 $date{'day'} = $days[$wday];
2023 $date{'month'} = $months[$mon];
2024 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2025 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2026 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2027 $mday, $months[$mon], $hour ,$min;
2028 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2029 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2031 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2032 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2033 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2034 $date{'hour_local'} = $hour;
2035 $date{'minute_local'} = $min;
2036 $date{'tz_local'} = $tz;
2037 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2038 1900+$year, $mon+1, $mday,
2039 $hour, $min, $sec, $tz);
2048 open my $fd, "-|", git_cmd
(), "cat-file", "tag", $tag_id or return;
2049 $tag{'id'} = $tag_id;
2050 while (my $line = <$fd>) {
2052 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2053 $tag{'object'} = $1;
2054 } elsif ($line =~ m/^type (.+)$/) {
2056 } elsif ($line =~ m/^tag (.+)$/) {
2058 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2059 $tag{'author'} = $1;
2062 } elsif ($line =~ m/--BEGIN/) {
2063 push @comment, $line;
2065 } elsif ($line eq "") {
2069 push @comment, <$fd>;
2070 $tag{'comment'} = \
@comment;
2071 close $fd or return;
2072 if (!defined $tag{'name'}) {
2078 sub parse_commit_text
{
2079 my ($commit_text, $withparents) = @_;
2080 my @commit_lines = split '\n', $commit_text;
2083 pop @commit_lines; # Remove '\0'
2085 if (! @commit_lines) {
2089 my $header = shift @commit_lines;
2090 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2093 ($co{'id'}, my @parents) = split ' ', $header;
2094 while (my $line = shift @commit_lines) {
2095 last if $line eq "\n";
2096 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2098 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2100 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2102 $co{'author_epoch'} = $2;
2103 $co{'author_tz'} = $3;
2104 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2105 $co{'author_name'} = $1;
2106 $co{'author_email'} = $2;
2108 $co{'author_name'} = $co{'author'};
2110 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2111 $co{'committer'} = $1;
2112 $co{'committer_epoch'} = $2;
2113 $co{'committer_tz'} = $3;
2114 $co{'committer_name'} = $co{'committer'};
2115 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2116 $co{'committer_name'} = $1;
2117 $co{'committer_email'} = $2;
2119 $co{'committer_name'} = $co{'committer'};
2123 if (!defined $co{'tree'}) {
2126 $co{'parents'} = \
@parents;
2127 $co{'parent'} = $parents[0];
2129 foreach my $title (@commit_lines) {
2132 $co{'title'} = chop_str
($title, 80, 5);
2133 # remove leading stuff of merges to make the interesting part visible
2134 if (length($title) > 50) {
2135 $title =~ s/^Automatic //;
2136 $title =~ s/^merge (of|with) /Merge ... /i;
2137 if (length($title) > 50) {
2138 $title =~ s/(http|rsync):\/\///;
2140 if (length($title) > 50) {
2141 $title =~ s/(master|www|rsync)\.//;
2143 if (length($title) > 50) {
2144 $title =~ s/kernel.org:?//;
2146 if (length($title) > 50) {
2147 $title =~ s/\/pub\/scm//;
2150 $co{'title_short'} = chop_str
($title, 50, 5);
2154 if (! defined $co{'title'} || $co{'title'} eq "") {
2155 $co{'title'} = $co{'title_short'} = '(no commit message)';
2157 # remove added spaces
2158 foreach my $line (@commit_lines) {
2161 $co{'comment'} = \
@commit_lines;
2163 my $age = time - $co{'committer_epoch'};
2165 $co{'age_string'} = age_string
($age);
2166 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2167 if ($age > 60*60*24*7*2) {
2168 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2169 $co{'age_string_age'} = $co{'age_string'};
2171 $co{'age_string_date'} = $co{'age_string'};
2172 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2178 my ($commit_id) = @_;
2183 open my $fd, "-|", git_cmd
(), "rev-list",
2189 or die_error
(500, "Open git-rev-list failed");
2190 %co = parse_commit_text
(<$fd>, 1);
2197 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2205 open my $fd, "-|", git_cmd
(), "rev-list",
2208 ("--max-count=" . $maxcount),
2209 ("--skip=" . $skip),
2213 ($filename ? ($filename) : ())
2214 or die_error
(500, "Open git-rev-list failed");
2215 while (my $line = <$fd>) {
2216 my %co = parse_commit_text
($line);
2221 return wantarray ? @cos : \
@cos;
2224 # parse line of git-diff-tree "raw" output
2225 sub parse_difftree_raw_line
{
2229 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2230 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2231 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2232 $res{'from_mode'} = $1;
2233 $res{'to_mode'} = $2;
2234 $res{'from_id'} = $3;
2236 $res{'status'} = $5;
2237 $res{'similarity'} = $6;
2238 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2239 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
2241 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
2244 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2245 # combined diff (for merge commit)
2246 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2247 $res{'nparents'} = length($1);
2248 $res{'from_mode'} = [ split(' ', $2) ];
2249 $res{'to_mode'} = pop @{$res{'from_mode'}};
2250 $res{'from_id'} = [ split(' ', $3) ];
2251 $res{'to_id'} = pop @{$res{'from_id'}};
2252 $res{'status'} = [ split('', $4) ];
2253 $res{'to_file'} = unquote
($5);
2255 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2256 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2257 $res{'commit'} = $1;
2260 return wantarray ? %res : \
%res;
2263 # wrapper: return parsed line of git-diff-tree "raw" output
2264 # (the argument might be raw line, or parsed info)
2265 sub parsed_difftree_line
{
2266 my $line_or_ref = shift;
2268 if (ref($line_or_ref) eq "HASH") {
2269 # pre-parsed (or generated by hand)
2270 return $line_or_ref;
2272 return parse_difftree_raw_line
($line_or_ref);
2276 # parse line of git-ls-tree output
2277 sub parse_ls_tree_line
($;%) {
2282 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2283 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2291 $res{'name'} = unquote
($4);
2294 return wantarray ? %res : \
%res;
2297 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2298 sub parse_from_to_diffinfo
{
2299 my ($diffinfo, $from, $to, @parents) = @_;
2301 if ($diffinfo->{'nparents'}) {
2303 $from->{'file'} = [];
2304 $from->{'href'} = [];
2305 fill_from_file_info
($diffinfo, @parents)
2306 unless exists $diffinfo->{'from_file'};
2307 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2308 $from->{'file'}[$i] =
2309 defined $diffinfo->{'from_file'}[$i] ?
2310 $diffinfo->{'from_file'}[$i] :
2311 $diffinfo->{'to_file'};
2312 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2313 $from->{'href'}[$i] = href
(action
=>"blob",
2314 hash_base
=>$parents[$i],
2315 hash
=>$diffinfo->{'from_id'}[$i],
2316 file_name
=>$from->{'file'}[$i]);
2318 $from->{'href'}[$i] = undef;
2322 # ordinary (not combined) diff
2323 $from->{'file'} = $diffinfo->{'from_file'};
2324 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2325 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
2326 hash
=>$diffinfo->{'from_id'},
2327 file_name
=>$from->{'file'});
2329 delete $from->{'href'};
2333 $to->{'file'} = $diffinfo->{'to_file'};
2334 if (!is_deleted
($diffinfo)) { # file exists in result
2335 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
2336 hash
=>$diffinfo->{'to_id'},
2337 file_name
=>$to->{'file'});
2339 delete $to->{'href'};
2343 ## ......................................................................
2344 ## parse to array of hashes functions
2346 sub git_get_heads_list
{
2350 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2351 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2352 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2355 while (my $line = <$fd>) {
2359 my ($refinfo, $committerinfo) = split(/\0/, $line);
2360 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2361 my ($committer, $epoch, $tz) =
2362 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2363 $ref_item{'fullname'} = $name;
2364 $name =~ s!^refs/heads/!!;
2366 $ref_item{'name'} = $name;
2367 $ref_item{'id'} = $hash;
2368 $ref_item{'title'} = $title || '(no commit message)';
2369 $ref_item{'epoch'} = $epoch;
2371 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2373 $ref_item{'age'} = "unknown";
2376 push @headslist, \
%ref_item;
2380 return wantarray ? @headslist : \
@headslist;
2383 sub git_get_tags_list
{
2387 open my $fd, '-|', git_cmd
(), 'for-each-ref',
2388 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2389 '--format=%(objectname) %(objecttype) %(refname) '.
2390 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2393 while (my $line = <$fd>) {
2397 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2398 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2399 my ($creator, $epoch, $tz) =
2400 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2401 $ref_item{'fullname'} = $name;
2402 $name =~ s!^refs/tags/!!;
2404 $ref_item{'type'} = $type;
2405 $ref_item{'id'} = $id;
2406 $ref_item{'name'} = $name;
2407 if ($type eq "tag") {
2408 $ref_item{'subject'} = $title;
2409 $ref_item{'reftype'} = $reftype;
2410 $ref_item{'refid'} = $refid;
2412 $ref_item{'reftype'} = $type;
2413 $ref_item{'refid'} = $id;
2416 if ($type eq "tag" || $type eq "commit") {
2417 $ref_item{'epoch'} = $epoch;
2419 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
2421 $ref_item{'age'} = "unknown";
2425 push @tagslist, \
%ref_item;
2429 return wantarray ? @tagslist : \
@tagslist;
2432 ## ----------------------------------------------------------------------
2433 ## filesystem-related functions
2435 sub get_file_owner
{
2438 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2439 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2440 if (!defined $gcos) {
2444 $owner =~ s/[,;].*$//;
2445 return to_utf8
($owner);
2448 ## ......................................................................
2449 ## mimetype related functions
2451 sub mimetype_guess_file
{
2452 my $filename = shift;
2453 my $mimemap = shift;
2454 -r
$mimemap or return undef;
2457 open(MIME
, $mimemap) or return undef;
2459 next if m/^#/; # skip comments
2460 my ($mime, $exts) = split(/\t+/);
2461 if (defined $exts) {
2462 my @exts = split(/\s+/, $exts);
2463 foreach my $ext (@exts) {
2464 $mimemap{$ext} = $mime;
2470 $filename =~ /\.([^.]*)$/;
2471 return $mimemap{$1};
2474 sub mimetype_guess
{
2475 my $filename = shift;
2477 $filename =~ /\./ or return undef;
2479 if ($mimetypes_file) {
2480 my $file = $mimetypes_file;
2481 if ($file !~ m!^/!) { # if it is relative path
2482 # it is relative to project
2483 $file = "$projectroot/$project/$file";
2485 $mime = mimetype_guess_file
($filename, $file);
2487 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types');
2493 my $filename = shift;
2496 my $mime = mimetype_guess
($filename);
2497 $mime and return $mime;
2501 return $default_blob_plain_mimetype unless $fd;
2504 return 'text/plain';
2505 } elsif (! $filename) {
2506 return 'application/octet-stream';
2507 } elsif ($filename =~ m/\.png$/i) {
2509 } elsif ($filename =~ m/\.gif$/i) {
2511 } elsif ($filename =~ m/\.jpe?g$/i) {
2512 return 'image/jpeg';
2514 return 'application/octet-stream';
2518 sub blob_contenttype
{
2519 my ($fd, $file_name, $type) = @_;
2521 $type ||= blob_mimetype
($fd, $file_name);
2522 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2523 $type .= "; charset=$default_text_plain_charset";
2529 ## ======================================================================
2530 ## functions printing HTML: header, footer, error page
2532 sub git_header_html
{
2533 my $status = shift || "200 OK";
2534 my $expires = shift;
2536 my $title = "$site_name";
2537 if (defined $project) {
2538 $title .= " - " . to_utf8
($project);
2539 if (defined $action) {
2540 $title .= "/$action";
2541 if (defined $file_name) {
2542 $title .= " - " . esc_path
($file_name);
2543 if ($action eq "tree" && $file_name !~ m
|/$|) {
2550 # require explicit support from the UA if we are to send the page as
2551 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2552 # we have to do this because MSIE sometimes globs '*/*', pretending to
2553 # support xhtml+xml but choking when it gets what it asked for.
2554 if (defined $cgi->http('HTTP_ACCEPT') &&
2555 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\
+xml
(,|;|\s
|$)/ &&
2556 $cgi->Accept('application/xhtml+xml') != 0) {
2557 $content_type = 'application/xhtml+xml';
2559 $content_type = 'text/html';
2561 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
2562 -status
=> $status, -expires
=> $expires);
2563 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2565 <?xml version="1.0" encoding="utf-8"?>
2566 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2567 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2568 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2569 <!-- git core binaries version $git_version -->
2571 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2572 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2573 <meta name="robots" content="index, nofollow"/>
2574 <title>$title</title>
2576 # print out each stylesheet that exist
2577 if (defined $stylesheet) {
2578 #provides backwards capability for those people who define style sheet in a config file
2579 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2581 foreach my $stylesheet (@stylesheets) {
2582 next unless $stylesheet;
2583 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2586 if (defined $project) {
2587 my %href_params = get_feed_info
();
2588 if (!exists $href_params{'-title'}) {
2589 $href_params{'-title'} = 'log';
2592 foreach my $format qw(RSS Atom) {
2593 my $type = lc($format);
2595 '-rel' => 'alternate',
2596 '-title' => "$project - $href_params{'-title'} - $format feed",
2597 '-type' => "application/$type+xml"
2600 $href_params{'action'} = $type;
2601 $link_attr{'-href'} = href
(%href_params);
2603 "rel=\"$link_attr{'-rel'}\" ".
2604 "title=\"$link_attr{'-title'}\" ".
2605 "href=\"$link_attr{'-href'}\" ".
2606 "type=\"$link_attr{'-type'}\" ".
2609 $href_params{'extra_options'} = '--no-merges';
2610 $link_attr{'-href'} = href
(%href_params);
2611 $link_attr{'-title'} .= ' (no merges)';
2613 "rel=\"$link_attr{'-rel'}\" ".
2614 "title=\"$link_attr{'-title'}\" ".
2615 "href=\"$link_attr{'-href'}\" ".
2616 "type=\"$link_attr{'-type'}\" ".
2621 printf('<link rel="alternate" title="%s projects list" '.
2622 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2623 $site_name, href
(project
=>undef, action
=>"project_index"));
2624 printf('<link rel="alternate" title="%s projects feeds" '.
2625 'href="%s" type="text/x-opml" />'."\n",
2626 $site_name, href
(project
=>undef, action
=>"opml"));
2628 if (defined $favicon) {
2629 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2635 if (-f
$site_header) {
2636 open (my $fd, $site_header);
2641 print "<div class=\"page_header\">\n" .
2642 $cgi->a({-href
=> esc_url
($logo_url),
2643 -title
=> $logo_label},
2644 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2645 print $cgi->a({-href
=> esc_url
($home_link)}, $home_link_str) . " / ";
2646 if (defined $project) {
2647 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($project));
2648 if (defined $action) {
2655 my ($have_search) = gitweb_check_feature
('search');
2656 if (defined $project && $have_search) {
2657 if (!defined $searchtext) {
2661 if (defined $hash_base) {
2662 $search_hash = $hash_base;
2663 } elsif (defined $hash) {
2664 $search_hash = $hash;
2666 $search_hash = "HEAD";
2668 my $action = $my_uri;
2669 my ($use_pathinfo) = gitweb_check_feature
('pathinfo');
2670 if ($use_pathinfo) {
2671 $action .= "/".esc_url
($project);
2673 print $cgi->startform(-method => "get", -action
=> $action) .
2674 "<div class=\"search\">\n" .
2676 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
2677 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
2678 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
2679 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
2680 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2681 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help")}, "?")) .
2683 $cgi->textfield(-name
=> "s", -value
=> $searchtext) . "\n" .
2684 "<span title=\"Extended regular expression\">" .
2685 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
2686 -checked
=> $search_use_regexp) .
2689 $cgi->end_form() . "\n";
2693 sub git_footer_html
{
2694 my $feed_class = 'rss_logo';
2696 print "<div class=\"page_footer\">\n";
2697 if (defined $project) {
2698 my $descr = git_get_project_description
($project);
2699 if (defined $descr) {
2700 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
2703 my %href_params = get_feed_info
();
2704 if (!%href_params) {
2705 $feed_class .= ' generic';
2707 $href_params{'-title'} ||= 'log';
2709 foreach my $format qw(RSS Atom) {
2710 $href_params{'action'} = lc($format);
2711 print $cgi->a({-href
=> href
(%href_params),
2712 -title
=> "$href_params{'-title'} $format feed",
2713 -class => $feed_class}, $format)."\n";
2717 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml"),
2718 -class => $feed_class}, "OPML") . " ";
2719 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index"),
2720 -class => $feed_class}, "TXT") . "\n";
2722 print "</div>\n"; # class="page_footer"
2724 if (-f
$site_footer) {
2725 open (my $fd, $site_footer);
2734 # die_error(<http_status_code>, <error_message>)
2735 # Example: die_error(404, 'Hash not found')
2736 # By convention, use the following status codes (as defined in RFC 2616):
2737 # 400: Invalid or missing CGI parameters, or
2738 # requested object exists but has wrong type.
2739 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2740 # this server or project.
2741 # 404: Requested object/revision/project doesn't exist.
2742 # 500: The server isn't configured properly, or
2743 # an internal error occurred (e.g. failed assertions caused by bugs), or
2744 # an unknown error occurred (e.g. the git binary died unexpectedly).
2746 my $status = shift || 500;
2747 my $error = shift || "Internal server error";
2749 my %http_responses = (400 => '400 Bad Request',
2750 403 => '403 Forbidden',
2751 404 => '404 Not Found',
2752 500 => '500 Internal Server Error');
2753 git_header_html
($http_responses{$status});
2755 <div class="page_body">
2765 ## ----------------------------------------------------------------------
2766 ## functions printing or outputting HTML: navigation
2768 sub git_print_page_nav
{
2769 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2770 $extra = '' if !defined $extra; # pager or formats
2772 my @navs = qw(summary shortlog log commit commitdiff tree);
2774 @navs = grep { $_ ne $suppress } @navs;
2777 my %arg = map { $_ => {action
=>$_} } @navs;
2778 if (defined $head) {
2779 for (qw(commit commitdiff)) {
2780 $arg{$_}{'hash'} = $head;
2782 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2783 for (qw(shortlog log)) {
2784 $arg{$_}{'hash'} = $head;
2789 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2790 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2792 my @actions = gitweb_check_feature
('actions');
2794 my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
2795 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2797 $link =~ s
#%n#$project#g;
2798 $link =~ s
#%f#$git_dir#g;
2799 $treehead ? $link =~ s
#%h#$treehead#g : $link =~ s#%h##g;
2800 $treebase ? $link =~ s
#%b#$treebase#g : $link =~ s#%b##g;
2801 $arg{$label}{'_href'} = $link;
2804 print "<div class=\"page_nav\">\n" .
2806 map { $_ eq $current ?
2807 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ? $arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
2809 print "<br/>\n$extra<br/>\n" .
2813 sub format_paging_nav
{
2814 my ($action, $hash, $head, $page, $has_next_link) = @_;
2818 if ($hash ne $head || $page) {
2819 $paging_nav .= $cgi->a({-href
=> href
(action
=>$action)}, "HEAD");
2821 $paging_nav .= "HEAD";
2825 $paging_nav .= " ⋅ " .
2826 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
2827 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
2829 $paging_nav .= " ⋅ prev";
2832 if ($has_next_link) {
2833 $paging_nav .= " ⋅ " .
2834 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
2835 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
2837 $paging_nav .= " ⋅ next";
2843 ## ......................................................................
2844 ## functions printing or outputting HTML: div
2846 sub git_print_header_div
{
2847 my ($action, $title, $hash, $hash_base) = @_;
2850 $args{'action'} = $action;
2851 $args{'hash'} = $hash if $hash;
2852 $args{'hash_base'} = $hash_base if $hash_base;
2854 print "<div class=\"header\">\n" .
2855 $cgi->a({-href
=> href
(%args), -class => "title"},
2856 $title ? $title : $action) .
2860 #sub git_print_authorship (\%) {
2861 sub git_print_authorship
{
2864 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
2865 print "<div class=\"author_date\">" .
2866 esc_html
($co->{'author_name'}) .
2868 if ($ad{'hour_local'} < 6) {
2869 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2870 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2872 printf(" (%02d:%02d %s)",
2873 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2878 sub git_print_page_path
{
2884 print "<div class=\"page_path\">";
2885 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
2886 -title
=> 'tree root'}, to_utf8
("[$project]"));
2888 if (defined $name) {
2889 my @dirname = split '/', $name;
2890 my $basename = pop @dirname;
2893 foreach my $dir (@dirname) {
2894 $fullname .= ($fullname ? '/' : '') . $dir;
2895 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
2897 -title
=> $fullname}, esc_path
($dir));
2900 if (defined $type && $type eq 'blob') {
2901 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
2903 -title
=> $name}, esc_path
($basename));
2904 } elsif (defined $type && $type eq 'tree') {
2905 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
2907 -title
=> $name}, esc_path
($basename));
2910 print esc_path
($basename);
2913 print "<br/></div>\n";
2916 # sub git_print_log (\@;%) {
2917 sub git_print_log
($;%) {
2921 if ($opts{'-remove_title'}) {
2922 # remove title, i.e. first line of log
2925 # remove leading empty lines
2926 while (defined $log->[0] && $log->[0] eq "") {
2933 foreach my $line (@$log) {
2934 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2937 if (! $opts{'-remove_signoff'}) {
2938 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
2941 # remove signoff lines
2948 # print only one empty line
2949 # do not print empty line after signoff
2951 next if ($empty || $signoff);
2957 print format_log_line_html
($line) . "<br/>\n";
2960 if ($opts{'-final_empty_line'}) {
2961 # end with single empty line
2962 print "<br/>\n" unless $empty;
2966 # return link target (what link points to)
2967 sub git_get_link_target
{
2972 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
2976 $link_target = <$fd>;
2981 return $link_target;
2984 # given link target, and the directory (basedir) the link is in,
2985 # return target of link relative to top directory (top tree);
2986 # return undef if it is not possible (including absolute links).
2987 sub normalize_link_target
{
2988 my ($link_target, $basedir, $hash_base) = @_;
2990 # we can normalize symlink target only if $hash_base is provided
2991 return unless $hash_base;
2993 # absolute symlinks (beginning with '/') cannot be normalized
2994 return if (substr($link_target, 0, 1) eq '/');
2996 # normalize link target to path from top (root) tree (dir)
2999 $path = $basedir . '/' . $link_target;
3001 # we are in top (root) tree (dir)
3002 $path = $link_target;
3005 # remove //, /./, and /../
3007 foreach my $part (split('/', $path)) {
3008 # discard '.' and ''
3009 next if (!$part || $part eq '.');
3011 if ($part eq '..') {
3015 # link leads outside repository (outside top dir)
3019 push @path_parts, $part;
3022 $path = join('/', @path_parts);
3027 # print tree entry (row of git_tree), but without encompassing <tr> element
3028 sub git_print_tree_entry
{
3029 my ($t, $basedir, $hash_base, $have_blame) = @_;
3032 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3034 # The format of a table row is: mode list link. Where mode is
3035 # the mode of the entry, list is the name of the entry, an href,
3036 # and link is the action links of the entry.
3038 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
3039 if ($t->{'type'} eq "blob") {
3040 print "<td class=\"list\">" .
3041 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3042 file_name
=>"$basedir$t->{'name'}", %base_key),
3043 -class => "list"}, esc_path
($t->{'name'}));
3044 if (S_ISLNK
(oct $t->{'mode'})) {
3045 my $link_target = git_get_link_target
($t->{'hash'});
3047 my $norm_target = normalize_link_target
($link_target, $basedir, $hash_base);
3048 if (defined $norm_target) {
3050 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
3051 file_name
=>$norm_target),
3052 -title
=> $norm_target}, esc_path
($link_target));
3054 print " -> " . esc_path
($link_target);
3059 print "<td class=\"link\">";
3060 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
3061 file_name
=>"$basedir$t->{'name'}", %base_key)},
3065 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
3066 file_name
=>"$basedir$t->{'name'}", %base_key)},
3069 if (defined $hash_base) {
3071 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3072 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
3076 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
3077 file_name
=>"$basedir$t->{'name'}")},
3081 } elsif ($t->{'type'} eq "tree") {
3082 print "<td class=\"list\">";
3083 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3084 file_name
=>"$basedir$t->{'name'}", %base_key)},
3085 esc_path
($t->{'name'}));
3087 print "<td class=\"link\">";
3088 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
3089 file_name
=>"$basedir$t->{'name'}", %base_key)},
3091 if (defined $hash_base) {
3093 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
3094 file_name
=>"$basedir$t->{'name'}")},
3099 # unknown object: we can only present history for it
3100 # (this includes 'commit' object, i.e. submodule support)
3101 print "<td class=\"list\">" .
3102 esc_path
($t->{'name'}) .
3104 print "<td class=\"link\">";
3105 if (defined $hash_base) {
3106 print $cgi->a({-href
=> href
(action
=>"history",
3107 hash_base
=>$hash_base,
3108 file_name
=>"$basedir$t->{'name'}")},
3115 ## ......................................................................
3116 ## functions printing large fragments of HTML
3118 # get pre-image filenames for merge (combined) diff
3119 sub fill_from_file_info
{
3120 my ($diff, @parents) = @_;
3122 $diff->{'from_file'} = [ ];
3123 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3124 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3125 if ($diff->{'status'}[$i] eq 'R' ||
3126 $diff->{'status'}[$i] eq 'C') {
3127 $diff->{'from_file'}[$i] =
3128 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
3135 # is current raw difftree line of file deletion
3137 my $diffinfo = shift;
3139 return $diffinfo->{'to_id'} eq ('0' x
40);
3142 # does patch correspond to [previous] difftree raw line
3143 # $diffinfo - hashref of parsed raw diff format
3144 # $patchinfo - hashref of parsed patch diff format
3145 # (the same keys as in $diffinfo)
3146 sub is_patch_split
{
3147 my ($diffinfo, $patchinfo) = @_;
3149 return defined $diffinfo && defined $patchinfo
3150 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3154 sub git_difftree_body
{
3155 my ($difftree, $hash, @parents) = @_;
3156 my ($parent) = $parents[0];
3157 my ($have_blame) = gitweb_check_feature
('blame');
3158 print "<div class=\"list_head\">\n";
3159 if ($#{$difftree} > 10) {
3160 print(($#{$difftree} + 1) . " files changed:\n");
3164 print "<table class=\"" .
3165 (@parents > 1 ? "combined " : "") .
3168 # header only for combined diff in 'commitdiff' view
3169 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3172 print "<thead><tr>\n" .
3173 "<th></th><th></th>\n"; # filename, patchN link
3174 for (my $i = 0; $i < @parents; $i++) {
3175 my $par = $parents[$i];
3177 $cgi->a({-href
=> href
(action
=>"commitdiff",
3178 hash
=>$hash, hash_parent
=>$par),
3179 -title
=> 'commitdiff to parent number ' .
3180 ($i+1) . ': ' . substr($par,0,7)},
3184 print "</tr></thead>\n<tbody>\n";
3189 foreach my $line (@{$difftree}) {
3190 my $diff = parsed_difftree_line
($line);
3193 print "<tr class=\"dark\">\n";
3195 print "<tr class=\"light\">\n";
3199 if (exists $diff->{'nparents'}) { # combined diff
3201 fill_from_file_info
($diff, @parents)
3202 unless exists $diff->{'from_file'};
3204 if (!is_deleted
($diff)) {
3205 # file exists in the result (child) commit
3207 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3208 file_name
=>$diff->{'to_file'},
3210 -class => "list"}, esc_path
($diff->{'to_file'})) .
3214 esc_path
($diff->{'to_file'}) .
3218 if ($action eq 'commitdiff') {
3221 print "<td class=\"link\">" .
3222 $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3227 my $has_history = 0;
3228 my $not_deleted = 0;
3229 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3230 my $hash_parent = $parents[$i];
3231 my $from_hash = $diff->{'from_id'}[$i];
3232 my $from_path = $diff->{'from_file'}[$i];
3233 my $status = $diff->{'status'}[$i];
3235 $has_history ||= ($status ne 'A');
3236 $not_deleted ||= ($status ne 'D');
3238 if ($status eq 'A') {
3239 print "<td class=\"link\" align=\"right\"> | </td>\n";
3240 } elsif ($status eq 'D') {
3241 print "<td class=\"link\">" .
3242 $cgi->a({-href
=> href
(action
=>"blob",
3245 file_name
=>$from_path)},
3249 if ($diff->{'to_id'} eq $from_hash) {
3250 print "<td class=\"link nochange\">";
3252 print "<td class=\"link\">";
3254 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3255 hash
=>$diff->{'to_id'},
3256 hash_parent
=>$from_hash,
3258 hash_parent_base
=>$hash_parent,
3259 file_name
=>$diff->{'to_file'},
3260 file_parent
=>$from_path)},
3266 print "<td class=\"link\">";
3268 print $cgi->a({-href
=> href
(action
=>"blob",
3269 hash
=>$diff->{'to_id'},
3270 file_name
=>$diff->{'to_file'},
3273 print " | " if ($has_history);
3276 print $cgi->a({-href
=> href
(action
=>"history",
3277 file_name
=>$diff->{'to_file'},
3284 next; # instead of 'else' clause, to avoid extra indent
3286 # else ordinary diff
3288 my ($to_mode_oct, $to_mode_str, $to_file_type);
3289 my ($from_mode_oct, $from_mode_str, $from_file_type);
3290 if ($diff->{'to_mode'} ne ('0' x
6)) {
3291 $to_mode_oct = oct $diff->{'to_mode'};
3292 if (S_ISREG
($to_mode_oct)) { # only for regular file
3293 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3295 $to_file_type = file_type
($diff->{'to_mode'});
3297 if ($diff->{'from_mode'} ne ('0' x
6)) {
3298 $from_mode_oct = oct $diff->{'from_mode'};
3299 if (S_ISREG
($to_mode_oct)) { # only for regular file
3300 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3302 $from_file_type = file_type
($diff->{'from_mode'});
3305 if ($diff->{'status'} eq "A") { # created
3306 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3307 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3308 $mode_chng .= "]</span>";
3310 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3311 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3312 -class => "list"}, esc_path
($diff->{'file'}));
3314 print "<td>$mode_chng</td>\n";
3315 print "<td class=\"link\">";
3316 if ($action eq 'commitdiff') {
3319 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3322 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3323 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3327 } elsif ($diff->{'status'} eq "D") { # deleted
3328 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3330 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3331 hash_base
=>$parent, file_name
=>$diff->{'file'}),
3332 -class => "list"}, esc_path
($diff->{'file'}));
3334 print "<td>$mode_chng</td>\n";
3335 print "<td class=\"link\">";
3336 if ($action eq 'commitdiff') {
3339 print $cgi->a({-href
=> "#patch$patchno"}, "patch");
3342 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
3343 hash_base
=>$parent, file_name
=>$diff->{'file'})},
3346 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
3347 file_name
=>$diff->{'file'})},
3350 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
3351 file_name
=>$diff->{'file'})},
3355 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3356 my $mode_chnge = "";
3357 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3358 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3359 if ($from_file_type ne $to_file_type) {
3360 $mode_chnge .= " from $from_file_type to $to_file_type";
3362 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3363 if ($from_mode_str && $to_mode_str) {
3364 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3365 } elsif ($to_mode_str) {
3366 $mode_chnge .= " mode: $to_mode_str";
3369 $mode_chnge .= "]</span>\n";
3372 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3373 hash_base
=>$hash, file_name
=>$diff->{'file'}),
3374 -class => "list"}, esc_path
($diff->{'file'}));
3376 print "<td>$mode_chnge</td>\n";
3377 print "<td class=\"link\">";
3378 if ($action eq 'commitdiff') {
3381 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3383 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3384 # "commit" view and modified file (not onlu mode changed)
3385 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3386 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3387 hash_base
=>$hash, hash_parent_base
=>$parent,
3388 file_name
=>$diff->{'file'})},
3392 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3393 hash_base
=>$hash, file_name
=>$diff->{'file'})},
3396 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3397 file_name
=>$diff->{'file'})},
3400 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3401 file_name
=>$diff->{'file'})},
3405 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3406 my %status_name = ('R' => 'moved', 'C' => 'copied');
3407 my $nstatus = $status_name{$diff->{'status'}};
3409 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3410 # mode also for directories, so we cannot use $to_mode_str
3411 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3414 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
3415 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
3416 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
3417 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3418 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
3419 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
3420 -class => "list"}, esc_path
($diff->{'from_file'})) .
3421 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3422 "<td class=\"link\">";
3423 if ($action eq 'commitdiff') {
3426 print $cgi->a({-href
=> "#patch$patchno"}, "patch") .
3428 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3429 # "commit" view and modified file (not only pure rename or copy)
3430 print $cgi->a({-href
=> href
(action
=>"blobdiff",
3431 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
3432 hash_base
=>$hash, hash_parent_base
=>$parent,
3433 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
3437 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
3438 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
3441 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
3442 file_name
=>$diff->{'to_file'})},
3445 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
3446 file_name
=>$diff->{'to_file'})},
3450 } # we should not encounter Unmerged (U) or Unknown (X) status
3453 print "</tbody>" if $has_header;
3457 sub git_patchset_body
{
3458 my ($fd, $difftree, $hash, @hash_parents) = @_;
3459 my ($hash_parent) = $hash_parents[0];
3461 my $is_combined = (@hash_parents > 1);
3463 my $patch_number = 0;
3469 print "<div class=\"patchset\">\n";
3471 # skip to first patch
3472 while ($patch_line = <$fd>) {
3475 last if ($patch_line =~ m/^diff /);
3479 while ($patch_line) {
3481 # parse "git diff" header line
3482 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3483 # $1 is from_name, which we do not use
3484 $to_name = unquote
($2);
3485 $to_name =~ s!^b/!!;
3486 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3487 # $1 is 'cc' or 'combined', which we do not use
3488 $to_name = unquote
($2);
3493 # check if current patch belong to current raw line
3494 # and parse raw git-diff line if needed
3495 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
3496 # this is continuation of a split patch
3497 print "<div class=\"patch cont\">\n";
3499 # advance raw git-diff output if needed
3500 $patch_idx++ if defined $diffinfo;
3502 # read and prepare patch information
3503 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3505 # compact combined diff output can have some patches skipped
3506 # find which patch (using pathname of result) we are at now;
3508 while ($to_name ne $diffinfo->{'to_file'}) {
3509 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3510 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3511 "</div>\n"; # class="patch"
3516 last if $patch_idx > $#$difftree;
3517 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3521 # modifies %from, %to hashes
3522 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
3524 # this is first patch for raw difftree line with $patch_idx index
3525 # we index @$difftree array from 0, but number patches from 1
3526 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3530 #assert($patch_line =~ m/^diff /) if DEBUG;
3531 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3533 # print "git diff" header
3534 print format_git_diff_header_line
($patch_line, $diffinfo,
3537 # print extended diff header
3538 print "<div class=\"diff extended_header\">\n";
3540 while ($patch_line = <$fd>) {
3543 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
3545 print format_extended_diff_header_line
($patch_line, $diffinfo,
3548 print "</div>\n"; # class="diff extended_header"
3550 # from-file/to-file diff header
3551 if (! $patch_line) {
3552 print "</div>\n"; # class="patch"
3555 next PATCH
if ($patch_line =~ m/^diff /);
3556 #assert($patch_line =~ m/^---/) if DEBUG;
3558 my $last_patch_line = $patch_line;
3559 $patch_line = <$fd>;
3561 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3563 print format_diff_from_to_header
($last_patch_line, $patch_line,
3564 $diffinfo, \
%from, \
%to,
3569 while ($patch_line = <$fd>) {
3572 next PATCH
if ($patch_line =~ m/^diff /);
3574 print format_diff_line
($patch_line, \
%from, \
%to);
3578 print "</div>\n"; # class="patch"
3581 # for compact combined (--cc) format, with chunk and patch simpliciaction
3582 # patchset might be empty, but there might be unprocessed raw lines
3583 for (++$patch_idx if $patch_number > 0;
3584 $patch_idx < @$difftree;
3586 # read and prepare patch information
3587 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
3589 # generate anchor for "patch" links in difftree / whatchanged part
3590 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3591 format_diff_cc_simplified
($diffinfo, @hash_parents) .
3592 "</div>\n"; # class="patch"
3597 if ($patch_number == 0) {
3598 if (@hash_parents > 1) {
3599 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3601 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3605 print "</div>\n"; # class="patchset"
3608 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3610 # fills project list info (age, description, owner, forks) for each
3611 # project in the list, removing invalid projects from returned list
3612 # NOTE: modifies $projlist, but does not remove entries from it
3613 sub fill_project_list_info
{
3614 my ($projlist, $check_forks) = @_;
3618 foreach my $pr (@$projlist) {
3619 my (@activity) = git_get_last_activity
($pr->{'path'});
3620 unless (@activity) {
3623 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3624 if (!defined $pr->{'descr'}) {
3625 my $descr = git_get_project_description
($pr->{'path'}) || "";
3626 $descr = to_utf8
($descr);
3627 $pr->{'descr_long'} = $descr;
3628 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
3630 if (!defined $pr->{'owner'}) {
3631 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
3634 my $pname = $pr->{'path'};
3635 if (($pname =~ s/\.git$//) &&
3636 ($pname !~ /\/$/) &&
3637 (-d
"$projectroot/$pname")) {
3638 $pr->{'forks'} = "-d $projectroot/$pname";
3643 push @projects, $pr;
3649 # print 'sort by' <th> element, generating 'sort by $name' replay link
3650 # if that order is not selected
3652 my ($name, $order, $header) = @_;
3653 $header ||= ucfirst($name);
3655 if ($order eq $name) {
3656 print "<th>$header</th>\n";
3659 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
3660 -class => "header"}, $header) .
3665 sub git_project_list_body
{
3666 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3668 my ($check_forks) = gitweb_check_feature
('forks');
3669 my @projects = fill_project_list_info
($projlist, $check_forks);
3671 $order ||= $default_projects_order;
3672 $from = 0 unless defined $from;
3673 $to = $#projects if (!defined $to || $#projects < $to);
3676 project
=> { key
=> 'path', type
=> 'str' },
3677 descr
=> { key
=> 'descr_long', type
=> 'str' },
3678 owner
=> { key
=> 'owner', type
=> 'str' },
3679 age
=> { key
=> 'age', type
=> 'num' }
3681 my $oi = $order_info{$order};
3682 if ($oi->{'type'} eq 'str') {
3683 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3685 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3688 print "<table class=\"project_list\">\n";
3689 unless ($no_header) {
3692 print "<th></th>\n";
3694 print_sort_th
('project', $order, 'Project');
3695 print_sort_th
('descr', $order, 'Description');
3696 print_sort_th
('owner', $order, 'Owner');
3697 print_sort_th
('age', $order, 'Last Change');
3698 print "<th></th>\n" . # for links
3702 for (my $i = $from; $i <= $to; $i++) {
3703 my $pr = $projects[$i];
3705 print "<tr class=\"dark\">\n";
3707 print "<tr class=\"light\">\n";
3712 if ($pr->{'forks'}) {
3713 print "<!-- $pr->{'forks'} -->\n";
3714 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "+");
3718 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3719 -class => "list"}, esc_html
($pr->{'path'})) . "</td>\n" .
3720 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
3721 -class => "list", -title
=> $pr->{'descr_long'}},
3722 esc_html
($pr->{'descr'})) . "</td>\n" .
3723 "<td><i>" . chop_and_escape_str
($pr->{'owner'}, 15) . "</i></td>\n";
3724 print "<td class=\"". age_class
($pr->{'age'}) . "\">" .
3725 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3726 "<td class=\"link\">" .
3727 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
3728 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "shortlog") . " | " .
3729 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"log")}, "log") . " | " .
3730 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
3731 ($pr->{'forks'} ? " | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
3735 if (defined $extra) {
3738 print "<td></td>\n";
3740 print "<td colspan=\"5\">$extra</td>\n" .
3746 sub git_shortlog_body
{
3747 # uses global variable $project
3748 my ($commitlist, $from, $to, $refs, $extra) = @_;
3750 $from = 0 unless defined $from;
3751 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3753 print "<table class=\"shortlog\">\n";
3755 for (my $i = $from; $i <= $to; $i++) {
3756 my %co = %{$commitlist->[$i]};
3757 my $commit = $co{'id'};
3758 my $ref = format_ref_marker
($refs, $commit);
3760 print "<tr class=\"dark\">\n";
3762 print "<tr class=\"light\">\n";
3765 my $author = chop_and_escape_str
($co{'author_name'}, 10);
3766 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3767 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3768 "<td><i>" . $author . "</i></td>\n" .
3770 print format_subject_html
($co{'title'}, $co{'title_short'},
3771 href
(action
=>"commit", hash
=>$commit), $ref);
3773 "<td class=\"link\">" .
3774 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
3775 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
3776 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
3777 my $snapshot_links = format_snapshot_links
($commit);
3778 if (defined $snapshot_links) {
3779 print " | " . $snapshot_links;
3784 if (defined $extra) {
3786 "<td colspan=\"4\">$extra</td>\n" .
3792 sub git_history_body
{
3793 # Warning: assumes constant type (blob or tree) during history
3794 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3796 $from = 0 unless defined $from;
3797 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3799 print "<table class=\"history\">\n";
3801 for (my $i = $from; $i <= $to; $i++) {
3802 my %co = %{$commitlist->[$i]};
3806 my $commit = $co{'id'};
3808 my $ref = format_ref_marker
($refs, $commit);
3811 print "<tr class=\"dark\">\n";
3813 print "<tr class=\"light\">\n";
3816 # shortlog uses chop_str($co{'author_name'}, 10)
3817 my $author = chop_and_escape_str
($co{'author_name'}, 15, 3);
3818 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3819 "<td><i>" . $author . "</i></td>\n" .
3821 # originally git_history used chop_str($co{'title'}, 50)
3822 print format_subject_html
($co{'title'}, $co{'title_short'},
3823 href
(action
=>"commit", hash
=>$commit), $ref);
3825 "<td class=\"link\">" .
3826 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
3827 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
3829 if ($ftype eq 'blob') {
3830 my $blob_current = git_get_hash_by_path
($hash_base, $file_name);
3831 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
3832 if (defined $blob_current && defined $blob_parent &&
3833 $blob_current ne $blob_parent) {
3835 $cgi->a({-href
=> href
(action
=>"blobdiff",
3836 hash
=>$blob_current, hash_parent
=>$blob_parent,
3837 hash_base
=>$hash_base, hash_parent_base
=>$commit,
3838 file_name
=>$file_name)},
3845 if (defined $extra) {
3847 "<td colspan=\"4\">$extra</td>\n" .
3854 # uses global variable $project
3855 my ($taglist, $from, $to, $extra) = @_;
3856 $from = 0 unless defined $from;
3857 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3859 print "<table class=\"tags\">\n";
3861 for (my $i = $from; $i <= $to; $i++) {
3862 my $entry = $taglist->[$i];
3864 my $comment = $tag{'subject'};
3866 if (defined $comment) {
3867 $comment_short = chop_str
($comment, 30, 5);
3870 print "<tr class=\"dark\">\n";
3872 print "<tr class=\"light\">\n";
3875 if (defined $tag{'age'}) {
3876 print "<td><i>$tag{'age'}</i></td>\n";
3878 print "<td></td>\n";
3881 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
3882 -class => "list name"}, esc_html
($tag{'name'})) .
3885 if (defined $comment) {
3886 print format_subject_html
($comment, $comment_short,
3887 href
(action
=>"tag", hash
=>$tag{'id'}));
3890 "<td class=\"selflink\">";
3891 if ($tag{'type'} eq "tag") {
3892 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
3897 "<td class=\"link\">" . " | " .
3898 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
3899 if ($tag{'reftype'} eq "commit") {
3900 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "shortlog") .
3901 " | " . $cgi->a({-href
=> href
(action
=>"log", hash
=>$tag{'fullname'})}, "log");
3902 } elsif ($tag{'reftype'} eq "blob") {
3903 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
3908 if (defined $extra) {
3910 "<td colspan=\"5\">$extra</td>\n" .
3916 sub git_heads_body
{
3917 # uses global variable $project
3918 my ($headlist, $head, $from, $to, $extra) = @_;
3919 $from = 0 unless defined $from;
3920 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3922 print "<table class=\"heads\">\n";
3924 for (my $i = $from; $i <= $to; $i++) {
3925 my $entry = $headlist->[$i];
3927 my $curr = $ref{'id'} eq $head;
3929 print "<tr class=\"dark\">\n";
3931 print "<tr class=\"light\">\n";
3934 print "<td><i>$ref{'age'}</i></td>\n" .
3935 ($curr ? "<td class=\"current_head\">" : "<td>") .
3936 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
3937 -class => "list name"},esc_html
($ref{'name'})) .
3939 "<td class=\"link\">" .
3940 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "shortlog") . " | " .
3941 $cgi->a({-href
=> href
(action
=>"log", hash
=>$ref{'fullname'})}, "log") . " | " .
3942 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'name'})}, "tree") .
3946 if (defined $extra) {
3948 "<td colspan=\"3\">$extra</td>\n" .
3954 sub git_search_grep_body
{
3955 my ($commitlist, $from, $to, $extra) = @_;
3956 $from = 0 unless defined $from;
3957 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3959 print "<table class=\"commit_search\">\n";
3961 for (my $i = $from; $i <= $to; $i++) {
3962 my %co = %{$commitlist->[$i]};
3966 my $commit = $co{'id'};
3968 print "<tr class=\"dark\">\n";
3970 print "<tr class=\"light\">\n";
3973 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
3974 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3975 "<td><i>" . $author . "</i></td>\n" .
3977 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
3978 -class => "list subject"},
3979 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
3980 my $comment = $co{'comment'};
3981 foreach my $line (@$comment) {
3982 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
3983 my ($lead, $match, $trail) = ($1, $2, $3);
3984 $match = chop_str
($match, 70, 5, 'center');
3985 my $contextlen = int((80 - length($match))/2);
3986 $contextlen = 30 if ($contextlen > 30);
3987 $lead = chop_str
($lead, $contextlen, 10, 'left');
3988 $trail = chop_str
($trail, $contextlen, 10, 'right');
3990 $lead = esc_html
($lead);
3991 $match = esc_html
($match);
3992 $trail = esc_html
($trail);
3994 print "$lead<span class=\"match\">$match</span>$trail<br />";
3998 "<td class=\"link\">" .
3999 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
4001 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
4003 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
4007 if (defined $extra) {
4009 "<td colspan=\"3\">$extra</td>\n" .
4015 ## ======================================================================
4016 ## ======================================================================
4019 sub git_project_list
{
4020 my $order = $cgi->param('o');
4021 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4022 die_error
(400, "Unknown order parameter");
4025 my @list = git_get_projects_list
();
4027 die_error
(404, "No projects found");
4031 if (-f
$home_text) {
4032 print "<div class=\"index_include\">\n";
4033 open (my $fd, $home_text);
4038 git_project_list_body
(\
@list, $order);
4043 my $order = $cgi->param('o');
4044 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4045 die_error
(400, "Unknown order parameter");
4048 my @list = git_get_projects_list
($project);
4050 die_error
(404, "No forks found");
4054 git_print_page_nav
('','');
4055 git_print_header_div
('summary', "$project forks");
4056 git_project_list_body
(\
@list, $order);
4060 sub git_project_index
{
4061 my @projects = git_get_projects_list
($project);
4064 -type
=> 'text/plain',
4065 -charset
=> 'utf-8',
4066 -content_disposition
=> 'inline; filename="index.aux"');
4068 foreach my $pr (@projects) {
4069 if (!exists $pr->{'owner'}) {
4070 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
4073 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4074 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4075 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4076 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
4080 print "$path $owner\n";
4085 my $descr = git_get_project_description
($project) || "none";
4086 my %co = parse_commit
("HEAD");
4087 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4088 my $head = $co{'id'};
4090 my $owner = git_get_project_owner
($project);
4092 my $refs = git_get_references
();
4093 # These get_*_list functions return one more to allow us to see if
4094 # there are more ...
4095 my @taglist = git_get_tags_list
(16);
4096 my @headlist = git_get_heads_list
(16);
4098 my ($check_forks) = gitweb_check_feature
('forks');
4101 @forklist = git_get_projects_list
($project);
4105 git_print_page_nav
('summary','', $head);
4107 print "<div class=\"title\"> </div>\n";
4108 print "<table class=\"projects_list\">\n" .
4109 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n" .
4110 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html
($owner) . "</td></tr>\n";
4111 if (defined $cd{'rfc2822'}) {
4112 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4115 # use per project git URL list in $projectroot/$project/cloneurl
4116 # or make project git URL from git base URL and project name
4117 my $url_tag = "URL";
4118 my @url_list = git_get_project_url_list
($project);
4119 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4120 foreach my $git_url (@url_list) {
4121 next unless $git_url;
4122 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4127 if (-s
"$projectroot/$project/README.html") {
4128 if (open my $fd, "$projectroot/$project/README.html") {
4129 print "<div class=\"title\">readme</div>\n" .
4130 "<div class=\"readme\">\n";
4131 print $_ while (<$fd>);
4132 print "\n</div>\n"; # class="readme"
4137 # we need to request one more than 16 (0..15) to check if
4139 my @commitlist = $head ? parse_commits
($head, 17) : ();
4141 git_print_header_div
('shortlog');
4142 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
4143 $#commitlist <= 15 ? undef :
4144 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
4148 git_print_header_div
('tags');
4149 git_tags_body
(\
@taglist, 0, 15,
4150 $#taglist <= 15 ? undef :
4151 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
4155 git_print_header_div
('heads');
4156 git_heads_body
(\
@headlist, $head, 0, 15,
4157 $#headlist <= 15 ? undef :
4158 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
4162 git_print_header_div
('forks');
4163 git_project_list_body
(\
@forklist, 'age', 0, 15,
4164 $#forklist <= 15 ? undef :
4165 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
4173 my $head = git_get_head_hash
($project);
4175 git_print_page_nav
('','', $head,undef,$head);
4176 my %tag = parse_tag
($hash);
4179 die_error
(404, "Unknown tag object");
4182 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
4183 print "<div class=\"title_text\">\n" .
4184 "<table class=\"object_header\">\n" .
4186 "<td>object</td>\n" .
4187 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4188 $tag{'object'}) . "</td>\n" .
4189 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
4190 $tag{'type'}) . "</td>\n" .
4192 if (defined($tag{'author'})) {
4193 my %ad = parse_date
($tag{'epoch'}, $tag{'tz'});
4194 print "<tr><td>author</td><td>" . esc_html
($tag{'author'}) . "</td></tr>\n";
4195 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4196 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4199 print "</table>\n\n" .
4201 print "<div class=\"page_body\">";
4202 my $comment = $tag{'comment'};
4203 foreach my $line (@$comment) {
4205 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
4215 gitweb_check_feature
('blame')
4216 or die_error
(403, "Blame view not allowed");
4218 die_error
(400, "No file name given") unless $file_name;
4219 $hash_base ||= git_get_head_hash
($project);
4220 die_error
(404, "Couldn't find base commit") unless ($hash_base);
4221 my %co = parse_commit
($hash_base)
4222 or die_error
(404, "Commit not found");
4223 if (!defined $hash) {
4224 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
4225 or die_error
(404, "Error looking up file");
4227 $ftype = git_get_type
($hash);
4228 if ($ftype !~ "blob") {
4229 die_error
(400, "Object is not a blob");
4231 open ($fd, "-|", git_cmd
(), "blame", '-p', '--',
4232 $file_name, $hash_base)
4233 or die_error
(500, "Open git-blame failed");
4236 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
4239 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4242 $cgi->a({-href
=> href
(action
=>"blame", file_name
=>$file_name)},
4244 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4245 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4246 git_print_page_path
($file_name, $ftype, $hash_base);
4247 my @rev_color = (qw(light2 dark2));
4248 my $num_colors = scalar(@rev_color);
4249 my $current_color = 0;
4252 <div class="page_body">
4253 <table class="blame">
4254 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4259 last unless defined $_;
4260 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4261 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4262 if (!exists $metainfo{$full_rev}) {
4263 $metainfo{$full_rev} = {};
4265 my $meta = $metainfo{$full_rev};
4268 if (/^(\S+) (.*)$/) {
4274 my $rev = substr($full_rev, 0, 8);
4275 my $author = $meta->{'author'};
4276 my %date = parse_date
($meta->{'author-time'},
4277 $meta->{'author-tz'});
4278 my $date = $date{'iso-tz'};
4280 $current_color = ++$current_color % $num_colors;
4282 print "<tr class=\"$rev_color[$current_color]\">\n";
4284 print "<td class=\"sha1\"";
4285 print " title=\"". esc_html
($author) . ", $date\"";
4286 print " rowspan=\"$group_size\"" if ($group_size > 1);
4288 print $cgi->a({-href
=> href
(action
=>"commit",
4290 file_name
=>$file_name)},
4294 open (my $dd, "-|", git_cmd
(), "rev-parse", "$full_rev^")
4295 or die_error
(500, "Open git-rev-parse failed");
4296 my $parent_commit = <$dd>;
4298 chomp($parent_commit);
4299 my $blamed = href
(action
=> 'blame',
4300 file_name
=> $meta->{'filename'},
4301 hash_base
=> $parent_commit);
4302 print "<td class=\"linenr\">";
4303 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
4305 -class => "linenr" },
4308 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
4314 or print "Reading blob failed\n";
4319 my $head = git_get_head_hash
($project);
4321 git_print_page_nav
('','', $head,undef,$head);
4322 git_print_header_div
('summary', $project);
4324 my @tagslist = git_get_tags_list
();
4326 git_tags_body
(\
@tagslist);
4332 my $head = git_get_head_hash
($project);
4334 git_print_page_nav
('','', $head,undef,$head);
4335 git_print_header_div
('summary', $project);
4337 my @headslist = git_get_heads_list
();
4339 git_heads_body
(\
@headslist, $head);
4344 sub git_blob_plain
{
4348 if (!defined $hash) {
4349 if (defined $file_name) {
4350 my $base = $hash_base || git_get_head_hash
($project);
4351 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4352 or die_error
(404, "Cannot find file");
4354 die_error
(400, "No file name defined");
4356 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4357 # blobs defined by non-textual hash id's can be cached
4361 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4362 or die_error
(500, "Open git-cat-file blob '$hash' failed");
4364 # content-type (can include charset)
4365 $type = blob_contenttype
($fd, $file_name, $type);
4367 # "save as" filename, even when no $file_name is given
4368 my $save_as = "$hash";
4369 if (defined $file_name) {
4370 $save_as = $file_name;
4371 } elsif ($type =~ m/^text\//) {
4377 -expires
=> $expires,
4378 -content_disposition
=> 'inline; filename="' . $save_as . '"');
4380 binmode STDOUT
, ':raw';
4382 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4390 if (!defined $hash) {
4391 if (defined $file_name) {
4392 my $base = $hash_base || git_get_head_hash
($project);
4393 $hash = git_get_hash_by_path
($base, $file_name, "blob")
4394 or die_error
(404, "Cannot find file");
4396 die_error
(400, "No file name defined");
4398 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4399 # blobs defined by non-textual hash id's can be cached
4403 my ($have_blame) = gitweb_check_feature
('blame');
4404 open my $fd, "-|", git_cmd
(), "cat-file", "blob", $hash
4405 or die_error
(500, "Couldn't cat $file_name, $hash");
4406 my $mimetype = blob_mimetype
($fd, $file_name);
4407 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
4409 return git_blob_plain
($mimetype);
4411 # we can have blame only for text/* mimetype
4412 $have_blame &&= ($mimetype =~ m!^text/!);
4414 git_header_html
(undef, $expires);
4415 my $formats_nav = '';
4416 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4417 if (defined $file_name) {
4420 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
4425 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4428 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4431 $cgi->a({-href
=> href
(action
=>"blob",
4432 hash_base
=>"HEAD", file_name
=>$file_name)},
4436 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
4439 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4440 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4442 print "<div class=\"page_nav\">\n" .
4443 "<br/><br/></div>\n" .
4444 "<div class=\"title\">$hash</div>\n";
4446 git_print_page_path
($file_name, "blob", $hash_base);
4447 print "<div class=\"page_body\">\n";
4448 if ($mimetype =~ m!^image/!) {
4449 print qq
!<img type
="$mimetype"!;
4451 print qq
! alt
="$file_name" title
="$file_name"!;
4454 href(action=>"blob_plain
", hash=>$hash,
4455 hash_base=>$hash_base, file_name=>$file_name) .
4459 while (my $line = <$fd>) {
4462 $line = untabify
($line);
4463 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4464 $nr, $nr, $nr, esc_html
($line, -nbsp
=>1);
4468 or print "Reading blob failed.\n";
4474 if (!defined $hash_base) {
4475 $hash_base = "HEAD";
4477 if (!defined $hash) {
4478 if (defined $file_name) {
4479 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
4484 die_error
(404, "No such tree") unless defined($hash);
4486 open my $fd, "-|", git_cmd
(), "ls-tree", '-z', $hash
4487 or die_error
(500, "Open git-ls-tree failed");
4488 my @entries = map { chomp; $_ } <$fd>;
4489 close $fd or die_error
(404, "Reading tree failed");
4492 my $refs = git_get_references
();
4493 my $ref = format_ref_marker
($refs, $hash_base);
4496 my ($have_blame) = gitweb_check_feature
('blame');
4497 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4499 if (defined $file_name) {
4501 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
4503 $cgi->a({-href
=> href
(action
=>"tree",
4504 hash_base
=>"HEAD", file_name
=>$file_name)},
4507 my $snapshot_links = format_snapshot_links
($hash);
4508 if (defined $snapshot_links) {
4509 # FIXME: Should be available when we have no hash base as well.
4510 push @views_nav, $snapshot_links;
4512 git_print_page_nav
('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4513 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash_base);
4516 print "<div class=\"page_nav\">\n";
4517 print "<br/><br/></div>\n";
4518 print "<div class=\"title\">$hash</div>\n";
4520 if (defined $file_name) {
4521 $basedir = $file_name;
4522 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4525 git_print_page_path
($file_name, 'tree', $hash_base);
4527 print "<div class=\"page_body\">\n";
4528 print "<table class=\"tree\">\n";
4530 # '..' (top directory) link if possible
4531 if (defined $hash_base &&
4532 defined $file_name && $file_name =~ m![^/]+$!) {
4534 print "<tr class=\"dark\">\n";
4536 print "<tr class=\"light\">\n";
4540 my $up = $file_name;
4541 $up =~ s!/?[^/]+$!!;
4542 undef $up unless $up;
4543 # based on git_print_tree_entry
4544 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
4545 print '<td class="list">';
4546 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hash_base,
4550 print "<td class=\"link\"></td>\n";
4554 foreach my $line (@entries) {
4555 my %t = parse_ls_tree_line
($line, -z
=> 1);
4558 print "<tr class=\"dark\">\n";
4560 print "<tr class=\"light\">\n";
4564 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
4568 print "</table>\n" .
4574 my @supported_fmts = gitweb_check_feature
('snapshot');
4575 @supported_fmts = filter_snapshot_fmts
(@supported_fmts);
4577 my $format = $cgi->param('sf');
4578 if (!@supported_fmts) {
4579 die_error
(403, "Snapshots not allowed");
4581 # default to first supported snapshot format
4582 $format ||= $supported_fmts[0];
4583 if ($format !~ m/^[a-z0-9]+$/) {
4584 die_error
(400, "Invalid snapshot format parameter");
4585 } elsif (!exists($known_snapshot_formats{$format})) {
4586 die_error
(400, "Unknown snapshot format");
4587 } elsif (!grep($_ eq $format, @supported_fmts)) {
4588 die_error
(403, "Unsupported snapshot format");
4591 if (!defined $hash) {
4592 $hash = git_get_head_hash
($project);
4595 my $name = $project;
4596 $name =~ s
,([^/])/*\
.git
$,$1,;
4597 $name = basename
($name);
4598 my $filename = to_utf8
($name);
4599 $name =~ s/\047/\047\\\047\047/g;
4601 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4602 $cmd = quote_command
(
4603 git_cmd
(), 'archive',
4604 "--format=$known_snapshot_formats{$format}{'format'}",
4605 "--prefix=$name/", $hash);
4606 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4607 $cmd .= ' | ' . quote_command
(@{$known_snapshot_formats{$format}{'compressor'}});
4611 -type
=> $known_snapshot_formats{$format}{'type'},
4612 -content_disposition
=> 'inline; filename="' . "$filename" . '"',
4613 -status
=> '200 OK');
4615 open my $fd, "-|", $cmd
4616 or die_error
(500, "Execute git-archive failed");
4617 binmode STDOUT
, ':raw';
4619 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
4624 my $head = git_get_head_hash
($project);
4625 if (!defined $hash) {
4628 if (!defined $page) {
4631 my $refs = git_get_references
();
4633 my @commitlist = parse_commits
($hash, 101, (100 * $page));
4635 my $paging_nav = format_paging_nav
('log', $hash, $head, $page, $#commitlist >= 100);
4638 git_print_page_nav
('log','', $hash,undef,undef, $paging_nav);
4641 my %co = parse_commit
($hash);
4643 git_print_header_div
('summary', $project);
4644 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4646 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4647 for (my $i = 0; $i <= $to; $i++) {
4648 my %co = %{$commitlist[$i]};
4650 my $commit = $co{'id'};
4651 my $ref = format_ref_marker
($refs, $commit);
4652 my %ad = parse_date
($co{'author_epoch'});
4653 git_print_header_div
('commit',
4654 "<span class=\"age\">$co{'age_string'}</span>" .
4655 esc_html
($co{'title'}) . $ref,
4657 print "<div class=\"title_text\">\n" .
4658 "<div class=\"log_link\">\n" .
4659 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
4661 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
4663 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
4666 "<i>" . esc_html
($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4669 print "<div class=\"log_body\">\n";
4670 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
4673 if ($#commitlist >= 100) {
4674 print "<div class=\"page_nav\">\n";
4675 print $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4676 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4683 $hash ||= $hash_base || "HEAD";
4684 my %co = parse_commit
($hash)
4685 or die_error
(404, "Unknown commit object");
4686 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
4687 my %cd = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
4689 my $parent = $co{'parent'};
4690 my $parents = $co{'parents'}; # listref
4692 # we need to prepare $formats_nav before any parameter munging
4694 if (!defined $parent) {
4696 $formats_nav .= '(initial)';
4697 } elsif (@$parents == 1) {
4698 # single parent commit
4701 $cgi->a({-href
=> href
(action
=>"commit",
4703 esc_html
(substr($parent, 0, 7))) .
4710 $cgi->a({-href
=> href
(action
=>"commit",
4712 esc_html
(substr($_, 0, 7)));
4717 if (!defined $parent) {
4721 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', "--no-commit-id",
4723 (@$parents <= 1 ? $parent : '-c'),
4725 or die_error
(500, "Open git-diff-tree failed");
4726 @difftree = map { chomp; $_ } <$fd>;
4727 close $fd or die_error
(404, "Reading git-diff-tree failed");
4729 # non-textual hash id's can be cached
4731 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4734 my $refs = git_get_references
();
4735 my $ref = format_ref_marker
($refs, $co{'id'});
4737 git_header_html
(undef, $expires);
4738 git_print_page_nav
('commit', '',
4739 $hash, $co{'tree'}, $hash,
4742 if (defined $co{'parent'}) {
4743 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
4745 git_print_header_div
('tree', esc_html
($co{'title'}) . $ref, $co{'tree'}, $hash);
4747 print "<div class=\"title_text\">\n" .
4748 "<table class=\"object_header\">\n";
4749 print "<tr><td>author</td><td>" . esc_html
($co{'author'}) . "</td></tr>\n".
4751 "<td></td><td> $ad{'rfc2822'}";
4752 if ($ad{'hour_local'} < 6) {
4753 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4754 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4756 printf(" (%02d:%02d %s)",
4757 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4761 print "<tr><td>committer</td><td>" . esc_html
($co{'committer'}) . "</td></tr>\n";
4762 print "<tr><td></td><td> $cd{'rfc2822'}" .
4763 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4765 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4768 "<td class=\"sha1\">" .
4769 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
4770 class => "list"}, $co{'tree'}) .
4772 "<td class=\"link\">" .
4773 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
4775 my $snapshot_links = format_snapshot_links
($hash);
4776 if (defined $snapshot_links) {
4777 print " | " . $snapshot_links;
4782 foreach my $par (@$parents) {
4785 "<td class=\"sha1\">" .
4786 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
4787 class => "list"}, $par) .
4789 "<td class=\"link\">" .
4790 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
4792 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
4799 print "<div class=\"page_body\">\n";
4800 git_print_log
($co{'comment'});
4803 git_difftree_body
(\
@difftree, $hash, @$parents);
4809 # object is defined by:
4810 # - hash or hash_base alone
4811 # - hash_base and file_name
4814 # - hash or hash_base alone
4815 if ($hash || ($hash_base && !defined $file_name)) {
4816 my $object_id = $hash || $hash_base;
4818 open my $fd, "-|", quote_command
(
4819 git_cmd
(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4820 or die_error
(404, "Object does not exist");
4824 or die_error
(404, "Object does not exist");
4826 # - hash_base and file_name
4827 } elsif ($hash_base && defined $file_name) {
4828 $file_name =~ s
,/+$,,;
4830 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
4831 or die_error
(404, "Base object does not exist");
4833 # here errors should not hapen
4834 open my $fd, "-|", git_cmd
(), "ls-tree", $hash_base, "--", $file_name
4835 or die_error
(500, "Open git-ls-tree failed");
4839 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4840 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4841 die_error
(404, "File or directory for given base does not exist");
4846 die_error
(400, "Not enough information to find object");
4849 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
4850 hash
=>$hash, hash_base
=>$hash_base,
4851 file_name
=>$file_name),
4852 -status
=> '302 Found');
4856 my $format = shift || 'html';
4863 # preparing $fd and %diffinfo for git_patchset_body
4865 if (defined $hash_base && defined $hash_parent_base) {
4866 if (defined $file_name) {
4868 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4869 $hash_parent_base, $hash_base,
4870 "--", (defined $file_parent ? $file_parent : ()), $file_name
4871 or die_error
(500, "Open git-diff-tree failed");
4872 @difftree = map { chomp; $_ } <$fd>;
4874 or die_error
(404, "Reading git-diff-tree failed");
4876 or die_error
(404, "Blob diff not found");
4878 } elsif (defined $hash &&
4879 $hash =~ /[0-9a-fA-F]{40}/) {
4880 # try to find filename from $hash
4882 # read filtered raw output
4883 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4884 $hash_parent_base, $hash_base, "--"
4885 or die_error
(500, "Open git-diff-tree failed");
4887 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4889 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4890 map { chomp; $_ } <$fd>;
4892 or die_error
(404, "Reading git-diff-tree failed");
4894 or die_error
(404, "Blob diff not found");
4897 die_error
(400, "Missing one of the blob diff parameters");
4900 if (@difftree > 1) {
4901 die_error
(400, "Ambiguous blob diff specification");
4904 %diffinfo = parse_difftree_raw_line
($difftree[0]);
4905 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4906 $file_name ||= $diffinfo{'to_file'};
4908 $hash_parent ||= $diffinfo{'from_id'};
4909 $hash ||= $diffinfo{'to_id'};
4911 # non-textual hash id's can be cached
4912 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4913 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4918 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
4919 '-p', ($format eq 'html' ? "--full-index" : ()),
4920 $hash_parent_base, $hash_base,
4921 "--", (defined $file_parent ? $file_parent : ()), $file_name
4922 or die_error
(500, "Open git-diff-tree failed");
4925 # old/legacy style URI
4926 if (!%diffinfo && # if new style URI failed
4927 defined $hash && defined $hash_parent) {
4928 # fake git-diff-tree raw output
4929 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
4930 $diffinfo{'from_id'} = $hash_parent;
4931 $diffinfo{'to_id'} = $hash;
4932 if (defined $file_name) {
4933 if (defined $file_parent) {
4934 $diffinfo{'status'} = '2';
4935 $diffinfo{'from_file'} = $file_parent;
4936 $diffinfo{'to_file'} = $file_name;
4937 } else { # assume not renamed
4938 $diffinfo{'status'} = '1';
4939 $diffinfo{'from_file'} = $file_name;
4940 $diffinfo{'to_file'} = $file_name;
4942 } else { # no filename given
4943 $diffinfo{'status'} = '2';
4944 $diffinfo{'from_file'} = $hash_parent;
4945 $diffinfo{'to_file'} = $hash;
4948 # non-textual hash id's can be cached
4949 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
4950 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
4955 open $fd, "-|", git_cmd
(), "diff", @diff_opts,
4956 '-p', ($format eq 'html' ? "--full-index" : ()),
4957 $hash_parent, $hash, "--"
4958 or die_error
(500, "Open git-diff failed");
4960 die_error
(400, "Missing one of the blob diff parameters")
4965 if ($format eq 'html') {
4967 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
4969 git_header_html
(undef, $expires);
4970 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
4971 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4972 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
4974 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
4975 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
4977 if (defined $file_name) {
4978 git_print_page_path
($file_name, "blob", $hash_base);
4980 print "<div class=\"page_path\"></div>\n";
4983 } elsif ($format eq 'plain') {
4985 -type
=> 'text/plain',
4986 -charset
=> 'utf-8',
4987 -expires
=> $expires,
4988 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
4990 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
4993 die_error
(400, "Unknown blobdiff format");
4997 if ($format eq 'html') {
4998 print "<div class=\"page_body\">\n";
5000 git_patchset_body
($fd, [ \
%diffinfo ], $hash_base, $hash_parent_base);
5003 print "</div>\n"; # class="page_body"
5007 while (my $line = <$fd>) {
5008 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5009 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5013 last if $line =~ m!^\+\+\+!;
5021 sub git_blobdiff_plain
{
5022 git_blobdiff
('plain');
5025 sub git_commitdiff
{
5026 my $format = shift || 'html';
5027 $hash ||= $hash_base || "HEAD";
5028 my %co = parse_commit
($hash)
5029 or die_error
(404, "Unknown commit object");
5031 # choose format for commitdiff for merge
5032 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5033 $hash_parent = '--cc';
5035 # we need to prepare $formats_nav before almost any parameter munging
5037 if ($format eq 'html') {
5039 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
5042 if (defined $hash_parent &&
5043 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5044 # commitdiff with two commits given
5045 my $hash_parent_short = $hash_parent;
5046 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5047 $hash_parent_short = substr($hash_parent, 0, 7);
5051 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5052 if ($co{'parents'}[$i] eq $hash_parent) {
5053 $formats_nav .= ' parent ' . ($i+1);
5057 $formats_nav .= ': ' .
5058 $cgi->a({-href
=> href
(action
=>"commitdiff",
5059 hash
=>$hash_parent)},
5060 esc_html
($hash_parent_short)) .
5062 } elsif (!$co{'parent'}) {
5064 $formats_nav .= ' (initial)';
5065 } elsif (scalar @{$co{'parents'}} == 1) {
5066 # single parent commit
5069 $cgi->a({-href
=> href
(action
=>"commitdiff",
5070 hash
=>$co{'parent'})},
5071 esc_html
(substr($co{'parent'}, 0, 7))) .
5075 if ($hash_parent eq '--cc') {
5076 $formats_nav .= ' | ' .
5077 $cgi->a({-href
=> href
(action
=>"commitdiff",
5078 hash
=>$hash, hash_parent
=>'-c')},
5080 } else { # $hash_parent eq '-c'
5081 $formats_nav .= ' | ' .
5082 $cgi->a({-href
=> href
(action
=>"commitdiff",
5083 hash
=>$hash, hash_parent
=>'--cc')},
5089 $cgi->a({-href
=> href
(action
=>"commitdiff",
5091 esc_html
(substr($_, 0, 7)));
5092 } @{$co{'parents'}} ) .
5097 my $hash_parent_param = $hash_parent;
5098 if (!defined $hash_parent_param) {
5099 # --cc for multiple parents, --root for parentless
5100 $hash_parent_param =
5101 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5107 if ($format eq 'html') {
5108 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5109 "--no-commit-id", "--patch-with-raw", "--full-index",
5110 $hash_parent_param, $hash, "--"
5111 or die_error
(500, "Open git-diff-tree failed");
5113 while (my $line = <$fd>) {
5115 # empty line ends raw part of diff-tree output
5117 push @difftree, scalar parse_difftree_raw_line
($line);
5120 } elsif ($format eq 'plain') {
5121 open $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5122 '-p', $hash_parent_param, $hash, "--"
5123 or die_error
(500, "Open git-diff-tree failed");
5126 die_error
(400, "Unknown commitdiff format");
5129 # non-textual hash id's can be cached
5131 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5135 # write commit message
5136 if ($format eq 'html') {
5137 my $refs = git_get_references
();
5138 my $ref = format_ref_marker
($refs, $co{'id'});
5140 git_header_html
(undef, $expires);
5141 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5142 git_print_header_div
('commit', esc_html
($co{'title'}) . $ref, $hash);
5143 git_print_authorship
(\
%co);
5144 print "<div class=\"page_body\">\n";
5145 if (@{$co{'comment'}} > 1) {
5146 print "<div class=\"log\">\n";
5147 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
5148 print "</div>\n"; # class="log"
5151 } elsif ($format eq 'plain') {
5152 my $refs = git_get_references
("tags");
5153 my $tagname = git_get_rev_name_tags
($hash);
5154 my $filename = basename
($project) . "-$hash.patch";
5157 -type
=> 'text/plain',
5158 -charset
=> 'utf-8',
5159 -expires
=> $expires,
5160 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
5161 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
5162 print "From: " . to_utf8
($co{'author'}) . "\n";
5163 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5164 print "Subject: " . to_utf8
($co{'title'}) . "\n";
5166 print "X-Git-Tag: $tagname\n" if $tagname;
5167 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5169 foreach my $line (@{$co{'comment'}}) {
5170 print to_utf8
($line) . "\n";
5176 if ($format eq 'html') {
5177 my $use_parents = !defined $hash_parent ||
5178 $hash_parent eq '-c' || $hash_parent eq '--cc';
5179 git_difftree_body
(\
@difftree, $hash,
5180 $use_parents ? @{$co{'parents'}} : $hash_parent);
5183 git_patchset_body
($fd, \
@difftree, $hash,
5184 $use_parents ? @{$co{'parents'}} : $hash_parent);
5186 print "</div>\n"; # class="page_body"
5189 } elsif ($format eq 'plain') {
5193 or print "Reading git-diff-tree failed\n";
5197 sub git_commitdiff_plain
{
5198 git_commitdiff
('plain');
5202 if (!defined $hash_base) {
5203 $hash_base = git_get_head_hash
($project);
5205 if (!defined $page) {
5209 my %co = parse_commit
($hash_base)
5210 or die_error
(404, "Unknown commit object");
5212 my $refs = git_get_references
();
5213 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5215 my @commitlist = parse_commits
($hash_base, 101, (100 * $page),
5216 $file_name, "--full-history")
5217 or die_error
(404, "No such file or directory on given branch");
5219 if (!defined $hash && defined $file_name) {
5220 # some commits could have deleted file in question,
5221 # and not have it in tree, but one of them has to have it
5222 for (my $i = 0; $i <= @commitlist; $i++) {
5223 $hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
5224 last if defined $hash;
5227 if (defined $hash) {
5228 $ftype = git_get_type
($hash);
5230 if (!defined $ftype) {
5231 die_error
(500, "Unknown type of object");
5234 my $paging_nav = '';
5237 $cgi->a({-href
=> href
(action
=>"history", hash
=>$hash, hash_base
=>$hash_base,
5238 file_name
=>$file_name)},
5240 $paging_nav .= " ⋅ " .
5241 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5242 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5244 $paging_nav .= "first";
5245 $paging_nav .= " ⋅ prev";
5248 if ($#commitlist >= 100) {
5250 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5251 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5252 $paging_nav .= " ⋅ $next_link";
5254 $paging_nav .= " ⋅ next";
5258 git_print_page_nav
('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5259 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
5260 git_print_page_path
($file_name, $ftype, $hash_base);
5262 git_history_body
(\
@commitlist, 0, 99,
5263 $refs, $hash_base, $ftype, $next_link);
5269 gitweb_check_feature
('search') or die_error
(403, "Search is disabled");
5270 if (!defined $searchtext) {
5271 die_error
(400, "Text field is empty");
5273 if (!defined $hash) {
5274 $hash = git_get_head_hash
($project);
5276 my %co = parse_commit
($hash);
5278 die_error
(404, "Unknown commit object");
5280 if (!defined $page) {
5284 $searchtype ||= 'commit';
5285 if ($searchtype eq 'pickaxe') {
5286 # pickaxe may take all resources of your box and run for several minutes
5287 # with every query - so decide by yourself how public you make this feature
5288 gitweb_check_feature
('pickaxe')
5289 or die_error
(403, "Pickaxe is disabled");
5291 if ($searchtype eq 'grep') {
5292 gitweb_check_feature
('grep')
5293 or die_error
(403, "Grep is disabled");
5298 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5300 if ($searchtype eq 'commit') {
5301 $greptype = "--grep=";
5302 } elsif ($searchtype eq 'author') {
5303 $greptype = "--author=";
5304 } elsif ($searchtype eq 'committer') {
5305 $greptype = "--committer=";
5307 $greptype .= $searchtext;
5308 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
5309 $greptype, '--regexp-ignore-case',
5310 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5312 my $paging_nav = '';
5315 $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
5316 searchtext
=>$searchtext,
5317 searchtype
=>$searchtype)},
5319 $paging_nav .= " ⋅ " .
5320 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5321 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5323 $paging_nav .= "first";
5324 $paging_nav .= " ⋅ prev";
5327 if ($#commitlist >= 100) {
5329 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5330 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5331 $paging_nav .= " ⋅ $next_link";
5333 $paging_nav .= " ⋅ next";
5336 if ($#commitlist >= 100) {
5339 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
5340 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5341 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
5344 if ($searchtype eq 'pickaxe') {
5345 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5346 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5348 print "<table class=\"pickaxe search\">\n";
5351 open my $fd, '-|', git_cmd
(), '--no-pager', 'log', @diff_opts,
5352 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5353 ($search_use_regexp ? '--pickaxe-regex' : ());
5356 while (my $line = <$fd>) {
5360 my %set = parse_difftree_raw_line
($line);
5361 if (defined $set{'commit'}) {
5362 # finish previous commit
5365 "<td class=\"link\">" .
5366 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5368 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5374 print "<tr class=\"dark\">\n";
5376 print "<tr class=\"light\">\n";
5379 %co = parse_commit
($set{'commit'});
5380 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
5381 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5382 "<td><i>$author</i></td>\n" .
5384 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
5385 -class => "list subject"},
5386 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
5387 } elsif (defined $set{'to_id'}) {
5388 next if ($set{'to_id'} =~ m/^0{40}$/);
5390 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
5391 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
5393 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
5399 # finish last commit (warning: repetition!)
5402 "<td class=\"link\">" .
5403 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
5405 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
5413 if ($searchtype eq 'grep') {
5414 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
5415 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
5417 print "<table class=\"grep_search\">\n";
5421 open my $fd, "-|", git_cmd
(), 'grep', '-n',
5422 $search_use_regexp ? ('-E', '-i') : '-F',
5423 $searchtext, $co{'tree'};
5425 while (my $line = <$fd>) {
5427 my ($file, $lno, $ltext, $binary);
5428 last if ($matches++ > 1000);
5429 if ($line =~ /^Binary file (.+) matches$/) {
5433 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5435 if ($file ne $lastfile) {
5436 $lastfile and print "</td></tr>\n";
5438 print "<tr class=\"dark\">\n";
5440 print "<tr class=\"light\">\n";
5442 print "<td class=\"list\">".
5443 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5444 file_name
=>"$file"),
5445 -class => "list"}, esc_path
($file));
5446 print "</td><td>\n";
5450 print "<div class=\"binary\">Binary file</div>\n";
5452 $ltext = untabify
($ltext);
5453 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5454 $ltext = esc_html
($1, -nbsp
=>1);
5455 $ltext .= '<span class="match">';
5456 $ltext .= esc_html
($2, -nbsp
=>1);
5457 $ltext .= '</span>';
5458 $ltext .= esc_html
($3, -nbsp
=>1);
5460 $ltext = esc_html
($ltext, -nbsp
=>1);
5462 print "<div class=\"pre\">" .
5463 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$co{'hash'},
5464 file_name
=>"$file").'#l'.$lno,
5465 -class => "linenr"}, sprintf('%4i', $lno))
5466 . ' ' . $ltext . "</div>\n";
5470 print "</td></tr>\n";
5471 if ($matches > 1000) {
5472 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5475 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5484 sub git_search_help
{
5486 git_print_page_nav
('','', $hash,$hash,$hash);
5488 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5489 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5490 the pattern entered is recognized as the POSIX extended
5491 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5494 <dt><b>commit</b></dt>
5495 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5497 my ($have_grep) = gitweb_check_feature
('grep');
5500 <dt><b>grep</b></dt>
5501 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5502 a different one) are searched for the given pattern. On large trees, this search can take
5503 a while and put some strain on the server, so please use it with some consideration. Note that
5504 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5505 case-sensitive.</dd>
5509 <dt><b>author</b></dt>
5510 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5511 <dt><b>committer</b></dt>
5512 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5514 my ($have_pickaxe) = gitweb_check_feature
('pickaxe');
5515 if ($have_pickaxe) {
5517 <dt><b>pickaxe</b></dt>
5518 <dd>All commits that caused the string to appear or disappear from any file (changes that
5519 added, removed or "modified" the string) will be listed. This search can take a while and
5520 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5521 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5529 my $head = git_get_head_hash
($project);
5530 if (!defined $hash) {
5533 if (!defined $page) {
5536 my $refs = git_get_references
();
5538 my $commit_hash = $hash;
5539 if (defined $hash_parent) {
5540 $commit_hash = "$hash_parent..$hash";
5542 my @commitlist = parse_commits
($commit_hash, 101, (100 * $page));
5544 my $paging_nav = format_paging_nav
('shortlog', $hash, $head, $page, $#commitlist >= 100);
5546 if ($#commitlist >= 100) {
5548 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5549 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5553 git_print_page_nav
('shortlog','', $hash,$hash,$hash, $paging_nav);
5554 git_print_header_div
('summary', $project);
5556 git_shortlog_body
(\
@commitlist, 0, 99, $refs, $next_link);
5561 ## ......................................................................
5562 ## feeds (RSS, Atom; OPML)
5565 my $format = shift || 'atom';
5566 my ($have_blame) = gitweb_check_feature
('blame');
5568 # Atom: http://www.atomenabled.org/developers/syndication/
5569 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5570 if ($format ne 'rss' && $format ne 'atom') {
5571 die_error
(400, "Unknown web feed format");
5574 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5575 my $head = $hash || 'HEAD';
5576 my @commitlist = parse_commits
($head, 150, 0, $file_name);
5580 my $content_type = "application/$format+xml";
5581 if (defined $cgi->http('HTTP_ACCEPT') &&
5582 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5583 # browser (feed reader) prefers text/xml
5584 $content_type = 'text/xml';
5586 if (defined($commitlist[0])) {
5587 %latest_commit = %{$commitlist[0]};
5588 %latest_date = parse_date
($latest_commit{'author_epoch'});
5590 -type
=> $content_type,
5591 -charset
=> 'utf-8',
5592 -last_modified
=> $latest_date{'rfc2822'});
5595 -type
=> $content_type,
5596 -charset
=> 'utf-8');
5599 # Optimization: skip generating the body if client asks only
5600 # for Last-Modified date.
5601 return if ($cgi->request_method() eq 'HEAD');
5604 my $title = "$site_name - $project/$action";
5605 my $feed_type = 'log';
5606 if (defined $hash) {
5607 $title .= " - '$hash'";
5608 $feed_type = 'branch log';
5609 if (defined $file_name) {
5610 $title .= " :: $file_name";
5611 $feed_type = 'history';
5613 } elsif (defined $file_name) {
5614 $title .= " - $file_name";
5615 $feed_type = 'history';
5617 $title .= " $feed_type";
5618 my $descr = git_get_project_description
($project);
5619 if (defined $descr) {
5620 $descr = esc_html
($descr);
5622 $descr = "$project " .
5623 ($format eq 'rss' ? 'RSS' : 'Atom') .
5626 my $owner = git_get_project_owner
($project);
5627 $owner = esc_html
($owner);
5631 if (defined $file_name) {
5632 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
5633 } elsif (defined $hash) {
5634 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
5636 $alt_url = href
(-full
=>1, action
=>"summary");
5638 print qq
!<?xml version
="1.0" encoding
="utf-8"?>\n!;
5639 if ($format eq 'rss') {
5641 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5644 print "<title>$title</title>\n" .
5645 "<link>$alt_url</link>\n" .
5646 "<description>$descr</description>\n" .
5647 "<language>en</language>\n";
5648 } elsif ($format eq 'atom') {
5650 <feed xmlns="http://www.w3.org/2005/Atom">
5652 print "<title>$title</title>\n" .
5653 "<subtitle>$descr</subtitle>\n" .
5654 '<link rel="alternate" type="text/html" href="' .
5655 $alt_url . '" />' . "\n" .
5656 '<link rel="self" type="' . $content_type . '" href="' .
5657 $cgi->self_url() . '" />' . "\n" .
5658 "<id>" . href
(-full
=>1) . "</id>\n" .
5659 # use project owner for feed author
5660 "<author><name>$owner</name></author>\n";
5661 if (defined $favicon) {
5662 print "<icon>" . esc_url
($favicon) . "</icon>\n";
5664 if (defined $logo_url) {
5665 # not twice as wide as tall: 72 x 27 pixels
5666 print "<logo>" . esc_url
($logo) . "</logo>\n";
5668 if (! %latest_date) {
5669 # dummy date to keep the feed valid until commits trickle in:
5670 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5672 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5677 for (my $i = 0; $i <= $#commitlist; $i++) {
5678 my %co = %{$commitlist[$i]};
5679 my $commit = $co{'id'};
5680 # we read 150, we always show 30 and the ones more recent than 48 hours
5681 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5684 my %cd = parse_date
($co{'author_epoch'});
5686 # get list of changed files
5687 open my $fd, "-|", git_cmd
(), "diff-tree", '-r', @diff_opts,
5688 $co{'parent'} || "--root",
5689 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5691 my @difftree = map { chomp; $_ } <$fd>;
5695 # print element (entry, item)
5696 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
5697 if ($format eq 'rss') {
5699 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
5700 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
5701 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5702 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5703 "<link>$co_url</link>\n" .
5704 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
5705 "<content:encoded>" .
5707 } elsif ($format eq 'atom') {
5709 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
5710 "<updated>$cd{'iso-8601'}</updated>\n" .
5712 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
5713 if ($co{'author_email'}) {
5714 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
5716 print "</author>\n" .
5717 # use committer for contributor
5719 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
5720 if ($co{'committer_email'}) {
5721 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
5723 print "</contributor>\n" .
5724 "<published>$cd{'iso-8601'}</published>\n" .
5725 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5726 "<id>$co_url</id>\n" .
5727 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
5728 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5730 my $comment = $co{'comment'};
5732 foreach my $line (@$comment) {
5733 $line = esc_html
($line);
5736 print "</pre><ul>\n";
5737 foreach my $difftree_line (@difftree) {
5738 my %difftree = parse_difftree_raw_line
($difftree_line);
5739 next if !$difftree{'from_id'};
5741 my $file = $difftree{'file'} || $difftree{'to_file'};
5745 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
5746 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
5747 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
5748 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
5749 -title
=> "diff"}, 'D');
5751 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
5752 file_name
=>$file, hash_base
=>$commit),
5753 -title
=> "blame"}, 'B');
5755 # if this is not a feed of a file history
5756 if (!defined $file_name || $file_name ne $file) {
5757 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
5758 file_name
=>$file, hash
=>$commit),
5759 -title
=> "history"}, 'H');
5761 $file = esc_path
($file);
5765 if ($format eq 'rss') {
5766 print "</ul>]]>\n" .
5767 "</content:encoded>\n" .
5769 } elsif ($format eq 'atom') {
5770 print "</ul>\n</div>\n" .
5777 if ($format eq 'rss') {
5778 print "</channel>\n</rss>\n";
5779 } elsif ($format eq 'atom') {
5793 my @list = git_get_projects_list
();
5795 print $cgi->header(-type
=> 'text/xml', -charset
=> 'utf-8');
5797 <?xml version="1.0" encoding="utf-8"?>
5798 <opml version="1.0">
5800 <title>$site_name OPML Export</title>
5803 <outline text="git RSS feeds">
5806 foreach my $pr (@list) {
5808 my $head = git_get_head_hash
($proj{'path'});
5809 if (!defined $head) {
5812 $git_dir = "$projectroot/$proj{'path'}";
5813 my %co = parse_commit
($head);
5818 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
5819 my $rss = "$my_url?p=$proj{'path'};a=rss";
5820 my $html = "$my_url?p=$proj{'path'};a=summary";
5821 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";