gitweb: Support for tag clouds
[alt-git.git] / gitweb / gitweb.perl
blob0cb29705b2e8b343b162554ceef094741400fe56
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 # if we're called with PATH_INFO, we have to strip that
31 # from the URL to find our real URL
32 if (my $path_info = $ENV{"PATH_INFO"}) {
33 $my_url =~ s,\Q$path_info\E$,,;
34 $my_uri =~ s,\Q$path_info\E$,,;
37 # core git executable to use
38 # this can just be "git" if your webserver has a sensible PATH
39 our $GIT = "++GIT_BINDIR++/git";
41 # absolute fs-path which will be prepended to the project path
42 #our $projectroot = "/pub/scm";
43 our $projectroot = "++GITWEB_PROJECTROOT++";
45 # fs traversing limit for getting project list
46 # the number is relative to the projectroot
47 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
49 # target of the home link on top of all pages
50 our $home_link = $my_uri || "/";
52 # string of the home link on top of all pages
53 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
55 # name of your site or organization to appear in page titles
56 # replace this with something more descriptive for clearer bookmarks
57 our $site_name = "++GITWEB_SITENAME++"
58 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
60 # filename of html text to include at top of each page
61 our $site_header = "++GITWEB_SITE_HEADER++";
62 # html text to include at home page
63 our $home_text = "++GITWEB_HOMETEXT++";
64 # filename of html text to include at bottom of each page
65 our $site_footer = "++GITWEB_SITE_FOOTER++";
67 # URI of stylesheets
68 our @stylesheets = ("++GITWEB_CSS++");
69 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
70 our $stylesheet = undef;
71 # URI of GIT logo (72x27 size)
72 our $logo = "++GITWEB_LOGO++";
73 # URI of GIT favicon, assumed to be image/png type
74 our $favicon = "++GITWEB_FAVICON++";
76 # URI and label (title) of GIT logo link
77 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
78 #our $logo_label = "git documentation";
79 our $logo_url = "http://git.or.cz/";
80 our $logo_label = "git homepage";
82 # source of projects list
83 our $projects_list = "++GITWEB_LIST++";
85 # the width (in characters) of the projects list "Description" column
86 our $projects_list_description_width = 25;
88 # default order of projects list
89 # valid values are none, project, descr, owner, and age
90 our $default_projects_order = "project";
92 # show repository only if this file exists
93 # (only effective if this variable evaluates to true)
94 our $export_ok = "++GITWEB_EXPORT_OK++";
96 # only allow viewing of repositories also shown on the overview page
97 our $strict_export = "++GITWEB_STRICT_EXPORT++";
99 # list of git base URLs used for URL to where fetch project from,
100 # i.e. full URL is "$git_base_url/$project"
101 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
103 # default blob_plain mimetype and default charset for text/plain blob
104 our $default_blob_plain_mimetype = 'text/plain';
105 our $default_text_plain_charset = undef;
107 # file to use for guessing MIME types before trying /etc/mime.types
108 # (relative to the current git repository)
109 our $mimetypes_file = undef;
111 # assume this charset if line contains non-UTF-8 characters;
112 # it should be valid encoding (see Encoding::Supported(3pm) for list),
113 # for which encoding all byte sequences are valid, for example
114 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
115 # could be even 'utf-8' for the old behavior)
116 our $fallback_encoding = 'latin1';
118 # rename detection options for git-diff and git-diff-tree
119 # - default is '-M', with the cost proportional to
120 # (number of removed files) * (number of new files).
121 # - more costly is '-C' (which implies '-M'), with the cost proportional to
122 # (number of changed files + number of removed files) * (number of new files)
123 # - even more costly is '-C', '--find-copies-harder' with cost
124 # (number of files in the original tree) * (number of new files)
125 # - one might want to include '-B' option, e.g. '-B', '-M'
126 our @diff_opts = ('-M'); # taken from git_commit
128 # information about snapshot formats that gitweb is capable of serving
129 our %known_snapshot_formats = (
130 # name => {
131 # 'display' => display name,
132 # 'type' => mime type,
133 # 'suffix' => filename suffix,
134 # 'format' => --format for git-archive,
135 # 'compressor' => [compressor command and arguments]
136 # (array reference, optional)}
138 'tgz' => {
139 'display' => 'tar.gz',
140 'type' => 'application/x-gzip',
141 'suffix' => '.tar.gz',
142 'format' => 'tar',
143 'compressor' => ['gzip']},
145 'tbz2' => {
146 'display' => 'tar.bz2',
147 'type' => 'application/x-bzip2',
148 'suffix' => '.tar.bz2',
149 'format' => 'tar',
150 'compressor' => ['bzip2']},
152 'zip' => {
153 'display' => 'zip',
154 'type' => 'application/x-zip',
155 'suffix' => '.zip',
156 'format' => 'zip'},
159 # Aliases so we understand old gitweb.snapshot values in repository
160 # configuration.
161 our %known_snapshot_format_aliases = (
162 'gzip' => 'tgz',
163 'bzip2' => 'tbz2',
165 # backward compatibility: legacy gitweb config support
166 'x-gzip' => undef, 'gz' => undef,
167 'x-bzip2' => undef, 'bz2' => undef,
168 'x-zip' => undef, '' => undef,
171 # You define site-wide feature defaults here; override them with
172 # $GITWEB_CONFIG as necessary.
173 our %feature = (
174 # feature => {
175 # 'sub' => feature-sub (subroutine),
176 # 'override' => allow-override (boolean),
177 # 'default' => [ default options...] (array reference)}
179 # if feature is overridable (it means that allow-override has true value),
180 # then feature-sub will be called with default options as parameters;
181 # return value of feature-sub indicates if to enable specified feature
183 # if there is no 'sub' key (no feature-sub), then feature cannot be
184 # overriden
186 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
188 # Enable the 'blame' blob view, showing the last commit that modified
189 # each line in the file. This can be very CPU-intensive.
191 # To enable system wide have in $GITWEB_CONFIG
192 # $feature{'blame'}{'default'} = [1];
193 # To have project specific config enable override in $GITWEB_CONFIG
194 # $feature{'blame'}{'override'} = 1;
195 # and in project config gitweb.blame = 0|1;
196 'blame' => {
197 'sub' => \&feature_blame,
198 'override' => 0,
199 'default' => [0]},
201 # Enable the 'snapshot' link, providing a compressed archive of any
202 # tree. This can potentially generate high traffic if you have large
203 # project.
205 # Value is a list of formats defined in %known_snapshot_formats that
206 # you wish to offer.
207 # To disable system wide have in $GITWEB_CONFIG
208 # $feature{'snapshot'}{'default'} = [];
209 # To have project specific config enable override in $GITWEB_CONFIG
210 # $feature{'snapshot'}{'override'} = 1;
211 # and in project config, a comma-separated list of formats or "none"
212 # to disable. Example: gitweb.snapshot = tbz2,zip;
213 'snapshot' => {
214 'sub' => \&feature_snapshot,
215 'override' => 0,
216 'default' => ['tgz']},
218 # Enable text search, which will list the commits which match author,
219 # committer or commit text to a given string. Enabled by default.
220 # Project specific override is not supported.
221 'search' => {
222 'override' => 0,
223 'default' => [1]},
225 # Enable grep search, which will list the files in currently selected
226 # tree containing the given string. Enabled by default. This can be
227 # potentially CPU-intensive, of course.
229 # To enable system wide have in $GITWEB_CONFIG
230 # $feature{'grep'}{'default'} = [1];
231 # To have project specific config enable override in $GITWEB_CONFIG
232 # $feature{'grep'}{'override'} = 1;
233 # and in project config gitweb.grep = 0|1;
234 'grep' => {
235 'override' => 0,
236 'default' => [1]},
238 # Enable the pickaxe search, which will list the commits that modified
239 # a given string in a file. This can be practical and quite faster
240 # alternative to 'blame', but still potentially CPU-intensive.
242 # To enable system wide have in $GITWEB_CONFIG
243 # $feature{'pickaxe'}{'default'} = [1];
244 # To have project specific config enable override in $GITWEB_CONFIG
245 # $feature{'pickaxe'}{'override'} = 1;
246 # and in project config gitweb.pickaxe = 0|1;
247 'pickaxe' => {
248 'sub' => \&feature_pickaxe,
249 'override' => 0,
250 'default' => [1]},
252 # Make gitweb use an alternative format of the URLs which can be
253 # more readable and natural-looking: project name is embedded
254 # directly in the path and the query string contains other
255 # auxiliary information. All gitweb installations recognize
256 # URL in either format; this configures in which formats gitweb
257 # generates links.
259 # To enable system wide have in $GITWEB_CONFIG
260 # $feature{'pathinfo'}{'default'} = [1];
261 # Project specific override is not supported.
263 # Note that you will need to change the default location of CSS,
264 # favicon, logo and possibly other files to an absolute URL. Also,
265 # if gitweb.cgi serves as your indexfile, you will need to force
266 # $my_uri to contain the script name in your $GITWEB_CONFIG.
267 'pathinfo' => {
268 'override' => 0,
269 'default' => [0]},
271 # Make gitweb consider projects in project root subdirectories
272 # to be forks of existing projects. Given project $projname.git,
273 # projects matching $projname/*.git will not be shown in the main
274 # projects list, instead a '+' mark will be added to $projname
275 # there and a 'forks' view will be enabled for the project, listing
276 # all the forks. If project list is taken from a file, forks have
277 # to be listed after the main project.
279 # To enable system wide have in $GITWEB_CONFIG
280 # $feature{'forks'}{'default'} = [1];
281 # Project specific override is not supported.
282 'forks' => {
283 'override' => 0,
284 'default' => [0]},
286 # Allow gitweb scan project content tags described in ctags/
287 # of project repository, and display the popular Web 2.0-ish
288 # "tag cloud" near the project list. Note that this is something
289 # COMPLETELY different from the normal Git tags.
291 # gitweb by itself can show existing tags, but it does not handle
292 # tagging itself; you need an external application for that.
293 # For an example script, check Girocco's cgi/tagproj.cgi.
294 # You may want to install the HTML::TagCloud Perl module to get
295 # a pretty tag cloud instead of just a list of tags.
297 # To enable system wide have in $GITWEB_CONFIG
298 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
299 # Project specific override is not supported.
300 'ctags' => {
301 'override' => 0,
302 'default' => [0]},
305 sub gitweb_check_feature {
306 my ($name) = @_;
307 return unless exists $feature{$name};
308 my ($sub, $override, @defaults) = (
309 $feature{$name}{'sub'},
310 $feature{$name}{'override'},
311 @{$feature{$name}{'default'}});
312 if (!$override) { return @defaults; }
313 if (!defined $sub) {
314 warn "feature $name is not overrideable";
315 return @defaults;
317 return $sub->(@defaults);
320 sub feature_blame {
321 my ($val) = git_get_project_config('blame', '--bool');
323 if ($val eq 'true') {
324 return 1;
325 } elsif ($val eq 'false') {
326 return 0;
329 return $_[0];
332 sub feature_snapshot {
333 my (@fmts) = @_;
335 my ($val) = git_get_project_config('snapshot');
337 if ($val) {
338 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
341 return @fmts;
344 sub feature_grep {
345 my ($val) = git_get_project_config('grep', '--bool');
347 if ($val eq 'true') {
348 return (1);
349 } elsif ($val eq 'false') {
350 return (0);
353 return ($_[0]);
356 sub feature_pickaxe {
357 my ($val) = git_get_project_config('pickaxe', '--bool');
359 if ($val eq 'true') {
360 return (1);
361 } elsif ($val eq 'false') {
362 return (0);
365 return ($_[0]);
368 # checking HEAD file with -e is fragile if the repository was
369 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
370 # and then pruned.
371 sub check_head_link {
372 my ($dir) = @_;
373 my $headfile = "$dir/HEAD";
374 return ((-e $headfile) ||
375 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
378 sub check_export_ok {
379 my ($dir) = @_;
380 return (check_head_link($dir) &&
381 (!$export_ok || -e "$dir/$export_ok"));
384 # process alternate names for backward compatibility
385 # filter out unsupported (unknown) snapshot formats
386 sub filter_snapshot_fmts {
387 my @fmts = @_;
389 @fmts = map {
390 exists $known_snapshot_format_aliases{$_} ?
391 $known_snapshot_format_aliases{$_} : $_} @fmts;
392 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
396 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
397 if (-e $GITWEB_CONFIG) {
398 do $GITWEB_CONFIG;
399 } else {
400 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
401 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
404 # version of the core git binary
405 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
407 $projects_list ||= $projectroot;
409 # ======================================================================
410 # input validation and dispatch
411 our $action = $cgi->param('a');
412 if (defined $action) {
413 if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
414 die_error(400, "Invalid action parameter");
418 # parameters which are pathnames
419 our $project = $cgi->param('p');
420 if (defined $project) {
421 if (!validate_pathname($project) ||
422 !(-d "$projectroot/$project") ||
423 !check_head_link("$projectroot/$project") ||
424 ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
425 ($strict_export && !project_in_list($project))) {
426 undef $project;
427 die_error(404, "No such project");
431 our $file_name = $cgi->param('f');
432 if (defined $file_name) {
433 if (!validate_pathname($file_name)) {
434 die_error(400, "Invalid file parameter");
438 our $file_parent = $cgi->param('fp');
439 if (defined $file_parent) {
440 if (!validate_pathname($file_parent)) {
441 die_error(400, "Invalid file parent parameter");
445 # parameters which are refnames
446 our $hash = $cgi->param('h');
447 if (defined $hash) {
448 if (!validate_refname($hash)) {
449 die_error(400, "Invalid hash parameter");
453 our $hash_parent = $cgi->param('hp');
454 if (defined $hash_parent) {
455 if (!validate_refname($hash_parent)) {
456 die_error(400, "Invalid hash parent parameter");
460 our $hash_base = $cgi->param('hb');
461 if (defined $hash_base) {
462 if (!validate_refname($hash_base)) {
463 die_error(400, "Invalid hash base parameter");
467 my %allowed_options = (
468 "--no-merges" => [ qw(rss atom log shortlog history) ],
471 our @extra_options = $cgi->param('opt');
472 if (defined @extra_options) {
473 foreach my $opt (@extra_options) {
474 if (not exists $allowed_options{$opt}) {
475 die_error(400, "Invalid option parameter");
477 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
478 die_error(400, "Invalid option parameter for this action");
483 our $hash_parent_base = $cgi->param('hpb');
484 if (defined $hash_parent_base) {
485 if (!validate_refname($hash_parent_base)) {
486 die_error(400, "Invalid hash parent base parameter");
490 # other parameters
491 our $page = $cgi->param('pg');
492 if (defined $page) {
493 if ($page =~ m/[^0-9]/) {
494 die_error(400, "Invalid page parameter");
498 our $searchtype = $cgi->param('st');
499 if (defined $searchtype) {
500 if ($searchtype =~ m/[^a-z]/) {
501 die_error(400, "Invalid searchtype parameter");
505 our $search_use_regexp = $cgi->param('sr');
507 our $searchtext = $cgi->param('s');
508 our $search_regexp;
509 if (defined $searchtext) {
510 if (length($searchtext) < 2) {
511 die_error(403, "At least two characters are required for search parameter");
513 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
516 # now read PATH_INFO and use it as alternative to parameters
517 sub evaluate_path_info {
518 return if defined $project;
519 my $path_info = $ENV{"PATH_INFO"};
520 return if !$path_info;
521 $path_info =~ s,^/+,,;
522 return if !$path_info;
523 # find which part of PATH_INFO is project
524 $project = $path_info;
525 $project =~ s,/+$,,;
526 while ($project && !check_head_link("$projectroot/$project")) {
527 $project =~ s,/*[^/]*$,,;
529 # validate project
530 $project = validate_pathname($project);
531 if (!$project ||
532 ($export_ok && !-e "$projectroot/$project/$export_ok") ||
533 ($strict_export && !project_in_list($project))) {
534 undef $project;
535 return;
537 # do not change any parameters if an action is given using the query string
538 return if $action;
539 $path_info =~ s,^\Q$project\E/*,,;
540 my ($refname, $pathname) = split(/:/, $path_info, 2);
541 if (defined $pathname) {
542 # we got "project.git/branch:filename" or "project.git/branch:dir/"
543 # we could use git_get_type(branch:pathname), but it needs $git_dir
544 $pathname =~ s,^/+,,;
545 if (!$pathname || substr($pathname, -1) eq "/") {
546 $action ||= "tree";
547 $pathname =~ s,/$,,;
548 } else {
549 $action ||= "blob_plain";
551 $hash_base ||= validate_refname($refname);
552 $file_name ||= validate_pathname($pathname);
553 } elsif (defined $refname) {
554 # we got "project.git/branch"
555 $action ||= "shortlog";
556 $hash ||= validate_refname($refname);
559 evaluate_path_info();
561 # path to the current git repository
562 our $git_dir;
563 $git_dir = "$projectroot/$project" if $project;
565 # dispatch
566 my %actions = (
567 "blame" => \&git_blame,
568 "blobdiff" => \&git_blobdiff,
569 "blobdiff_plain" => \&git_blobdiff_plain,
570 "blob" => \&git_blob,
571 "blob_plain" => \&git_blob_plain,
572 "commitdiff" => \&git_commitdiff,
573 "commitdiff_plain" => \&git_commitdiff_plain,
574 "commit" => \&git_commit,
575 "forks" => \&git_forks,
576 "heads" => \&git_heads,
577 "history" => \&git_history,
578 "log" => \&git_log,
579 "rss" => \&git_rss,
580 "atom" => \&git_atom,
581 "search" => \&git_search,
582 "search_help" => \&git_search_help,
583 "shortlog" => \&git_shortlog,
584 "summary" => \&git_summary,
585 "tag" => \&git_tag,
586 "tags" => \&git_tags,
587 "tree" => \&git_tree,
588 "snapshot" => \&git_snapshot,
589 "object" => \&git_object,
590 # those below don't need $project
591 "opml" => \&git_opml,
592 "project_list" => \&git_project_list,
593 "project_index" => \&git_project_index,
596 if (!defined $action) {
597 if (defined $hash) {
598 $action = git_get_type($hash);
599 } elsif (defined $hash_base && defined $file_name) {
600 $action = git_get_type("$hash_base:$file_name");
601 } elsif (defined $project) {
602 $action = 'summary';
603 } else {
604 $action = 'project_list';
607 if (!defined($actions{$action})) {
608 die_error(400, "Unknown action");
610 if ($action !~ m/^(opml|project_list|project_index)$/ &&
611 !$project) {
612 die_error(400, "Project needed");
614 $actions{$action}->();
615 exit;
617 ## ======================================================================
618 ## action links
620 sub href (%) {
621 my %params = @_;
622 # default is to use -absolute url() i.e. $my_uri
623 my $href = $params{-full} ? $my_url : $my_uri;
625 # XXX: Warning: If you touch this, check the search form for updating,
626 # too.
628 my @mapping = (
629 project => "p",
630 action => "a",
631 file_name => "f",
632 file_parent => "fp",
633 hash => "h",
634 hash_parent => "hp",
635 hash_base => "hb",
636 hash_parent_base => "hpb",
637 page => "pg",
638 order => "o",
639 searchtext => "s",
640 searchtype => "st",
641 snapshot_format => "sf",
642 extra_options => "opt",
643 search_use_regexp => "sr",
645 my %mapping = @mapping;
647 $params{'project'} = $project unless exists $params{'project'};
649 if ($params{-replay}) {
650 while (my ($name, $symbol) = each %mapping) {
651 if (!exists $params{$name}) {
652 # to allow for multivalued params we use arrayref form
653 $params{$name} = [ $cgi->param($symbol) ];
658 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
659 if ($use_pathinfo) {
660 # use PATH_INFO for project name
661 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
662 delete $params{'project'};
664 # Summary just uses the project path URL
665 if (defined $params{'action'} && $params{'action'} eq 'summary') {
666 delete $params{'action'};
670 # now encode the parameters explicitly
671 my @result = ();
672 for (my $i = 0; $i < @mapping; $i += 2) {
673 my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
674 if (defined $params{$name}) {
675 if (ref($params{$name}) eq "ARRAY") {
676 foreach my $par (@{$params{$name}}) {
677 push @result, $symbol . "=" . esc_param($par);
679 } else {
680 push @result, $symbol . "=" . esc_param($params{$name});
684 $href .= "?" . join(';', @result) if scalar @result;
686 return $href;
690 ## ======================================================================
691 ## validation, quoting/unquoting and escaping
693 sub validate_pathname {
694 my $input = shift || return undef;
696 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
697 # at the beginning, at the end, and between slashes.
698 # also this catches doubled slashes
699 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
700 return undef;
702 # no null characters
703 if ($input =~ m!\0!) {
704 return undef;
706 return $input;
709 sub validate_refname {
710 my $input = shift || return undef;
712 # textual hashes are O.K.
713 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
714 return $input;
716 # it must be correct pathname
717 $input = validate_pathname($input)
718 or return undef;
719 # restrictions on ref name according to git-check-ref-format
720 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
721 return undef;
723 return $input;
726 # decode sequences of octets in utf8 into Perl's internal form,
727 # which is utf-8 with utf8 flag set if needed. gitweb writes out
728 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
729 sub to_utf8 {
730 my $str = shift;
731 if (utf8::valid($str)) {
732 utf8::decode($str);
733 return $str;
734 } else {
735 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
739 # quote unsafe chars, but keep the slash, even when it's not
740 # correct, but quoted slashes look too horrible in bookmarks
741 sub esc_param {
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 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
750 sub esc_url {
751 my $str = shift;
752 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
753 $str =~ s/\+/%2B/g;
754 $str =~ s/ /\+/g;
755 return $str;
758 # replace invalid utf8 character with SUBSTITUTION sequence
759 sub esc_html ($;%) {
760 my $str = shift;
761 my %opts = @_;
763 $str = to_utf8($str);
764 $str = $cgi->escapeHTML($str);
765 if ($opts{'-nbsp'}) {
766 $str =~ s/ /&nbsp;/g;
768 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
769 return $str;
772 # quote control characters and escape filename to HTML
773 sub esc_path {
774 my $str = shift;
775 my %opts = @_;
777 $str = to_utf8($str);
778 $str = $cgi->escapeHTML($str);
779 if ($opts{'-nbsp'}) {
780 $str =~ s/ /&nbsp;/g;
782 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
783 return $str;
786 # Make control characters "printable", using character escape codes (CEC)
787 sub quot_cec {
788 my $cntrl = shift;
789 my %opts = @_;
790 my %es = ( # character escape codes, aka escape sequences
791 "\t" => '\t', # tab (HT)
792 "\n" => '\n', # line feed (LF)
793 "\r" => '\r', # carrige return (CR)
794 "\f" => '\f', # form feed (FF)
795 "\b" => '\b', # backspace (BS)
796 "\a" => '\a', # alarm (bell) (BEL)
797 "\e" => '\e', # escape (ESC)
798 "\013" => '\v', # vertical tab (VT)
799 "\000" => '\0', # nul character (NUL)
801 my $chr = ( (exists $es{$cntrl})
802 ? $es{$cntrl}
803 : sprintf('\%2x', ord($cntrl)) );
804 if ($opts{-nohtml}) {
805 return $chr;
806 } else {
807 return "<span class=\"cntrl\">$chr</span>";
811 # Alternatively use unicode control pictures codepoints,
812 # Unicode "printable representation" (PR)
813 sub quot_upr {
814 my $cntrl = shift;
815 my %opts = @_;
817 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
818 if ($opts{-nohtml}) {
819 return $chr;
820 } else {
821 return "<span class=\"cntrl\">$chr</span>";
825 # git may return quoted and escaped filenames
826 sub unquote {
827 my $str = shift;
829 sub unq {
830 my $seq = shift;
831 my %es = ( # character escape codes, aka escape sequences
832 't' => "\t", # tab (HT, TAB)
833 'n' => "\n", # newline (NL)
834 'r' => "\r", # return (CR)
835 'f' => "\f", # form feed (FF)
836 'b' => "\b", # backspace (BS)
837 'a' => "\a", # alarm (bell) (BEL)
838 'e' => "\e", # escape (ESC)
839 'v' => "\013", # vertical tab (VT)
842 if ($seq =~ m/^[0-7]{1,3}$/) {
843 # octal char sequence
844 return chr(oct($seq));
845 } elsif (exists $es{$seq}) {
846 # C escape sequence, aka character escape code
847 return $es{$seq};
849 # quoted ordinary character
850 return $seq;
853 if ($str =~ m/^"(.*)"$/) {
854 # needs unquoting
855 $str = $1;
856 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
858 return $str;
861 # escape tabs (convert tabs to spaces)
862 sub untabify {
863 my $line = shift;
865 while ((my $pos = index($line, "\t")) != -1) {
866 if (my $count = (8 - ($pos % 8))) {
867 my $spaces = ' ' x $count;
868 $line =~ s/\t/$spaces/;
872 return $line;
875 sub project_in_list {
876 my $project = shift;
877 my @list = git_get_projects_list();
878 return @list && scalar(grep { $_->{'path'} eq $project } @list);
881 ## ----------------------------------------------------------------------
882 ## HTML aware string manipulation
884 # Try to chop given string on a word boundary between position
885 # $len and $len+$add_len. If there is no word boundary there,
886 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
887 # (marking chopped part) would be longer than given string.
888 sub chop_str {
889 my $str = shift;
890 my $len = shift;
891 my $add_len = shift || 10;
892 my $where = shift || 'right'; # 'left' | 'center' | 'right'
894 # Make sure perl knows it is utf8 encoded so we don't
895 # cut in the middle of a utf8 multibyte char.
896 $str = to_utf8($str);
898 # allow only $len chars, but don't cut a word if it would fit in $add_len
899 # if it doesn't fit, cut it if it's still longer than the dots we would add
900 # remove chopped character entities entirely
902 # when chopping in the middle, distribute $len into left and right part
903 # return early if chopping wouldn't make string shorter
904 if ($where eq 'center') {
905 return $str if ($len + 5 >= length($str)); # filler is length 5
906 $len = int($len/2);
907 } else {
908 return $str if ($len + 4 >= length($str)); # filler is length 4
911 # regexps: ending and beginning with word part up to $add_len
912 my $endre = qr/.{$len}\w{0,$add_len}/;
913 my $begre = qr/\w{0,$add_len}.{$len}/;
915 if ($where eq 'left') {
916 $str =~ m/^(.*?)($begre)$/;
917 my ($lead, $body) = ($1, $2);
918 if (length($lead) > 4) {
919 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
920 $lead = " ...";
922 return "$lead$body";
924 } elsif ($where eq 'center') {
925 $str =~ m/^($endre)(.*)$/;
926 my ($left, $str) = ($1, $2);
927 $str =~ m/^(.*?)($begre)$/;
928 my ($mid, $right) = ($1, $2);
929 if (length($mid) > 5) {
930 $left =~ s/&[^;]*$//;
931 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
932 $mid = " ... ";
934 return "$left$mid$right";
936 } else {
937 $str =~ m/^($endre)(.*)$/;
938 my $body = $1;
939 my $tail = $2;
940 if (length($tail) > 4) {
941 $body =~ s/&[^;]*$//;
942 $tail = "... ";
944 return "$body$tail";
948 # takes the same arguments as chop_str, but also wraps a <span> around the
949 # result with a title attribute if it does get chopped. Additionally, the
950 # string is HTML-escaped.
951 sub chop_and_escape_str {
952 my ($str) = @_;
954 my $chopped = chop_str(@_);
955 if ($chopped eq $str) {
956 return esc_html($chopped);
957 } else {
958 $str =~ s/([[:cntrl:]])/?/g;
959 return $cgi->span({-title=>$str}, esc_html($chopped));
963 ## ----------------------------------------------------------------------
964 ## functions returning short strings
966 # CSS class for given age value (in seconds)
967 sub age_class {
968 my $age = shift;
970 if (!defined $age) {
971 return "noage";
972 } elsif ($age < 60*60*2) {
973 return "age0";
974 } elsif ($age < 60*60*24*2) {
975 return "age1";
976 } else {
977 return "age2";
981 # convert age in seconds to "nn units ago" string
982 sub age_string {
983 my $age = shift;
984 my $age_str;
986 if ($age > 60*60*24*365*2) {
987 $age_str = (int $age/60/60/24/365);
988 $age_str .= " years ago";
989 } elsif ($age > 60*60*24*(365/12)*2) {
990 $age_str = int $age/60/60/24/(365/12);
991 $age_str .= " months ago";
992 } elsif ($age > 60*60*24*7*2) {
993 $age_str = int $age/60/60/24/7;
994 $age_str .= " weeks ago";
995 } elsif ($age > 60*60*24*2) {
996 $age_str = int $age/60/60/24;
997 $age_str .= " days ago";
998 } elsif ($age > 60*60*2) {
999 $age_str = int $age/60/60;
1000 $age_str .= " hours ago";
1001 } elsif ($age > 60*2) {
1002 $age_str = int $age/60;
1003 $age_str .= " min ago";
1004 } elsif ($age > 2) {
1005 $age_str = int $age;
1006 $age_str .= " sec ago";
1007 } else {
1008 $age_str .= " right now";
1010 return $age_str;
1013 use constant {
1014 S_IFINVALID => 0030000,
1015 S_IFGITLINK => 0160000,
1018 # submodule/subproject, a commit object reference
1019 sub S_ISGITLINK($) {
1020 my $mode = shift;
1022 return (($mode & S_IFMT) == S_IFGITLINK)
1025 # convert file mode in octal to symbolic file mode string
1026 sub mode_str {
1027 my $mode = oct shift;
1029 if (S_ISGITLINK($mode)) {
1030 return 'm---------';
1031 } elsif (S_ISDIR($mode & S_IFMT)) {
1032 return 'drwxr-xr-x';
1033 } elsif (S_ISLNK($mode)) {
1034 return 'lrwxrwxrwx';
1035 } elsif (S_ISREG($mode)) {
1036 # git cares only about the executable bit
1037 if ($mode & S_IXUSR) {
1038 return '-rwxr-xr-x';
1039 } else {
1040 return '-rw-r--r--';
1042 } else {
1043 return '----------';
1047 # convert file mode in octal to file type string
1048 sub file_type {
1049 my $mode = shift;
1051 if ($mode !~ m/^[0-7]+$/) {
1052 return $mode;
1053 } else {
1054 $mode = oct $mode;
1057 if (S_ISGITLINK($mode)) {
1058 return "submodule";
1059 } elsif (S_ISDIR($mode & S_IFMT)) {
1060 return "directory";
1061 } elsif (S_ISLNK($mode)) {
1062 return "symlink";
1063 } elsif (S_ISREG($mode)) {
1064 return "file";
1065 } else {
1066 return "unknown";
1070 # convert file mode in octal to file type description string
1071 sub file_type_long {
1072 my $mode = shift;
1074 if ($mode !~ m/^[0-7]+$/) {
1075 return $mode;
1076 } else {
1077 $mode = oct $mode;
1080 if (S_ISGITLINK($mode)) {
1081 return "submodule";
1082 } elsif (S_ISDIR($mode & S_IFMT)) {
1083 return "directory";
1084 } elsif (S_ISLNK($mode)) {
1085 return "symlink";
1086 } elsif (S_ISREG($mode)) {
1087 if ($mode & S_IXUSR) {
1088 return "executable";
1089 } else {
1090 return "file";
1092 } else {
1093 return "unknown";
1098 ## ----------------------------------------------------------------------
1099 ## functions returning short HTML fragments, or transforming HTML fragments
1100 ## which don't belong to other sections
1102 # format line of commit message.
1103 sub format_log_line_html {
1104 my $line = shift;
1106 $line = esc_html($line, -nbsp=>1);
1107 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1108 my $hash_text = $1;
1109 my $link =
1110 $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1111 -class => "text"}, $hash_text);
1112 $line =~ s/$hash_text/$link/;
1114 return $line;
1117 # format marker of refs pointing to given object
1119 # the destination action is chosen based on object type and current context:
1120 # - for annotated tags, we choose the tag view unless it's the current view
1121 # already, in which case we go to shortlog view
1122 # - for other refs, we keep the current view if we're in history, shortlog or
1123 # log view, and select shortlog otherwise
1124 sub format_ref_marker {
1125 my ($refs, $id) = @_;
1126 my $markers = '';
1128 if (defined $refs->{$id}) {
1129 foreach my $ref (@{$refs->{$id}}) {
1130 # this code exploits the fact that non-lightweight tags are the
1131 # only indirect objects, and that they are the only objects for which
1132 # we want to use tag instead of shortlog as action
1133 my ($type, $name) = qw();
1134 my $indirect = ($ref =~ s/\^\{\}$//);
1135 # e.g. tags/v2.6.11 or heads/next
1136 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1137 $type = $1;
1138 $name = $2;
1139 } else {
1140 $type = "ref";
1141 $name = $ref;
1144 my $class = $type;
1145 $class .= " indirect" if $indirect;
1147 my $dest_action = "shortlog";
1149 if ($indirect) {
1150 $dest_action = "tag" unless $action eq "tag";
1151 } elsif ($action =~ /^(history|(short)?log)$/) {
1152 $dest_action = $action;
1155 my $dest = "";
1156 $dest .= "refs/" unless $ref =~ m!^refs/!;
1157 $dest .= $ref;
1159 my $link = $cgi->a({
1160 -href => href(
1161 action=>$dest_action,
1162 hash=>$dest
1163 )}, $name);
1165 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1166 $link . "</span>";
1170 if ($markers) {
1171 return ' <span class="refs">'. $markers . '</span>';
1172 } else {
1173 return "";
1177 # format, perhaps shortened and with markers, title line
1178 sub format_subject_html {
1179 my ($long, $short, $href, $extra) = @_;
1180 $extra = '' unless defined($extra);
1182 if (length($short) < length($long)) {
1183 return $cgi->a({-href => $href, -class => "list subject",
1184 -title => to_utf8($long)},
1185 esc_html($short) . $extra);
1186 } else {
1187 return $cgi->a({-href => $href, -class => "list subject"},
1188 esc_html($long) . $extra);
1192 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1193 sub format_git_diff_header_line {
1194 my $line = shift;
1195 my $diffinfo = shift;
1196 my ($from, $to) = @_;
1198 if ($diffinfo->{'nparents'}) {
1199 # combined diff
1200 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1201 if ($to->{'href'}) {
1202 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1203 esc_path($to->{'file'}));
1204 } else { # file was deleted (no href)
1205 $line .= esc_path($to->{'file'});
1207 } else {
1208 # "ordinary" diff
1209 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1210 if ($from->{'href'}) {
1211 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1212 'a/' . esc_path($from->{'file'}));
1213 } else { # file was added (no href)
1214 $line .= 'a/' . esc_path($from->{'file'});
1216 $line .= ' ';
1217 if ($to->{'href'}) {
1218 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1219 'b/' . esc_path($to->{'file'}));
1220 } else { # file was deleted
1221 $line .= 'b/' . esc_path($to->{'file'});
1225 return "<div class=\"diff header\">$line</div>\n";
1228 # format extended diff header line, before patch itself
1229 sub format_extended_diff_header_line {
1230 my $line = shift;
1231 my $diffinfo = shift;
1232 my ($from, $to) = @_;
1234 # match <path>
1235 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1236 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1237 esc_path($from->{'file'}));
1239 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1240 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1241 esc_path($to->{'file'}));
1243 # match single <mode>
1244 if ($line =~ m/\s(\d{6})$/) {
1245 $line .= '<span class="info"> (' .
1246 file_type_long($1) .
1247 ')</span>';
1249 # match <hash>
1250 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1251 # can match only for combined diff
1252 $line = 'index ';
1253 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1254 if ($from->{'href'}[$i]) {
1255 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1256 -class=>"hash"},
1257 substr($diffinfo->{'from_id'}[$i],0,7));
1258 } else {
1259 $line .= '0' x 7;
1261 # separator
1262 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1264 $line .= '..';
1265 if ($to->{'href'}) {
1266 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1267 substr($diffinfo->{'to_id'},0,7));
1268 } else {
1269 $line .= '0' x 7;
1272 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1273 # can match only for ordinary diff
1274 my ($from_link, $to_link);
1275 if ($from->{'href'}) {
1276 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1277 substr($diffinfo->{'from_id'},0,7));
1278 } else {
1279 $from_link = '0' x 7;
1281 if ($to->{'href'}) {
1282 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1283 substr($diffinfo->{'to_id'},0,7));
1284 } else {
1285 $to_link = '0' x 7;
1287 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1288 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1291 return $line . "<br/>\n";
1294 # format from-file/to-file diff header
1295 sub format_diff_from_to_header {
1296 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1297 my $line;
1298 my $result = '';
1300 $line = $from_line;
1301 #assert($line =~ m/^---/) if DEBUG;
1302 # no extra formatting for "^--- /dev/null"
1303 if (! $diffinfo->{'nparents'}) {
1304 # ordinary (single parent) diff
1305 if ($line =~ m!^--- "?a/!) {
1306 if ($from->{'href'}) {
1307 $line = '--- a/' .
1308 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1309 esc_path($from->{'file'}));
1310 } else {
1311 $line = '--- a/' .
1312 esc_path($from->{'file'});
1315 $result .= qq!<div class="diff from_file">$line</div>\n!;
1317 } else {
1318 # combined diff (merge commit)
1319 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1320 if ($from->{'href'}[$i]) {
1321 $line = '--- ' .
1322 $cgi->a({-href=>href(action=>"blobdiff",
1323 hash_parent=>$diffinfo->{'from_id'}[$i],
1324 hash_parent_base=>$parents[$i],
1325 file_parent=>$from->{'file'}[$i],
1326 hash=>$diffinfo->{'to_id'},
1327 hash_base=>$hash,
1328 file_name=>$to->{'file'}),
1329 -class=>"path",
1330 -title=>"diff" . ($i+1)},
1331 $i+1) .
1332 '/' .
1333 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1334 esc_path($from->{'file'}[$i]));
1335 } else {
1336 $line = '--- /dev/null';
1338 $result .= qq!<div class="diff from_file">$line</div>\n!;
1342 $line = $to_line;
1343 #assert($line =~ m/^\+\+\+/) if DEBUG;
1344 # no extra formatting for "^+++ /dev/null"
1345 if ($line =~ m!^\+\+\+ "?b/!) {
1346 if ($to->{'href'}) {
1347 $line = '+++ b/' .
1348 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1349 esc_path($to->{'file'}));
1350 } else {
1351 $line = '+++ b/' .
1352 esc_path($to->{'file'});
1355 $result .= qq!<div class="diff to_file">$line</div>\n!;
1357 return $result;
1360 # create note for patch simplified by combined diff
1361 sub format_diff_cc_simplified {
1362 my ($diffinfo, @parents) = @_;
1363 my $result = '';
1365 $result .= "<div class=\"diff header\">" .
1366 "diff --cc ";
1367 if (!is_deleted($diffinfo)) {
1368 $result .= $cgi->a({-href => href(action=>"blob",
1369 hash_base=>$hash,
1370 hash=>$diffinfo->{'to_id'},
1371 file_name=>$diffinfo->{'to_file'}),
1372 -class => "path"},
1373 esc_path($diffinfo->{'to_file'}));
1374 } else {
1375 $result .= esc_path($diffinfo->{'to_file'});
1377 $result .= "</div>\n" . # class="diff header"
1378 "<div class=\"diff nodifferences\">" .
1379 "Simple merge" .
1380 "</div>\n"; # class="diff nodifferences"
1382 return $result;
1385 # format patch (diff) line (not to be used for diff headers)
1386 sub format_diff_line {
1387 my $line = shift;
1388 my ($from, $to) = @_;
1389 my $diff_class = "";
1391 chomp $line;
1393 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1394 # combined diff
1395 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1396 if ($line =~ m/^\@{3}/) {
1397 $diff_class = " chunk_header";
1398 } elsif ($line =~ m/^\\/) {
1399 $diff_class = " incomplete";
1400 } elsif ($prefix =~ tr/+/+/) {
1401 $diff_class = " add";
1402 } elsif ($prefix =~ tr/-/-/) {
1403 $diff_class = " rem";
1405 } else {
1406 # assume ordinary diff
1407 my $char = substr($line, 0, 1);
1408 if ($char eq '+') {
1409 $diff_class = " add";
1410 } elsif ($char eq '-') {
1411 $diff_class = " rem";
1412 } elsif ($char eq '@') {
1413 $diff_class = " chunk_header";
1414 } elsif ($char eq "\\") {
1415 $diff_class = " incomplete";
1418 $line = untabify($line);
1419 if ($from && $to && $line =~ m/^\@{2} /) {
1420 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1421 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1423 $from_lines = 0 unless defined $from_lines;
1424 $to_lines = 0 unless defined $to_lines;
1426 if ($from->{'href'}) {
1427 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1428 -class=>"list"}, $from_text);
1430 if ($to->{'href'}) {
1431 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1432 -class=>"list"}, $to_text);
1434 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1435 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1436 return "<div class=\"diff$diff_class\">$line</div>\n";
1437 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1438 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1439 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1441 @from_text = split(' ', $ranges);
1442 for (my $i = 0; $i < @from_text; ++$i) {
1443 ($from_start[$i], $from_nlines[$i]) =
1444 (split(',', substr($from_text[$i], 1)), 0);
1447 $to_text = pop @from_text;
1448 $to_start = pop @from_start;
1449 $to_nlines = pop @from_nlines;
1451 $line = "<span class=\"chunk_info\">$prefix ";
1452 for (my $i = 0; $i < @from_text; ++$i) {
1453 if ($from->{'href'}[$i]) {
1454 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1455 -class=>"list"}, $from_text[$i]);
1456 } else {
1457 $line .= $from_text[$i];
1459 $line .= " ";
1461 if ($to->{'href'}) {
1462 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1463 -class=>"list"}, $to_text);
1464 } else {
1465 $line .= $to_text;
1467 $line .= " $prefix</span>" .
1468 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1469 return "<div class=\"diff$diff_class\">$line</div>\n";
1471 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1474 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1475 # linked. Pass the hash of the tree/commit to snapshot.
1476 sub format_snapshot_links {
1477 my ($hash) = @_;
1478 my @snapshot_fmts = gitweb_check_feature('snapshot');
1479 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1480 my $num_fmts = @snapshot_fmts;
1481 if ($num_fmts > 1) {
1482 # A parenthesized list of links bearing format names.
1483 # e.g. "snapshot (_tar.gz_ _zip_)"
1484 return "snapshot (" . join(' ', map
1485 $cgi->a({
1486 -href => href(
1487 action=>"snapshot",
1488 hash=>$hash,
1489 snapshot_format=>$_
1491 }, $known_snapshot_formats{$_}{'display'})
1492 , @snapshot_fmts) . ")";
1493 } elsif ($num_fmts == 1) {
1494 # A single "snapshot" link whose tooltip bears the format name.
1495 # i.e. "_snapshot_"
1496 my ($fmt) = @snapshot_fmts;
1497 return
1498 $cgi->a({
1499 -href => href(
1500 action=>"snapshot",
1501 hash=>$hash,
1502 snapshot_format=>$fmt
1504 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1505 }, "snapshot");
1506 } else { # $num_fmts == 0
1507 return undef;
1511 ## ......................................................................
1512 ## functions returning values to be passed, perhaps after some
1513 ## transformation, to other functions; e.g. returning arguments to href()
1515 # returns hash to be passed to href to generate gitweb URL
1516 # in -title key it returns description of link
1517 sub get_feed_info {
1518 my $format = shift || 'Atom';
1519 my %res = (action => lc($format));
1521 # feed links are possible only for project views
1522 return unless (defined $project);
1523 # some views should link to OPML, or to generic project feed,
1524 # or don't have specific feed yet (so they should use generic)
1525 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1527 my $branch;
1528 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1529 # from tag links; this also makes possible to detect branch links
1530 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1531 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1532 $branch = $1;
1534 # find log type for feed description (title)
1535 my $type = 'log';
1536 if (defined $file_name) {
1537 $type = "history of $file_name";
1538 $type .= "/" if ($action eq 'tree');
1539 $type .= " on '$branch'" if (defined $branch);
1540 } else {
1541 $type = "log of $branch" if (defined $branch);
1544 $res{-title} = $type;
1545 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1546 $res{'file_name'} = $file_name;
1548 return %res;
1551 ## ----------------------------------------------------------------------
1552 ## git utility subroutines, invoking git commands
1554 # returns path to the core git executable and the --git-dir parameter as list
1555 sub git_cmd {
1556 return $GIT, '--git-dir='.$git_dir;
1559 # quote the given arguments for passing them to the shell
1560 # quote_command("command", "arg 1", "arg with ' and ! characters")
1561 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1562 # Try to avoid using this function wherever possible.
1563 sub quote_command {
1564 return join(' ',
1565 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1568 # get HEAD ref of given project as hash
1569 sub git_get_head_hash {
1570 my $project = shift;
1571 my $o_git_dir = $git_dir;
1572 my $retval = undef;
1573 $git_dir = "$projectroot/$project";
1574 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1575 my $head = <$fd>;
1576 close $fd;
1577 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1578 $retval = $1;
1581 if (defined $o_git_dir) {
1582 $git_dir = $o_git_dir;
1584 return $retval;
1587 # get type of given object
1588 sub git_get_type {
1589 my $hash = shift;
1591 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1592 my $type = <$fd>;
1593 close $fd or return;
1594 chomp $type;
1595 return $type;
1598 # repository configuration
1599 our $config_file = '';
1600 our %config;
1602 # store multiple values for single key as anonymous array reference
1603 # single values stored directly in the hash, not as [ <value> ]
1604 sub hash_set_multi {
1605 my ($hash, $key, $value) = @_;
1607 if (!exists $hash->{$key}) {
1608 $hash->{$key} = $value;
1609 } elsif (!ref $hash->{$key}) {
1610 $hash->{$key} = [ $hash->{$key}, $value ];
1611 } else {
1612 push @{$hash->{$key}}, $value;
1616 # return hash of git project configuration
1617 # optionally limited to some section, e.g. 'gitweb'
1618 sub git_parse_project_config {
1619 my $section_regexp = shift;
1620 my %config;
1622 local $/ = "\0";
1624 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1625 or return;
1627 while (my $keyval = <$fh>) {
1628 chomp $keyval;
1629 my ($key, $value) = split(/\n/, $keyval, 2);
1631 hash_set_multi(\%config, $key, $value)
1632 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1634 close $fh;
1636 return %config;
1639 # convert config value to boolean, 'true' or 'false'
1640 # no value, number > 0, 'true' and 'yes' values are true
1641 # rest of values are treated as false (never as error)
1642 sub config_to_bool {
1643 my $val = shift;
1645 # strip leading and trailing whitespace
1646 $val =~ s/^\s+//;
1647 $val =~ s/\s+$//;
1649 return (!defined $val || # section.key
1650 ($val =~ /^\d+$/ && $val) || # section.key = 1
1651 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1654 # convert config value to simple decimal number
1655 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1656 # to be multiplied by 1024, 1048576, or 1073741824
1657 sub config_to_int {
1658 my $val = shift;
1660 # strip leading and trailing whitespace
1661 $val =~ s/^\s+//;
1662 $val =~ s/\s+$//;
1664 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1665 $unit = lc($unit);
1666 # unknown unit is treated as 1
1667 return $num * ($unit eq 'g' ? 1073741824 :
1668 $unit eq 'm' ? 1048576 :
1669 $unit eq 'k' ? 1024 : 1);
1671 return $val;
1674 # convert config value to array reference, if needed
1675 sub config_to_multi {
1676 my $val = shift;
1678 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1681 sub git_get_project_config {
1682 my ($key, $type) = @_;
1684 # key sanity check
1685 return unless ($key);
1686 $key =~ s/^gitweb\.//;
1687 return if ($key =~ m/\W/);
1689 # type sanity check
1690 if (defined $type) {
1691 $type =~ s/^--//;
1692 $type = undef
1693 unless ($type eq 'bool' || $type eq 'int');
1696 # get config
1697 if (!defined $config_file ||
1698 $config_file ne "$git_dir/config") {
1699 %config = git_parse_project_config('gitweb');
1700 $config_file = "$git_dir/config";
1703 # ensure given type
1704 if (!defined $type) {
1705 return $config{"gitweb.$key"};
1706 } elsif ($type eq 'bool') {
1707 # backward compatibility: 'git config --bool' returns true/false
1708 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1709 } elsif ($type eq 'int') {
1710 return config_to_int($config{"gitweb.$key"});
1712 return $config{"gitweb.$key"};
1715 # get hash of given path at given ref
1716 sub git_get_hash_by_path {
1717 my $base = shift;
1718 my $path = shift || return undef;
1719 my $type = shift;
1721 $path =~ s,/+$,,;
1723 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1724 or die_error(500, "Open git-ls-tree failed");
1725 my $line = <$fd>;
1726 close $fd or return undef;
1728 if (!defined $line) {
1729 # there is no tree or hash given by $path at $base
1730 return undef;
1733 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1734 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1735 if (defined $type && $type ne $2) {
1736 # type doesn't match
1737 return undef;
1739 return $3;
1742 # get path of entry with given hash at given tree-ish (ref)
1743 # used to get 'from' filename for combined diff (merge commit) for renames
1744 sub git_get_path_by_hash {
1745 my $base = shift || return;
1746 my $hash = shift || return;
1748 local $/ = "\0";
1750 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1751 or return undef;
1752 while (my $line = <$fd>) {
1753 chomp $line;
1755 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1756 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1757 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1758 close $fd;
1759 return $1;
1762 close $fd;
1763 return undef;
1766 ## ......................................................................
1767 ## git utility functions, directly accessing git repository
1769 sub git_get_project_description {
1770 my $path = shift;
1772 $git_dir = "$projectroot/$path";
1773 open my $fd, "$git_dir/description"
1774 or return git_get_project_config('description');
1775 my $descr = <$fd>;
1776 close $fd;
1777 if (defined $descr) {
1778 chomp $descr;
1780 return $descr;
1783 sub git_get_project_ctags {
1784 my $path = shift;
1785 my $ctags = {};
1787 $git_dir = "$projectroot/$path";
1788 foreach (<$git_dir/ctags/*>) {
1789 open CT, $_ or next;
1790 my $val = <CT>;
1791 chomp $val;
1792 close CT;
1793 my $ctag = $_; $ctag =~ s#.*/##;
1794 $ctags->{$ctag} = $val;
1796 $ctags;
1799 sub git_populate_project_tagcloud {
1800 my $ctags = shift;
1802 # First, merge different-cased tags; tags vote on casing
1803 my %ctags_lc;
1804 foreach (keys %$ctags) {
1805 $ctags_lc{lc $_}->{count} += $ctags->{$_};
1806 if (not $ctags_lc{lc $_}->{topcount}
1807 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
1808 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
1809 $ctags_lc{lc $_}->{topname} = $_;
1813 my $cloud;
1814 if (eval { require HTML::TagCloud; 1; }) {
1815 $cloud = HTML::TagCloud->new;
1816 foreach (sort keys %ctags_lc) {
1817 # Pad the title with spaces so that the cloud looks
1818 # less crammed.
1819 my $title = $ctags_lc{$_}->{topname};
1820 $title =~ s/ /&nbsp;/g;
1821 $title =~ s/^/&nbsp;/g;
1822 $title =~ s/$/&nbsp;/g;
1823 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
1825 } else {
1826 $cloud = \%ctags_lc;
1828 $cloud;
1831 sub git_show_project_tagcloud {
1832 my ($cloud, $count) = @_;
1833 print STDERR ref($cloud)."..\n";
1834 if (ref $cloud eq 'HTML::TagCloud') {
1835 return $cloud->html_and_css($count);
1836 } else {
1837 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
1838 return '<p align="center">' . join (', ', map {
1839 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1840 } splice(@tags, 0, $count)) . '</p>';
1844 sub git_get_project_url_list {
1845 my $path = shift;
1847 $git_dir = "$projectroot/$path";
1848 open my $fd, "$git_dir/cloneurl"
1849 or return wantarray ?
1850 @{ config_to_multi(git_get_project_config('url')) } :
1851 config_to_multi(git_get_project_config('url'));
1852 my @git_project_url_list = map { chomp; $_ } <$fd>;
1853 close $fd;
1855 return wantarray ? @git_project_url_list : \@git_project_url_list;
1858 sub git_get_projects_list {
1859 my ($filter) = @_;
1860 my @list;
1862 $filter ||= '';
1863 $filter =~ s/\.git$//;
1865 my ($check_forks) = gitweb_check_feature('forks');
1867 if (-d $projects_list) {
1868 # search in directory
1869 my $dir = $projects_list . ($filter ? "/$filter" : '');
1870 # remove the trailing "/"
1871 $dir =~ s!/+$!!;
1872 my $pfxlen = length("$dir");
1873 my $pfxdepth = ($dir =~ tr!/!!);
1875 File::Find::find({
1876 follow_fast => 1, # follow symbolic links
1877 follow_skip => 2, # ignore duplicates
1878 dangling_symlinks => 0, # ignore dangling symlinks, silently
1879 wanted => sub {
1880 # skip project-list toplevel, if we get it.
1881 return if (m!^[/.]$!);
1882 # only directories can be git repositories
1883 return unless (-d $_);
1884 # don't traverse too deep (Find is super slow on os x)
1885 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1886 $File::Find::prune = 1;
1887 return;
1890 my $subdir = substr($File::Find::name, $pfxlen + 1);
1891 # we check related file in $projectroot
1892 if ($check_forks and $subdir =~ m#/.#) {
1893 $File::Find::prune = 1;
1894 } elsif (check_export_ok("$projectroot/$filter/$subdir")) {
1895 push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1896 $File::Find::prune = 1;
1899 }, "$dir");
1901 } elsif (-f $projects_list) {
1902 # read from file(url-encoded):
1903 # 'git%2Fgit.git Linus+Torvalds'
1904 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1905 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1906 my %paths;
1907 open my ($fd), $projects_list or return;
1908 PROJECT:
1909 while (my $line = <$fd>) {
1910 chomp $line;
1911 my ($path, $owner) = split ' ', $line;
1912 $path = unescape($path);
1913 $owner = unescape($owner);
1914 if (!defined $path) {
1915 next;
1917 if ($filter ne '') {
1918 # looking for forks;
1919 my $pfx = substr($path, 0, length($filter));
1920 if ($pfx ne $filter) {
1921 next PROJECT;
1923 my $sfx = substr($path, length($filter));
1924 if ($sfx !~ /^\/.*\.git$/) {
1925 next PROJECT;
1927 } elsif ($check_forks) {
1928 PATH:
1929 foreach my $filter (keys %paths) {
1930 # looking for forks;
1931 my $pfx = substr($path, 0, length($filter));
1932 if ($pfx ne $filter) {
1933 next PATH;
1935 my $sfx = substr($path, length($filter));
1936 if ($sfx !~ /^\/.*\.git$/) {
1937 next PATH;
1939 # is a fork, don't include it in
1940 # the list
1941 next PROJECT;
1944 if (check_export_ok("$projectroot/$path")) {
1945 my $pr = {
1946 path => $path,
1947 owner => to_utf8($owner),
1949 push @list, $pr;
1950 (my $forks_path = $path) =~ s/\.git$//;
1951 $paths{$forks_path}++;
1954 close $fd;
1956 return @list;
1959 our $gitweb_project_owner = undef;
1960 sub git_get_project_list_from_file {
1962 return if (defined $gitweb_project_owner);
1964 $gitweb_project_owner = {};
1965 # read from file (url-encoded):
1966 # 'git%2Fgit.git Linus+Torvalds'
1967 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1968 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1969 if (-f $projects_list) {
1970 open (my $fd , $projects_list);
1971 while (my $line = <$fd>) {
1972 chomp $line;
1973 my ($pr, $ow) = split ' ', $line;
1974 $pr = unescape($pr);
1975 $ow = unescape($ow);
1976 $gitweb_project_owner->{$pr} = to_utf8($ow);
1978 close $fd;
1982 sub git_get_project_owner {
1983 my $project = shift;
1984 my $owner;
1986 return undef unless $project;
1987 $git_dir = "$projectroot/$project";
1989 if (!defined $gitweb_project_owner) {
1990 git_get_project_list_from_file();
1993 if (exists $gitweb_project_owner->{$project}) {
1994 $owner = $gitweb_project_owner->{$project};
1996 if (!defined $owner){
1997 $owner = git_get_project_config('owner');
1999 if (!defined $owner) {
2000 $owner = get_file_owner("$git_dir");
2003 return $owner;
2006 sub git_get_last_activity {
2007 my ($path) = @_;
2008 my $fd;
2010 $git_dir = "$projectroot/$path";
2011 open($fd, "-|", git_cmd(), 'for-each-ref',
2012 '--format=%(committer)',
2013 '--sort=-committerdate',
2014 '--count=1',
2015 'refs/heads') or return;
2016 my $most_recent = <$fd>;
2017 close $fd or return;
2018 if (defined $most_recent &&
2019 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2020 my $timestamp = $1;
2021 my $age = time - $timestamp;
2022 return ($age, age_string($age));
2024 return (undef, undef);
2027 sub git_get_references {
2028 my $type = shift || "";
2029 my %refs;
2030 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2031 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2032 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2033 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2034 or return;
2036 while (my $line = <$fd>) {
2037 chomp $line;
2038 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2039 if (defined $refs{$1}) {
2040 push @{$refs{$1}}, $2;
2041 } else {
2042 $refs{$1} = [ $2 ];
2046 close $fd or return;
2047 return \%refs;
2050 sub git_get_rev_name_tags {
2051 my $hash = shift || return undef;
2053 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2054 or return;
2055 my $name_rev = <$fd>;
2056 close $fd;
2058 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2059 return $1;
2060 } else {
2061 # catches also '$hash undefined' output
2062 return undef;
2066 ## ----------------------------------------------------------------------
2067 ## parse to hash functions
2069 sub parse_date {
2070 my $epoch = shift;
2071 my $tz = shift || "-0000";
2073 my %date;
2074 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2075 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2076 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2077 $date{'hour'} = $hour;
2078 $date{'minute'} = $min;
2079 $date{'mday'} = $mday;
2080 $date{'day'} = $days[$wday];
2081 $date{'month'} = $months[$mon];
2082 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2083 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2084 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2085 $mday, $months[$mon], $hour ,$min;
2086 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2087 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2089 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2090 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2091 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2092 $date{'hour_local'} = $hour;
2093 $date{'minute_local'} = $min;
2094 $date{'tz_local'} = $tz;
2095 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2096 1900+$year, $mon+1, $mday,
2097 $hour, $min, $sec, $tz);
2098 return %date;
2101 sub parse_tag {
2102 my $tag_id = shift;
2103 my %tag;
2104 my @comment;
2106 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2107 $tag{'id'} = $tag_id;
2108 while (my $line = <$fd>) {
2109 chomp $line;
2110 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2111 $tag{'object'} = $1;
2112 } elsif ($line =~ m/^type (.+)$/) {
2113 $tag{'type'} = $1;
2114 } elsif ($line =~ m/^tag (.+)$/) {
2115 $tag{'name'} = $1;
2116 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2117 $tag{'author'} = $1;
2118 $tag{'epoch'} = $2;
2119 $tag{'tz'} = $3;
2120 } elsif ($line =~ m/--BEGIN/) {
2121 push @comment, $line;
2122 last;
2123 } elsif ($line eq "") {
2124 last;
2127 push @comment, <$fd>;
2128 $tag{'comment'} = \@comment;
2129 close $fd or return;
2130 if (!defined $tag{'name'}) {
2131 return
2133 return %tag
2136 sub parse_commit_text {
2137 my ($commit_text, $withparents) = @_;
2138 my @commit_lines = split '\n', $commit_text;
2139 my %co;
2141 pop @commit_lines; # Remove '\0'
2143 if (! @commit_lines) {
2144 return;
2147 my $header = shift @commit_lines;
2148 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2149 return;
2151 ($co{'id'}, my @parents) = split ' ', $header;
2152 while (my $line = shift @commit_lines) {
2153 last if $line eq "\n";
2154 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2155 $co{'tree'} = $1;
2156 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2157 push @parents, $1;
2158 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2159 $co{'author'} = $1;
2160 $co{'author_epoch'} = $2;
2161 $co{'author_tz'} = $3;
2162 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2163 $co{'author_name'} = $1;
2164 $co{'author_email'} = $2;
2165 } else {
2166 $co{'author_name'} = $co{'author'};
2168 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2169 $co{'committer'} = $1;
2170 $co{'committer_epoch'} = $2;
2171 $co{'committer_tz'} = $3;
2172 $co{'committer_name'} = $co{'committer'};
2173 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2174 $co{'committer_name'} = $1;
2175 $co{'committer_email'} = $2;
2176 } else {
2177 $co{'committer_name'} = $co{'committer'};
2181 if (!defined $co{'tree'}) {
2182 return;
2184 $co{'parents'} = \@parents;
2185 $co{'parent'} = $parents[0];
2187 foreach my $title (@commit_lines) {
2188 $title =~ s/^ //;
2189 if ($title ne "") {
2190 $co{'title'} = chop_str($title, 80, 5);
2191 # remove leading stuff of merges to make the interesting part visible
2192 if (length($title) > 50) {
2193 $title =~ s/^Automatic //;
2194 $title =~ s/^merge (of|with) /Merge ... /i;
2195 if (length($title) > 50) {
2196 $title =~ s/(http|rsync):\/\///;
2198 if (length($title) > 50) {
2199 $title =~ s/(master|www|rsync)\.//;
2201 if (length($title) > 50) {
2202 $title =~ s/kernel.org:?//;
2204 if (length($title) > 50) {
2205 $title =~ s/\/pub\/scm//;
2208 $co{'title_short'} = chop_str($title, 50, 5);
2209 last;
2212 if (! defined $co{'title'} || $co{'title'} eq "") {
2213 $co{'title'} = $co{'title_short'} = '(no commit message)';
2215 # remove added spaces
2216 foreach my $line (@commit_lines) {
2217 $line =~ s/^ //;
2219 $co{'comment'} = \@commit_lines;
2221 my $age = time - $co{'committer_epoch'};
2222 $co{'age'} = $age;
2223 $co{'age_string'} = age_string($age);
2224 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2225 if ($age > 60*60*24*7*2) {
2226 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2227 $co{'age_string_age'} = $co{'age_string'};
2228 } else {
2229 $co{'age_string_date'} = $co{'age_string'};
2230 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2232 return %co;
2235 sub parse_commit {
2236 my ($commit_id) = @_;
2237 my %co;
2239 local $/ = "\0";
2241 open my $fd, "-|", git_cmd(), "rev-list",
2242 "--parents",
2243 "--header",
2244 "--max-count=1",
2245 $commit_id,
2246 "--",
2247 or die_error(500, "Open git-rev-list failed");
2248 %co = parse_commit_text(<$fd>, 1);
2249 close $fd;
2251 return %co;
2254 sub parse_commits {
2255 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2256 my @cos;
2258 $maxcount ||= 1;
2259 $skip ||= 0;
2261 local $/ = "\0";
2263 open my $fd, "-|", git_cmd(), "rev-list",
2264 "--header",
2265 @args,
2266 ("--max-count=" . $maxcount),
2267 ("--skip=" . $skip),
2268 @extra_options,
2269 $commit_id,
2270 "--",
2271 ($filename ? ($filename) : ())
2272 or die_error(500, "Open git-rev-list failed");
2273 while (my $line = <$fd>) {
2274 my %co = parse_commit_text($line);
2275 push @cos, \%co;
2277 close $fd;
2279 return wantarray ? @cos : \@cos;
2282 # parse line of git-diff-tree "raw" output
2283 sub parse_difftree_raw_line {
2284 my $line = shift;
2285 my %res;
2287 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2288 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2289 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2290 $res{'from_mode'} = $1;
2291 $res{'to_mode'} = $2;
2292 $res{'from_id'} = $3;
2293 $res{'to_id'} = $4;
2294 $res{'status'} = $5;
2295 $res{'similarity'} = $6;
2296 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2297 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2298 } else {
2299 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2302 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2303 # combined diff (for merge commit)
2304 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2305 $res{'nparents'} = length($1);
2306 $res{'from_mode'} = [ split(' ', $2) ];
2307 $res{'to_mode'} = pop @{$res{'from_mode'}};
2308 $res{'from_id'} = [ split(' ', $3) ];
2309 $res{'to_id'} = pop @{$res{'from_id'}};
2310 $res{'status'} = [ split('', $4) ];
2311 $res{'to_file'} = unquote($5);
2313 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2314 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2315 $res{'commit'} = $1;
2318 return wantarray ? %res : \%res;
2321 # wrapper: return parsed line of git-diff-tree "raw" output
2322 # (the argument might be raw line, or parsed info)
2323 sub parsed_difftree_line {
2324 my $line_or_ref = shift;
2326 if (ref($line_or_ref) eq "HASH") {
2327 # pre-parsed (or generated by hand)
2328 return $line_or_ref;
2329 } else {
2330 return parse_difftree_raw_line($line_or_ref);
2334 # parse line of git-ls-tree output
2335 sub parse_ls_tree_line ($;%) {
2336 my $line = shift;
2337 my %opts = @_;
2338 my %res;
2340 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2341 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2343 $res{'mode'} = $1;
2344 $res{'type'} = $2;
2345 $res{'hash'} = $3;
2346 if ($opts{'-z'}) {
2347 $res{'name'} = $4;
2348 } else {
2349 $res{'name'} = unquote($4);
2352 return wantarray ? %res : \%res;
2355 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2356 sub parse_from_to_diffinfo {
2357 my ($diffinfo, $from, $to, @parents) = @_;
2359 if ($diffinfo->{'nparents'}) {
2360 # combined diff
2361 $from->{'file'} = [];
2362 $from->{'href'} = [];
2363 fill_from_file_info($diffinfo, @parents)
2364 unless exists $diffinfo->{'from_file'};
2365 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2366 $from->{'file'}[$i] =
2367 defined $diffinfo->{'from_file'}[$i] ?
2368 $diffinfo->{'from_file'}[$i] :
2369 $diffinfo->{'to_file'};
2370 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2371 $from->{'href'}[$i] = href(action=>"blob",
2372 hash_base=>$parents[$i],
2373 hash=>$diffinfo->{'from_id'}[$i],
2374 file_name=>$from->{'file'}[$i]);
2375 } else {
2376 $from->{'href'}[$i] = undef;
2379 } else {
2380 # ordinary (not combined) diff
2381 $from->{'file'} = $diffinfo->{'from_file'};
2382 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2383 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2384 hash=>$diffinfo->{'from_id'},
2385 file_name=>$from->{'file'});
2386 } else {
2387 delete $from->{'href'};
2391 $to->{'file'} = $diffinfo->{'to_file'};
2392 if (!is_deleted($diffinfo)) { # file exists in result
2393 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2394 hash=>$diffinfo->{'to_id'},
2395 file_name=>$to->{'file'});
2396 } else {
2397 delete $to->{'href'};
2401 ## ......................................................................
2402 ## parse to array of hashes functions
2404 sub git_get_heads_list {
2405 my $limit = shift;
2406 my @headslist;
2408 open my $fd, '-|', git_cmd(), 'for-each-ref',
2409 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2410 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2411 'refs/heads'
2412 or return;
2413 while (my $line = <$fd>) {
2414 my %ref_item;
2416 chomp $line;
2417 my ($refinfo, $committerinfo) = split(/\0/, $line);
2418 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2419 my ($committer, $epoch, $tz) =
2420 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2421 $ref_item{'fullname'} = $name;
2422 $name =~ s!^refs/heads/!!;
2424 $ref_item{'name'} = $name;
2425 $ref_item{'id'} = $hash;
2426 $ref_item{'title'} = $title || '(no commit message)';
2427 $ref_item{'epoch'} = $epoch;
2428 if ($epoch) {
2429 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2430 } else {
2431 $ref_item{'age'} = "unknown";
2434 push @headslist, \%ref_item;
2436 close $fd;
2438 return wantarray ? @headslist : \@headslist;
2441 sub git_get_tags_list {
2442 my $limit = shift;
2443 my @tagslist;
2445 open my $fd, '-|', git_cmd(), 'for-each-ref',
2446 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2447 '--format=%(objectname) %(objecttype) %(refname) '.
2448 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2449 'refs/tags'
2450 or return;
2451 while (my $line = <$fd>) {
2452 my %ref_item;
2454 chomp $line;
2455 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2456 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2457 my ($creator, $epoch, $tz) =
2458 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2459 $ref_item{'fullname'} = $name;
2460 $name =~ s!^refs/tags/!!;
2462 $ref_item{'type'} = $type;
2463 $ref_item{'id'} = $id;
2464 $ref_item{'name'} = $name;
2465 if ($type eq "tag") {
2466 $ref_item{'subject'} = $title;
2467 $ref_item{'reftype'} = $reftype;
2468 $ref_item{'refid'} = $refid;
2469 } else {
2470 $ref_item{'reftype'} = $type;
2471 $ref_item{'refid'} = $id;
2474 if ($type eq "tag" || $type eq "commit") {
2475 $ref_item{'epoch'} = $epoch;
2476 if ($epoch) {
2477 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2478 } else {
2479 $ref_item{'age'} = "unknown";
2483 push @tagslist, \%ref_item;
2485 close $fd;
2487 return wantarray ? @tagslist : \@tagslist;
2490 ## ----------------------------------------------------------------------
2491 ## filesystem-related functions
2493 sub get_file_owner {
2494 my $path = shift;
2496 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2497 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2498 if (!defined $gcos) {
2499 return undef;
2501 my $owner = $gcos;
2502 $owner =~ s/[,;].*$//;
2503 return to_utf8($owner);
2506 ## ......................................................................
2507 ## mimetype related functions
2509 sub mimetype_guess_file {
2510 my $filename = shift;
2511 my $mimemap = shift;
2512 -r $mimemap or return undef;
2514 my %mimemap;
2515 open(MIME, $mimemap) or return undef;
2516 while (<MIME>) {
2517 next if m/^#/; # skip comments
2518 my ($mime, $exts) = split(/\t+/);
2519 if (defined $exts) {
2520 my @exts = split(/\s+/, $exts);
2521 foreach my $ext (@exts) {
2522 $mimemap{$ext} = $mime;
2526 close(MIME);
2528 $filename =~ /\.([^.]*)$/;
2529 return $mimemap{$1};
2532 sub mimetype_guess {
2533 my $filename = shift;
2534 my $mime;
2535 $filename =~ /\./ or return undef;
2537 if ($mimetypes_file) {
2538 my $file = $mimetypes_file;
2539 if ($file !~ m!^/!) { # if it is relative path
2540 # it is relative to project
2541 $file = "$projectroot/$project/$file";
2543 $mime = mimetype_guess_file($filename, $file);
2545 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2546 return $mime;
2549 sub blob_mimetype {
2550 my $fd = shift;
2551 my $filename = shift;
2553 if ($filename) {
2554 my $mime = mimetype_guess($filename);
2555 $mime and return $mime;
2558 # just in case
2559 return $default_blob_plain_mimetype unless $fd;
2561 if (-T $fd) {
2562 return 'text/plain';
2563 } elsif (! $filename) {
2564 return 'application/octet-stream';
2565 } elsif ($filename =~ m/\.png$/i) {
2566 return 'image/png';
2567 } elsif ($filename =~ m/\.gif$/i) {
2568 return 'image/gif';
2569 } elsif ($filename =~ m/\.jpe?g$/i) {
2570 return 'image/jpeg';
2571 } else {
2572 return 'application/octet-stream';
2576 sub blob_contenttype {
2577 my ($fd, $file_name, $type) = @_;
2579 $type ||= blob_mimetype($fd, $file_name);
2580 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2581 $type .= "; charset=$default_text_plain_charset";
2584 return $type;
2587 ## ======================================================================
2588 ## functions printing HTML: header, footer, error page
2590 sub git_header_html {
2591 my $status = shift || "200 OK";
2592 my $expires = shift;
2594 my $title = "$site_name";
2595 if (defined $project) {
2596 $title .= " - " . to_utf8($project);
2597 if (defined $action) {
2598 $title .= "/$action";
2599 if (defined $file_name) {
2600 $title .= " - " . esc_path($file_name);
2601 if ($action eq "tree" && $file_name !~ m|/$|) {
2602 $title .= "/";
2607 my $content_type;
2608 # require explicit support from the UA if we are to send the page as
2609 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2610 # we have to do this because MSIE sometimes globs '*/*', pretending to
2611 # support xhtml+xml but choking when it gets what it asked for.
2612 if (defined $cgi->http('HTTP_ACCEPT') &&
2613 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2614 $cgi->Accept('application/xhtml+xml') != 0) {
2615 $content_type = 'application/xhtml+xml';
2616 } else {
2617 $content_type = 'text/html';
2619 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2620 -status=> $status, -expires => $expires);
2621 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2622 print <<EOF;
2623 <?xml version="1.0" encoding="utf-8"?>
2624 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2625 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2626 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2627 <!-- git core binaries version $git_version -->
2628 <head>
2629 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2630 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2631 <meta name="robots" content="index, nofollow"/>
2632 <title>$title</title>
2634 # print out each stylesheet that exist
2635 if (defined $stylesheet) {
2636 #provides backwards capability for those people who define style sheet in a config file
2637 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2638 } else {
2639 foreach my $stylesheet (@stylesheets) {
2640 next unless $stylesheet;
2641 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2644 if (defined $project) {
2645 my %href_params = get_feed_info();
2646 if (!exists $href_params{'-title'}) {
2647 $href_params{'-title'} = 'log';
2650 foreach my $format qw(RSS Atom) {
2651 my $type = lc($format);
2652 my %link_attr = (
2653 '-rel' => 'alternate',
2654 '-title' => "$project - $href_params{'-title'} - $format feed",
2655 '-type' => "application/$type+xml"
2658 $href_params{'action'} = $type;
2659 $link_attr{'-href'} = href(%href_params);
2660 print "<link ".
2661 "rel=\"$link_attr{'-rel'}\" ".
2662 "title=\"$link_attr{'-title'}\" ".
2663 "href=\"$link_attr{'-href'}\" ".
2664 "type=\"$link_attr{'-type'}\" ".
2665 "/>\n";
2667 $href_params{'extra_options'} = '--no-merges';
2668 $link_attr{'-href'} = href(%href_params);
2669 $link_attr{'-title'} .= ' (no merges)';
2670 print "<link ".
2671 "rel=\"$link_attr{'-rel'}\" ".
2672 "title=\"$link_attr{'-title'}\" ".
2673 "href=\"$link_attr{'-href'}\" ".
2674 "type=\"$link_attr{'-type'}\" ".
2675 "/>\n";
2678 } else {
2679 printf('<link rel="alternate" title="%s projects list" '.
2680 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2681 $site_name, href(project=>undef, action=>"project_index"));
2682 printf('<link rel="alternate" title="%s projects feeds" '.
2683 'href="%s" type="text/x-opml" />'."\n",
2684 $site_name, href(project=>undef, action=>"opml"));
2686 if (defined $favicon) {
2687 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2690 print "</head>\n" .
2691 "<body>\n";
2693 if (-f $site_header) {
2694 open (my $fd, $site_header);
2695 print <$fd>;
2696 close $fd;
2699 print "<div class=\"page_header\">\n" .
2700 $cgi->a({-href => esc_url($logo_url),
2701 -title => $logo_label},
2702 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2703 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2704 if (defined $project) {
2705 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2706 if (defined $action) {
2707 print " / $action";
2709 print "\n";
2711 print "</div>\n";
2713 my ($have_search) = gitweb_check_feature('search');
2714 if (defined $project && $have_search) {
2715 if (!defined $searchtext) {
2716 $searchtext = "";
2718 my $search_hash;
2719 if (defined $hash_base) {
2720 $search_hash = $hash_base;
2721 } elsif (defined $hash) {
2722 $search_hash = $hash;
2723 } else {
2724 $search_hash = "HEAD";
2726 my $action = $my_uri;
2727 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2728 if ($use_pathinfo) {
2729 $action .= "/".esc_url($project);
2731 print $cgi->startform(-method => "get", -action => $action) .
2732 "<div class=\"search\">\n" .
2733 (!$use_pathinfo &&
2734 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2735 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2736 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2737 $cgi->popup_menu(-name => 'st', -default => 'commit',
2738 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2739 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2740 " search:\n",
2741 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2742 "<span title=\"Extended regular expression\">" .
2743 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2744 -checked => $search_use_regexp) .
2745 "</span>" .
2746 "</div>" .
2747 $cgi->end_form() . "\n";
2751 sub git_footer_html {
2752 my $feed_class = 'rss_logo';
2754 print "<div class=\"page_footer\">\n";
2755 if (defined $project) {
2756 my $descr = git_get_project_description($project);
2757 if (defined $descr) {
2758 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2761 my %href_params = get_feed_info();
2762 if (!%href_params) {
2763 $feed_class .= ' generic';
2765 $href_params{'-title'} ||= 'log';
2767 foreach my $format qw(RSS Atom) {
2768 $href_params{'action'} = lc($format);
2769 print $cgi->a({-href => href(%href_params),
2770 -title => "$href_params{'-title'} $format feed",
2771 -class => $feed_class}, $format)."\n";
2774 } else {
2775 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2776 -class => $feed_class}, "OPML") . " ";
2777 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2778 -class => $feed_class}, "TXT") . "\n";
2780 print "</div>\n"; # class="page_footer"
2782 if (-f $site_footer) {
2783 open (my $fd, $site_footer);
2784 print <$fd>;
2785 close $fd;
2788 print "</body>\n" .
2789 "</html>";
2792 # die_error(<http_status_code>, <error_message>)
2793 # Example: die_error(404, 'Hash not found')
2794 # By convention, use the following status codes (as defined in RFC 2616):
2795 # 400: Invalid or missing CGI parameters, or
2796 # requested object exists but has wrong type.
2797 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2798 # this server or project.
2799 # 404: Requested object/revision/project doesn't exist.
2800 # 500: The server isn't configured properly, or
2801 # an internal error occurred (e.g. failed assertions caused by bugs), or
2802 # an unknown error occurred (e.g. the git binary died unexpectedly).
2803 sub die_error {
2804 my $status = shift || 500;
2805 my $error = shift || "Internal server error";
2807 my %http_responses = (400 => '400 Bad Request',
2808 403 => '403 Forbidden',
2809 404 => '404 Not Found',
2810 500 => '500 Internal Server Error');
2811 git_header_html($http_responses{$status});
2812 print <<EOF;
2813 <div class="page_body">
2814 <br /><br />
2815 $status - $error
2816 <br />
2817 </div>
2819 git_footer_html();
2820 exit;
2823 ## ----------------------------------------------------------------------
2824 ## functions printing or outputting HTML: navigation
2826 sub git_print_page_nav {
2827 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2828 $extra = '' if !defined $extra; # pager or formats
2830 my @navs = qw(summary shortlog log commit commitdiff tree);
2831 if ($suppress) {
2832 @navs = grep { $_ ne $suppress } @navs;
2835 my %arg = map { $_ => {action=>$_} } @navs;
2836 if (defined $head) {
2837 for (qw(commit commitdiff)) {
2838 $arg{$_}{'hash'} = $head;
2840 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2841 for (qw(shortlog log)) {
2842 $arg{$_}{'hash'} = $head;
2846 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2847 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2849 print "<div class=\"page_nav\">\n" .
2850 (join " | ",
2851 map { $_ eq $current ?
2852 $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
2853 } @navs);
2854 print "<br/>\n$extra<br/>\n" .
2855 "</div>\n";
2858 sub format_paging_nav {
2859 my ($action, $hash, $head, $page, $has_next_link) = @_;
2860 my $paging_nav;
2863 if ($hash ne $head || $page) {
2864 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2865 } else {
2866 $paging_nav .= "HEAD";
2869 if ($page > 0) {
2870 $paging_nav .= " &sdot; " .
2871 $cgi->a({-href => href(-replay=>1, page=>$page-1),
2872 -accesskey => "p", -title => "Alt-p"}, "prev");
2873 } else {
2874 $paging_nav .= " &sdot; prev";
2877 if ($has_next_link) {
2878 $paging_nav .= " &sdot; " .
2879 $cgi->a({-href => href(-replay=>1, page=>$page+1),
2880 -accesskey => "n", -title => "Alt-n"}, "next");
2881 } else {
2882 $paging_nav .= " &sdot; next";
2885 return $paging_nav;
2888 ## ......................................................................
2889 ## functions printing or outputting HTML: div
2891 sub git_print_header_div {
2892 my ($action, $title, $hash, $hash_base) = @_;
2893 my %args = ();
2895 $args{'action'} = $action;
2896 $args{'hash'} = $hash if $hash;
2897 $args{'hash_base'} = $hash_base if $hash_base;
2899 print "<div class=\"header\">\n" .
2900 $cgi->a({-href => href(%args), -class => "title"},
2901 $title ? $title : $action) .
2902 "\n</div>\n";
2905 #sub git_print_authorship (\%) {
2906 sub git_print_authorship {
2907 my $co = shift;
2909 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
2910 print "<div class=\"author_date\">" .
2911 esc_html($co->{'author_name'}) .
2912 " [$ad{'rfc2822'}";
2913 if ($ad{'hour_local'} < 6) {
2914 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
2915 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2916 } else {
2917 printf(" (%02d:%02d %s)",
2918 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
2920 print "]</div>\n";
2923 sub git_print_page_path {
2924 my $name = shift;
2925 my $type = shift;
2926 my $hb = shift;
2929 print "<div class=\"page_path\">";
2930 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
2931 -title => 'tree root'}, to_utf8("[$project]"));
2932 print " / ";
2933 if (defined $name) {
2934 my @dirname = split '/', $name;
2935 my $basename = pop @dirname;
2936 my $fullname = '';
2938 foreach my $dir (@dirname) {
2939 $fullname .= ($fullname ? '/' : '') . $dir;
2940 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
2941 hash_base=>$hb),
2942 -title => $fullname}, esc_path($dir));
2943 print " / ";
2945 if (defined $type && $type eq 'blob') {
2946 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
2947 hash_base=>$hb),
2948 -title => $name}, esc_path($basename));
2949 } elsif (defined $type && $type eq 'tree') {
2950 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
2951 hash_base=>$hb),
2952 -title => $name}, esc_path($basename));
2953 print " / ";
2954 } else {
2955 print esc_path($basename);
2958 print "<br/></div>\n";
2961 # sub git_print_log (\@;%) {
2962 sub git_print_log ($;%) {
2963 my $log = shift;
2964 my %opts = @_;
2966 if ($opts{'-remove_title'}) {
2967 # remove title, i.e. first line of log
2968 shift @$log;
2970 # remove leading empty lines
2971 while (defined $log->[0] && $log->[0] eq "") {
2972 shift @$log;
2975 # print log
2976 my $signoff = 0;
2977 my $empty = 0;
2978 foreach my $line (@$log) {
2979 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
2980 $signoff = 1;
2981 $empty = 0;
2982 if (! $opts{'-remove_signoff'}) {
2983 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
2984 next;
2985 } else {
2986 # remove signoff lines
2987 next;
2989 } else {
2990 $signoff = 0;
2993 # print only one empty line
2994 # do not print empty line after signoff
2995 if ($line eq "") {
2996 next if ($empty || $signoff);
2997 $empty = 1;
2998 } else {
2999 $empty = 0;
3002 print format_log_line_html($line) . "<br/>\n";
3005 if ($opts{'-final_empty_line'}) {
3006 # end with single empty line
3007 print "<br/>\n" unless $empty;
3011 # return link target (what link points to)
3012 sub git_get_link_target {
3013 my $hash = shift;
3014 my $link_target;
3016 # read link
3017 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3018 or return;
3020 local $/;
3021 $link_target = <$fd>;
3023 close $fd
3024 or return;
3026 return $link_target;
3029 # given link target, and the directory (basedir) the link is in,
3030 # return target of link relative to top directory (top tree);
3031 # return undef if it is not possible (including absolute links).
3032 sub normalize_link_target {
3033 my ($link_target, $basedir, $hash_base) = @_;
3035 # we can normalize symlink target only if $hash_base is provided
3036 return unless $hash_base;
3038 # absolute symlinks (beginning with '/') cannot be normalized
3039 return if (substr($link_target, 0, 1) eq '/');
3041 # normalize link target to path from top (root) tree (dir)
3042 my $path;
3043 if ($basedir) {
3044 $path = $basedir . '/' . $link_target;
3045 } else {
3046 # we are in top (root) tree (dir)
3047 $path = $link_target;
3050 # remove //, /./, and /../
3051 my @path_parts;
3052 foreach my $part (split('/', $path)) {
3053 # discard '.' and ''
3054 next if (!$part || $part eq '.');
3055 # handle '..'
3056 if ($part eq '..') {
3057 if (@path_parts) {
3058 pop @path_parts;
3059 } else {
3060 # link leads outside repository (outside top dir)
3061 return;
3063 } else {
3064 push @path_parts, $part;
3067 $path = join('/', @path_parts);
3069 return $path;
3072 # print tree entry (row of git_tree), but without encompassing <tr> element
3073 sub git_print_tree_entry {
3074 my ($t, $basedir, $hash_base, $have_blame) = @_;
3076 my %base_key = ();
3077 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3079 # The format of a table row is: mode list link. Where mode is
3080 # the mode of the entry, list is the name of the entry, an href,
3081 # and link is the action links of the entry.
3083 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3084 if ($t->{'type'} eq "blob") {
3085 print "<td class=\"list\">" .
3086 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3087 file_name=>"$basedir$t->{'name'}", %base_key),
3088 -class => "list"}, esc_path($t->{'name'}));
3089 if (S_ISLNK(oct $t->{'mode'})) {
3090 my $link_target = git_get_link_target($t->{'hash'});
3091 if ($link_target) {
3092 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3093 if (defined $norm_target) {
3094 print " -> " .
3095 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3096 file_name=>$norm_target),
3097 -title => $norm_target}, esc_path($link_target));
3098 } else {
3099 print " -> " . esc_path($link_target);
3103 print "</td>\n";
3104 print "<td class=\"link\">";
3105 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3106 file_name=>"$basedir$t->{'name'}", %base_key)},
3107 "blob");
3108 if ($have_blame) {
3109 print " | " .
3110 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3111 file_name=>"$basedir$t->{'name'}", %base_key)},
3112 "blame");
3114 if (defined $hash_base) {
3115 print " | " .
3116 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3117 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3118 "history");
3120 print " | " .
3121 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3122 file_name=>"$basedir$t->{'name'}")},
3123 "raw");
3124 print "</td>\n";
3126 } elsif ($t->{'type'} eq "tree") {
3127 print "<td class=\"list\">";
3128 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3129 file_name=>"$basedir$t->{'name'}", %base_key)},
3130 esc_path($t->{'name'}));
3131 print "</td>\n";
3132 print "<td class=\"link\">";
3133 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3134 file_name=>"$basedir$t->{'name'}", %base_key)},
3135 "tree");
3136 if (defined $hash_base) {
3137 print " | " .
3138 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3139 file_name=>"$basedir$t->{'name'}")},
3140 "history");
3142 print "</td>\n";
3143 } else {
3144 # unknown object: we can only present history for it
3145 # (this includes 'commit' object, i.e. submodule support)
3146 print "<td class=\"list\">" .
3147 esc_path($t->{'name'}) .
3148 "</td>\n";
3149 print "<td class=\"link\">";
3150 if (defined $hash_base) {
3151 print $cgi->a({-href => href(action=>"history",
3152 hash_base=>$hash_base,
3153 file_name=>"$basedir$t->{'name'}")},
3154 "history");
3156 print "</td>\n";
3160 ## ......................................................................
3161 ## functions printing large fragments of HTML
3163 # get pre-image filenames for merge (combined) diff
3164 sub fill_from_file_info {
3165 my ($diff, @parents) = @_;
3167 $diff->{'from_file'} = [ ];
3168 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3169 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3170 if ($diff->{'status'}[$i] eq 'R' ||
3171 $diff->{'status'}[$i] eq 'C') {
3172 $diff->{'from_file'}[$i] =
3173 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3177 return $diff;
3180 # is current raw difftree line of file deletion
3181 sub is_deleted {
3182 my $diffinfo = shift;
3184 return $diffinfo->{'to_id'} eq ('0' x 40);
3187 # does patch correspond to [previous] difftree raw line
3188 # $diffinfo - hashref of parsed raw diff format
3189 # $patchinfo - hashref of parsed patch diff format
3190 # (the same keys as in $diffinfo)
3191 sub is_patch_split {
3192 my ($diffinfo, $patchinfo) = @_;
3194 return defined $diffinfo && defined $patchinfo
3195 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3199 sub git_difftree_body {
3200 my ($difftree, $hash, @parents) = @_;
3201 my ($parent) = $parents[0];
3202 my ($have_blame) = gitweb_check_feature('blame');
3203 print "<div class=\"list_head\">\n";
3204 if ($#{$difftree} > 10) {
3205 print(($#{$difftree} + 1) . " files changed:\n");
3207 print "</div>\n";
3209 print "<table class=\"" .
3210 (@parents > 1 ? "combined " : "") .
3211 "diff_tree\">\n";
3213 # header only for combined diff in 'commitdiff' view
3214 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3215 if ($has_header) {
3216 # table header
3217 print "<thead><tr>\n" .
3218 "<th></th><th></th>\n"; # filename, patchN link
3219 for (my $i = 0; $i < @parents; $i++) {
3220 my $par = $parents[$i];
3221 print "<th>" .
3222 $cgi->a({-href => href(action=>"commitdiff",
3223 hash=>$hash, hash_parent=>$par),
3224 -title => 'commitdiff to parent number ' .
3225 ($i+1) . ': ' . substr($par,0,7)},
3226 $i+1) .
3227 "&nbsp;</th>\n";
3229 print "</tr></thead>\n<tbody>\n";
3232 my $alternate = 1;
3233 my $patchno = 0;
3234 foreach my $line (@{$difftree}) {
3235 my $diff = parsed_difftree_line($line);
3237 if ($alternate) {
3238 print "<tr class=\"dark\">\n";
3239 } else {
3240 print "<tr class=\"light\">\n";
3242 $alternate ^= 1;
3244 if (exists $diff->{'nparents'}) { # combined diff
3246 fill_from_file_info($diff, @parents)
3247 unless exists $diff->{'from_file'};
3249 if (!is_deleted($diff)) {
3250 # file exists in the result (child) commit
3251 print "<td>" .
3252 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3253 file_name=>$diff->{'to_file'},
3254 hash_base=>$hash),
3255 -class => "list"}, esc_path($diff->{'to_file'})) .
3256 "</td>\n";
3257 } else {
3258 print "<td>" .
3259 esc_path($diff->{'to_file'}) .
3260 "</td>\n";
3263 if ($action eq 'commitdiff') {
3264 # link to patch
3265 $patchno++;
3266 print "<td class=\"link\">" .
3267 $cgi->a({-href => "#patch$patchno"}, "patch") .
3268 " | " .
3269 "</td>\n";
3272 my $has_history = 0;
3273 my $not_deleted = 0;
3274 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3275 my $hash_parent = $parents[$i];
3276 my $from_hash = $diff->{'from_id'}[$i];
3277 my $from_path = $diff->{'from_file'}[$i];
3278 my $status = $diff->{'status'}[$i];
3280 $has_history ||= ($status ne 'A');
3281 $not_deleted ||= ($status ne 'D');
3283 if ($status eq 'A') {
3284 print "<td class=\"link\" align=\"right\"> | </td>\n";
3285 } elsif ($status eq 'D') {
3286 print "<td class=\"link\">" .
3287 $cgi->a({-href => href(action=>"blob",
3288 hash_base=>$hash,
3289 hash=>$from_hash,
3290 file_name=>$from_path)},
3291 "blob" . ($i+1)) .
3292 " | </td>\n";
3293 } else {
3294 if ($diff->{'to_id'} eq $from_hash) {
3295 print "<td class=\"link nochange\">";
3296 } else {
3297 print "<td class=\"link\">";
3299 print $cgi->a({-href => href(action=>"blobdiff",
3300 hash=>$diff->{'to_id'},
3301 hash_parent=>$from_hash,
3302 hash_base=>$hash,
3303 hash_parent_base=>$hash_parent,
3304 file_name=>$diff->{'to_file'},
3305 file_parent=>$from_path)},
3306 "diff" . ($i+1)) .
3307 " | </td>\n";
3311 print "<td class=\"link\">";
3312 if ($not_deleted) {
3313 print $cgi->a({-href => href(action=>"blob",
3314 hash=>$diff->{'to_id'},
3315 file_name=>$diff->{'to_file'},
3316 hash_base=>$hash)},
3317 "blob");
3318 print " | " if ($has_history);
3320 if ($has_history) {
3321 print $cgi->a({-href => href(action=>"history",
3322 file_name=>$diff->{'to_file'},
3323 hash_base=>$hash)},
3324 "history");
3326 print "</td>\n";
3328 print "</tr>\n";
3329 next; # instead of 'else' clause, to avoid extra indent
3331 # else ordinary diff
3333 my ($to_mode_oct, $to_mode_str, $to_file_type);
3334 my ($from_mode_oct, $from_mode_str, $from_file_type);
3335 if ($diff->{'to_mode'} ne ('0' x 6)) {
3336 $to_mode_oct = oct $diff->{'to_mode'};
3337 if (S_ISREG($to_mode_oct)) { # only for regular file
3338 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3340 $to_file_type = file_type($diff->{'to_mode'});
3342 if ($diff->{'from_mode'} ne ('0' x 6)) {
3343 $from_mode_oct = oct $diff->{'from_mode'};
3344 if (S_ISREG($to_mode_oct)) { # only for regular file
3345 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3347 $from_file_type = file_type($diff->{'from_mode'});
3350 if ($diff->{'status'} eq "A") { # created
3351 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3352 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3353 $mode_chng .= "]</span>";
3354 print "<td>";
3355 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3356 hash_base=>$hash, file_name=>$diff->{'file'}),
3357 -class => "list"}, esc_path($diff->{'file'}));
3358 print "</td>\n";
3359 print "<td>$mode_chng</td>\n";
3360 print "<td class=\"link\">";
3361 if ($action eq 'commitdiff') {
3362 # link to patch
3363 $patchno++;
3364 print $cgi->a({-href => "#patch$patchno"}, "patch");
3365 print " | ";
3367 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3368 hash_base=>$hash, file_name=>$diff->{'file'})},
3369 "blob");
3370 print "</td>\n";
3372 } elsif ($diff->{'status'} eq "D") { # deleted
3373 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3374 print "<td>";
3375 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3376 hash_base=>$parent, file_name=>$diff->{'file'}),
3377 -class => "list"}, esc_path($diff->{'file'}));
3378 print "</td>\n";
3379 print "<td>$mode_chng</td>\n";
3380 print "<td class=\"link\">";
3381 if ($action eq 'commitdiff') {
3382 # link to patch
3383 $patchno++;
3384 print $cgi->a({-href => "#patch$patchno"}, "patch");
3385 print " | ";
3387 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3388 hash_base=>$parent, file_name=>$diff->{'file'})},
3389 "blob") . " | ";
3390 if ($have_blame) {
3391 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3392 file_name=>$diff->{'file'})},
3393 "blame") . " | ";
3395 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3396 file_name=>$diff->{'file'})},
3397 "history");
3398 print "</td>\n";
3400 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3401 my $mode_chnge = "";
3402 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3403 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3404 if ($from_file_type ne $to_file_type) {
3405 $mode_chnge .= " from $from_file_type to $to_file_type";
3407 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3408 if ($from_mode_str && $to_mode_str) {
3409 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3410 } elsif ($to_mode_str) {
3411 $mode_chnge .= " mode: $to_mode_str";
3414 $mode_chnge .= "]</span>\n";
3416 print "<td>";
3417 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3418 hash_base=>$hash, file_name=>$diff->{'file'}),
3419 -class => "list"}, esc_path($diff->{'file'}));
3420 print "</td>\n";
3421 print "<td>$mode_chnge</td>\n";
3422 print "<td class=\"link\">";
3423 if ($action eq 'commitdiff') {
3424 # link to patch
3425 $patchno++;
3426 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3427 " | ";
3428 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3429 # "commit" view and modified file (not onlu mode changed)
3430 print $cgi->a({-href => href(action=>"blobdiff",
3431 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3432 hash_base=>$hash, hash_parent_base=>$parent,
3433 file_name=>$diff->{'file'})},
3434 "diff") .
3435 " | ";
3437 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3438 hash_base=>$hash, file_name=>$diff->{'file'})},
3439 "blob") . " | ";
3440 if ($have_blame) {
3441 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3442 file_name=>$diff->{'file'})},
3443 "blame") . " | ";
3445 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3446 file_name=>$diff->{'file'})},
3447 "history");
3448 print "</td>\n";
3450 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3451 my %status_name = ('R' => 'moved', 'C' => 'copied');
3452 my $nstatus = $status_name{$diff->{'status'}};
3453 my $mode_chng = "";
3454 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3455 # mode also for directories, so we cannot use $to_mode_str
3456 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3458 print "<td>" .
3459 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3460 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3461 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3462 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3463 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3464 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3465 -class => "list"}, esc_path($diff->{'from_file'})) .
3466 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3467 "<td class=\"link\">";
3468 if ($action eq 'commitdiff') {
3469 # link to patch
3470 $patchno++;
3471 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3472 " | ";
3473 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3474 # "commit" view and modified file (not only pure rename or copy)
3475 print $cgi->a({-href => href(action=>"blobdiff",
3476 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3477 hash_base=>$hash, hash_parent_base=>$parent,
3478 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3479 "diff") .
3480 " | ";
3482 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3483 hash_base=>$parent, file_name=>$diff->{'to_file'})},
3484 "blob") . " | ";
3485 if ($have_blame) {
3486 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3487 file_name=>$diff->{'to_file'})},
3488 "blame") . " | ";
3490 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3491 file_name=>$diff->{'to_file'})},
3492 "history");
3493 print "</td>\n";
3495 } # we should not encounter Unmerged (U) or Unknown (X) status
3496 print "</tr>\n";
3498 print "</tbody>" if $has_header;
3499 print "</table>\n";
3502 sub git_patchset_body {
3503 my ($fd, $difftree, $hash, @hash_parents) = @_;
3504 my ($hash_parent) = $hash_parents[0];
3506 my $is_combined = (@hash_parents > 1);
3507 my $patch_idx = 0;
3508 my $patch_number = 0;
3509 my $patch_line;
3510 my $diffinfo;
3511 my $to_name;
3512 my (%from, %to);
3514 print "<div class=\"patchset\">\n";
3516 # skip to first patch
3517 while ($patch_line = <$fd>) {
3518 chomp $patch_line;
3520 last if ($patch_line =~ m/^diff /);
3523 PATCH:
3524 while ($patch_line) {
3526 # parse "git diff" header line
3527 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3528 # $1 is from_name, which we do not use
3529 $to_name = unquote($2);
3530 $to_name =~ s!^b/!!;
3531 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3532 # $1 is 'cc' or 'combined', which we do not use
3533 $to_name = unquote($2);
3534 } else {
3535 $to_name = undef;
3538 # check if current patch belong to current raw line
3539 # and parse raw git-diff line if needed
3540 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3541 # this is continuation of a split patch
3542 print "<div class=\"patch cont\">\n";
3543 } else {
3544 # advance raw git-diff output if needed
3545 $patch_idx++ if defined $diffinfo;
3547 # read and prepare patch information
3548 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3550 # compact combined diff output can have some patches skipped
3551 # find which patch (using pathname of result) we are at now;
3552 if ($is_combined) {
3553 while ($to_name ne $diffinfo->{'to_file'}) {
3554 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3555 format_diff_cc_simplified($diffinfo, @hash_parents) .
3556 "</div>\n"; # class="patch"
3558 $patch_idx++;
3559 $patch_number++;
3561 last if $patch_idx > $#$difftree;
3562 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3566 # modifies %from, %to hashes
3567 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3569 # this is first patch for raw difftree line with $patch_idx index
3570 # we index @$difftree array from 0, but number patches from 1
3571 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3574 # git diff header
3575 #assert($patch_line =~ m/^diff /) if DEBUG;
3576 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3577 $patch_number++;
3578 # print "git diff" header
3579 print format_git_diff_header_line($patch_line, $diffinfo,
3580 \%from, \%to);
3582 # print extended diff header
3583 print "<div class=\"diff extended_header\">\n";
3584 EXTENDED_HEADER:
3585 while ($patch_line = <$fd>) {
3586 chomp $patch_line;
3588 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3590 print format_extended_diff_header_line($patch_line, $diffinfo,
3591 \%from, \%to);
3593 print "</div>\n"; # class="diff extended_header"
3595 # from-file/to-file diff header
3596 if (! $patch_line) {
3597 print "</div>\n"; # class="patch"
3598 last PATCH;
3600 next PATCH if ($patch_line =~ m/^diff /);
3601 #assert($patch_line =~ m/^---/) if DEBUG;
3603 my $last_patch_line = $patch_line;
3604 $patch_line = <$fd>;
3605 chomp $patch_line;
3606 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3608 print format_diff_from_to_header($last_patch_line, $patch_line,
3609 $diffinfo, \%from, \%to,
3610 @hash_parents);
3612 # the patch itself
3613 LINE:
3614 while ($patch_line = <$fd>) {
3615 chomp $patch_line;
3617 next PATCH if ($patch_line =~ m/^diff /);
3619 print format_diff_line($patch_line, \%from, \%to);
3622 } continue {
3623 print "</div>\n"; # class="patch"
3626 # for compact combined (--cc) format, with chunk and patch simpliciaction
3627 # patchset might be empty, but there might be unprocessed raw lines
3628 for (++$patch_idx if $patch_number > 0;
3629 $patch_idx < @$difftree;
3630 ++$patch_idx) {
3631 # read and prepare patch information
3632 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3634 # generate anchor for "patch" links in difftree / whatchanged part
3635 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3636 format_diff_cc_simplified($diffinfo, @hash_parents) .
3637 "</div>\n"; # class="patch"
3639 $patch_number++;
3642 if ($patch_number == 0) {
3643 if (@hash_parents > 1) {
3644 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3645 } else {
3646 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3650 print "</div>\n"; # class="patchset"
3653 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3655 # fills project list info (age, description, owner, forks) for each
3656 # project in the list, removing invalid projects from returned list
3657 # NOTE: modifies $projlist, but does not remove entries from it
3658 sub fill_project_list_info {
3659 my ($projlist, $check_forks) = @_;
3660 my @projects;
3662 my $show_ctags = gitweb_check_feature('ctags');
3663 PROJECT:
3664 foreach my $pr (@$projlist) {
3665 my (@activity) = git_get_last_activity($pr->{'path'});
3666 unless (@activity) {
3667 next PROJECT;
3669 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3670 if (!defined $pr->{'descr'}) {
3671 my $descr = git_get_project_description($pr->{'path'}) || "";
3672 $descr = to_utf8($descr);
3673 $pr->{'descr_long'} = $descr;
3674 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3676 if (!defined $pr->{'owner'}) {
3677 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3679 if ($check_forks) {
3680 my $pname = $pr->{'path'};
3681 if (($pname =~ s/\.git$//) &&
3682 ($pname !~ /\/$/) &&
3683 (-d "$projectroot/$pname")) {
3684 $pr->{'forks'} = "-d $projectroot/$pname";
3685 } else {
3686 $pr->{'forks'} = 0;
3689 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3690 push @projects, $pr;
3693 return @projects;
3696 # print 'sort by' <th> element, either sorting by $key if $name eq $order
3697 # (changing $list), or generating 'sort by $name' replay link otherwise
3698 sub print_sort_th {
3699 my ($str_sort, $name, $order, $key, $header, $list) = @_;
3700 $key ||= $name;
3701 $header ||= ucfirst($name);
3703 if ($order eq $name) {
3704 if ($str_sort) {
3705 @$list = sort {$a->{$key} cmp $b->{$key}} @$list;
3706 } else {
3707 @$list = sort {$a->{$key} <=> $b->{$key}} @$list;
3709 print "<th>$header</th>\n";
3710 } else {
3711 print "<th>" .
3712 $cgi->a({-href => href(-replay=>1, order=>$name),
3713 -class => "header"}, $header) .
3714 "</th>\n";
3718 sub print_sort_th_str {
3719 print_sort_th(1, @_);
3722 sub print_sort_th_num {
3723 print_sort_th(0, @_);
3726 sub git_project_list_body {
3727 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3729 my ($check_forks) = gitweb_check_feature('forks');
3730 my @projects = fill_project_list_info($projlist, $check_forks);
3732 $order ||= $default_projects_order;
3733 $from = 0 unless defined $from;
3734 $to = $#projects if (!defined $to || $#projects < $to);
3736 my $show_ctags = gitweb_check_feature('ctags');
3737 if ($show_ctags) {
3738 my %ctags;
3739 foreach my $p (@projects) {
3740 foreach my $ct (keys %{$p->{'ctags'}}) {
3741 $ctags{$ct} += $p->{'ctags'}->{$ct};
3744 my $cloud = git_populate_project_tagcloud(\%ctags);
3745 print git_show_project_tagcloud($cloud, 64);
3748 print "<table class=\"project_list\">\n";
3749 unless ($no_header) {
3750 print "<tr>\n";
3751 if ($check_forks) {
3752 print "<th></th>\n";
3754 print_sort_th_str('project', $order, 'path',
3755 'Project', \@projects);
3756 print_sort_th_str('descr', $order, 'descr_long',
3757 'Description', \@projects);
3758 print_sort_th_str('owner', $order, 'owner',
3759 'Owner', \@projects);
3760 print_sort_th_num('age', $order, 'age',
3761 'Last Change', \@projects);
3762 print "<th></th>\n" . # for links
3763 "</tr>\n";
3765 my $alternate = 1;
3766 my $tagfilter = $cgi->param('by_tag');
3767 for (my $i = $from; $i <= $to; $i++) {
3768 my $pr = $projects[$i];
3769 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3770 if ($alternate) {
3771 print "<tr class=\"dark\">\n";
3772 } else {
3773 print "<tr class=\"light\">\n";
3775 $alternate ^= 1;
3776 if ($check_forks) {
3777 print "<td>";
3778 if ($pr->{'forks'}) {
3779 print "<!-- $pr->{'forks'} -->\n";
3780 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3782 print "</td>\n";
3784 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3785 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3786 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3787 -class => "list", -title => $pr->{'descr_long'}},
3788 esc_html($pr->{'descr'})) . "</td>\n" .
3789 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3790 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3791 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3792 "<td class=\"link\">" .
3793 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
3794 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3795 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3796 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3797 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3798 "</td>\n" .
3799 "</tr>\n";
3801 if (defined $extra) {
3802 print "<tr>\n";
3803 if ($check_forks) {
3804 print "<td></td>\n";
3806 print "<td colspan=\"5\">$extra</td>\n" .
3807 "</tr>\n";
3809 print "</table>\n";
3812 sub git_shortlog_body {
3813 # uses global variable $project
3814 my ($commitlist, $from, $to, $refs, $extra) = @_;
3816 $from = 0 unless defined $from;
3817 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3819 print "<table class=\"shortlog\">\n";
3820 my $alternate = 1;
3821 for (my $i = $from; $i <= $to; $i++) {
3822 my %co = %{$commitlist->[$i]};
3823 my $commit = $co{'id'};
3824 my $ref = format_ref_marker($refs, $commit);
3825 if ($alternate) {
3826 print "<tr class=\"dark\">\n";
3827 } else {
3828 print "<tr class=\"light\">\n";
3830 $alternate ^= 1;
3831 my $author = chop_and_escape_str($co{'author_name'}, 10);
3832 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3833 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3834 "<td><i>" . $author . "</i></td>\n" .
3835 "<td>";
3836 print format_subject_html($co{'title'}, $co{'title_short'},
3837 href(action=>"commit", hash=>$commit), $ref);
3838 print "</td>\n" .
3839 "<td class=\"link\">" .
3840 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3841 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3842 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3843 my $snapshot_links = format_snapshot_links($commit);
3844 if (defined $snapshot_links) {
3845 print " | " . $snapshot_links;
3847 print "</td>\n" .
3848 "</tr>\n";
3850 if (defined $extra) {
3851 print "<tr>\n" .
3852 "<td colspan=\"4\">$extra</td>\n" .
3853 "</tr>\n";
3855 print "</table>\n";
3858 sub git_history_body {
3859 # Warning: assumes constant type (blob or tree) during history
3860 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3862 $from = 0 unless defined $from;
3863 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3865 print "<table class=\"history\">\n";
3866 my $alternate = 1;
3867 for (my $i = $from; $i <= $to; $i++) {
3868 my %co = %{$commitlist->[$i]};
3869 if (!%co) {
3870 next;
3872 my $commit = $co{'id'};
3874 my $ref = format_ref_marker($refs, $commit);
3876 if ($alternate) {
3877 print "<tr class=\"dark\">\n";
3878 } else {
3879 print "<tr class=\"light\">\n";
3881 $alternate ^= 1;
3882 # shortlog uses chop_str($co{'author_name'}, 10)
3883 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3884 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3885 "<td><i>" . $author . "</i></td>\n" .
3886 "<td>";
3887 # originally git_history used chop_str($co{'title'}, 50)
3888 print format_subject_html($co{'title'}, $co{'title_short'},
3889 href(action=>"commit", hash=>$commit), $ref);
3890 print "</td>\n" .
3891 "<td class=\"link\">" .
3892 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
3893 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
3895 if ($ftype eq 'blob') {
3896 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
3897 my $blob_parent = git_get_hash_by_path($commit, $file_name);
3898 if (defined $blob_current && defined $blob_parent &&
3899 $blob_current ne $blob_parent) {
3900 print " | " .
3901 $cgi->a({-href => href(action=>"blobdiff",
3902 hash=>$blob_current, hash_parent=>$blob_parent,
3903 hash_base=>$hash_base, hash_parent_base=>$commit,
3904 file_name=>$file_name)},
3905 "diff to current");
3908 print "</td>\n" .
3909 "</tr>\n";
3911 if (defined $extra) {
3912 print "<tr>\n" .
3913 "<td colspan=\"4\">$extra</td>\n" .
3914 "</tr>\n";
3916 print "</table>\n";
3919 sub git_tags_body {
3920 # uses global variable $project
3921 my ($taglist, $from, $to, $extra) = @_;
3922 $from = 0 unless defined $from;
3923 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
3925 print "<table class=\"tags\">\n";
3926 my $alternate = 1;
3927 for (my $i = $from; $i <= $to; $i++) {
3928 my $entry = $taglist->[$i];
3929 my %tag = %$entry;
3930 my $comment = $tag{'subject'};
3931 my $comment_short;
3932 if (defined $comment) {
3933 $comment_short = chop_str($comment, 30, 5);
3935 if ($alternate) {
3936 print "<tr class=\"dark\">\n";
3937 } else {
3938 print "<tr class=\"light\">\n";
3940 $alternate ^= 1;
3941 if (defined $tag{'age'}) {
3942 print "<td><i>$tag{'age'}</i></td>\n";
3943 } else {
3944 print "<td></td>\n";
3946 print "<td>" .
3947 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
3948 -class => "list name"}, esc_html($tag{'name'})) .
3949 "</td>\n" .
3950 "<td>";
3951 if (defined $comment) {
3952 print format_subject_html($comment, $comment_short,
3953 href(action=>"tag", hash=>$tag{'id'}));
3955 print "</td>\n" .
3956 "<td class=\"selflink\">";
3957 if ($tag{'type'} eq "tag") {
3958 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
3959 } else {
3960 print "&nbsp;";
3962 print "</td>\n" .
3963 "<td class=\"link\">" . " | " .
3964 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
3965 if ($tag{'reftype'} eq "commit") {
3966 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
3967 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
3968 } elsif ($tag{'reftype'} eq "blob") {
3969 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
3971 print "</td>\n" .
3972 "</tr>";
3974 if (defined $extra) {
3975 print "<tr>\n" .
3976 "<td colspan=\"5\">$extra</td>\n" .
3977 "</tr>\n";
3979 print "</table>\n";
3982 sub git_heads_body {
3983 # uses global variable $project
3984 my ($headlist, $head, $from, $to, $extra) = @_;
3985 $from = 0 unless defined $from;
3986 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
3988 print "<table class=\"heads\">\n";
3989 my $alternate = 1;
3990 for (my $i = $from; $i <= $to; $i++) {
3991 my $entry = $headlist->[$i];
3992 my %ref = %$entry;
3993 my $curr = $ref{'id'} eq $head;
3994 if ($alternate) {
3995 print "<tr class=\"dark\">\n";
3996 } else {
3997 print "<tr class=\"light\">\n";
3999 $alternate ^= 1;
4000 print "<td><i>$ref{'age'}</i></td>\n" .
4001 ($curr ? "<td class=\"current_head\">" : "<td>") .
4002 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4003 -class => "list name"},esc_html($ref{'name'})) .
4004 "</td>\n" .
4005 "<td class=\"link\">" .
4006 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4007 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4008 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4009 "</td>\n" .
4010 "</tr>";
4012 if (defined $extra) {
4013 print "<tr>\n" .
4014 "<td colspan=\"3\">$extra</td>\n" .
4015 "</tr>\n";
4017 print "</table>\n";
4020 sub git_search_grep_body {
4021 my ($commitlist, $from, $to, $extra) = @_;
4022 $from = 0 unless defined $from;
4023 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4025 print "<table class=\"commit_search\">\n";
4026 my $alternate = 1;
4027 for (my $i = $from; $i <= $to; $i++) {
4028 my %co = %{$commitlist->[$i]};
4029 if (!%co) {
4030 next;
4032 my $commit = $co{'id'};
4033 if ($alternate) {
4034 print "<tr class=\"dark\">\n";
4035 } else {
4036 print "<tr class=\"light\">\n";
4038 $alternate ^= 1;
4039 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4040 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4041 "<td><i>" . $author . "</i></td>\n" .
4042 "<td>" .
4043 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4044 -class => "list subject"},
4045 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4046 my $comment = $co{'comment'};
4047 foreach my $line (@$comment) {
4048 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4049 my ($lead, $match, $trail) = ($1, $2, $3);
4050 $match = chop_str($match, 70, 5, 'center');
4051 my $contextlen = int((80 - length($match))/2);
4052 $contextlen = 30 if ($contextlen > 30);
4053 $lead = chop_str($lead, $contextlen, 10, 'left');
4054 $trail = chop_str($trail, $contextlen, 10, 'right');
4056 $lead = esc_html($lead);
4057 $match = esc_html($match);
4058 $trail = esc_html($trail);
4060 print "$lead<span class=\"match\">$match</span>$trail<br />";
4063 print "</td>\n" .
4064 "<td class=\"link\">" .
4065 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4066 " | " .
4067 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4068 " | " .
4069 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4070 print "</td>\n" .
4071 "</tr>\n";
4073 if (defined $extra) {
4074 print "<tr>\n" .
4075 "<td colspan=\"3\">$extra</td>\n" .
4076 "</tr>\n";
4078 print "</table>\n";
4081 ## ======================================================================
4082 ## ======================================================================
4083 ## actions
4085 sub git_project_list {
4086 my $order = $cgi->param('o');
4087 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4088 die_error(400, "Unknown order parameter");
4091 my @list = git_get_projects_list();
4092 if (!@list) {
4093 die_error(404, "No projects found");
4096 git_header_html();
4097 if (-f $home_text) {
4098 print "<div class=\"index_include\">\n";
4099 open (my $fd, $home_text);
4100 print <$fd>;
4101 close $fd;
4102 print "</div>\n";
4104 git_project_list_body(\@list, $order);
4105 git_footer_html();
4108 sub git_forks {
4109 my $order = $cgi->param('o');
4110 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4111 die_error(400, "Unknown order parameter");
4114 my @list = git_get_projects_list($project);
4115 if (!@list) {
4116 die_error(404, "No forks found");
4119 git_header_html();
4120 git_print_page_nav('','');
4121 git_print_header_div('summary', "$project forks");
4122 git_project_list_body(\@list, $order);
4123 git_footer_html();
4126 sub git_project_index {
4127 my @projects = git_get_projects_list($project);
4129 print $cgi->header(
4130 -type => 'text/plain',
4131 -charset => 'utf-8',
4132 -content_disposition => 'inline; filename="index.aux"');
4134 foreach my $pr (@projects) {
4135 if (!exists $pr->{'owner'}) {
4136 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4139 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4140 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4141 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4142 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4143 $path =~ s/ /\+/g;
4144 $owner =~ s/ /\+/g;
4146 print "$path $owner\n";
4150 sub git_summary {
4151 my $descr = git_get_project_description($project) || "none";
4152 my %co = parse_commit("HEAD");
4153 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4154 my $head = $co{'id'};
4156 my $owner = git_get_project_owner($project);
4158 my $refs = git_get_references();
4159 # These get_*_list functions return one more to allow us to see if
4160 # there are more ...
4161 my @taglist = git_get_tags_list(16);
4162 my @headlist = git_get_heads_list(16);
4163 my @forklist;
4164 my ($check_forks) = gitweb_check_feature('forks');
4166 if ($check_forks) {
4167 @forklist = git_get_projects_list($project);
4170 git_header_html();
4171 git_print_page_nav('summary','', $head);
4173 print "<div class=\"title\">&nbsp;</div>\n";
4174 print "<table class=\"projects_list\">\n" .
4175 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4176 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4177 if (defined $cd{'rfc2822'}) {
4178 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4181 # use per project git URL list in $projectroot/$project/cloneurl
4182 # or make project git URL from git base URL and project name
4183 my $url_tag = "URL";
4184 my @url_list = git_get_project_url_list($project);
4185 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4186 foreach my $git_url (@url_list) {
4187 next unless $git_url;
4188 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4189 $url_tag = "";
4192 # Tag cloud
4193 my $show_ctags = (gitweb_check_feature('ctags'))[0];
4194 if ($show_ctags) {
4195 my $ctags = git_get_project_ctags($project);
4196 my $cloud = git_populate_project_tagcloud($ctags);
4197 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4198 print "</td>\n<td>" unless %$ctags;
4199 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4200 print "</td>\n<td>" if %$ctags;
4201 print git_show_project_tagcloud($cloud, 48);
4202 print "</td></tr>";
4205 print "</table>\n";
4207 if (-s "$projectroot/$project/README.html") {
4208 if (open my $fd, "$projectroot/$project/README.html") {
4209 print "<div class=\"title\">readme</div>\n" .
4210 "<div class=\"readme\">\n";
4211 print $_ while (<$fd>);
4212 print "\n</div>\n"; # class="readme"
4213 close $fd;
4217 # we need to request one more than 16 (0..15) to check if
4218 # those 16 are all
4219 my @commitlist = $head ? parse_commits($head, 17) : ();
4220 if (@commitlist) {
4221 git_print_header_div('shortlog');
4222 git_shortlog_body(\@commitlist, 0, 15, $refs,
4223 $#commitlist <= 15 ? undef :
4224 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4227 if (@taglist) {
4228 git_print_header_div('tags');
4229 git_tags_body(\@taglist, 0, 15,
4230 $#taglist <= 15 ? undef :
4231 $cgi->a({-href => href(action=>"tags")}, "..."));
4234 if (@headlist) {
4235 git_print_header_div('heads');
4236 git_heads_body(\@headlist, $head, 0, 15,
4237 $#headlist <= 15 ? undef :
4238 $cgi->a({-href => href(action=>"heads")}, "..."));
4241 if (@forklist) {
4242 git_print_header_div('forks');
4243 git_project_list_body(\@forklist, undef, 0, 15,
4244 $#forklist <= 15 ? undef :
4245 $cgi->a({-href => href(action=>"forks")}, "..."),
4246 'noheader');
4249 git_footer_html();
4252 sub git_tag {
4253 my $head = git_get_head_hash($project);
4254 git_header_html();
4255 git_print_page_nav('','', $head,undef,$head);
4256 my %tag = parse_tag($hash);
4258 if (! %tag) {
4259 die_error(404, "Unknown tag object");
4262 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4263 print "<div class=\"title_text\">\n" .
4264 "<table class=\"object_header\">\n" .
4265 "<tr>\n" .
4266 "<td>object</td>\n" .
4267 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4268 $tag{'object'}) . "</td>\n" .
4269 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4270 $tag{'type'}) . "</td>\n" .
4271 "</tr>\n";
4272 if (defined($tag{'author'})) {
4273 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4274 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4275 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4276 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4277 "</td></tr>\n";
4279 print "</table>\n\n" .
4280 "</div>\n";
4281 print "<div class=\"page_body\">";
4282 my $comment = $tag{'comment'};
4283 foreach my $line (@$comment) {
4284 chomp $line;
4285 print esc_html($line, -nbsp=>1) . "<br/>\n";
4287 print "</div>\n";
4288 git_footer_html();
4291 sub git_blame {
4292 my $fd;
4293 my $ftype;
4295 gitweb_check_feature('blame')
4296 or die_error(403, "Blame view not allowed");
4298 die_error(400, "No file name given") unless $file_name;
4299 $hash_base ||= git_get_head_hash($project);
4300 die_error(404, "Couldn't find base commit") unless ($hash_base);
4301 my %co = parse_commit($hash_base)
4302 or die_error(404, "Commit not found");
4303 if (!defined $hash) {
4304 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4305 or die_error(404, "Error looking up file");
4307 $ftype = git_get_type($hash);
4308 if ($ftype !~ "blob") {
4309 die_error(400, "Object is not a blob");
4311 open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4312 $file_name, $hash_base)
4313 or die_error(500, "Open git-blame failed");
4314 git_header_html();
4315 my $formats_nav =
4316 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4317 "blob") .
4318 " | " .
4319 $cgi->a({-href => href(action=>"history", -replay=>1)},
4320 "history") .
4321 " | " .
4322 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4323 "HEAD");
4324 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4325 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4326 git_print_page_path($file_name, $ftype, $hash_base);
4327 my @rev_color = (qw(light2 dark2));
4328 my $num_colors = scalar(@rev_color);
4329 my $current_color = 0;
4330 my $last_rev;
4331 print <<HTML;
4332 <div class="page_body">
4333 <table class="blame">
4334 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4335 HTML
4336 my %metainfo = ();
4337 while (1) {
4338 $_ = <$fd>;
4339 last unless defined $_;
4340 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4341 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4342 if (!exists $metainfo{$full_rev}) {
4343 $metainfo{$full_rev} = {};
4345 my $meta = $metainfo{$full_rev};
4346 while (<$fd>) {
4347 last if (s/^\t//);
4348 if (/^(\S+) (.*)$/) {
4349 $meta->{$1} = $2;
4352 my $data = $_;
4353 chomp $data;
4354 my $rev = substr($full_rev, 0, 8);
4355 my $author = $meta->{'author'};
4356 my %date = parse_date($meta->{'author-time'},
4357 $meta->{'author-tz'});
4358 my $date = $date{'iso-tz'};
4359 if ($group_size) {
4360 $current_color = ++$current_color % $num_colors;
4362 print "<tr class=\"$rev_color[$current_color]\">\n";
4363 if ($group_size) {
4364 print "<td class=\"sha1\"";
4365 print " title=\"". esc_html($author) . ", $date\"";
4366 print " rowspan=\"$group_size\"" if ($group_size > 1);
4367 print ">";
4368 print $cgi->a({-href => href(action=>"commit",
4369 hash=>$full_rev,
4370 file_name=>$file_name)},
4371 esc_html($rev));
4372 print "</td>\n";
4374 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4375 or die_error(500, "Open git-rev-parse failed");
4376 my $parent_commit = <$dd>;
4377 close $dd;
4378 chomp($parent_commit);
4379 my $blamed = href(action => 'blame',
4380 file_name => $meta->{'filename'},
4381 hash_base => $parent_commit);
4382 print "<td class=\"linenr\">";
4383 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4384 -id => "l$lineno",
4385 -class => "linenr" },
4386 esc_html($lineno));
4387 print "</td>";
4388 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4389 print "</tr>\n";
4391 print "</table>\n";
4392 print "</div>";
4393 close $fd
4394 or print "Reading blob failed\n";
4395 git_footer_html();
4398 sub git_tags {
4399 my $head = git_get_head_hash($project);
4400 git_header_html();
4401 git_print_page_nav('','', $head,undef,$head);
4402 git_print_header_div('summary', $project);
4404 my @tagslist = git_get_tags_list();
4405 if (@tagslist) {
4406 git_tags_body(\@tagslist);
4408 git_footer_html();
4411 sub git_heads {
4412 my $head = git_get_head_hash($project);
4413 git_header_html();
4414 git_print_page_nav('','', $head,undef,$head);
4415 git_print_header_div('summary', $project);
4417 my @headslist = git_get_heads_list();
4418 if (@headslist) {
4419 git_heads_body(\@headslist, $head);
4421 git_footer_html();
4424 sub git_blob_plain {
4425 my $type = shift;
4426 my $expires;
4428 if (!defined $hash) {
4429 if (defined $file_name) {
4430 my $base = $hash_base || git_get_head_hash($project);
4431 $hash = git_get_hash_by_path($base, $file_name, "blob")
4432 or die_error(404, "Cannot find file");
4433 } else {
4434 die_error(400, "No file name defined");
4436 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4437 # blobs defined by non-textual hash id's can be cached
4438 $expires = "+1d";
4441 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4442 or die_error(500, "Open git-cat-file blob '$hash' failed");
4444 # content-type (can include charset)
4445 $type = blob_contenttype($fd, $file_name, $type);
4447 # "save as" filename, even when no $file_name is given
4448 my $save_as = "$hash";
4449 if (defined $file_name) {
4450 $save_as = $file_name;
4451 } elsif ($type =~ m/^text\//) {
4452 $save_as .= '.txt';
4455 print $cgi->header(
4456 -type => $type,
4457 -expires => $expires,
4458 -content_disposition => 'inline; filename="' . $save_as . '"');
4459 undef $/;
4460 binmode STDOUT, ':raw';
4461 print <$fd>;
4462 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4463 $/ = "\n";
4464 close $fd;
4467 sub git_blob {
4468 my $expires;
4470 if (!defined $hash) {
4471 if (defined $file_name) {
4472 my $base = $hash_base || git_get_head_hash($project);
4473 $hash = git_get_hash_by_path($base, $file_name, "blob")
4474 or die_error(404, "Cannot find file");
4475 } else {
4476 die_error(400, "No file name defined");
4478 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4479 # blobs defined by non-textual hash id's can be cached
4480 $expires = "+1d";
4483 my ($have_blame) = gitweb_check_feature('blame');
4484 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4485 or die_error(500, "Couldn't cat $file_name, $hash");
4486 my $mimetype = blob_mimetype($fd, $file_name);
4487 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4488 close $fd;
4489 return git_blob_plain($mimetype);
4491 # we can have blame only for text/* mimetype
4492 $have_blame &&= ($mimetype =~ m!^text/!);
4494 git_header_html(undef, $expires);
4495 my $formats_nav = '';
4496 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4497 if (defined $file_name) {
4498 if ($have_blame) {
4499 $formats_nav .=
4500 $cgi->a({-href => href(action=>"blame", -replay=>1)},
4501 "blame") .
4502 " | ";
4504 $formats_nav .=
4505 $cgi->a({-href => href(action=>"history", -replay=>1)},
4506 "history") .
4507 " | " .
4508 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4509 "raw") .
4510 " | " .
4511 $cgi->a({-href => href(action=>"blob",
4512 hash_base=>"HEAD", file_name=>$file_name)},
4513 "HEAD");
4514 } else {
4515 $formats_nav .=
4516 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4517 "raw");
4519 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4520 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4521 } else {
4522 print "<div class=\"page_nav\">\n" .
4523 "<br/><br/></div>\n" .
4524 "<div class=\"title\">$hash</div>\n";
4526 git_print_page_path($file_name, "blob", $hash_base);
4527 print "<div class=\"page_body\">\n";
4528 if ($mimetype =~ m!^image/!) {
4529 print qq!<img type="$mimetype"!;
4530 if ($file_name) {
4531 print qq! alt="$file_name" title="$file_name"!;
4533 print qq! src="! .
4534 href(action=>"blob_plain", hash=>$hash,
4535 hash_base=>$hash_base, file_name=>$file_name) .
4536 qq!" />\n!;
4537 } else {
4538 my $nr;
4539 while (my $line = <$fd>) {
4540 chomp $line;
4541 $nr++;
4542 $line = untabify($line);
4543 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4544 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4547 close $fd
4548 or print "Reading blob failed.\n";
4549 print "</div>";
4550 git_footer_html();
4553 sub git_tree {
4554 if (!defined $hash_base) {
4555 $hash_base = "HEAD";
4557 if (!defined $hash) {
4558 if (defined $file_name) {
4559 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4560 } else {
4561 $hash = $hash_base;
4564 die_error(404, "No such tree") unless defined($hash);
4565 $/ = "\0";
4566 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4567 or die_error(500, "Open git-ls-tree failed");
4568 my @entries = map { chomp; $_ } <$fd>;
4569 close $fd or die_error(404, "Reading tree failed");
4570 $/ = "\n";
4572 my $refs = git_get_references();
4573 my $ref = format_ref_marker($refs, $hash_base);
4574 git_header_html();
4575 my $basedir = '';
4576 my ($have_blame) = gitweb_check_feature('blame');
4577 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4578 my @views_nav = ();
4579 if (defined $file_name) {
4580 push @views_nav,
4581 $cgi->a({-href => href(action=>"history", -replay=>1)},
4582 "history"),
4583 $cgi->a({-href => href(action=>"tree",
4584 hash_base=>"HEAD", file_name=>$file_name)},
4585 "HEAD"),
4587 my $snapshot_links = format_snapshot_links($hash);
4588 if (defined $snapshot_links) {
4589 # FIXME: Should be available when we have no hash base as well.
4590 push @views_nav, $snapshot_links;
4592 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4593 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4594 } else {
4595 undef $hash_base;
4596 print "<div class=\"page_nav\">\n";
4597 print "<br/><br/></div>\n";
4598 print "<div class=\"title\">$hash</div>\n";
4600 if (defined $file_name) {
4601 $basedir = $file_name;
4602 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4603 $basedir .= '/';
4605 git_print_page_path($file_name, 'tree', $hash_base);
4607 print "<div class=\"page_body\">\n";
4608 print "<table class=\"tree\">\n";
4609 my $alternate = 1;
4610 # '..' (top directory) link if possible
4611 if (defined $hash_base &&
4612 defined $file_name && $file_name =~ m![^/]+$!) {
4613 if ($alternate) {
4614 print "<tr class=\"dark\">\n";
4615 } else {
4616 print "<tr class=\"light\">\n";
4618 $alternate ^= 1;
4620 my $up = $file_name;
4621 $up =~ s!/?[^/]+$!!;
4622 undef $up unless $up;
4623 # based on git_print_tree_entry
4624 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4625 print '<td class="list">';
4626 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4627 file_name=>$up)},
4628 "..");
4629 print "</td>\n";
4630 print "<td class=\"link\"></td>\n";
4632 print "</tr>\n";
4634 foreach my $line (@entries) {
4635 my %t = parse_ls_tree_line($line, -z => 1);
4637 if ($alternate) {
4638 print "<tr class=\"dark\">\n";
4639 } else {
4640 print "<tr class=\"light\">\n";
4642 $alternate ^= 1;
4644 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4646 print "</tr>\n";
4648 print "</table>\n" .
4649 "</div>";
4650 git_footer_html();
4653 sub git_snapshot {
4654 my @supported_fmts = gitweb_check_feature('snapshot');
4655 @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4657 my $format = $cgi->param('sf');
4658 if (!@supported_fmts) {
4659 die_error(403, "Snapshots not allowed");
4661 # default to first supported snapshot format
4662 $format ||= $supported_fmts[0];
4663 if ($format !~ m/^[a-z0-9]+$/) {
4664 die_error(400, "Invalid snapshot format parameter");
4665 } elsif (!exists($known_snapshot_formats{$format})) {
4666 die_error(400, "Unknown snapshot format");
4667 } elsif (!grep($_ eq $format, @supported_fmts)) {
4668 die_error(403, "Unsupported snapshot format");
4671 if (!defined $hash) {
4672 $hash = git_get_head_hash($project);
4675 my $name = $project;
4676 $name =~ s,([^/])/*\.git$,$1,;
4677 $name = basename($name);
4678 my $filename = to_utf8($name);
4679 $name =~ s/\047/\047\\\047\047/g;
4680 my $cmd;
4681 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4682 $cmd = quote_command(
4683 git_cmd(), 'archive',
4684 "--format=$known_snapshot_formats{$format}{'format'}",
4685 "--prefix=$name/", $hash);
4686 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4687 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4690 print $cgi->header(
4691 -type => $known_snapshot_formats{$format}{'type'},
4692 -content_disposition => 'inline; filename="' . "$filename" . '"',
4693 -status => '200 OK');
4695 open my $fd, "-|", $cmd
4696 or die_error(500, "Execute git-archive failed");
4697 binmode STDOUT, ':raw';
4698 print <$fd>;
4699 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4700 close $fd;
4703 sub git_log {
4704 my $head = git_get_head_hash($project);
4705 if (!defined $hash) {
4706 $hash = $head;
4708 if (!defined $page) {
4709 $page = 0;
4711 my $refs = git_get_references();
4713 my @commitlist = parse_commits($hash, 101, (100 * $page));
4715 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4717 git_header_html();
4718 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4720 if (!@commitlist) {
4721 my %co = parse_commit($hash);
4723 git_print_header_div('summary', $project);
4724 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4726 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4727 for (my $i = 0; $i <= $to; $i++) {
4728 my %co = %{$commitlist[$i]};
4729 next if !%co;
4730 my $commit = $co{'id'};
4731 my $ref = format_ref_marker($refs, $commit);
4732 my %ad = parse_date($co{'author_epoch'});
4733 git_print_header_div('commit',
4734 "<span class=\"age\">$co{'age_string'}</span>" .
4735 esc_html($co{'title'}) . $ref,
4736 $commit);
4737 print "<div class=\"title_text\">\n" .
4738 "<div class=\"log_link\">\n" .
4739 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4740 " | " .
4741 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4742 " | " .
4743 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4744 "<br/>\n" .
4745 "</div>\n" .
4746 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4747 "</div>\n";
4749 print "<div class=\"log_body\">\n";
4750 git_print_log($co{'comment'}, -final_empty_line=> 1);
4751 print "</div>\n";
4753 if ($#commitlist >= 100) {
4754 print "<div class=\"page_nav\">\n";
4755 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4756 -accesskey => "n", -title => "Alt-n"}, "next");
4757 print "</div>\n";
4759 git_footer_html();
4762 sub git_commit {
4763 $hash ||= $hash_base || "HEAD";
4764 my %co = parse_commit($hash)
4765 or die_error(404, "Unknown commit object");
4766 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4767 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4769 my $parent = $co{'parent'};
4770 my $parents = $co{'parents'}; # listref
4772 # we need to prepare $formats_nav before any parameter munging
4773 my $formats_nav;
4774 if (!defined $parent) {
4775 # --root commitdiff
4776 $formats_nav .= '(initial)';
4777 } elsif (@$parents == 1) {
4778 # single parent commit
4779 $formats_nav .=
4780 '(parent: ' .
4781 $cgi->a({-href => href(action=>"commit",
4782 hash=>$parent)},
4783 esc_html(substr($parent, 0, 7))) .
4784 ')';
4785 } else {
4786 # merge commit
4787 $formats_nav .=
4788 '(merge: ' .
4789 join(' ', map {
4790 $cgi->a({-href => href(action=>"commit",
4791 hash=>$_)},
4792 esc_html(substr($_, 0, 7)));
4793 } @$parents ) .
4794 ')';
4797 if (!defined $parent) {
4798 $parent = "--root";
4800 my @difftree;
4801 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4802 @diff_opts,
4803 (@$parents <= 1 ? $parent : '-c'),
4804 $hash, "--"
4805 or die_error(500, "Open git-diff-tree failed");
4806 @difftree = map { chomp; $_ } <$fd>;
4807 close $fd or die_error(404, "Reading git-diff-tree failed");
4809 # non-textual hash id's can be cached
4810 my $expires;
4811 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4812 $expires = "+1d";
4814 my $refs = git_get_references();
4815 my $ref = format_ref_marker($refs, $co{'id'});
4817 git_header_html(undef, $expires);
4818 git_print_page_nav('commit', '',
4819 $hash, $co{'tree'}, $hash,
4820 $formats_nav);
4822 if (defined $co{'parent'}) {
4823 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4824 } else {
4825 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4827 print "<div class=\"title_text\">\n" .
4828 "<table class=\"object_header\">\n";
4829 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4830 "<tr>" .
4831 "<td></td><td> $ad{'rfc2822'}";
4832 if ($ad{'hour_local'} < 6) {
4833 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4834 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4835 } else {
4836 printf(" (%02d:%02d %s)",
4837 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4839 print "</td>" .
4840 "</tr>\n";
4841 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4842 print "<tr><td></td><td> $cd{'rfc2822'}" .
4843 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4844 "</td></tr>\n";
4845 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4846 print "<tr>" .
4847 "<td>tree</td>" .
4848 "<td class=\"sha1\">" .
4849 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4850 class => "list"}, $co{'tree'}) .
4851 "</td>" .
4852 "<td class=\"link\">" .
4853 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4854 "tree");
4855 my $snapshot_links = format_snapshot_links($hash);
4856 if (defined $snapshot_links) {
4857 print " | " . $snapshot_links;
4859 print "</td>" .
4860 "</tr>\n";
4862 foreach my $par (@$parents) {
4863 print "<tr>" .
4864 "<td>parent</td>" .
4865 "<td class=\"sha1\">" .
4866 $cgi->a({-href => href(action=>"commit", hash=>$par),
4867 class => "list"}, $par) .
4868 "</td>" .
4869 "<td class=\"link\">" .
4870 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4871 " | " .
4872 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4873 "</td>" .
4874 "</tr>\n";
4876 print "</table>".
4877 "</div>\n";
4879 print "<div class=\"page_body\">\n";
4880 git_print_log($co{'comment'});
4881 print "</div>\n";
4883 git_difftree_body(\@difftree, $hash, @$parents);
4885 git_footer_html();
4888 sub git_object {
4889 # object is defined by:
4890 # - hash or hash_base alone
4891 # - hash_base and file_name
4892 my $type;
4894 # - hash or hash_base alone
4895 if ($hash || ($hash_base && !defined $file_name)) {
4896 my $object_id = $hash || $hash_base;
4898 open my $fd, "-|", quote_command(
4899 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
4900 or die_error(404, "Object does not exist");
4901 $type = <$fd>;
4902 chomp $type;
4903 close $fd
4904 or die_error(404, "Object does not exist");
4906 # - hash_base and file_name
4907 } elsif ($hash_base && defined $file_name) {
4908 $file_name =~ s,/+$,,;
4910 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
4911 or die_error(404, "Base object does not exist");
4913 # here errors should not hapen
4914 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
4915 or die_error(500, "Open git-ls-tree failed");
4916 my $line = <$fd>;
4917 close $fd;
4919 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4920 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
4921 die_error(404, "File or directory for given base does not exist");
4923 $type = $2;
4924 $hash = $3;
4925 } else {
4926 die_error(400, "Not enough information to find object");
4929 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
4930 hash=>$hash, hash_base=>$hash_base,
4931 file_name=>$file_name),
4932 -status => '302 Found');
4935 sub git_blobdiff {
4936 my $format = shift || 'html';
4938 my $fd;
4939 my @difftree;
4940 my %diffinfo;
4941 my $expires;
4943 # preparing $fd and %diffinfo for git_patchset_body
4944 # new style URI
4945 if (defined $hash_base && defined $hash_parent_base) {
4946 if (defined $file_name) {
4947 # read raw output
4948 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4949 $hash_parent_base, $hash_base,
4950 "--", (defined $file_parent ? $file_parent : ()), $file_name
4951 or die_error(500, "Open git-diff-tree failed");
4952 @difftree = map { chomp; $_ } <$fd>;
4953 close $fd
4954 or die_error(404, "Reading git-diff-tree failed");
4955 @difftree
4956 or die_error(404, "Blob diff not found");
4958 } elsif (defined $hash &&
4959 $hash =~ /[0-9a-fA-F]{40}/) {
4960 # try to find filename from $hash
4962 # read filtered raw output
4963 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4964 $hash_parent_base, $hash_base, "--"
4965 or die_error(500, "Open git-diff-tree failed");
4966 @difftree =
4967 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
4968 # $hash == to_id
4969 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
4970 map { chomp; $_ } <$fd>;
4971 close $fd
4972 or die_error(404, "Reading git-diff-tree failed");
4973 @difftree
4974 or die_error(404, "Blob diff not found");
4976 } else {
4977 die_error(400, "Missing one of the blob diff parameters");
4980 if (@difftree > 1) {
4981 die_error(400, "Ambiguous blob diff specification");
4984 %diffinfo = parse_difftree_raw_line($difftree[0]);
4985 $file_parent ||= $diffinfo{'from_file'} || $file_name;
4986 $file_name ||= $diffinfo{'to_file'};
4988 $hash_parent ||= $diffinfo{'from_id'};
4989 $hash ||= $diffinfo{'to_id'};
4991 # non-textual hash id's can be cached
4992 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
4993 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
4994 $expires = '+1d';
4997 # open patch output
4998 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
4999 '-p', ($format eq 'html' ? "--full-index" : ()),
5000 $hash_parent_base, $hash_base,
5001 "--", (defined $file_parent ? $file_parent : ()), $file_name
5002 or die_error(500, "Open git-diff-tree failed");
5005 # old/legacy style URI
5006 if (!%diffinfo && # if new style URI failed
5007 defined $hash && defined $hash_parent) {
5008 # fake git-diff-tree raw output
5009 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5010 $diffinfo{'from_id'} = $hash_parent;
5011 $diffinfo{'to_id'} = $hash;
5012 if (defined $file_name) {
5013 if (defined $file_parent) {
5014 $diffinfo{'status'} = '2';
5015 $diffinfo{'from_file'} = $file_parent;
5016 $diffinfo{'to_file'} = $file_name;
5017 } else { # assume not renamed
5018 $diffinfo{'status'} = '1';
5019 $diffinfo{'from_file'} = $file_name;
5020 $diffinfo{'to_file'} = $file_name;
5022 } else { # no filename given
5023 $diffinfo{'status'} = '2';
5024 $diffinfo{'from_file'} = $hash_parent;
5025 $diffinfo{'to_file'} = $hash;
5028 # non-textual hash id's can be cached
5029 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5030 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5031 $expires = '+1d';
5034 # open patch output
5035 open $fd, "-|", git_cmd(), "diff", @diff_opts,
5036 '-p', ($format eq 'html' ? "--full-index" : ()),
5037 $hash_parent, $hash, "--"
5038 or die_error(500, "Open git-diff failed");
5039 } else {
5040 die_error(400, "Missing one of the blob diff parameters")
5041 unless %diffinfo;
5044 # header
5045 if ($format eq 'html') {
5046 my $formats_nav =
5047 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5048 "raw");
5049 git_header_html(undef, $expires);
5050 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5051 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5052 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5053 } else {
5054 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5055 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5057 if (defined $file_name) {
5058 git_print_page_path($file_name, "blob", $hash_base);
5059 } else {
5060 print "<div class=\"page_path\"></div>\n";
5063 } elsif ($format eq 'plain') {
5064 print $cgi->header(
5065 -type => 'text/plain',
5066 -charset => 'utf-8',
5067 -expires => $expires,
5068 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5070 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5072 } else {
5073 die_error(400, "Unknown blobdiff format");
5076 # patch
5077 if ($format eq 'html') {
5078 print "<div class=\"page_body\">\n";
5080 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5081 close $fd;
5083 print "</div>\n"; # class="page_body"
5084 git_footer_html();
5086 } else {
5087 while (my $line = <$fd>) {
5088 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5089 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5091 print $line;
5093 last if $line =~ m!^\+\+\+!;
5095 local $/ = undef;
5096 print <$fd>;
5097 close $fd;
5101 sub git_blobdiff_plain {
5102 git_blobdiff('plain');
5105 sub git_commitdiff {
5106 my $format = shift || 'html';
5107 $hash ||= $hash_base || "HEAD";
5108 my %co = parse_commit($hash)
5109 or die_error(404, "Unknown commit object");
5111 # choose format for commitdiff for merge
5112 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5113 $hash_parent = '--cc';
5115 # we need to prepare $formats_nav before almost any parameter munging
5116 my $formats_nav;
5117 if ($format eq 'html') {
5118 $formats_nav =
5119 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5120 "raw");
5122 if (defined $hash_parent &&
5123 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5124 # commitdiff with two commits given
5125 my $hash_parent_short = $hash_parent;
5126 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5127 $hash_parent_short = substr($hash_parent, 0, 7);
5129 $formats_nav .=
5130 ' (from';
5131 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5132 if ($co{'parents'}[$i] eq $hash_parent) {
5133 $formats_nav .= ' parent ' . ($i+1);
5134 last;
5137 $formats_nav .= ': ' .
5138 $cgi->a({-href => href(action=>"commitdiff",
5139 hash=>$hash_parent)},
5140 esc_html($hash_parent_short)) .
5141 ')';
5142 } elsif (!$co{'parent'}) {
5143 # --root commitdiff
5144 $formats_nav .= ' (initial)';
5145 } elsif (scalar @{$co{'parents'}} == 1) {
5146 # single parent commit
5147 $formats_nav .=
5148 ' (parent: ' .
5149 $cgi->a({-href => href(action=>"commitdiff",
5150 hash=>$co{'parent'})},
5151 esc_html(substr($co{'parent'}, 0, 7))) .
5152 ')';
5153 } else {
5154 # merge commit
5155 if ($hash_parent eq '--cc') {
5156 $formats_nav .= ' | ' .
5157 $cgi->a({-href => href(action=>"commitdiff",
5158 hash=>$hash, hash_parent=>'-c')},
5159 'combined');
5160 } else { # $hash_parent eq '-c'
5161 $formats_nav .= ' | ' .
5162 $cgi->a({-href => href(action=>"commitdiff",
5163 hash=>$hash, hash_parent=>'--cc')},
5164 'compact');
5166 $formats_nav .=
5167 ' (merge: ' .
5168 join(' ', map {
5169 $cgi->a({-href => href(action=>"commitdiff",
5170 hash=>$_)},
5171 esc_html(substr($_, 0, 7)));
5172 } @{$co{'parents'}} ) .
5173 ')';
5177 my $hash_parent_param = $hash_parent;
5178 if (!defined $hash_parent_param) {
5179 # --cc for multiple parents, --root for parentless
5180 $hash_parent_param =
5181 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5184 # read commitdiff
5185 my $fd;
5186 my @difftree;
5187 if ($format eq 'html') {
5188 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5189 "--no-commit-id", "--patch-with-raw", "--full-index",
5190 $hash_parent_param, $hash, "--"
5191 or die_error(500, "Open git-diff-tree failed");
5193 while (my $line = <$fd>) {
5194 chomp $line;
5195 # empty line ends raw part of diff-tree output
5196 last unless $line;
5197 push @difftree, scalar parse_difftree_raw_line($line);
5200 } elsif ($format eq 'plain') {
5201 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5202 '-p', $hash_parent_param, $hash, "--"
5203 or die_error(500, "Open git-diff-tree failed");
5205 } else {
5206 die_error(400, "Unknown commitdiff format");
5209 # non-textual hash id's can be cached
5210 my $expires;
5211 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5212 $expires = "+1d";
5215 # write commit message
5216 if ($format eq 'html') {
5217 my $refs = git_get_references();
5218 my $ref = format_ref_marker($refs, $co{'id'});
5220 git_header_html(undef, $expires);
5221 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5222 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5223 git_print_authorship(\%co);
5224 print "<div class=\"page_body\">\n";
5225 if (@{$co{'comment'}} > 1) {
5226 print "<div class=\"log\">\n";
5227 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5228 print "</div>\n"; # class="log"
5231 } elsif ($format eq 'plain') {
5232 my $refs = git_get_references("tags");
5233 my $tagname = git_get_rev_name_tags($hash);
5234 my $filename = basename($project) . "-$hash.patch";
5236 print $cgi->header(
5237 -type => 'text/plain',
5238 -charset => 'utf-8',
5239 -expires => $expires,
5240 -content_disposition => 'inline; filename="' . "$filename" . '"');
5241 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5242 print "From: " . to_utf8($co{'author'}) . "\n";
5243 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5244 print "Subject: " . to_utf8($co{'title'}) . "\n";
5246 print "X-Git-Tag: $tagname\n" if $tagname;
5247 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5249 foreach my $line (@{$co{'comment'}}) {
5250 print to_utf8($line) . "\n";
5252 print "---\n\n";
5255 # write patch
5256 if ($format eq 'html') {
5257 my $use_parents = !defined $hash_parent ||
5258 $hash_parent eq '-c' || $hash_parent eq '--cc';
5259 git_difftree_body(\@difftree, $hash,
5260 $use_parents ? @{$co{'parents'}} : $hash_parent);
5261 print "<br/>\n";
5263 git_patchset_body($fd, \@difftree, $hash,
5264 $use_parents ? @{$co{'parents'}} : $hash_parent);
5265 close $fd;
5266 print "</div>\n"; # class="page_body"
5267 git_footer_html();
5269 } elsif ($format eq 'plain') {
5270 local $/ = undef;
5271 print <$fd>;
5272 close $fd
5273 or print "Reading git-diff-tree failed\n";
5277 sub git_commitdiff_plain {
5278 git_commitdiff('plain');
5281 sub git_history {
5282 if (!defined $hash_base) {
5283 $hash_base = git_get_head_hash($project);
5285 if (!defined $page) {
5286 $page = 0;
5288 my $ftype;
5289 my %co = parse_commit($hash_base)
5290 or die_error(404, "Unknown commit object");
5292 my $refs = git_get_references();
5293 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5295 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5296 $file_name, "--full-history")
5297 or die_error(404, "No such file or directory on given branch");
5299 if (!defined $hash && defined $file_name) {
5300 # some commits could have deleted file in question,
5301 # and not have it in tree, but one of them has to have it
5302 for (my $i = 0; $i <= @commitlist; $i++) {
5303 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5304 last if defined $hash;
5307 if (defined $hash) {
5308 $ftype = git_get_type($hash);
5310 if (!defined $ftype) {
5311 die_error(500, "Unknown type of object");
5314 my $paging_nav = '';
5315 if ($page > 0) {
5316 $paging_nav .=
5317 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5318 file_name=>$file_name)},
5319 "first");
5320 $paging_nav .= " &sdot; " .
5321 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5322 -accesskey => "p", -title => "Alt-p"}, "prev");
5323 } else {
5324 $paging_nav .= "first";
5325 $paging_nav .= " &sdot; prev";
5327 my $next_link = '';
5328 if ($#commitlist >= 100) {
5329 $next_link =
5330 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5331 -accesskey => "n", -title => "Alt-n"}, "next");
5332 $paging_nav .= " &sdot; $next_link";
5333 } else {
5334 $paging_nav .= " &sdot; next";
5337 git_header_html();
5338 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5339 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5340 git_print_page_path($file_name, $ftype, $hash_base);
5342 git_history_body(\@commitlist, 0, 99,
5343 $refs, $hash_base, $ftype, $next_link);
5345 git_footer_html();
5348 sub git_search {
5349 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5350 if (!defined $searchtext) {
5351 die_error(400, "Text field is empty");
5353 if (!defined $hash) {
5354 $hash = git_get_head_hash($project);
5356 my %co = parse_commit($hash);
5357 if (!%co) {
5358 die_error(404, "Unknown commit object");
5360 if (!defined $page) {
5361 $page = 0;
5364 $searchtype ||= 'commit';
5365 if ($searchtype eq 'pickaxe') {
5366 # pickaxe may take all resources of your box and run for several minutes
5367 # with every query - so decide by yourself how public you make this feature
5368 gitweb_check_feature('pickaxe')
5369 or die_error(403, "Pickaxe is disabled");
5371 if ($searchtype eq 'grep') {
5372 gitweb_check_feature('grep')
5373 or die_error(403, "Grep is disabled");
5376 git_header_html();
5378 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5379 my $greptype;
5380 if ($searchtype eq 'commit') {
5381 $greptype = "--grep=";
5382 } elsif ($searchtype eq 'author') {
5383 $greptype = "--author=";
5384 } elsif ($searchtype eq 'committer') {
5385 $greptype = "--committer=";
5387 $greptype .= $searchtext;
5388 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5389 $greptype, '--regexp-ignore-case',
5390 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5392 my $paging_nav = '';
5393 if ($page > 0) {
5394 $paging_nav .=
5395 $cgi->a({-href => href(action=>"search", hash=>$hash,
5396 searchtext=>$searchtext,
5397 searchtype=>$searchtype)},
5398 "first");
5399 $paging_nav .= " &sdot; " .
5400 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5401 -accesskey => "p", -title => "Alt-p"}, "prev");
5402 } else {
5403 $paging_nav .= "first";
5404 $paging_nav .= " &sdot; prev";
5406 my $next_link = '';
5407 if ($#commitlist >= 100) {
5408 $next_link =
5409 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5410 -accesskey => "n", -title => "Alt-n"}, "next");
5411 $paging_nav .= " &sdot; $next_link";
5412 } else {
5413 $paging_nav .= " &sdot; next";
5416 if ($#commitlist >= 100) {
5419 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5420 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5421 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5424 if ($searchtype eq 'pickaxe') {
5425 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5426 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5428 print "<table class=\"pickaxe search\">\n";
5429 my $alternate = 1;
5430 $/ = "\n";
5431 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5432 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5433 ($search_use_regexp ? '--pickaxe-regex' : ());
5434 undef %co;
5435 my @files;
5436 while (my $line = <$fd>) {
5437 chomp $line;
5438 next unless $line;
5440 my %set = parse_difftree_raw_line($line);
5441 if (defined $set{'commit'}) {
5442 # finish previous commit
5443 if (%co) {
5444 print "</td>\n" .
5445 "<td class=\"link\">" .
5446 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5447 " | " .
5448 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5449 print "</td>\n" .
5450 "</tr>\n";
5453 if ($alternate) {
5454 print "<tr class=\"dark\">\n";
5455 } else {
5456 print "<tr class=\"light\">\n";
5458 $alternate ^= 1;
5459 %co = parse_commit($set{'commit'});
5460 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5461 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5462 "<td><i>$author</i></td>\n" .
5463 "<td>" .
5464 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5465 -class => "list subject"},
5466 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5467 } elsif (defined $set{'to_id'}) {
5468 next if ($set{'to_id'} =~ m/^0{40}$/);
5470 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5471 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5472 -class => "list"},
5473 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5474 "<br/>\n";
5477 close $fd;
5479 # finish last commit (warning: repetition!)
5480 if (%co) {
5481 print "</td>\n" .
5482 "<td class=\"link\">" .
5483 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5484 " | " .
5485 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5486 print "</td>\n" .
5487 "</tr>\n";
5490 print "</table>\n";
5493 if ($searchtype eq 'grep') {
5494 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5495 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5497 print "<table class=\"grep_search\">\n";
5498 my $alternate = 1;
5499 my $matches = 0;
5500 $/ = "\n";
5501 open my $fd, "-|", git_cmd(), 'grep', '-n',
5502 $search_use_regexp ? ('-E', '-i') : '-F',
5503 $searchtext, $co{'tree'};
5504 my $lastfile = '';
5505 while (my $line = <$fd>) {
5506 chomp $line;
5507 my ($file, $lno, $ltext, $binary);
5508 last if ($matches++ > 1000);
5509 if ($line =~ /^Binary file (.+) matches$/) {
5510 $file = $1;
5511 $binary = 1;
5512 } else {
5513 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5515 if ($file ne $lastfile) {
5516 $lastfile and print "</td></tr>\n";
5517 if ($alternate++) {
5518 print "<tr class=\"dark\">\n";
5519 } else {
5520 print "<tr class=\"light\">\n";
5522 print "<td class=\"list\">".
5523 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5524 file_name=>"$file"),
5525 -class => "list"}, esc_path($file));
5526 print "</td><td>\n";
5527 $lastfile = $file;
5529 if ($binary) {
5530 print "<div class=\"binary\">Binary file</div>\n";
5531 } else {
5532 $ltext = untabify($ltext);
5533 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5534 $ltext = esc_html($1, -nbsp=>1);
5535 $ltext .= '<span class="match">';
5536 $ltext .= esc_html($2, -nbsp=>1);
5537 $ltext .= '</span>';
5538 $ltext .= esc_html($3, -nbsp=>1);
5539 } else {
5540 $ltext = esc_html($ltext, -nbsp=>1);
5542 print "<div class=\"pre\">" .
5543 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5544 file_name=>"$file").'#l'.$lno,
5545 -class => "linenr"}, sprintf('%4i', $lno))
5546 . ' ' . $ltext . "</div>\n";
5549 if ($lastfile) {
5550 print "</td></tr>\n";
5551 if ($matches > 1000) {
5552 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5554 } else {
5555 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5557 close $fd;
5559 print "</table>\n";
5561 git_footer_html();
5564 sub git_search_help {
5565 git_header_html();
5566 git_print_page_nav('','', $hash,$hash,$hash);
5567 print <<EOT;
5568 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5569 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5570 the pattern entered is recognized as the POSIX extended
5571 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5572 insensitive).</p>
5573 <dl>
5574 <dt><b>commit</b></dt>
5575 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5577 my ($have_grep) = gitweb_check_feature('grep');
5578 if ($have_grep) {
5579 print <<EOT;
5580 <dt><b>grep</b></dt>
5581 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5582 a different one) are searched for the given pattern. On large trees, this search can take
5583 a while and put some strain on the server, so please use it with some consideration. Note that
5584 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5585 case-sensitive.</dd>
5588 print <<EOT;
5589 <dt><b>author</b></dt>
5590 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5591 <dt><b>committer</b></dt>
5592 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5594 my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5595 if ($have_pickaxe) {
5596 print <<EOT;
5597 <dt><b>pickaxe</b></dt>
5598 <dd>All commits that caused the string to appear or disappear from any file (changes that
5599 added, removed or "modified" the string) will be listed. This search can take a while and
5600 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5601 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5604 print "</dl>\n";
5605 git_footer_html();
5608 sub git_shortlog {
5609 my $head = git_get_head_hash($project);
5610 if (!defined $hash) {
5611 $hash = $head;
5613 if (!defined $page) {
5614 $page = 0;
5616 my $refs = git_get_references();
5618 my $commit_hash = $hash;
5619 if (defined $hash_parent) {
5620 $commit_hash = "$hash_parent..$hash";
5622 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5624 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5625 my $next_link = '';
5626 if ($#commitlist >= 100) {
5627 $next_link =
5628 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5629 -accesskey => "n", -title => "Alt-n"}, "next");
5632 git_header_html();
5633 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5634 git_print_header_div('summary', $project);
5636 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5638 git_footer_html();
5641 ## ......................................................................
5642 ## feeds (RSS, Atom; OPML)
5644 sub git_feed {
5645 my $format = shift || 'atom';
5646 my ($have_blame) = gitweb_check_feature('blame');
5648 # Atom: http://www.atomenabled.org/developers/syndication/
5649 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5650 if ($format ne 'rss' && $format ne 'atom') {
5651 die_error(400, "Unknown web feed format");
5654 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5655 my $head = $hash || 'HEAD';
5656 my @commitlist = parse_commits($head, 150, 0, $file_name);
5658 my %latest_commit;
5659 my %latest_date;
5660 my $content_type = "application/$format+xml";
5661 if (defined $cgi->http('HTTP_ACCEPT') &&
5662 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5663 # browser (feed reader) prefers text/xml
5664 $content_type = 'text/xml';
5666 if (defined($commitlist[0])) {
5667 %latest_commit = %{$commitlist[0]};
5668 %latest_date = parse_date($latest_commit{'author_epoch'});
5669 print $cgi->header(
5670 -type => $content_type,
5671 -charset => 'utf-8',
5672 -last_modified => $latest_date{'rfc2822'});
5673 } else {
5674 print $cgi->header(
5675 -type => $content_type,
5676 -charset => 'utf-8');
5679 # Optimization: skip generating the body if client asks only
5680 # for Last-Modified date.
5681 return if ($cgi->request_method() eq 'HEAD');
5683 # header variables
5684 my $title = "$site_name - $project/$action";
5685 my $feed_type = 'log';
5686 if (defined $hash) {
5687 $title .= " - '$hash'";
5688 $feed_type = 'branch log';
5689 if (defined $file_name) {
5690 $title .= " :: $file_name";
5691 $feed_type = 'history';
5693 } elsif (defined $file_name) {
5694 $title .= " - $file_name";
5695 $feed_type = 'history';
5697 $title .= " $feed_type";
5698 my $descr = git_get_project_description($project);
5699 if (defined $descr) {
5700 $descr = esc_html($descr);
5701 } else {
5702 $descr = "$project " .
5703 ($format eq 'rss' ? 'RSS' : 'Atom') .
5704 " feed";
5706 my $owner = git_get_project_owner($project);
5707 $owner = esc_html($owner);
5709 #header
5710 my $alt_url;
5711 if (defined $file_name) {
5712 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5713 } elsif (defined $hash) {
5714 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5715 } else {
5716 $alt_url = href(-full=>1, action=>"summary");
5718 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5719 if ($format eq 'rss') {
5720 print <<XML;
5721 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5722 <channel>
5724 print "<title>$title</title>\n" .
5725 "<link>$alt_url</link>\n" .
5726 "<description>$descr</description>\n" .
5727 "<language>en</language>\n";
5728 } elsif ($format eq 'atom') {
5729 print <<XML;
5730 <feed xmlns="http://www.w3.org/2005/Atom">
5732 print "<title>$title</title>\n" .
5733 "<subtitle>$descr</subtitle>\n" .
5734 '<link rel="alternate" type="text/html" href="' .
5735 $alt_url . '" />' . "\n" .
5736 '<link rel="self" type="' . $content_type . '" href="' .
5737 $cgi->self_url() . '" />' . "\n" .
5738 "<id>" . href(-full=>1) . "</id>\n" .
5739 # use project owner for feed author
5740 "<author><name>$owner</name></author>\n";
5741 if (defined $favicon) {
5742 print "<icon>" . esc_url($favicon) . "</icon>\n";
5744 if (defined $logo_url) {
5745 # not twice as wide as tall: 72 x 27 pixels
5746 print "<logo>" . esc_url($logo) . "</logo>\n";
5748 if (! %latest_date) {
5749 # dummy date to keep the feed valid until commits trickle in:
5750 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5751 } else {
5752 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5756 # contents
5757 for (my $i = 0; $i <= $#commitlist; $i++) {
5758 my %co = %{$commitlist[$i]};
5759 my $commit = $co{'id'};
5760 # we read 150, we always show 30 and the ones more recent than 48 hours
5761 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5762 last;
5764 my %cd = parse_date($co{'author_epoch'});
5766 # get list of changed files
5767 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5768 $co{'parent'} || "--root",
5769 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5770 or next;
5771 my @difftree = map { chomp; $_ } <$fd>;
5772 close $fd
5773 or next;
5775 # print element (entry, item)
5776 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5777 if ($format eq 'rss') {
5778 print "<item>\n" .
5779 "<title>" . esc_html($co{'title'}) . "</title>\n" .
5780 "<author>" . esc_html($co{'author'}) . "</author>\n" .
5781 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5782 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5783 "<link>$co_url</link>\n" .
5784 "<description>" . esc_html($co{'title'}) . "</description>\n" .
5785 "<content:encoded>" .
5786 "<![CDATA[\n";
5787 } elsif ($format eq 'atom') {
5788 print "<entry>\n" .
5789 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5790 "<updated>$cd{'iso-8601'}</updated>\n" .
5791 "<author>\n" .
5792 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
5793 if ($co{'author_email'}) {
5794 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
5796 print "</author>\n" .
5797 # use committer for contributor
5798 "<contributor>\n" .
5799 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5800 if ($co{'committer_email'}) {
5801 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5803 print "</contributor>\n" .
5804 "<published>$cd{'iso-8601'}</published>\n" .
5805 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5806 "<id>$co_url</id>\n" .
5807 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5808 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5810 my $comment = $co{'comment'};
5811 print "<pre>\n";
5812 foreach my $line (@$comment) {
5813 $line = esc_html($line);
5814 print "$line\n";
5816 print "</pre><ul>\n";
5817 foreach my $difftree_line (@difftree) {
5818 my %difftree = parse_difftree_raw_line($difftree_line);
5819 next if !$difftree{'from_id'};
5821 my $file = $difftree{'file'} || $difftree{'to_file'};
5823 print "<li>" .
5824 "[" .
5825 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5826 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5827 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5828 file_name=>$file, file_parent=>$difftree{'from_file'}),
5829 -title => "diff"}, 'D');
5830 if ($have_blame) {
5831 print $cgi->a({-href => href(-full=>1, action=>"blame",
5832 file_name=>$file, hash_base=>$commit),
5833 -title => "blame"}, 'B');
5835 # if this is not a feed of a file history
5836 if (!defined $file_name || $file_name ne $file) {
5837 print $cgi->a({-href => href(-full=>1, action=>"history",
5838 file_name=>$file, hash=>$commit),
5839 -title => "history"}, 'H');
5841 $file = esc_path($file);
5842 print "] ".
5843 "$file</li>\n";
5845 if ($format eq 'rss') {
5846 print "</ul>]]>\n" .
5847 "</content:encoded>\n" .
5848 "</item>\n";
5849 } elsif ($format eq 'atom') {
5850 print "</ul>\n</div>\n" .
5851 "</content>\n" .
5852 "</entry>\n";
5856 # end of feed
5857 if ($format eq 'rss') {
5858 print "</channel>\n</rss>\n";
5859 } elsif ($format eq 'atom') {
5860 print "</feed>\n";
5864 sub git_rss {
5865 git_feed('rss');
5868 sub git_atom {
5869 git_feed('atom');
5872 sub git_opml {
5873 my @list = git_get_projects_list();
5875 print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5876 print <<XML;
5877 <?xml version="1.0" encoding="utf-8"?>
5878 <opml version="1.0">
5879 <head>
5880 <title>$site_name OPML Export</title>
5881 </head>
5882 <body>
5883 <outline text="git RSS feeds">
5886 foreach my $pr (@list) {
5887 my %proj = %$pr;
5888 my $head = git_get_head_hash($proj{'path'});
5889 if (!defined $head) {
5890 next;
5892 $git_dir = "$projectroot/$proj{'path'}";
5893 my %co = parse_commit($head);
5894 if (!%co) {
5895 next;
5898 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
5899 my $rss = "$my_url?p=$proj{'path'};a=rss";
5900 my $html = "$my_url?p=$proj{'path'};a=summary";
5901 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
5903 print <<XML;
5904 </outline>
5905 </body>
5906 </opml>