id=metadata_ctags
[git/gitweb.git] / gitweb / gitweb.perl
blob02083d517656dd8dc14bc2be449e9cf35c0136c6
1 #!/usr/bin/perl
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
10 use strict;
11 use warnings;
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser);
15 use Encode;
16 use Fcntl ':mode';
17 use File::Find qw();
18 use File::Basename qw(basename);
19 binmode STDOUT, ':utf8';
21 BEGIN {
22 CGI->compile() if $ENV{'MOD_PERL'};
25 our $cgi = new CGI;
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute => 1);
30 # core git executable to use
31 # this can just be "git" if your webserver has a sensible PATH
32 our $GIT = "++GIT_BINDIR++/git";
34 # absolute fs-path which will be prepended to the project path
35 #our $projectroot = "/pub/scm";
36 our $projectroot = "++GITWEB_PROJECTROOT++";
38 # fs traversing limit for getting project list
39 # the number is relative to the projectroot
40 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
42 # target of the home link on top of all pages
43 our $home_link = $my_uri || "/";
45 # string of the home link on top of all pages
46 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
48 # name of your site or organization to appear in page titles
49 # replace this with something more descriptive for clearer bookmarks
50 our $site_name = "++GITWEB_SITENAME++"
51 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
53 # filename of html text to include at top of each page
54 our $site_header = "++GITWEB_SITE_HEADER++";
55 # html text to include at home page
56 our $home_text = "++GITWEB_HOMETEXT++";
57 # filename of html text to include at bottom of each page
58 our $site_footer = "++GITWEB_SITE_FOOTER++";
60 # URI of stylesheets
61 our @stylesheets = ("++GITWEB_CSS++");
62 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
63 our $stylesheet = undef;
64 # URI of GIT logo (72x27 size)
65 our $logo = "++GITWEB_LOGO++";
66 # URI of GIT favicon, assumed to be image/png type
67 our $favicon = "++GITWEB_FAVICON++";
69 # URI and label (title) of GIT logo link
70 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
71 #our $logo_label = "git documentation";
72 our $logo_url = "http://git.or.cz/";
73 our $logo_label = "git homepage";
75 # source of projects list
76 our $projects_list = "++GITWEB_LIST++";
78 # the width (in characters) of the projects list "Description" column
79 our $projects_list_description_width = 25;
81 # default order of projects list
82 # valid values are none, project, descr, owner, and age
83 our $default_projects_order = "project";
85 # show repository only if this file exists
86 # (only effective if this variable evaluates to true)
87 our $export_ok = "++GITWEB_EXPORT_OK++";
89 # only allow viewing of repositories also shown on the overview page
90 our $strict_export = "++GITWEB_STRICT_EXPORT++";
92 # list of git base URLs used for URL to where fetch project from,
93 # i.e. full URL is "$git_base_url/$project"
94 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
96 # default blob_plain mimetype and default charset for text/plain blob
97 our $default_blob_plain_mimetype = 'text/plain';
98 our $default_text_plain_charset = undef;
100 # file to use for guessing MIME types before trying /etc/mime.types
101 # (relative to the current git repository)
102 our $mimetypes_file = undef;
104 # assume this charset if line contains non-UTF-8 characters;
105 # it should be valid encoding (see Encoding::Supported(3pm) for list),
106 # for which encoding all byte sequences are valid, for example
107 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
108 # could be even 'utf-8' for the old behavior)
109 our $fallback_encoding = 'latin1';
111 # rename detection options for git-diff and git-diff-tree
112 # - default is '-M', with the cost proportional to
113 # (number of removed files) * (number of new files).
114 # - more costly is '-C' (which implies '-M'), with the cost proportional to
115 # (number of changed files + number of removed files) * (number of new files)
116 # - even more costly is '-C', '--find-copies-harder' with cost
117 # (number of files in the original tree) * (number of new files)
118 # - one might want to include '-B' option, e.g. '-B', '-M'
119 our @diff_opts = ('-M'); # taken from git_commit
121 # information about snapshot formats that gitweb is capable of serving
122 our %known_snapshot_formats = (
123 # name => {
124 # 'display' => display name,
125 # 'type' => mime type,
126 # 'suffix' => filename suffix,
127 # 'format' => --format for git-archive,
128 # 'compressor' => [compressor command and arguments]
129 # (array reference, optional)}
131 'tgz' => {
132 'display' => 'tar.gz',
133 'type' => 'application/x-gzip',
134 'suffix' => '.tar.gz',
135 'format' => 'tar',
136 'compressor' => ['gzip']},
138 'tbz2' => {
139 'display' => 'tar.bz2',
140 'type' => 'application/x-bzip2',
141 'suffix' => '.tar.bz2',
142 'format' => 'tar',
143 'compressor' => ['bzip2']},
145 'zip' => {
146 'display' => 'zip',
147 'type' => 'application/x-zip',
148 'suffix' => '.zip',
149 'format' => 'zip'},
152 # Aliases so we understand old gitweb.snapshot values in repository
153 # configuration.
154 our %known_snapshot_format_aliases = (
155 'gzip' => 'tgz',
156 'bzip2' => 'tbz2',
158 # backward compatibility: legacy gitweb config support
159 'x-gzip' => undef, 'gz' => undef,
160 'x-bzip2' => undef, 'bz2' => undef,
161 'x-zip' => undef, '' => undef,
164 # You define site-wide feature defaults here; override them with
165 # $GITWEB_CONFIG as necessary.
166 our %feature = (
167 # feature => {
168 # 'sub' => feature-sub (subroutine),
169 # 'override' => allow-override (boolean),
170 # 'default' => [ default options...] (array reference)}
172 # if feature is overridable (it means that allow-override has true value),
173 # then feature-sub will be called with default options as parameters;
174 # return value of feature-sub indicates if to enable specified feature
176 # if there is no 'sub' key (no feature-sub), then feature cannot be
177 # overriden
179 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
181 # Enable the 'blame' blob view, showing the last commit that modified
182 # each line in the file. This can be very CPU-intensive.
184 # To enable system wide have in $GITWEB_CONFIG
185 # $feature{'blame'}{'default'} = [1];
186 # To have project specific config enable override in $GITWEB_CONFIG
187 # $feature{'blame'}{'override'} = 1;
188 # and in project config gitweb.blame = 0|1;
189 'blame' => {
190 'sub' => \&feature_blame,
191 'override' => 0,
192 'default' => [0]},
194 # Enable the 'snapshot' link, providing a compressed archive of any
195 # tree. This can potentially generate high traffic if you have large
196 # project.
198 # Value is a list of formats defined in %known_snapshot_formats that
199 # you wish to offer.
200 # To disable system wide have in $GITWEB_CONFIG
201 # $feature{'snapshot'}{'default'} = [];
202 # To have project specific config enable override in $GITWEB_CONFIG
203 # $feature{'snapshot'}{'override'} = 1;
204 # and in project config, a comma-separated list of formats or "none"
205 # to disable. Example: gitweb.snapshot = tbz2,zip;
206 'snapshot' => {
207 'sub' => \&feature_snapshot,
208 'override' => 0,
209 'default' => ['tgz']},
211 # Enable text search, which will list the commits which match author,
212 # committer or commit text to a given string. Enabled by default.
213 # Project specific override is not supported.
214 'search' => {
215 'override' => 0,
216 'default' => [1]},
218 # Enable grep search, which will list the files in currently selected
219 # tree containing the given string. Enabled by default. This can be
220 # potentially CPU-intensive, of course.
222 # To enable system wide have in $GITWEB_CONFIG
223 # $feature{'grep'}{'default'} = [1];
224 # To have project specific config enable override in $GITWEB_CONFIG
225 # $feature{'grep'}{'override'} = 1;
226 # and in project config gitweb.grep = 0|1;
227 'grep' => {
228 'override' => 0,
229 'default' => [1]},
231 # Enable the pickaxe search, which will list the commits that modified
232 # a given string in a file. This can be practical and quite faster
233 # alternative to 'blame', but still potentially CPU-intensive.
235 # To enable system wide have in $GITWEB_CONFIG
236 # $feature{'pickaxe'}{'default'} = [1];
237 # To have project specific config enable override in $GITWEB_CONFIG
238 # $feature{'pickaxe'}{'override'} = 1;
239 # and in project config gitweb.pickaxe = 0|1;
240 'pickaxe' => {
241 'sub' => \&feature_pickaxe,
242 'override' => 0,
243 'default' => [1]},
245 # Make gitweb use an alternative format of the URLs which can be
246 # more readable and natural-looking: project name is embedded
247 # directly in the path and the query string contains other
248 # auxiliary information. All gitweb installations recognize
249 # URL in either format; this configures in which formats gitweb
250 # generates links.
252 # To enable system wide have in $GITWEB_CONFIG
253 # $feature{'pathinfo'}{'default'} = [1];
254 # Project specific override is not supported.
256 # Note that you will need to change the default location of CSS,
257 # favicon, logo and possibly other files to an absolute URL. Also,
258 # if gitweb.cgi serves as your indexfile, you will need to force
259 # $my_uri to contain the script name in your $GITWEB_CONFIG.
260 'pathinfo' => {
261 'override' => 0,
262 'default' => [0]},
264 # Make gitweb consider projects in project root subdirectories
265 # to be forks of existing projects. Given project $projname.git,
266 # projects matching $projname/*.git will not be shown in the main
267 # projects list, instead a '+' mark will be added to $projname
268 # there and a 'forks' view will be enabled for the project, listing
269 # all the forks. If project list is taken from a file, forks have
270 # to be listed after the main project.
272 # To enable system wide have in $GITWEB_CONFIG
273 # $feature{'forks'}{'default'} = [1];
274 # Project specific override is not supported.
275 'forks' => {
276 'override' => 0,
277 'default' => [0]},
279 # Allow gitweb scan project content tags described in ctags/
280 # of project repository, and display the popular Web 2.0-ish
281 # "tag cloud" near the project list. Note that this is something
282 # COMPLETELY different from the normal Git tags.
284 # gitweb by itself can show existing tags, but it does not handle
285 # tagging itself; you need an external application for that.
286 # For an example script, check Girocco's cgi/tagproj.cgi.
288 # To enable system wide have in $GITWEB_CONFIG
289 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
290 # Project specific override is not supported.
291 'ctags' => {
292 'override' => 0,
293 'default' => [0]},
296 sub gitweb_check_feature {
297 my ($name) = @_;
298 return unless exists $feature{$name};
299 my ($sub, $override, @defaults) = (
300 $feature{$name}{'sub'},
301 $feature{$name}{'override'},
302 @{$feature{$name}{'default'}});
303 if (!$override) { return @defaults; }
304 if (!defined $sub) {
305 warn "feature $name is not overrideable";
306 return @defaults;
308 return $sub->(@defaults);
311 sub feature_blame {
312 my ($val) = git_get_project_config('blame', '--bool');
314 if ($val eq 'true') {
315 return 1;
316 } elsif ($val eq 'false') {
317 return 0;
320 return $_[0];
323 sub feature_snapshot {
324 my (@fmts) = @_;
326 my ($val) = git_get_project_config('snapshot');
328 if ($val) {
329 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
332 return @fmts;
335 sub feature_grep {
336 my ($val) = git_get_project_config('grep', '--bool');
338 if ($val eq 'true') {
339 return (1);
340 } elsif ($val eq 'false') {
341 return (0);
344 return ($_[0]);
347 sub feature_pickaxe {
348 my ($val) = git_get_project_config('pickaxe', '--bool');
350 if ($val eq 'true') {
351 return (1);
352 } elsif ($val eq 'false') {
353 return (0);
356 return ($_[0]);
359 # checking HEAD file with -e is fragile if the repository was
360 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
361 # and then pruned.
362 sub check_head_link {
363 my ($dir) = @_;
364 my $headfile = "$dir/HEAD";
365 return ((-e $headfile) ||
366 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
369 sub check_export_ok {
370 my ($dir) = @_;
371 return (check_head_link($dir) &&
372 (!$export_ok || -e "$dir/$export_ok"));
375 # process alternate names for backward compatibility
376 # filter out unsupported (unknown) snapshot formats
377 sub filter_snapshot_fmts {
378 my @fmts = @_;
380 @fmts = map {
381 exists $known_snapshot_format_aliases{$_} ?
382 $known_snapshot_format_aliases{$_} : $_} @fmts;
383 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
387 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
388 if (-e $GITWEB_CONFIG) {
389 do $GITWEB_CONFIG;
390 } else {
391 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
392 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
395 # version of the core git binary
396 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
398 $projects_list ||= $projectroot;
400 # ======================================================================
401 # input validation and dispatch
402 our $action = $cgi->param('a');
403 if (defined $action) {
404 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
405 die_error(400, "Invalid action parameter");
409 # parameters which are pathnames
410 our $project = $cgi->param('p');
411 if (defined $project) {
412 if (!validate_pathname($project) ||
413 !(-d "$projectroot/$project") ||
414 !check_head_link("$projectroot/$project") ||
415 ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
416 ($strict_export && !project_in_list($project))) {
417 undef $project;
418 die_error(404, "No such project");
422 our $file_name = $cgi->param('f');
423 if (defined $file_name) {
424 if (!validate_pathname($file_name)) {
425 die_error(400, "Invalid file parameter");
429 our $file_parent = $cgi->param('fp');
430 if (defined $file_parent) {
431 if (!validate_pathname($file_parent)) {
432 die_error(400, "Invalid file parent parameter");
436 # parameters which are refnames
437 our $hash = $cgi->param('h');
438 if (defined $hash) {
439 if (!validate_refname($hash)) {
440 die_error(400, "Invalid hash parameter");
444 our $hash_parent = $cgi->param('hp');
445 if (defined $hash_parent) {
446 if (!validate_refname($hash_parent)) {
447 die_error(400, "Invalid hash parent parameter");
451 our $hash_base = $cgi->param('hb');
452 if (defined $hash_base) {
453 if (!validate_refname($hash_base)) {
454 die_error(400, "Invalid hash base parameter");
458 my %allowed_options = (
459 "--no-merges" => [ qw(rss atom log shortlog history) ],
462 our @extra_options = $cgi->param('opt');
463 if (defined @extra_options) {
464 foreach my $opt (@extra_options) {
465 if (not exists $allowed_options{$opt}) {
466 die_error(400, "Invalid option parameter");
468 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
469 die_error(400, "Invalid option parameter for this action");
474 our $hash_parent_base = $cgi->param('hpb');
475 if (defined $hash_parent_base) {
476 if (!validate_refname($hash_parent_base)) {
477 die_error(400, "Invalid hash parent base parameter");
481 # other parameters
482 our $page = $cgi->param('pg');
483 if (defined $page) {
484 if ($page =~ m/[^0-9]/) {
485 die_error(400, "Invalid page parameter");
489 our $searchtype = $cgi->param('st');
490 if (defined $searchtype) {
491 if ($searchtype =~ m/[^a-z]/) {
492 die_error(400, "Invalid searchtype parameter");
496 our $search_use_regexp = $cgi->param('sr');
498 our $searchtext = $cgi->param('s');
499 our $search_regexp;
500 if (defined $searchtext) {
501 if (length($searchtext) < 2) {
502 die_error(403, "At least two characters are required for search parameter");
504 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
507 # now read PATH_INFO and use it as alternative to parameters
508 sub evaluate_path_info {
509 return if defined $project;
510 my $path_info = $ENV{"PATH_INFO"};
511 return if !$path_info;
512 $path_info =~ s,^/+,,;
513 return if !$path_info;
514 # find which part of PATH_INFO is project
515 $project = $path_info;
516 $project =~ s,/+$,,;
517 while ($project && !check_head_link("$projectroot/$project")) {
518 $project =~ s,/*[^/]*$,,;
520 # validate project
521 $project = validate_pathname($project);
522 if (!$project ||
523 ($export_ok && !-e "$projectroot/$project/$export_ok") ||
524 ($strict_export && !project_in_list($project))) {
525 undef $project;
526 return;
528 # do not change any parameters if an action is given using the query string
529 return if $action;
530 $path_info =~ s,^\Q$project\E/*,,;
531 my ($refname, $pathname) = split(/:/, $path_info, 2);
532 if (defined $pathname) {
533 # we got "project.git/branch:filename" or "project.git/branch:dir/"
534 # we could use git_get_type(branch:pathname), but it needs $git_dir
535 $pathname =~ s,^/+,,;
536 if (!$pathname || substr($pathname, -1) eq "/") {
537 $action ||= "tree";
538 $pathname =~ s,/$,,;
539 } else {
540 $action ||= "blob_plain";
542 $hash_base ||= validate_refname($refname);
543 $file_name ||= validate_pathname($pathname);
544 } elsif (defined $refname) {
545 # we got "project.git/branch"
546 $action ||= "shortlog";
547 $hash ||= validate_refname($refname);
550 evaluate_path_info();
552 # path to the current git repository
553 our $git_dir;
554 $git_dir = "$projectroot/$project" if $project;
556 # dispatch
557 my %actions = (
558 "blame" => \&git_blame,
559 "blobdiff" => \&git_blobdiff,
560 "blobdiff_plain" => \&git_blobdiff_plain,
561 "blob" => \&git_blob,
562 "blob_plain" => \&git_blob_plain,
563 "commitdiff" => \&git_commitdiff,
564 "commitdiff_plain" => \&git_commitdiff_plain,
565 "commit" => \&git_commit,
566 "forks" => \&git_forks,
567 "heads" => \&git_heads,
568 "history" => \&git_history,
569 "log" => \&git_log,
570 "rss" => \&git_rss,
571 "atom" => \&git_atom,
572 "search" => \&git_search,
573 "search_help" => \&git_search_help,
574 "shortlog" => \&git_shortlog,
575 "summary" => \&git_summary,
576 "tag" => \&git_tag,
577 "tags" => \&git_tags,
578 "tree" => \&git_tree,
579 "snapshot" => \&git_snapshot,
580 "object" => \&git_object,
581 # those below don't need $project
582 "opml" => \&git_opml,
583 "project_list" => \&git_project_list,
584 "project_index" => \&git_project_index,
587 if (!defined $action) {
588 if (defined $hash) {
589 $action = git_get_type($hash);
590 } elsif (defined $hash_base && defined $file_name) {
591 $action = git_get_type("$hash_base:$file_name");
592 } elsif (defined $project) {
593 $action = 'summary';
594 } else {
595 $action = 'project_list';
598 if (!defined($actions{$action})) {
599 die_error(400, "Unknown action");
601 if ($action !~ m/^(opml|project_list|project_index)$/ &&
602 !$project) {
603 die_error(400, "Project needed");
605 $actions{$action}->();
606 exit;
608 ## ======================================================================
609 ## action links
611 sub href (%) {
612 my %params = @_;
613 # default is to use -absolute url() i.e. $my_uri
614 my $href = $params{-full} ? $my_url : $my_uri;
616 # XXX: Warning: If you touch this, check the search form for updating,
617 # too.
619 my @mapping = (
620 project => "p",
621 action => "a",
622 file_name => "f",
623 file_parent => "fp",
624 hash => "h",
625 hash_parent => "hp",
626 hash_base => "hb",
627 hash_parent_base => "hpb",
628 page => "pg",
629 order => "o",
630 searchtext => "s",
631 searchtype => "st",
632 snapshot_format => "sf",
633 extra_options => "opt",
634 search_use_regexp => "sr",
636 my %mapping = @mapping;
638 $params{'project'} = $project unless exists $params{'project'};
640 if ($params{-replay}) {
641 while (my ($name, $symbol) = each %mapping) {
642 if (!exists $params{$name}) {
643 # to allow for multivalued params we use arrayref form
644 $params{$name} = [ $cgi->param($symbol) ];
649 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
650 if ($use_pathinfo) {
651 # use PATH_INFO for project name
652 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
653 delete $params{'project'};
655 # Summary just uses the project path URL
656 if (defined $params{'action'} && $params{'action'} eq 'summary') {
657 delete $params{'action'};
661 # now encode the parameters explicitly
662 my @result = ();
663 for (my $i = 0; $i < @mapping; $i += 2) {
664 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
665 if (defined $params{$name}) {
666 if (ref($params{$name}) eq "ARRAY") {
667 foreach my $par (@{$params{$name}}) {
668 push @result, $symbol . "=" . esc_param($par);
670 } else {
671 push @result, $symbol . "=" . esc_param($params{$name});
675 $href .= "?" . join(';', @result) if scalar @result;
677 return $href;
681 ## ======================================================================
682 ## validation, quoting/unquoting and escaping
684 sub validate_pathname {
685 my $input = shift || return undef;
687 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
688 # at the beginning, at the end, and between slashes.
689 # also this catches doubled slashes
690 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
691 return undef;
693 # no null characters
694 if ($input =~ m!\0!) {
695 return undef;
697 return $input;
700 sub validate_refname {
701 my $input = shift || return undef;
703 # textual hashes are O.K.
704 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
705 return $input;
707 # it must be correct pathname
708 $input = validate_pathname($input)
709 or return undef;
710 # restrictions on ref name according to git-check-ref-format
711 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
712 return undef;
714 return $input;
717 # decode sequences of octets in utf8 into Perl's internal form,
718 # which is utf-8 with utf8 flag set if needed. gitweb writes out
719 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
720 sub to_utf8 {
721 my $str = shift;
722 if (utf8::valid($str)) {
723 utf8::decode($str);
724 return $str;
725 } else {
726 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
730 # quote unsafe chars, but keep the slash, even when it's not
731 # correct, but quoted slashes look too horrible in bookmarks
732 sub esc_param {
733 my $str = shift;
734 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
735 $str =~ s/\+/%2B/g;
736 $str =~ s/ /\+/g;
737 return $str;
740 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
741 sub esc_url {
742 my $str = shift;
743 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
744 $str =~ s/\+/%2B/g;
745 $str =~ s/ /\+/g;
746 return $str;
749 # replace invalid utf8 character with SUBSTITUTION sequence
750 sub esc_html ($;%) {
751 my $str = shift;
752 my %opts = @_;
754 $str = to_utf8($str);
755 $str = $cgi->escapeHTML($str);
756 if ($opts{'-nbsp'}) {
757 $str =~ s/ /&nbsp;/g;
759 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
760 return $str;
763 # quote control characters and escape filename to HTML
764 sub esc_path {
765 my $str = shift;
766 my %opts = @_;
768 $str = to_utf8($str);
769 $str = $cgi->escapeHTML($str);
770 if ($opts{'-nbsp'}) {
771 $str =~ s/ /&nbsp;/g;
773 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
774 return $str;
777 # Make control characters "printable", using character escape codes (CEC)
778 sub quot_cec {
779 my $cntrl = shift;
780 my %opts = @_;
781 my %es = ( # character escape codes, aka escape sequences
782 "\t" => '\t', # tab (HT)
783 "\n" => '\n', # line feed (LF)
784 "\r" => '\r', # carrige return (CR)
785 "\f" => '\f', # form feed (FF)
786 "\b" => '\b', # backspace (BS)
787 "\a" => '\a', # alarm (bell) (BEL)
788 "\e" => '\e', # escape (ESC)
789 "\013" => '\v', # vertical tab (VT)
790 "\000" => '\0', # nul character (NUL)
792 my $chr = ( (exists $es{$cntrl})
793 ? $es{$cntrl}
794 : sprintf('\%03o', ord($cntrl)) );
795 if ($opts{-nohtml}) {
796 return $chr;
797 } else {
798 return "<span class=\"cntrl\">$chr</span>";
802 # Alternatively use unicode control pictures codepoints,
803 # Unicode "printable representation" (PR)
804 sub quot_upr {
805 my $cntrl = shift;
806 my %opts = @_;
808 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
809 if ($opts{-nohtml}) {
810 return $chr;
811 } else {
812 return "<span class=\"cntrl\">$chr</span>";
816 # git may return quoted and escaped filenames
817 sub unquote {
818 my $str = shift;
820 sub unq {
821 my $seq = shift;
822 my %es = ( # character escape codes, aka escape sequences
823 't' => "\t", # tab (HT, TAB)
824 'n' => "\n", # newline (NL)
825 'r' => "\r", # return (CR)
826 'f' => "\f", # form feed (FF)
827 'b' => "\b", # backspace (BS)
828 'a' => "\a", # alarm (bell) (BEL)
829 'e' => "\e", # escape (ESC)
830 'v' => "\013", # vertical tab (VT)
833 if ($seq =~ m/^[0-7]{1,3}$/) {
834 # octal char sequence
835 return chr(oct($seq));
836 } elsif (exists $es{$seq}) {
837 # C escape sequence, aka character escape code
838 return $es{$seq};
840 # quoted ordinary character
841 return $seq;
844 if ($str =~ m/^"(.*)"$/) {
845 # needs unquoting
846 $str = $1;
847 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
849 return $str;
852 # escape tabs (convert tabs to spaces)
853 sub untabify {
854 my $line = shift;
856 while ((my $pos = index($line, "\t")) != -1) {
857 if (my $count = (8 - ($pos % 8))) {
858 my $spaces = ' ' x $count;
859 $line =~ s/\t/$spaces/;
863 return $line;
866 sub project_in_list {
867 my $project = shift;
868 my @list = git_get_projects_list();
869 return @list && scalar(grep { $_->{'path'} eq $project } @list);
872 ## ----------------------------------------------------------------------
873 ## HTML aware string manipulation
875 # Try to chop given string on a word boundary between position
876 # $len and $len+$add_len. If there is no word boundary there,
877 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
878 # (marking chopped part) would be longer than given string.
879 sub chop_str {
880 my $str = shift;
881 my $len = shift;
882 my $add_len = shift || 10;
883 my $where = shift || 'right'; # 'left' | 'center' | 'right'
885 # Make sure perl knows it is utf8 encoded so we don't
886 # cut in the middle of a utf8 multibyte char.
887 $str = to_utf8($str);
889 # allow only $len chars, but don't cut a word if it would fit in $add_len
890 # if it doesn't fit, cut it if it's still longer than the dots we would add
891 # remove chopped character entities entirely
893 # when chopping in the middle, distribute $len into left and right part
894 # return early if chopping wouldn't make string shorter
895 if ($where eq 'center') {
896 return $str if ($len + 5 >= length($str)); # filler is length 5
897 $len = int($len/2);
898 } else {
899 return $str if ($len + 4 >= length($str)); # filler is length 4
902 # regexps: ending and beginning with word part up to $add_len
903 my $endre = qr/.{$len}\w{0,$add_len}/;
904 my $begre = qr/\w{0,$add_len}.{$len}/;
906 if ($where eq 'left') {
907 $str =~ m/^(.*?)($begre)$/;
908 my ($lead, $body) = ($1, $2);
909 if (length($lead) > 4) {
910 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
911 $lead = " ...";
913 return "$lead$body";
915 } elsif ($where eq 'center') {
916 $str =~ m/^($endre)(.*)$/;
917 my ($left, $str) = ($1, $2);
918 $str =~ m/^(.*?)($begre)$/;
919 my ($mid, $right) = ($1, $2);
920 if (length($mid) > 5) {
921 $left =~ s/&[^;]*$//;
922 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
923 $mid = " ... ";
925 return "$left$mid$right";
927 } else {
928 $str =~ m/^($endre)(.*)$/;
929 my $body = $1;
930 my $tail = $2;
931 if (length($tail) > 4) {
932 $body =~ s/&[^;]*$//;
933 $tail = "... ";
935 return "$body$tail";
939 # takes the same arguments as chop_str, but also wraps a <span> around the
940 # result with a title attribute if it does get chopped. Additionally, the
941 # string is HTML-escaped.
942 sub chop_and_escape_str {
943 my ($str) = @_;
945 my $chopped = chop_str(@_);
946 if ($chopped eq $str) {
947 return esc_html($chopped);
948 } else {
949 $str =~ s/([[:cntrl:]])/?/g;
950 return $cgi->span({-title=>$str}, esc_html($chopped));
954 ## ----------------------------------------------------------------------
955 ## functions returning short strings
957 # CSS class for given age value (in seconds)
958 sub age_class {
959 my $age = shift;
961 if (!defined $age) {
962 return "noage";
963 } elsif ($age < 60*60*2) {
964 return "age0";
965 } elsif ($age < 60*60*24*2) {
966 return "age1";
967 } else {
968 return "age2";
972 # convert age in seconds to "nn units ago" string
973 sub age_string {
974 my $age = shift;
975 my $age_str;
977 if ($age > 60*60*24*365*2) {
978 $age_str = (int $age/60/60/24/365);
979 $age_str .= " years ago";
980 } elsif ($age > 60*60*24*(365/12)*2) {
981 $age_str = int $age/60/60/24/(365/12);
982 $age_str .= " months ago";
983 } elsif ($age > 60*60*24*7*2) {
984 $age_str = int $age/60/60/24/7;
985 $age_str .= " weeks ago";
986 } elsif ($age > 60*60*24*2) {
987 $age_str = int $age/60/60/24;
988 $age_str .= " days ago";
989 } elsif ($age > 60*60*2) {
990 $age_str = int $age/60/60;
991 $age_str .= " hours ago";
992 } elsif ($age > 60*2) {
993 $age_str = int $age/60;
994 $age_str .= " min ago";
995 } elsif ($age > 2) {
996 $age_str = int $age;
997 $age_str .= " sec ago";
998 } else {
999 $age_str .= " right now";
1001 return $age_str;
1004 use constant {
1005 S_IFINVALID => 0030000,
1006 S_IFGITLINK => 0160000,
1009 # submodule/subproject, a commit object reference
1010 sub S_ISGITLINK($) {
1011 my $mode = shift;
1013 return (($mode & S_IFMT) == S_IFGITLINK)
1016 # convert file mode in octal to symbolic file mode string
1017 sub mode_str {
1018 my $mode = oct shift;
1020 if (S_ISGITLINK($mode)) {
1021 return 'm---------';
1022 } elsif (S_ISDIR($mode & S_IFMT)) {
1023 return 'drwxr-xr-x';
1024 } elsif (S_ISLNK($mode)) {
1025 return 'lrwxrwxrwx';
1026 } elsif (S_ISREG($mode)) {
1027 # git cares only about the executable bit
1028 if ($mode & S_IXUSR) {
1029 return '-rwxr-xr-x';
1030 } else {
1031 return '-rw-r--r--';
1033 } else {
1034 return '----------';
1038 # convert file mode in octal to file type string
1039 sub file_type {
1040 my $mode = shift;
1042 if ($mode !~ m/^[0-7]+$/) {
1043 return $mode;
1044 } else {
1045 $mode = oct $mode;
1048 if (S_ISGITLINK($mode)) {
1049 return "submodule";
1050 } elsif (S_ISDIR($mode & S_IFMT)) {
1051 return "directory";
1052 } elsif (S_ISLNK($mode)) {
1053 return "symlink";
1054 } elsif (S_ISREG($mode)) {
1055 return "file";
1056 } else {
1057 return "unknown";
1061 # convert file mode in octal to file type description string
1062 sub file_type_long {
1063 my $mode = shift;
1065 if ($mode !~ m/^[0-7]+$/) {
1066 return $mode;
1067 } else {
1068 $mode = oct $mode;
1071 if (S_ISGITLINK($mode)) {
1072 return "submodule";
1073 } elsif (S_ISDIR($mode & S_IFMT)) {
1074 return "directory";
1075 } elsif (S_ISLNK($mode)) {
1076 return "symlink";
1077 } elsif (S_ISREG($mode)) {
1078 if ($mode & S_IXUSR) {
1079 return "executable";
1080 } else {
1081 return "file";
1083 } else {
1084 return "unknown";
1089 ## ----------------------------------------------------------------------
1090 ## functions returning short HTML fragments, or transforming HTML fragments
1091 ## which don't belong to other sections
1093 # format line of commit message.
1094 sub format_log_line_html {
1095 my $line = shift;
1097 $line = esc_html($line, -nbsp=>1);
1098 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1099 my $hash_text = $1;
1100 my $link =
1101 $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1102 -class => "text"}, $hash_text);
1103 $line =~ s/$hash_text/$link/;
1105 return $line;
1108 # format marker of refs pointing to given object
1110 # the destination action is chosen based on object type and current context:
1111 # - for annotated tags, we choose the tag view unless it's the current view
1112 # already, in which case we go to shortlog view
1113 # - for other refs, we keep the current view if we're in history, shortlog or
1114 # log view, and select shortlog otherwise
1115 sub format_ref_marker {
1116 my ($refs, $id) = @_;
1117 my $markers = '';
1119 if (defined $refs->{$id}) {
1120 foreach my $ref (@{$refs->{$id}}) {
1121 # this code exploits the fact that non-lightweight tags are the
1122 # only indirect objects, and that they are the only objects for which
1123 # we want to use tag instead of shortlog as action
1124 my ($type, $name) = qw();
1125 my $indirect = ($ref =~ s/\^\{\}$//);
1126 # e.g. tags/v2.6.11 or heads/next
1127 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1128 $type = $1;
1129 $name = $2;
1130 } else {
1131 $type = "ref";
1132 $name = $ref;
1135 my $class = $type;
1136 $class .= " indirect" if $indirect;
1138 my $dest_action = "shortlog";
1140 if ($indirect) {
1141 $dest_action = "tag" unless $action eq "tag";
1142 } elsif ($action =~ /^(history|(short)?log)$/) {
1143 $dest_action = $action;
1146 my $dest = "";
1147 $dest .= "refs/" unless $ref =~ m!^refs/!;
1148 $dest .= $ref;
1150 my $link = $cgi->a({
1151 -href => href(
1152 action=>$dest_action,
1153 hash=>$dest
1154 )}, $name);
1156 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1157 $link . "</span>";
1161 if ($markers) {
1162 return ' <span class="refs">'. $markers . '</span>';
1163 } else {
1164 return "";
1168 # format, perhaps shortened and with markers, title line
1169 sub format_subject_html {
1170 my ($long, $short, $href, $extra) = @_;
1171 $extra = '' unless defined($extra);
1173 if (length($short) < length($long)) {
1174 return $cgi->a({-href => $href, -class => "list subject",
1175 -title => to_utf8($long)},
1176 esc_html($short) . $extra);
1177 } else {
1178 return $cgi->a({-href => $href, -class => "list subject"},
1179 esc_html($long) . $extra);
1183 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1184 sub format_git_diff_header_line {
1185 my $line = shift;
1186 my $diffinfo = shift;
1187 my ($from, $to) = @_;
1189 if ($diffinfo->{'nparents'}) {
1190 # combined diff
1191 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1192 if ($to->{'href'}) {
1193 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1194 esc_path($to->{'file'}));
1195 } else { # file was deleted (no href)
1196 $line .= esc_path($to->{'file'});
1198 } else {
1199 # "ordinary" diff
1200 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1201 if ($from->{'href'}) {
1202 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1203 'a/' . esc_path($from->{'file'}));
1204 } else { # file was added (no href)
1205 $line .= 'a/' . esc_path($from->{'file'});
1207 $line .= ' ';
1208 if ($to->{'href'}) {
1209 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1210 'b/' . esc_path($to->{'file'}));
1211 } else { # file was deleted
1212 $line .= 'b/' . esc_path($to->{'file'});
1216 return "<div class=\"diff header\">$line</div>\n";
1219 # format extended diff header line, before patch itself
1220 sub format_extended_diff_header_line {
1221 my $line = shift;
1222 my $diffinfo = shift;
1223 my ($from, $to) = @_;
1225 # match <path>
1226 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1227 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1228 esc_path($from->{'file'}));
1230 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1231 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1232 esc_path($to->{'file'}));
1234 # match single <mode>
1235 if ($line =~ m/\s(\d{6})$/) {
1236 $line .= '<span class="info"> (' .
1237 file_type_long($1) .
1238 ')</span>';
1240 # match <hash>
1241 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1242 # can match only for combined diff
1243 $line = 'index ';
1244 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1245 if ($from->{'href'}[$i]) {
1246 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1247 -class=>"hash"},
1248 substr($diffinfo->{'from_id'}[$i],0,7));
1249 } else {
1250 $line .= '0' x 7;
1252 # separator
1253 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1255 $line .= '..';
1256 if ($to->{'href'}) {
1257 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1258 substr($diffinfo->{'to_id'},0,7));
1259 } else {
1260 $line .= '0' x 7;
1263 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1264 # can match only for ordinary diff
1265 my ($from_link, $to_link);
1266 if ($from->{'href'}) {
1267 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1268 substr($diffinfo->{'from_id'},0,7));
1269 } else {
1270 $from_link = '0' x 7;
1272 if ($to->{'href'}) {
1273 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1274 substr($diffinfo->{'to_id'},0,7));
1275 } else {
1276 $to_link = '0' x 7;
1278 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1279 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1282 return $line . "<br/>\n";
1285 # format from-file/to-file diff header
1286 sub format_diff_from_to_header {
1287 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1288 my $line;
1289 my $result = '';
1291 $line = $from_line;
1292 #assert($line =~ m/^---/) if DEBUG;
1293 # no extra formatting for "^--- /dev/null"
1294 if (! $diffinfo->{'nparents'}) {
1295 # ordinary (single parent) diff
1296 if ($line =~ m!^--- "?a/!) {
1297 if ($from->{'href'}) {
1298 $line = '--- a/' .
1299 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1300 esc_path($from->{'file'}));
1301 } else {
1302 $line = '--- a/' .
1303 esc_path($from->{'file'});
1306 $result .= qq!<div class="diff from_file">$line</div>\n!;
1308 } else {
1309 # combined diff (merge commit)
1310 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1311 if ($from->{'href'}[$i]) {
1312 $line = '--- ' .
1313 $cgi->a({-href=>href(action=>"blobdiff",
1314 hash_parent=>$diffinfo->{'from_id'}[$i],
1315 hash_parent_base=>$parents[$i],
1316 file_parent=>$from->{'file'}[$i],
1317 hash=>$diffinfo->{'to_id'},
1318 hash_base=>$hash,
1319 file_name=>$to->{'file'}),
1320 -class=>"path",
1321 -title=>"diff" . ($i+1)},
1322 $i+1) .
1323 '/' .
1324 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1325 esc_path($from->{'file'}[$i]));
1326 } else {
1327 $line = '--- /dev/null';
1329 $result .= qq!<div class="diff from_file">$line</div>\n!;
1333 $line = $to_line;
1334 #assert($line =~ m/^\+\+\+/) if DEBUG;
1335 # no extra formatting for "^+++ /dev/null"
1336 if ($line =~ m!^\+\+\+ "?b/!) {
1337 if ($to->{'href'}) {
1338 $line = '+++ b/' .
1339 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1340 esc_path($to->{'file'}));
1341 } else {
1342 $line = '+++ b/' .
1343 esc_path($to->{'file'});
1346 $result .= qq!<div class="diff to_file">$line</div>\n!;
1348 return $result;
1351 # create note for patch simplified by combined diff
1352 sub format_diff_cc_simplified {
1353 my ($diffinfo, @parents) = @_;
1354 my $result = '';
1356 $result .= "<div class=\"diff header\">" .
1357 "diff --cc ";
1358 if (!is_deleted($diffinfo)) {
1359 $result .= $cgi->a({-href => href(action=>"blob",
1360 hash_base=>$hash,
1361 hash=>$diffinfo->{'to_id'},
1362 file_name=>$diffinfo->{'to_file'}),
1363 -class => "path"},
1364 esc_path($diffinfo->{'to_file'}));
1365 } else {
1366 $result .= esc_path($diffinfo->{'to_file'});
1368 $result .= "</div>\n" . # class="diff header"
1369 "<div class=\"diff nodifferences\">" .
1370 "Simple merge" .
1371 "</div>\n"; # class="diff nodifferences"
1373 return $result;
1376 # format patch (diff) line (not to be used for diff headers)
1377 sub format_diff_line {
1378 my $line = shift;
1379 my ($from, $to) = @_;
1380 my $diff_class = "";
1382 chomp $line;
1384 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1385 # combined diff
1386 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1387 if ($line =~ m/^\@{3}/) {
1388 $diff_class = " chunk_header";
1389 } elsif ($line =~ m/^\\/) {
1390 $diff_class = " incomplete";
1391 } elsif ($prefix =~ tr/+/+/) {
1392 $diff_class = " add";
1393 } elsif ($prefix =~ tr/-/-/) {
1394 $diff_class = " rem";
1396 } else {
1397 # assume ordinary diff
1398 my $char = substr($line, 0, 1);
1399 if ($char eq '+') {
1400 $diff_class = " add";
1401 } elsif ($char eq '-') {
1402 $diff_class = " rem";
1403 } elsif ($char eq '@') {
1404 $diff_class = " chunk_header";
1405 } elsif ($char eq "\\") {
1406 $diff_class = " incomplete";
1409 $line = untabify($line);
1410 if ($from && $to && $line =~ m/^\@{2} /) {
1411 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1412 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1414 $from_lines = 0 unless defined $from_lines;
1415 $to_lines = 0 unless defined $to_lines;
1417 if ($from->{'href'}) {
1418 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1419 -class=>"list"}, $from_text);
1421 if ($to->{'href'}) {
1422 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1423 -class=>"list"}, $to_text);
1425 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1426 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1427 return "<div class=\"diff$diff_class\">$line</div>\n";
1428 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1429 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1430 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1432 @from_text = split(' ', $ranges);
1433 for (my $i = 0; $i < @from_text; ++$i) {
1434 ($from_start[$i], $from_nlines[$i]) =
1435 (split(',', substr($from_text[$i], 1)), 0);
1438 $to_text = pop @from_text;
1439 $to_start = pop @from_start;
1440 $to_nlines = pop @from_nlines;
1442 $line = "<span class=\"chunk_info\">$prefix ";
1443 for (my $i = 0; $i < @from_text; ++$i) {
1444 if ($from->{'href'}[$i]) {
1445 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1446 -class=>"list"}, $from_text[$i]);
1447 } else {
1448 $line .= $from_text[$i];
1450 $line .= " ";
1452 if ($to->{'href'}) {
1453 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1454 -class=>"list"}, $to_text);
1455 } else {
1456 $line .= $to_text;
1458 $line .= " $prefix</span>" .
1459 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1460 return "<div class=\"diff$diff_class\">$line</div>\n";
1462 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1465 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1466 # linked. Pass the hash of the tree/commit to snapshot.
1467 sub format_snapshot_links {
1468 my ($hash) = @_;
1469 my @snapshot_fmts = gitweb_check_feature('snapshot');
1470 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1471 my $num_fmts = @snapshot_fmts;
1472 if ($num_fmts > 1) {
1473 # A parenthesized list of links bearing format names.
1474 # e.g. "snapshot (_tar.gz_ _zip_)"
1475 return "snapshot (" . join(' ', map
1476 $cgi->a({
1477 -href => href(
1478 action=>"snapshot",
1479 hash=>$hash,
1480 snapshot_format=>$_
1482 }, $known_snapshot_formats{$_}{'display'})
1483 , @snapshot_fmts) . ")";
1484 } elsif ($num_fmts == 1) {
1485 # A single "snapshot" link whose tooltip bears the format name.
1486 # i.e. "_snapshot_"
1487 my ($fmt) = @snapshot_fmts;
1488 return
1489 $cgi->a({
1490 -href => href(
1491 action=>"snapshot",
1492 hash=>$hash,
1493 snapshot_format=>$fmt
1495 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1496 }, "snapshot");
1497 } else { # $num_fmts == 0
1498 return undef;
1502 ## ......................................................................
1503 ## functions returning values to be passed, perhaps after some
1504 ## transformation, to other functions; e.g. returning arguments to href()
1506 # returns hash to be passed to href to generate gitweb URL
1507 # in -title key it returns description of link
1508 sub get_feed_info {
1509 my $format = shift || 'Atom';
1510 my %res = (action => lc($format));
1512 # feed links are possible only for project views
1513 return unless (defined $project);
1514 # some views should link to OPML, or to generic project feed,
1515 # or don't have specific feed yet (so they should use generic)
1516 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1518 my $branch;
1519 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1520 # from tag links; this also makes possible to detect branch links
1521 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1522 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1523 $branch = $1;
1525 # find log type for feed description (title)
1526 my $type = 'log';
1527 if (defined $file_name) {
1528 $type = "history of $file_name";
1529 $type .= "/" if ($action eq 'tree');
1530 $type .= " on '$branch'" if (defined $branch);
1531 } else {
1532 $type = "log of $branch" if (defined $branch);
1535 $res{-title} = $type;
1536 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1537 $res{'file_name'} = $file_name;
1539 return %res;
1542 ## ----------------------------------------------------------------------
1543 ## git utility subroutines, invoking git commands
1545 # returns path to the core git executable and the --git-dir parameter as list
1546 sub git_cmd {
1547 return $GIT, '--git-dir='.$git_dir;
1550 # quote the given arguments for passing them to the shell
1551 # quote_command("command", "arg 1", "arg with ' and ! characters")
1552 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1553 # Try to avoid using this function wherever possible.
1554 sub quote_command {
1555 return join(' ',
1556 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1559 # get HEAD ref of given project as hash
1560 sub git_get_head_hash {
1561 my $project = shift;
1562 my $o_git_dir = $git_dir;
1563 my $retval = undef;
1564 $git_dir = "$projectroot/$project";
1565 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1566 my $head = <$fd>;
1567 close $fd;
1568 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1569 $retval = $1;
1572 if (defined $o_git_dir) {
1573 $git_dir = $o_git_dir;
1575 return $retval;
1578 # get type of given object
1579 sub git_get_type {
1580 my $hash = shift;
1582 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1583 my $type = <$fd>;
1584 close $fd or return;
1585 chomp $type;
1586 return $type;
1589 # repository configuration
1590 our $config_file = '';
1591 our %config;
1593 # store multiple values for single key as anonymous array reference
1594 # single values stored directly in the hash, not as [ <value> ]
1595 sub hash_set_multi {
1596 my ($hash, $key, $value) = @_;
1598 if (!exists $hash->{$key}) {
1599 $hash->{$key} = $value;
1600 } elsif (!ref $hash->{$key}) {
1601 $hash->{$key} = [ $hash->{$key}, $value ];
1602 } else {
1603 push @{$hash->{$key}}, $value;
1607 # return hash of git project configuration
1608 # optionally limited to some section, e.g. 'gitweb'
1609 sub git_parse_project_config {
1610 my $section_regexp = shift;
1611 my %config;
1613 local $/ = "\0";
1615 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1616 or return;
1618 while (my $keyval = <$fh>) {
1619 chomp $keyval;
1620 my ($key, $value) = split(/\n/, $keyval, 2);
1622 hash_set_multi(\%config, $key, $value)
1623 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1625 close $fh;
1627 return %config;
1630 # convert config value to boolean, 'true' or 'false'
1631 # no value, number > 0, 'true' and 'yes' values are true
1632 # rest of values are treated as false (never as error)
1633 sub config_to_bool {
1634 my $val = shift;
1636 # strip leading and trailing whitespace
1637 $val =~ s/^\s+//;
1638 $val =~ s/\s+$//;
1640 return (!defined $val || # section.key
1641 ($val =~ /^\d+$/ && $val) || # section.key = 1
1642 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1645 # convert config value to simple decimal number
1646 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1647 # to be multiplied by 1024, 1048576, or 1073741824
1648 sub config_to_int {
1649 my $val = shift;
1651 # strip leading and trailing whitespace
1652 $val =~ s/^\s+//;
1653 $val =~ s/\s+$//;
1655 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1656 $unit = lc($unit);
1657 # unknown unit is treated as 1
1658 return $num * ($unit eq 'g' ? 1073741824 :
1659 $unit eq 'm' ? 1048576 :
1660 $unit eq 'k' ? 1024 : 1);
1662 return $val;
1665 # convert config value to array reference, if needed
1666 sub config_to_multi {
1667 my $val = shift;
1669 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1672 sub git_get_project_config {
1673 my ($key, $type) = @_;
1675 # key sanity check
1676 return unless ($key);
1677 $key =~ s/^gitweb\.//;
1678 return if ($key =~ m/\W/);
1680 # type sanity check
1681 if (defined $type) {
1682 $type =~ s/^--//;
1683 $type = undef
1684 unless ($type eq 'bool' || $type eq 'int');
1687 # get config
1688 if (!defined $config_file ||
1689 $config_file ne "$git_dir/config") {
1690 %config = git_parse_project_config('gitweb');
1691 $config_file = "$git_dir/config";
1694 # ensure given type
1695 if (!defined $type) {
1696 return $config{"gitweb.$key"};
1697 } elsif ($type eq 'bool') {
1698 # backward compatibility: 'git config --bool' returns true/false
1699 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1700 } elsif ($type eq 'int') {
1701 return config_to_int($config{"gitweb.$key"});
1703 return $config{"gitweb.$key"};
1706 # get hash of given path at given ref
1707 sub git_get_hash_by_path {
1708 my $base = shift;
1709 my $path = shift || return undef;
1710 my $type = shift;
1712 $path =~ s,/+$,,;
1714 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1715 or die_error(500, "Open git-ls-tree failed");
1716 my $line = <$fd>;
1717 close $fd or return undef;
1719 if (!defined $line) {
1720 # there is no tree or hash given by $path at $base
1721 return undef;
1724 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1725 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1726 if (defined $type && $type ne $2) {
1727 # type doesn't match
1728 return undef;
1730 return $3;
1733 # get path of entry with given hash at given tree-ish (ref)
1734 # used to get 'from' filename for combined diff (merge commit) for renames
1735 sub git_get_path_by_hash {
1736 my $base = shift || return;
1737 my $hash = shift || return;
1739 local $/ = "\0";
1741 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1742 or return undef;
1743 while (my $line = <$fd>) {
1744 chomp $line;
1746 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1747 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1748 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1749 close $fd;
1750 return $1;
1753 close $fd;
1754 return undef;
1757 ## ......................................................................
1758 ## git utility functions, directly accessing git repository
1760 sub git_get_project_description {
1761 my $path = shift;
1763 $git_dir = "$projectroot/$path";
1764 open my $fd, "$git_dir/description"
1765 or return git_get_project_config('description');
1766 my $descr = <$fd>;
1767 close $fd;
1768 if (defined $descr) {
1769 chomp $descr;
1771 return $descr;
1774 sub git_get_project_ctags {
1775 my $path = shift;
1776 my $ctags = {};
1778 $git_dir = "$projectroot/$path";
1779 foreach (<$git_dir/ctags/*>) {
1780 open CT, $_ or next;
1781 my $val = <CT>;
1782 chomp $val;
1783 close CT;
1784 my $ctag = $_; $ctag =~ s#.*/##;
1785 $ctags->{$ctag} = $val;
1787 $ctags;
1790 sub git_populate_project_tagcloud {
1791 my $ctags = shift;
1793 # First, merge different-cased tags; tags vote on casing
1794 my %ctags_lc;
1795 foreach (keys %$ctags) {
1796 $ctags_lc{lc $_}->{count} += $ctags->{$_};
1797 if (not $ctags_lc{lc $_}->{topcount}
1798 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
1799 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
1800 $ctags_lc{lc $_}->{topname} = $_;
1804 my $cloud;
1805 if (eval { require HTML::TagCloud; 1; }) {
1806 $cloud = HTML::TagCloud->new;
1807 foreach (sort keys %ctags_lc) {
1808 # Pad the title with spaces so that the cloud looks
1809 # less crammed.
1810 my $title = $ctags_lc{$_}->{topname};
1811 $title =~ s/ /&nbsp;/g;
1812 $title =~ s/^/&nbsp;/g;
1813 $title =~ s/$/&nbsp;/g;
1814 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
1816 } else {
1817 $cloud = \%ctags_lc;
1819 $cloud;
1822 sub git_show_project_tagcloud {
1823 my ($cloud, $count) = @_;
1824 print STDERR ref($cloud)."..\n";
1825 if (ref $cloud eq 'HTML::TagCloud') {
1826 return $cloud->html_and_css($count);
1827 } else {
1828 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
1829 return '<p align="center">' . join (', ', map {
1830 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1831 } splice(@tags, 0, $count)) . '</p>';
1835 sub git_get_project_url_list {
1836 my $path = shift;
1838 $git_dir = "$projectroot/$path";
1839 open my $fd, "$git_dir/cloneurl"
1840 or return wantarray ?
1841 @{ config_to_multi(git_get_project_config('url')) } :
1842 config_to_multi(git_get_project_config('url'));
1843 my @git_project_url_list = map { chomp; $_ } <$fd>;
1844 close $fd;
1846 return wantarray ? @git_project_url_list : \@git_project_url_list;
1849 sub git_get_projects_list {
1850 my ($filter) = @_;
1851 my @list;
1853 $filter ||= '';
1854 $filter =~ s/\.git$//;
1856 my ($check_forks) = gitweb_check_feature('forks');
1858 if (-d $projects_list) {
1859 # search in directory
1860 my $dir = $projects_list . ($filter ? "/$filter" : '');
1861 # remove the trailing "/"
1862 $dir =~ s!/+$!!;
1863 my $pfxlen = length("$dir");
1864 my $pfxdepth = ($dir =~ tr!/!!);
1866 File::Find::find({
1867 follow_fast => 1, # follow symbolic links
1868 follow_skip => 2, # ignore duplicates
1869 dangling_symlinks => 0, # ignore dangling symlinks, silently
1870 wanted => sub {
1871 # skip project-list toplevel, if we get it.
1872 return if (m!^[/.]$!);
1873 # only directories can be git repositories
1874 return unless (-d $_);
1875 # don't traverse too deep (Find is super slow on os x)
1876 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1877 $File::Find::prune = 1;
1878 return;
1881 my $subdir = substr($File::Find::name, $pfxlen + 1);
1882 # we check related file in $projectroot
1883 if ($check_forks and $subdir =~ m#/.#) {
1884 $File::Find::prune = 1;
1885 } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
1886 push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1887 $File::Find::prune = 1;
1890 }, "$dir");
1892 } elsif (-f $projects_list) {
1893 # read from file(url-encoded):
1894 # 'git%2Fgit.git Linus+Torvalds'
1895 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1896 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1897 my %paths;
1898 open my ($fd), $projects_list or return;
1899 PROJECT:
1900 while (my $line = <$fd>) {
1901 chomp $line;
1902 my ($path, $owner) = split ' ', $line;
1903 $path = unescape($path);
1904 $owner = unescape($owner);
1905 if (!defined $path) {
1906 next;
1908 if ($filter ne '') {
1909 # looking for forks;
1910 my $pfx = substr($path, 0, length($filter));
1911 if ($pfx ne $filter) {
1912 next PROJECT;
1914 my $sfx = substr($path, length($filter));
1915 if ($sfx !~ /^\/.*\.git$/) {
1916 next PROJECT;
1918 } elsif ($check_forks) {
1919 PATH:
1920 foreach my $filter (keys %paths) {
1921 # looking for forks;
1922 my $pfx = substr($path, 0, length($filter));
1923 if ($pfx ne $filter) {
1924 next PATH;
1926 my $sfx = substr($path, length($filter));
1927 if ($sfx !~ /^\/.*\.git$/) {
1928 next PATH;
1930 # is a fork, don't include it in
1931 # the list
1932 next PROJECT;
1935 if (check_export_ok("$projectroot/$path")) {
1936 my $pr = {
1937 path => $path,
1938 owner => to_utf8($owner),
1940 push @list, $pr;
1941 (my $forks_path = $path) =~ s/\.git$//;
1942 $paths{$forks_path}++;
1945 close $fd;
1947 return @list;
1950 our $gitweb_project_owner = undef;
1951 sub git_get_project_list_from_file {
1953 return if (defined $gitweb_project_owner);
1955 $gitweb_project_owner = {};
1956 # read from file (url-encoded):
1957 # 'git%2Fgit.git Linus+Torvalds'
1958 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1959 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1960 if (-f $projects_list) {
1961 open (my $fd , $projects_list);
1962 while (my $line = <$fd>) {
1963 chomp $line;
1964 my ($pr, $ow) = split ' ', $line;
1965 $pr = unescape($pr);
1966 $ow = unescape($ow);
1967 $gitweb_project_owner->{$pr} = to_utf8($ow);
1969 close $fd;
1973 sub git_get_project_owner {
1974 my $project = shift;
1975 my $owner;
1977 return undef unless $project;
1978 $git_dir = "$projectroot/$project";
1980 if (!defined $gitweb_project_owner) {
1981 git_get_project_list_from_file();
1984 if (exists $gitweb_project_owner->{$project}) {
1985 $owner = $gitweb_project_owner->{$project};
1987 if (!defined $owner){
1988 $owner = git_get_project_config('owner');
1990 if (!defined $owner) {
1991 $owner = get_file_owner("$git_dir");
1994 return $owner;
1997 sub git_get_last_activity {
1998 my ($path) = @_;
1999 my $fd;
2001 $git_dir = "$projectroot/$path";
2002 open($fd, "-|", git_cmd(), 'for-each-ref',
2003 '--format=%(committer)',
2004 '--sort=-committerdate',
2005 '--count=1',
2006 'refs/heads') or return;
2007 my $most_recent = <$fd>;
2008 close $fd or return;
2009 if (defined $most_recent &&
2010 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2011 my $timestamp = $1;
2012 my $age = time - $timestamp;
2013 return ($age, age_string($age));
2015 return (undef, undef);
2018 sub git_get_references {
2019 my $type = shift || "";
2020 my %refs;
2021 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2022 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2023 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2024 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2025 or return;
2027 while (my $line = <$fd>) {
2028 chomp $line;
2029 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2030 if (defined $refs{$1}) {
2031 push @{$refs{$1}}, $2;
2032 } else {
2033 $refs{$1} = [ $2 ];
2037 close $fd or return;
2038 return \%refs;
2041 sub git_get_rev_name_tags {
2042 my $hash = shift || return undef;
2044 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2045 or return;
2046 my $name_rev = <$fd>;
2047 close $fd;
2049 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2050 return $1;
2051 } else {
2052 # catches also '$hash undefined' output
2053 return undef;
2057 ## ----------------------------------------------------------------------
2058 ## parse to hash functions
2060 sub parse_date {
2061 my $epoch = shift;
2062 my $tz = shift || "-0000";
2064 my %date;
2065 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2066 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2067 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2068 $date{'hour'} = $hour;
2069 $date{'minute'} = $min;
2070 $date{'mday'} = $mday;
2071 $date{'day'} = $days[$wday];
2072 $date{'month'} = $months[$mon];
2073 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2074 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2075 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2076 $mday, $months[$mon], $hour ,$min;
2077 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2078 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2080 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2081 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2082 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2083 $date{'hour_local'} = $hour;
2084 $date{'minute_local'} = $min;
2085 $date{'tz_local'} = $tz;
2086 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2087 1900+$year, $mon+1, $mday,
2088 $hour, $min, $sec, $tz);
2089 return %date;
2092 sub parse_tag {
2093 my $tag_id = shift;
2094 my %tag;
2095 my @comment;
2097 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2098 $tag{'id'} = $tag_id;
2099 while (my $line = <$fd>) {
2100 chomp $line;
2101 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2102 $tag{'object'} = $1;
2103 } elsif ($line =~ m/^type (.+)$/) {
2104 $tag{'type'} = $1;
2105 } elsif ($line =~ m/^tag (.+)$/) {
2106 $tag{'name'} = $1;
2107 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2108 $tag{'author'} = $1;
2109 $tag{'epoch'} = $2;
2110 $tag{'tz'} = $3;
2111 } elsif ($line =~ m/--BEGIN/) {
2112 push @comment, $line;
2113 last;
2114 } elsif ($line eq "") {
2115 last;
2118 push @comment, <$fd>;
2119 $tag{'comment'} = \@comment;
2120 close $fd or return;
2121 if (!defined $tag{'name'}) {
2122 return
2124 return %tag
2127 sub parse_commit_text {
2128 my ($commit_text, $withparents) = @_;
2129 my @commit_lines = split '\n', $commit_text;
2130 my %co;
2132 pop @commit_lines; # Remove '\0'
2134 if (! @commit_lines) {
2135 return;
2138 my $header = shift @commit_lines;
2139 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2140 return;
2142 ($co{'id'}, my @parents) = split ' ', $header;
2143 while (my $line = shift @commit_lines) {
2144 last if $line eq "\n";
2145 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2146 $co{'tree'} = $1;
2147 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2148 push @parents, $1;
2149 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2150 $co{'author'} = $1;
2151 $co{'author_epoch'} = $2;
2152 $co{'author_tz'} = $3;
2153 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2154 $co{'author_name'} = $1;
2155 $co{'author_email'} = $2;
2156 } else {
2157 $co{'author_name'} = $co{'author'};
2159 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2160 $co{'committer'} = $1;
2161 $co{'committer_epoch'} = $2;
2162 $co{'committer_tz'} = $3;
2163 $co{'committer_name'} = $co{'committer'};
2164 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2165 $co{'committer_name'} = $1;
2166 $co{'committer_email'} = $2;
2167 } else {
2168 $co{'committer_name'} = $co{'committer'};
2172 if (!defined $co{'tree'}) {
2173 return;
2175 $co{'parents'} = \@parents;
2176 $co{'parent'} = $parents[0];
2178 foreach my $title (@commit_lines) {
2179 $title =~ s/^ //;
2180 if ($title ne "") {
2181 $co{'title'} = chop_str($title, 80, 5);
2182 # remove leading stuff of merges to make the interesting part visible
2183 if (length($title) > 50) {
2184 $title =~ s/^Automatic //;
2185 $title =~ s/^merge (of|with) /Merge ... /i;
2186 if (length($title) > 50) {
2187 $title =~ s/(http|rsync):\/\///;
2189 if (length($title) > 50) {
2190 $title =~ s/(master|www|rsync)\.//;
2192 if (length($title) > 50) {
2193 $title =~ s/kernel.org:?//;
2195 if (length($title) > 50) {
2196 $title =~ s/\/pub\/scm//;
2199 $co{'title_short'} = chop_str($title, 50, 5);
2200 last;
2203 if (! defined $co{'title'} || $co{'title'} eq "") {
2204 $co{'title'} = $co{'title_short'} = '(no commit message)';
2206 # remove added spaces
2207 foreach my $line (@commit_lines) {
2208 $line =~ s/^ //;
2210 $co{'comment'} = \@commit_lines;
2212 my $age = time - $co{'committer_epoch'};
2213 $co{'age'} = $age;
2214 $co{'age_string'} = age_string($age);
2215 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2216 if ($age > 60*60*24*7*2) {
2217 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2218 $co{'age_string_age'} = $co{'age_string'};
2219 } else {
2220 $co{'age_string_date'} = $co{'age_string'};
2221 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2223 return %co;
2226 sub parse_commit {
2227 my ($commit_id) = @_;
2228 my %co;
2230 local $/ = "\0";
2232 open my $fd, "-|", git_cmd(), "rev-list",
2233 "--parents",
2234 "--header",
2235 "--max-count=1",
2236 $commit_id,
2237 "--",
2238 or die_error(500, "Open git-rev-list failed");
2239 %co = parse_commit_text(<$fd>, 1);
2240 close $fd;
2242 return %co;
2245 sub parse_commits {
2246 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2247 my @cos;
2249 $maxcount ||= 1;
2250 $skip ||= 0;
2252 local $/ = "\0";
2254 open my $fd, "-|", git_cmd(), "rev-list",
2255 "--header",
2256 @args,
2257 ("--max-count=" . $maxcount),
2258 ("--skip=" . $skip),
2259 @extra_options,
2260 $commit_id,
2261 "--",
2262 ($filename ? ($filename) : ())
2263 or die_error(500, "Open git-rev-list failed");
2264 while (my $line = <$fd>) {
2265 my %co = parse_commit_text($line);
2266 push @cos, \%co;
2268 close $fd;
2270 return wantarray ? @cos : \@cos;
2273 # parse line of git-diff-tree "raw" output
2274 sub parse_difftree_raw_line {
2275 my $line = shift;
2276 my %res;
2278 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2279 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2280 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2281 $res{'from_mode'} = $1;
2282 $res{'to_mode'} = $2;
2283 $res{'from_id'} = $3;
2284 $res{'to_id'} = $4;
2285 $res{'status'} = $5;
2286 $res{'similarity'} = $6;
2287 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2288 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2289 } else {
2290 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2293 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2294 # combined diff (for merge commit)
2295 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2296 $res{'nparents'} = length($1);
2297 $res{'from_mode'} = [ split(' ', $2) ];
2298 $res{'to_mode'} = pop @{$res{'from_mode'}};
2299 $res{'from_id'} = [ split(' ', $3) ];
2300 $res{'to_id'} = pop @{$res{'from_id'}};
2301 $res{'status'} = [ split('', $4) ];
2302 $res{'to_file'} = unquote($5);
2304 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2305 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2306 $res{'commit'} = $1;
2309 return wantarray ? %res : \%res;
2312 # wrapper: return parsed line of git-diff-tree "raw" output
2313 # (the argument might be raw line, or parsed info)
2314 sub parsed_difftree_line {
2315 my $line_or_ref = shift;
2317 if (ref($line_or_ref) eq "HASH") {
2318 # pre-parsed (or generated by hand)
2319 return $line_or_ref;
2320 } else {
2321 return parse_difftree_raw_line($line_or_ref);
2325 # parse line of git-ls-tree output
2326 sub parse_ls_tree_line ($;%) {
2327 my $line = shift;
2328 my %opts = @_;
2329 my %res;
2331 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2332 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2334 $res{'mode'} = $1;
2335 $res{'type'} = $2;
2336 $res{'hash'} = $3;
2337 if ($opts{'-z'}) {
2338 $res{'name'} = $4;
2339 } else {
2340 $res{'name'} = unquote($4);
2343 return wantarray ? %res : \%res;
2346 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2347 sub parse_from_to_diffinfo {
2348 my ($diffinfo, $from, $to, @parents) = @_;
2350 if ($diffinfo->{'nparents'}) {
2351 # combined diff
2352 $from->{'file'} = [];
2353 $from->{'href'} = [];
2354 fill_from_file_info($diffinfo, @parents)
2355 unless exists $diffinfo->{'from_file'};
2356 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2357 $from->{'file'}[$i] =
2358 defined $diffinfo->{'from_file'}[$i] ?
2359 $diffinfo->{'from_file'}[$i] :
2360 $diffinfo->{'to_file'};
2361 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2362 $from->{'href'}[$i] = href(action=>"blob",
2363 hash_base=>$parents[$i],
2364 hash=>$diffinfo->{'from_id'}[$i],
2365 file_name=>$from->{'file'}[$i]);
2366 } else {
2367 $from->{'href'}[$i] = undef;
2370 } else {
2371 # ordinary (not combined) diff
2372 $from->{'file'} = $diffinfo->{'from_file'};
2373 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2374 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2375 hash=>$diffinfo->{'from_id'},
2376 file_name=>$from->{'file'});
2377 } else {
2378 delete $from->{'href'};
2382 $to->{'file'} = $diffinfo->{'to_file'};
2383 if (!is_deleted($diffinfo)) { # file exists in result
2384 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2385 hash=>$diffinfo->{'to_id'},
2386 file_name=>$to->{'file'});
2387 } else {
2388 delete $to->{'href'};
2392 ## ......................................................................
2393 ## parse to array of hashes functions
2395 sub git_get_heads_list {
2396 my $limit = shift;
2397 my @headslist;
2399 open my $fd, '-|', git_cmd(), 'for-each-ref',
2400 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2401 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2402 'refs/heads'
2403 or return;
2404 while (my $line = <$fd>) {
2405 my %ref_item;
2407 chomp $line;
2408 my ($refinfo, $committerinfo) = split(/\0/, $line);
2409 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2410 my ($committer, $epoch, $tz) =
2411 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2412 $ref_item{'fullname'} = $name;
2413 $name =~ s!^refs/heads/!!;
2415 $ref_item{'name'} = $name;
2416 $ref_item{'id'} = $hash;
2417 $ref_item{'title'} = $title || '(no commit message)';
2418 $ref_item{'epoch'} = $epoch;
2419 if ($epoch) {
2420 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2421 } else {
2422 $ref_item{'age'} = "unknown";
2425 push @headslist, \%ref_item;
2427 close $fd;
2429 return wantarray ? @headslist : \@headslist;
2432 sub git_get_tags_list {
2433 my $limit = shift;
2434 my @tagslist;
2436 open my $fd, '-|', git_cmd(), 'for-each-ref',
2437 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2438 '--format=%(objectname) %(objecttype) %(refname) '.
2439 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2440 'refs/tags'
2441 or return;
2442 while (my $line = <$fd>) {
2443 my %ref_item;
2445 chomp $line;
2446 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2447 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2448 my ($creator, $epoch, $tz) =
2449 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2450 $ref_item{'fullname'} = $name;
2451 $name =~ s!^refs/tags/!!;
2453 $ref_item{'type'} = $type;
2454 $ref_item{'id'} = $id;
2455 $ref_item{'name'} = $name;
2456 if ($type eq "tag") {
2457 $ref_item{'subject'} = $title;
2458 $ref_item{'reftype'} = $reftype;
2459 $ref_item{'refid'} = $refid;
2460 } else {
2461 $ref_item{'reftype'} = $type;
2462 $ref_item{'refid'} = $id;
2465 if ($type eq "tag" || $type eq "commit") {
2466 $ref_item{'epoch'} = $epoch;
2467 if ($epoch) {
2468 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2469 } else {
2470 $ref_item{'age'} = "unknown";
2474 push @tagslist, \%ref_item;
2476 close $fd;
2478 return wantarray ? @tagslist : \@tagslist;
2481 ## ----------------------------------------------------------------------
2482 ## filesystem-related functions
2484 sub get_file_owner {
2485 my $path = shift;
2487 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2488 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2489 if (!defined $gcos) {
2490 return undef;
2492 my $owner = $gcos;
2493 $owner =~ s/[,;].*$//;
2494 return to_utf8($owner);
2497 ## ......................................................................
2498 ## mimetype related functions
2500 sub mimetype_guess_file {
2501 my $filename = shift;
2502 my $mimemap = shift;
2503 -r $mimemap or return undef;
2505 my %mimemap;
2506 open(MIME, $mimemap) or return undef;
2507 while (<MIME>) {
2508 next if m/^#/; # skip comments
2509 my ($mime, $exts) = split(/\t+/);
2510 if (defined $exts) {
2511 my @exts = split(/\s+/, $exts);
2512 foreach my $ext (@exts) {
2513 $mimemap{$ext} = $mime;
2517 close(MIME);
2519 $filename =~ /\.([^.]*)$/;
2520 return $mimemap{$1};
2523 sub mimetype_guess {
2524 my $filename = shift;
2525 my $mime;
2526 $filename =~ /\./ or return undef;
2528 if ($mimetypes_file) {
2529 my $file = $mimetypes_file;
2530 if ($file !~ m!^/!) { # if it is relative path
2531 # it is relative to project
2532 $file = "$projectroot/$project/$file";
2534 $mime = mimetype_guess_file($filename, $file);
2536 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2537 return $mime;
2540 sub blob_mimetype {
2541 my $fd = shift;
2542 my $filename = shift;
2544 if ($filename) {
2545 my $mime = mimetype_guess($filename);
2546 $mime and return $mime;
2549 # just in case
2550 return $default_blob_plain_mimetype unless $fd;
2552 if (-T $fd) {
2553 return 'text/plain';
2554 } elsif (! $filename) {
2555 return 'application/octet-stream';
2556 } elsif ($filename =~ m/\.png$/i) {
2557 return 'image/png';
2558 } elsif ($filename =~ m/\.gif$/i) {
2559 return 'image/gif';
2560 } elsif ($filename =~ m/\.jpe?g$/i) {
2561 return 'image/jpeg';
2562 } else {
2563 return 'application/octet-stream';
2567 sub blob_contenttype {
2568 my ($fd, $file_name, $type) = @_;
2570 $type ||= blob_mimetype($fd, $file_name);
2571 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2572 $type .= "; charset=$default_text_plain_charset";
2575 return $type;
2578 ## ======================================================================
2579 ## functions printing HTML: header, footer, error page
2581 sub git_header_html {
2582 my $status = shift || "200 OK";
2583 my $expires = shift;
2585 my $title = "$site_name";
2586 if (defined $project) {
2587 $title .= " - " . to_utf8($project);
2588 if (defined $action) {
2589 $title .= "/$action";
2590 if (defined $file_name) {
2591 $title .= " - " . esc_path($file_name);
2592 if ($action eq "tree" && $file_name !~ m|/$|) {
2593 $title .= "/";
2598 my $content_type;
2599 # require explicit support from the UA if we are to send the page as
2600 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2601 # we have to do this because MSIE sometimes globs '*/*', pretending to
2602 # support xhtml+xml but choking when it gets what it asked for.
2603 if (defined $cgi->http('HTTP_ACCEPT') &&
2604 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2605 $cgi->Accept('application/xhtml+xml') != 0) {
2606 $content_type = 'application/xhtml+xml';
2607 } else {
2608 $content_type = 'text/html';
2610 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2611 -status=> $status, -expires => $expires);
2612 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2613 print <<EOF;
2614 <?xml version="1.0" encoding="utf-8"?>
2615 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2616 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2617 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2618 <!-- git core binaries version $git_version -->
2619 <head>
2620 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2621 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2622 <meta name="robots" content="index, nofollow"/>
2623 <title>$title</title>
2625 # print out each stylesheet that exist
2626 if (defined $stylesheet) {
2627 #provides backwards capability for those people who define style sheet in a config file
2628 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2629 } else {
2630 foreach my $stylesheet (@stylesheets) {
2631 next unless $stylesheet;
2632 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2635 if (defined $project) {
2636 my %href_params = get_feed_info();
2637 if (!exists $href_params{'-title'}) {
2638 $href_params{'-title'} = 'log';
2641 foreach my $format qw(RSS Atom) {
2642 my $type = lc($format);
2643 my %link_attr = (
2644 '-rel' => 'alternate',
2645 '-title' => "$project - $href_params{'-title'} - $format feed",
2646 '-type' => "application/$type+xml"
2649 $href_params{'action'} = $type;
2650 $link_attr{'-href'} = href(%href_params);
2651 print "<link ".
2652 "rel=\"$link_attr{'-rel'}\" ".
2653 "title=\"$link_attr{'-title'}\" ".
2654 "href=\"$link_attr{'-href'}\" ".
2655 "type=\"$link_attr{'-type'}\" ".
2656 "/>\n";
2658 $href_params{'extra_options'} = '--no-merges';
2659 $link_attr{'-href'} = href(%href_params);
2660 $link_attr{'-title'} .= ' (no merges)';
2661 print "<link ".
2662 "rel=\"$link_attr{'-rel'}\" ".
2663 "title=\"$link_attr{'-title'}\" ".
2664 "href=\"$link_attr{'-href'}\" ".
2665 "type=\"$link_attr{'-type'}\" ".
2666 "/>\n";
2669 } else {
2670 printf('<link rel="alternate" title="%s projects list" '.
2671 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2672 $site_name, href(project=>undef, action=>"project_index"));
2673 printf('<link rel="alternate" title="%s projects feeds" '.
2674 'href="%s" type="text/x-opml" />'."\n",
2675 $site_name, href(project=>undef, action=>"opml"));
2677 if (defined $favicon) {
2678 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2681 print "</head>\n" .
2682 "<body>\n";
2684 if (-f $site_header) {
2685 open (my $fd, $site_header);
2686 print <$fd>;
2687 close $fd;
2690 print "<div class=\"page_header\">\n" .
2691 $cgi->a({-href => esc_url($logo_url),
2692 -title => $logo_label},
2693 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2694 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2695 if (defined $project) {
2696 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2697 if (defined $action) {
2698 print " / $action";
2700 print "\n";
2702 print "</div>\n";
2704 my ($have_search) = gitweb_check_feature('search');
2705 if (defined $project && $have_search) {
2706 if (!defined $searchtext) {
2707 $searchtext = "";
2709 my $search_hash;
2710 if (defined $hash_base) {
2711 $search_hash = $hash_base;
2712 } elsif (defined $hash) {
2713 $search_hash = $hash;
2714 } else {
2715 $search_hash = "HEAD";
2717 my $action = $my_uri;
2718 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2719 if ($use_pathinfo) {
2720 $action .= "/".esc_url($project);
2722 print $cgi->startform(-method => "get", -action => $action) .
2723 "<div class=\"search\">\n" .
2724 (!$use_pathinfo &&
2725 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2726 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2727 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2728 $cgi->popup_menu(-name => 'st', -default => 'commit',
2729 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2730 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2731 " search:\n",
2732 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2733 "<span title=\"Extended regular expression\">" .
2734 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2735 -checked => $search_use_regexp) .
2736 "</span>" .
2737 "</div>" .
2738 $cgi->end_form() . "\n";
2742 sub git_footer_html {
2743 my $feed_class = 'rss_logo';
2745 print "<div class=\"page_footer\">\n";
2746 if (defined $project) {
2747 my $descr = git_get_project_description($project);
2748 if (defined $descr) {
2749 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2752 my %href_params = get_feed_info();
2753 if (!%href_params) {
2754 $feed_class .= ' generic';
2756 $href_params{'-title'} ||= 'log';
2758 foreach my $format qw(RSS Atom) {
2759 $href_params{'action'} = lc($format);
2760 print $cgi->a({-href => href(%href_params),
2761 -title => "$href_params{'-title'} $format feed",
2762 -class => $feed_class}, $format)."\n";
2765 } else {
2766 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2767 -class => $feed_class}, "OPML") . " ";
2768 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2769 -class => $feed_class}, "TXT") . "\n";
2771 print "</div>\n"; # class="page_footer"
2773 if (-f $site_footer) {
2774 open (my $fd, $site_footer);
2775 print <$fd>;
2776 close $fd;
2779 print "</body>\n" .
2780 "</html>";
2783 # die_error(<http_status_code>, <error_message>)
2784 # Example: die_error(404, 'Hash not found')
2785 # By convention, use the following status codes (as defined in RFC 2616):
2786 # 400: Invalid or missing CGI parameters, or
2787 # requested object exists but has wrong type.
2788 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2789 # this server or project.
2790 # 404: Requested object/revision/project doesn't exist.
2791 # 500: The server isn't configured properly, or
2792 # an internal error occurred (e.g. failed assertions caused by bugs), or
2793 # an unknown error occurred (e.g. the git binary died unexpectedly).
2794 sub die_error {
2795 my $status = shift || 500;
2796 my $error = shift || "Internal server error";
2798 my %http_responses = (400 => '400 Bad Request',
2799 403 => '403 Forbidden',
2800 404 => '404 Not Found',
2801 500 => '500 Internal Server Error');
2802 git_header_html($http_responses{$status});
2803 print <<EOF;
2804 <div class="page_body">
2805 <br /><br />
2806 $status - $error
2807 <br />
2808 </div>
2810 git_footer_html();
2811 exit;
2814 ## ----------------------------------------------------------------------
2815 ## functions printing or outputting HTML: navigation
2817 sub git_print_page_nav {
2818 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2819 $extra = '' if !defined $extra; # pager or formats
2821 my @navs = qw(summary shortlog log commit commitdiff tree);
2822 if ($suppress) {
2823 @navs = grep { $_ ne $suppress } @navs;
2826 my %arg = map { $_ => {action=>$_} } @navs;
2827 if (defined $head) {
2828 for (qw(commit commitdiff)) {
2829 $arg{$_}{'hash'} = $head;
2831 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2832 for (qw(shortlog log)) {
2833 $arg{$_}{'hash'} = $head;
2837 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2838 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2840 print "<div class=\"page_nav\">\n" .
2841 (join " | ",
2842 map { $_ eq $current ?
2843 $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2844 } @navs);
2845 print "<br/>\n$extra<br/>\n" .
2846 "</div>\n";
2849 sub format_paging_nav {
2850 my ($action, $hash, $head, $page, $has_next_link) = @_;
2851 my $paging_nav;
2854 if ($hash ne $head || $page) {
2855 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2856 } else {
2857 $paging_nav .= "HEAD";
2860 if ($page > 0) {
2861 $paging_nav .= " &sdot; " .
2862 $cgi->a({-href => href(-replay=>1, page=>$page-1),
2863 -accesskey => "p", -title => "Alt-p"}, "prev");
2864 } else {
2865 $paging_nav .= " &sdot; prev";
2868 if ($has_next_link) {
2869 $paging_nav .= " &sdot; " .
2870 $cgi->a({-href => href(-replay=>1, page=>$page+1),
2871 -accesskey => "n", -title => "Alt-n"}, "next");
2872 } else {
2873 $paging_nav .= " &sdot; next";
2876 return $paging_nav;
2879 ## ......................................................................
2880 ## functions printing or outputting HTML: div
2882 sub git_print_header_div {
2883 my ($action, $title, $hash, $hash_base) = @_;
2884 my %args = ();
2886 $args{'action'} = $action;
2887 $args{'hash'} = $hash if $hash;
2888 $args{'hash_base'} = $hash_base if $hash_base;
2890 print "<div class=\"header\">\n" .
2891 $cgi->a({-href => href(%args), -class => "title"},
2892 $title ? $title : $action) .
2893 "\n</div>\n";
2896 #sub git_print_authorship (\%) {
2897 sub git_print_authorship {
2898 my $co = shift;
2900 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2901 print "<div class=\"author_date\">" .
2902 esc_html($co->{'author_name'}) .
2903 " [$ad{'rfc2822'}";
2904 if ($ad{'hour_local'} < 6) {
2905 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2906 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2907 } else {
2908 printf(" (%02d:%02d %s)",
2909 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2911 print "]</div>\n";
2914 sub git_print_page_path {
2915 my $name = shift;
2916 my $type = shift;
2917 my $hb = shift;
2920 print "<div class=\"page_path\">";
2921 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2922 -title => 'tree root'}, to_utf8("[$project]"));
2923 print " / ";
2924 if (defined $name) {
2925 my @dirname = split '/', $name;
2926 my $basename = pop @dirname;
2927 my $fullname = '';
2929 foreach my $dir (@dirname) {
2930 $fullname .= ($fullname ? '/' : '') . $dir;
2931 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2932 hash_base=>$hb),
2933 -title => $fullname}, esc_path($dir));
2934 print " / ";
2936 if (defined $type && $type eq 'blob') {
2937 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2938 hash_base=>$hb),
2939 -title => $name}, esc_path($basename));
2940 } elsif (defined $type && $type eq 'tree') {
2941 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2942 hash_base=>$hb),
2943 -title => $name}, esc_path($basename));
2944 print " / ";
2945 } else {
2946 print esc_path($basename);
2949 print "<br/></div>\n";
2952 # sub git_print_log (\@;%) {
2953 sub git_print_log ($;%) {
2954 my $log = shift;
2955 my %opts = @_;
2957 if ($opts{'-remove_title'}) {
2958 # remove title, i.e. first line of log
2959 shift @$log;
2961 # remove leading empty lines
2962 while (defined $log->[0] && $log->[0] eq "") {
2963 shift @$log;
2966 # print log
2967 my $signoff = 0;
2968 my $empty = 0;
2969 foreach my $line (@$log) {
2970 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2971 $signoff = 1;
2972 $empty = 0;
2973 if (! $opts{'-remove_signoff'}) {
2974 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2975 next;
2976 } else {
2977 # remove signoff lines
2978 next;
2980 } else {
2981 $signoff = 0;
2984 # print only one empty line
2985 # do not print empty line after signoff
2986 if ($line eq "") {
2987 next if ($empty || $signoff);
2988 $empty = 1;
2989 } else {
2990 $empty = 0;
2993 print format_log_line_html($line) . "<br/>\n";
2996 if ($opts{'-final_empty_line'}) {
2997 # end with single empty line
2998 print "<br/>\n" unless $empty;
3002 # return link target (what link points to)
3003 sub git_get_link_target {
3004 my $hash = shift;
3005 my $link_target;
3007 # read link
3008 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3009 or return;
3011 local $/;
3012 $link_target = <$fd>;
3014 close $fd
3015 or return;
3017 return $link_target;
3020 # given link target, and the directory (basedir) the link is in,
3021 # return target of link relative to top directory (top tree);
3022 # return undef if it is not possible (including absolute links).
3023 sub normalize_link_target {
3024 my ($link_target, $basedir, $hash_base) = @_;
3026 # we can normalize symlink target only if $hash_base is provided
3027 return unless $hash_base;
3029 # absolute symlinks (beginning with '/') cannot be normalized
3030 return if (substr($link_target, 0, 1) eq '/');
3032 # normalize link target to path from top (root) tree (dir)
3033 my $path;
3034 if ($basedir) {
3035 $path = $basedir . '/' . $link_target;
3036 } else {
3037 # we are in top (root) tree (dir)
3038 $path = $link_target;
3041 # remove //, /./, and /../
3042 my @path_parts;
3043 foreach my $part (split('/', $path)) {
3044 # discard '.' and ''
3045 next if (!$part || $part eq '.');
3046 # handle '..'
3047 if ($part eq '..') {
3048 if (@path_parts) {
3049 pop @path_parts;
3050 } else {
3051 # link leads outside repository (outside top dir)
3052 return;
3054 } else {
3055 push @path_parts, $part;
3058 $path = join('/', @path_parts);
3060 return $path;
3063 # print tree entry (row of git_tree), but without encompassing <tr> element
3064 sub git_print_tree_entry {
3065 my ($t, $basedir, $hash_base, $have_blame) = @_;
3067 my %base_key = ();
3068 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3070 # The format of a table row is: mode list link. Where mode is
3071 # the mode of the entry, list is the name of the entry, an href,
3072 # and link is the action links of the entry.
3074 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3075 if ($t->{'type'} eq "blob") {
3076 print "<td class=\"list\">" .
3077 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3078 file_name=>"$basedir$t->{'name'}", %base_key),
3079 -class => "list"}, esc_path($t->{'name'}));
3080 if (S_ISLNK(oct $t->{'mode'})) {
3081 my $link_target = git_get_link_target($t->{'hash'});
3082 if ($link_target) {
3083 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3084 if (defined $norm_target) {
3085 print " -> " .
3086 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3087 file_name=>$norm_target),
3088 -title => $norm_target}, esc_path($link_target));
3089 } else {
3090 print " -> " . esc_path($link_target);
3094 print "</td>\n";
3095 print "<td class=\"link\">";
3096 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3097 file_name=>"$basedir$t->{'name'}", %base_key)},
3098 "blob");
3099 if ($have_blame) {
3100 print " | " .
3101 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3102 file_name=>"$basedir$t->{'name'}", %base_key)},
3103 "blame");
3105 if (defined $hash_base) {
3106 print " | " .
3107 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3108 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3109 "history");
3111 print " | " .
3112 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3113 file_name=>"$basedir$t->{'name'}")},
3114 "raw");
3115 print "</td>\n";
3117 } elsif ($t->{'type'} eq "tree") {
3118 print "<td class=\"list\">";
3119 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3120 file_name=>"$basedir$t->{'name'}", %base_key)},
3121 esc_path($t->{'name'}));
3122 print "</td>\n";
3123 print "<td class=\"link\">";
3124 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3125 file_name=>"$basedir$t->{'name'}", %base_key)},
3126 "tree");
3127 if (defined $hash_base) {
3128 print " | " .
3129 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3130 file_name=>"$basedir$t->{'name'}")},
3131 "history");
3133 print "</td>\n";
3134 } else {
3135 # unknown object: we can only present history for it
3136 # (this includes 'commit' object, i.e. submodule support)
3137 print "<td class=\"list\">" .
3138 esc_path($t->{'name'}) .
3139 "</td>\n";
3140 print "<td class=\"link\">";
3141 if (defined $hash_base) {
3142 print $cgi->a({-href => href(action=>"history",
3143 hash_base=>$hash_base,
3144 file_name=>"$basedir$t->{'name'}")},
3145 "history");
3147 print "</td>\n";
3151 ## ......................................................................
3152 ## functions printing large fragments of HTML
3154 # get pre-image filenames for merge (combined) diff
3155 sub fill_from_file_info {
3156 my ($diff, @parents) = @_;
3158 $diff->{'from_file'} = [ ];
3159 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3160 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3161 if ($diff->{'status'}[$i] eq 'R' ||
3162 $diff->{'status'}[$i] eq 'C') {
3163 $diff->{'from_file'}[$i] =
3164 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3168 return $diff;
3171 # is current raw difftree line of file deletion
3172 sub is_deleted {
3173 my $diffinfo = shift;
3175 return $diffinfo->{'to_id'} eq ('0' x 40);
3178 # does patch correspond to [previous] difftree raw line
3179 # $diffinfo - hashref of parsed raw diff format
3180 # $patchinfo - hashref of parsed patch diff format
3181 # (the same keys as in $diffinfo)
3182 sub is_patch_split {
3183 my ($diffinfo, $patchinfo) = @_;
3185 return defined $diffinfo && defined $patchinfo
3186 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3190 sub git_difftree_body {
3191 my ($difftree, $hash, @parents) = @_;
3192 my ($parent) = $parents[0];
3193 my ($have_blame) = gitweb_check_feature('blame');
3194 print "<div class=\"list_head\">\n";
3195 if ($#{$difftree} > 10) {
3196 print(($#{$difftree} + 1) . " files changed:\n");
3198 print "</div>\n";
3200 print "<table class=\"" .
3201 (@parents > 1 ? "combined " : "") .
3202 "diff_tree\">\n";
3204 # header only for combined diff in 'commitdiff' view
3205 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3206 if ($has_header) {
3207 # table header
3208 print "<thead><tr>\n" .
3209 "<th></th><th></th>\n"; # filename, patchN link
3210 for (my $i = 0; $i < @parents; $i++) {
3211 my $par = $parents[$i];
3212 print "<th>" .
3213 $cgi->a({-href => href(action=>"commitdiff",
3214 hash=>$hash, hash_parent=>$par),
3215 -title => 'commitdiff to parent number ' .
3216 ($i+1) . ': ' . substr($par,0,7)},
3217 $i+1) .
3218 "&nbsp;</th>\n";
3220 print "</tr></thead>\n<tbody>\n";
3223 my $alternate = 1;
3224 my $patchno = 0;
3225 foreach my $line (@{$difftree}) {
3226 my $diff = parsed_difftree_line($line);
3228 if ($alternate) {
3229 print "<tr class=\"dark\">\n";
3230 } else {
3231 print "<tr class=\"light\">\n";
3233 $alternate ^= 1;
3235 if (exists $diff->{'nparents'}) { # combined diff
3237 fill_from_file_info($diff, @parents)
3238 unless exists $diff->{'from_file'};
3240 if (!is_deleted($diff)) {
3241 # file exists in the result (child) commit
3242 print "<td>" .
3243 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3244 file_name=>$diff->{'to_file'},
3245 hash_base=>$hash),
3246 -class => "list"}, esc_path($diff->{'to_file'})) .
3247 "</td>\n";
3248 } else {
3249 print "<td>" .
3250 esc_path($diff->{'to_file'}) .
3251 "</td>\n";
3254 if ($action eq 'commitdiff') {
3255 # link to patch
3256 $patchno++;
3257 print "<td class=\"link\">" .
3258 $cgi->a({-href => "#patch$patchno"}, "patch") .
3259 " | " .
3260 "</td>\n";
3263 my $has_history = 0;
3264 my $not_deleted = 0;
3265 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3266 my $hash_parent = $parents[$i];
3267 my $from_hash = $diff->{'from_id'}[$i];
3268 my $from_path = $diff->{'from_file'}[$i];
3269 my $status = $diff->{'status'}[$i];
3271 $has_history ||= ($status ne 'A');
3272 $not_deleted ||= ($status ne 'D');
3274 if ($status eq 'A') {
3275 print "<td class=\"link\" align=\"right\"> | </td>\n";
3276 } elsif ($status eq 'D') {
3277 print "<td class=\"link\">" .
3278 $cgi->a({-href => href(action=>"blob",
3279 hash_base=>$hash,
3280 hash=>$from_hash,
3281 file_name=>$from_path)},
3282 "blob" . ($i+1)) .
3283 " | </td>\n";
3284 } else {
3285 if ($diff->{'to_id'} eq $from_hash) {
3286 print "<td class=\"link nochange\">";
3287 } else {
3288 print "<td class=\"link\">";
3290 print $cgi->a({-href => href(action=>"blobdiff",
3291 hash=>$diff->{'to_id'},
3292 hash_parent=>$from_hash,
3293 hash_base=>$hash,
3294 hash_parent_base=>$hash_parent,
3295 file_name=>$diff->{'to_file'},
3296 file_parent=>$from_path)},
3297 "diff" . ($i+1)) .
3298 " | </td>\n";
3302 print "<td class=\"link\">";
3303 if ($not_deleted) {
3304 print $cgi->a({-href => href(action=>"blob",
3305 hash=>$diff->{'to_id'},
3306 file_name=>$diff->{'to_file'},
3307 hash_base=>$hash)},
3308 "blob");
3309 print " | " if ($has_history);
3311 if ($has_history) {
3312 print $cgi->a({-href => href(action=>"history",
3313 file_name=>$diff->{'to_file'},
3314 hash_base=>$hash)},
3315 "history");
3317 print "</td>\n";
3319 print "</tr>\n";
3320 next; # instead of 'else' clause, to avoid extra indent
3322 # else ordinary diff
3324 my ($to_mode_oct, $to_mode_str, $to_file_type);
3325 my ($from_mode_oct, $from_mode_str, $from_file_type);
3326 if ($diff->{'to_mode'} ne ('0' x 6)) {
3327 $to_mode_oct = oct $diff->{'to_mode'};
3328 if (S_ISREG($to_mode_oct)) { # only for regular file
3329 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3331 $to_file_type = file_type($diff->{'to_mode'});
3333 if ($diff->{'from_mode'} ne ('0' x 6)) {
3334 $from_mode_oct = oct $diff->{'from_mode'};
3335 if (S_ISREG($to_mode_oct)) { # only for regular file
3336 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3338 $from_file_type = file_type($diff->{'from_mode'});
3341 if ($diff->{'status'} eq "A") { # created
3342 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3343 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3344 $mode_chng .= "]</span>";
3345 print "<td>";
3346 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3347 hash_base=>$hash, file_name=>$diff->{'file'}),
3348 -class => "list"}, esc_path($diff->{'file'}));
3349 print "</td>\n";
3350 print "<td>$mode_chng</td>\n";
3351 print "<td class=\"link\">";
3352 if ($action eq 'commitdiff') {
3353 # link to patch
3354 $patchno++;
3355 print $cgi->a({-href => "#patch$patchno"}, "patch");
3356 print " | ";
3358 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3359 hash_base=>$hash, file_name=>$diff->{'file'})},
3360 "blob");
3361 print "</td>\n";
3363 } elsif ($diff->{'status'} eq "D") { # deleted
3364 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3365 print "<td>";
3366 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3367 hash_base=>$parent, file_name=>$diff->{'file'}),
3368 -class => "list"}, esc_path($diff->{'file'}));
3369 print "</td>\n";
3370 print "<td>$mode_chng</td>\n";
3371 print "<td class=\"link\">";
3372 if ($action eq 'commitdiff') {
3373 # link to patch
3374 $patchno++;
3375 print $cgi->a({-href => "#patch$patchno"}, "patch");
3376 print " | ";
3378 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3379 hash_base=>$parent, file_name=>$diff->{'file'})},
3380 "blob") . " | ";
3381 if ($have_blame) {
3382 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3383 file_name=>$diff->{'file'})},
3384 "blame") . " | ";
3386 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3387 file_name=>$diff->{'file'})},
3388 "history");
3389 print "</td>\n";
3391 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3392 my $mode_chnge = "";
3393 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3394 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3395 if ($from_file_type ne $to_file_type) {
3396 $mode_chnge .= " from $from_file_type to $to_file_type";
3398 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3399 if ($from_mode_str && $to_mode_str) {
3400 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3401 } elsif ($to_mode_str) {
3402 $mode_chnge .= " mode: $to_mode_str";
3405 $mode_chnge .= "]</span>\n";
3407 print "<td>";
3408 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3409 hash_base=>$hash, file_name=>$diff->{'file'}),
3410 -class => "list"}, esc_path($diff->{'file'}));
3411 print "</td>\n";
3412 print "<td>$mode_chnge</td>\n";
3413 print "<td class=\"link\">";
3414 if ($action eq 'commitdiff') {
3415 # link to patch
3416 $patchno++;
3417 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3418 " | ";
3419 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3420 # "commit" view and modified file (not onlu mode changed)
3421 print $cgi->a({-href => href(action=>"blobdiff",
3422 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3423 hash_base=>$hash, hash_parent_base=>$parent,
3424 file_name=>$diff->{'file'})},
3425 "diff") .
3426 " | ";
3428 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3429 hash_base=>$hash, file_name=>$diff->{'file'})},
3430 "blob") . " | ";
3431 if ($have_blame) {
3432 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3433 file_name=>$diff->{'file'})},
3434 "blame") . " | ";
3436 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3437 file_name=>$diff->{'file'})},
3438 "history");
3439 print "</td>\n";
3441 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3442 my %status_name = ('R' => 'moved', 'C' => 'copied');
3443 my $nstatus = $status_name{$diff->{'status'}};
3444 my $mode_chng = "";
3445 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3446 # mode also for directories, so we cannot use $to_mode_str
3447 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3449 print "<td>" .
3450 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3451 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3452 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3453 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3454 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3455 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3456 -class => "list"}, esc_path($diff->{'from_file'})) .
3457 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3458 "<td class=\"link\">";
3459 if ($action eq 'commitdiff') {
3460 # link to patch
3461 $patchno++;
3462 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3463 " | ";
3464 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3465 # "commit" view and modified file (not only pure rename or copy)
3466 print $cgi->a({-href => href(action=>"blobdiff",
3467 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3468 hash_base=>$hash, hash_parent_base=>$parent,
3469 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3470 "diff") .
3471 " | ";
3473 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3474 hash_base=>$parent, file_name=>$diff->{'to_file'})},
3475 "blob") . " | ";
3476 if ($have_blame) {
3477 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3478 file_name=>$diff->{'to_file'})},
3479 "blame") . " | ";
3481 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3482 file_name=>$diff->{'to_file'})},
3483 "history");
3484 print "</td>\n";
3486 } # we should not encounter Unmerged (U) or Unknown (X) status
3487 print "</tr>\n";
3489 print "</tbody>" if $has_header;
3490 print "</table>\n";
3493 sub git_patchset_body {
3494 my ($fd, $difftree, $hash, @hash_parents) = @_;
3495 my ($hash_parent) = $hash_parents[0];
3497 my $is_combined = (@hash_parents > 1);
3498 my $patch_idx = 0;
3499 my $patch_number = 0;
3500 my $patch_line;
3501 my $diffinfo;
3502 my $to_name;
3503 my (%from, %to);
3505 print "<div class=\"patchset\">\n";
3507 # skip to first patch
3508 while ($patch_line = <$fd>) {
3509 chomp $patch_line;
3511 last if ($patch_line =~ m/^diff /);
3514 PATCH:
3515 while ($patch_line) {
3517 # parse "git diff" header line
3518 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3519 # $1 is from_name, which we do not use
3520 $to_name = unquote($2);
3521 $to_name =~ s!^b/!!;
3522 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3523 # $1 is 'cc' or 'combined', which we do not use
3524 $to_name = unquote($2);
3525 } else {
3526 $to_name = undef;
3529 # check if current patch belong to current raw line
3530 # and parse raw git-diff line if needed
3531 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3532 # this is continuation of a split patch
3533 print "<div class=\"patch cont\">\n";
3534 } else {
3535 # advance raw git-diff output if needed
3536 $patch_idx++ if defined $diffinfo;
3538 # read and prepare patch information
3539 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3541 # compact combined diff output can have some patches skipped
3542 # find which patch (using pathname of result) we are at now;
3543 if ($is_combined) {
3544 while ($to_name ne $diffinfo->{'to_file'}) {
3545 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3546 format_diff_cc_simplified($diffinfo, @hash_parents) .
3547 "</div>\n"; # class="patch"
3549 $patch_idx++;
3550 $patch_number++;
3552 last if $patch_idx > $#$difftree;
3553 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3557 # modifies %from, %to hashes
3558 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3560 # this is first patch for raw difftree line with $patch_idx index
3561 # we index @$difftree array from 0, but number patches from 1
3562 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3565 # git diff header
3566 #assert($patch_line =~ m/^diff /) if DEBUG;
3567 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3568 $patch_number++;
3569 # print "git diff" header
3570 print format_git_diff_header_line($patch_line, $diffinfo,
3571 \%from, \%to);
3573 # print extended diff header
3574 print "<div class=\"diff extended_header\">\n";
3575 EXTENDED_HEADER:
3576 while ($patch_line = <$fd>) {
3577 chomp $patch_line;
3579 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3581 print format_extended_diff_header_line($patch_line, $diffinfo,
3582 \%from, \%to);
3584 print "</div>\n"; # class="diff extended_header"
3586 # from-file/to-file diff header
3587 if (! $patch_line) {
3588 print "</div>\n"; # class="patch"
3589 last PATCH;
3591 next PATCH if ($patch_line =~ m/^diff /);
3592 #assert($patch_line =~ m/^---/) if DEBUG;
3594 my $last_patch_line = $patch_line;
3595 $patch_line = <$fd>;
3596 chomp $patch_line;
3597 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3599 print format_diff_from_to_header($last_patch_line, $patch_line,
3600 $diffinfo, \%from, \%to,
3601 @hash_parents);
3603 # the patch itself
3604 LINE:
3605 while ($patch_line = <$fd>) {
3606 chomp $patch_line;
3608 next PATCH if ($patch_line =~ m/^diff /);
3610 print format_diff_line($patch_line, \%from, \%to);
3613 } continue {
3614 print "</div>\n"; # class="patch"
3617 # for compact combined (--cc) format, with chunk and patch simpliciaction
3618 # patchset might be empty, but there might be unprocessed raw lines
3619 for (++$patch_idx if $patch_number > 0;
3620 $patch_idx < @$difftree;
3621 ++$patch_idx) {
3622 # read and prepare patch information
3623 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3625 # generate anchor for "patch" links in difftree / whatchanged part
3626 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3627 format_diff_cc_simplified($diffinfo, @hash_parents) .
3628 "</div>\n"; # class="patch"
3630 $patch_number++;
3633 if ($patch_number == 0) {
3634 if (@hash_parents > 1) {
3635 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3636 } else {
3637 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3641 print "</div>\n"; # class="patchset"
3644 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3646 # fills project list info (age, description, owner, forks) for each
3647 # project in the list, removing invalid projects from returned list
3648 # NOTE: modifies $projlist, but does not remove entries from it
3649 sub fill_project_list_info {
3650 my ($projlist, $check_forks) = @_;
3651 my @projects;
3653 my $show_ctags = gitweb_check_feature('ctags');
3654 PROJECT:
3655 foreach my $pr (@$projlist) {
3656 my (@activity) = git_get_last_activity($pr->{'path'});
3657 unless (@activity) {
3658 next PROJECT;
3660 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3661 if (!defined $pr->{'descr'}) {
3662 my $descr = git_get_project_description($pr->{'path'}) || "";
3663 $descr = to_utf8($descr);
3664 $pr->{'descr_long'} = $descr;
3665 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3667 if (!defined $pr->{'owner'}) {
3668 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3670 if ($check_forks) {
3671 my $pname = $pr->{'path'};
3672 if (($pname =~ s/\.git$//) &&
3673 ($pname !~ /\/$/) &&
3674 (-d "$projectroot/$pname")) {
3675 $pr->{'forks'} = "-d $projectroot/$pname";
3676 } else {
3677 $pr->{'forks'} = 0;
3680 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3681 push @projects, $pr;
3684 return @projects;
3687 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3688 # (changing $list), or generating 'sort by $name' replay link otherwise
3689 sub print_sort_th {
3690 my ($str_sort, $name, $order, $key, $header, $list) = @_;
3691 $key ||= $name;
3692 $header ||= ucfirst($name);
3694 if ($order eq $name) {
3695 if ($str_sort) {
3696 @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
3697 } else {
3698 @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
3700 print "<th>$header</th>\n";
3701 } else {
3702 print "<th>" .
3703 $cgi->a({-href => href(-replay=>1, order=>$name),
3704 -class => "header"}, $header) .
3705 "</th>\n";
3709 sub print_sort_th_str {
3710 print_sort_th(1, @_);
3713 sub print_sort_th_num {
3714 print_sort_th(0, @_);
3717 sub git_project_list_body {
3718 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3720 my ($check_forks) = gitweb_check_feature('forks');
3721 my @projects = fill_project_list_info($projlist, $check_forks);
3723 $order ||= $default_projects_order;
3724 $from = 0 unless defined $from;
3725 $to = $#projects if (!defined $to || $#projects < $to);
3727 my $show_ctags = gitweb_check_feature('ctags');
3728 if ($show_ctags) {
3729 my %ctags;
3730 foreach my $p (@projects) {
3731 foreach my $ct (keys %{$p->{'ctags'}}) {
3732 $ctags{$ct} += $p->{'ctags'}->{$ct};
3735 my $cloud = git_populate_project_tagcloud(\%ctags);
3736 print git_show_project_tagcloud($cloud, 64);
3739 print "<table class=\"project_list\">\n";
3740 unless ($no_header) {
3741 print "<tr>\n";
3742 if ($check_forks) {
3743 print "<th></th>\n";
3745 print_sort_th_str('project', $order, 'path',
3746 'Project', \@projects);
3747 print_sort_th_str('descr', $order, 'descr_long',
3748 'Description', \@projects);
3749 print_sort_th_str('owner', $order, 'owner',
3750 'Owner', \@projects);
3751 print_sort_th_num('age', $order, 'age',
3752 'Last Change', \@projects);
3753 print "<th></th>\n" . # for links
3754 "</tr>\n";
3756 my $alternate = 1;
3757 my $tagfilter = $cgi->param('by_tag');
3758 for (my $i = $from; $i <= $to; $i++) {
3759 my $pr = $projects[$i];
3760 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3761 if ($alternate) {
3762 print "<tr class=\"dark\">\n";
3763 } else {
3764 print "<tr class=\"light\">\n";
3766 $alternate ^= 1;
3767 if ($check_forks) {
3768 print "<td>";
3769 if ($pr->{'forks'}) {
3770 print "<!-- $pr->{'forks'} -->\n";
3771 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3773 print "</td>\n";
3775 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3776 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3777 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3778 -class => "list", -title => $pr->{'descr_long'}},
3779 esc_html($pr->{'descr'})) . "</td>\n" .
3780 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3781 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3782 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3783 "<td class=\"link\">" .
3784 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
3785 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3786 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3787 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3788 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3789 "</td>\n" .
3790 "</tr>\n";
3792 if (defined $extra) {
3793 print "<tr>\n";
3794 if ($check_forks) {
3795 print "<td></td>\n";
3797 print "<td colspan=\"5\">$extra</td>\n" .
3798 "</tr>\n";
3800 print "</table>\n";
3803 sub git_shortlog_body {
3804 # uses global variable $project
3805 my ($commitlist, $from, $to, $refs, $extra) = @_;
3807 $from = 0 unless defined $from;
3808 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3810 print "<table class=\"shortlog\">\n";
3811 my $alternate = 1;
3812 for (my $i = $from; $i <= $to; $i++) {
3813 my %co = %{$commitlist->[$i]};
3814 my $commit = $co{'id'};
3815 my $ref = format_ref_marker($refs, $commit);
3816 if ($alternate) {
3817 print "<tr class=\"dark\">\n";
3818 } else {
3819 print "<tr class=\"light\">\n";
3821 $alternate ^= 1;
3822 my $author = chop_and_escape_str($co{'author_name'}, 10);
3823 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3824 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3825 "<td><i>" . $author . "</i></td>\n" .
3826 "<td>";
3827 print format_subject_html($co{'title'}, $co{'title_short'},
3828 href(action=>"commit", hash=>$commit), $ref);
3829 print "</td>\n" .
3830 "<td class=\"link\">" .
3831 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3832 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3833 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3834 my $snapshot_links = format_snapshot_links($commit);
3835 if (defined $snapshot_links) {
3836 print " | " . $snapshot_links;
3838 print "</td>\n" .
3839 "</tr>\n";
3841 if (defined $extra) {
3842 print "<tr>\n" .
3843 "<td colspan=\"4\">$extra</td>\n" .
3844 "</tr>\n";
3846 print "</table>\n";
3849 sub git_history_body {
3850 # Warning: assumes constant type (blob or tree) during history
3851 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3853 $from = 0 unless defined $from;
3854 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3856 print "<table class=\"history\">\n";
3857 my $alternate = 1;
3858 for (my $i = $from; $i <= $to; $i++) {
3859 my %co = %{$commitlist->[$i]};
3860 if (!%co) {
3861 next;
3863 my $commit = $co{'id'};
3865 my $ref = format_ref_marker($refs, $commit);
3867 if ($alternate) {
3868 print "<tr class=\"dark\">\n";
3869 } else {
3870 print "<tr class=\"light\">\n";
3872 $alternate ^= 1;
3873 # shortlog uses chop_str($co{'author_name'}, 10)
3874 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3875 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3876 "<td><i>" . $author . "</i></td>\n" .
3877 "<td>";
3878 # originally git_history used chop_str($co{'title'}, 50)
3879 print format_subject_html($co{'title'}, $co{'title_short'},
3880 href(action=>"commit", hash=>$commit), $ref);
3881 print "</td>\n" .
3882 "<td class=\"link\">" .
3883 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3884 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3886 if ($ftype eq 'blob') {
3887 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3888 my $blob_parent = git_get_hash_by_path($commit, $file_name);
3889 if (defined $blob_current && defined $blob_parent &&
3890 $blob_current ne $blob_parent) {
3891 print " | " .
3892 $cgi->a({-href => href(action=>"blobdiff",
3893 hash=>$blob_current, hash_parent=>$blob_parent,
3894 hash_base=>$hash_base, hash_parent_base=>$commit,
3895 file_name=>$file_name)},
3896 "diff to current");
3899 print "</td>\n" .
3900 "</tr>\n";
3902 if (defined $extra) {
3903 print "<tr>\n" .
3904 "<td colspan=\"4\">$extra</td>\n" .
3905 "</tr>\n";
3907 print "</table>\n";
3910 sub git_tags_body {
3911 # uses global variable $project
3912 my ($taglist, $from, $to, $extra) = @_;
3913 $from = 0 unless defined $from;
3914 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3916 print "<table class=\"tags\">\n";
3917 my $alternate = 1;
3918 for (my $i = $from; $i <= $to; $i++) {
3919 my $entry = $taglist->[$i];
3920 my %tag = %$entry;
3921 my $comment = $tag{'subject'};
3922 my $comment_short;
3923 if (defined $comment) {
3924 $comment_short = chop_str($comment, 30, 5);
3926 if ($alternate) {
3927 print "<tr class=\"dark\">\n";
3928 } else {
3929 print "<tr class=\"light\">\n";
3931 $alternate ^= 1;
3932 if (defined $tag{'age'}) {
3933 print "<td><i>$tag{'age'}</i></td>\n";
3934 } else {
3935 print "<td></td>\n";
3937 print "<td>" .
3938 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3939 -class => "list name"}, esc_html($tag{'name'})) .
3940 "</td>\n" .
3941 "<td>";
3942 if (defined $comment) {
3943 print format_subject_html($comment, $comment_short,
3944 href(action=>"tag", hash=>$tag{'id'}));
3946 print "</td>\n" .
3947 "<td class=\"selflink\">";
3948 if ($tag{'type'} eq "tag") {
3949 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3950 } else {
3951 print "&nbsp;";
3953 print "</td>\n" .
3954 "<td class=\"link\">" . " | " .
3955 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3956 if ($tag{'reftype'} eq "commit") {
3957 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3958 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3959 } elsif ($tag{'reftype'} eq "blob") {
3960 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3962 print "</td>\n" .
3963 "</tr>";
3965 if (defined $extra) {
3966 print "<tr>\n" .
3967 "<td colspan=\"5\">$extra</td>\n" .
3968 "</tr>\n";
3970 print "</table>\n";
3973 sub git_heads_body {
3974 # uses global variable $project
3975 my ($headlist, $head, $from, $to, $extra) = @_;
3976 $from = 0 unless defined $from;
3977 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3979 print "<table class=\"heads\">\n";
3980 my $alternate = 1;
3981 for (my $i = $from; $i <= $to; $i++) {
3982 my $entry = $headlist->[$i];
3983 my %ref = %$entry;
3984 my $curr = $ref{'id'} eq $head;
3985 if ($alternate) {
3986 print "<tr class=\"dark\">\n";
3987 } else {
3988 print "<tr class=\"light\">\n";
3990 $alternate ^= 1;
3991 print "<td><i>$ref{'age'}</i></td>\n" .
3992 ($curr ? "<td class=\"current_head\">" : "<td>") .
3993 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
3994 -class => "list name"},esc_html($ref{'name'})) .
3995 "</td>\n" .
3996 "<td class=\"link\">" .
3997 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
3998 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
3999 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4000 "</td>\n" .
4001 "</tr>";
4003 if (defined $extra) {
4004 print "<tr>\n" .
4005 "<td colspan=\"3\">$extra</td>\n" .
4006 "</tr>\n";
4008 print "</table>\n";
4011 sub git_search_grep_body {
4012 my ($commitlist, $from, $to, $extra) = @_;
4013 $from = 0 unless defined $from;
4014 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4016 print "<table class=\"commit_search\">\n";
4017 my $alternate = 1;
4018 for (my $i = $from; $i <= $to; $i++) {
4019 my %co = %{$commitlist->[$i]};
4020 if (!%co) {
4021 next;
4023 my $commit = $co{'id'};
4024 if ($alternate) {
4025 print "<tr class=\"dark\">\n";
4026 } else {
4027 print "<tr class=\"light\">\n";
4029 $alternate ^= 1;
4030 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4031 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4032 "<td><i>" . $author . "</i></td>\n" .
4033 "<td>" .
4034 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4035 -class => "list subject"},
4036 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4037 my $comment = $co{'comment'};
4038 foreach my $line (@$comment) {
4039 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4040 my ($lead, $match, $trail) = ($1, $2, $3);
4041 $match = chop_str($match, 70, 5, 'center');
4042 my $contextlen = int((80 - length($match))/2);
4043 $contextlen = 30 if ($contextlen > 30);
4044 $lead = chop_str($lead, $contextlen, 10, 'left');
4045 $trail = chop_str($trail, $contextlen, 10, 'right');
4047 $lead = esc_html($lead);
4048 $match = esc_html($match);
4049 $trail = esc_html($trail);
4051 print "$lead<span class=\"match\">$match</span>$trail<br />";
4054 print "</td>\n" .
4055 "<td class=\"link\">" .
4056 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4057 " | " .
4058 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4059 " | " .
4060 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4061 print "</td>\n" .
4062 "</tr>\n";
4064 if (defined $extra) {
4065 print "<tr>\n" .
4066 "<td colspan=\"3\">$extra</td>\n" .
4067 "</tr>\n";
4069 print "</table>\n";
4072 ## ======================================================================
4073 ## ======================================================================
4074 ## actions
4076 sub git_project_list {
4077 my $order = $cgi->param('o');
4078 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4079 die_error(400, "Unknown order parameter");
4082 my @list = git_get_projects_list();
4083 if (!@list) {
4084 die_error(404, "No projects found");
4087 git_header_html();
4088 if (-f $home_text) {
4089 print "<div class=\"index_include\">\n";
4090 open (my $fd, $home_text);
4091 print <$fd>;
4092 close $fd;
4093 print "</div>\n";
4095 git_project_list_body(\@list, $order);
4096 git_footer_html();
4099 sub git_forks {
4100 my $order = $cgi->param('o');
4101 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4102 die_error(400, "Unknown order parameter");
4105 my @list = git_get_projects_list($project);
4106 if (!@list) {
4107 die_error(404, "No forks found");
4110 git_header_html();
4111 git_print_page_nav('','');
4112 git_print_header_div('summary', "$project forks");
4113 git_project_list_body(\@list, $order);
4114 git_footer_html();
4117 sub git_project_index {
4118 my @projects = git_get_projects_list($project);
4120 print $cgi->header(
4121 -type => 'text/plain',
4122 -charset => 'utf-8',
4123 -content_disposition => 'inline; filename="index.aux"');
4125 foreach my $pr (@projects) {
4126 if (!exists $pr->{'owner'}) {
4127 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4130 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4131 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4132 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4133 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4134 $path =~ s/ /\+/g;
4135 $owner =~ s/ /\+/g;
4137 print "$path $owner\n";
4141 sub git_summary {
4142 my $descr = git_get_project_description($project) || "none";
4143 my %co = parse_commit("HEAD");
4144 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4145 my $head = $co{'id'};
4147 my $owner = git_get_project_owner($project);
4149 my $refs = git_get_references();
4150 # These get_*_list functions return one more to allow us to see if
4151 # there are more ...
4152 my @taglist = git_get_tags_list(16);
4153 my @headlist = git_get_heads_list(16);
4154 my @forklist;
4155 my ($check_forks) = gitweb_check_feature('forks');
4157 if ($check_forks) {
4158 @forklist = git_get_projects_list($project);
4161 git_header_html();
4162 git_print_page_nav('summary','', $head);
4164 print "<div class=\"title\">&nbsp;</div>\n";
4165 print "<table class=\"projects_list\">\n" .
4166 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4167 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4168 if (defined $cd{'rfc2822'}) {
4169 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4172 # use per project git URL list in $projectroot/$project/cloneurl
4173 # or make project git URL from git base URL and project name
4174 my $url_tag = "URL";
4175 my @url_list = git_get_project_url_list($project);
4176 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4177 foreach my $git_url (@url_list) {
4178 next unless $git_url;
4179 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4180 $url_tag = "";
4183 # Tag cloud
4184 my $show_ctags = (gitweb_check_feature('ctags'))[0];
4185 if ($show_ctags) {
4186 my $ctags = git_get_project_ctags($project);
4187 my $cloud = git_populate_project_tagcloud($ctags);
4188 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4189 print "</td>\n<td>" unless %$ctags;
4190 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4191 print "</td>\n<td>" if %$ctags;
4192 print git_show_project_tagcloud($cloud, 48);
4193 print "</td></tr>";
4196 print "</table>\n";
4198 if (-s "$projectroot/$project/README.html") {
4199 if (open my $fd, "$projectroot/$project/README.html") {
4200 print "<div class=\"title\">readme</div>\n" .
4201 "<div class=\"readme\">\n";
4202 print $_ while (<$fd>);
4203 print "\n</div>\n"; # class="readme"
4204 close $fd;
4208 # we need to request one more than 16 (0..15) to check if
4209 # those 16 are all
4210 my @commitlist = $head ? parse_commits($head, 17) : ();
4211 if (@commitlist) {
4212 git_print_header_div('shortlog');
4213 git_shortlog_body(\@commitlist, 0, 15, $refs,
4214 $#commitlist <= 15 ? undef :
4215 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4218 if (@taglist) {
4219 git_print_header_div('tags');
4220 git_tags_body(\@taglist, 0, 15,
4221 $#taglist <= 15 ? undef :
4222 $cgi->a({-href => href(action=>"tags")}, "..."));
4225 if (@headlist) {
4226 git_print_header_div('heads');
4227 git_heads_body(\@headlist, $head, 0, 15,
4228 $#headlist <= 15 ? undef :
4229 $cgi->a({-href => href(action=>"heads")}, "..."));
4232 if (@forklist) {
4233 git_print_header_div('forks');
4234 git_project_list_body(\@forklist, undef, 0, 15,
4235 $#forklist <= 15 ? undef :
4236 $cgi->a({-href => href(action=>"forks")}, "..."),
4237 'noheader');
4240 git_footer_html();
4243 sub git_tag {
4244 my $head = git_get_head_hash($project);
4245 git_header_html();
4246 git_print_page_nav('','', $head,undef,$head);
4247 my %tag = parse_tag($hash);
4249 if (! %tag) {
4250 die_error(404, "Unknown tag object");
4253 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4254 print "<div class=\"title_text\">\n" .
4255 "<table class=\"object_header\">\n" .
4256 "<tr>\n" .
4257 "<td>object</td>\n" .
4258 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4259 $tag{'object'}) . "</td>\n" .
4260 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4261 $tag{'type'}) . "</td>\n" .
4262 "</tr>\n";
4263 if (defined($tag{'author'})) {
4264 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4265 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4266 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4267 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4268 "</td></tr>\n";
4270 print "</table>\n\n" .
4271 "</div>\n";
4272 print "<div class=\"page_body\">";
4273 my $comment = $tag{'comment'};
4274 foreach my $line (@$comment) {
4275 chomp $line;
4276 print esc_html($line, -nbsp=>1) . "<br/>\n";
4278 print "</div>\n";
4279 git_footer_html();
4282 sub git_blame {
4283 my $fd;
4284 my $ftype;
4286 gitweb_check_feature('blame')
4287 or die_error(403, "Blame view not allowed");
4289 die_error(400, "No file name given") unless $file_name;
4290 $hash_base ||= git_get_head_hash($project);
4291 die_error(404, "Couldn't find base commit") unless ($hash_base);
4292 my %co = parse_commit($hash_base)
4293 or die_error(404, "Commit not found");
4294 if (!defined $hash) {
4295 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4296 or die_error(404, "Error looking up file");
4298 $ftype = git_get_type($hash);
4299 if ($ftype !~ "blob") {
4300 die_error(400, "Object is not a blob");
4302 open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4303 $file_name, $hash_base)
4304 or die_error(500, "Open git-blame failed");
4305 git_header_html();
4306 my $formats_nav =
4307 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4308 "blob") .
4309 " | " .
4310 $cgi->a({-href => href(action=>"history", -replay=>1)},
4311 "history") .
4312 " | " .
4313 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4314 "HEAD");
4315 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4316 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4317 git_print_page_path($file_name, $ftype, $hash_base);
4318 my @rev_color = (qw(light2 dark2));
4319 my $num_colors = scalar(@rev_color);
4320 my $current_color = 0;
4321 my $last_rev;
4322 print <<HTML;
4323 <div class="page_body">
4324 <table class="blame">
4325 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4326 HTML
4327 my %metainfo = ();
4328 while (1) {
4329 $_ = <$fd>;
4330 last unless defined $_;
4331 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4332 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4333 if (!exists $metainfo{$full_rev}) {
4334 $metainfo{$full_rev} = {};
4336 my $meta = $metainfo{$full_rev};
4337 while (<$fd>) {
4338 last if (s/^\t//);
4339 if (/^(\S+) (.*)$/) {
4340 $meta->{$1} = $2;
4343 my $data = $_;
4344 chomp $data;
4345 my $rev = substr($full_rev, 0, 8);
4346 my $author = $meta->{'author'};
4347 my %date = parse_date($meta->{'author-time'},
4348 $meta->{'author-tz'});
4349 my $date = $date{'iso-tz'};
4350 if ($group_size) {
4351 $current_color = ++$current_color % $num_colors;
4353 print "<tr class=\"$rev_color[$current_color]\">\n";
4354 if ($group_size) {
4355 print "<td class=\"sha1\"";
4356 print " title=\"". esc_html($author) . ", $date\"";
4357 print " rowspan=\"$group_size\"" if ($group_size > 1);
4358 print ">";
4359 print $cgi->a({-href => href(action=>"commit",
4360 hash=>$full_rev,
4361 file_name=>$file_name)},
4362 esc_html($rev));
4363 print "</td>\n";
4365 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4366 or die_error(500, "Open git-rev-parse failed");
4367 my $parent_commit = <$dd>;
4368 close $dd;
4369 chomp($parent_commit);
4370 my $blamed = href(action => 'blame',
4371 file_name => $meta->{'filename'},
4372 hash_base => $parent_commit);
4373 print "<td class=\"linenr\">";
4374 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4375 -id => "l$lineno",
4376 -class => "linenr" },
4377 esc_html($lineno));
4378 print "</td>";
4379 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4380 print "</tr>\n";
4382 print "</table>\n";
4383 print "</div>";
4384 close $fd
4385 or print "Reading blob failed\n";
4386 git_footer_html();
4389 sub git_tags {
4390 my $head = git_get_head_hash($project);
4391 git_header_html();
4392 git_print_page_nav('','', $head,undef,$head);
4393 git_print_header_div('summary', $project);
4395 my @tagslist = git_get_tags_list();
4396 if (@tagslist) {
4397 git_tags_body(\@tagslist);
4399 git_footer_html();
4402 sub git_heads {
4403 my $head = git_get_head_hash($project);
4404 git_header_html();
4405 git_print_page_nav('','', $head,undef,$head);
4406 git_print_header_div('summary', $project);
4408 my @headslist = git_get_heads_list();
4409 if (@headslist) {
4410 git_heads_body(\@headslist, $head);
4412 git_footer_html();
4415 sub git_blob_plain {
4416 my $type = shift;
4417 my $expires;
4419 if (!defined $hash) {
4420 if (defined $file_name) {
4421 my $base = $hash_base || git_get_head_hash($project);
4422 $hash = git_get_hash_by_path($base, $file_name, "blob")
4423 or die_error(404, "Cannot find file");
4424 } else {
4425 die_error(400, "No file name defined");
4427 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4428 # blobs defined by non-textual hash id's can be cached
4429 $expires = "+1d";
4432 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4433 or die_error(500, "Open git-cat-file blob '$hash' failed");
4435 # content-type (can include charset)
4436 $type = blob_contenttype($fd, $file_name, $type);
4438 # "save as" filename, even when no $file_name is given
4439 my $save_as = "$hash";
4440 if (defined $file_name) {
4441 $save_as = $file_name;
4442 } elsif ($type =~ m/^text\//) {
4443 $save_as .= '.txt';
4446 print $cgi->header(
4447 -type => $type,
4448 -expires => $expires,
4449 -content_disposition => 'inline; filename="' . $save_as . '"');
4450 undef $/;
4451 binmode STDOUT, ':raw';
4452 print <$fd>;
4453 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4454 $/ = "\n";
4455 close $fd;
4458 sub git_blob {
4459 my $expires;
4461 if (!defined $hash) {
4462 if (defined $file_name) {
4463 my $base = $hash_base || git_get_head_hash($project);
4464 $hash = git_get_hash_by_path($base, $file_name, "blob")
4465 or die_error(404, "Cannot find file");
4466 } else {
4467 die_error(400, "No file name defined");
4469 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4470 # blobs defined by non-textual hash id's can be cached
4471 $expires = "+1d";
4474 my ($have_blame) = gitweb_check_feature('blame');
4475 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4476 or die_error(500, "Couldn't cat $file_name, $hash");
4477 my $mimetype = blob_mimetype($fd, $file_name);
4478 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4479 close $fd;
4480 return git_blob_plain($mimetype);
4482 # we can have blame only for text/* mimetype
4483 $have_blame &&= ($mimetype =~ m!^text/!);
4485 git_header_html(undef, $expires);
4486 my $formats_nav = '';
4487 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4488 if (defined $file_name) {
4489 if ($have_blame) {
4490 $formats_nav .=
4491 $cgi->a({-href => href(action=>"blame", -replay=>1)},
4492 "blame") .
4493 " | ";
4495 $formats_nav .=
4496 $cgi->a({-href => href(action=>"history", -replay=>1)},
4497 "history") .
4498 " | " .
4499 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4500 "raw") .
4501 " | " .
4502 $cgi->a({-href => href(action=>"blob",
4503 hash_base=>"HEAD", file_name=>$file_name)},
4504 "HEAD");
4505 } else {
4506 $formats_nav .=
4507 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4508 "raw");
4510 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4511 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4512 } else {
4513 print "<div class=\"page_nav\">\n" .
4514 "<br/><br/></div>\n" .
4515 "<div class=\"title\">$hash</div>\n";
4517 git_print_page_path($file_name, "blob", $hash_base);
4518 print "<div class=\"page_body\">\n";
4519 if ($mimetype =~ m!^image/!) {
4520 print qq!<img type="$mimetype"!;
4521 if ($file_name) {
4522 print qq! alt="$file_name" title="$file_name"!;
4524 print qq! src="! .
4525 href(action=>"blob_plain", hash=>$hash,
4526 hash_base=>$hash_base, file_name=>$file_name) .
4527 qq!" />\n!;
4528 } else {
4529 my $nr;
4530 while (my $line = <$fd>) {
4531 chomp $line;
4532 $nr++;
4533 $line = untabify($line);
4534 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4535 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4538 close $fd
4539 or print "Reading blob failed.\n";
4540 print "</div>";
4541 git_footer_html();
4544 sub git_tree {
4545 if (!defined $hash_base) {
4546 $hash_base = "HEAD";
4548 if (!defined $hash) {
4549 if (defined $file_name) {
4550 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4551 } else {
4552 $hash = $hash_base;
4555 $/ = "\0";
4556 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4557 or die_error(500, "Open git-ls-tree failed");
4558 my @entries = map { chomp; $_ } <$fd>;
4559 close $fd or die_error(404, "Reading tree failed");
4560 $/ = "\n";
4562 my $refs = git_get_references();
4563 my $ref = format_ref_marker($refs, $hash_base);
4564 git_header_html();
4565 my $basedir = '';
4566 my ($have_blame) = gitweb_check_feature('blame');
4567 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4568 my @views_nav = ();
4569 if (defined $file_name) {
4570 push @views_nav,
4571 $cgi->a({-href => href(action=>"history", -replay=>1)},
4572 "history"),
4573 $cgi->a({-href => href(action=>"tree",
4574 hash_base=>"HEAD", file_name=>$file_name)},
4575 "HEAD"),
4577 my $snapshot_links = format_snapshot_links($hash);
4578 if (defined $snapshot_links) {
4579 # FIXME: Should be available when we have no hash base as well.
4580 push @views_nav, $snapshot_links;
4582 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4583 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4584 } else {
4585 undef $hash_base;
4586 print "<div class=\"page_nav\">\n";
4587 print "<br/><br/></div>\n";
4588 print "<div class=\"title\">$hash</div>\n";
4590 if (defined $file_name) {
4591 $basedir = $file_name;
4592 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4593 $basedir .= '/';
4596 git_print_page_path($file_name, 'tree', $hash_base);
4597 print "<div class=\"page_body\">\n";
4598 print "<table class=\"tree\">\n";
4599 my $alternate = 1;
4600 # '..' (top directory) link if possible
4601 if (defined $hash_base &&
4602 defined $file_name && $file_name =~ m![^/]+$!) {
4603 if ($alternate) {
4604 print "<tr class=\"dark\">\n";
4605 } else {
4606 print "<tr class=\"light\">\n";
4608 $alternate ^= 1;
4610 my $up = $file_name;
4611 $up =~ s!/?[^/]+$!!;
4612 undef $up unless $up;
4613 # based on git_print_tree_entry
4614 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4615 print '<td class="list">';
4616 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4617 file_name=>$up)},
4618 "..");
4619 print "</td>\n";
4620 print "<td class=\"link\"></td>\n";
4622 print "</tr>\n";
4624 foreach my $line (@entries) {
4625 my %t = parse_ls_tree_line($line, -z => 1);
4627 if ($alternate) {
4628 print "<tr class=\"dark\">\n";
4629 } else {
4630 print "<tr class=\"light\">\n";
4632 $alternate ^= 1;
4634 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4636 print "</tr>\n";
4638 print "</table>\n" .
4639 "</div>";
4640 git_footer_html();
4643 sub git_snapshot {
4644 my @supported_fmts = gitweb_check_feature('snapshot');
4645 @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4647 my $format = $cgi->param('sf');
4648 if (!@supported_fmts) {
4649 die_error(403, "Snapshots not allowed");
4651 # default to first supported snapshot format
4652 $format ||= $supported_fmts[0];
4653 if ($format !~ m/^[a-z0-9]+$/) {
4654 die_error(400, "Invalid snapshot format parameter");
4655 } elsif (!exists($known_snapshot_formats{$format})) {
4656 die_error(400, "Unknown snapshot format");
4657 } elsif (!grep($_ eq $format, @supported_fmts)) {
4658 die_error(403, "Unsupported snapshot format");
4661 if (!defined $hash) {
4662 $hash = git_get_head_hash($project);
4665 my $name = $project;
4666 $name =~ s,([^/])/*\.git$,$1,;
4667 $name = basename($name);
4668 my $filename = to_utf8($name);
4669 $name =~ s/\047/\047\\\047\047/g;
4670 my $cmd;
4671 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4672 $cmd = quote_command(
4673 git_cmd(), 'archive',
4674 "--format=$known_snapshot_formats{$format}{'format'}",
4675 "--prefix=$name/", $hash);
4676 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4677 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4680 print $cgi->header(
4681 -type => $known_snapshot_formats{$format}{'type'},
4682 -content_disposition => 'inline; filename="' . "$filename" . '"',
4683 -status => '200 OK');
4685 open my $fd, "-|", $cmd
4686 or die_error(500, "Execute git-archive failed");
4687 binmode STDOUT, ':raw';
4688 print <$fd>;
4689 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4690 close $fd;
4693 sub git_log {
4694 my $head = git_get_head_hash($project);
4695 if (!defined $hash) {
4696 $hash = $head;
4698 if (!defined $page) {
4699 $page = 0;
4701 my $refs = git_get_references();
4703 my @commitlist = parse_commits($hash, 101, (100 * $page));
4705 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4707 git_header_html();
4708 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4710 if (!@commitlist) {
4711 my %co = parse_commit($hash);
4713 git_print_header_div('summary', $project);
4714 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4716 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4717 for (my $i = 0; $i <= $to; $i++) {
4718 my %co = %{$commitlist[$i]};
4719 next if !%co;
4720 my $commit = $co{'id'};
4721 my $ref = format_ref_marker($refs, $commit);
4722 my %ad = parse_date($co{'author_epoch'});
4723 git_print_header_div('commit',
4724 "<span class=\"age\">$co{'age_string'}</span>" .
4725 esc_html($co{'title'}) . $ref,
4726 $commit);
4727 print "<div class=\"title_text\">\n" .
4728 "<div class=\"log_link\">\n" .
4729 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4730 " | " .
4731 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4732 " | " .
4733 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4734 "<br/>\n" .
4735 "</div>\n" .
4736 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4737 "</div>\n";
4739 print "<div class=\"log_body\">\n";
4740 git_print_log($co{'comment'}, -final_empty_line=> 1);
4741 print "</div>\n";
4743 if ($#commitlist >= 100) {
4744 print "<div class=\"page_nav\">\n";
4745 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4746 -accesskey => "n", -title => "Alt-n"}, "next");
4747 print "</div>\n";
4749 git_footer_html();
4752 sub git_commit {
4753 $hash ||= $hash_base || "HEAD";
4754 my %co = parse_commit($hash)
4755 or die_error(404, "Unknown commit object");
4756 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4757 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4759 my $parent = $co{'parent'};
4760 my $parents = $co{'parents'}; # listref
4762 # we need to prepare $formats_nav before any parameter munging
4763 my $formats_nav;
4764 if (!defined $parent) {
4765 # --root commitdiff
4766 $formats_nav .= '(initial)';
4767 } elsif (@$parents == 1) {
4768 # single parent commit
4769 $formats_nav .=
4770 '(parent: ' .
4771 $cgi->a({-href => href(action=>"commit",
4772 hash=>$parent)},
4773 esc_html(substr($parent, 0, 7))) .
4774 ')';
4775 } else {
4776 # merge commit
4777 $formats_nav .=
4778 '(merge: ' .
4779 join(' ', map {
4780 $cgi->a({-href => href(action=>"commit",
4781 hash=>$_)},
4782 esc_html(substr($_, 0, 7)));
4783 } @$parents ) .
4784 ')';
4787 if (!defined $parent) {
4788 $parent = "--root";
4790 my @difftree;
4791 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4792 @diff_opts,
4793 (@$parents <= 1 ? $parent : '-c'),
4794 $hash, "--"
4795 or die_error(500, "Open git-diff-tree failed");
4796 @difftree = map { chomp; $_ } <$fd>;
4797 close $fd or die_error(404, "Reading git-diff-tree failed");
4799 # non-textual hash id's can be cached
4800 my $expires;
4801 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4802 $expires = "+1d";
4804 my $refs = git_get_references();
4805 my $ref = format_ref_marker($refs, $co{'id'});
4807 git_header_html(undef, $expires);
4808 git_print_page_nav('commit', '',
4809 $hash, $co{'tree'}, $hash,
4810 $formats_nav);
4812 if (defined $co{'parent'}) {
4813 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4814 } else {
4815 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4817 print "<div class=\"title_text\">\n" .
4818 "<table class=\"object_header\">\n";
4819 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4820 "<tr>" .
4821 "<td></td><td> $ad{'rfc2822'}";
4822 if ($ad{'hour_local'} < 6) {
4823 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4824 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4825 } else {
4826 printf(" (%02d:%02d %s)",
4827 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4829 print "</td>" .
4830 "</tr>\n";
4831 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4832 print "<tr><td></td><td> $cd{'rfc2822'}" .
4833 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4834 "</td></tr>\n";
4835 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4836 print "<tr>" .
4837 "<td>tree</td>" .
4838 "<td class=\"sha1\">" .
4839 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4840 class => "list"}, $co{'tree'}) .
4841 "</td>" .
4842 "<td class=\"link\">" .
4843 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4844 "tree");
4845 my $snapshot_links = format_snapshot_links($hash);
4846 if (defined $snapshot_links) {
4847 print " | " . $snapshot_links;
4849 print "</td>" .
4850 "</tr>\n";
4852 foreach my $par (@$parents) {
4853 print "<tr>" .
4854 "<td>parent</td>" .
4855 "<td class=\"sha1\">" .
4856 $cgi->a({-href => href(action=>"commit", hash=>$par),
4857 class => "list"}, $par) .
4858 "</td>" .
4859 "<td class=\"link\">" .
4860 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4861 " | " .
4862 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4863 "</td>" .
4864 "</tr>\n";
4866 print "</table>".
4867 "</div>\n";
4869 print "<div class=\"page_body\">\n";
4870 git_print_log($co{'comment'});
4871 print "</div>\n";
4873 git_difftree_body(\@difftree, $hash, @$parents);
4875 git_footer_html();
4878 sub git_object {
4879 # object is defined by:
4880 # - hash or hash_base alone
4881 # - hash_base and file_name
4882 my $type;
4884 # - hash or hash_base alone
4885 if ($hash || ($hash_base && !defined $file_name)) {
4886 my $object_id = $hash || $hash_base;
4888 open my $fd, "-|", quote_command(
4889 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4890 or die_error(404, "Object does not exist");
4891 $type = <$fd>;
4892 chomp $type;
4893 close $fd
4894 or die_error(404, "Object does not exist");
4896 # - hash_base and file_name
4897 } elsif ($hash_base && defined $file_name) {
4898 $file_name =~ s,/+$,,;
4900 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4901 or die_error(404, "Base object does not exist");
4903 # here errors should not hapen
4904 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4905 or die_error(500, "Open git-ls-tree failed");
4906 my $line = <$fd>;
4907 close $fd;
4909 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4910 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4911 die_error(404, "File or directory for given base does not exist");
4913 $type = $2;
4914 $hash = $3;
4915 } else {
4916 die_error(400, "Not enough information to find object");
4919 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4920 hash=>$hash, hash_base=>$hash_base,
4921 file_name=>$file_name),
4922 -status => '302 Found');
4925 sub git_blobdiff {
4926 my $format = shift || 'html';
4928 my $fd;
4929 my @difftree;
4930 my %diffinfo;
4931 my $expires;
4933 # preparing $fd and %diffinfo for git_patchset_body
4934 # new style URI
4935 if (defined $hash_base && defined $hash_parent_base) {
4936 if (defined $file_name) {
4937 # read raw output
4938 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4939 $hash_parent_base, $hash_base,
4940 "--", (defined $file_parent ? $file_parent : ()), $file_name
4941 or die_error(500, "Open git-diff-tree failed");
4942 @difftree = map { chomp; $_ } <$fd>;
4943 close $fd
4944 or die_error(404, "Reading git-diff-tree failed");
4945 @difftree
4946 or die_error(404, "Blob diff not found");
4948 } elsif (defined $hash &&
4949 $hash =~ /[0-9a-fA-F]{40}/) {
4950 # try to find filename from $hash
4952 # read filtered raw output
4953 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4954 $hash_parent_base, $hash_base, "--"
4955 or die_error(500, "Open git-diff-tree failed");
4956 @difftree =
4957 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4958 # $hash == to_id
4959 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4960 map { chomp; $_ } <$fd>;
4961 close $fd
4962 or die_error(404, "Reading git-diff-tree failed");
4963 @difftree
4964 or die_error(404, "Blob diff not found");
4966 } else {
4967 die_error(400, "Missing one of the blob diff parameters");
4970 if (@difftree > 1) {
4971 die_error(400, "Ambiguous blob diff specification");
4974 %diffinfo = parse_difftree_raw_line($difftree[0]);
4975 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4976 $file_name ||= $diffinfo{'to_file'};
4978 $hash_parent ||= $diffinfo{'from_id'};
4979 $hash ||= $diffinfo{'to_id'};
4981 # non-textual hash id's can be cached
4982 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4983 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4984 $expires = '+1d';
4987 # open patch output
4988 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4989 '-p', ($format eq 'html' ? "--full-index" : ()),
4990 $hash_parent_base, $hash_base,
4991 "--", (defined $file_parent ? $file_parent : ()), $file_name
4992 or die_error(500, "Open git-diff-tree failed");
4995 # old/legacy style URI
4996 if (!%diffinfo && # if new style URI failed
4997 defined $hash && defined $hash_parent) {
4998 # fake git-diff-tree raw output
4999 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5000 $diffinfo{'from_id'} = $hash_parent;
5001 $diffinfo{'to_id'} = $hash;
5002 if (defined $file_name) {
5003 if (defined $file_parent) {
5004 $diffinfo{'status'} = '2';
5005 $diffinfo{'from_file'} = $file_parent;
5006 $diffinfo{'to_file'} = $file_name;
5007 } else { # assume not renamed
5008 $diffinfo{'status'} = '1';
5009 $diffinfo{'from_file'} = $file_name;
5010 $diffinfo{'to_file'} = $file_name;
5012 } else { # no filename given
5013 $diffinfo{'status'} = '2';
5014 $diffinfo{'from_file'} = $hash_parent;
5015 $diffinfo{'to_file'} = $hash;
5018 # non-textual hash id's can be cached
5019 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5020 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5021 $expires = '+1d';
5024 # open patch output
5025 open $fd, "-|", git_cmd(), "diff", @diff_opts,
5026 '-p', ($format eq 'html' ? "--full-index" : ()),
5027 $hash_parent, $hash, "--"
5028 or die_error(500, "Open git-diff failed");
5029 } else {
5030 die_error(400, "Missing one of the blob diff parameters")
5031 unless %diffinfo;
5034 # header
5035 if ($format eq 'html') {
5036 my $formats_nav =
5037 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5038 "raw");
5039 git_header_html(undef, $expires);
5040 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5041 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5042 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5043 } else {
5044 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5045 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5047 if (defined $file_name) {
5048 git_print_page_path($file_name, "blob", $hash_base);
5049 } else {
5050 print "<div class=\"page_path\"></div>\n";
5053 } elsif ($format eq 'plain') {
5054 print $cgi->header(
5055 -type => 'text/plain',
5056 -charset => 'utf-8',
5057 -expires => $expires,
5058 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5060 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5062 } else {
5063 die_error(400, "Unknown blobdiff format");
5066 # patch
5067 if ($format eq 'html') {
5068 print "<div class=\"page_body\">\n";
5070 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5071 close $fd;
5073 print "</div>\n"; # class="page_body"
5074 git_footer_html();
5076 } else {
5077 while (my $line = <$fd>) {
5078 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5079 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5081 print $line;
5083 last if $line =~ m!^\+\+\+!;
5085 local $/ = undef;
5086 print <$fd>;
5087 close $fd;
5091 sub git_blobdiff_plain {
5092 git_blobdiff('plain');
5095 sub git_commitdiff {
5096 my $format = shift || 'html';
5097 $hash ||= $hash_base || "HEAD";
5098 my %co = parse_commit($hash)
5099 or die_error(404, "Unknown commit object");
5101 # choose format for commitdiff for merge
5102 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5103 $hash_parent = '--cc';
5105 # we need to prepare $formats_nav before almost any parameter munging
5106 my $formats_nav;
5107 if ($format eq 'html') {
5108 $formats_nav =
5109 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5110 "raw");
5112 if (defined $hash_parent &&
5113 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5114 # commitdiff with two commits given
5115 my $hash_parent_short = $hash_parent;
5116 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5117 $hash_parent_short = substr($hash_parent, 0, 7);
5119 $formats_nav .=
5120 ' (from';
5121 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5122 if ($co{'parents'}[$i] eq $hash_parent) {
5123 $formats_nav .= ' parent ' . ($i+1);
5124 last;
5127 $formats_nav .= ': ' .
5128 $cgi->a({-href => href(action=>"commitdiff",
5129 hash=>$hash_parent)},
5130 esc_html($hash_parent_short)) .
5131 ')';
5132 } elsif (!$co{'parent'}) {
5133 # --root commitdiff
5134 $formats_nav .= ' (initial)';
5135 } elsif (scalar @{$co{'parents'}} == 1) {
5136 # single parent commit
5137 $formats_nav .=
5138 ' (parent: ' .
5139 $cgi->a({-href => href(action=>"commitdiff",
5140 hash=>$co{'parent'})},
5141 esc_html(substr($co{'parent'}, 0, 7))) .
5142 ')';
5143 } else {
5144 # merge commit
5145 if ($hash_parent eq '--cc') {
5146 $formats_nav .= ' | ' .
5147 $cgi->a({-href => href(action=>"commitdiff",
5148 hash=>$hash, hash_parent=>'-c')},
5149 'combined');
5150 } else { # $hash_parent eq '-c'
5151 $formats_nav .= ' | ' .
5152 $cgi->a({-href => href(action=>"commitdiff",
5153 hash=>$hash, hash_parent=>'--cc')},
5154 'compact');
5156 $formats_nav .=
5157 ' (merge: ' .
5158 join(' ', map {
5159 $cgi->a({-href => href(action=>"commitdiff",
5160 hash=>$_)},
5161 esc_html(substr($_, 0, 7)));
5162 } @{$co{'parents'}} ) .
5163 ')';
5167 my $hash_parent_param = $hash_parent;
5168 if (!defined $hash_parent_param) {
5169 # --cc for multiple parents, --root for parentless
5170 $hash_parent_param =
5171 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5174 # read commitdiff
5175 my $fd;
5176 my @difftree;
5177 if ($format eq 'html') {
5178 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5179 "--no-commit-id", "--patch-with-raw", "--full-index",
5180 $hash_parent_param, $hash, "--"
5181 or die_error(500, "Open git-diff-tree failed");
5183 while (my $line = <$fd>) {
5184 chomp $line;
5185 # empty line ends raw part of diff-tree output
5186 last unless $line;
5187 push @difftree, scalar parse_difftree_raw_line($line);
5190 } elsif ($format eq 'plain') {
5191 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5192 '-p', $hash_parent_param, $hash, "--"
5193 or die_error(500, "Open git-diff-tree failed");
5195 } else {
5196 die_error(400, "Unknown commitdiff format");
5199 # non-textual hash id's can be cached
5200 my $expires;
5201 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5202 $expires = "+1d";
5205 # write commit message
5206 if ($format eq 'html') {
5207 my $refs = git_get_references();
5208 my $ref = format_ref_marker($refs, $co{'id'});
5210 git_header_html(undef, $expires);
5211 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5212 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5213 git_print_authorship(\%co);
5214 print "<div class=\"page_body\">\n";
5215 if (@{$co{'comment'}} > 1) {
5216 print "<div class=\"log\">\n";
5217 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5218 print "</div>\n"; # class="log"
5221 } elsif ($format eq 'plain') {
5222 my $refs = git_get_references("tags");
5223 my $tagname = git_get_rev_name_tags($hash);
5224 my $filename = basename($project) . "-$hash.patch";
5226 print $cgi->header(
5227 -type => 'text/plain',
5228 -charset => 'utf-8',
5229 -expires => $expires,
5230 -content_disposition => 'inline; filename="' . "$filename" . '"');
5231 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5232 print "From: " . to_utf8($co{'author'}) . "\n";
5233 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5234 print "Subject: " . to_utf8($co{'title'}) . "\n";
5236 print "X-Git-Tag: $tagname\n" if $tagname;
5237 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5239 foreach my $line (@{$co{'comment'}}) {
5240 print to_utf8($line) . "\n";
5242 print "---\n\n";
5245 # write patch
5246 if ($format eq 'html') {
5247 my $use_parents = !defined $hash_parent ||
5248 $hash_parent eq '-c' || $hash_parent eq '--cc';
5249 git_difftree_body(\@difftree, $hash,
5250 $use_parents ? @{$co{'parents'}} : $hash_parent);
5251 print "<br/>\n";
5253 git_patchset_body($fd, \@difftree, $hash,
5254 $use_parents ? @{$co{'parents'}} : $hash_parent);
5255 close $fd;
5256 print "</div>\n"; # class="page_body"
5257 git_footer_html();
5259 } elsif ($format eq 'plain') {
5260 local $/ = undef;
5261 print <$fd>;
5262 close $fd
5263 or print "Reading git-diff-tree failed\n";
5267 sub git_commitdiff_plain {
5268 git_commitdiff('plain');
5271 sub git_history {
5272 if (!defined $hash_base) {
5273 $hash_base = git_get_head_hash($project);
5275 if (!defined $page) {
5276 $page = 0;
5278 my $ftype;
5279 my %co = parse_commit($hash_base)
5280 or die_error(404, "Unknown commit object");
5282 my $refs = git_get_references();
5283 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5285 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5286 $file_name, "--full-history")
5287 or die_error(404, "No such file or directory on given branch");
5289 if (!defined $hash && defined $file_name) {
5290 # some commits could have deleted file in question,
5291 # and not have it in tree, but one of them has to have it
5292 for (my $i = 0; $i <= @commitlist; $i++) {
5293 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5294 last if defined $hash;
5297 if (defined $hash) {
5298 $ftype = git_get_type($hash);
5300 if (!defined $ftype) {
5301 die_error(500, "Unknown type of object");
5304 my $paging_nav = '';
5305 if ($page > 0) {
5306 $paging_nav .=
5307 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5308 file_name=>$file_name)},
5309 "first");
5310 $paging_nav .= " &sdot; " .
5311 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5312 -accesskey => "p", -title => "Alt-p"}, "prev");
5313 } else {
5314 $paging_nav .= "first";
5315 $paging_nav .= " &sdot; prev";
5317 my $next_link = '';
5318 if ($#commitlist >= 100) {
5319 $next_link =
5320 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5321 -accesskey => "n", -title => "Alt-n"}, "next");
5322 $paging_nav .= " &sdot; $next_link";
5323 } else {
5324 $paging_nav .= " &sdot; next";
5327 git_header_html();
5328 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5329 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5330 git_print_page_path($file_name, $ftype, $hash_base);
5332 git_history_body(\@commitlist, 0, 99,
5333 $refs, $hash_base, $ftype, $next_link);
5335 git_footer_html();
5338 sub git_search {
5339 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5340 if (!defined $searchtext) {
5341 die_error(400, "Text field is empty");
5343 if (!defined $hash) {
5344 $hash = git_get_head_hash($project);
5346 my %co = parse_commit($hash);
5347 if (!%co) {
5348 die_error(404, "Unknown commit object");
5350 if (!defined $page) {
5351 $page = 0;
5354 $searchtype ||= 'commit';
5355 if ($searchtype eq 'pickaxe') {
5356 # pickaxe may take all resources of your box and run for several minutes
5357 # with every query - so decide by yourself how public you make this feature
5358 gitweb_check_feature('pickaxe')
5359 or die_error(403, "Pickaxe is disabled");
5361 if ($searchtype eq 'grep') {
5362 gitweb_check_feature('grep')
5363 or die_error(403, "Grep is disabled");
5366 git_header_html();
5368 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5369 my $greptype;
5370 if ($searchtype eq 'commit') {
5371 $greptype = "--grep=";
5372 } elsif ($searchtype eq 'author') {
5373 $greptype = "--author=";
5374 } elsif ($searchtype eq 'committer') {
5375 $greptype = "--committer=";
5377 $greptype .= $searchtext;
5378 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5379 $greptype, '--regexp-ignore-case',
5380 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5382 my $paging_nav = '';
5383 if ($page > 0) {
5384 $paging_nav .=
5385 $cgi->a({-href => href(action=>"search", hash=>$hash,
5386 searchtext=>$searchtext,
5387 searchtype=>$searchtype)},
5388 "first");
5389 $paging_nav .= " &sdot; " .
5390 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5391 -accesskey => "p", -title => "Alt-p"}, "prev");
5392 } else {
5393 $paging_nav .= "first";
5394 $paging_nav .= " &sdot; prev";
5396 my $next_link = '';
5397 if ($#commitlist >= 100) {
5398 $next_link =
5399 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5400 -accesskey => "n", -title => "Alt-n"}, "next");
5401 $paging_nav .= " &sdot; $next_link";
5402 } else {
5403 $paging_nav .= " &sdot; next";
5406 if ($#commitlist >= 100) {
5409 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5410 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5411 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5414 if ($searchtype eq 'pickaxe') {
5415 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5416 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5418 print "<table class=\"pickaxe search\">\n";
5419 my $alternate = 1;
5420 $/ = "\n";
5421 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5422 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5423 ($search_use_regexp ? '--pickaxe-regex' : ());
5424 undef %co;
5425 my @files;
5426 while (my $line = <$fd>) {
5427 chomp $line;
5428 next unless $line;
5430 my %set = parse_difftree_raw_line($line);
5431 if (defined $set{'commit'}) {
5432 # finish previous commit
5433 if (%co) {
5434 print "</td>\n" .
5435 "<td class=\"link\">" .
5436 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5437 " | " .
5438 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5439 print "</td>\n" .
5440 "</tr>\n";
5443 if ($alternate) {
5444 print "<tr class=\"dark\">\n";
5445 } else {
5446 print "<tr class=\"light\">\n";
5448 $alternate ^= 1;
5449 %co = parse_commit($set{'commit'});
5450 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5451 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5452 "<td><i>$author</i></td>\n" .
5453 "<td>" .
5454 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5455 -class => "list subject"},
5456 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5457 } elsif (defined $set{'to_id'}) {
5458 next if ($set{'to_id'} =~ m/^0{40}$/);
5460 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5461 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5462 -class => "list"},
5463 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5464 "<br/>\n";
5467 close $fd;
5469 # finish last commit (warning: repetition!)
5470 if (%co) {
5471 print "</td>\n" .
5472 "<td class=\"link\">" .
5473 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5474 " | " .
5475 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5476 print "</td>\n" .
5477 "</tr>\n";
5480 print "</table>\n";
5483 if ($searchtype eq 'grep') {
5484 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5485 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5487 print "<table class=\"grep_search\">\n";
5488 my $alternate = 1;
5489 my $matches = 0;
5490 $/ = "\n";
5491 open my $fd, "-|", git_cmd(), 'grep', '-n',
5492 $search_use_regexp ? ('-E', '-i') : '-F',
5493 $searchtext, $co{'tree'};
5494 my $lastfile = '';
5495 while (my $line = <$fd>) {
5496 chomp $line;
5497 my ($file, $lno, $ltext, $binary);
5498 last if ($matches++ > 1000);
5499 if ($line =~ /^Binary file (.+) matches$/) {
5500 $file = $1;
5501 $binary = 1;
5502 } else {
5503 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5505 if ($file ne $lastfile) {
5506 $lastfile and print "</td></tr>\n";
5507 if ($alternate++) {
5508 print "<tr class=\"dark\">\n";
5509 } else {
5510 print "<tr class=\"light\">\n";
5512 print "<td class=\"list\">".
5513 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5514 file_name=>"$file"),
5515 -class => "list"}, esc_path($file));
5516 print "</td><td>\n";
5517 $lastfile = $file;
5519 if ($binary) {
5520 print "<div class=\"binary\">Binary file</div>\n";
5521 } else {
5522 $ltext = untabify($ltext);
5523 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5524 $ltext = esc_html($1, -nbsp=>1);
5525 $ltext .= '<span class="match">';
5526 $ltext .= esc_html($2, -nbsp=>1);
5527 $ltext .= '</span>';
5528 $ltext .= esc_html($3, -nbsp=>1);
5529 } else {
5530 $ltext = esc_html($ltext, -nbsp=>1);
5532 print "<div class=\"pre\">" .
5533 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5534 file_name=>"$file").'#l'.$lno,
5535 -class => "linenr"}, sprintf('%4i', $lno))
5536 . ' ' . $ltext . "</div>\n";
5539 if ($lastfile) {
5540 print "</td></tr>\n";
5541 if ($matches > 1000) {
5542 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5544 } else {
5545 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5547 close $fd;
5549 print "</table>\n";
5551 git_footer_html();
5554 sub git_search_help {
5555 git_header_html();
5556 git_print_page_nav('','', $hash,$hash,$hash);
5557 print <<EOT;
5558 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5559 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5560 the pattern entered is recognized as the POSIX extended
5561 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5562 insensitive).</p>
5563 <dl>
5564 <dt><b>commit</b></dt>
5565 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5567 my ($have_grep) = gitweb_check_feature('grep');
5568 if ($have_grep) {
5569 print <<EOT;
5570 <dt><b>grep</b></dt>
5571 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5572 a different one) are searched for the given pattern. On large trees, this search can take
5573 a while and put some strain on the server, so please use it with some consideration. Note that
5574 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5575 case-sensitive.</dd>
5578 print <<EOT;
5579 <dt><b>author</b></dt>
5580 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5581 <dt><b>committer</b></dt>
5582 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5584 my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5585 if ($have_pickaxe) {
5586 print <<EOT;
5587 <dt><b>pickaxe</b></dt>
5588 <dd>All commits that caused the string to appear or disappear from any file (changes that
5589 added, removed or "modified" the string) will be listed. This search can take a while and
5590 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5591 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5594 print "</dl>\n";
5595 git_footer_html();
5598 sub git_shortlog {
5599 my $head = git_get_head_hash($project);
5600 if (!defined $hash) {
5601 $hash = $head;
5603 if (!defined $page) {
5604 $page = 0;
5606 my $refs = git_get_references();
5608 my @commitlist = parse_commits($hash, 101, (100 * $page));
5610 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5611 my $next_link = '';
5612 if ($#commitlist >= 100) {
5613 $next_link =
5614 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5615 -accesskey => "n", -title => "Alt-n"}, "next");
5618 git_header_html();
5619 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5620 git_print_header_div('summary', $project);
5622 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5624 git_footer_html();
5627 ## ......................................................................
5628 ## feeds (RSS, Atom; OPML)
5630 sub git_feed {
5631 my $format = shift || 'atom';
5632 my ($have_blame) = gitweb_check_feature('blame');
5634 # Atom: http://www.atomenabled.org/developers/syndication/
5635 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5636 if ($format ne 'rss' && $format ne 'atom') {
5637 die_error(400, "Unknown web feed format");
5640 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5641 my $head = $hash || 'HEAD';
5642 my @commitlist = parse_commits($head, 150, 0, $file_name);
5644 my %latest_commit;
5645 my %latest_date;
5646 my $content_type = "application/$format+xml";
5647 if (defined $cgi->http('HTTP_ACCEPT') &&
5648 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5649 # browser (feed reader) prefers text/xml
5650 $content_type = 'text/xml';
5652 if (defined($commitlist[0])) {
5653 %latest_commit = %{$commitlist[0]};
5654 %latest_date = parse_date($latest_commit{'author_epoch'});
5655 print $cgi->header(
5656 -type => $content_type,
5657 -charset => 'utf-8',
5658 -last_modified => $latest_date{'rfc2822'});
5659 } else {
5660 print $cgi->header(
5661 -type => $content_type,
5662 -charset => 'utf-8');
5665 # Optimization: skip generating the body if client asks only
5666 # for Last-Modified date.
5667 return if ($cgi->request_method() eq 'HEAD');
5669 # header variables
5670 my $title = "$site_name - $project/$action";
5671 my $feed_type = 'log';
5672 if (defined $hash) {
5673 $title .= " - '$hash'";
5674 $feed_type = 'branch log';
5675 if (defined $file_name) {
5676 $title .= " :: $file_name";
5677 $feed_type = 'history';
5679 } elsif (defined $file_name) {
5680 $title .= " - $file_name";
5681 $feed_type = 'history';
5683 $title .= " $feed_type";
5684 my $descr = git_get_project_description($project);
5685 if (defined $descr) {
5686 $descr = esc_html($descr);
5687 } else {
5688 $descr = "$project " .
5689 ($format eq 'rss' ? 'RSS' : 'Atom') .
5690 " feed";
5692 my $owner = git_get_project_owner($project);
5693 $owner = esc_html($owner);
5695 #header
5696 my $alt_url;
5697 if (defined $file_name) {
5698 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5699 } elsif (defined $hash) {
5700 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5701 } else {
5702 $alt_url = href(-full=>1, action=>"summary");
5704 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5705 if ($format eq 'rss') {
5706 print <<XML;
5707 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5708 <channel>
5710 print "<title>$title</title>\n" .
5711 "<link>$alt_url</link>\n" .
5712 "<description>$descr</description>\n" .
5713 "<language>en</language>\n";
5714 } elsif ($format eq 'atom') {
5715 print <<XML;
5716 <feed xmlns="http://www.w3.org/2005/Atom">
5718 print "<title>$title</title>\n" .
5719 "<subtitle>$descr</subtitle>\n" .
5720 '<link rel="alternate" type="text/html" href="' .
5721 $alt_url . '" />' . "\n" .
5722 '<link rel="self" type="' . $content_type . '" href="' .
5723 $cgi->self_url() . '" />' . "\n" .
5724 "<id>" . href(-full=>1) . "</id>\n" .
5725 # use project owner for feed author
5726 "<author><name>$owner</name></author>\n";
5727 if (defined $favicon) {
5728 print "<icon>" . esc_url($favicon) . "</icon>\n";
5730 if (defined $logo_url) {
5731 # not twice as wide as tall: 72 x 27 pixels
5732 print "<logo>" . esc_url($logo) . "</logo>\n";
5734 if (! %latest_date) {
5735 # dummy date to keep the feed valid until commits trickle in:
5736 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5737 } else {
5738 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5742 # contents
5743 for (my $i = 0; $i <= $#commitlist; $i++) {
5744 my %co = %{$commitlist[$i]};
5745 my $commit = $co{'id'};
5746 # we read 150, we always show 30 and the ones more recent than 48 hours
5747 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5748 last;
5750 my %cd = parse_date($co{'author_epoch'});
5752 # get list of changed files
5753 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5754 $co{'parent'} || "--root",
5755 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5756 or next;
5757 my @difftree = map { chomp; $_ } <$fd>;
5758 close $fd
5759 or next;
5761 # print element (entry, item)
5762 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5763 if ($format eq 'rss') {
5764 print "<item>\n" .
5765 "<title>" . esc_html($co{'title'}) . "</title>\n" .
5766 "<author>" . esc_html($co{'author'}) . "</author>\n" .
5767 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5768 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5769 "<link>$co_url</link>\n" .
5770 "<description>" . esc_html($co{'title'}) . "</description>\n" .
5771 "<content:encoded>" .
5772 "<![CDATA[\n";
5773 } elsif ($format eq 'atom') {
5774 print "<entry>\n" .
5775 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5776 "<updated>$cd{'iso-8601'}</updated>\n" .
5777 "<author>\n" .
5778 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
5779 if ($co{'author_email'}) {
5780 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
5782 print "</author>\n" .
5783 # use committer for contributor
5784 "<contributor>\n" .
5785 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5786 if ($co{'committer_email'}) {
5787 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5789 print "</contributor>\n" .
5790 "<published>$cd{'iso-8601'}</published>\n" .
5791 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5792 "<id>$co_url</id>\n" .
5793 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5794 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5796 my $comment = $co{'comment'};
5797 print "<pre>\n";
5798 foreach my $line (@$comment) {
5799 $line = esc_html($line);
5800 print "$line\n";
5802 print "</pre><ul>\n";
5803 foreach my $difftree_line (@difftree) {
5804 my %difftree = parse_difftree_raw_line($difftree_line);
5805 next if !$difftree{'from_id'};
5807 my $file = $difftree{'file'} || $difftree{'to_file'};
5809 print "<li>" .
5810 "[" .
5811 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5812 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5813 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5814 file_name=>$file, file_parent=>$difftree{'from_file'}),
5815 -title => "diff"}, 'D');
5816 if ($have_blame) {
5817 print $cgi->a({-href => href(-full=>1, action=>"blame",
5818 file_name=>$file, hash_base=>$commit),
5819 -title => "blame"}, 'B');
5821 # if this is not a feed of a file history
5822 if (!defined $file_name || $file_name ne $file) {
5823 print $cgi->a({-href => href(-full=>1, action=>"history",
5824 file_name=>$file, hash=>$commit),
5825 -title => "history"}, 'H');
5827 $file = esc_path($file);
5828 print "] ".
5829 "$file</li>\n";
5831 if ($format eq 'rss') {
5832 print "</ul>]]>\n" .
5833 "</content:encoded>\n" .
5834 "</item>\n";
5835 } elsif ($format eq 'atom') {
5836 print "</ul>\n</div>\n" .
5837 "</content>\n" .
5838 "</entry>\n";
5842 # end of feed
5843 if ($format eq 'rss') {
5844 print "</channel>\n</rss>\n";
5845 } elsif ($format eq 'atom') {
5846 print "</feed>\n";
5850 sub git_rss {
5851 git_feed('rss');
5854 sub git_atom {
5855 git_feed('atom');
5858 sub git_opml {
5859 my @list = git_get_projects_list();
5861 print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5862 print <<XML;
5863 <?xml version="1.0" encoding="utf-8"?>
5864 <opml version="1.0">
5865 <head>
5866 <title>$site_name OPML Export</title>
5867 </head>
5868 <body>
5869 <outline text="git RSS feeds">
5872 foreach my $pr (@list) {
5873 my %proj = %$pr;
5874 my $head = git_get_head_hash($proj{'path'});
5875 if (!defined $head) {
5876 next;
5878 $git_dir = "$projectroot/$proj{'path'}";
5879 my %co = parse_commit($head);
5880 if (!%co) {
5881 next;
5884 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5885 my $rss = "$my_url?p=$proj{'path'};a=rss";
5886 my $html = "$my_url?p=$proj{'path'};a=summary";
5887 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5889 print <<XML;
5890 </outline>
5891 </body>
5892 </opml>