gitweb: parse project/action/hash_base:filename PATH_INFO
[git/gitweb.git] / gitweb / gitweb.perl
blobd09cf0a5202043663b0a45cf89c90b0a0c2edaf4
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 # we make $path_info global because it's also used later on
33 my $path_info = $ENV{"PATH_INFO"};
34 if ($path_info) {
35 $my_url =~ s,\Q$path_info\E$,,;
36 $my_uri =~ s,\Q$path_info\E$,,;
39 # core git executable to use
40 # this can just be "git" if your webserver has a sensible PATH
41 our $GIT = "++GIT_BINDIR++/git";
43 # absolute fs-path which will be prepended to the project path
44 #our $projectroot = "/pub/scm";
45 our $projectroot = "++GITWEB_PROJECTROOT++";
47 # fs traversing limit for getting project list
48 # the number is relative to the projectroot
49 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
51 # target of the home link on top of all pages
52 our $home_link = $my_uri || "/";
54 # string of the home link on top of all pages
55 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
57 # name of your site or organization to appear in page titles
58 # replace this with something more descriptive for clearer bookmarks
59 our $site_name = "++GITWEB_SITENAME++"
60 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
62 # filename of html text to include at top of each page
63 our $site_header = "++GITWEB_SITE_HEADER++";
64 # html text to include at home page
65 our $home_text = "++GITWEB_HOMETEXT++";
66 # filename of html text to include at bottom of each page
67 our $site_footer = "++GITWEB_SITE_FOOTER++";
69 # URI of stylesheets
70 our @stylesheets = ("++GITWEB_CSS++");
71 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
72 our $stylesheet = undef;
73 # URI of GIT logo (72x27 size)
74 our $logo = "++GITWEB_LOGO++";
75 # URI of GIT favicon, assumed to be image/png type
76 our $favicon = "++GITWEB_FAVICON++";
78 # URI and label (title) of GIT logo link
79 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
80 #our $logo_label = "git documentation";
81 our $logo_url = "http://git.or.cz/";
82 our $logo_label = "git homepage";
84 # source of projects list
85 our $projects_list = "++GITWEB_LIST++";
87 # the width (in characters) of the projects list "Description" column
88 our $projects_list_description_width = 25;
90 # default order of projects list
91 # valid values are none, project, descr, owner, and age
92 our $default_projects_order = "project";
94 # show repository only if this file exists
95 # (only effective if this variable evaluates to true)
96 our $export_ok = "++GITWEB_EXPORT_OK++";
98 # only allow viewing of repositories also shown on the overview page
99 our $strict_export = "++GITWEB_STRICT_EXPORT++";
101 # list of git base URLs used for URL to where fetch project from,
102 # i.e. full URL is "$git_base_url/$project"
103 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
105 # default blob_plain mimetype and default charset for text/plain blob
106 our $default_blob_plain_mimetype = 'text/plain';
107 our $default_text_plain_charset = undef;
109 # file to use for guessing MIME types before trying /etc/mime.types
110 # (relative to the current git repository)
111 our $mimetypes_file = undef;
113 # assume this charset if line contains non-UTF-8 characters;
114 # it should be valid encoding (see Encoding::Supported(3pm) for list),
115 # for which encoding all byte sequences are valid, for example
116 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
117 # could be even 'utf-8' for the old behavior)
118 our $fallback_encoding = 'latin1';
120 # rename detection options for git-diff and git-diff-tree
121 # - default is '-M', with the cost proportional to
122 # (number of removed files) * (number of new files).
123 # - more costly is '-C' (which implies '-M'), with the cost proportional to
124 # (number of changed files + number of removed files) * (number of new files)
125 # - even more costly is '-C', '--find-copies-harder' with cost
126 # (number of files in the original tree) * (number of new files)
127 # - one might want to include '-B' option, e.g. '-B', '-M'
128 our @diff_opts = ('-M'); # taken from git_commit
130 # information about snapshot formats that gitweb is capable of serving
131 our %known_snapshot_formats = (
132 # name => {
133 # 'display' => display name,
134 # 'type' => mime type,
135 # 'suffix' => filename suffix,
136 # 'format' => --format for git-archive,
137 # 'compressor' => [compressor command and arguments]
138 # (array reference, optional)}
140 'tgz' => {
141 'display' => 'tar.gz',
142 'type' => 'application/x-gzip',
143 'suffix' => '.tar.gz',
144 'format' => 'tar',
145 'compressor' => ['gzip']},
147 'tbz2' => {
148 'display' => 'tar.bz2',
149 'type' => 'application/x-bzip2',
150 'suffix' => '.tar.bz2',
151 'format' => 'tar',
152 'compressor' => ['bzip2']},
154 'zip' => {
155 'display' => 'zip',
156 'type' => 'application/x-zip',
157 'suffix' => '.zip',
158 'format' => 'zip'},
161 # Aliases so we understand old gitweb.snapshot values in repository
162 # configuration.
163 our %known_snapshot_format_aliases = (
164 'gzip' => 'tgz',
165 'bzip2' => 'tbz2',
167 # backward compatibility: legacy gitweb config support
168 'x-gzip' => undef, 'gz' => undef,
169 'x-bzip2' => undef, 'bz2' => undef,
170 'x-zip' => undef, '' => undef,
173 # You define site-wide feature defaults here; override them with
174 # $GITWEB_CONFIG as necessary.
175 our %feature = (
176 # feature => {
177 # 'sub' => feature-sub (subroutine),
178 # 'override' => allow-override (boolean),
179 # 'default' => [ default options...] (array reference)}
181 # if feature is overridable (it means that allow-override has true value),
182 # then feature-sub will be called with default options as parameters;
183 # return value of feature-sub indicates if to enable specified feature
185 # if there is no 'sub' key (no feature-sub), then feature cannot be
186 # overriden
188 # use gitweb_check_feature(<feature>) to check if <feature> is enabled
190 # Enable the 'blame' blob view, showing the last commit that modified
191 # each line in the file. This can be very CPU-intensive.
193 # To enable system wide have in $GITWEB_CONFIG
194 # $feature{'blame'}{'default'} = [1];
195 # To have project specific config enable override in $GITWEB_CONFIG
196 # $feature{'blame'}{'override'} = 1;
197 # and in project config gitweb.blame = 0|1;
198 'blame' => {
199 'sub' => \&feature_blame,
200 'override' => 0,
201 'default' => [0]},
203 # Enable the 'snapshot' link, providing a compressed archive of any
204 # tree. This can potentially generate high traffic if you have large
205 # project.
207 # Value is a list of formats defined in %known_snapshot_formats that
208 # you wish to offer.
209 # To disable system wide have in $GITWEB_CONFIG
210 # $feature{'snapshot'}{'default'} = [];
211 # To have project specific config enable override in $GITWEB_CONFIG
212 # $feature{'snapshot'}{'override'} = 1;
213 # and in project config, a comma-separated list of formats or "none"
214 # to disable. Example: gitweb.snapshot = tbz2,zip;
215 'snapshot' => {
216 'sub' => \&feature_snapshot,
217 'override' => 0,
218 'default' => ['tgz']},
220 # Enable text search, which will list the commits which match author,
221 # committer or commit text to a given string. Enabled by default.
222 # Project specific override is not supported.
223 'search' => {
224 'override' => 0,
225 'default' => [1]},
227 # Enable grep search, which will list the files in currently selected
228 # tree containing the given string. Enabled by default. This can be
229 # potentially CPU-intensive, of course.
231 # To enable system wide have in $GITWEB_CONFIG
232 # $feature{'grep'}{'default'} = [1];
233 # To have project specific config enable override in $GITWEB_CONFIG
234 # $feature{'grep'}{'override'} = 1;
235 # and in project config gitweb.grep = 0|1;
236 'grep' => {
237 'override' => 0,
238 'default' => [1]},
240 # Enable the pickaxe search, which will list the commits that modified
241 # a given string in a file. This can be practical and quite faster
242 # alternative to 'blame', but still potentially CPU-intensive.
244 # To enable system wide have in $GITWEB_CONFIG
245 # $feature{'pickaxe'}{'default'} = [1];
246 # To have project specific config enable override in $GITWEB_CONFIG
247 # $feature{'pickaxe'}{'override'} = 1;
248 # and in project config gitweb.pickaxe = 0|1;
249 'pickaxe' => {
250 'sub' => \&feature_pickaxe,
251 'override' => 0,
252 'default' => [1]},
254 # Make gitweb use an alternative format of the URLs which can be
255 # more readable and natural-looking: project name is embedded
256 # directly in the path and the query string contains other
257 # auxiliary information. All gitweb installations recognize
258 # URL in either format; this configures in which formats gitweb
259 # generates links.
261 # To enable system wide have in $GITWEB_CONFIG
262 # $feature{'pathinfo'}{'default'} = [1];
263 # Project specific override is not supported.
265 # Note that you will need to change the default location of CSS,
266 # favicon, logo and possibly other files to an absolute URL. Also,
267 # if gitweb.cgi serves as your indexfile, you will need to force
268 # $my_uri to contain the script name in your $GITWEB_CONFIG.
269 'pathinfo' => {
270 'override' => 0,
271 'default' => [0]},
273 # Make gitweb consider projects in project root subdirectories
274 # to be forks of existing projects. Given project $projname.git,
275 # projects matching $projname/*.git will not be shown in the main
276 # projects list, instead a '+' mark will be added to $projname
277 # there and a 'forks' view will be enabled for the project, listing
278 # all the forks. If project list is taken from a file, forks have
279 # to be listed after the main project.
281 # To enable system wide have in $GITWEB_CONFIG
282 # $feature{'forks'}{'default'} = [1];
283 # Project specific override is not supported.
284 'forks' => {
285 'override' => 0,
286 'default' => [0]},
288 # Insert custom links to the action bar of all project pages.
289 # This enables you mainly to link to third-party scripts integrating
290 # into gitweb; e.g. git-browser for graphical history representation
291 # or custom web-based repository administration interface.
293 # The 'default' value consists of a list of triplets in the form
294 # (label, link, position) where position is the label after which
295 # to inster the link and link is a format string where %n expands
296 # to the project name, %f to the project path within the filesystem,
297 # %h to the current hash (h gitweb parameter) and %b to the current
298 # hash base (hb gitweb parameter).
300 # To enable system wide have in $GITWEB_CONFIG e.g.
301 # $feature{'actions'}{'default'} = [('graphiclog',
302 # '/git-browser/by-commit.html?r=%n', 'summary')];
303 # Project specific override is not supported.
304 'actions' => {
305 'override' => 0,
306 'default' => []},
308 # Allow gitweb scan project content tags described in ctags/
309 # of project repository, and display the popular Web 2.0-ish
310 # "tag cloud" near the project list. Note that this is something
311 # COMPLETELY different from the normal Git tags.
313 # gitweb by itself can show existing tags, but it does not handle
314 # tagging itself; you need an external application for that.
315 # For an example script, check Girocco's cgi/tagproj.cgi.
316 # You may want to install the HTML::TagCloud Perl module to get
317 # a pretty tag cloud instead of just a list of tags.
319 # To enable system wide have in $GITWEB_CONFIG
320 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
321 # Project specific override is not supported.
322 'ctags' => {
323 'override' => 0,
324 'default' => [0]},
327 sub gitweb_check_feature {
328 my ($name) = @_;
329 return unless exists $feature{$name};
330 my ($sub, $override, @defaults) = (
331 $feature{$name}{'sub'},
332 $feature{$name}{'override'},
333 @{$feature{$name}{'default'}});
334 if (!$override) { return @defaults; }
335 if (!defined $sub) {
336 warn "feature $name is not overrideable";
337 return @defaults;
339 return $sub->(@defaults);
342 sub feature_blame {
343 my ($val) = git_get_project_config('blame', '--bool');
345 if ($val eq 'true') {
346 return 1;
347 } elsif ($val eq 'false') {
348 return 0;
351 return $_[0];
354 sub feature_snapshot {
355 my (@fmts) = @_;
357 my ($val) = git_get_project_config('snapshot');
359 if ($val) {
360 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
363 return @fmts;
366 sub feature_grep {
367 my ($val) = git_get_project_config('grep', '--bool');
369 if ($val eq 'true') {
370 return (1);
371 } elsif ($val eq 'false') {
372 return (0);
375 return ($_[0]);
378 sub feature_pickaxe {
379 my ($val) = git_get_project_config('pickaxe', '--bool');
381 if ($val eq 'true') {
382 return (1);
383 } elsif ($val eq 'false') {
384 return (0);
387 return ($_[0]);
390 # checking HEAD file with -e is fragile if the repository was
391 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
392 # and then pruned.
393 sub check_head_link {
394 my ($dir) = @_;
395 my $headfile = "$dir/HEAD";
396 return ((-e $headfile) ||
397 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
400 sub check_export_ok {
401 my ($dir) = @_;
402 return (check_head_link($dir) &&
403 (!$export_ok || -e "$dir/$export_ok"));
406 # process alternate names for backward compatibility
407 # filter out unsupported (unknown) snapshot formats
408 sub filter_snapshot_fmts {
409 my @fmts = @_;
411 @fmts = map {
412 exists $known_snapshot_format_aliases{$_} ?
413 $known_snapshot_format_aliases{$_} : $_} @fmts;
414 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
418 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
419 if (-e $GITWEB_CONFIG) {
420 do $GITWEB_CONFIG;
421 } else {
422 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
423 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
426 # version of the core git binary
427 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
429 $projects_list ||= $projectroot;
431 # ======================================================================
432 # input validation and dispatch
434 # input parameters can be collected from a variety of sources (presently, CGI
435 # and PATH_INFO), so we define an %input_params hash that collects them all
436 # together during validation: this allows subsequent uses (e.g. href()) to be
437 # agnostic of the parameter origin
439 my %input_params = ();
441 # input parameters are stored with the long parameter name as key. This will
442 # also be used in the href subroutine to convert parameters to their CGI
443 # equivalent, and since the href() usage is the most frequent one, we store
444 # the name -> CGI key mapping here, instead of the reverse.
446 # XXX: Warning: If you touch this, check the search form for updating,
447 # too.
449 my @cgi_param_mapping = (
450 project => "p",
451 action => "a",
452 file_name => "f",
453 file_parent => "fp",
454 hash => "h",
455 hash_parent => "hp",
456 hash_base => "hb",
457 hash_parent_base => "hpb",
458 page => "pg",
459 order => "o",
460 searchtext => "s",
461 searchtype => "st",
462 snapshot_format => "sf",
463 extra_options => "opt",
464 search_use_regexp => "sr",
466 my %cgi_param_mapping = @cgi_param_mapping;
468 # we will also need to know the possible actions, for validation
469 my %actions = (
470 "blame" => \&git_blame,
471 "blobdiff" => \&git_blobdiff,
472 "blobdiff_plain" => \&git_blobdiff_plain,
473 "blob" => \&git_blob,
474 "blob_plain" => \&git_blob_plain,
475 "commitdiff" => \&git_commitdiff,
476 "commitdiff_plain" => \&git_commitdiff_plain,
477 "commit" => \&git_commit,
478 "forks" => \&git_forks,
479 "heads" => \&git_heads,
480 "history" => \&git_history,
481 "log" => \&git_log,
482 "rss" => \&git_rss,
483 "atom" => \&git_atom,
484 "search" => \&git_search,
485 "search_help" => \&git_search_help,
486 "shortlog" => \&git_shortlog,
487 "summary" => \&git_summary,
488 "tag" => \&git_tag,
489 "tags" => \&git_tags,
490 "tree" => \&git_tree,
491 "snapshot" => \&git_snapshot,
492 "object" => \&git_object,
493 # those below don't need $project
494 "opml" => \&git_opml,
495 "project_list" => \&git_project_list,
496 "project_index" => \&git_project_index,
499 # finally, we have the hash of allowed extra_options for the commands that
500 # allow them
501 my %allowed_options = (
502 "--no-merges" => [ qw(rss atom log shortlog history) ],
505 # fill %input_params with the CGI parameters. All values except for 'opt'
506 # should be single values, but opt can be an array. We should probably
507 # build an array of parameters that can be multi-valued, but since for the time
508 # being it's only this one, we just single it out
509 while (my ($name, $symbol) = each %cgi_param_mapping) {
510 if ($symbol eq 'opt') {
511 $input_params{$name} = [ $cgi->param($symbol) ];
512 } else {
513 $input_params{$name} = $cgi->param($symbol);
517 # now read PATH_INFO and update the parameter list for missing parameters
518 sub evaluate_path_info {
519 return if defined $input_params{'project'};
520 return if !$path_info;
521 $path_info =~ s,^/+,,;
522 return if !$path_info;
524 # find which part of PATH_INFO is project
525 my $project = $path_info;
526 $project =~ s,/+$,,;
527 while ($project && !check_head_link("$projectroot/$project")) {
528 $project =~ s,/*[^/]*$,,;
530 return unless $project;
531 $input_params{'project'} = $project;
533 # do not change any parameters if an action is given using the query string
534 return if $input_params{'action'};
535 $path_info =~ s,^\Q$project\E/*,,;
537 # next, check if we have an action
538 my $action = $path_info;
539 $action =~ s,/.*$,,;
540 if (exists $actions{$action}) {
541 $path_info =~ s,^$action/*,,;
542 $input_params{'action'} = $action;
545 # list of actions that want hash_base instead of hash, but can have no
546 # pathname (f) parameter
547 my @wants_base = (
548 'tree',
549 'history',
552 my ($refname, $pathname) = split(/:/, $path_info, 2);
553 if (defined $pathname) {
554 # we got "branch:filename" or "branch:dir/"
555 # we could use git_get_type(branch:pathname), but:
556 # - it needs $git_dir
557 # - it does a git() call
558 # - the convention of terminating directories with a slash
559 # makes it superfluous
560 # - embedding the action in the PATH_INFO would make it even
561 # more superfluous
562 $pathname =~ s,^/+,,;
563 if (!$pathname || substr($pathname, -1) eq "/") {
564 $input_params{'action'} ||= "tree";
565 $pathname =~ s,/$,,;
566 } else {
567 $input_params{'action'} ||= "blob_plain";
569 $input_params{'hash_base'} ||= $refname;
570 $input_params{'file_name'} ||= $pathname;
571 } elsif (defined $refname) {
572 # we got "branch". In this case we have to choose if we have to
573 # set hash or hash_base.
575 # Most of the actions without a pathname only want hash to be
576 # set, except for the ones specified in @wants_base that want
577 # hash_base instead. It should also be noted that hand-crafted
578 # links having 'history' as an action and no pathname or hash
579 # set will fail, but that happens regardless of PATH_INFO.
580 $input_params{'action'} ||= "shortlog";
581 if (grep { $_ eq $input_params{'action'} } @wants_base) {
582 $input_params{'hash_base'} ||= $refname;
583 } else {
584 $input_params{'hash'} ||= $refname;
588 evaluate_path_info();
590 our $action = $input_params{'action'};
591 if (defined $action) {
592 if (!validate_action($action)) {
593 die_error(400, "Invalid action parameter");
597 # parameters which are pathnames
598 our $project = $input_params{'project'};
599 if (defined $project) {
600 if (!validate_project($project)) {
601 undef $project;
602 die_error(404, "No such project");
606 our $file_name = $input_params{'file_name'};
607 if (defined $file_name) {
608 if (!validate_pathname($file_name)) {
609 die_error(400, "Invalid file parameter");
613 our $file_parent = $input_params{'file_parent'};
614 if (defined $file_parent) {
615 if (!validate_pathname($file_parent)) {
616 die_error(400, "Invalid file parent parameter");
620 # parameters which are refnames
621 our $hash = $input_params{'hash'};
622 if (defined $hash) {
623 if (!validate_refname($hash)) {
624 die_error(400, "Invalid hash parameter");
628 our $hash_parent = $input_params{'hash_parent'};
629 if (defined $hash_parent) {
630 if (!validate_refname($hash_parent)) {
631 die_error(400, "Invalid hash parent parameter");
635 our $hash_base = $input_params{'hash_base'};
636 if (defined $hash_base) {
637 if (!validate_refname($hash_base)) {
638 die_error(400, "Invalid hash base parameter");
642 our @extra_options = @{$input_params{'extra_options'}};
643 # @extra_options is always defined, since it can only be (currently) set from
644 # CGI, and $cgi->param() returns the empty array in array context if the param
645 # is not set
646 foreach my $opt (@extra_options) {
647 if (not exists $allowed_options{$opt}) {
648 die_error(400, "Invalid option parameter");
650 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
651 die_error(400, "Invalid option parameter for this action");
655 our $hash_parent_base = $input_params{'hash_parent_base'};
656 if (defined $hash_parent_base) {
657 if (!validate_refname($hash_parent_base)) {
658 die_error(400, "Invalid hash parent base parameter");
662 # other parameters
663 our $page = $input_params{'page'};
664 if (defined $page) {
665 if ($page =~ m/[^0-9]/) {
666 die_error(400, "Invalid page parameter");
670 our $searchtype = $input_params{'searchtype'};
671 if (defined $searchtype) {
672 if ($searchtype =~ m/[^a-z]/) {
673 die_error(400, "Invalid searchtype parameter");
677 our $search_use_regexp = $input_params{'search_use_regexp'};
679 our $searchtext = $input_params{'searchtext'};
680 our $search_regexp;
681 if (defined $searchtext) {
682 if (length($searchtext) < 2) {
683 die_error(403, "At least two characters are required for search parameter");
685 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
688 # path to the current git repository
689 our $git_dir;
690 $git_dir = "$projectroot/$project" if $project;
692 # dispatch
693 if (!defined $action) {
694 if (defined $hash) {
695 $action = git_get_type($hash);
696 } elsif (defined $hash_base && defined $file_name) {
697 $action = git_get_type("$hash_base:$file_name");
698 } elsif (defined $project) {
699 $action = 'summary';
700 } else {
701 $action = 'project_list';
704 if (!defined($actions{$action})) {
705 die_error(400, "Unknown action");
707 if ($action !~ m/^(opml|project_list|project_index)$/ &&
708 !$project) {
709 die_error(400, "Project needed");
711 $actions{$action}->();
712 exit;
714 ## ======================================================================
715 ## action links
717 sub href (%) {
718 my %params = @_;
719 # default is to use -absolute url() i.e. $my_uri
720 my $href = $params{-full} ? $my_url : $my_uri;
722 $params{'project'} = $project unless exists $params{'project'};
724 if ($params{-replay}) {
725 while (my ($name, $symbol) = each %cgi_param_mapping) {
726 if (!exists $params{$name}) {
727 $params{$name} = $input_params{$name};
732 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
733 if ($use_pathinfo) {
734 # use PATH_INFO for project name
735 $href .= "/".esc_url($params{'project'}) if defined $params{'project'};
736 delete $params{'project'};
738 # Summary just uses the project path URL
739 if (defined $params{'action'} && $params{'action'} eq 'summary') {
740 delete $params{'action'};
744 # now encode the parameters explicitly
745 my @result = ();
746 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
747 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
748 if (defined $params{$name}) {
749 if (ref($params{$name}) eq "ARRAY") {
750 foreach my $par (@{$params{$name}}) {
751 push @result, $symbol . "=" . esc_param($par);
753 } else {
754 push @result, $symbol . "=" . esc_param($params{$name});
758 $href .= "?" . join(';', @result) if scalar @result;
760 return $href;
764 ## ======================================================================
765 ## validation, quoting/unquoting and escaping
767 sub validate_action {
768 my $input = shift || return undef;
769 return undef unless exists $actions{$input};
770 return $input;
773 sub validate_project {
774 my $input = shift || return undef;
775 if (!validate_pathname($input) ||
776 !(-d "$projectroot/$input") ||
777 !check_head_link("$projectroot/$input") ||
778 ($export_ok && !(-e "$projectroot/$input/$export_ok")) ||
779 ($strict_export && !project_in_list($input))) {
780 return undef;
781 } else {
782 return $input;
786 sub validate_pathname {
787 my $input = shift || return undef;
789 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
790 # at the beginning, at the end, and between slashes.
791 # also this catches doubled slashes
792 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
793 return undef;
795 # no null characters
796 if ($input =~ m!\0!) {
797 return undef;
799 return $input;
802 sub validate_refname {
803 my $input = shift || return undef;
805 # textual hashes are O.K.
806 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
807 return $input;
809 # it must be correct pathname
810 $input = validate_pathname($input)
811 or return undef;
812 # restrictions on ref name according to git-check-ref-format
813 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
814 return undef;
816 return $input;
819 # decode sequences of octets in utf8 into Perl's internal form,
820 # which is utf-8 with utf8 flag set if needed. gitweb writes out
821 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
822 sub to_utf8 {
823 my $str = shift;
824 if (utf8::valid($str)) {
825 utf8::decode($str);
826 return $str;
827 } else {
828 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
832 # quote unsafe chars, but keep the slash, even when it's not
833 # correct, but quoted slashes look too horrible in bookmarks
834 sub esc_param {
835 my $str = shift;
836 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
837 $str =~ s/\+/%2B/g;
838 $str =~ s/ /\+/g;
839 return $str;
842 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
843 sub esc_url {
844 my $str = shift;
845 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
846 $str =~ s/\+/%2B/g;
847 $str =~ s/ /\+/g;
848 return $str;
851 # replace invalid utf8 character with SUBSTITUTION sequence
852 sub esc_html ($;%) {
853 my $str = shift;
854 my %opts = @_;
856 $str = to_utf8($str);
857 $str = $cgi->escapeHTML($str);
858 if ($opts{'-nbsp'}) {
859 $str =~ s/ /&nbsp;/g;
861 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
862 return $str;
865 # quote control characters and escape filename to HTML
866 sub esc_path {
867 my $str = shift;
868 my %opts = @_;
870 $str = to_utf8($str);
871 $str = $cgi->escapeHTML($str);
872 if ($opts{'-nbsp'}) {
873 $str =~ s/ /&nbsp;/g;
875 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
876 return $str;
879 # Make control characters "printable", using character escape codes (CEC)
880 sub quot_cec {
881 my $cntrl = shift;
882 my %opts = @_;
883 my %es = ( # character escape codes, aka escape sequences
884 "\t" => '\t', # tab (HT)
885 "\n" => '\n', # line feed (LF)
886 "\r" => '\r', # carrige return (CR)
887 "\f" => '\f', # form feed (FF)
888 "\b" => '\b', # backspace (BS)
889 "\a" => '\a', # alarm (bell) (BEL)
890 "\e" => '\e', # escape (ESC)
891 "\013" => '\v', # vertical tab (VT)
892 "\000" => '\0', # nul character (NUL)
894 my $chr = ( (exists $es{$cntrl})
895 ? $es{$cntrl}
896 : sprintf('\%2x', ord($cntrl)) );
897 if ($opts{-nohtml}) {
898 return $chr;
899 } else {
900 return "<span class=\"cntrl\">$chr</span>";
904 # Alternatively use unicode control pictures codepoints,
905 # Unicode "printable representation" (PR)
906 sub quot_upr {
907 my $cntrl = shift;
908 my %opts = @_;
910 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
911 if ($opts{-nohtml}) {
912 return $chr;
913 } else {
914 return "<span class=\"cntrl\">$chr</span>";
918 # git may return quoted and escaped filenames
919 sub unquote {
920 my $str = shift;
922 sub unq {
923 my $seq = shift;
924 my %es = ( # character escape codes, aka escape sequences
925 't' => "\t", # tab (HT, TAB)
926 'n' => "\n", # newline (NL)
927 'r' => "\r", # return (CR)
928 'f' => "\f", # form feed (FF)
929 'b' => "\b", # backspace (BS)
930 'a' => "\a", # alarm (bell) (BEL)
931 'e' => "\e", # escape (ESC)
932 'v' => "\013", # vertical tab (VT)
935 if ($seq =~ m/^[0-7]{1,3}$/) {
936 # octal char sequence
937 return chr(oct($seq));
938 } elsif (exists $es{$seq}) {
939 # C escape sequence, aka character escape code
940 return $es{$seq};
942 # quoted ordinary character
943 return $seq;
946 if ($str =~ m/^"(.*)"$/) {
947 # needs unquoting
948 $str = $1;
949 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
951 return $str;
954 # escape tabs (convert tabs to spaces)
955 sub untabify {
956 my $line = shift;
958 while ((my $pos = index($line, "\t")) != -1) {
959 if (my $count = (8 - ($pos % 8))) {
960 my $spaces = ' ' x $count;
961 $line =~ s/\t/$spaces/;
965 return $line;
968 sub project_in_list {
969 my $project = shift;
970 my @list = git_get_projects_list();
971 return @list && scalar(grep { $_->{'path'} eq $project } @list);
974 ## ----------------------------------------------------------------------
975 ## HTML aware string manipulation
977 # Try to chop given string on a word boundary between position
978 # $len and $len+$add_len. If there is no word boundary there,
979 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
980 # (marking chopped part) would be longer than given string.
981 sub chop_str {
982 my $str = shift;
983 my $len = shift;
984 my $add_len = shift || 10;
985 my $where = shift || 'right'; # 'left' | 'center' | 'right'
987 # Make sure perl knows it is utf8 encoded so we don't
988 # cut in the middle of a utf8 multibyte char.
989 $str = to_utf8($str);
991 # allow only $len chars, but don't cut a word if it would fit in $add_len
992 # if it doesn't fit, cut it if it's still longer than the dots we would add
993 # remove chopped character entities entirely
995 # when chopping in the middle, distribute $len into left and right part
996 # return early if chopping wouldn't make string shorter
997 if ($where eq 'center') {
998 return $str if ($len + 5 >= length($str)); # filler is length 5
999 $len = int($len/2);
1000 } else {
1001 return $str if ($len + 4 >= length($str)); # filler is length 4
1004 # regexps: ending and beginning with word part up to $add_len
1005 my $endre = qr/.{$len}\w{0,$add_len}/;
1006 my $begre = qr/\w{0,$add_len}.{$len}/;
1008 if ($where eq 'left') {
1009 $str =~ m/^(.*?)($begre)$/;
1010 my ($lead, $body) = ($1, $2);
1011 if (length($lead) > 4) {
1012 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1013 $lead = " ...";
1015 return "$lead$body";
1017 } elsif ($where eq 'center') {
1018 $str =~ m/^($endre)(.*)$/;
1019 my ($left, $str) = ($1, $2);
1020 $str =~ m/^(.*?)($begre)$/;
1021 my ($mid, $right) = ($1, $2);
1022 if (length($mid) > 5) {
1023 $left =~ s/&[^;]*$//;
1024 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1025 $mid = " ... ";
1027 return "$left$mid$right";
1029 } else {
1030 $str =~ m/^($endre)(.*)$/;
1031 my $body = $1;
1032 my $tail = $2;
1033 if (length($tail) > 4) {
1034 $body =~ s/&[^;]*$//;
1035 $tail = "... ";
1037 return "$body$tail";
1041 # takes the same arguments as chop_str, but also wraps a <span> around the
1042 # result with a title attribute if it does get chopped. Additionally, the
1043 # string is HTML-escaped.
1044 sub chop_and_escape_str {
1045 my ($str) = @_;
1047 my $chopped = chop_str(@_);
1048 if ($chopped eq $str) {
1049 return esc_html($chopped);
1050 } else {
1051 $str =~ s/([[:cntrl:]])/?/g;
1052 return $cgi->span({-title=>$str}, esc_html($chopped));
1056 ## ----------------------------------------------------------------------
1057 ## functions returning short strings
1059 # CSS class for given age value (in seconds)
1060 sub age_class {
1061 my $age = shift;
1063 if (!defined $age) {
1064 return "noage";
1065 } elsif ($age < 60*60*2) {
1066 return "age0";
1067 } elsif ($age < 60*60*24*2) {
1068 return "age1";
1069 } else {
1070 return "age2";
1074 # convert age in seconds to "nn units ago" string
1075 sub age_string {
1076 my $age = shift;
1077 my $age_str;
1079 if ($age > 60*60*24*365*2) {
1080 $age_str = (int $age/60/60/24/365);
1081 $age_str .= " years ago";
1082 } elsif ($age > 60*60*24*(365/12)*2) {
1083 $age_str = int $age/60/60/24/(365/12);
1084 $age_str .= " months ago";
1085 } elsif ($age > 60*60*24*7*2) {
1086 $age_str = int $age/60/60/24/7;
1087 $age_str .= " weeks ago";
1088 } elsif ($age > 60*60*24*2) {
1089 $age_str = int $age/60/60/24;
1090 $age_str .= " days ago";
1091 } elsif ($age > 60*60*2) {
1092 $age_str = int $age/60/60;
1093 $age_str .= " hours ago";
1094 } elsif ($age > 60*2) {
1095 $age_str = int $age/60;
1096 $age_str .= " min ago";
1097 } elsif ($age > 2) {
1098 $age_str = int $age;
1099 $age_str .= " sec ago";
1100 } else {
1101 $age_str .= " right now";
1103 return $age_str;
1106 use constant {
1107 S_IFINVALID => 0030000,
1108 S_IFGITLINK => 0160000,
1111 # submodule/subproject, a commit object reference
1112 sub S_ISGITLINK($) {
1113 my $mode = shift;
1115 return (($mode & S_IFMT) == S_IFGITLINK)
1118 # convert file mode in octal to symbolic file mode string
1119 sub mode_str {
1120 my $mode = oct shift;
1122 if (S_ISGITLINK($mode)) {
1123 return 'm---------';
1124 } elsif (S_ISDIR($mode & S_IFMT)) {
1125 return 'drwxr-xr-x';
1126 } elsif (S_ISLNK($mode)) {
1127 return 'lrwxrwxrwx';
1128 } elsif (S_ISREG($mode)) {
1129 # git cares only about the executable bit
1130 if ($mode & S_IXUSR) {
1131 return '-rwxr-xr-x';
1132 } else {
1133 return '-rw-r--r--';
1135 } else {
1136 return '----------';
1140 # convert file mode in octal to file type string
1141 sub file_type {
1142 my $mode = shift;
1144 if ($mode !~ m/^[0-7]+$/) {
1145 return $mode;
1146 } else {
1147 $mode = oct $mode;
1150 if (S_ISGITLINK($mode)) {
1151 return "submodule";
1152 } elsif (S_ISDIR($mode & S_IFMT)) {
1153 return "directory";
1154 } elsif (S_ISLNK($mode)) {
1155 return "symlink";
1156 } elsif (S_ISREG($mode)) {
1157 return "file";
1158 } else {
1159 return "unknown";
1163 # convert file mode in octal to file type description string
1164 sub file_type_long {
1165 my $mode = shift;
1167 if ($mode !~ m/^[0-7]+$/) {
1168 return $mode;
1169 } else {
1170 $mode = oct $mode;
1173 if (S_ISGITLINK($mode)) {
1174 return "submodule";
1175 } elsif (S_ISDIR($mode & S_IFMT)) {
1176 return "directory";
1177 } elsif (S_ISLNK($mode)) {
1178 return "symlink";
1179 } elsif (S_ISREG($mode)) {
1180 if ($mode & S_IXUSR) {
1181 return "executable";
1182 } else {
1183 return "file";
1185 } else {
1186 return "unknown";
1191 ## ----------------------------------------------------------------------
1192 ## functions returning short HTML fragments, or transforming HTML fragments
1193 ## which don't belong to other sections
1195 # format line of commit message.
1196 sub format_log_line_html {
1197 my $line = shift;
1199 $line = esc_html($line, -nbsp=>1);
1200 if ($line =~ m/([0-9a-fA-F]{8,40})/) {
1201 my $hash_text = $1;
1202 my $link =
1203 $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1204 -class => "text"}, $hash_text);
1205 $line =~ s/$hash_text/$link/;
1207 return $line;
1210 # format marker of refs pointing to given object
1212 # the destination action is chosen based on object type and current context:
1213 # - for annotated tags, we choose the tag view unless it's the current view
1214 # already, in which case we go to shortlog view
1215 # - for other refs, we keep the current view if we're in history, shortlog or
1216 # log view, and select shortlog otherwise
1217 sub format_ref_marker {
1218 my ($refs, $id) = @_;
1219 my $markers = '';
1221 if (defined $refs->{$id}) {
1222 foreach my $ref (@{$refs->{$id}}) {
1223 # this code exploits the fact that non-lightweight tags are the
1224 # only indirect objects, and that they are the only objects for which
1225 # we want to use tag instead of shortlog as action
1226 my ($type, $name) = qw();
1227 my $indirect = ($ref =~ s/\^\{\}$//);
1228 # e.g. tags/v2.6.11 or heads/next
1229 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1230 $type = $1;
1231 $name = $2;
1232 } else {
1233 $type = "ref";
1234 $name = $ref;
1237 my $class = $type;
1238 $class .= " indirect" if $indirect;
1240 my $dest_action = "shortlog";
1242 if ($indirect) {
1243 $dest_action = "tag" unless $action eq "tag";
1244 } elsif ($action =~ /^(history|(short)?log)$/) {
1245 $dest_action = $action;
1248 my $dest = "";
1249 $dest .= "refs/" unless $ref =~ m!^refs/!;
1250 $dest .= $ref;
1252 my $link = $cgi->a({
1253 -href => href(
1254 action=>$dest_action,
1255 hash=>$dest
1256 )}, $name);
1258 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1259 $link . "</span>";
1263 if ($markers) {
1264 return ' <span class="refs">'. $markers . '</span>';
1265 } else {
1266 return "";
1270 # format, perhaps shortened and with markers, title line
1271 sub format_subject_html {
1272 my ($long, $short, $href, $extra) = @_;
1273 $extra = '' unless defined($extra);
1275 if (length($short) < length($long)) {
1276 return $cgi->a({-href => $href, -class => "list subject",
1277 -title => to_utf8($long)},
1278 esc_html($short) . $extra);
1279 } else {
1280 return $cgi->a({-href => $href, -class => "list subject"},
1281 esc_html($long) . $extra);
1285 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1286 sub format_git_diff_header_line {
1287 my $line = shift;
1288 my $diffinfo = shift;
1289 my ($from, $to) = @_;
1291 if ($diffinfo->{'nparents'}) {
1292 # combined diff
1293 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1294 if ($to->{'href'}) {
1295 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1296 esc_path($to->{'file'}));
1297 } else { # file was deleted (no href)
1298 $line .= esc_path($to->{'file'});
1300 } else {
1301 # "ordinary" diff
1302 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1303 if ($from->{'href'}) {
1304 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1305 'a/' . esc_path($from->{'file'}));
1306 } else { # file was added (no href)
1307 $line .= 'a/' . esc_path($from->{'file'});
1309 $line .= ' ';
1310 if ($to->{'href'}) {
1311 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1312 'b/' . esc_path($to->{'file'}));
1313 } else { # file was deleted
1314 $line .= 'b/' . esc_path($to->{'file'});
1318 return "<div class=\"diff header\">$line</div>\n";
1321 # format extended diff header line, before patch itself
1322 sub format_extended_diff_header_line {
1323 my $line = shift;
1324 my $diffinfo = shift;
1325 my ($from, $to) = @_;
1327 # match <path>
1328 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1329 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1330 esc_path($from->{'file'}));
1332 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1333 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1334 esc_path($to->{'file'}));
1336 # match single <mode>
1337 if ($line =~ m/\s(\d{6})$/) {
1338 $line .= '<span class="info"> (' .
1339 file_type_long($1) .
1340 ')</span>';
1342 # match <hash>
1343 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1344 # can match only for combined diff
1345 $line = 'index ';
1346 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1347 if ($from->{'href'}[$i]) {
1348 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1349 -class=>"hash"},
1350 substr($diffinfo->{'from_id'}[$i],0,7));
1351 } else {
1352 $line .= '0' x 7;
1354 # separator
1355 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1357 $line .= '..';
1358 if ($to->{'href'}) {
1359 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1360 substr($diffinfo->{'to_id'},0,7));
1361 } else {
1362 $line .= '0' x 7;
1365 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1366 # can match only for ordinary diff
1367 my ($from_link, $to_link);
1368 if ($from->{'href'}) {
1369 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1370 substr($diffinfo->{'from_id'},0,7));
1371 } else {
1372 $from_link = '0' x 7;
1374 if ($to->{'href'}) {
1375 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1376 substr($diffinfo->{'to_id'},0,7));
1377 } else {
1378 $to_link = '0' x 7;
1380 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1381 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1384 return $line . "<br/>\n";
1387 # format from-file/to-file diff header
1388 sub format_diff_from_to_header {
1389 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1390 my $line;
1391 my $result = '';
1393 $line = $from_line;
1394 #assert($line =~ m/^---/) if DEBUG;
1395 # no extra formatting for "^--- /dev/null"
1396 if (! $diffinfo->{'nparents'}) {
1397 # ordinary (single parent) diff
1398 if ($line =~ m!^--- "?a/!) {
1399 if ($from->{'href'}) {
1400 $line = '--- a/' .
1401 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1402 esc_path($from->{'file'}));
1403 } else {
1404 $line = '--- a/' .
1405 esc_path($from->{'file'});
1408 $result .= qq!<div class="diff from_file">$line</div>\n!;
1410 } else {
1411 # combined diff (merge commit)
1412 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1413 if ($from->{'href'}[$i]) {
1414 $line = '--- ' .
1415 $cgi->a({-href=>href(action=>"blobdiff",
1416 hash_parent=>$diffinfo->{'from_id'}[$i],
1417 hash_parent_base=>$parents[$i],
1418 file_parent=>$from->{'file'}[$i],
1419 hash=>$diffinfo->{'to_id'},
1420 hash_base=>$hash,
1421 file_name=>$to->{'file'}),
1422 -class=>"path",
1423 -title=>"diff" . ($i+1)},
1424 $i+1) .
1425 '/' .
1426 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1427 esc_path($from->{'file'}[$i]));
1428 } else {
1429 $line = '--- /dev/null';
1431 $result .= qq!<div class="diff from_file">$line</div>\n!;
1435 $line = $to_line;
1436 #assert($line =~ m/^\+\+\+/) if DEBUG;
1437 # no extra formatting for "^+++ /dev/null"
1438 if ($line =~ m!^\+\+\+ "?b/!) {
1439 if ($to->{'href'}) {
1440 $line = '+++ b/' .
1441 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1442 esc_path($to->{'file'}));
1443 } else {
1444 $line = '+++ b/' .
1445 esc_path($to->{'file'});
1448 $result .= qq!<div class="diff to_file">$line</div>\n!;
1450 return $result;
1453 # create note for patch simplified by combined diff
1454 sub format_diff_cc_simplified {
1455 my ($diffinfo, @parents) = @_;
1456 my $result = '';
1458 $result .= "<div class=\"diff header\">" .
1459 "diff --cc ";
1460 if (!is_deleted($diffinfo)) {
1461 $result .= $cgi->a({-href => href(action=>"blob",
1462 hash_base=>$hash,
1463 hash=>$diffinfo->{'to_id'},
1464 file_name=>$diffinfo->{'to_file'}),
1465 -class => "path"},
1466 esc_path($diffinfo->{'to_file'}));
1467 } else {
1468 $result .= esc_path($diffinfo->{'to_file'});
1470 $result .= "</div>\n" . # class="diff header"
1471 "<div class=\"diff nodifferences\">" .
1472 "Simple merge" .
1473 "</div>\n"; # class="diff nodifferences"
1475 return $result;
1478 # format patch (diff) line (not to be used for diff headers)
1479 sub format_diff_line {
1480 my $line = shift;
1481 my ($from, $to) = @_;
1482 my $diff_class = "";
1484 chomp $line;
1486 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1487 # combined diff
1488 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1489 if ($line =~ m/^\@{3}/) {
1490 $diff_class = " chunk_header";
1491 } elsif ($line =~ m/^\\/) {
1492 $diff_class = " incomplete";
1493 } elsif ($prefix =~ tr/+/+/) {
1494 $diff_class = " add";
1495 } elsif ($prefix =~ tr/-/-/) {
1496 $diff_class = " rem";
1498 } else {
1499 # assume ordinary diff
1500 my $char = substr($line, 0, 1);
1501 if ($char eq '+') {
1502 $diff_class = " add";
1503 } elsif ($char eq '-') {
1504 $diff_class = " rem";
1505 } elsif ($char eq '@') {
1506 $diff_class = " chunk_header";
1507 } elsif ($char eq "\\") {
1508 $diff_class = " incomplete";
1511 $line = untabify($line);
1512 if ($from && $to && $line =~ m/^\@{2} /) {
1513 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1514 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1516 $from_lines = 0 unless defined $from_lines;
1517 $to_lines = 0 unless defined $to_lines;
1519 if ($from->{'href'}) {
1520 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1521 -class=>"list"}, $from_text);
1523 if ($to->{'href'}) {
1524 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1525 -class=>"list"}, $to_text);
1527 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1528 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1529 return "<div class=\"diff$diff_class\">$line</div>\n";
1530 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1531 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1532 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1534 @from_text = split(' ', $ranges);
1535 for (my $i = 0; $i < @from_text; ++$i) {
1536 ($from_start[$i], $from_nlines[$i]) =
1537 (split(',', substr($from_text[$i], 1)), 0);
1540 $to_text = pop @from_text;
1541 $to_start = pop @from_start;
1542 $to_nlines = pop @from_nlines;
1544 $line = "<span class=\"chunk_info\">$prefix ";
1545 for (my $i = 0; $i < @from_text; ++$i) {
1546 if ($from->{'href'}[$i]) {
1547 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1548 -class=>"list"}, $from_text[$i]);
1549 } else {
1550 $line .= $from_text[$i];
1552 $line .= " ";
1554 if ($to->{'href'}) {
1555 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1556 -class=>"list"}, $to_text);
1557 } else {
1558 $line .= $to_text;
1560 $line .= " $prefix</span>" .
1561 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1562 return "<div class=\"diff$diff_class\">$line</div>\n";
1564 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1567 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1568 # linked. Pass the hash of the tree/commit to snapshot.
1569 sub format_snapshot_links {
1570 my ($hash) = @_;
1571 my @snapshot_fmts = gitweb_check_feature('snapshot');
1572 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1573 my $num_fmts = @snapshot_fmts;
1574 if ($num_fmts > 1) {
1575 # A parenthesized list of links bearing format names.
1576 # e.g. "snapshot (_tar.gz_ _zip_)"
1577 return "snapshot (" . join(' ', map
1578 $cgi->a({
1579 -href => href(
1580 action=>"snapshot",
1581 hash=>$hash,
1582 snapshot_format=>$_
1584 }, $known_snapshot_formats{$_}{'display'})
1585 , @snapshot_fmts) . ")";
1586 } elsif ($num_fmts == 1) {
1587 # A single "snapshot" link whose tooltip bears the format name.
1588 # i.e. "_snapshot_"
1589 my ($fmt) = @snapshot_fmts;
1590 return
1591 $cgi->a({
1592 -href => href(
1593 action=>"snapshot",
1594 hash=>$hash,
1595 snapshot_format=>$fmt
1597 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1598 }, "snapshot");
1599 } else { # $num_fmts == 0
1600 return undef;
1604 ## ......................................................................
1605 ## functions returning values to be passed, perhaps after some
1606 ## transformation, to other functions; e.g. returning arguments to href()
1608 # returns hash to be passed to href to generate gitweb URL
1609 # in -title key it returns description of link
1610 sub get_feed_info {
1611 my $format = shift || 'Atom';
1612 my %res = (action => lc($format));
1614 # feed links are possible only for project views
1615 return unless (defined $project);
1616 # some views should link to OPML, or to generic project feed,
1617 # or don't have specific feed yet (so they should use generic)
1618 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1620 my $branch;
1621 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1622 # from tag links; this also makes possible to detect branch links
1623 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1624 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1625 $branch = $1;
1627 # find log type for feed description (title)
1628 my $type = 'log';
1629 if (defined $file_name) {
1630 $type = "history of $file_name";
1631 $type .= "/" if ($action eq 'tree');
1632 $type .= " on '$branch'" if (defined $branch);
1633 } else {
1634 $type = "log of $branch" if (defined $branch);
1637 $res{-title} = $type;
1638 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1639 $res{'file_name'} = $file_name;
1641 return %res;
1644 ## ----------------------------------------------------------------------
1645 ## git utility subroutines, invoking git commands
1647 # returns path to the core git executable and the --git-dir parameter as list
1648 sub git_cmd {
1649 return $GIT, '--git-dir='.$git_dir;
1652 # quote the given arguments for passing them to the shell
1653 # quote_command("command", "arg 1", "arg with ' and ! characters")
1654 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1655 # Try to avoid using this function wherever possible.
1656 sub quote_command {
1657 return join(' ',
1658 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
1661 # get HEAD ref of given project as hash
1662 sub git_get_head_hash {
1663 my $project = shift;
1664 my $o_git_dir = $git_dir;
1665 my $retval = undef;
1666 $git_dir = "$projectroot/$project";
1667 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1668 my $head = <$fd>;
1669 close $fd;
1670 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1671 $retval = $1;
1674 if (defined $o_git_dir) {
1675 $git_dir = $o_git_dir;
1677 return $retval;
1680 # get type of given object
1681 sub git_get_type {
1682 my $hash = shift;
1684 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
1685 my $type = <$fd>;
1686 close $fd or return;
1687 chomp $type;
1688 return $type;
1691 # repository configuration
1692 our $config_file = '';
1693 our %config;
1695 # store multiple values for single key as anonymous array reference
1696 # single values stored directly in the hash, not as [ <value> ]
1697 sub hash_set_multi {
1698 my ($hash, $key, $value) = @_;
1700 if (!exists $hash->{$key}) {
1701 $hash->{$key} = $value;
1702 } elsif (!ref $hash->{$key}) {
1703 $hash->{$key} = [ $hash->{$key}, $value ];
1704 } else {
1705 push @{$hash->{$key}}, $value;
1709 # return hash of git project configuration
1710 # optionally limited to some section, e.g. 'gitweb'
1711 sub git_parse_project_config {
1712 my $section_regexp = shift;
1713 my %config;
1715 local $/ = "\0";
1717 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1718 or return;
1720 while (my $keyval = <$fh>) {
1721 chomp $keyval;
1722 my ($key, $value) = split(/\n/, $keyval, 2);
1724 hash_set_multi(\%config, $key, $value)
1725 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1727 close $fh;
1729 return %config;
1732 # convert config value to boolean, 'true' or 'false'
1733 # no value, number > 0, 'true' and 'yes' values are true
1734 # rest of values are treated as false (never as error)
1735 sub config_to_bool {
1736 my $val = shift;
1738 # strip leading and trailing whitespace
1739 $val =~ s/^\s+//;
1740 $val =~ s/\s+$//;
1742 return (!defined $val || # section.key
1743 ($val =~ /^\d+$/ && $val) || # section.key = 1
1744 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1747 # convert config value to simple decimal number
1748 # an optional value suffix of 'k', 'm', or 'g' will cause the value
1749 # to be multiplied by 1024, 1048576, or 1073741824
1750 sub config_to_int {
1751 my $val = shift;
1753 # strip leading and trailing whitespace
1754 $val =~ s/^\s+//;
1755 $val =~ s/\s+$//;
1757 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1758 $unit = lc($unit);
1759 # unknown unit is treated as 1
1760 return $num * ($unit eq 'g' ? 1073741824 :
1761 $unit eq 'm' ? 1048576 :
1762 $unit eq 'k' ? 1024 : 1);
1764 return $val;
1767 # convert config value to array reference, if needed
1768 sub config_to_multi {
1769 my $val = shift;
1771 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
1774 sub git_get_project_config {
1775 my ($key, $type) = @_;
1777 # key sanity check
1778 return unless ($key);
1779 $key =~ s/^gitweb\.//;
1780 return if ($key =~ m/\W/);
1782 # type sanity check
1783 if (defined $type) {
1784 $type =~ s/^--//;
1785 $type = undef
1786 unless ($type eq 'bool' || $type eq 'int');
1789 # get config
1790 if (!defined $config_file ||
1791 $config_file ne "$git_dir/config") {
1792 %config = git_parse_project_config('gitweb');
1793 $config_file = "$git_dir/config";
1796 # ensure given type
1797 if (!defined $type) {
1798 return $config{"gitweb.$key"};
1799 } elsif ($type eq 'bool') {
1800 # backward compatibility: 'git config --bool' returns true/false
1801 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1802 } elsif ($type eq 'int') {
1803 return config_to_int($config{"gitweb.$key"});
1805 return $config{"gitweb.$key"};
1808 # get hash of given path at given ref
1809 sub git_get_hash_by_path {
1810 my $base = shift;
1811 my $path = shift || return undef;
1812 my $type = shift;
1814 $path =~ s,/+$,,;
1816 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
1817 or die_error(500, "Open git-ls-tree failed");
1818 my $line = <$fd>;
1819 close $fd or return undef;
1821 if (!defined $line) {
1822 # there is no tree or hash given by $path at $base
1823 return undef;
1826 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
1827 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1828 if (defined $type && $type ne $2) {
1829 # type doesn't match
1830 return undef;
1832 return $3;
1835 # get path of entry with given hash at given tree-ish (ref)
1836 # used to get 'from' filename for combined diff (merge commit) for renames
1837 sub git_get_path_by_hash {
1838 my $base = shift || return;
1839 my $hash = shift || return;
1841 local $/ = "\0";
1843 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
1844 or return undef;
1845 while (my $line = <$fd>) {
1846 chomp $line;
1848 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
1849 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
1850 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
1851 close $fd;
1852 return $1;
1855 close $fd;
1856 return undef;
1859 ## ......................................................................
1860 ## git utility functions, directly accessing git repository
1862 sub git_get_project_description {
1863 my $path = shift;
1865 $git_dir = "$projectroot/$path";
1866 open my $fd, "$git_dir/description"
1867 or return git_get_project_config('description');
1868 my $descr = <$fd>;
1869 close $fd;
1870 if (defined $descr) {
1871 chomp $descr;
1873 return $descr;
1876 sub git_get_project_ctags {
1877 my $path = shift;
1878 my $ctags = {};
1880 $git_dir = "$projectroot/$path";
1881 foreach (<$git_dir/ctags/*>) {
1882 open CT, $_ or next;
1883 my $val = <CT>;
1884 chomp $val;
1885 close CT;
1886 my $ctag = $_; $ctag =~ s#.*/##;
1887 $ctags->{$ctag} = $val;
1889 $ctags;
1892 sub git_populate_project_tagcloud {
1893 my $ctags = shift;
1895 # First, merge different-cased tags; tags vote on casing
1896 my %ctags_lc;
1897 foreach (keys %$ctags) {
1898 $ctags_lc{lc $_}->{count} += $ctags->{$_};
1899 if (not $ctags_lc{lc $_}->{topcount}
1900 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
1901 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
1902 $ctags_lc{lc $_}->{topname} = $_;
1906 my $cloud;
1907 if (eval { require HTML::TagCloud; 1; }) {
1908 $cloud = HTML::TagCloud->new;
1909 foreach (sort keys %ctags_lc) {
1910 # Pad the title with spaces so that the cloud looks
1911 # less crammed.
1912 my $title = $ctags_lc{$_}->{topname};
1913 $title =~ s/ /&nbsp;/g;
1914 $title =~ s/^/&nbsp;/g;
1915 $title =~ s/$/&nbsp;/g;
1916 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
1918 } else {
1919 $cloud = \%ctags_lc;
1921 $cloud;
1924 sub git_show_project_tagcloud {
1925 my ($cloud, $count) = @_;
1926 print STDERR ref($cloud)."..\n";
1927 if (ref $cloud eq 'HTML::TagCloud') {
1928 return $cloud->html_and_css($count);
1929 } else {
1930 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
1931 return '<p align="center">' . join (', ', map {
1932 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
1933 } splice(@tags, 0, $count)) . '</p>';
1937 sub git_get_project_url_list {
1938 my $path = shift;
1940 $git_dir = "$projectroot/$path";
1941 open my $fd, "$git_dir/cloneurl"
1942 or return wantarray ?
1943 @{ config_to_multi(git_get_project_config('url')) } :
1944 config_to_multi(git_get_project_config('url'));
1945 my @git_project_url_list = map { chomp; $_ } <$fd>;
1946 close $fd;
1948 return wantarray ? @git_project_url_list : \@git_project_url_list;
1951 sub git_get_projects_list {
1952 my ($filter) = @_;
1953 my @list;
1955 $filter ||= '';
1956 $filter =~ s/\.git$//;
1958 my ($check_forks) = gitweb_check_feature('forks');
1960 if (-d $projects_list) {
1961 # search in directory
1962 my $dir = $projects_list . ($filter ? "/$filter" : '');
1963 # remove the trailing "/"
1964 $dir =~ s!/+$!!;
1965 my $pfxlen = length("$dir");
1966 my $pfxdepth = ($dir =~ tr!/!!);
1968 File::Find::find({
1969 follow_fast => 1, # follow symbolic links
1970 follow_skip => 2, # ignore duplicates
1971 dangling_symlinks => 0, # ignore dangling symlinks, silently
1972 wanted => sub {
1973 # skip project-list toplevel, if we get it.
1974 return if (m!^[/.]$!);
1975 # only directories can be git repositories
1976 return unless (-d $_);
1977 # don't traverse too deep (Find is super slow on os x)
1978 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
1979 $File::Find::prune = 1;
1980 return;
1983 my $subdir = substr($File::Find::name, $pfxlen + 1);
1984 # we check related file in $projectroot
1985 if (check_export_ok("$projectroot/$filter/$subdir")) {
1986 push @list, { path => ($filter ? "$filter/" : '') . $subdir };
1987 $File::Find::prune = 1;
1990 }, "$dir");
1992 } elsif (-f $projects_list) {
1993 # read from file(url-encoded):
1994 # 'git%2Fgit.git Linus+Torvalds'
1995 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
1996 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
1997 my %paths;
1998 open my ($fd), $projects_list or return;
1999 PROJECT:
2000 while (my $line = <$fd>) {
2001 chomp $line;
2002 my ($path, $owner) = split ' ', $line;
2003 $path = unescape($path);
2004 $owner = unescape($owner);
2005 if (!defined $path) {
2006 next;
2008 if ($filter ne '') {
2009 # looking for forks;
2010 my $pfx = substr($path, 0, length($filter));
2011 if ($pfx ne $filter) {
2012 next PROJECT;
2014 my $sfx = substr($path, length($filter));
2015 if ($sfx !~ /^\/.*\.git$/) {
2016 next PROJECT;
2018 } elsif ($check_forks) {
2019 PATH:
2020 foreach my $filter (keys %paths) {
2021 # looking for forks;
2022 my $pfx = substr($path, 0, length($filter));
2023 if ($pfx ne $filter) {
2024 next PATH;
2026 my $sfx = substr($path, length($filter));
2027 if ($sfx !~ /^\/.*\.git$/) {
2028 next PATH;
2030 # is a fork, don't include it in
2031 # the list
2032 next PROJECT;
2035 if (check_export_ok("$projectroot/$path")) {
2036 my $pr = {
2037 path => $path,
2038 owner => to_utf8($owner),
2040 push @list, $pr;
2041 (my $forks_path = $path) =~ s/\.git$//;
2042 $paths{$forks_path}++;
2045 close $fd;
2047 return @list;
2050 our $gitweb_project_owner = undef;
2051 sub git_get_project_list_from_file {
2053 return if (defined $gitweb_project_owner);
2055 $gitweb_project_owner = {};
2056 # read from file (url-encoded):
2057 # 'git%2Fgit.git Linus+Torvalds'
2058 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2059 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2060 if (-f $projects_list) {
2061 open (my $fd , $projects_list);
2062 while (my $line = <$fd>) {
2063 chomp $line;
2064 my ($pr, $ow) = split ' ', $line;
2065 $pr = unescape($pr);
2066 $ow = unescape($ow);
2067 $gitweb_project_owner->{$pr} = to_utf8($ow);
2069 close $fd;
2073 sub git_get_project_owner {
2074 my $project = shift;
2075 my $owner;
2077 return undef unless $project;
2078 $git_dir = "$projectroot/$project";
2080 if (!defined $gitweb_project_owner) {
2081 git_get_project_list_from_file();
2084 if (exists $gitweb_project_owner->{$project}) {
2085 $owner = $gitweb_project_owner->{$project};
2087 if (!defined $owner){
2088 $owner = git_get_project_config('owner');
2090 if (!defined $owner) {
2091 $owner = get_file_owner("$git_dir");
2094 return $owner;
2097 sub git_get_last_activity {
2098 my ($path) = @_;
2099 my $fd;
2101 $git_dir = "$projectroot/$path";
2102 open($fd, "-|", git_cmd(), 'for-each-ref',
2103 '--format=%(committer)',
2104 '--sort=-committerdate',
2105 '--count=1',
2106 'refs/heads') or return;
2107 my $most_recent = <$fd>;
2108 close $fd or return;
2109 if (defined $most_recent &&
2110 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2111 my $timestamp = $1;
2112 my $age = time - $timestamp;
2113 return ($age, age_string($age));
2115 return (undef, undef);
2118 sub git_get_references {
2119 my $type = shift || "";
2120 my %refs;
2121 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2122 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2123 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2124 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2125 or return;
2127 while (my $line = <$fd>) {
2128 chomp $line;
2129 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2130 if (defined $refs{$1}) {
2131 push @{$refs{$1}}, $2;
2132 } else {
2133 $refs{$1} = [ $2 ];
2137 close $fd or return;
2138 return \%refs;
2141 sub git_get_rev_name_tags {
2142 my $hash = shift || return undef;
2144 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2145 or return;
2146 my $name_rev = <$fd>;
2147 close $fd;
2149 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2150 return $1;
2151 } else {
2152 # catches also '$hash undefined' output
2153 return undef;
2157 ## ----------------------------------------------------------------------
2158 ## parse to hash functions
2160 sub parse_date {
2161 my $epoch = shift;
2162 my $tz = shift || "-0000";
2164 my %date;
2165 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2166 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2167 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2168 $date{'hour'} = $hour;
2169 $date{'minute'} = $min;
2170 $date{'mday'} = $mday;
2171 $date{'day'} = $days[$wday];
2172 $date{'month'} = $months[$mon];
2173 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2174 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2175 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2176 $mday, $months[$mon], $hour ,$min;
2177 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2178 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2180 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2181 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2182 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2183 $date{'hour_local'} = $hour;
2184 $date{'minute_local'} = $min;
2185 $date{'tz_local'} = $tz;
2186 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2187 1900+$year, $mon+1, $mday,
2188 $hour, $min, $sec, $tz);
2189 return %date;
2192 sub parse_tag {
2193 my $tag_id = shift;
2194 my %tag;
2195 my @comment;
2197 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2198 $tag{'id'} = $tag_id;
2199 while (my $line = <$fd>) {
2200 chomp $line;
2201 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2202 $tag{'object'} = $1;
2203 } elsif ($line =~ m/^type (.+)$/) {
2204 $tag{'type'} = $1;
2205 } elsif ($line =~ m/^tag (.+)$/) {
2206 $tag{'name'} = $1;
2207 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2208 $tag{'author'} = $1;
2209 $tag{'epoch'} = $2;
2210 $tag{'tz'} = $3;
2211 } elsif ($line =~ m/--BEGIN/) {
2212 push @comment, $line;
2213 last;
2214 } elsif ($line eq "") {
2215 last;
2218 push @comment, <$fd>;
2219 $tag{'comment'} = \@comment;
2220 close $fd or return;
2221 if (!defined $tag{'name'}) {
2222 return
2224 return %tag
2227 sub parse_commit_text {
2228 my ($commit_text, $withparents) = @_;
2229 my @commit_lines = split '\n', $commit_text;
2230 my %co;
2232 pop @commit_lines; # Remove '\0'
2234 if (! @commit_lines) {
2235 return;
2238 my $header = shift @commit_lines;
2239 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2240 return;
2242 ($co{'id'}, my @parents) = split ' ', $header;
2243 while (my $line = shift @commit_lines) {
2244 last if $line eq "\n";
2245 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2246 $co{'tree'} = $1;
2247 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2248 push @parents, $1;
2249 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2250 $co{'author'} = $1;
2251 $co{'author_epoch'} = $2;
2252 $co{'author_tz'} = $3;
2253 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2254 $co{'author_name'} = $1;
2255 $co{'author_email'} = $2;
2256 } else {
2257 $co{'author_name'} = $co{'author'};
2259 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2260 $co{'committer'} = $1;
2261 $co{'committer_epoch'} = $2;
2262 $co{'committer_tz'} = $3;
2263 $co{'committer_name'} = $co{'committer'};
2264 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2265 $co{'committer_name'} = $1;
2266 $co{'committer_email'} = $2;
2267 } else {
2268 $co{'committer_name'} = $co{'committer'};
2272 if (!defined $co{'tree'}) {
2273 return;
2275 $co{'parents'} = \@parents;
2276 $co{'parent'} = $parents[0];
2278 foreach my $title (@commit_lines) {
2279 $title =~ s/^ //;
2280 if ($title ne "") {
2281 $co{'title'} = chop_str($title, 80, 5);
2282 # remove leading stuff of merges to make the interesting part visible
2283 if (length($title) > 50) {
2284 $title =~ s/^Automatic //;
2285 $title =~ s/^merge (of|with) /Merge ... /i;
2286 if (length($title) > 50) {
2287 $title =~ s/(http|rsync):\/\///;
2289 if (length($title) > 50) {
2290 $title =~ s/(master|www|rsync)\.//;
2292 if (length($title) > 50) {
2293 $title =~ s/kernel.org:?//;
2295 if (length($title) > 50) {
2296 $title =~ s/\/pub\/scm//;
2299 $co{'title_short'} = chop_str($title, 50, 5);
2300 last;
2303 if (! defined $co{'title'} || $co{'title'} eq "") {
2304 $co{'title'} = $co{'title_short'} = '(no commit message)';
2306 # remove added spaces
2307 foreach my $line (@commit_lines) {
2308 $line =~ s/^ //;
2310 $co{'comment'} = \@commit_lines;
2312 my $age = time - $co{'committer_epoch'};
2313 $co{'age'} = $age;
2314 $co{'age_string'} = age_string($age);
2315 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2316 if ($age > 60*60*24*7*2) {
2317 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2318 $co{'age_string_age'} = $co{'age_string'};
2319 } else {
2320 $co{'age_string_date'} = $co{'age_string'};
2321 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2323 return %co;
2326 sub parse_commit {
2327 my ($commit_id) = @_;
2328 my %co;
2330 local $/ = "\0";
2332 open my $fd, "-|", git_cmd(), "rev-list",
2333 "--parents",
2334 "--header",
2335 "--max-count=1",
2336 $commit_id,
2337 "--",
2338 or die_error(500, "Open git-rev-list failed");
2339 %co = parse_commit_text(<$fd>, 1);
2340 close $fd;
2342 return %co;
2345 sub parse_commits {
2346 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2347 my @cos;
2349 $maxcount ||= 1;
2350 $skip ||= 0;
2352 local $/ = "\0";
2354 open my $fd, "-|", git_cmd(), "rev-list",
2355 "--header",
2356 @args,
2357 ("--max-count=" . $maxcount),
2358 ("--skip=" . $skip),
2359 @extra_options,
2360 $commit_id,
2361 "--",
2362 ($filename ? ($filename) : ())
2363 or die_error(500, "Open git-rev-list failed");
2364 while (my $line = <$fd>) {
2365 my %co = parse_commit_text($line);
2366 push @cos, \%co;
2368 close $fd;
2370 return wantarray ? @cos : \@cos;
2373 # parse line of git-diff-tree "raw" output
2374 sub parse_difftree_raw_line {
2375 my $line = shift;
2376 my %res;
2378 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2379 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2380 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2381 $res{'from_mode'} = $1;
2382 $res{'to_mode'} = $2;
2383 $res{'from_id'} = $3;
2384 $res{'to_id'} = $4;
2385 $res{'status'} = $5;
2386 $res{'similarity'} = $6;
2387 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2388 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2389 } else {
2390 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2393 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2394 # combined diff (for merge commit)
2395 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2396 $res{'nparents'} = length($1);
2397 $res{'from_mode'} = [ split(' ', $2) ];
2398 $res{'to_mode'} = pop @{$res{'from_mode'}};
2399 $res{'from_id'} = [ split(' ', $3) ];
2400 $res{'to_id'} = pop @{$res{'from_id'}};
2401 $res{'status'} = [ split('', $4) ];
2402 $res{'to_file'} = unquote($5);
2404 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2405 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2406 $res{'commit'} = $1;
2409 return wantarray ? %res : \%res;
2412 # wrapper: return parsed line of git-diff-tree "raw" output
2413 # (the argument might be raw line, or parsed info)
2414 sub parsed_difftree_line {
2415 my $line_or_ref = shift;
2417 if (ref($line_or_ref) eq "HASH") {
2418 # pre-parsed (or generated by hand)
2419 return $line_or_ref;
2420 } else {
2421 return parse_difftree_raw_line($line_or_ref);
2425 # parse line of git-ls-tree output
2426 sub parse_ls_tree_line ($;%) {
2427 my $line = shift;
2428 my %opts = @_;
2429 my %res;
2431 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2432 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2434 $res{'mode'} = $1;
2435 $res{'type'} = $2;
2436 $res{'hash'} = $3;
2437 if ($opts{'-z'}) {
2438 $res{'name'} = $4;
2439 } else {
2440 $res{'name'} = unquote($4);
2443 return wantarray ? %res : \%res;
2446 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2447 sub parse_from_to_diffinfo {
2448 my ($diffinfo, $from, $to, @parents) = @_;
2450 if ($diffinfo->{'nparents'}) {
2451 # combined diff
2452 $from->{'file'} = [];
2453 $from->{'href'} = [];
2454 fill_from_file_info($diffinfo, @parents)
2455 unless exists $diffinfo->{'from_file'};
2456 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2457 $from->{'file'}[$i] =
2458 defined $diffinfo->{'from_file'}[$i] ?
2459 $diffinfo->{'from_file'}[$i] :
2460 $diffinfo->{'to_file'};
2461 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2462 $from->{'href'}[$i] = href(action=>"blob",
2463 hash_base=>$parents[$i],
2464 hash=>$diffinfo->{'from_id'}[$i],
2465 file_name=>$from->{'file'}[$i]);
2466 } else {
2467 $from->{'href'}[$i] = undef;
2470 } else {
2471 # ordinary (not combined) diff
2472 $from->{'file'} = $diffinfo->{'from_file'};
2473 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2474 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2475 hash=>$diffinfo->{'from_id'},
2476 file_name=>$from->{'file'});
2477 } else {
2478 delete $from->{'href'};
2482 $to->{'file'} = $diffinfo->{'to_file'};
2483 if (!is_deleted($diffinfo)) { # file exists in result
2484 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2485 hash=>$diffinfo->{'to_id'},
2486 file_name=>$to->{'file'});
2487 } else {
2488 delete $to->{'href'};
2492 ## ......................................................................
2493 ## parse to array of hashes functions
2495 sub git_get_heads_list {
2496 my $limit = shift;
2497 my @headslist;
2499 open my $fd, '-|', git_cmd(), 'for-each-ref',
2500 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2501 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2502 'refs/heads'
2503 or return;
2504 while (my $line = <$fd>) {
2505 my %ref_item;
2507 chomp $line;
2508 my ($refinfo, $committerinfo) = split(/\0/, $line);
2509 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2510 my ($committer, $epoch, $tz) =
2511 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2512 $ref_item{'fullname'} = $name;
2513 $name =~ s!^refs/heads/!!;
2515 $ref_item{'name'} = $name;
2516 $ref_item{'id'} = $hash;
2517 $ref_item{'title'} = $title || '(no commit message)';
2518 $ref_item{'epoch'} = $epoch;
2519 if ($epoch) {
2520 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2521 } else {
2522 $ref_item{'age'} = "unknown";
2525 push @headslist, \%ref_item;
2527 close $fd;
2529 return wantarray ? @headslist : \@headslist;
2532 sub git_get_tags_list {
2533 my $limit = shift;
2534 my @tagslist;
2536 open my $fd, '-|', git_cmd(), 'for-each-ref',
2537 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2538 '--format=%(objectname) %(objecttype) %(refname) '.
2539 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2540 'refs/tags'
2541 or return;
2542 while (my $line = <$fd>) {
2543 my %ref_item;
2545 chomp $line;
2546 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2547 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2548 my ($creator, $epoch, $tz) =
2549 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2550 $ref_item{'fullname'} = $name;
2551 $name =~ s!^refs/tags/!!;
2553 $ref_item{'type'} = $type;
2554 $ref_item{'id'} = $id;
2555 $ref_item{'name'} = $name;
2556 if ($type eq "tag") {
2557 $ref_item{'subject'} = $title;
2558 $ref_item{'reftype'} = $reftype;
2559 $ref_item{'refid'} = $refid;
2560 } else {
2561 $ref_item{'reftype'} = $type;
2562 $ref_item{'refid'} = $id;
2565 if ($type eq "tag" || $type eq "commit") {
2566 $ref_item{'epoch'} = $epoch;
2567 if ($epoch) {
2568 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2569 } else {
2570 $ref_item{'age'} = "unknown";
2574 push @tagslist, \%ref_item;
2576 close $fd;
2578 return wantarray ? @tagslist : \@tagslist;
2581 ## ----------------------------------------------------------------------
2582 ## filesystem-related functions
2584 sub get_file_owner {
2585 my $path = shift;
2587 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2588 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2589 if (!defined $gcos) {
2590 return undef;
2592 my $owner = $gcos;
2593 $owner =~ s/[,;].*$//;
2594 return to_utf8($owner);
2597 ## ......................................................................
2598 ## mimetype related functions
2600 sub mimetype_guess_file {
2601 my $filename = shift;
2602 my $mimemap = shift;
2603 -r $mimemap or return undef;
2605 my %mimemap;
2606 open(MIME, $mimemap) or return undef;
2607 while (<MIME>) {
2608 next if m/^#/; # skip comments
2609 my ($mime, $exts) = split(/\t+/);
2610 if (defined $exts) {
2611 my @exts = split(/\s+/, $exts);
2612 foreach my $ext (@exts) {
2613 $mimemap{$ext} = $mime;
2617 close(MIME);
2619 $filename =~ /\.([^.]*)$/;
2620 return $mimemap{$1};
2623 sub mimetype_guess {
2624 my $filename = shift;
2625 my $mime;
2626 $filename =~ /\./ or return undef;
2628 if ($mimetypes_file) {
2629 my $file = $mimetypes_file;
2630 if ($file !~ m!^/!) { # if it is relative path
2631 # it is relative to project
2632 $file = "$projectroot/$project/$file";
2634 $mime = mimetype_guess_file($filename, $file);
2636 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2637 return $mime;
2640 sub blob_mimetype {
2641 my $fd = shift;
2642 my $filename = shift;
2644 if ($filename) {
2645 my $mime = mimetype_guess($filename);
2646 $mime and return $mime;
2649 # just in case
2650 return $default_blob_plain_mimetype unless $fd;
2652 if (-T $fd) {
2653 return 'text/plain';
2654 } elsif (! $filename) {
2655 return 'application/octet-stream';
2656 } elsif ($filename =~ m/\.png$/i) {
2657 return 'image/png';
2658 } elsif ($filename =~ m/\.gif$/i) {
2659 return 'image/gif';
2660 } elsif ($filename =~ m/\.jpe?g$/i) {
2661 return 'image/jpeg';
2662 } else {
2663 return 'application/octet-stream';
2667 sub blob_contenttype {
2668 my ($fd, $file_name, $type) = @_;
2670 $type ||= blob_mimetype($fd, $file_name);
2671 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2672 $type .= "; charset=$default_text_plain_charset";
2675 return $type;
2678 ## ======================================================================
2679 ## functions printing HTML: header, footer, error page
2681 sub git_header_html {
2682 my $status = shift || "200 OK";
2683 my $expires = shift;
2685 my $title = "$site_name";
2686 if (defined $project) {
2687 $title .= " - " . to_utf8($project);
2688 if (defined $action) {
2689 $title .= "/$action";
2690 if (defined $file_name) {
2691 $title .= " - " . esc_path($file_name);
2692 if ($action eq "tree" && $file_name !~ m|/$|) {
2693 $title .= "/";
2698 my $content_type;
2699 # require explicit support from the UA if we are to send the page as
2700 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2701 # we have to do this because MSIE sometimes globs '*/*', pretending to
2702 # support xhtml+xml but choking when it gets what it asked for.
2703 if (defined $cgi->http('HTTP_ACCEPT') &&
2704 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2705 $cgi->Accept('application/xhtml+xml') != 0) {
2706 $content_type = 'application/xhtml+xml';
2707 } else {
2708 $content_type = 'text/html';
2710 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2711 -status=> $status, -expires => $expires);
2712 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
2713 print <<EOF;
2714 <?xml version="1.0" encoding="utf-8"?>
2715 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2716 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
2717 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
2718 <!-- git core binaries version $git_version -->
2719 <head>
2720 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
2721 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
2722 <meta name="robots" content="index, nofollow"/>
2723 <title>$title</title>
2725 # print out each stylesheet that exist
2726 if (defined $stylesheet) {
2727 #provides backwards capability for those people who define style sheet in a config file
2728 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2729 } else {
2730 foreach my $stylesheet (@stylesheets) {
2731 next unless $stylesheet;
2732 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2735 if (defined $project) {
2736 my %href_params = get_feed_info();
2737 if (!exists $href_params{'-title'}) {
2738 $href_params{'-title'} = 'log';
2741 foreach my $format qw(RSS Atom) {
2742 my $type = lc($format);
2743 my %link_attr = (
2744 '-rel' => 'alternate',
2745 '-title' => "$project - $href_params{'-title'} - $format feed",
2746 '-type' => "application/$type+xml"
2749 $href_params{'action'} = $type;
2750 $link_attr{'-href'} = href(%href_params);
2751 print "<link ".
2752 "rel=\"$link_attr{'-rel'}\" ".
2753 "title=\"$link_attr{'-title'}\" ".
2754 "href=\"$link_attr{'-href'}\" ".
2755 "type=\"$link_attr{'-type'}\" ".
2756 "/>\n";
2758 $href_params{'extra_options'} = '--no-merges';
2759 $link_attr{'-href'} = href(%href_params);
2760 $link_attr{'-title'} .= ' (no merges)';
2761 print "<link ".
2762 "rel=\"$link_attr{'-rel'}\" ".
2763 "title=\"$link_attr{'-title'}\" ".
2764 "href=\"$link_attr{'-href'}\" ".
2765 "type=\"$link_attr{'-type'}\" ".
2766 "/>\n";
2769 } else {
2770 printf('<link rel="alternate" title="%s projects list" '.
2771 'href="%s" type="text/plain; charset=utf-8" />'."\n",
2772 $site_name, href(project=>undef, action=>"project_index"));
2773 printf('<link rel="alternate" title="%s projects feeds" '.
2774 'href="%s" type="text/x-opml" />'."\n",
2775 $site_name, href(project=>undef, action=>"opml"));
2777 if (defined $favicon) {
2778 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
2781 print "</head>\n" .
2782 "<body>\n";
2784 if (-f $site_header) {
2785 open (my $fd, $site_header);
2786 print <$fd>;
2787 close $fd;
2790 print "<div class=\"page_header\">\n" .
2791 $cgi->a({-href => esc_url($logo_url),
2792 -title => $logo_label},
2793 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
2794 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
2795 if (defined $project) {
2796 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
2797 if (defined $action) {
2798 print " / $action";
2800 print "\n";
2802 print "</div>\n";
2804 my ($have_search) = gitweb_check_feature('search');
2805 if (defined $project && $have_search) {
2806 if (!defined $searchtext) {
2807 $searchtext = "";
2809 my $search_hash;
2810 if (defined $hash_base) {
2811 $search_hash = $hash_base;
2812 } elsif (defined $hash) {
2813 $search_hash = $hash;
2814 } else {
2815 $search_hash = "HEAD";
2817 my $action = $my_uri;
2818 my ($use_pathinfo) = gitweb_check_feature('pathinfo');
2819 if ($use_pathinfo) {
2820 $action .= "/".esc_url($project);
2822 print $cgi->startform(-method => "get", -action => $action) .
2823 "<div class=\"search\">\n" .
2824 (!$use_pathinfo &&
2825 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
2826 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
2827 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
2828 $cgi->popup_menu(-name => 'st', -default => 'commit',
2829 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
2830 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
2831 " search:\n",
2832 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
2833 "<span title=\"Extended regular expression\">" .
2834 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
2835 -checked => $search_use_regexp) .
2836 "</span>" .
2837 "</div>" .
2838 $cgi->end_form() . "\n";
2842 sub git_footer_html {
2843 my $feed_class = 'rss_logo';
2845 print "<div class=\"page_footer\">\n";
2846 if (defined $project) {
2847 my $descr = git_get_project_description($project);
2848 if (defined $descr) {
2849 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
2852 my %href_params = get_feed_info();
2853 if (!%href_params) {
2854 $feed_class .= ' generic';
2856 $href_params{'-title'} ||= 'log';
2858 foreach my $format qw(RSS Atom) {
2859 $href_params{'action'} = lc($format);
2860 print $cgi->a({-href => href(%href_params),
2861 -title => "$href_params{'-title'} $format feed",
2862 -class => $feed_class}, $format)."\n";
2865 } else {
2866 print $cgi->a({-href => href(project=>undef, action=>"opml"),
2867 -class => $feed_class}, "OPML") . " ";
2868 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
2869 -class => $feed_class}, "TXT") . "\n";
2871 print "</div>\n"; # class="page_footer"
2873 if (-f $site_footer) {
2874 open (my $fd, $site_footer);
2875 print <$fd>;
2876 close $fd;
2879 print "</body>\n" .
2880 "</html>";
2883 # die_error(<http_status_code>, <error_message>)
2884 # Example: die_error(404, 'Hash not found')
2885 # By convention, use the following status codes (as defined in RFC 2616):
2886 # 400: Invalid or missing CGI parameters, or
2887 # requested object exists but has wrong type.
2888 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
2889 # this server or project.
2890 # 404: Requested object/revision/project doesn't exist.
2891 # 500: The server isn't configured properly, or
2892 # an internal error occurred (e.g. failed assertions caused by bugs), or
2893 # an unknown error occurred (e.g. the git binary died unexpectedly).
2894 sub die_error {
2895 my $status = shift || 500;
2896 my $error = shift || "Internal server error";
2898 my %http_responses = (400 => '400 Bad Request',
2899 403 => '403 Forbidden',
2900 404 => '404 Not Found',
2901 500 => '500 Internal Server Error');
2902 git_header_html($http_responses{$status});
2903 print <<EOF;
2904 <div class="page_body">
2905 <br /><br />
2906 $status - $error
2907 <br />
2908 </div>
2910 git_footer_html();
2911 exit;
2914 ## ----------------------------------------------------------------------
2915 ## functions printing or outputting HTML: navigation
2917 sub git_print_page_nav {
2918 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
2919 $extra = '' if !defined $extra; # pager or formats
2921 my @navs = qw(summary shortlog log commit commitdiff tree);
2922 if ($suppress) {
2923 @navs = grep { $_ ne $suppress } @navs;
2926 my %arg = map { $_ => {action=>$_} } @navs;
2927 if (defined $head) {
2928 for (qw(commit commitdiff)) {
2929 $arg{$_}{'hash'} = $head;
2931 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
2932 for (qw(shortlog log)) {
2933 $arg{$_}{'hash'} = $head;
2938 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
2939 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
2941 my @actions = gitweb_check_feature('actions');
2942 while (@actions) {
2943 my ($label, $link, $pos) = (shift(@actions), shift(@actions), shift(@actions));
2944 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
2945 # munch munch
2946 $link =~ s#%n#$project#g;
2947 $link =~ s#%f#$git_dir#g;
2948 $treehead ? $link =~ s#%h#$treehead#g : $link =~ s#%h##g;
2949 $treebase ? $link =~ s#%b#$treebase#g : $link =~ s#%b##g;
2950 $arg{$label}{'_href'} = $link;
2953 print "<div class=\"page_nav\">\n" .
2954 (join " | ",
2955 map { $_ eq $current ?
2956 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
2957 } @navs);
2958 print "<br/>\n$extra<br/>\n" .
2959 "</div>\n";
2962 sub format_paging_nav {
2963 my ($action, $hash, $head, $page, $has_next_link) = @_;
2964 my $paging_nav;
2967 if ($hash ne $head || $page) {
2968 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
2969 } else {
2970 $paging_nav .= "HEAD";
2973 if ($page > 0) {
2974 $paging_nav .= " &sdot; " .
2975 $cgi->a({-href => href(-replay=>1, page=>$page-1),
2976 -accesskey => "p", -title => "Alt-p"}, "prev");
2977 } else {
2978 $paging_nav .= " &sdot; prev";
2981 if ($has_next_link) {
2982 $paging_nav .= " &sdot; " .
2983 $cgi->a({-href => href(-replay=>1, page=>$page+1),
2984 -accesskey => "n", -title => "Alt-n"}, "next");
2985 } else {
2986 $paging_nav .= " &sdot; next";
2989 return $paging_nav;
2992 ## ......................................................................
2993 ## functions printing or outputting HTML: div
2995 sub git_print_header_div {
2996 my ($action, $title, $hash, $hash_base) = @_;
2997 my %args = ();
2999 $args{'action'} = $action;
3000 $args{'hash'} = $hash if $hash;
3001 $args{'hash_base'} = $hash_base if $hash_base;
3003 print "<div class=\"header\">\n" .
3004 $cgi->a({-href => href(%args), -class => "title"},
3005 $title ? $title : $action) .
3006 "\n</div>\n";
3009 #sub git_print_authorship (\%) {
3010 sub git_print_authorship {
3011 my $co = shift;
3013 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3014 print "<div class=\"author_date\">" .
3015 esc_html($co->{'author_name'}) .
3016 " [$ad{'rfc2822'}";
3017 if ($ad{'hour_local'} < 6) {
3018 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3019 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3020 } else {
3021 printf(" (%02d:%02d %s)",
3022 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3024 print "]</div>\n";
3027 sub git_print_page_path {
3028 my $name = shift;
3029 my $type = shift;
3030 my $hb = shift;
3033 print "<div class=\"page_path\">";
3034 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3035 -title => 'tree root'}, to_utf8("[$project]"));
3036 print " / ";
3037 if (defined $name) {
3038 my @dirname = split '/', $name;
3039 my $basename = pop @dirname;
3040 my $fullname = '';
3042 foreach my $dir (@dirname) {
3043 $fullname .= ($fullname ? '/' : '') . $dir;
3044 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3045 hash_base=>$hb),
3046 -title => $fullname}, esc_path($dir));
3047 print " / ";
3049 if (defined $type && $type eq 'blob') {
3050 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3051 hash_base=>$hb),
3052 -title => $name}, esc_path($basename));
3053 } elsif (defined $type && $type eq 'tree') {
3054 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3055 hash_base=>$hb),
3056 -title => $name}, esc_path($basename));
3057 print " / ";
3058 } else {
3059 print esc_path($basename);
3062 print "<br/></div>\n";
3065 # sub git_print_log (\@;%) {
3066 sub git_print_log ($;%) {
3067 my $log = shift;
3068 my %opts = @_;
3070 if ($opts{'-remove_title'}) {
3071 # remove title, i.e. first line of log
3072 shift @$log;
3074 # remove leading empty lines
3075 while (defined $log->[0] && $log->[0] eq "") {
3076 shift @$log;
3079 # print log
3080 my $signoff = 0;
3081 my $empty = 0;
3082 foreach my $line (@$log) {
3083 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3084 $signoff = 1;
3085 $empty = 0;
3086 if (! $opts{'-remove_signoff'}) {
3087 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3088 next;
3089 } else {
3090 # remove signoff lines
3091 next;
3093 } else {
3094 $signoff = 0;
3097 # print only one empty line
3098 # do not print empty line after signoff
3099 if ($line eq "") {
3100 next if ($empty || $signoff);
3101 $empty = 1;
3102 } else {
3103 $empty = 0;
3106 print format_log_line_html($line) . "<br/>\n";
3109 if ($opts{'-final_empty_line'}) {
3110 # end with single empty line
3111 print "<br/>\n" unless $empty;
3115 # return link target (what link points to)
3116 sub git_get_link_target {
3117 my $hash = shift;
3118 my $link_target;
3120 # read link
3121 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3122 or return;
3124 local $/;
3125 $link_target = <$fd>;
3127 close $fd
3128 or return;
3130 return $link_target;
3133 # given link target, and the directory (basedir) the link is in,
3134 # return target of link relative to top directory (top tree);
3135 # return undef if it is not possible (including absolute links).
3136 sub normalize_link_target {
3137 my ($link_target, $basedir, $hash_base) = @_;
3139 # we can normalize symlink target only if $hash_base is provided
3140 return unless $hash_base;
3142 # absolute symlinks (beginning with '/') cannot be normalized
3143 return if (substr($link_target, 0, 1) eq '/');
3145 # normalize link target to path from top (root) tree (dir)
3146 my $path;
3147 if ($basedir) {
3148 $path = $basedir . '/' . $link_target;
3149 } else {
3150 # we are in top (root) tree (dir)
3151 $path = $link_target;
3154 # remove //, /./, and /../
3155 my @path_parts;
3156 foreach my $part (split('/', $path)) {
3157 # discard '.' and ''
3158 next if (!$part || $part eq '.');
3159 # handle '..'
3160 if ($part eq '..') {
3161 if (@path_parts) {
3162 pop @path_parts;
3163 } else {
3164 # link leads outside repository (outside top dir)
3165 return;
3167 } else {
3168 push @path_parts, $part;
3171 $path = join('/', @path_parts);
3173 return $path;
3176 # print tree entry (row of git_tree), but without encompassing <tr> element
3177 sub git_print_tree_entry {
3178 my ($t, $basedir, $hash_base, $have_blame) = @_;
3180 my %base_key = ();
3181 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3183 # The format of a table row is: mode list link. Where mode is
3184 # the mode of the entry, list is the name of the entry, an href,
3185 # and link is the action links of the entry.
3187 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3188 if ($t->{'type'} eq "blob") {
3189 print "<td class=\"list\">" .
3190 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3191 file_name=>"$basedir$t->{'name'}", %base_key),
3192 -class => "list"}, esc_path($t->{'name'}));
3193 if (S_ISLNK(oct $t->{'mode'})) {
3194 my $link_target = git_get_link_target($t->{'hash'});
3195 if ($link_target) {
3196 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3197 if (defined $norm_target) {
3198 print " -> " .
3199 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3200 file_name=>$norm_target),
3201 -title => $norm_target}, esc_path($link_target));
3202 } else {
3203 print " -> " . esc_path($link_target);
3207 print "</td>\n";
3208 print "<td class=\"link\">";
3209 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3210 file_name=>"$basedir$t->{'name'}", %base_key)},
3211 "blob");
3212 if ($have_blame) {
3213 print " | " .
3214 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3215 file_name=>"$basedir$t->{'name'}", %base_key)},
3216 "blame");
3218 if (defined $hash_base) {
3219 print " | " .
3220 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3221 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3222 "history");
3224 print " | " .
3225 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3226 file_name=>"$basedir$t->{'name'}")},
3227 "raw");
3228 print "</td>\n";
3230 } elsif ($t->{'type'} eq "tree") {
3231 print "<td class=\"list\">";
3232 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3233 file_name=>"$basedir$t->{'name'}", %base_key)},
3234 esc_path($t->{'name'}));
3235 print "</td>\n";
3236 print "<td class=\"link\">";
3237 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3238 file_name=>"$basedir$t->{'name'}", %base_key)},
3239 "tree");
3240 if (defined $hash_base) {
3241 print " | " .
3242 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3243 file_name=>"$basedir$t->{'name'}")},
3244 "history");
3246 print "</td>\n";
3247 } else {
3248 # unknown object: we can only present history for it
3249 # (this includes 'commit' object, i.e. submodule support)
3250 print "<td class=\"list\">" .
3251 esc_path($t->{'name'}) .
3252 "</td>\n";
3253 print "<td class=\"link\">";
3254 if (defined $hash_base) {
3255 print $cgi->a({-href => href(action=>"history",
3256 hash_base=>$hash_base,
3257 file_name=>"$basedir$t->{'name'}")},
3258 "history");
3260 print "</td>\n";
3264 ## ......................................................................
3265 ## functions printing large fragments of HTML
3267 # get pre-image filenames for merge (combined) diff
3268 sub fill_from_file_info {
3269 my ($diff, @parents) = @_;
3271 $diff->{'from_file'} = [ ];
3272 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3273 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3274 if ($diff->{'status'}[$i] eq 'R' ||
3275 $diff->{'status'}[$i] eq 'C') {
3276 $diff->{'from_file'}[$i] =
3277 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3281 return $diff;
3284 # is current raw difftree line of file deletion
3285 sub is_deleted {
3286 my $diffinfo = shift;
3288 return $diffinfo->{'to_id'} eq ('0' x 40);
3291 # does patch correspond to [previous] difftree raw line
3292 # $diffinfo - hashref of parsed raw diff format
3293 # $patchinfo - hashref of parsed patch diff format
3294 # (the same keys as in $diffinfo)
3295 sub is_patch_split {
3296 my ($diffinfo, $patchinfo) = @_;
3298 return defined $diffinfo && defined $patchinfo
3299 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3303 sub git_difftree_body {
3304 my ($difftree, $hash, @parents) = @_;
3305 my ($parent) = $parents[0];
3306 my ($have_blame) = gitweb_check_feature('blame');
3307 print "<div class=\"list_head\">\n";
3308 if ($#{$difftree} > 10) {
3309 print(($#{$difftree} + 1) . " files changed:\n");
3311 print "</div>\n";
3313 print "<table class=\"" .
3314 (@parents > 1 ? "combined " : "") .
3315 "diff_tree\">\n";
3317 # header only for combined diff in 'commitdiff' view
3318 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3319 if ($has_header) {
3320 # table header
3321 print "<thead><tr>\n" .
3322 "<th></th><th></th>\n"; # filename, patchN link
3323 for (my $i = 0; $i < @parents; $i++) {
3324 my $par = $parents[$i];
3325 print "<th>" .
3326 $cgi->a({-href => href(action=>"commitdiff",
3327 hash=>$hash, hash_parent=>$par),
3328 -title => 'commitdiff to parent number ' .
3329 ($i+1) . ': ' . substr($par,0,7)},
3330 $i+1) .
3331 "&nbsp;</th>\n";
3333 print "</tr></thead>\n<tbody>\n";
3336 my $alternate = 1;
3337 my $patchno = 0;
3338 foreach my $line (@{$difftree}) {
3339 my $diff = parsed_difftree_line($line);
3341 if ($alternate) {
3342 print "<tr class=\"dark\">\n";
3343 } else {
3344 print "<tr class=\"light\">\n";
3346 $alternate ^= 1;
3348 if (exists $diff->{'nparents'}) { # combined diff
3350 fill_from_file_info($diff, @parents)
3351 unless exists $diff->{'from_file'};
3353 if (!is_deleted($diff)) {
3354 # file exists in the result (child) commit
3355 print "<td>" .
3356 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3357 file_name=>$diff->{'to_file'},
3358 hash_base=>$hash),
3359 -class => "list"}, esc_path($diff->{'to_file'})) .
3360 "</td>\n";
3361 } else {
3362 print "<td>" .
3363 esc_path($diff->{'to_file'}) .
3364 "</td>\n";
3367 if ($action eq 'commitdiff') {
3368 # link to patch
3369 $patchno++;
3370 print "<td class=\"link\">" .
3371 $cgi->a({-href => "#patch$patchno"}, "patch") .
3372 " | " .
3373 "</td>\n";
3376 my $has_history = 0;
3377 my $not_deleted = 0;
3378 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3379 my $hash_parent = $parents[$i];
3380 my $from_hash = $diff->{'from_id'}[$i];
3381 my $from_path = $diff->{'from_file'}[$i];
3382 my $status = $diff->{'status'}[$i];
3384 $has_history ||= ($status ne 'A');
3385 $not_deleted ||= ($status ne 'D');
3387 if ($status eq 'A') {
3388 print "<td class=\"link\" align=\"right\"> | </td>\n";
3389 } elsif ($status eq 'D') {
3390 print "<td class=\"link\">" .
3391 $cgi->a({-href => href(action=>"blob",
3392 hash_base=>$hash,
3393 hash=>$from_hash,
3394 file_name=>$from_path)},
3395 "blob" . ($i+1)) .
3396 " | </td>\n";
3397 } else {
3398 if ($diff->{'to_id'} eq $from_hash) {
3399 print "<td class=\"link nochange\">";
3400 } else {
3401 print "<td class=\"link\">";
3403 print $cgi->a({-href => href(action=>"blobdiff",
3404 hash=>$diff->{'to_id'},
3405 hash_parent=>$from_hash,
3406 hash_base=>$hash,
3407 hash_parent_base=>$hash_parent,
3408 file_name=>$diff->{'to_file'},
3409 file_parent=>$from_path)},
3410 "diff" . ($i+1)) .
3411 " | </td>\n";
3415 print "<td class=\"link\">";
3416 if ($not_deleted) {
3417 print $cgi->a({-href => href(action=>"blob",
3418 hash=>$diff->{'to_id'},
3419 file_name=>$diff->{'to_file'},
3420 hash_base=>$hash)},
3421 "blob");
3422 print " | " if ($has_history);
3424 if ($has_history) {
3425 print $cgi->a({-href => href(action=>"history",
3426 file_name=>$diff->{'to_file'},
3427 hash_base=>$hash)},
3428 "history");
3430 print "</td>\n";
3432 print "</tr>\n";
3433 next; # instead of 'else' clause, to avoid extra indent
3435 # else ordinary diff
3437 my ($to_mode_oct, $to_mode_str, $to_file_type);
3438 my ($from_mode_oct, $from_mode_str, $from_file_type);
3439 if ($diff->{'to_mode'} ne ('0' x 6)) {
3440 $to_mode_oct = oct $diff->{'to_mode'};
3441 if (S_ISREG($to_mode_oct)) { # only for regular file
3442 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3444 $to_file_type = file_type($diff->{'to_mode'});
3446 if ($diff->{'from_mode'} ne ('0' x 6)) {
3447 $from_mode_oct = oct $diff->{'from_mode'};
3448 if (S_ISREG($to_mode_oct)) { # only for regular file
3449 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3451 $from_file_type = file_type($diff->{'from_mode'});
3454 if ($diff->{'status'} eq "A") { # created
3455 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3456 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3457 $mode_chng .= "]</span>";
3458 print "<td>";
3459 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3460 hash_base=>$hash, file_name=>$diff->{'file'}),
3461 -class => "list"}, esc_path($diff->{'file'}));
3462 print "</td>\n";
3463 print "<td>$mode_chng</td>\n";
3464 print "<td class=\"link\">";
3465 if ($action eq 'commitdiff') {
3466 # link to patch
3467 $patchno++;
3468 print $cgi->a({-href => "#patch$patchno"}, "patch");
3469 print " | ";
3471 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3472 hash_base=>$hash, file_name=>$diff->{'file'})},
3473 "blob");
3474 print "</td>\n";
3476 } elsif ($diff->{'status'} eq "D") { # deleted
3477 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3478 print "<td>";
3479 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3480 hash_base=>$parent, file_name=>$diff->{'file'}),
3481 -class => "list"}, esc_path($diff->{'file'}));
3482 print "</td>\n";
3483 print "<td>$mode_chng</td>\n";
3484 print "<td class=\"link\">";
3485 if ($action eq 'commitdiff') {
3486 # link to patch
3487 $patchno++;
3488 print $cgi->a({-href => "#patch$patchno"}, "patch");
3489 print " | ";
3491 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3492 hash_base=>$parent, file_name=>$diff->{'file'})},
3493 "blob") . " | ";
3494 if ($have_blame) {
3495 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3496 file_name=>$diff->{'file'})},
3497 "blame") . " | ";
3499 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3500 file_name=>$diff->{'file'})},
3501 "history");
3502 print "</td>\n";
3504 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3505 my $mode_chnge = "";
3506 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3507 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3508 if ($from_file_type ne $to_file_type) {
3509 $mode_chnge .= " from $from_file_type to $to_file_type";
3511 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3512 if ($from_mode_str && $to_mode_str) {
3513 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3514 } elsif ($to_mode_str) {
3515 $mode_chnge .= " mode: $to_mode_str";
3518 $mode_chnge .= "]</span>\n";
3520 print "<td>";
3521 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3522 hash_base=>$hash, file_name=>$diff->{'file'}),
3523 -class => "list"}, esc_path($diff->{'file'}));
3524 print "</td>\n";
3525 print "<td>$mode_chnge</td>\n";
3526 print "<td class=\"link\">";
3527 if ($action eq 'commitdiff') {
3528 # link to patch
3529 $patchno++;
3530 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3531 " | ";
3532 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3533 # "commit" view and modified file (not onlu mode changed)
3534 print $cgi->a({-href => href(action=>"blobdiff",
3535 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3536 hash_base=>$hash, hash_parent_base=>$parent,
3537 file_name=>$diff->{'file'})},
3538 "diff") .
3539 " | ";
3541 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3542 hash_base=>$hash, file_name=>$diff->{'file'})},
3543 "blob") . " | ";
3544 if ($have_blame) {
3545 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3546 file_name=>$diff->{'file'})},
3547 "blame") . " | ";
3549 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3550 file_name=>$diff->{'file'})},
3551 "history");
3552 print "</td>\n";
3554 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3555 my %status_name = ('R' => 'moved', 'C' => 'copied');
3556 my $nstatus = $status_name{$diff->{'status'}};
3557 my $mode_chng = "";
3558 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3559 # mode also for directories, so we cannot use $to_mode_str
3560 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3562 print "<td>" .
3563 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3564 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3565 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3566 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3567 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3568 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3569 -class => "list"}, esc_path($diff->{'from_file'})) .
3570 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3571 "<td class=\"link\">";
3572 if ($action eq 'commitdiff') {
3573 # link to patch
3574 $patchno++;
3575 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3576 " | ";
3577 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3578 # "commit" view and modified file (not only pure rename or copy)
3579 print $cgi->a({-href => href(action=>"blobdiff",
3580 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3581 hash_base=>$hash, hash_parent_base=>$parent,
3582 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3583 "diff") .
3584 " | ";
3586 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3587 hash_base=>$parent, file_name=>$diff->{'to_file'})},
3588 "blob") . " | ";
3589 if ($have_blame) {
3590 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3591 file_name=>$diff->{'to_file'})},
3592 "blame") . " | ";
3594 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3595 file_name=>$diff->{'to_file'})},
3596 "history");
3597 print "</td>\n";
3599 } # we should not encounter Unmerged (U) or Unknown (X) status
3600 print "</tr>\n";
3602 print "</tbody>" if $has_header;
3603 print "</table>\n";
3606 sub git_patchset_body {
3607 my ($fd, $difftree, $hash, @hash_parents) = @_;
3608 my ($hash_parent) = $hash_parents[0];
3610 my $is_combined = (@hash_parents > 1);
3611 my $patch_idx = 0;
3612 my $patch_number = 0;
3613 my $patch_line;
3614 my $diffinfo;
3615 my $to_name;
3616 my (%from, %to);
3618 print "<div class=\"patchset\">\n";
3620 # skip to first patch
3621 while ($patch_line = <$fd>) {
3622 chomp $patch_line;
3624 last if ($patch_line =~ m/^diff /);
3627 PATCH:
3628 while ($patch_line) {
3630 # parse "git diff" header line
3631 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3632 # $1 is from_name, which we do not use
3633 $to_name = unquote($2);
3634 $to_name =~ s!^b/!!;
3635 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3636 # $1 is 'cc' or 'combined', which we do not use
3637 $to_name = unquote($2);
3638 } else {
3639 $to_name = undef;
3642 # check if current patch belong to current raw line
3643 # and parse raw git-diff line if needed
3644 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
3645 # this is continuation of a split patch
3646 print "<div class=\"patch cont\">\n";
3647 } else {
3648 # advance raw git-diff output if needed
3649 $patch_idx++ if defined $diffinfo;
3651 # read and prepare patch information
3652 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3654 # compact combined diff output can have some patches skipped
3655 # find which patch (using pathname of result) we are at now;
3656 if ($is_combined) {
3657 while ($to_name ne $diffinfo->{'to_file'}) {
3658 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3659 format_diff_cc_simplified($diffinfo, @hash_parents) .
3660 "</div>\n"; # class="patch"
3662 $patch_idx++;
3663 $patch_number++;
3665 last if $patch_idx > $#$difftree;
3666 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3670 # modifies %from, %to hashes
3671 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
3673 # this is first patch for raw difftree line with $patch_idx index
3674 # we index @$difftree array from 0, but number patches from 1
3675 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
3678 # git diff header
3679 #assert($patch_line =~ m/^diff /) if DEBUG;
3680 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3681 $patch_number++;
3682 # print "git diff" header
3683 print format_git_diff_header_line($patch_line, $diffinfo,
3684 \%from, \%to);
3686 # print extended diff header
3687 print "<div class=\"diff extended_header\">\n";
3688 EXTENDED_HEADER:
3689 while ($patch_line = <$fd>) {
3690 chomp $patch_line;
3692 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3694 print format_extended_diff_header_line($patch_line, $diffinfo,
3695 \%from, \%to);
3697 print "</div>\n"; # class="diff extended_header"
3699 # from-file/to-file diff header
3700 if (! $patch_line) {
3701 print "</div>\n"; # class="patch"
3702 last PATCH;
3704 next PATCH if ($patch_line =~ m/^diff /);
3705 #assert($patch_line =~ m/^---/) if DEBUG;
3707 my $last_patch_line = $patch_line;
3708 $patch_line = <$fd>;
3709 chomp $patch_line;
3710 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
3712 print format_diff_from_to_header($last_patch_line, $patch_line,
3713 $diffinfo, \%from, \%to,
3714 @hash_parents);
3716 # the patch itself
3717 LINE:
3718 while ($patch_line = <$fd>) {
3719 chomp $patch_line;
3721 next PATCH if ($patch_line =~ m/^diff /);
3723 print format_diff_line($patch_line, \%from, \%to);
3726 } continue {
3727 print "</div>\n"; # class="patch"
3730 # for compact combined (--cc) format, with chunk and patch simpliciaction
3731 # patchset might be empty, but there might be unprocessed raw lines
3732 for (++$patch_idx if $patch_number > 0;
3733 $patch_idx < @$difftree;
3734 ++$patch_idx) {
3735 # read and prepare patch information
3736 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
3738 # generate anchor for "patch" links in difftree / whatchanged part
3739 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3740 format_diff_cc_simplified($diffinfo, @hash_parents) .
3741 "</div>\n"; # class="patch"
3743 $patch_number++;
3746 if ($patch_number == 0) {
3747 if (@hash_parents > 1) {
3748 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3749 } else {
3750 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3754 print "</div>\n"; # class="patchset"
3757 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3759 # fills project list info (age, description, owner, forks) for each
3760 # project in the list, removing invalid projects from returned list
3761 # NOTE: modifies $projlist, but does not remove entries from it
3762 sub fill_project_list_info {
3763 my ($projlist, $check_forks) = @_;
3764 my @projects;
3766 my $show_ctags = gitweb_check_feature('ctags');
3767 PROJECT:
3768 foreach my $pr (@$projlist) {
3769 my (@activity) = git_get_last_activity($pr->{'path'});
3770 unless (@activity) {
3771 next PROJECT;
3773 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
3774 if (!defined $pr->{'descr'}) {
3775 my $descr = git_get_project_description($pr->{'path'}) || "";
3776 $descr = to_utf8($descr);
3777 $pr->{'descr_long'} = $descr;
3778 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
3780 if (!defined $pr->{'owner'}) {
3781 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
3783 if ($check_forks) {
3784 my $pname = $pr->{'path'};
3785 if (($pname =~ s/\.git$//) &&
3786 ($pname !~ /\/$/) &&
3787 (-d "$projectroot/$pname")) {
3788 $pr->{'forks'} = "-d $projectroot/$pname";
3789 } else {
3790 $pr->{'forks'} = 0;
3793 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
3794 push @projects, $pr;
3797 return @projects;
3800 # print 'sort by' <th> element, generating 'sort by $name' replay link
3801 # if that order is not selected
3802 sub print_sort_th {
3803 my ($name, $order, $header) = @_;
3804 $header ||= ucfirst($name);
3806 if ($order eq $name) {
3807 print "<th>$header</th>\n";
3808 } else {
3809 print "<th>" .
3810 $cgi->a({-href => href(-replay=>1, order=>$name),
3811 -class => "header"}, $header) .
3812 "</th>\n";
3816 sub git_project_list_body {
3817 # actually uses global variable $project
3818 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
3820 my ($check_forks) = gitweb_check_feature('forks');
3821 my @projects = fill_project_list_info($projlist, $check_forks);
3823 $order ||= $default_projects_order;
3824 $from = 0 unless defined $from;
3825 $to = $#projects if (!defined $to || $#projects < $to);
3827 my %order_info = (
3828 project => { key => 'path', type => 'str' },
3829 descr => { key => 'descr_long', type => 'str' },
3830 owner => { key => 'owner', type => 'str' },
3831 age => { key => 'age', type => 'num' }
3833 my $oi = $order_info{$order};
3834 if ($oi->{'type'} eq 'str') {
3835 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
3836 } else {
3837 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
3840 my $show_ctags = gitweb_check_feature('ctags');
3841 if ($show_ctags) {
3842 my %ctags;
3843 foreach my $p (@projects) {
3844 foreach my $ct (keys %{$p->{'ctags'}}) {
3845 $ctags{$ct} += $p->{'ctags'}->{$ct};
3848 my $cloud = git_populate_project_tagcloud(\%ctags);
3849 print git_show_project_tagcloud($cloud, 64);
3852 print "<table class=\"project_list\">\n";
3853 unless ($no_header) {
3854 print "<tr>\n";
3855 if ($check_forks) {
3856 print "<th></th>\n";
3858 print_sort_th('project', $order, 'Project');
3859 print_sort_th('descr', $order, 'Description');
3860 print_sort_th('owner', $order, 'Owner');
3861 print_sort_th('age', $order, 'Last Change');
3862 print "<th></th>\n" . # for links
3863 "</tr>\n";
3865 my $alternate = 1;
3866 my $tagfilter = $cgi->param('by_tag');
3867 for (my $i = $from; $i <= $to; $i++) {
3868 my $pr = $projects[$i];
3870 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
3871 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
3872 and not $pr->{'descr_long'} =~ /$searchtext/;
3873 # Weed out forks or non-matching entries of search
3874 if ($check_forks) {
3875 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
3876 $forkbase="^$forkbase" if $forkbase;
3877 next if not $searchtext and not $tagfilter and $show_ctags
3878 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
3881 if ($alternate) {
3882 print "<tr class=\"dark\">\n";
3883 } else {
3884 print "<tr class=\"light\">\n";
3886 $alternate ^= 1;
3887 if ($check_forks) {
3888 print "<td>";
3889 if ($pr->{'forks'}) {
3890 print "<!-- $pr->{'forks'} -->\n";
3891 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
3893 print "</td>\n";
3895 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3896 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
3897 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
3898 -class => "list", -title => $pr->{'descr_long'}},
3899 esc_html($pr->{'descr'})) . "</td>\n" .
3900 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
3901 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
3902 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
3903 "<td class=\"link\">" .
3904 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
3905 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
3906 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
3907 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
3908 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
3909 "</td>\n" .
3910 "</tr>\n";
3912 if (defined $extra) {
3913 print "<tr>\n";
3914 if ($check_forks) {
3915 print "<td></td>\n";
3917 print "<td colspan=\"5\">$extra</td>\n" .
3918 "</tr>\n";
3920 print "</table>\n";
3923 sub git_shortlog_body {
3924 # uses global variable $project
3925 my ($commitlist, $from, $to, $refs, $extra) = @_;
3927 $from = 0 unless defined $from;
3928 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
3930 print "<table class=\"shortlog\">\n";
3931 my $alternate = 1;
3932 for (my $i = $from; $i <= $to; $i++) {
3933 my %co = %{$commitlist->[$i]};
3934 my $commit = $co{'id'};
3935 my $ref = format_ref_marker($refs, $commit);
3936 if ($alternate) {
3937 print "<tr class=\"dark\">\n";
3938 } else {
3939 print "<tr class=\"light\">\n";
3941 $alternate ^= 1;
3942 my $author = chop_and_escape_str($co{'author_name'}, 10);
3943 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
3944 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3945 "<td><i>" . $author . "</i></td>\n" .
3946 "<td>";
3947 print format_subject_html($co{'title'}, $co{'title_short'},
3948 href(action=>"commit", hash=>$commit), $ref);
3949 print "</td>\n" .
3950 "<td class=\"link\">" .
3951 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
3952 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
3953 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
3954 my $snapshot_links = format_snapshot_links($commit);
3955 if (defined $snapshot_links) {
3956 print " | " . $snapshot_links;
3958 print "</td>\n" .
3959 "</tr>\n";
3961 if (defined $extra) {
3962 print "<tr>\n" .
3963 "<td colspan=\"4\">$extra</td>\n" .
3964 "</tr>\n";
3966 print "</table>\n";
3969 sub git_history_body {
3970 # Warning: assumes constant type (blob or tree) during history
3971 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
3973 $from = 0 unless defined $from;
3974 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
3976 print "<table class=\"history\">\n";
3977 my $alternate = 1;
3978 for (my $i = $from; $i <= $to; $i++) {
3979 my %co = %{$commitlist->[$i]};
3980 if (!%co) {
3981 next;
3983 my $commit = $co{'id'};
3985 my $ref = format_ref_marker($refs, $commit);
3987 if ($alternate) {
3988 print "<tr class=\"dark\">\n";
3989 } else {
3990 print "<tr class=\"light\">\n";
3992 $alternate ^= 1;
3993 # shortlog uses chop_str($co{'author_name'}, 10)
3994 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
3995 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3996 "<td><i>" . $author . "</i></td>\n" .
3997 "<td>";
3998 # originally git_history used chop_str($co{'title'}, 50)
3999 print format_subject_html($co{'title'}, $co{'title_short'},
4000 href(action=>"commit", hash=>$commit), $ref);
4001 print "</td>\n" .
4002 "<td class=\"link\">" .
4003 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4004 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4006 if ($ftype eq 'blob') {
4007 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4008 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4009 if (defined $blob_current && defined $blob_parent &&
4010 $blob_current ne $blob_parent) {
4011 print " | " .
4012 $cgi->a({-href => href(action=>"blobdiff",
4013 hash=>$blob_current, hash_parent=>$blob_parent,
4014 hash_base=>$hash_base, hash_parent_base=>$commit,
4015 file_name=>$file_name)},
4016 "diff to current");
4019 print "</td>\n" .
4020 "</tr>\n";
4022 if (defined $extra) {
4023 print "<tr>\n" .
4024 "<td colspan=\"4\">$extra</td>\n" .
4025 "</tr>\n";
4027 print "</table>\n";
4030 sub git_tags_body {
4031 # uses global variable $project
4032 my ($taglist, $from, $to, $extra) = @_;
4033 $from = 0 unless defined $from;
4034 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4036 print "<table class=\"tags\">\n";
4037 my $alternate = 1;
4038 for (my $i = $from; $i <= $to; $i++) {
4039 my $entry = $taglist->[$i];
4040 my %tag = %$entry;
4041 my $comment = $tag{'subject'};
4042 my $comment_short;
4043 if (defined $comment) {
4044 $comment_short = chop_str($comment, 30, 5);
4046 if ($alternate) {
4047 print "<tr class=\"dark\">\n";
4048 } else {
4049 print "<tr class=\"light\">\n";
4051 $alternate ^= 1;
4052 if (defined $tag{'age'}) {
4053 print "<td><i>$tag{'age'}</i></td>\n";
4054 } else {
4055 print "<td></td>\n";
4057 print "<td>" .
4058 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4059 -class => "list name"}, esc_html($tag{'name'})) .
4060 "</td>\n" .
4061 "<td>";
4062 if (defined $comment) {
4063 print format_subject_html($comment, $comment_short,
4064 href(action=>"tag", hash=>$tag{'id'}));
4066 print "</td>\n" .
4067 "<td class=\"selflink\">";
4068 if ($tag{'type'} eq "tag") {
4069 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4070 } else {
4071 print "&nbsp;";
4073 print "</td>\n" .
4074 "<td class=\"link\">" . " | " .
4075 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4076 if ($tag{'reftype'} eq "commit") {
4077 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4078 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4079 } elsif ($tag{'reftype'} eq "blob") {
4080 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4082 print "</td>\n" .
4083 "</tr>";
4085 if (defined $extra) {
4086 print "<tr>\n" .
4087 "<td colspan=\"5\">$extra</td>\n" .
4088 "</tr>\n";
4090 print "</table>\n";
4093 sub git_heads_body {
4094 # uses global variable $project
4095 my ($headlist, $head, $from, $to, $extra) = @_;
4096 $from = 0 unless defined $from;
4097 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4099 print "<table class=\"heads\">\n";
4100 my $alternate = 1;
4101 for (my $i = $from; $i <= $to; $i++) {
4102 my $entry = $headlist->[$i];
4103 my %ref = %$entry;
4104 my $curr = $ref{'id'} eq $head;
4105 if ($alternate) {
4106 print "<tr class=\"dark\">\n";
4107 } else {
4108 print "<tr class=\"light\">\n";
4110 $alternate ^= 1;
4111 print "<td><i>$ref{'age'}</i></td>\n" .
4112 ($curr ? "<td class=\"current_head\">" : "<td>") .
4113 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4114 -class => "list name"},esc_html($ref{'name'})) .
4115 "</td>\n" .
4116 "<td class=\"link\">" .
4117 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4118 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4119 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4120 "</td>\n" .
4121 "</tr>";
4123 if (defined $extra) {
4124 print "<tr>\n" .
4125 "<td colspan=\"3\">$extra</td>\n" .
4126 "</tr>\n";
4128 print "</table>\n";
4131 sub git_search_grep_body {
4132 my ($commitlist, $from, $to, $extra) = @_;
4133 $from = 0 unless defined $from;
4134 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4136 print "<table class=\"commit_search\">\n";
4137 my $alternate = 1;
4138 for (my $i = $from; $i <= $to; $i++) {
4139 my %co = %{$commitlist->[$i]};
4140 if (!%co) {
4141 next;
4143 my $commit = $co{'id'};
4144 if ($alternate) {
4145 print "<tr class=\"dark\">\n";
4146 } else {
4147 print "<tr class=\"light\">\n";
4149 $alternate ^= 1;
4150 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
4151 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4152 "<td><i>" . $author . "</i></td>\n" .
4153 "<td>" .
4154 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4155 -class => "list subject"},
4156 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4157 my $comment = $co{'comment'};
4158 foreach my $line (@$comment) {
4159 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4160 my ($lead, $match, $trail) = ($1, $2, $3);
4161 $match = chop_str($match, 70, 5, 'center');
4162 my $contextlen = int((80 - length($match))/2);
4163 $contextlen = 30 if ($contextlen > 30);
4164 $lead = chop_str($lead, $contextlen, 10, 'left');
4165 $trail = chop_str($trail, $contextlen, 10, 'right');
4167 $lead = esc_html($lead);
4168 $match = esc_html($match);
4169 $trail = esc_html($trail);
4171 print "$lead<span class=\"match\">$match</span>$trail<br />";
4174 print "</td>\n" .
4175 "<td class=\"link\">" .
4176 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4177 " | " .
4178 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4179 " | " .
4180 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4181 print "</td>\n" .
4182 "</tr>\n";
4184 if (defined $extra) {
4185 print "<tr>\n" .
4186 "<td colspan=\"3\">$extra</td>\n" .
4187 "</tr>\n";
4189 print "</table>\n";
4192 ## ======================================================================
4193 ## ======================================================================
4194 ## actions
4196 sub git_project_list {
4197 my $order = $input_params{'order'};
4198 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4199 die_error(400, "Unknown order parameter");
4202 my @list = git_get_projects_list();
4203 if (!@list) {
4204 die_error(404, "No projects found");
4207 git_header_html();
4208 if (-f $home_text) {
4209 print "<div class=\"index_include\">\n";
4210 open (my $fd, $home_text);
4211 print <$fd>;
4212 close $fd;
4213 print "</div>\n";
4215 print $cgi->startform(-method => "get") .
4216 "<p class=\"projsearch\">Search:\n" .
4217 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4218 "</p>" .
4219 $cgi->end_form() . "\n";
4220 git_project_list_body(\@list, $order);
4221 git_footer_html();
4224 sub git_forks {
4225 my $order = $input_params{'order'};
4226 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4227 die_error(400, "Unknown order parameter");
4230 my @list = git_get_projects_list($project);
4231 if (!@list) {
4232 die_error(404, "No forks found");
4235 git_header_html();
4236 git_print_page_nav('','');
4237 git_print_header_div('summary', "$project forks");
4238 git_project_list_body(\@list, $order);
4239 git_footer_html();
4242 sub git_project_index {
4243 my @projects = git_get_projects_list($project);
4245 print $cgi->header(
4246 -type => 'text/plain',
4247 -charset => 'utf-8',
4248 -content_disposition => 'inline; filename="index.aux"');
4250 foreach my $pr (@projects) {
4251 if (!exists $pr->{'owner'}) {
4252 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4255 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4256 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4257 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4258 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4259 $path =~ s/ /\+/g;
4260 $owner =~ s/ /\+/g;
4262 print "$path $owner\n";
4266 sub git_summary {
4267 my $descr = git_get_project_description($project) || "none";
4268 my %co = parse_commit("HEAD");
4269 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4270 my $head = $co{'id'};
4272 my $owner = git_get_project_owner($project);
4274 my $refs = git_get_references();
4275 # These get_*_list functions return one more to allow us to see if
4276 # there are more ...
4277 my @taglist = git_get_tags_list(16);
4278 my @headlist = git_get_heads_list(16);
4279 my @forklist;
4280 my ($check_forks) = gitweb_check_feature('forks');
4282 if ($check_forks) {
4283 @forklist = git_get_projects_list($project);
4286 git_header_html();
4287 git_print_page_nav('summary','', $head);
4289 print "<div class=\"title\">&nbsp;</div>\n";
4290 print "<table class=\"projects_list\">\n" .
4291 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4292 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4293 if (defined $cd{'rfc2822'}) {
4294 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4297 # use per project git URL list in $projectroot/$project/cloneurl
4298 # or make project git URL from git base URL and project name
4299 my $url_tag = "URL";
4300 my @url_list = git_get_project_url_list($project);
4301 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4302 foreach my $git_url (@url_list) {
4303 next unless $git_url;
4304 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4305 $url_tag = "";
4308 # Tag cloud
4309 my $show_ctags = (gitweb_check_feature('ctags'))[0];
4310 if ($show_ctags) {
4311 my $ctags = git_get_project_ctags($project);
4312 my $cloud = git_populate_project_tagcloud($ctags);
4313 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4314 print "</td>\n<td>" unless %$ctags;
4315 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4316 print "</td>\n<td>" if %$ctags;
4317 print git_show_project_tagcloud($cloud, 48);
4318 print "</td></tr>";
4321 print "</table>\n";
4323 if (-s "$projectroot/$project/README.html") {
4324 if (open my $fd, "$projectroot/$project/README.html") {
4325 print "<div class=\"title\">readme</div>\n" .
4326 "<div class=\"readme\">\n";
4327 print $_ while (<$fd>);
4328 print "\n</div>\n"; # class="readme"
4329 close $fd;
4333 # we need to request one more than 16 (0..15) to check if
4334 # those 16 are all
4335 my @commitlist = $head ? parse_commits($head, 17) : ();
4336 if (@commitlist) {
4337 git_print_header_div('shortlog');
4338 git_shortlog_body(\@commitlist, 0, 15, $refs,
4339 $#commitlist <= 15 ? undef :
4340 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4343 if (@taglist) {
4344 git_print_header_div('tags');
4345 git_tags_body(\@taglist, 0, 15,
4346 $#taglist <= 15 ? undef :
4347 $cgi->a({-href => href(action=>"tags")}, "..."));
4350 if (@headlist) {
4351 git_print_header_div('heads');
4352 git_heads_body(\@headlist, $head, 0, 15,
4353 $#headlist <= 15 ? undef :
4354 $cgi->a({-href => href(action=>"heads")}, "..."));
4357 if (@forklist) {
4358 git_print_header_div('forks');
4359 git_project_list_body(\@forklist, 'age', 0, 15,
4360 $#forklist <= 15 ? undef :
4361 $cgi->a({-href => href(action=>"forks")}, "..."),
4362 'no_header');
4365 git_footer_html();
4368 sub git_tag {
4369 my $head = git_get_head_hash($project);
4370 git_header_html();
4371 git_print_page_nav('','', $head,undef,$head);
4372 my %tag = parse_tag($hash);
4374 if (! %tag) {
4375 die_error(404, "Unknown tag object");
4378 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4379 print "<div class=\"title_text\">\n" .
4380 "<table class=\"object_header\">\n" .
4381 "<tr>\n" .
4382 "<td>object</td>\n" .
4383 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4384 $tag{'object'}) . "</td>\n" .
4385 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4386 $tag{'type'}) . "</td>\n" .
4387 "</tr>\n";
4388 if (defined($tag{'author'})) {
4389 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
4390 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
4391 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4392 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4393 "</td></tr>\n";
4395 print "</table>\n\n" .
4396 "</div>\n";
4397 print "<div class=\"page_body\">";
4398 my $comment = $tag{'comment'};
4399 foreach my $line (@$comment) {
4400 chomp $line;
4401 print esc_html($line, -nbsp=>1) . "<br/>\n";
4403 print "</div>\n";
4404 git_footer_html();
4407 sub git_blame {
4408 my $fd;
4409 my $ftype;
4411 gitweb_check_feature('blame')
4412 or die_error(403, "Blame view not allowed");
4414 die_error(400, "No file name given") unless $file_name;
4415 $hash_base ||= git_get_head_hash($project);
4416 die_error(404, "Couldn't find base commit") unless ($hash_base);
4417 my %co = parse_commit($hash_base)
4418 or die_error(404, "Commit not found");
4419 if (!defined $hash) {
4420 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4421 or die_error(404, "Error looking up file");
4423 $ftype = git_get_type($hash);
4424 if ($ftype !~ "blob") {
4425 die_error(400, "Object is not a blob");
4427 open ($fd, "-|", git_cmd(), "blame", '-p', '--',
4428 $file_name, $hash_base)
4429 or die_error(500, "Open git-blame failed");
4430 git_header_html();
4431 my $formats_nav =
4432 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4433 "blob") .
4434 " | " .
4435 $cgi->a({-href => href(action=>"history", -replay=>1)},
4436 "history") .
4437 " | " .
4438 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4439 "HEAD");
4440 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4441 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4442 git_print_page_path($file_name, $ftype, $hash_base);
4443 my @rev_color = (qw(light2 dark2));
4444 my $num_colors = scalar(@rev_color);
4445 my $current_color = 0;
4446 my $last_rev;
4447 print <<HTML;
4448 <div class="page_body">
4449 <table class="blame">
4450 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4451 HTML
4452 my %metainfo = ();
4453 while (1) {
4454 $_ = <$fd>;
4455 last unless defined $_;
4456 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4457 /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/;
4458 if (!exists $metainfo{$full_rev}) {
4459 $metainfo{$full_rev} = {};
4461 my $meta = $metainfo{$full_rev};
4462 while (<$fd>) {
4463 last if (s/^\t//);
4464 if (/^(\S+) (.*)$/) {
4465 $meta->{$1} = $2;
4468 my $data = $_;
4469 chomp $data;
4470 my $rev = substr($full_rev, 0, 8);
4471 my $author = $meta->{'author'};
4472 my %date = parse_date($meta->{'author-time'},
4473 $meta->{'author-tz'});
4474 my $date = $date{'iso-tz'};
4475 if ($group_size) {
4476 $current_color = ++$current_color % $num_colors;
4478 print "<tr class=\"$rev_color[$current_color]\">\n";
4479 if ($group_size) {
4480 print "<td class=\"sha1\"";
4481 print " title=\"". esc_html($author) . ", $date\"";
4482 print " rowspan=\"$group_size\"" if ($group_size > 1);
4483 print ">";
4484 print $cgi->a({-href => href(action=>"commit",
4485 hash=>$full_rev,
4486 file_name=>$file_name)},
4487 esc_html($rev));
4488 print "</td>\n";
4490 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4491 or die_error(500, "Open git-rev-parse failed");
4492 my $parent_commit = <$dd>;
4493 close $dd;
4494 chomp($parent_commit);
4495 my $blamed = href(action => 'blame',
4496 file_name => $meta->{'filename'},
4497 hash_base => $parent_commit);
4498 print "<td class=\"linenr\">";
4499 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4500 -id => "l$lineno",
4501 -class => "linenr" },
4502 esc_html($lineno));
4503 print "</td>";
4504 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4505 print "</tr>\n";
4507 print "</table>\n";
4508 print "</div>";
4509 close $fd
4510 or print "Reading blob failed\n";
4511 git_footer_html();
4514 sub git_tags {
4515 my $head = git_get_head_hash($project);
4516 git_header_html();
4517 git_print_page_nav('','', $head,undef,$head);
4518 git_print_header_div('summary', $project);
4520 my @tagslist = git_get_tags_list();
4521 if (@tagslist) {
4522 git_tags_body(\@tagslist);
4524 git_footer_html();
4527 sub git_heads {
4528 my $head = git_get_head_hash($project);
4529 git_header_html();
4530 git_print_page_nav('','', $head,undef,$head);
4531 git_print_header_div('summary', $project);
4533 my @headslist = git_get_heads_list();
4534 if (@headslist) {
4535 git_heads_body(\@headslist, $head);
4537 git_footer_html();
4540 sub git_blob_plain {
4541 my $type = shift;
4542 my $expires;
4544 if (!defined $hash) {
4545 if (defined $file_name) {
4546 my $base = $hash_base || git_get_head_hash($project);
4547 $hash = git_get_hash_by_path($base, $file_name, "blob")
4548 or die_error(404, "Cannot find file");
4549 } else {
4550 die_error(400, "No file name defined");
4552 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4553 # blobs defined by non-textual hash id's can be cached
4554 $expires = "+1d";
4557 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4558 or die_error(500, "Open git-cat-file blob '$hash' failed");
4560 # content-type (can include charset)
4561 $type = blob_contenttype($fd, $file_name, $type);
4563 # "save as" filename, even when no $file_name is given
4564 my $save_as = "$hash";
4565 if (defined $file_name) {
4566 $save_as = $file_name;
4567 } elsif ($type =~ m/^text\//) {
4568 $save_as .= '.txt';
4571 print $cgi->header(
4572 -type => $type,
4573 -expires => $expires,
4574 -content_disposition => 'inline; filename="' . $save_as . '"');
4575 undef $/;
4576 binmode STDOUT, ':raw';
4577 print <$fd>;
4578 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4579 $/ = "\n";
4580 close $fd;
4583 sub git_blob {
4584 my $expires;
4586 if (!defined $hash) {
4587 if (defined $file_name) {
4588 my $base = $hash_base || git_get_head_hash($project);
4589 $hash = git_get_hash_by_path($base, $file_name, "blob")
4590 or die_error(404, "Cannot find file");
4591 } else {
4592 die_error(400, "No file name defined");
4594 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4595 # blobs defined by non-textual hash id's can be cached
4596 $expires = "+1d";
4599 my ($have_blame) = gitweb_check_feature('blame');
4600 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4601 or die_error(500, "Couldn't cat $file_name, $hash");
4602 my $mimetype = blob_mimetype($fd, $file_name);
4603 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
4604 close $fd;
4605 return git_blob_plain($mimetype);
4607 # we can have blame only for text/* mimetype
4608 $have_blame &&= ($mimetype =~ m!^text/!);
4610 git_header_html(undef, $expires);
4611 my $formats_nav = '';
4612 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4613 if (defined $file_name) {
4614 if ($have_blame) {
4615 $formats_nav .=
4616 $cgi->a({-href => href(action=>"blame", -replay=>1)},
4617 "blame") .
4618 " | ";
4620 $formats_nav .=
4621 $cgi->a({-href => href(action=>"history", -replay=>1)},
4622 "history") .
4623 " | " .
4624 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4625 "raw") .
4626 " | " .
4627 $cgi->a({-href => href(action=>"blob",
4628 hash_base=>"HEAD", file_name=>$file_name)},
4629 "HEAD");
4630 } else {
4631 $formats_nav .=
4632 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4633 "raw");
4635 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4636 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4637 } else {
4638 print "<div class=\"page_nav\">\n" .
4639 "<br/><br/></div>\n" .
4640 "<div class=\"title\">$hash</div>\n";
4642 git_print_page_path($file_name, "blob", $hash_base);
4643 print "<div class=\"page_body\">\n";
4644 if ($mimetype =~ m!^image/!) {
4645 print qq!<img type="$mimetype"!;
4646 if ($file_name) {
4647 print qq! alt="$file_name" title="$file_name"!;
4649 print qq! src="! .
4650 href(action=>"blob_plain", hash=>$hash,
4651 hash_base=>$hash_base, file_name=>$file_name) .
4652 qq!" />\n!;
4653 } else {
4654 my $nr;
4655 while (my $line = <$fd>) {
4656 chomp $line;
4657 $nr++;
4658 $line = untabify($line);
4659 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4660 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4663 close $fd
4664 or print "Reading blob failed.\n";
4665 print "</div>";
4666 git_footer_html();
4669 sub git_tree {
4670 if (!defined $hash_base) {
4671 $hash_base = "HEAD";
4673 if (!defined $hash) {
4674 if (defined $file_name) {
4675 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4676 } else {
4677 $hash = $hash_base;
4680 die_error(404, "No such tree") unless defined($hash);
4681 $/ = "\0";
4682 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4683 or die_error(500, "Open git-ls-tree failed");
4684 my @entries = map { chomp; $_ } <$fd>;
4685 close $fd or die_error(404, "Reading tree failed");
4686 $/ = "\n";
4688 my $refs = git_get_references();
4689 my $ref = format_ref_marker($refs, $hash_base);
4690 git_header_html();
4691 my $basedir = '';
4692 my ($have_blame) = gitweb_check_feature('blame');
4693 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
4694 my @views_nav = ();
4695 if (defined $file_name) {
4696 push @views_nav,
4697 $cgi->a({-href => href(action=>"history", -replay=>1)},
4698 "history"),
4699 $cgi->a({-href => href(action=>"tree",
4700 hash_base=>"HEAD", file_name=>$file_name)},
4701 "HEAD"),
4703 my $snapshot_links = format_snapshot_links($hash);
4704 if (defined $snapshot_links) {
4705 # FIXME: Should be available when we have no hash base as well.
4706 push @views_nav, $snapshot_links;
4708 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
4709 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
4710 } else {
4711 undef $hash_base;
4712 print "<div class=\"page_nav\">\n";
4713 print "<br/><br/></div>\n";
4714 print "<div class=\"title\">$hash</div>\n";
4716 if (defined $file_name) {
4717 $basedir = $file_name;
4718 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4719 $basedir .= '/';
4721 git_print_page_path($file_name, 'tree', $hash_base);
4723 print "<div class=\"page_body\">\n";
4724 print "<table class=\"tree\">\n";
4725 my $alternate = 1;
4726 # '..' (top directory) link if possible
4727 if (defined $hash_base &&
4728 defined $file_name && $file_name =~ m![^/]+$!) {
4729 if ($alternate) {
4730 print "<tr class=\"dark\">\n";
4731 } else {
4732 print "<tr class=\"light\">\n";
4734 $alternate ^= 1;
4736 my $up = $file_name;
4737 $up =~ s!/?[^/]+$!!;
4738 undef $up unless $up;
4739 # based on git_print_tree_entry
4740 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4741 print '<td class="list">';
4742 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4743 file_name=>$up)},
4744 "..");
4745 print "</td>\n";
4746 print "<td class=\"link\"></td>\n";
4748 print "</tr>\n";
4750 foreach my $line (@entries) {
4751 my %t = parse_ls_tree_line($line, -z => 1);
4753 if ($alternate) {
4754 print "<tr class=\"dark\">\n";
4755 } else {
4756 print "<tr class=\"light\">\n";
4758 $alternate ^= 1;
4760 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
4762 print "</tr>\n";
4764 print "</table>\n" .
4765 "</div>";
4766 git_footer_html();
4769 sub git_snapshot {
4770 my @supported_fmts = gitweb_check_feature('snapshot');
4771 @supported_fmts = filter_snapshot_fmts(@supported_fmts);
4773 my $format = $input_params{'snapshot_format'};
4774 if (!@supported_fmts) {
4775 die_error(403, "Snapshots not allowed");
4777 # default to first supported snapshot format
4778 $format ||= $supported_fmts[0];
4779 if ($format !~ m/^[a-z0-9]+$/) {
4780 die_error(400, "Invalid snapshot format parameter");
4781 } elsif (!exists($known_snapshot_formats{$format})) {
4782 die_error(400, "Unknown snapshot format");
4783 } elsif (!grep($_ eq $format, @supported_fmts)) {
4784 die_error(403, "Unsupported snapshot format");
4787 if (!defined $hash) {
4788 $hash = git_get_head_hash($project);
4791 my $name = $project;
4792 $name =~ s,([^/])/*\.git$,$1,;
4793 $name = basename($name);
4794 my $filename = to_utf8($name);
4795 $name =~ s/\047/\047\\\047\047/g;
4796 my $cmd;
4797 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
4798 $cmd = quote_command(
4799 git_cmd(), 'archive',
4800 "--format=$known_snapshot_formats{$format}{'format'}",
4801 "--prefix=$name/", $hash);
4802 if (exists $known_snapshot_formats{$format}{'compressor'}) {
4803 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
4806 print $cgi->header(
4807 -type => $known_snapshot_formats{$format}{'type'},
4808 -content_disposition => 'inline; filename="' . "$filename" . '"',
4809 -status => '200 OK');
4811 open my $fd, "-|", $cmd
4812 or die_error(500, "Execute git-archive failed");
4813 binmode STDOUT, ':raw';
4814 print <$fd>;
4815 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4816 close $fd;
4819 sub git_log {
4820 my $head = git_get_head_hash($project);
4821 if (!defined $hash) {
4822 $hash = $head;
4824 if (!defined $page) {
4825 $page = 0;
4827 my $refs = git_get_references();
4829 my @commitlist = parse_commits($hash, 101, (100 * $page));
4831 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
4833 git_header_html();
4834 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
4836 if (!@commitlist) {
4837 my %co = parse_commit($hash);
4839 git_print_header_div('summary', $project);
4840 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
4842 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
4843 for (my $i = 0; $i <= $to; $i++) {
4844 my %co = %{$commitlist[$i]};
4845 next if !%co;
4846 my $commit = $co{'id'};
4847 my $ref = format_ref_marker($refs, $commit);
4848 my %ad = parse_date($co{'author_epoch'});
4849 git_print_header_div('commit',
4850 "<span class=\"age\">$co{'age_string'}</span>" .
4851 esc_html($co{'title'}) . $ref,
4852 $commit);
4853 print "<div class=\"title_text\">\n" .
4854 "<div class=\"log_link\">\n" .
4855 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4856 " | " .
4857 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4858 " | " .
4859 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4860 "<br/>\n" .
4861 "</div>\n" .
4862 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
4863 "</div>\n";
4865 print "<div class=\"log_body\">\n";
4866 git_print_log($co{'comment'}, -final_empty_line=> 1);
4867 print "</div>\n";
4869 if ($#commitlist >= 100) {
4870 print "<div class=\"page_nav\">\n";
4871 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
4872 -accesskey => "n", -title => "Alt-n"}, "next");
4873 print "</div>\n";
4875 git_footer_html();
4878 sub git_commit {
4879 $hash ||= $hash_base || "HEAD";
4880 my %co = parse_commit($hash)
4881 or die_error(404, "Unknown commit object");
4882 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
4883 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
4885 my $parent = $co{'parent'};
4886 my $parents = $co{'parents'}; # listref
4888 # we need to prepare $formats_nav before any parameter munging
4889 my $formats_nav;
4890 if (!defined $parent) {
4891 # --root commitdiff
4892 $formats_nav .= '(initial)';
4893 } elsif (@$parents == 1) {
4894 # single parent commit
4895 $formats_nav .=
4896 '(parent: ' .
4897 $cgi->a({-href => href(action=>"commit",
4898 hash=>$parent)},
4899 esc_html(substr($parent, 0, 7))) .
4900 ')';
4901 } else {
4902 # merge commit
4903 $formats_nav .=
4904 '(merge: ' .
4905 join(' ', map {
4906 $cgi->a({-href => href(action=>"commit",
4907 hash=>$_)},
4908 esc_html(substr($_, 0, 7)));
4909 } @$parents ) .
4910 ')';
4913 if (!defined $parent) {
4914 $parent = "--root";
4916 my @difftree;
4917 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
4918 @diff_opts,
4919 (@$parents <= 1 ? $parent : '-c'),
4920 $hash, "--"
4921 or die_error(500, "Open git-diff-tree failed");
4922 @difftree = map { chomp; $_ } <$fd>;
4923 close $fd or die_error(404, "Reading git-diff-tree failed");
4925 # non-textual hash id's can be cached
4926 my $expires;
4927 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4928 $expires = "+1d";
4930 my $refs = git_get_references();
4931 my $ref = format_ref_marker($refs, $co{'id'});
4933 git_header_html(undef, $expires);
4934 git_print_page_nav('commit', '',
4935 $hash, $co{'tree'}, $hash,
4936 $formats_nav);
4938 if (defined $co{'parent'}) {
4939 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
4940 } else {
4941 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
4943 print "<div class=\"title_text\">\n" .
4944 "<table class=\"object_header\">\n";
4945 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
4946 "<tr>" .
4947 "<td></td><td> $ad{'rfc2822'}";
4948 if ($ad{'hour_local'} < 6) {
4949 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
4950 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4951 } else {
4952 printf(" (%02d:%02d %s)",
4953 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
4955 print "</td>" .
4956 "</tr>\n";
4957 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
4958 print "<tr><td></td><td> $cd{'rfc2822'}" .
4959 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
4960 "</td></tr>\n";
4961 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
4962 print "<tr>" .
4963 "<td>tree</td>" .
4964 "<td class=\"sha1\">" .
4965 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
4966 class => "list"}, $co{'tree'}) .
4967 "</td>" .
4968 "<td class=\"link\">" .
4969 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
4970 "tree");
4971 my $snapshot_links = format_snapshot_links($hash);
4972 if (defined $snapshot_links) {
4973 print " | " . $snapshot_links;
4975 print "</td>" .
4976 "</tr>\n";
4978 foreach my $par (@$parents) {
4979 print "<tr>" .
4980 "<td>parent</td>" .
4981 "<td class=\"sha1\">" .
4982 $cgi->a({-href => href(action=>"commit", hash=>$par),
4983 class => "list"}, $par) .
4984 "</td>" .
4985 "<td class=\"link\">" .
4986 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
4987 " | " .
4988 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
4989 "</td>" .
4990 "</tr>\n";
4992 print "</table>".
4993 "</div>\n";
4995 print "<div class=\"page_body\">\n";
4996 git_print_log($co{'comment'});
4997 print "</div>\n";
4999 git_difftree_body(\@difftree, $hash, @$parents);
5001 git_footer_html();
5004 sub git_object {
5005 # object is defined by:
5006 # - hash or hash_base alone
5007 # - hash_base and file_name
5008 my $type;
5010 # - hash or hash_base alone
5011 if ($hash || ($hash_base && !defined $file_name)) {
5012 my $object_id = $hash || $hash_base;
5014 open my $fd, "-|", quote_command(
5015 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5016 or die_error(404, "Object does not exist");
5017 $type = <$fd>;
5018 chomp $type;
5019 close $fd
5020 or die_error(404, "Object does not exist");
5022 # - hash_base and file_name
5023 } elsif ($hash_base && defined $file_name) {
5024 $file_name =~ s,/+$,,;
5026 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5027 or die_error(404, "Base object does not exist");
5029 # here errors should not hapen
5030 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5031 or die_error(500, "Open git-ls-tree failed");
5032 my $line = <$fd>;
5033 close $fd;
5035 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5036 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5037 die_error(404, "File or directory for given base does not exist");
5039 $type = $2;
5040 $hash = $3;
5041 } else {
5042 die_error(400, "Not enough information to find object");
5045 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5046 hash=>$hash, hash_base=>$hash_base,
5047 file_name=>$file_name),
5048 -status => '302 Found');
5051 sub git_blobdiff {
5052 my $format = shift || 'html';
5054 my $fd;
5055 my @difftree;
5056 my %diffinfo;
5057 my $expires;
5059 # preparing $fd and %diffinfo for git_patchset_body
5060 # new style URI
5061 if (defined $hash_base && defined $hash_parent_base) {
5062 if (defined $file_name) {
5063 # read raw output
5064 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5065 $hash_parent_base, $hash_base,
5066 "--", (defined $file_parent ? $file_parent : ()), $file_name
5067 or die_error(500, "Open git-diff-tree failed");
5068 @difftree = map { chomp; $_ } <$fd>;
5069 close $fd
5070 or die_error(404, "Reading git-diff-tree failed");
5071 @difftree
5072 or die_error(404, "Blob diff not found");
5074 } elsif (defined $hash &&
5075 $hash =~ /[0-9a-fA-F]{40}/) {
5076 # try to find filename from $hash
5078 # read filtered raw output
5079 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5080 $hash_parent_base, $hash_base, "--"
5081 or die_error(500, "Open git-diff-tree failed");
5082 @difftree =
5083 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5084 # $hash == to_id
5085 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5086 map { chomp; $_ } <$fd>;
5087 close $fd
5088 or die_error(404, "Reading git-diff-tree failed");
5089 @difftree
5090 or die_error(404, "Blob diff not found");
5092 } else {
5093 die_error(400, "Missing one of the blob diff parameters");
5096 if (@difftree > 1) {
5097 die_error(400, "Ambiguous blob diff specification");
5100 %diffinfo = parse_difftree_raw_line($difftree[0]);
5101 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5102 $file_name ||= $diffinfo{'to_file'};
5104 $hash_parent ||= $diffinfo{'from_id'};
5105 $hash ||= $diffinfo{'to_id'};
5107 # non-textual hash id's can be cached
5108 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5109 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5110 $expires = '+1d';
5113 # open patch output
5114 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5115 '-p', ($format eq 'html' ? "--full-index" : ()),
5116 $hash_parent_base, $hash_base,
5117 "--", (defined $file_parent ? $file_parent : ()), $file_name
5118 or die_error(500, "Open git-diff-tree failed");
5121 # old/legacy style URI
5122 if (!%diffinfo && # if new style URI failed
5123 defined $hash && defined $hash_parent) {
5124 # fake git-diff-tree raw output
5125 $diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
5126 $diffinfo{'from_id'} = $hash_parent;
5127 $diffinfo{'to_id'} = $hash;
5128 if (defined $file_name) {
5129 if (defined $file_parent) {
5130 $diffinfo{'status'} = '2';
5131 $diffinfo{'from_file'} = $file_parent;
5132 $diffinfo{'to_file'} = $file_name;
5133 } else { # assume not renamed
5134 $diffinfo{'status'} = '1';
5135 $diffinfo{'from_file'} = $file_name;
5136 $diffinfo{'to_file'} = $file_name;
5138 } else { # no filename given
5139 $diffinfo{'status'} = '2';
5140 $diffinfo{'from_file'} = $hash_parent;
5141 $diffinfo{'to_file'} = $hash;
5144 # non-textual hash id's can be cached
5145 if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
5146 $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5147 $expires = '+1d';
5150 # open patch output
5151 open $fd, "-|", git_cmd(), "diff", @diff_opts,
5152 '-p', ($format eq 'html' ? "--full-index" : ()),
5153 $hash_parent, $hash, "--"
5154 or die_error(500, "Open git-diff failed");
5155 } else {
5156 die_error(400, "Missing one of the blob diff parameters")
5157 unless %diffinfo;
5160 # header
5161 if ($format eq 'html') {
5162 my $formats_nav =
5163 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5164 "raw");
5165 git_header_html(undef, $expires);
5166 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5167 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5168 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5169 } else {
5170 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5171 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5173 if (defined $file_name) {
5174 git_print_page_path($file_name, "blob", $hash_base);
5175 } else {
5176 print "<div class=\"page_path\"></div>\n";
5179 } elsif ($format eq 'plain') {
5180 print $cgi->header(
5181 -type => 'text/plain',
5182 -charset => 'utf-8',
5183 -expires => $expires,
5184 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5186 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5188 } else {
5189 die_error(400, "Unknown blobdiff format");
5192 # patch
5193 if ($format eq 'html') {
5194 print "<div class=\"page_body\">\n";
5196 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5197 close $fd;
5199 print "</div>\n"; # class="page_body"
5200 git_footer_html();
5202 } else {
5203 while (my $line = <$fd>) {
5204 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5205 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5207 print $line;
5209 last if $line =~ m!^\+\+\+!;
5211 local $/ = undef;
5212 print <$fd>;
5213 close $fd;
5217 sub git_blobdiff_plain {
5218 git_blobdiff('plain');
5221 sub git_commitdiff {
5222 my $format = shift || 'html';
5223 $hash ||= $hash_base || "HEAD";
5224 my %co = parse_commit($hash)
5225 or die_error(404, "Unknown commit object");
5227 # choose format for commitdiff for merge
5228 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5229 $hash_parent = '--cc';
5231 # we need to prepare $formats_nav before almost any parameter munging
5232 my $formats_nav;
5233 if ($format eq 'html') {
5234 $formats_nav =
5235 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5236 "raw");
5238 if (defined $hash_parent &&
5239 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5240 # commitdiff with two commits given
5241 my $hash_parent_short = $hash_parent;
5242 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5243 $hash_parent_short = substr($hash_parent, 0, 7);
5245 $formats_nav .=
5246 ' (from';
5247 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5248 if ($co{'parents'}[$i] eq $hash_parent) {
5249 $formats_nav .= ' parent ' . ($i+1);
5250 last;
5253 $formats_nav .= ': ' .
5254 $cgi->a({-href => href(action=>"commitdiff",
5255 hash=>$hash_parent)},
5256 esc_html($hash_parent_short)) .
5257 ')';
5258 } elsif (!$co{'parent'}) {
5259 # --root commitdiff
5260 $formats_nav .= ' (initial)';
5261 } elsif (scalar @{$co{'parents'}} == 1) {
5262 # single parent commit
5263 $formats_nav .=
5264 ' (parent: ' .
5265 $cgi->a({-href => href(action=>"commitdiff",
5266 hash=>$co{'parent'})},
5267 esc_html(substr($co{'parent'}, 0, 7))) .
5268 ')';
5269 } else {
5270 # merge commit
5271 if ($hash_parent eq '--cc') {
5272 $formats_nav .= ' | ' .
5273 $cgi->a({-href => href(action=>"commitdiff",
5274 hash=>$hash, hash_parent=>'-c')},
5275 'combined');
5276 } else { # $hash_parent eq '-c'
5277 $formats_nav .= ' | ' .
5278 $cgi->a({-href => href(action=>"commitdiff",
5279 hash=>$hash, hash_parent=>'--cc')},
5280 'compact');
5282 $formats_nav .=
5283 ' (merge: ' .
5284 join(' ', map {
5285 $cgi->a({-href => href(action=>"commitdiff",
5286 hash=>$_)},
5287 esc_html(substr($_, 0, 7)));
5288 } @{$co{'parents'}} ) .
5289 ')';
5293 my $hash_parent_param = $hash_parent;
5294 if (!defined $hash_parent_param) {
5295 # --cc for multiple parents, --root for parentless
5296 $hash_parent_param =
5297 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5300 # read commitdiff
5301 my $fd;
5302 my @difftree;
5303 if ($format eq 'html') {
5304 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5305 "--no-commit-id", "--patch-with-raw", "--full-index",
5306 $hash_parent_param, $hash, "--"
5307 or die_error(500, "Open git-diff-tree failed");
5309 while (my $line = <$fd>) {
5310 chomp $line;
5311 # empty line ends raw part of diff-tree output
5312 last unless $line;
5313 push @difftree, scalar parse_difftree_raw_line($line);
5316 } elsif ($format eq 'plain') {
5317 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5318 '-p', $hash_parent_param, $hash, "--"
5319 or die_error(500, "Open git-diff-tree failed");
5321 } else {
5322 die_error(400, "Unknown commitdiff format");
5325 # non-textual hash id's can be cached
5326 my $expires;
5327 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5328 $expires = "+1d";
5331 # write commit message
5332 if ($format eq 'html') {
5333 my $refs = git_get_references();
5334 my $ref = format_ref_marker($refs, $co{'id'});
5336 git_header_html(undef, $expires);
5337 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5338 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5339 git_print_authorship(\%co);
5340 print "<div class=\"page_body\">\n";
5341 if (@{$co{'comment'}} > 1) {
5342 print "<div class=\"log\">\n";
5343 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5344 print "</div>\n"; # class="log"
5347 } elsif ($format eq 'plain') {
5348 my $refs = git_get_references("tags");
5349 my $tagname = git_get_rev_name_tags($hash);
5350 my $filename = basename($project) . "-$hash.patch";
5352 print $cgi->header(
5353 -type => 'text/plain',
5354 -charset => 'utf-8',
5355 -expires => $expires,
5356 -content_disposition => 'inline; filename="' . "$filename" . '"');
5357 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5358 print "From: " . to_utf8($co{'author'}) . "\n";
5359 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5360 print "Subject: " . to_utf8($co{'title'}) . "\n";
5362 print "X-Git-Tag: $tagname\n" if $tagname;
5363 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5365 foreach my $line (@{$co{'comment'}}) {
5366 print to_utf8($line) . "\n";
5368 print "---\n\n";
5371 # write patch
5372 if ($format eq 'html') {
5373 my $use_parents = !defined $hash_parent ||
5374 $hash_parent eq '-c' || $hash_parent eq '--cc';
5375 git_difftree_body(\@difftree, $hash,
5376 $use_parents ? @{$co{'parents'}} : $hash_parent);
5377 print "<br/>\n";
5379 git_patchset_body($fd, \@difftree, $hash,
5380 $use_parents ? @{$co{'parents'}} : $hash_parent);
5381 close $fd;
5382 print "</div>\n"; # class="page_body"
5383 git_footer_html();
5385 } elsif ($format eq 'plain') {
5386 local $/ = undef;
5387 print <$fd>;
5388 close $fd
5389 or print "Reading git-diff-tree failed\n";
5393 sub git_commitdiff_plain {
5394 git_commitdiff('plain');
5397 sub git_history {
5398 if (!defined $hash_base) {
5399 $hash_base = git_get_head_hash($project);
5401 if (!defined $page) {
5402 $page = 0;
5404 my $ftype;
5405 my %co = parse_commit($hash_base)
5406 or die_error(404, "Unknown commit object");
5408 my $refs = git_get_references();
5409 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5411 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5412 $file_name, "--full-history")
5413 or die_error(404, "No such file or directory on given branch");
5415 if (!defined $hash && defined $file_name) {
5416 # some commits could have deleted file in question,
5417 # and not have it in tree, but one of them has to have it
5418 for (my $i = 0; $i <= @commitlist; $i++) {
5419 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5420 last if defined $hash;
5423 if (defined $hash) {
5424 $ftype = git_get_type($hash);
5426 if (!defined $ftype) {
5427 die_error(500, "Unknown type of object");
5430 my $paging_nav = '';
5431 if ($page > 0) {
5432 $paging_nav .=
5433 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5434 file_name=>$file_name)},
5435 "first");
5436 $paging_nav .= " &sdot; " .
5437 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5438 -accesskey => "p", -title => "Alt-p"}, "prev");
5439 } else {
5440 $paging_nav .= "first";
5441 $paging_nav .= " &sdot; prev";
5443 my $next_link = '';
5444 if ($#commitlist >= 100) {
5445 $next_link =
5446 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5447 -accesskey => "n", -title => "Alt-n"}, "next");
5448 $paging_nav .= " &sdot; $next_link";
5449 } else {
5450 $paging_nav .= " &sdot; next";
5453 git_header_html();
5454 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5455 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5456 git_print_page_path($file_name, $ftype, $hash_base);
5458 git_history_body(\@commitlist, 0, 99,
5459 $refs, $hash_base, $ftype, $next_link);
5461 git_footer_html();
5464 sub git_search {
5465 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5466 if (!defined $searchtext) {
5467 die_error(400, "Text field is empty");
5469 if (!defined $hash) {
5470 $hash = git_get_head_hash($project);
5472 my %co = parse_commit($hash);
5473 if (!%co) {
5474 die_error(404, "Unknown commit object");
5476 if (!defined $page) {
5477 $page = 0;
5480 $searchtype ||= 'commit';
5481 if ($searchtype eq 'pickaxe') {
5482 # pickaxe may take all resources of your box and run for several minutes
5483 # with every query - so decide by yourself how public you make this feature
5484 gitweb_check_feature('pickaxe')
5485 or die_error(403, "Pickaxe is disabled");
5487 if ($searchtype eq 'grep') {
5488 gitweb_check_feature('grep')
5489 or die_error(403, "Grep is disabled");
5492 git_header_html();
5494 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5495 my $greptype;
5496 if ($searchtype eq 'commit') {
5497 $greptype = "--grep=";
5498 } elsif ($searchtype eq 'author') {
5499 $greptype = "--author=";
5500 } elsif ($searchtype eq 'committer') {
5501 $greptype = "--committer=";
5503 $greptype .= $searchtext;
5504 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5505 $greptype, '--regexp-ignore-case',
5506 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5508 my $paging_nav = '';
5509 if ($page > 0) {
5510 $paging_nav .=
5511 $cgi->a({-href => href(action=>"search", hash=>$hash,
5512 searchtext=>$searchtext,
5513 searchtype=>$searchtype)},
5514 "first");
5515 $paging_nav .= " &sdot; " .
5516 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5517 -accesskey => "p", -title => "Alt-p"}, "prev");
5518 } else {
5519 $paging_nav .= "first";
5520 $paging_nav .= " &sdot; prev";
5522 my $next_link = '';
5523 if ($#commitlist >= 100) {
5524 $next_link =
5525 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5526 -accesskey => "n", -title => "Alt-n"}, "next");
5527 $paging_nav .= " &sdot; $next_link";
5528 } else {
5529 $paging_nav .= " &sdot; next";
5532 if ($#commitlist >= 100) {
5535 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5536 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5537 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5540 if ($searchtype eq 'pickaxe') {
5541 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5542 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5544 print "<table class=\"pickaxe search\">\n";
5545 my $alternate = 1;
5546 $/ = "\n";
5547 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5548 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5549 ($search_use_regexp ? '--pickaxe-regex' : ());
5550 undef %co;
5551 my @files;
5552 while (my $line = <$fd>) {
5553 chomp $line;
5554 next unless $line;
5556 my %set = parse_difftree_raw_line($line);
5557 if (defined $set{'commit'}) {
5558 # finish previous commit
5559 if (%co) {
5560 print "</td>\n" .
5561 "<td class=\"link\">" .
5562 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5563 " | " .
5564 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5565 print "</td>\n" .
5566 "</tr>\n";
5569 if ($alternate) {
5570 print "<tr class=\"dark\">\n";
5571 } else {
5572 print "<tr class=\"light\">\n";
5574 $alternate ^= 1;
5575 %co = parse_commit($set{'commit'});
5576 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5577 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5578 "<td><i>$author</i></td>\n" .
5579 "<td>" .
5580 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5581 -class => "list subject"},
5582 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5583 } elsif (defined $set{'to_id'}) {
5584 next if ($set{'to_id'} =~ m/^0{40}$/);
5586 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5587 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5588 -class => "list"},
5589 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5590 "<br/>\n";
5593 close $fd;
5595 # finish last commit (warning: repetition!)
5596 if (%co) {
5597 print "</td>\n" .
5598 "<td class=\"link\">" .
5599 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5600 " | " .
5601 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5602 print "</td>\n" .
5603 "</tr>\n";
5606 print "</table>\n";
5609 if ($searchtype eq 'grep') {
5610 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5611 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5613 print "<table class=\"grep_search\">\n";
5614 my $alternate = 1;
5615 my $matches = 0;
5616 $/ = "\n";
5617 open my $fd, "-|", git_cmd(), 'grep', '-n',
5618 $search_use_regexp ? ('-E', '-i') : '-F',
5619 $searchtext, $co{'tree'};
5620 my $lastfile = '';
5621 while (my $line = <$fd>) {
5622 chomp $line;
5623 my ($file, $lno, $ltext, $binary);
5624 last if ($matches++ > 1000);
5625 if ($line =~ /^Binary file (.+) matches$/) {
5626 $file = $1;
5627 $binary = 1;
5628 } else {
5629 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5631 if ($file ne $lastfile) {
5632 $lastfile and print "</td></tr>\n";
5633 if ($alternate++) {
5634 print "<tr class=\"dark\">\n";
5635 } else {
5636 print "<tr class=\"light\">\n";
5638 print "<td class=\"list\">".
5639 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5640 file_name=>"$file"),
5641 -class => "list"}, esc_path($file));
5642 print "</td><td>\n";
5643 $lastfile = $file;
5645 if ($binary) {
5646 print "<div class=\"binary\">Binary file</div>\n";
5647 } else {
5648 $ltext = untabify($ltext);
5649 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5650 $ltext = esc_html($1, -nbsp=>1);
5651 $ltext .= '<span class="match">';
5652 $ltext .= esc_html($2, -nbsp=>1);
5653 $ltext .= '</span>';
5654 $ltext .= esc_html($3, -nbsp=>1);
5655 } else {
5656 $ltext = esc_html($ltext, -nbsp=>1);
5658 print "<div class=\"pre\">" .
5659 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5660 file_name=>"$file").'#l'.$lno,
5661 -class => "linenr"}, sprintf('%4i', $lno))
5662 . ' ' . $ltext . "</div>\n";
5665 if ($lastfile) {
5666 print "</td></tr>\n";
5667 if ($matches > 1000) {
5668 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5670 } else {
5671 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5673 close $fd;
5675 print "</table>\n";
5677 git_footer_html();
5680 sub git_search_help {
5681 git_header_html();
5682 git_print_page_nav('','', $hash,$hash,$hash);
5683 print <<EOT;
5684 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5685 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5686 the pattern entered is recognized as the POSIX extended
5687 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5688 insensitive).</p>
5689 <dl>
5690 <dt><b>commit</b></dt>
5691 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
5693 my ($have_grep) = gitweb_check_feature('grep');
5694 if ($have_grep) {
5695 print <<EOT;
5696 <dt><b>grep</b></dt>
5697 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
5698 a different one) are searched for the given pattern. On large trees, this search can take
5699 a while and put some strain on the server, so please use it with some consideration. Note that
5700 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5701 case-sensitive.</dd>
5704 print <<EOT;
5705 <dt><b>author</b></dt>
5706 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
5707 <dt><b>committer</b></dt>
5708 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
5710 my ($have_pickaxe) = gitweb_check_feature('pickaxe');
5711 if ($have_pickaxe) {
5712 print <<EOT;
5713 <dt><b>pickaxe</b></dt>
5714 <dd>All commits that caused the string to appear or disappear from any file (changes that
5715 added, removed or "modified" the string) will be listed. This search can take a while and
5716 takes a lot of strain on the server, so please use it wisely. Note that since you may be
5717 interested even in changes just changing the case as well, this search is case sensitive.</dd>
5720 print "</dl>\n";
5721 git_footer_html();
5724 sub git_shortlog {
5725 my $head = git_get_head_hash($project);
5726 if (!defined $hash) {
5727 $hash = $head;
5729 if (!defined $page) {
5730 $page = 0;
5732 my $refs = git_get_references();
5734 my $commit_hash = $hash;
5735 if (defined $hash_parent) {
5736 $commit_hash = "$hash_parent..$hash";
5738 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
5740 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
5741 my $next_link = '';
5742 if ($#commitlist >= 100) {
5743 $next_link =
5744 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5745 -accesskey => "n", -title => "Alt-n"}, "next");
5748 git_header_html();
5749 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
5750 git_print_header_div('summary', $project);
5752 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
5754 git_footer_html();
5757 ## ......................................................................
5758 ## feeds (RSS, Atom; OPML)
5760 sub git_feed {
5761 my $format = shift || 'atom';
5762 my ($have_blame) = gitweb_check_feature('blame');
5764 # Atom: http://www.atomenabled.org/developers/syndication/
5765 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
5766 if ($format ne 'rss' && $format ne 'atom') {
5767 die_error(400, "Unknown web feed format");
5770 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
5771 my $head = $hash || 'HEAD';
5772 my @commitlist = parse_commits($head, 150, 0, $file_name);
5774 my %latest_commit;
5775 my %latest_date;
5776 my $content_type = "application/$format+xml";
5777 if (defined $cgi->http('HTTP_ACCEPT') &&
5778 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
5779 # browser (feed reader) prefers text/xml
5780 $content_type = 'text/xml';
5782 if (defined($commitlist[0])) {
5783 %latest_commit = %{$commitlist[0]};
5784 %latest_date = parse_date($latest_commit{'author_epoch'});
5785 print $cgi->header(
5786 -type => $content_type,
5787 -charset => 'utf-8',
5788 -last_modified => $latest_date{'rfc2822'});
5789 } else {
5790 print $cgi->header(
5791 -type => $content_type,
5792 -charset => 'utf-8');
5795 # Optimization: skip generating the body if client asks only
5796 # for Last-Modified date.
5797 return if ($cgi->request_method() eq 'HEAD');
5799 # header variables
5800 my $title = "$site_name - $project/$action";
5801 my $feed_type = 'log';
5802 if (defined $hash) {
5803 $title .= " - '$hash'";
5804 $feed_type = 'branch log';
5805 if (defined $file_name) {
5806 $title .= " :: $file_name";
5807 $feed_type = 'history';
5809 } elsif (defined $file_name) {
5810 $title .= " - $file_name";
5811 $feed_type = 'history';
5813 $title .= " $feed_type";
5814 my $descr = git_get_project_description($project);
5815 if (defined $descr) {
5816 $descr = esc_html($descr);
5817 } else {
5818 $descr = "$project " .
5819 ($format eq 'rss' ? 'RSS' : 'Atom') .
5820 " feed";
5822 my $owner = git_get_project_owner($project);
5823 $owner = esc_html($owner);
5825 #header
5826 my $alt_url;
5827 if (defined $file_name) {
5828 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
5829 } elsif (defined $hash) {
5830 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
5831 } else {
5832 $alt_url = href(-full=>1, action=>"summary");
5834 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
5835 if ($format eq 'rss') {
5836 print <<XML;
5837 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
5838 <channel>
5840 print "<title>$title</title>\n" .
5841 "<link>$alt_url</link>\n" .
5842 "<description>$descr</description>\n" .
5843 "<language>en</language>\n";
5844 } elsif ($format eq 'atom') {
5845 print <<XML;
5846 <feed xmlns="http://www.w3.org/2005/Atom">
5848 print "<title>$title</title>\n" .
5849 "<subtitle>$descr</subtitle>\n" .
5850 '<link rel="alternate" type="text/html" href="' .
5851 $alt_url . '" />' . "\n" .
5852 '<link rel="self" type="' . $content_type . '" href="' .
5853 $cgi->self_url() . '" />' . "\n" .
5854 "<id>" . href(-full=>1) . "</id>\n" .
5855 # use project owner for feed author
5856 "<author><name>$owner</name></author>\n";
5857 if (defined $favicon) {
5858 print "<icon>" . esc_url($favicon) . "</icon>\n";
5860 if (defined $logo_url) {
5861 # not twice as wide as tall: 72 x 27 pixels
5862 print "<logo>" . esc_url($logo) . "</logo>\n";
5864 if (! %latest_date) {
5865 # dummy date to keep the feed valid until commits trickle in:
5866 print "<updated>1970-01-01T00:00:00Z</updated>\n";
5867 } else {
5868 print "<updated>$latest_date{'iso-8601'}</updated>\n";
5872 # contents
5873 for (my $i = 0; $i <= $#commitlist; $i++) {
5874 my %co = %{$commitlist[$i]};
5875 my $commit = $co{'id'};
5876 # we read 150, we always show 30 and the ones more recent than 48 hours
5877 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
5878 last;
5880 my %cd = parse_date($co{'author_epoch'});
5882 # get list of changed files
5883 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5884 $co{'parent'} || "--root",
5885 $co{'id'}, "--", (defined $file_name ? $file_name : ())
5886 or next;
5887 my @difftree = map { chomp; $_ } <$fd>;
5888 close $fd
5889 or next;
5891 # print element (entry, item)
5892 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
5893 if ($format eq 'rss') {
5894 print "<item>\n" .
5895 "<title>" . esc_html($co{'title'}) . "</title>\n" .
5896 "<author>" . esc_html($co{'author'}) . "</author>\n" .
5897 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
5898 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
5899 "<link>$co_url</link>\n" .
5900 "<description>" . esc_html($co{'title'}) . "</description>\n" .
5901 "<content:encoded>" .
5902 "<![CDATA[\n";
5903 } elsif ($format eq 'atom') {
5904 print "<entry>\n" .
5905 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
5906 "<updated>$cd{'iso-8601'}</updated>\n" .
5907 "<author>\n" .
5908 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
5909 if ($co{'author_email'}) {
5910 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
5912 print "</author>\n" .
5913 # use committer for contributor
5914 "<contributor>\n" .
5915 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
5916 if ($co{'committer_email'}) {
5917 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
5919 print "</contributor>\n" .
5920 "<published>$cd{'iso-8601'}</published>\n" .
5921 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
5922 "<id>$co_url</id>\n" .
5923 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
5924 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
5926 my $comment = $co{'comment'};
5927 print "<pre>\n";
5928 foreach my $line (@$comment) {
5929 $line = esc_html($line);
5930 print "$line\n";
5932 print "</pre><ul>\n";
5933 foreach my $difftree_line (@difftree) {
5934 my %difftree = parse_difftree_raw_line($difftree_line);
5935 next if !$difftree{'from_id'};
5937 my $file = $difftree{'file'} || $difftree{'to_file'};
5939 print "<li>" .
5940 "[" .
5941 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
5942 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
5943 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
5944 file_name=>$file, file_parent=>$difftree{'from_file'}),
5945 -title => "diff"}, 'D');
5946 if ($have_blame) {
5947 print $cgi->a({-href => href(-full=>1, action=>"blame",
5948 file_name=>$file, hash_base=>$commit),
5949 -title => "blame"}, 'B');
5951 # if this is not a feed of a file history
5952 if (!defined $file_name || $file_name ne $file) {
5953 print $cgi->a({-href => href(-full=>1, action=>"history",
5954 file_name=>$file, hash=>$commit),
5955 -title => "history"}, 'H');
5957 $file = esc_path($file);
5958 print "] ".
5959 "$file</li>\n";
5961 if ($format eq 'rss') {
5962 print "</ul>]]>\n" .
5963 "</content:encoded>\n" .
5964 "</item>\n";
5965 } elsif ($format eq 'atom') {
5966 print "</ul>\n</div>\n" .
5967 "</content>\n" .
5968 "</entry>\n";
5972 # end of feed
5973 if ($format eq 'rss') {
5974 print "</channel>\n</rss>\n";
5975 } elsif ($format eq 'atom') {
5976 print "</feed>\n";
5980 sub git_rss {
5981 git_feed('rss');
5984 sub git_atom {
5985 git_feed('atom');
5988 sub git_opml {
5989 my @list = git_get_projects_list();
5991 print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
5992 print <<XML;
5993 <?xml version="1.0" encoding="utf-8"?>
5994 <opml version="1.0">
5995 <head>
5996 <title>$site_name OPML Export</title>
5997 </head>
5998 <body>
5999 <outline text="git RSS feeds">
6002 foreach my $pr (@list) {
6003 my %proj = %$pr;
6004 my $head = git_get_head_hash($proj{'path'});
6005 if (!defined $head) {
6006 next;
6008 $git_dir = "$projectroot/$proj{'path'}";
6009 my %co = parse_commit($head);
6010 if (!%co) {
6011 next;
6014 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6015 my $rss = "$my_url?p=$proj{'path'};a=rss";
6016 my $html = "$my_url?p=$proj{'path'};a=summary";
6017 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6019 print <<XML;
6020 </outline>
6021 </body>
6022 </opml>