Merge commit 'refs/top-bases/t/projlist-cache/caching' into t/projlist-cache/caching
[git/gitweb.git] / gitweb / gitweb.perl
blob9dfd6998ab8c81d2358d79dcb8d58b41b64a9c3d
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 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
31 # needed and used only for URLs with nonempty PATH_INFO
32 our $base_url = $my_url;
34 # When the script is used as DirectoryIndex, the URL does not contain the name
35 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
36 # have to do it ourselves. We make $path_info global because it's also used
37 # later on.
39 # Another issue with the script being the DirectoryIndex is that the resulting
40 # $my_url data is not the full script URL: this is good, because we want
41 # generated links to keep implying the script name if it wasn't explicitly
42 # indicated in the URL we're handling, but it means that $my_url cannot be used
43 # as base URL.
44 # Therefore, if we needed to strip PATH_INFO, then we know that we have
45 # to build the base URL ourselves:
46 our $path_info = $ENV{"PATH_INFO"};
47 if ($path_info) {
48 if ($my_url =~ s,\Q$path_info\E$,, &&
49 $my_uri =~ s,\Q$path_info\E$,, &&
50 defined $ENV{'SCRIPT_NAME'}) {
51 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
55 # core git executable to use
56 # this can just be "git" if your webserver has a sensible PATH
57 our $GIT = "++GIT_BINDIR++/git";
59 # absolute fs-path which will be prepended to the project path
60 #our $projectroot = "/pub/scm";
61 our $projectroot = "++GITWEB_PROJECTROOT++";
63 # fs traversing limit for getting project list
64 # the number is relative to the projectroot
65 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
67 # target of the home link on top of all pages
68 our $home_link = $my_uri || "/";
70 # string of the home link on top of all pages
71 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
73 # name of your site or organization to appear in page titles
74 # replace this with something more descriptive for clearer bookmarks
75 our $site_name = "++GITWEB_SITENAME++"
76 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
78 # filename of html text to include at top of each page
79 our $site_header = "++GITWEB_SITE_HEADER++";
80 # html text to include at home page
81 our $home_text = "++GITWEB_HOMETEXT++";
82 # filename of html text to include at bottom of each page
83 our $site_footer = "++GITWEB_SITE_FOOTER++";
85 # URI of stylesheets
86 our @stylesheets = ("++GITWEB_CSS++");
87 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
88 our $stylesheet = undef;
89 # URI of GIT logo (72x27 size)
90 our $logo = "++GITWEB_LOGO++";
91 # URI of GIT favicon, assumed to be image/png type
92 our $favicon = "++GITWEB_FAVICON++";
94 # URI and label (title) of GIT logo link
95 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
96 #our $logo_label = "git documentation";
97 our $logo_url = "http://git-scm.com/";
98 our $logo_label = "git homepage";
100 # source of projects list
101 our $projects_list = "++GITWEB_LIST++";
103 # the width (in characters) of the projects list "Description" column
104 our $projects_list_description_width = 25;
106 # default order of projects list
107 # valid values are none, project, descr, owner, and age
108 our $default_projects_order = "project";
110 # show repository only if this file exists
111 # (only effective if this variable evaluates to true)
112 our $export_ok = "++GITWEB_EXPORT_OK++";
114 # show repository only if this subroutine returns true
115 # when given the path to the project, for example:
116 # sub { return -e "$_[0]/git-daemon-export-ok"; }
117 our $export_auth_hook = undef;
119 # only allow viewing of repositories also shown on the overview page
120 our $strict_export = "++GITWEB_STRICT_EXPORT++";
122 # list of git base URLs used for URL to where fetch project from,
123 # i.e. full URL is "$git_base_url/$project"
124 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
126 # default blob_plain mimetype and default charset for text/plain blob
127 our $default_blob_plain_mimetype = 'text/plain';
128 our $default_text_plain_charset = undef;
130 # file to use for guessing MIME types before trying /etc/mime.types
131 # (relative to the current git repository)
132 our $mimetypes_file = undef;
134 # assume this charset if line contains non-UTF-8 characters;
135 # it should be valid encoding (see Encoding::Supported(3pm) for list),
136 # for which encoding all byte sequences are valid, for example
137 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
138 # could be even 'utf-8' for the old behavior)
139 our $fallback_encoding = 'latin1';
141 # rename detection options for git-diff and git-diff-tree
142 # - default is '-M', with the cost proportional to
143 # (number of removed files) * (number of new files).
144 # - more costly is '-C' (which implies '-M'), with the cost proportional to
145 # (number of changed files + number of removed files) * (number of new files)
146 # - even more costly is '-C', '--find-copies-harder' with cost
147 # (number of files in the original tree) * (number of new files)
148 # - one might want to include '-B' option, e.g. '-B', '-M'
149 our @diff_opts = ('-M'); # taken from git_commit
151 # Disables features that would allow repository owners to inject script into
152 # the gitweb domain.
153 our $prevent_xss = 0;
155 # projects list cache for busy sites with many projects;
156 # if you set this to non-zero, it will be used as the cached
157 # index lifetime in minutes
159 # the cached list version is stored in $cache_dir/$cache_name and can
160 # be tweaked by other scripts running with the same uid as gitweb -
161 # use this ONLY at secure installations; only single gitweb project
162 # root per system is supported, unless you tweak configuration!
163 our $projlist_cache_lifetime = 0; # in minutes
164 # FHS compliant $cache_dir would be "/var/cache/gitweb"
165 our $cache_dir =
166 (defined $ENV{'TMPDIR'} ? $ENV{'TMPDIR'} : '/tmp').'/gitweb';
167 our $projlist_cache_name = 'gitweb.index.cache';
169 # information about snapshot formats that gitweb is capable of serving
170 our %known_snapshot_formats = (
171 # name => {
172 # 'display' => display name,
173 # 'type' => mime type,
174 # 'suffix' => filename suffix,
175 # 'format' => --format for git-archive,
176 # 'compressor' => [compressor command and arguments]
177 # (array reference, optional)
178 # 'disabled' => boolean (optional)}
180 'tgz' => {
181 'display' => 'tar.gz',
182 'type' => 'application/x-gzip',
183 'suffix' => '.tar.gz',
184 'format' => 'tar',
185 'compressor' => ['gzip']},
187 'tbz2' => {
188 'display' => 'tar.bz2',
189 'type' => 'application/x-bzip2',
190 'suffix' => '.tar.bz2',
191 'format' => 'tar',
192 'compressor' => ['bzip2']},
194 'txz' => {
195 'display' => 'tar.xz',
196 'type' => 'application/x-xz',
197 'suffix' => '.tar.xz',
198 'format' => 'tar',
199 'compressor' => ['xz'],
200 'disabled' => 1},
202 'zip' => {
203 'display' => 'zip',
204 'type' => 'application/x-zip',
205 'suffix' => '.zip',
206 'format' => 'zip'},
209 # Aliases so we understand old gitweb.snapshot values in repository
210 # configuration.
211 our %known_snapshot_format_aliases = (
212 'gzip' => 'tgz',
213 'bzip2' => 'tbz2',
214 'xz' => 'txz',
216 # backward compatibility: legacy gitweb config support
217 'x-gzip' => undef, 'gz' => undef,
218 'x-bzip2' => undef, 'bz2' => undef,
219 'x-zip' => undef, '' => undef,
222 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
223 # are changed, it may be appropriate to change these values too via
224 # $GITWEB_CONFIG.
225 our %avatar_size = (
226 'default' => 16,
227 'double' => 32
230 # You define site-wide feature defaults here; override them with
231 # $GITWEB_CONFIG as necessary.
232 our %feature = (
233 # feature => {
234 # 'sub' => feature-sub (subroutine),
235 # 'override' => allow-override (boolean),
236 # 'default' => [ default options...] (array reference)}
238 # if feature is overridable (it means that allow-override has true value),
239 # then feature-sub will be called with default options as parameters;
240 # return value of feature-sub indicates if to enable specified feature
242 # if there is no 'sub' key (no feature-sub), then feature cannot be
243 # overriden
245 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
246 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
247 # is enabled
249 # Enable the 'blame' blob view, showing the last commit that modified
250 # each line in the file. This can be very CPU-intensive.
252 # To enable system wide have in $GITWEB_CONFIG
253 # $feature{'blame'}{'default'} = [1];
254 # To have project specific config enable override in $GITWEB_CONFIG
255 # $feature{'blame'}{'override'} = 1;
256 # and in project config gitweb.blame = 0|1;
257 'blame' => {
258 'sub' => sub { feature_bool('blame', @_) },
259 'override' => 0,
260 'default' => [0]},
262 # Enable the 'snapshot' link, providing a compressed archive of any
263 # tree. This can potentially generate high traffic if you have large
264 # project.
266 # Value is a list of formats defined in %known_snapshot_formats that
267 # you wish to offer.
268 # To disable system wide have in $GITWEB_CONFIG
269 # $feature{'snapshot'}{'default'} = [];
270 # To have project specific config enable override in $GITWEB_CONFIG
271 # $feature{'snapshot'}{'override'} = 1;
272 # and in project config, a comma-separated list of formats or "none"
273 # to disable. Example: gitweb.snapshot = tbz2,zip;
274 'snapshot' => {
275 'sub' => \&feature_snapshot,
276 'override' => 0,
277 'default' => ['tgz']},
279 # Enable text search, which will list the commits which match author,
280 # committer or commit text to a given string. Enabled by default.
281 # Project specific override is not supported.
282 'search' => {
283 'override' => 0,
284 'default' => [1]},
286 # Enable grep search, which will list the files in currently selected
287 # tree containing the given string. Enabled by default. This can be
288 # potentially CPU-intensive, of course.
290 # To enable system wide have in $GITWEB_CONFIG
291 # $feature{'grep'}{'default'} = [1];
292 # To have project specific config enable override in $GITWEB_CONFIG
293 # $feature{'grep'}{'override'} = 1;
294 # and in project config gitweb.grep = 0|1;
295 'grep' => {
296 'sub' => sub { feature_bool('grep', @_) },
297 'override' => 0,
298 'default' => [1]},
300 # Enable the pickaxe search, which will list the commits that modified
301 # a given string in a file. This can be practical and quite faster
302 # alternative to 'blame', but still potentially CPU-intensive.
304 # To enable system wide have in $GITWEB_CONFIG
305 # $feature{'pickaxe'}{'default'} = [1];
306 # To have project specific config enable override in $GITWEB_CONFIG
307 # $feature{'pickaxe'}{'override'} = 1;
308 # and in project config gitweb.pickaxe = 0|1;
309 'pickaxe' => {
310 'sub' => sub { feature_bool('pickaxe', @_) },
311 'override' => 0,
312 'default' => [1]},
314 # Enable showing size of blobs in a 'tree' view, in a separate
315 # column, similar to what 'ls -l' does. This cost a bit of IO.
317 # To disable system wide have in $GITWEB_CONFIG
318 # $feature{'show-sizes'}{'default'} = [0];
319 # To have project specific config enable override in $GITWEB_CONFIG
320 # $feature{'show-sizes'}{'override'} = 1;
321 # and in project config gitweb.showsizes = 0|1;
322 'show-sizes' => {
323 'sub' => sub { feature_bool('showsizes', @_) },
324 'override' => 0,
325 'default' => [1]},
327 # Make gitweb use an alternative format of the URLs which can be
328 # more readable and natural-looking: project name is embedded
329 # directly in the path and the query string contains other
330 # auxiliary information. All gitweb installations recognize
331 # URL in either format; this configures in which formats gitweb
332 # generates links.
334 # To enable system wide have in $GITWEB_CONFIG
335 # $feature{'pathinfo'}{'default'} = [1];
336 # Project specific override is not supported.
338 # Note that you will need to change the default location of CSS,
339 # favicon, logo and possibly other files to an absolute URL. Also,
340 # if gitweb.cgi serves as your indexfile, you will need to force
341 # $my_uri to contain the script name in your $GITWEB_CONFIG.
342 'pathinfo' => {
343 'override' => 0,
344 'default' => [0]},
346 # Make gitweb consider projects in project root subdirectories
347 # to be forks of existing projects. Given project $projname.git,
348 # projects matching $projname/*.git will not be shown in the main
349 # projects list, instead a '+' mark will be added to $projname
350 # there and a 'forks' view will be enabled for the project, listing
351 # all the forks. If project list is taken from a file, forks have
352 # to be listed after the main project.
354 # To enable system wide have in $GITWEB_CONFIG
355 # $feature{'forks'}{'default'} = [1];
356 # Project specific override is not supported.
357 'forks' => {
358 'override' => 0,
359 'default' => [0]},
361 # Insert custom links to the action bar of all project pages.
362 # This enables you mainly to link to third-party scripts integrating
363 # into gitweb; e.g. git-browser for graphical history representation
364 # or custom web-based repository administration interface.
366 # The 'default' value consists of a list of triplets in the form
367 # (label, link, position) where position is the label after which
368 # to insert the link and link is a format string where %n expands
369 # to the project name, %f to the project path within the filesystem,
370 # %h to the current hash (h gitweb parameter) and %b to the current
371 # hash base (hb gitweb parameter); %% expands to %.
373 # To enable system wide have in $GITWEB_CONFIG e.g.
374 # $feature{'actions'}{'default'} = [('graphiclog',
375 # '/git-browser/by-commit.html?r=%n', 'summary')];
376 # Project specific override is not supported.
377 'actions' => {
378 'override' => 0,
379 'default' => []},
381 # Allow gitweb scan project content tags described in ctags/
382 # of project repository, and display the popular Web 2.0-ish
383 # "tag cloud" near the project list. Note that this is something
384 # COMPLETELY different from the normal Git tags.
386 # gitweb by itself can show existing tags, but it does not handle
387 # tagging itself; you need an external application for that.
388 # For an example script, check Girocco's cgi/tagproj.cgi.
389 # You may want to install the HTML::TagCloud Perl module to get
390 # a pretty tag cloud instead of just a list of tags.
392 # To enable system wide have in $GITWEB_CONFIG
393 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
394 # Project specific override is not supported.
395 'ctags' => {
396 'override' => 0,
397 'default' => [0]},
399 # The maximum number of patches in a patchset generated in patch
400 # view. Set this to 0 or undef to disable patch view, or to a
401 # negative number to remove any limit.
403 # To disable system wide have in $GITWEB_CONFIG
404 # $feature{'patches'}{'default'} = [0];
405 # To have project specific config enable override in $GITWEB_CONFIG
406 # $feature{'patches'}{'override'} = 1;
407 # and in project config gitweb.patches = 0|n;
408 # where n is the maximum number of patches allowed in a patchset.
409 'patches' => {
410 'sub' => \&feature_patches,
411 'override' => 0,
412 'default' => [16]},
414 # Avatar support. When this feature is enabled, views such as
415 # shortlog or commit will display an avatar associated with
416 # the email of the committer(s) and/or author(s).
418 # Currently available providers are gravatar and picon.
419 # If an unknown provider is specified, the feature is disabled.
421 # Gravatar depends on Digest::MD5.
422 # Picon currently relies on the indiana.edu database.
424 # To enable system wide have in $GITWEB_CONFIG
425 # $feature{'avatar'}{'default'} = ['<provider>'];
426 # where <provider> is either gravatar or picon.
427 # To have project specific config enable override in $GITWEB_CONFIG
428 # $feature{'avatar'}{'override'} = 1;
429 # and in project config gitweb.avatar = <provider>;
430 'avatar' => {
431 'sub' => \&feature_avatar,
432 'override' => 0,
433 'default' => ['']},
436 sub gitweb_get_feature {
437 my ($name) = @_;
438 return unless exists $feature{$name};
439 my ($sub, $override, @defaults) = (
440 $feature{$name}{'sub'},
441 $feature{$name}{'override'},
442 @{$feature{$name}{'default'}});
443 if (!$override) { return @defaults; }
444 if (!defined $sub) {
445 warn "feature $name is not overridable";
446 return @defaults;
448 return $sub->(@defaults);
451 # A wrapper to check if a given feature is enabled.
452 # With this, you can say
454 # my $bool_feat = gitweb_check_feature('bool_feat');
455 # gitweb_check_feature('bool_feat') or somecode;
457 # instead of
459 # my ($bool_feat) = gitweb_get_feature('bool_feat');
460 # (gitweb_get_feature('bool_feat'))[0] or somecode;
462 sub gitweb_check_feature {
463 return (gitweb_get_feature(@_))[0];
467 sub feature_bool {
468 my $key = shift;
469 my ($val) = git_get_project_config($key, '--bool');
471 if (!defined $val) {
472 return ($_[0]);
473 } elsif ($val eq 'true') {
474 return (1);
475 } elsif ($val eq 'false') {
476 return (0);
480 sub feature_snapshot {
481 my (@fmts) = @_;
483 my ($val) = git_get_project_config('snapshot');
485 if ($val) {
486 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
489 return @fmts;
492 sub feature_patches {
493 my @val = (git_get_project_config('patches', '--int'));
495 if (@val) {
496 return @val;
499 return ($_[0]);
502 sub feature_avatar {
503 my @val = (git_get_project_config('avatar'));
505 return @val ? @val : @_;
508 # checking HEAD file with -e is fragile if the repository was
509 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
510 # and then pruned.
511 sub check_head_link {
512 my ($dir) = @_;
513 my $headfile = "$dir/HEAD";
514 return ((-e $headfile) ||
515 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
518 sub check_export_ok {
519 my ($dir) = @_;
520 return (check_head_link($dir) &&
521 (!$export_ok || -e "$dir/$export_ok") &&
522 (!$export_auth_hook || $export_auth_hook->($dir)));
525 # process alternate names for backward compatibility
526 # filter out unsupported (unknown) snapshot formats
527 sub filter_snapshot_fmts {
528 my @fmts = @_;
530 @fmts = map {
531 exists $known_snapshot_format_aliases{$_} ?
532 $known_snapshot_format_aliases{$_} : $_} @fmts;
533 @fmts = grep {
534 exists $known_snapshot_formats{$_} &&
535 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
538 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
539 if (-e $GITWEB_CONFIG) {
540 do $GITWEB_CONFIG;
541 } else {
542 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
543 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
546 # version of the core git binary
547 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
549 $projects_list ||= $projectroot;
551 # ======================================================================
552 # input validation and dispatch
554 # input parameters can be collected from a variety of sources (presently, CGI
555 # and PATH_INFO), so we define an %input_params hash that collects them all
556 # together during validation: this allows subsequent uses (e.g. href()) to be
557 # agnostic of the parameter origin
559 our %input_params = ();
561 # input parameters are stored with the long parameter name as key. This will
562 # also be used in the href subroutine to convert parameters to their CGI
563 # equivalent, and since the href() usage is the most frequent one, we store
564 # the name -> CGI key mapping here, instead of the reverse.
566 # XXX: Warning: If you touch this, check the search form for updating,
567 # too.
569 our @cgi_param_mapping = (
570 project => "p",
571 action => "a",
572 file_name => "f",
573 file_parent => "fp",
574 hash => "h",
575 hash_parent => "hp",
576 hash_base => "hb",
577 hash_parent_base => "hpb",
578 page => "pg",
579 order => "o",
580 searchtext => "s",
581 searchtype => "st",
582 snapshot_format => "sf",
583 extra_options => "opt",
584 search_use_regexp => "sr",
586 our %cgi_param_mapping = @cgi_param_mapping;
588 # we will also need to know the possible actions, for validation
589 our %actions = (
590 "blame" => \&git_blame,
591 "blobdiff" => \&git_blobdiff,
592 "blobdiff_plain" => \&git_blobdiff_plain,
593 "blob" => \&git_blob,
594 "blob_plain" => \&git_blob_plain,
595 "commitdiff" => \&git_commitdiff,
596 "commitdiff_plain" => \&git_commitdiff_plain,
597 "commit" => \&git_commit,
598 "forks" => \&git_forks,
599 "heads" => \&git_heads,
600 "history" => \&git_history,
601 "log" => \&git_log,
602 "patch" => \&git_patch,
603 "patches" => \&git_patches,
604 "rss" => \&git_rss,
605 "atom" => \&git_atom,
606 "search" => \&git_search,
607 "search_help" => \&git_search_help,
608 "shortlog" => \&git_shortlog,
609 "summary" => \&git_summary,
610 "tag" => \&git_tag,
611 "tags" => \&git_tags,
612 "tree" => \&git_tree,
613 "snapshot" => \&git_snapshot,
614 "object" => \&git_object,
615 # those below don't need $project
616 "opml" => \&git_opml,
617 "project_list" => \&git_project_list,
618 "project_index" => \&git_project_index,
621 # finally, we have the hash of allowed extra_options for the commands that
622 # allow them
623 our %allowed_options = (
624 "--no-merges" => [ qw(rss atom log shortlog history) ],
627 # fill %input_params with the CGI parameters. All values except for 'opt'
628 # should be single values, but opt can be an array. We should probably
629 # build an array of parameters that can be multi-valued, but since for the time
630 # being it's only this one, we just single it out
631 while (my ($name, $symbol) = each %cgi_param_mapping) {
632 if ($symbol eq 'opt') {
633 $input_params{$name} = [ $cgi->param($symbol) ];
634 } else {
635 $input_params{$name} = $cgi->param($symbol);
639 # now read PATH_INFO and update the parameter list for missing parameters
640 sub evaluate_path_info {
641 return if defined $input_params{'project'};
642 return if !$path_info;
643 $path_info =~ s,^/+,,;
644 return if !$path_info;
646 # find which part of PATH_INFO is project
647 my $project = $path_info;
648 $project =~ s,/+$,,;
649 while ($project && !check_head_link("$projectroot/$project")) {
650 $project =~ s,/*[^/]*$,,;
652 return unless $project;
653 $input_params{'project'} = $project;
655 # do not change any parameters if an action is given using the query string
656 return if $input_params{'action'};
657 $path_info =~ s,^\Q$project\E/*,,;
659 # next, check if we have an action
660 my $action = $path_info;
661 $action =~ s,/.*$,,;
662 if (exists $actions{$action}) {
663 $path_info =~ s,^$action/*,,;
664 $input_params{'action'} = $action;
667 # list of actions that want hash_base instead of hash, but can have no
668 # pathname (f) parameter
669 my @wants_base = (
670 'tree',
671 'history',
674 # we want to catch
675 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
676 my ($parentrefname, $parentpathname, $refname, $pathname) =
677 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
679 # first, analyze the 'current' part
680 if (defined $pathname) {
681 # we got "branch:filename" or "branch:dir/"
682 # we could use git_get_type(branch:pathname), but:
683 # - it needs $git_dir
684 # - it does a git() call
685 # - the convention of terminating directories with a slash
686 # makes it superfluous
687 # - embedding the action in the PATH_INFO would make it even
688 # more superfluous
689 $pathname =~ s,^/+,,;
690 if (!$pathname || substr($pathname, -1) eq "/") {
691 $input_params{'action'} ||= "tree";
692 $pathname =~ s,/$,,;
693 } else {
694 # the default action depends on whether we had parent info
695 # or not
696 if ($parentrefname) {
697 $input_params{'action'} ||= "blobdiff_plain";
698 } else {
699 $input_params{'action'} ||= "blob_plain";
702 $input_params{'hash_base'} ||= $refname;
703 $input_params{'file_name'} ||= $pathname;
704 } elsif (defined $refname) {
705 # we got "branch". In this case we have to choose if we have to
706 # set hash or hash_base.
708 # Most of the actions without a pathname only want hash to be
709 # set, except for the ones specified in @wants_base that want
710 # hash_base instead. It should also be noted that hand-crafted
711 # links having 'history' as an action and no pathname or hash
712 # set will fail, but that happens regardless of PATH_INFO.
713 $input_params{'action'} ||= "shortlog";
714 if (grep { $_ eq $input_params{'action'} } @wants_base) {
715 $input_params{'hash_base'} ||= $refname;
716 } else {
717 $input_params{'hash'} ||= $refname;
721 # next, handle the 'parent' part, if present
722 if (defined $parentrefname) {
723 # a missing pathspec defaults to the 'current' filename, allowing e.g.
724 # someproject/blobdiff/oldrev..newrev:/filename
725 if ($parentpathname) {
726 $parentpathname =~ s,^/+,,;
727 $parentpathname =~ s,/$,,;
728 $input_params{'file_parent'} ||= $parentpathname;
729 } else {
730 $input_params{'file_parent'} ||= $input_params{'file_name'};
732 # we assume that hash_parent_base is wanted if a path was specified,
733 # or if the action wants hash_base instead of hash
734 if (defined $input_params{'file_parent'} ||
735 grep { $_ eq $input_params{'action'} } @wants_base) {
736 $input_params{'hash_parent_base'} ||= $parentrefname;
737 } else {
738 $input_params{'hash_parent'} ||= $parentrefname;
742 # for the snapshot action, we allow URLs in the form
743 # $project/snapshot/$hash.ext
744 # where .ext determines the snapshot and gets removed from the
745 # passed $refname to provide the $hash.
747 # To be able to tell that $refname includes the format extension, we
748 # require the following two conditions to be satisfied:
749 # - the hash input parameter MUST have been set from the $refname part
750 # of the URL (i.e. they must be equal)
751 # - the snapshot format MUST NOT have been defined already (e.g. from
752 # CGI parameter sf)
753 # It's also useless to try any matching unless $refname has a dot,
754 # so we check for that too
755 if (defined $input_params{'action'} &&
756 $input_params{'action'} eq 'snapshot' &&
757 defined $refname && index($refname, '.') != -1 &&
758 $refname eq $input_params{'hash'} &&
759 !defined $input_params{'snapshot_format'}) {
760 # We loop over the known snapshot formats, checking for
761 # extensions. Allowed extensions are both the defined suffix
762 # (which includes the initial dot already) and the snapshot
763 # format key itself, with a prepended dot
764 while (my ($fmt, $opt) = each %known_snapshot_formats) {
765 my $hash = $refname;
766 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
767 next;
769 my $sfx = $1;
770 # a valid suffix was found, so set the snapshot format
771 # and reset the hash parameter
772 $input_params{'snapshot_format'} = $fmt;
773 $input_params{'hash'} = $hash;
774 # we also set the format suffix to the one requested
775 # in the URL: this way a request for e.g. .tgz returns
776 # a .tgz instead of a .tar.gz
777 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
778 last;
782 evaluate_path_info();
784 our $action = $input_params{'action'};
785 if (defined $action) {
786 if (!validate_action($action)) {
787 die_error(400, "Invalid action parameter");
791 # parameters which are pathnames
792 our $project = $input_params{'project'};
793 if (defined $project) {
794 if (!validate_project($project)) {
795 undef $project;
796 die_error(404, "No such project");
800 our $file_name = $input_params{'file_name'};
801 if (defined $file_name) {
802 if (!validate_pathname($file_name)) {
803 die_error(400, "Invalid file parameter");
807 our $file_parent = $input_params{'file_parent'};
808 if (defined $file_parent) {
809 if (!validate_pathname($file_parent)) {
810 die_error(400, "Invalid file parent parameter");
814 # parameters which are refnames
815 our $hash = $input_params{'hash'};
816 if (defined $hash) {
817 if (!validate_refname($hash)) {
818 die_error(400, "Invalid hash parameter");
822 our $hash_parent = $input_params{'hash_parent'};
823 if (defined $hash_parent) {
824 if (!validate_refname($hash_parent)) {
825 die_error(400, "Invalid hash parent parameter");
829 our $hash_base = $input_params{'hash_base'};
830 if (defined $hash_base) {
831 if (!validate_refname($hash_base)) {
832 die_error(400, "Invalid hash base parameter");
836 our @extra_options = @{$input_params{'extra_options'}};
837 # @extra_options is always defined, since it can only be (currently) set from
838 # CGI, and $cgi->param() returns the empty array in array context if the param
839 # is not set
840 foreach my $opt (@extra_options) {
841 if (not exists $allowed_options{$opt}) {
842 die_error(400, "Invalid option parameter");
844 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
845 die_error(400, "Invalid option parameter for this action");
849 our $hash_parent_base = $input_params{'hash_parent_base'};
850 if (defined $hash_parent_base) {
851 if (!validate_refname($hash_parent_base)) {
852 die_error(400, "Invalid hash parent base parameter");
856 # other parameters
857 our $page = $input_params{'page'};
858 if (defined $page) {
859 if ($page =~ m/[^0-9]/) {
860 die_error(400, "Invalid page parameter");
864 our $searchtype = $input_params{'searchtype'};
865 if (defined $searchtype) {
866 if ($searchtype =~ m/[^a-z]/) {
867 die_error(400, "Invalid searchtype parameter");
871 our $search_use_regexp = $input_params{'search_use_regexp'};
873 our $searchtext = $input_params{'searchtext'};
874 our $search_regexp;
875 if (defined $searchtext) {
876 if (length($searchtext) < 2) {
877 die_error(403, "At least two characters are required for search parameter");
879 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
882 # path to the current git repository
883 our $git_dir;
884 $git_dir = "$projectroot/$project" if $project;
886 # list of supported snapshot formats
887 our @snapshot_fmts = gitweb_get_feature('snapshot');
888 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
890 # check that the avatar feature is set to a known provider name,
891 # and for each provider check if the dependencies are satisfied.
892 # if the provider name is invalid or the dependencies are not met,
893 # reset $git_avatar to the empty string.
894 our ($git_avatar) = gitweb_get_feature('avatar');
895 if ($git_avatar eq 'gravatar') {
896 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
897 } elsif ($git_avatar eq 'picon') {
898 # no dependencies
899 } else {
900 $git_avatar = '';
903 # dispatch
904 if (!defined $action) {
905 if (defined $hash) {
906 $action = git_get_type($hash);
907 } elsif (defined $hash_base && defined $file_name) {
908 $action = git_get_type("$hash_base:$file_name");
909 } elsif (defined $project) {
910 $action = 'summary';
911 } else {
912 $action = 'project_list';
915 if (!defined($actions{$action})) {
916 die_error(400, "Unknown action");
918 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
919 !$project) {
920 die_error(400, "Project needed");
922 $actions{$action}->();
923 exit;
925 ## ======================================================================
926 ## action links
928 sub href {
929 my %params = @_;
930 # default is to use -absolute url() i.e. $my_uri
931 my $href = $params{-full} ? $my_url : $my_uri;
933 $params{'project'} = $project unless exists $params{'project'};
935 if ($params{-replay}) {
936 while (my ($name, $symbol) = each %cgi_param_mapping) {
937 if (!exists $params{$name}) {
938 $params{$name} = $input_params{$name};
943 my $use_pathinfo = gitweb_check_feature('pathinfo');
944 if ($use_pathinfo and defined $params{'project'}) {
945 # try to put as many parameters as possible in PATH_INFO:
946 # - project name
947 # - action
948 # - hash_parent or hash_parent_base:/file_parent
949 # - hash or hash_base:/filename
950 # - the snapshot_format as an appropriate suffix
952 # When the script is the root DirectoryIndex for the domain,
953 # $href here would be something like http://gitweb.example.com/
954 # Thus, we strip any trailing / from $href, to spare us double
955 # slashes in the final URL
956 $href =~ s,/$,,;
958 # Then add the project name, if present
959 $href .= "/".esc_url($params{'project'});
960 delete $params{'project'};
962 # since we destructively absorb parameters, we keep this
963 # boolean that remembers if we're handling a snapshot
964 my $is_snapshot = $params{'action'} eq 'snapshot';
966 # Summary just uses the project path URL, any other action is
967 # added to the URL
968 if (defined $params{'action'}) {
969 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
970 delete $params{'action'};
973 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
974 # stripping nonexistent or useless pieces
975 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
976 || $params{'hash_parent'} || $params{'hash'});
977 if (defined $params{'hash_base'}) {
978 if (defined $params{'hash_parent_base'}) {
979 $href .= esc_url($params{'hash_parent_base'});
980 # skip the file_parent if it's the same as the file_name
981 if (defined $params{'file_parent'}) {
982 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
983 delete $params{'file_parent'};
984 } elsif ($params{'file_parent'} !~ /\.\./) {
985 $href .= ":/".esc_url($params{'file_parent'});
986 delete $params{'file_parent'};
989 $href .= "..";
990 delete $params{'hash_parent'};
991 delete $params{'hash_parent_base'};
992 } elsif (defined $params{'hash_parent'}) {
993 $href .= esc_url($params{'hash_parent'}). "..";
994 delete $params{'hash_parent'};
997 $href .= esc_url($params{'hash_base'});
998 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
999 $href .= ":/".esc_url($params{'file_name'});
1000 delete $params{'file_name'};
1002 delete $params{'hash'};
1003 delete $params{'hash_base'};
1004 } elsif (defined $params{'hash'}) {
1005 $href .= esc_url($params{'hash'});
1006 delete $params{'hash'};
1009 # If the action was a snapshot, we can absorb the
1010 # snapshot_format parameter too
1011 if ($is_snapshot) {
1012 my $fmt = $params{'snapshot_format'};
1013 # snapshot_format should always be defined when href()
1014 # is called, but just in case some code forgets, we
1015 # fall back to the default
1016 $fmt ||= $snapshot_fmts[0];
1017 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1018 delete $params{'snapshot_format'};
1022 # now encode the parameters explicitly
1023 my @result = ();
1024 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1025 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1026 if (defined $params{$name}) {
1027 if (ref($params{$name}) eq "ARRAY") {
1028 foreach my $par (@{$params{$name}}) {
1029 push @result, $symbol . "=" . esc_param($par);
1031 } else {
1032 push @result, $symbol . "=" . esc_param($params{$name});
1036 $href .= "?" . join(';', @result) if scalar @result;
1038 return $href;
1042 ## ======================================================================
1043 ## validation, quoting/unquoting and escaping
1045 sub validate_action {
1046 my $input = shift || return undef;
1047 return undef unless exists $actions{$input};
1048 return $input;
1051 sub validate_project {
1052 my $input = shift || return undef;
1053 if (!validate_pathname($input) ||
1054 !(-d "$projectroot/$input") ||
1055 !check_export_ok("$projectroot/$input") ||
1056 ($strict_export && !project_in_list($input))) {
1057 return undef;
1058 } else {
1059 return $input;
1063 sub validate_pathname {
1064 my $input = shift || return undef;
1066 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1067 # at the beginning, at the end, and between slashes.
1068 # also this catches doubled slashes
1069 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1070 return undef;
1072 # no null characters
1073 if ($input =~ m!\0!) {
1074 return undef;
1076 return $input;
1079 sub validate_refname {
1080 my $input = shift || return undef;
1082 # textual hashes are O.K.
1083 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1084 return $input;
1086 # it must be correct pathname
1087 $input = validate_pathname($input)
1088 or return undef;
1089 # restrictions on ref name according to git-check-ref-format
1090 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1091 return undef;
1093 return $input;
1096 # decode sequences of octets in utf8 into Perl's internal form,
1097 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1098 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1099 sub to_utf8 {
1100 my $str = shift;
1101 if (utf8::valid($str)) {
1102 utf8::decode($str);
1103 return $str;
1104 } else {
1105 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1109 # quote unsafe chars, but keep the slash, even when it's not
1110 # correct, but quoted slashes look too horrible in bookmarks
1111 sub esc_param {
1112 my $str = shift;
1113 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1114 $str =~ s/ /\+/g;
1115 return $str;
1118 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1119 sub esc_url {
1120 my $str = shift;
1121 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1122 $str =~ s/\+/%2B/g;
1123 $str =~ s/ /\+/g;
1124 return $str;
1127 # replace invalid utf8 character with SUBSTITUTION sequence
1128 sub esc_html {
1129 my $str = shift;
1130 my %opts = @_;
1132 $str = to_utf8($str);
1133 $str = $cgi->escapeHTML($str);
1134 if ($opts{'-nbsp'}) {
1135 $str =~ s/ /&nbsp;/g;
1137 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1138 return $str;
1141 # quote control characters and escape filename to HTML
1142 sub esc_path {
1143 my $str = shift;
1144 my %opts = @_;
1146 $str = to_utf8($str);
1147 $str = $cgi->escapeHTML($str);
1148 if ($opts{'-nbsp'}) {
1149 $str =~ s/ /&nbsp;/g;
1151 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1152 return $str;
1155 # Make control characters "printable", using character escape codes (CEC)
1156 sub quot_cec {
1157 my $cntrl = shift;
1158 my %opts = @_;
1159 my %es = ( # character escape codes, aka escape sequences
1160 "\t" => '\t', # tab (HT)
1161 "\n" => '\n', # line feed (LF)
1162 "\r" => '\r', # carrige return (CR)
1163 "\f" => '\f', # form feed (FF)
1164 "\b" => '\b', # backspace (BS)
1165 "\a" => '\a', # alarm (bell) (BEL)
1166 "\e" => '\e', # escape (ESC)
1167 "\013" => '\v', # vertical tab (VT)
1168 "\000" => '\0', # nul character (NUL)
1170 my $chr = ( (exists $es{$cntrl})
1171 ? $es{$cntrl}
1172 : sprintf('\%2x', ord($cntrl)) );
1173 if ($opts{-nohtml}) {
1174 return $chr;
1175 } else {
1176 return "<span class=\"cntrl\">$chr</span>";
1180 # Alternatively use unicode control pictures codepoints,
1181 # Unicode "printable representation" (PR)
1182 sub quot_upr {
1183 my $cntrl = shift;
1184 my %opts = @_;
1186 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1187 if ($opts{-nohtml}) {
1188 return $chr;
1189 } else {
1190 return "<span class=\"cntrl\">$chr</span>";
1194 # git may return quoted and escaped filenames
1195 sub unquote {
1196 my $str = shift;
1198 sub unq {
1199 my $seq = shift;
1200 my %es = ( # character escape codes, aka escape sequences
1201 't' => "\t", # tab (HT, TAB)
1202 'n' => "\n", # newline (NL)
1203 'r' => "\r", # return (CR)
1204 'f' => "\f", # form feed (FF)
1205 'b' => "\b", # backspace (BS)
1206 'a' => "\a", # alarm (bell) (BEL)
1207 'e' => "\e", # escape (ESC)
1208 'v' => "\013", # vertical tab (VT)
1211 if ($seq =~ m/^[0-7]{1,3}$/) {
1212 # octal char sequence
1213 return chr(oct($seq));
1214 } elsif (exists $es{$seq}) {
1215 # C escape sequence, aka character escape code
1216 return $es{$seq};
1218 # quoted ordinary character
1219 return $seq;
1222 if ($str =~ m/^"(.*)"$/) {
1223 # needs unquoting
1224 $str = $1;
1225 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1227 return $str;
1230 # escape tabs (convert tabs to spaces)
1231 sub untabify {
1232 my $line = shift;
1234 while ((my $pos = index($line, "\t")) != -1) {
1235 if (my $count = (8 - ($pos % 8))) {
1236 my $spaces = ' ' x $count;
1237 $line =~ s/\t/$spaces/;
1241 return $line;
1244 sub project_in_list {
1245 my $project = shift;
1246 my @list = git_get_projects_list();
1247 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1250 ## ----------------------------------------------------------------------
1251 ## HTML aware string manipulation
1253 # Try to chop given string on a word boundary between position
1254 # $len and $len+$add_len. If there is no word boundary there,
1255 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1256 # (marking chopped part) would be longer than given string.
1257 sub chop_str {
1258 my $str = shift;
1259 my $len = shift;
1260 my $add_len = shift || 10;
1261 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1263 # Make sure perl knows it is utf8 encoded so we don't
1264 # cut in the middle of a utf8 multibyte char.
1265 $str = to_utf8($str);
1267 # allow only $len chars, but don't cut a word if it would fit in $add_len
1268 # if it doesn't fit, cut it if it's still longer than the dots we would add
1269 # remove chopped character entities entirely
1271 # when chopping in the middle, distribute $len into left and right part
1272 # return early if chopping wouldn't make string shorter
1273 if ($where eq 'center') {
1274 return $str if ($len + 5 >= length($str)); # filler is length 5
1275 $len = int($len/2);
1276 } else {
1277 return $str if ($len + 4 >= length($str)); # filler is length 4
1280 # regexps: ending and beginning with word part up to $add_len
1281 my $endre = qr/.{$len}\w{0,$add_len}/;
1282 my $begre = qr/\w{0,$add_len}.{$len}/;
1284 if ($where eq 'left') {
1285 $str =~ m/^(.*?)($begre)$/;
1286 my ($lead, $body) = ($1, $2);
1287 if (length($lead) > 4) {
1288 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1289 $lead = " ...";
1291 return "$lead$body";
1293 } elsif ($where eq 'center') {
1294 $str =~ m/^($endre)(.*)$/;
1295 my ($left, $str) = ($1, $2);
1296 $str =~ m/^(.*?)($begre)$/;
1297 my ($mid, $right) = ($1, $2);
1298 if (length($mid) > 5) {
1299 $left =~ s/&[^;]*$//;
1300 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1301 $mid = " ... ";
1303 return "$left$mid$right";
1305 } else {
1306 $str =~ m/^($endre)(.*)$/;
1307 my $body = $1;
1308 my $tail = $2;
1309 if (length($tail) > 4) {
1310 $body =~ s/&[^;]*$//;
1311 $tail = "... ";
1313 return "$body$tail";
1317 # takes the same arguments as chop_str, but also wraps a <span> around the
1318 # result with a title attribute if it does get chopped. Additionally, the
1319 # string is HTML-escaped.
1320 sub chop_and_escape_str {
1321 my ($str) = @_;
1323 my $chopped = chop_str(@_);
1324 if ($chopped eq $str) {
1325 return esc_html($chopped);
1326 } else {
1327 $str =~ s/[[:cntrl:]]/?/g;
1328 return $cgi->span({-title=>$str}, esc_html($chopped));
1332 ## ----------------------------------------------------------------------
1333 ## functions returning short strings
1335 # CSS class for given age value (in seconds)
1336 sub age_class {
1337 my $age = shift;
1339 if (!defined $age) {
1340 return "noage";
1341 } elsif ($age < 60*60*2) {
1342 return "age0";
1343 } elsif ($age < 60*60*24*2) {
1344 return "age1";
1345 } else {
1346 return "age2";
1350 # convert age in seconds to "nn units ago" string
1351 sub age_string {
1352 my $age = shift;
1353 my $age_str;
1355 if ($age > 60*60*24*365*2) {
1356 $age_str = (int $age/60/60/24/365);
1357 $age_str .= " years ago";
1358 } elsif ($age > 60*60*24*(365/12)*2) {
1359 $age_str = int $age/60/60/24/(365/12);
1360 $age_str .= " months ago";
1361 } elsif ($age > 60*60*24*7*2) {
1362 $age_str = int $age/60/60/24/7;
1363 $age_str .= " weeks ago";
1364 } elsif ($age > 60*60*24*2) {
1365 $age_str = int $age/60/60/24;
1366 $age_str .= " days ago";
1367 } elsif ($age > 60*60*2) {
1368 $age_str = int $age/60/60;
1369 $age_str .= " hours ago";
1370 } elsif ($age > 60*2) {
1371 $age_str = int $age/60;
1372 $age_str .= " min ago";
1373 } elsif ($age > 2) {
1374 $age_str = int $age;
1375 $age_str .= " sec ago";
1376 } else {
1377 $age_str .= " right now";
1379 return $age_str;
1382 use constant {
1383 S_IFINVALID => 0030000,
1384 S_IFGITLINK => 0160000,
1387 # submodule/subproject, a commit object reference
1388 sub S_ISGITLINK {
1389 my $mode = shift;
1391 return (($mode & S_IFMT) == S_IFGITLINK)
1394 # convert file mode in octal to symbolic file mode string
1395 sub mode_str {
1396 my $mode = oct shift;
1398 if (S_ISGITLINK($mode)) {
1399 return 'm---------';
1400 } elsif (S_ISDIR($mode & S_IFMT)) {
1401 return 'drwxr-xr-x';
1402 } elsif (S_ISLNK($mode)) {
1403 return 'lrwxrwxrwx';
1404 } elsif (S_ISREG($mode)) {
1405 # git cares only about the executable bit
1406 if ($mode & S_IXUSR) {
1407 return '-rwxr-xr-x';
1408 } else {
1409 return '-rw-r--r--';
1411 } else {
1412 return '----------';
1416 # convert file mode in octal to file type string
1417 sub file_type {
1418 my $mode = shift;
1420 if ($mode !~ m/^[0-7]+$/) {
1421 return $mode;
1422 } else {
1423 $mode = oct $mode;
1426 if (S_ISGITLINK($mode)) {
1427 return "submodule";
1428 } elsif (S_ISDIR($mode & S_IFMT)) {
1429 return "directory";
1430 } elsif (S_ISLNK($mode)) {
1431 return "symlink";
1432 } elsif (S_ISREG($mode)) {
1433 return "file";
1434 } else {
1435 return "unknown";
1439 # convert file mode in octal to file type description string
1440 sub file_type_long {
1441 my $mode = shift;
1443 if ($mode !~ m/^[0-7]+$/) {
1444 return $mode;
1445 } else {
1446 $mode = oct $mode;
1449 if (S_ISGITLINK($mode)) {
1450 return "submodule";
1451 } elsif (S_ISDIR($mode & S_IFMT)) {
1452 return "directory";
1453 } elsif (S_ISLNK($mode)) {
1454 return "symlink";
1455 } elsif (S_ISREG($mode)) {
1456 if ($mode & S_IXUSR) {
1457 return "executable";
1458 } else {
1459 return "file";
1461 } else {
1462 return "unknown";
1467 ## ----------------------------------------------------------------------
1468 ## functions returning short HTML fragments, or transforming HTML fragments
1469 ## which don't belong to other sections
1471 # format line of commit message.
1472 sub format_log_line_html {
1473 my $line = shift;
1475 $line = esc_html($line, -nbsp=>1);
1476 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1477 $cgi->a({-href => href(action=>"object", hash=>$1),
1478 -class => "text"}, $1);
1479 }eg;
1481 return $line;
1484 # format marker of refs pointing to given object
1486 # the destination action is chosen based on object type and current context:
1487 # - for annotated tags, we choose the tag view unless it's the current view
1488 # already, in which case we go to shortlog view
1489 # - for other refs, we keep the current view if we're in history, shortlog or
1490 # log view, and select shortlog otherwise
1491 sub format_ref_marker {
1492 my ($refs, $id) = @_;
1493 my $markers = '';
1495 if (defined $refs->{$id}) {
1496 foreach my $ref (@{$refs->{$id}}) {
1497 # this code exploits the fact that non-lightweight tags are the
1498 # only indirect objects, and that they are the only objects for which
1499 # we want to use tag instead of shortlog as action
1500 my ($type, $name) = qw();
1501 my $indirect = ($ref =~ s/\^\{\}$//);
1502 # e.g. tags/v2.6.11 or heads/next
1503 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1504 $type = $1;
1505 $name = $2;
1506 } else {
1507 $type = "ref";
1508 $name = $ref;
1511 my $class = $type;
1512 $class .= " indirect" if $indirect;
1514 my $dest_action = "shortlog";
1516 if ($indirect) {
1517 $dest_action = "tag" unless $action eq "tag";
1518 } elsif ($action =~ /^(history|(short)?log)$/) {
1519 $dest_action = $action;
1522 my $dest = "";
1523 $dest .= "refs/" unless $ref =~ m!^refs/!;
1524 $dest .= $ref;
1526 my $link = $cgi->a({
1527 -href => href(
1528 action=>$dest_action,
1529 hash=>$dest
1530 )}, $name);
1532 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1533 $link . "</span>";
1537 if ($markers) {
1538 return ' <span class="refs">'. $markers . '</span>';
1539 } else {
1540 return "";
1544 # format, perhaps shortened and with markers, title line
1545 sub format_subject_html {
1546 my ($long, $short, $href, $extra) = @_;
1547 $extra = '' unless defined($extra);
1549 if (length($short) < length($long)) {
1550 $long =~ s/[[:cntrl:]]/?/g;
1551 return $cgi->a({-href => $href, -class => "list subject",
1552 -title => to_utf8($long)},
1553 esc_html($short)) . $extra;
1554 } else {
1555 return $cgi->a({-href => $href, -class => "list subject"},
1556 esc_html($long)) . $extra;
1560 # Rather than recomputing the url for an email multiple times, we cache it
1561 # after the first hit. This gives a visible benefit in views where the avatar
1562 # for the same email is used repeatedly (e.g. shortlog).
1563 # The cache is shared by all avatar engines (currently gravatar only), which
1564 # are free to use it as preferred. Since only one avatar engine is used for any
1565 # given page, there's no risk for cache conflicts.
1566 our %avatar_cache = ();
1568 # Compute the picon url for a given email, by using the picon search service over at
1569 # http://www.cs.indiana.edu/picons/search.html
1570 sub picon_url {
1571 my $email = lc shift;
1572 if (!$avatar_cache{$email}) {
1573 my ($user, $domain) = split('@', $email);
1574 $avatar_cache{$email} =
1575 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1576 "$domain/$user/" .
1577 "users+domains+unknown/up/single";
1579 return $avatar_cache{$email};
1582 # Compute the gravatar url for a given email, if it's not in the cache already.
1583 # Gravatar stores only the part of the URL before the size, since that's the
1584 # one computationally more expensive. This also allows reuse of the cache for
1585 # different sizes (for this particular engine).
1586 sub gravatar_url {
1587 my $email = lc shift;
1588 my $size = shift;
1589 $avatar_cache{$email} ||=
1590 "http://www.gravatar.com/avatar/" .
1591 Digest::MD5::md5_hex($email) . "?s=";
1592 return $avatar_cache{$email} . $size;
1595 # Insert an avatar for the given $email at the given $size if the feature
1596 # is enabled.
1597 sub git_get_avatar {
1598 my ($email, %opts) = @_;
1599 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
1600 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
1601 $opts{-size} ||= 'default';
1602 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1603 my $url = "";
1604 if ($git_avatar eq 'gravatar') {
1605 $url = gravatar_url($email, $size);
1606 } elsif ($git_avatar eq 'picon') {
1607 $url = picon_url($email);
1609 # Other providers can be added by extending the if chain, defining $url
1610 # as needed. If no variant puts something in $url, we assume avatars
1611 # are completely disabled/unavailable.
1612 if ($url) {
1613 return $pre_white .
1614 "<img width=\"$size\" " .
1615 "class=\"avatar\" " .
1616 "src=\"$url\" " .
1617 "alt=\"\" " .
1618 "/>" . $post_white;
1619 } else {
1620 return "";
1624 sub format_search_author {
1625 my ($author, $searchtype, $displaytext) = @_;
1626 my $have_search = gitweb_check_feature('search');
1628 if ($have_search) {
1629 my $performed = "";
1630 if ($searchtype eq 'author') {
1631 $performed = "authored";
1632 } elsif ($searchtype eq 'committer') {
1633 $performed = "committed";
1636 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1637 searchtext=>$author,
1638 searchtype=>$searchtype), class=>"list",
1639 title=>"Search for commits $performed by $author"},
1640 $displaytext);
1642 } else {
1643 return $displaytext;
1647 # format the author name of the given commit with the given tag
1648 # the author name is chopped and escaped according to the other
1649 # optional parameters (see chop_str).
1650 sub format_author_html {
1651 my $tag = shift;
1652 my $co = shift;
1653 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1654 return "<$tag class=\"author\">" .
1655 format_search_author($co->{'author_name'}, "author",
1656 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1657 $author) .
1658 "</$tag>";
1661 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1662 sub format_git_diff_header_line {
1663 my $line = shift;
1664 my $diffinfo = shift;
1665 my ($from, $to) = @_;
1667 if ($diffinfo->{'nparents'}) {
1668 # combined diff
1669 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1670 if ($to->{'href'}) {
1671 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1672 esc_path($to->{'file'}));
1673 } else { # file was deleted (no href)
1674 $line .= esc_path($to->{'file'});
1676 } else {
1677 # "ordinary" diff
1678 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1679 if ($from->{'href'}) {
1680 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1681 'a/' . esc_path($from->{'file'}));
1682 } else { # file was added (no href)
1683 $line .= 'a/' . esc_path($from->{'file'});
1685 $line .= ' ';
1686 if ($to->{'href'}) {
1687 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1688 'b/' . esc_path($to->{'file'}));
1689 } else { # file was deleted
1690 $line .= 'b/' . esc_path($to->{'file'});
1694 return "<div class=\"diff header\">$line</div>\n";
1697 # format extended diff header line, before patch itself
1698 sub format_extended_diff_header_line {
1699 my $line = shift;
1700 my $diffinfo = shift;
1701 my ($from, $to) = @_;
1703 # match <path>
1704 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1705 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1706 esc_path($from->{'file'}));
1708 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1709 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1710 esc_path($to->{'file'}));
1712 # match single <mode>
1713 if ($line =~ m/\s(\d{6})$/) {
1714 $line .= '<span class="info"> (' .
1715 file_type_long($1) .
1716 ')</span>';
1718 # match <hash>
1719 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1720 # can match only for combined diff
1721 $line = 'index ';
1722 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1723 if ($from->{'href'}[$i]) {
1724 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1725 -class=>"hash"},
1726 substr($diffinfo->{'from_id'}[$i],0,7));
1727 } else {
1728 $line .= '0' x 7;
1730 # separator
1731 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1733 $line .= '..';
1734 if ($to->{'href'}) {
1735 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1736 substr($diffinfo->{'to_id'},0,7));
1737 } else {
1738 $line .= '0' x 7;
1741 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1742 # can match only for ordinary diff
1743 my ($from_link, $to_link);
1744 if ($from->{'href'}) {
1745 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1746 substr($diffinfo->{'from_id'},0,7));
1747 } else {
1748 $from_link = '0' x 7;
1750 if ($to->{'href'}) {
1751 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1752 substr($diffinfo->{'to_id'},0,7));
1753 } else {
1754 $to_link = '0' x 7;
1756 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1757 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1760 return $line . "<br/>\n";
1763 # format from-file/to-file diff header
1764 sub format_diff_from_to_header {
1765 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1766 my $line;
1767 my $result = '';
1769 $line = $from_line;
1770 #assert($line =~ m/^---/) if DEBUG;
1771 # no extra formatting for "^--- /dev/null"
1772 if (! $diffinfo->{'nparents'}) {
1773 # ordinary (single parent) diff
1774 if ($line =~ m!^--- "?a/!) {
1775 if ($from->{'href'}) {
1776 $line = '--- a/' .
1777 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1778 esc_path($from->{'file'}));
1779 } else {
1780 $line = '--- a/' .
1781 esc_path($from->{'file'});
1784 $result .= qq!<div class="diff from_file">$line</div>\n!;
1786 } else {
1787 # combined diff (merge commit)
1788 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1789 if ($from->{'href'}[$i]) {
1790 $line = '--- ' .
1791 $cgi->a({-href=>href(action=>"blobdiff",
1792 hash_parent=>$diffinfo->{'from_id'}[$i],
1793 hash_parent_base=>$parents[$i],
1794 file_parent=>$from->{'file'}[$i],
1795 hash=>$diffinfo->{'to_id'},
1796 hash_base=>$hash,
1797 file_name=>$to->{'file'}),
1798 -class=>"path",
1799 -title=>"diff" . ($i+1)},
1800 $i+1) .
1801 '/' .
1802 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1803 esc_path($from->{'file'}[$i]));
1804 } else {
1805 $line = '--- /dev/null';
1807 $result .= qq!<div class="diff from_file">$line</div>\n!;
1811 $line = $to_line;
1812 #assert($line =~ m/^\+\+\+/) if DEBUG;
1813 # no extra formatting for "^+++ /dev/null"
1814 if ($line =~ m!^\+\+\+ "?b/!) {
1815 if ($to->{'href'}) {
1816 $line = '+++ b/' .
1817 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1818 esc_path($to->{'file'}));
1819 } else {
1820 $line = '+++ b/' .
1821 esc_path($to->{'file'});
1824 $result .= qq!<div class="diff to_file">$line</div>\n!;
1826 return $result;
1829 # create note for patch simplified by combined diff
1830 sub format_diff_cc_simplified {
1831 my ($diffinfo, @parents) = @_;
1832 my $result = '';
1834 $result .= "<div class=\"diff header\">" .
1835 "diff --cc ";
1836 if (!is_deleted($diffinfo)) {
1837 $result .= $cgi->a({-href => href(action=>"blob",
1838 hash_base=>$hash,
1839 hash=>$diffinfo->{'to_id'},
1840 file_name=>$diffinfo->{'to_file'}),
1841 -class => "path"},
1842 esc_path($diffinfo->{'to_file'}));
1843 } else {
1844 $result .= esc_path($diffinfo->{'to_file'});
1846 $result .= "</div>\n" . # class="diff header"
1847 "<div class=\"diff nodifferences\">" .
1848 "Simple merge" .
1849 "</div>\n"; # class="diff nodifferences"
1851 return $result;
1854 # format patch (diff) line (not to be used for diff headers)
1855 sub format_diff_line {
1856 my $line = shift;
1857 my ($from, $to) = @_;
1858 my $diff_class = "";
1860 chomp $line;
1862 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1863 # combined diff
1864 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1865 if ($line =~ m/^\@{3}/) {
1866 $diff_class = " chunk_header";
1867 } elsif ($line =~ m/^\\/) {
1868 $diff_class = " incomplete";
1869 } elsif ($prefix =~ tr/+/+/) {
1870 $diff_class = " add";
1871 } elsif ($prefix =~ tr/-/-/) {
1872 $diff_class = " rem";
1874 } else {
1875 # assume ordinary diff
1876 my $char = substr($line, 0, 1);
1877 if ($char eq '+') {
1878 $diff_class = " add";
1879 } elsif ($char eq '-') {
1880 $diff_class = " rem";
1881 } elsif ($char eq '@') {
1882 $diff_class = " chunk_header";
1883 } elsif ($char eq "\\") {
1884 $diff_class = " incomplete";
1887 $line = untabify($line);
1888 if ($from && $to && $line =~ m/^\@{2} /) {
1889 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1890 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1892 $from_lines = 0 unless defined $from_lines;
1893 $to_lines = 0 unless defined $to_lines;
1895 if ($from->{'href'}) {
1896 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1897 -class=>"list"}, $from_text);
1899 if ($to->{'href'}) {
1900 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1901 -class=>"list"}, $to_text);
1903 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1904 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1905 return "<div class=\"diff$diff_class\">$line</div>\n";
1906 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1907 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1908 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1910 @from_text = split(' ', $ranges);
1911 for (my $i = 0; $i < @from_text; ++$i) {
1912 ($from_start[$i], $from_nlines[$i]) =
1913 (split(',', substr($from_text[$i], 1)), 0);
1916 $to_text = pop @from_text;
1917 $to_start = pop @from_start;
1918 $to_nlines = pop @from_nlines;
1920 $line = "<span class=\"chunk_info\">$prefix ";
1921 for (my $i = 0; $i < @from_text; ++$i) {
1922 if ($from->{'href'}[$i]) {
1923 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1924 -class=>"list"}, $from_text[$i]);
1925 } else {
1926 $line .= $from_text[$i];
1928 $line .= " ";
1930 if ($to->{'href'}) {
1931 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1932 -class=>"list"}, $to_text);
1933 } else {
1934 $line .= $to_text;
1936 $line .= " $prefix</span>" .
1937 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1938 return "<div class=\"diff$diff_class\">$line</div>\n";
1940 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1943 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1944 # linked. Pass the hash of the tree/commit to snapshot.
1945 sub format_snapshot_links {
1946 my ($hash) = @_;
1947 my $num_fmts = @snapshot_fmts;
1948 if ($num_fmts > 1) {
1949 # A parenthesized list of links bearing format names.
1950 # e.g. "snapshot (_tar.gz_ _zip_)"
1951 return "snapshot (" . join(' ', map
1952 $cgi->a({
1953 -href => href(
1954 action=>"snapshot",
1955 hash=>$hash,
1956 snapshot_format=>$_
1958 }, $known_snapshot_formats{$_}{'display'})
1959 , @snapshot_fmts) . ")";
1960 } elsif ($num_fmts == 1) {
1961 # A single "snapshot" link whose tooltip bears the format name.
1962 # i.e. "_snapshot_"
1963 my ($fmt) = @snapshot_fmts;
1964 return
1965 $cgi->a({
1966 -href => href(
1967 action=>"snapshot",
1968 hash=>$hash,
1969 snapshot_format=>$fmt
1971 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1972 }, "snapshot");
1973 } else { # $num_fmts == 0
1974 return undef;
1978 ## ......................................................................
1979 ## functions returning values to be passed, perhaps after some
1980 ## transformation, to other functions; e.g. returning arguments to href()
1982 # returns hash to be passed to href to generate gitweb URL
1983 # in -title key it returns description of link
1984 sub get_feed_info {
1985 my $format = shift || 'Atom';
1986 my %res = (action => lc($format));
1988 # feed links are possible only for project views
1989 return unless (defined $project);
1990 # some views should link to OPML, or to generic project feed,
1991 # or don't have specific feed yet (so they should use generic)
1992 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1994 my $branch;
1995 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1996 # from tag links; this also makes possible to detect branch links
1997 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1998 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1999 $branch = $1;
2001 # find log type for feed description (title)
2002 my $type = 'log';
2003 if (defined $file_name) {
2004 $type = "history of $file_name";
2005 $type .= "/" if ($action eq 'tree');
2006 $type .= " on '$branch'" if (defined $branch);
2007 } else {
2008 $type = "log of $branch" if (defined $branch);
2011 $res{-title} = $type;
2012 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2013 $res{'file_name'} = $file_name;
2015 return %res;
2018 ## ----------------------------------------------------------------------
2019 ## git utility subroutines, invoking git commands
2021 # returns path to the core git executable and the --git-dir parameter as list
2022 sub git_cmd {
2023 return $GIT, '--git-dir='.$git_dir;
2026 # quote the given arguments for passing them to the shell
2027 # quote_command("command", "arg 1", "arg with ' and ! characters")
2028 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2029 # Try to avoid using this function wherever possible.
2030 sub quote_command {
2031 return join(' ',
2032 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2035 # get HEAD ref of given project as hash
2036 sub git_get_head_hash {
2037 my $project = shift;
2038 my $o_git_dir = $git_dir;
2039 my $retval = undef;
2040 $git_dir = "$projectroot/$project";
2041 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
2042 my $head = <$fd>;
2043 close $fd;
2044 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
2045 $retval = $1;
2048 if (defined $o_git_dir) {
2049 $git_dir = $o_git_dir;
2051 return $retval;
2054 # get type of given object
2055 sub git_get_type {
2056 my $hash = shift;
2058 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2059 my $type = <$fd>;
2060 close $fd or return;
2061 chomp $type;
2062 return $type;
2065 # repository configuration
2066 our $config_file = '';
2067 our %config;
2069 # store multiple values for single key as anonymous array reference
2070 # single values stored directly in the hash, not as [ <value> ]
2071 sub hash_set_multi {
2072 my ($hash, $key, $value) = @_;
2074 if (!exists $hash->{$key}) {
2075 $hash->{$key} = $value;
2076 } elsif (!ref $hash->{$key}) {
2077 $hash->{$key} = [ $hash->{$key}, $value ];
2078 } else {
2079 push @{$hash->{$key}}, $value;
2083 # return hash of git project configuration
2084 # optionally limited to some section, e.g. 'gitweb'
2085 sub git_parse_project_config {
2086 my $section_regexp = shift;
2087 my %config;
2089 local $/ = "\0";
2091 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2092 or return;
2094 while (my $keyval = <$fh>) {
2095 chomp $keyval;
2096 my ($key, $value) = split(/\n/, $keyval, 2);
2098 hash_set_multi(\%config, $key, $value)
2099 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2101 close $fh;
2103 return %config;
2106 # convert config value to boolean: 'true' or 'false'
2107 # no value, number > 0, 'true' and 'yes' values are true
2108 # rest of values are treated as false (never as error)
2109 sub config_to_bool {
2110 my $val = shift;
2112 return 1 if !defined $val; # section.key
2114 # strip leading and trailing whitespace
2115 $val =~ s/^\s+//;
2116 $val =~ s/\s+$//;
2118 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2119 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2122 # convert config value to simple decimal number
2123 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2124 # to be multiplied by 1024, 1048576, or 1073741824
2125 sub config_to_int {
2126 my $val = shift;
2128 # strip leading and trailing whitespace
2129 $val =~ s/^\s+//;
2130 $val =~ s/\s+$//;
2132 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2133 $unit = lc($unit);
2134 # unknown unit is treated as 1
2135 return $num * ($unit eq 'g' ? 1073741824 :
2136 $unit eq 'm' ? 1048576 :
2137 $unit eq 'k' ? 1024 : 1);
2139 return $val;
2142 # convert config value to array reference, if needed
2143 sub config_to_multi {
2144 my $val = shift;
2146 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2149 sub git_get_project_config {
2150 my ($key, $type) = @_;
2152 # key sanity check
2153 return unless ($key);
2154 $key =~ s/^gitweb\.//;
2155 return if ($key =~ m/\W/);
2157 # type sanity check
2158 if (defined $type) {
2159 $type =~ s/^--//;
2160 $type = undef
2161 unless ($type eq 'bool' || $type eq 'int');
2164 # get config
2165 if (!defined $config_file ||
2166 $config_file ne "$git_dir/config") {
2167 %config = git_parse_project_config('gitweb');
2168 $config_file = "$git_dir/config";
2171 # check if config variable (key) exists
2172 return unless exists $config{"gitweb.$key"};
2174 # ensure given type
2175 if (!defined $type) {
2176 return $config{"gitweb.$key"};
2177 } elsif ($type eq 'bool') {
2178 # backward compatibility: 'git config --bool' returns true/false
2179 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2180 } elsif ($type eq 'int') {
2181 return config_to_int($config{"gitweb.$key"});
2183 return $config{"gitweb.$key"};
2186 # get hash of given path at given ref
2187 sub git_get_hash_by_path {
2188 my $base = shift;
2189 my $path = shift || return undef;
2190 my $type = shift;
2192 $path =~ s,/+$,,;
2194 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2195 or die_error(500, "Open git-ls-tree failed");
2196 my $line = <$fd>;
2197 close $fd or return undef;
2199 if (!defined $line) {
2200 # there is no tree or hash given by $path at $base
2201 return undef;
2204 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2205 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2206 if (defined $type && $type ne $2) {
2207 # type doesn't match
2208 return undef;
2210 return $3;
2213 # get path of entry with given hash at given tree-ish (ref)
2214 # used to get 'from' filename for combined diff (merge commit) for renames
2215 sub git_get_path_by_hash {
2216 my $base = shift || return;
2217 my $hash = shift || return;
2219 local $/ = "\0";
2221 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2222 or return undef;
2223 while (my $line = <$fd>) {
2224 chomp $line;
2226 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2227 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2228 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2229 close $fd;
2230 return $1;
2233 close $fd;
2234 return undef;
2237 ## ......................................................................
2238 ## git utility functions, directly accessing git repository
2240 sub git_get_project_description {
2241 my $path = shift;
2243 $git_dir = "$projectroot/$path";
2244 open my $fd, '<', "$git_dir/description"
2245 or return git_get_project_config('description');
2246 my $descr = <$fd>;
2247 close $fd;
2248 if (defined $descr) {
2249 chomp $descr;
2251 return $descr;
2254 sub git_get_project_ctags {
2255 my $path = shift;
2256 my $ctags = {};
2258 $git_dir = "$projectroot/$path";
2259 opendir my $dh, "$git_dir/ctags"
2260 or return $ctags;
2261 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2262 open my $ct, '<', $_ or next;
2263 my $val = <$ct>;
2264 chomp $val;
2265 close $ct;
2266 my $ctag = $_; $ctag =~ s#.*/##;
2267 $ctags->{$ctag} = $val;
2269 closedir $dh;
2270 $ctags;
2273 sub git_populate_project_tagcloud {
2274 my $ctags = shift;
2276 # First, merge different-cased tags; tags vote on casing
2277 my %ctags_lc;
2278 foreach (keys %$ctags) {
2279 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2280 if (not $ctags_lc{lc $_}->{topcount}
2281 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2282 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2283 $ctags_lc{lc $_}->{topname} = $_;
2287 my $cloud;
2288 if (eval { require HTML::TagCloud; 1; }) {
2289 $cloud = HTML::TagCloud->new;
2290 foreach (sort keys %ctags_lc) {
2291 # Pad the title with spaces so that the cloud looks
2292 # less crammed.
2293 my $title = $ctags_lc{$_}->{topname};
2294 $title =~ s/ /&nbsp;/g;
2295 $title =~ s/^/&nbsp;/g;
2296 $title =~ s/$/&nbsp;/g;
2297 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2299 } else {
2300 $cloud = \%ctags_lc;
2302 $cloud;
2305 sub git_show_project_tagcloud {
2306 my ($cloud, $count) = @_;
2307 print STDERR ref($cloud)."..\n";
2308 if (ref $cloud eq 'HTML::TagCloud') {
2309 return $cloud->html_and_css($count);
2310 } else {
2311 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2312 return '<p align="center">' . join (', ', map {
2313 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2314 } splice(@tags, 0, $count)) . '</p>';
2318 sub git_get_project_url_list {
2319 my $path = shift;
2321 $git_dir = "$projectroot/$path";
2322 open my $fd, '<', "$git_dir/cloneurl"
2323 or return wantarray ?
2324 @{ config_to_multi(git_get_project_config('url')) } :
2325 config_to_multi(git_get_project_config('url'));
2326 my @git_project_url_list = map { chomp; $_ } <$fd>;
2327 close $fd;
2329 return wantarray ? @git_project_url_list : \@git_project_url_list;
2332 sub git_get_projects_list {
2333 my ($filter) = @_;
2334 my @list;
2336 $filter ||= '';
2337 $filter =~ s/\.git$//;
2339 my $check_forks = gitweb_check_feature('forks');
2341 if (-d $projects_list) {
2342 # search in directory
2343 my $dir = $projects_list . ($filter ? "/$filter" : '');
2344 # remove the trailing "/"
2345 $dir =~ s!/+$!!;
2346 my $pfxlen = length("$dir");
2347 my $pfxdepth = ($dir =~ tr!/!!);
2349 File::Find::find({
2350 follow_fast => 1, # follow symbolic links
2351 follow_skip => 2, # ignore duplicates
2352 dangling_symlinks => 0, # ignore dangling symlinks, silently
2353 wanted => sub {
2354 # skip project-list toplevel, if we get it.
2355 return if (m!^[/.]$!);
2356 # only directories can be git repositories
2357 return unless (-d $_);
2358 # don't traverse too deep (Find is super slow on os x)
2359 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2360 $File::Find::prune = 1;
2361 return;
2364 my $subdir = substr($File::Find::name, $pfxlen + 1);
2365 # we check related file in $projectroot
2366 my $path = ($filter ? "$filter/" : '') . $subdir;
2367 if (check_export_ok("$projectroot/$path")) {
2368 push @list, { path => $path };
2369 $File::Find::prune = 1;
2372 }, "$dir");
2374 } elsif (-f $projects_list) {
2375 # read from file(url-encoded):
2376 # 'git%2Fgit.git Linus+Torvalds'
2377 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2378 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2379 my %paths;
2380 open my $fd, '<', $projects_list or return;
2381 PROJECT:
2382 while (my $line = <$fd>) {
2383 chomp $line;
2384 my ($path, $owner) = split ' ', $line;
2385 $path = unescape($path);
2386 $owner = unescape($owner);
2387 if (!defined $path) {
2388 next;
2390 if ($filter ne '') {
2391 # looking for forks;
2392 my $pfx = substr($path, 0, length($filter));
2393 if ($pfx ne $filter) {
2394 next PROJECT;
2396 my $sfx = substr($path, length($filter));
2397 if ($sfx !~ /^\/.*\.git$/) {
2398 next PROJECT;
2400 } elsif ($check_forks) {
2401 PATH:
2402 foreach my $filter (keys %paths) {
2403 # looking for forks;
2404 my $pfx = substr($path, 0, length($filter));
2405 if ($pfx ne $filter) {
2406 next PATH;
2408 my $sfx = substr($path, length($filter));
2409 if ($sfx !~ /^\/.*\.git$/) {
2410 next PATH;
2412 # is a fork, don't include it in
2413 # the list
2414 next PROJECT;
2417 if (check_export_ok("$projectroot/$path")) {
2418 my $pr = {
2419 path => $path,
2420 owner => to_utf8($owner),
2422 push @list, $pr;
2423 (my $forks_path = $path) =~ s/\.git$//;
2424 $paths{$forks_path}++;
2427 close $fd;
2429 return @list;
2432 our $gitweb_project_owner = undef;
2433 sub git_get_project_list_from_file {
2435 return if (defined $gitweb_project_owner);
2437 $gitweb_project_owner = {};
2438 # read from file (url-encoded):
2439 # 'git%2Fgit.git Linus+Torvalds'
2440 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2441 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2442 if (-f $projects_list) {
2443 open(my $fd, '<', $projects_list);
2444 while (my $line = <$fd>) {
2445 chomp $line;
2446 my ($pr, $ow) = split ' ', $line;
2447 $pr = unescape($pr);
2448 $ow = unescape($ow);
2449 $gitweb_project_owner->{$pr} = to_utf8($ow);
2451 close $fd;
2455 sub git_get_project_owner {
2456 my $project = shift;
2457 my $owner;
2459 return undef unless $project;
2460 $git_dir = "$projectroot/$project";
2462 if (!defined $gitweb_project_owner) {
2463 git_get_project_list_from_file();
2466 if (exists $gitweb_project_owner->{$project}) {
2467 $owner = $gitweb_project_owner->{$project};
2469 if (!defined $owner){
2470 $owner = git_get_project_config('owner');
2472 if (!defined $owner) {
2473 $owner = get_file_owner("$git_dir");
2476 return $owner;
2479 sub git_get_last_activity {
2480 my ($path) = @_;
2481 my $fd;
2483 $git_dir = "$projectroot/$path";
2484 open($fd, "-|", git_cmd(), 'for-each-ref',
2485 '--format=%(committer)',
2486 '--sort=-committerdate',
2487 '--count=1',
2488 'refs/heads') or return;
2489 my $most_recent = <$fd>;
2490 close $fd or return;
2491 if (defined $most_recent &&
2492 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2493 my $timestamp = $1;
2494 my $age = time - $timestamp;
2495 return ($age, age_string($age));
2497 return (undef, undef);
2500 sub git_get_references {
2501 my $type = shift || "";
2502 my %refs;
2503 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2504 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2505 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2506 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2507 or return;
2509 while (my $line = <$fd>) {
2510 chomp $line;
2511 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2512 if (defined $refs{$1}) {
2513 push @{$refs{$1}}, $2;
2514 } else {
2515 $refs{$1} = [ $2 ];
2519 close $fd or return;
2520 return \%refs;
2523 sub git_get_rev_name_tags {
2524 my $hash = shift || return undef;
2526 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2527 or return;
2528 my $name_rev = <$fd>;
2529 close $fd;
2531 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2532 return $1;
2533 } else {
2534 # catches also '$hash undefined' output
2535 return undef;
2539 ## ----------------------------------------------------------------------
2540 ## parse to hash functions
2542 sub parse_date {
2543 my $epoch = shift;
2544 my $tz = shift || "-0000";
2546 my %date;
2547 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2548 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2549 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2550 $date{'hour'} = $hour;
2551 $date{'minute'} = $min;
2552 $date{'mday'} = $mday;
2553 $date{'day'} = $days[$wday];
2554 $date{'month'} = $months[$mon];
2555 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2556 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2557 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2558 $mday, $months[$mon], $hour ,$min;
2559 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2560 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2562 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2563 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2564 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2565 $date{'hour_local'} = $hour;
2566 $date{'minute_local'} = $min;
2567 $date{'tz_local'} = $tz;
2568 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2569 1900+$year, $mon+1, $mday,
2570 $hour, $min, $sec, $tz);
2571 return %date;
2574 sub parse_tag {
2575 my $tag_id = shift;
2576 my %tag;
2577 my @comment;
2579 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2580 $tag{'id'} = $tag_id;
2581 while (my $line = <$fd>) {
2582 chomp $line;
2583 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2584 $tag{'object'} = $1;
2585 } elsif ($line =~ m/^type (.+)$/) {
2586 $tag{'type'} = $1;
2587 } elsif ($line =~ m/^tag (.+)$/) {
2588 $tag{'name'} = $1;
2589 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2590 $tag{'author'} = $1;
2591 $tag{'author_epoch'} = $2;
2592 $tag{'author_tz'} = $3;
2593 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2594 $tag{'author_name'} = $1;
2595 $tag{'author_email'} = $2;
2596 } else {
2597 $tag{'author_name'} = $tag{'author'};
2599 } elsif ($line =~ m/--BEGIN/) {
2600 push @comment, $line;
2601 last;
2602 } elsif ($line eq "") {
2603 last;
2606 push @comment, <$fd>;
2607 $tag{'comment'} = \@comment;
2608 close $fd or return;
2609 if (!defined $tag{'name'}) {
2610 return
2612 return %tag
2615 sub parse_commit_text {
2616 my ($commit_text, $withparents) = @_;
2617 my @commit_lines = split '\n', $commit_text;
2618 my %co;
2620 pop @commit_lines; # Remove '\0'
2622 if (! @commit_lines) {
2623 return;
2626 my $header = shift @commit_lines;
2627 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2628 return;
2630 ($co{'id'}, my @parents) = split ' ', $header;
2631 while (my $line = shift @commit_lines) {
2632 last if $line eq "\n";
2633 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2634 $co{'tree'} = $1;
2635 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2636 push @parents, $1;
2637 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2638 $co{'author'} = to_utf8($1);
2639 $co{'author_epoch'} = $2;
2640 $co{'author_tz'} = $3;
2641 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2642 $co{'author_name'} = $1;
2643 $co{'author_email'} = $2;
2644 } else {
2645 $co{'author_name'} = $co{'author'};
2647 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2648 $co{'committer'} = to_utf8($1);
2649 $co{'committer_epoch'} = $2;
2650 $co{'committer_tz'} = $3;
2651 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2652 $co{'committer_name'} = $1;
2653 $co{'committer_email'} = $2;
2654 } else {
2655 $co{'committer_name'} = $co{'committer'};
2659 if (!defined $co{'tree'}) {
2660 return;
2662 $co{'parents'} = \@parents;
2663 $co{'parent'} = $parents[0];
2665 foreach my $title (@commit_lines) {
2666 $title =~ s/^ //;
2667 if ($title ne "") {
2668 $co{'title'} = chop_str($title, 80, 5);
2669 # remove leading stuff of merges to make the interesting part visible
2670 if (length($title) > 50) {
2671 $title =~ s/^Automatic //;
2672 $title =~ s/^merge (of|with) /Merge ... /i;
2673 if (length($title) > 50) {
2674 $title =~ s/(http|rsync):\/\///;
2676 if (length($title) > 50) {
2677 $title =~ s/(master|www|rsync)\.//;
2679 if (length($title) > 50) {
2680 $title =~ s/kernel.org:?//;
2682 if (length($title) > 50) {
2683 $title =~ s/\/pub\/scm//;
2686 $co{'title_short'} = chop_str($title, 50, 5);
2687 last;
2690 if (! defined $co{'title'} || $co{'title'} eq "") {
2691 $co{'title'} = $co{'title_short'} = '(no commit message)';
2693 # remove added spaces
2694 foreach my $line (@commit_lines) {
2695 $line =~ s/^ //;
2697 $co{'comment'} = \@commit_lines;
2699 my $age = time - $co{'committer_epoch'};
2700 $co{'age'} = $age;
2701 $co{'age_string'} = age_string($age);
2702 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2703 if ($age > 60*60*24*7*2) {
2704 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2705 $co{'age_string_age'} = $co{'age_string'};
2706 } else {
2707 $co{'age_string_date'} = $co{'age_string'};
2708 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2710 return %co;
2713 sub parse_commit {
2714 my ($commit_id) = @_;
2715 my %co;
2717 local $/ = "\0";
2719 open my $fd, "-|", git_cmd(), "rev-list",
2720 "--parents",
2721 "--header",
2722 "--max-count=1",
2723 $commit_id,
2724 "--",
2725 or die_error(500, "Open git-rev-list failed");
2726 %co = parse_commit_text(<$fd>, 1);
2727 close $fd;
2729 return %co;
2732 sub parse_commits {
2733 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2734 my @cos;
2736 $maxcount ||= 1;
2737 $skip ||= 0;
2739 local $/ = "\0";
2741 open my $fd, "-|", git_cmd(), "rev-list",
2742 "--header",
2743 @args,
2744 ("--max-count=" . $maxcount),
2745 ("--skip=" . $skip),
2746 @extra_options,
2747 $commit_id,
2748 "--",
2749 ($filename ? ($filename) : ())
2750 or die_error(500, "Open git-rev-list failed");
2751 while (my $line = <$fd>) {
2752 my %co = parse_commit_text($line);
2753 push @cos, \%co;
2755 close $fd;
2757 return wantarray ? @cos : \@cos;
2760 # parse line of git-diff-tree "raw" output
2761 sub parse_difftree_raw_line {
2762 my $line = shift;
2763 my %res;
2765 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2766 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2767 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2768 $res{'from_mode'} = $1;
2769 $res{'to_mode'} = $2;
2770 $res{'from_id'} = $3;
2771 $res{'to_id'} = $4;
2772 $res{'status'} = $5;
2773 $res{'similarity'} = $6;
2774 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2775 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2776 } else {
2777 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2780 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2781 # combined diff (for merge commit)
2782 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2783 $res{'nparents'} = length($1);
2784 $res{'from_mode'} = [ split(' ', $2) ];
2785 $res{'to_mode'} = pop @{$res{'from_mode'}};
2786 $res{'from_id'} = [ split(' ', $3) ];
2787 $res{'to_id'} = pop @{$res{'from_id'}};
2788 $res{'status'} = [ split('', $4) ];
2789 $res{'to_file'} = unquote($5);
2791 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2792 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2793 $res{'commit'} = $1;
2796 return wantarray ? %res : \%res;
2799 # wrapper: return parsed line of git-diff-tree "raw" output
2800 # (the argument might be raw line, or parsed info)
2801 sub parsed_difftree_line {
2802 my $line_or_ref = shift;
2804 if (ref($line_or_ref) eq "HASH") {
2805 # pre-parsed (or generated by hand)
2806 return $line_or_ref;
2807 } else {
2808 return parse_difftree_raw_line($line_or_ref);
2812 # parse line of git-ls-tree output
2813 sub parse_ls_tree_line {
2814 my $line = shift;
2815 my %opts = @_;
2816 my %res;
2818 if ($opts{'-l'}) {
2819 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
2820 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
2822 $res{'mode'} = $1;
2823 $res{'type'} = $2;
2824 $res{'hash'} = $3;
2825 $res{'size'} = $4;
2826 if ($opts{'-z'}) {
2827 $res{'name'} = $5;
2828 } else {
2829 $res{'name'} = unquote($5);
2831 } else {
2832 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2833 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2835 $res{'mode'} = $1;
2836 $res{'type'} = $2;
2837 $res{'hash'} = $3;
2838 if ($opts{'-z'}) {
2839 $res{'name'} = $4;
2840 } else {
2841 $res{'name'} = unquote($4);
2845 return wantarray ? %res : \%res;
2848 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2849 sub parse_from_to_diffinfo {
2850 my ($diffinfo, $from, $to, @parents) = @_;
2852 if ($diffinfo->{'nparents'}) {
2853 # combined diff
2854 $from->{'file'} = [];
2855 $from->{'href'} = [];
2856 fill_from_file_info($diffinfo, @parents)
2857 unless exists $diffinfo->{'from_file'};
2858 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2859 $from->{'file'}[$i] =
2860 defined $diffinfo->{'from_file'}[$i] ?
2861 $diffinfo->{'from_file'}[$i] :
2862 $diffinfo->{'to_file'};
2863 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2864 $from->{'href'}[$i] = href(action=>"blob",
2865 hash_base=>$parents[$i],
2866 hash=>$diffinfo->{'from_id'}[$i],
2867 file_name=>$from->{'file'}[$i]);
2868 } else {
2869 $from->{'href'}[$i] = undef;
2872 } else {
2873 # ordinary (not combined) diff
2874 $from->{'file'} = $diffinfo->{'from_file'};
2875 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2876 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2877 hash=>$diffinfo->{'from_id'},
2878 file_name=>$from->{'file'});
2879 } else {
2880 delete $from->{'href'};
2884 $to->{'file'} = $diffinfo->{'to_file'};
2885 if (!is_deleted($diffinfo)) { # file exists in result
2886 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2887 hash=>$diffinfo->{'to_id'},
2888 file_name=>$to->{'file'});
2889 } else {
2890 delete $to->{'href'};
2894 ## ......................................................................
2895 ## parse to array of hashes functions
2897 sub git_get_heads_list {
2898 my $limit = shift;
2899 my @headslist;
2901 open my $fd, '-|', git_cmd(), 'for-each-ref',
2902 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2903 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2904 'refs/heads'
2905 or return;
2906 while (my $line = <$fd>) {
2907 my %ref_item;
2909 chomp $line;
2910 my ($refinfo, $committerinfo) = split(/\0/, $line);
2911 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2912 my ($committer, $epoch, $tz) =
2913 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2914 $ref_item{'fullname'} = $name;
2915 $name =~ s!^refs/heads/!!;
2917 $ref_item{'name'} = $name;
2918 $ref_item{'id'} = $hash;
2919 $ref_item{'title'} = $title || '(no commit message)';
2920 $ref_item{'epoch'} = $epoch;
2921 if ($epoch) {
2922 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2923 } else {
2924 $ref_item{'age'} = "unknown";
2927 push @headslist, \%ref_item;
2929 close $fd;
2931 return wantarray ? @headslist : \@headslist;
2934 sub git_get_tags_list {
2935 my $limit = shift;
2936 my @tagslist;
2938 open my $fd, '-|', git_cmd(), 'for-each-ref',
2939 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2940 '--format=%(objectname) %(objecttype) %(refname) '.
2941 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2942 'refs/tags'
2943 or return;
2944 while (my $line = <$fd>) {
2945 my %ref_item;
2947 chomp $line;
2948 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2949 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2950 my ($creator, $epoch, $tz) =
2951 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2952 $ref_item{'fullname'} = $name;
2953 $name =~ s!^refs/tags/!!;
2955 $ref_item{'type'} = $type;
2956 $ref_item{'id'} = $id;
2957 $ref_item{'name'} = $name;
2958 if ($type eq "tag") {
2959 $ref_item{'subject'} = $title;
2960 $ref_item{'reftype'} = $reftype;
2961 $ref_item{'refid'} = $refid;
2962 } else {
2963 $ref_item{'reftype'} = $type;
2964 $ref_item{'refid'} = $id;
2967 if ($type eq "tag" || $type eq "commit") {
2968 $ref_item{'epoch'} = $epoch;
2969 if ($epoch) {
2970 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2971 } else {
2972 $ref_item{'age'} = "unknown";
2976 push @tagslist, \%ref_item;
2978 close $fd;
2980 return wantarray ? @tagslist : \@tagslist;
2983 ## ----------------------------------------------------------------------
2984 ## filesystem-related functions
2986 sub get_file_owner {
2987 my $path = shift;
2989 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2990 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2991 if (!defined $gcos) {
2992 return undef;
2994 my $owner = $gcos;
2995 $owner =~ s/[,;].*$//;
2996 return to_utf8($owner);
2999 # assume that file exists
3000 sub insert_file {
3001 my $filename = shift;
3003 open my $fd, '<', $filename;
3004 print map { to_utf8($_) } <$fd>;
3005 close $fd;
3008 ## ......................................................................
3009 ## mimetype related functions
3011 sub mimetype_guess_file {
3012 my $filename = shift;
3013 my $mimemap = shift;
3014 -r $mimemap or return undef;
3016 my %mimemap;
3017 open(my $mh, '<', $mimemap) or return undef;
3018 while (<$mh>) {
3019 next if m/^#/; # skip comments
3020 my ($mimetype, $exts) = split(/\t+/);
3021 if (defined $exts) {
3022 my @exts = split(/\s+/, $exts);
3023 foreach my $ext (@exts) {
3024 $mimemap{$ext} = $mimetype;
3028 close($mh);
3030 $filename =~ /\.([^.]*)$/;
3031 return $mimemap{$1};
3034 sub mimetype_guess {
3035 my $filename = shift;
3036 my $mime;
3037 $filename =~ /\./ or return undef;
3039 if ($mimetypes_file) {
3040 my $file = $mimetypes_file;
3041 if ($file !~ m!^/!) { # if it is relative path
3042 # it is relative to project
3043 $file = "$projectroot/$project/$file";
3045 $mime = mimetype_guess_file($filename, $file);
3047 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3048 return $mime;
3051 sub blob_mimetype {
3052 my $fd = shift;
3053 my $filename = shift;
3055 if ($filename) {
3056 my $mime = mimetype_guess($filename);
3057 $mime and return $mime;
3060 # just in case
3061 return $default_blob_plain_mimetype unless $fd;
3063 if (-T $fd) {
3064 return 'text/plain';
3065 } elsif (! $filename) {
3066 return 'application/octet-stream';
3067 } elsif ($filename =~ m/\.png$/i) {
3068 return 'image/png';
3069 } elsif ($filename =~ m/\.gif$/i) {
3070 return 'image/gif';
3071 } elsif ($filename =~ m/\.jpe?g$/i) {
3072 return 'image/jpeg';
3073 } else {
3074 return 'application/octet-stream';
3078 sub blob_contenttype {
3079 my ($fd, $file_name, $type) = @_;
3081 $type ||= blob_mimetype($fd, $file_name);
3082 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3083 $type .= "; charset=$default_text_plain_charset";
3086 return $type;
3089 ## ======================================================================
3090 ## functions printing HTML: header, footer, error page
3092 sub git_header_html {
3093 my $status = shift || "200 OK";
3094 my $expires = shift;
3096 my $title = "$site_name";
3097 if (defined $project) {
3098 $title .= " - " . to_utf8($project);
3099 if (defined $action) {
3100 $title .= "/$action";
3101 if (defined $file_name) {
3102 $title .= " - " . esc_path($file_name);
3103 if ($action eq "tree" && $file_name !~ m|/$|) {
3104 $title .= "/";
3109 my $content_type;
3110 # require explicit support from the UA if we are to send the page as
3111 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3112 # we have to do this because MSIE sometimes globs '*/*', pretending to
3113 # support xhtml+xml but choking when it gets what it asked for.
3114 if (defined $cgi->http('HTTP_ACCEPT') &&
3115 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3116 $cgi->Accept('application/xhtml+xml') != 0) {
3117 $content_type = 'application/xhtml+xml';
3118 } else {
3119 $content_type = 'text/html';
3121 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3122 -status=> $status, -expires => $expires);
3123 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3124 print <<EOF;
3125 <?xml version="1.0" encoding="utf-8"?>
3126 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3127 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3128 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3129 <!-- git core binaries version $git_version -->
3130 <head>
3131 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3132 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3133 <meta name="robots" content="index, nofollow"/>
3134 <title>$title</title>
3136 # the stylesheet, favicon etc urls won't work correctly with path_info
3137 # unless we set the appropriate base URL
3138 if ($ENV{'PATH_INFO'}) {
3139 print "<base href=\"".esc_url($base_url)."\" />\n";
3141 # print out each stylesheet that exist, providing backwards capability
3142 # for those people who defined $stylesheet in a config file
3143 if (defined $stylesheet) {
3144 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3145 } else {
3146 foreach my $stylesheet (@stylesheets) {
3147 next unless $stylesheet;
3148 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3151 if (defined $project) {
3152 my %href_params = get_feed_info();
3153 if (!exists $href_params{'-title'}) {
3154 $href_params{'-title'} = 'log';
3157 foreach my $format qw(RSS Atom) {
3158 my $type = lc($format);
3159 my %link_attr = (
3160 '-rel' => 'alternate',
3161 '-title' => "$project - $href_params{'-title'} - $format feed",
3162 '-type' => "application/$type+xml"
3165 $href_params{'action'} = $type;
3166 $link_attr{'-href'} = href(%href_params);
3167 print "<link ".
3168 "rel=\"$link_attr{'-rel'}\" ".
3169 "title=\"$link_attr{'-title'}\" ".
3170 "href=\"$link_attr{'-href'}\" ".
3171 "type=\"$link_attr{'-type'}\" ".
3172 "/>\n";
3174 $href_params{'extra_options'} = '--no-merges';
3175 $link_attr{'-href'} = href(%href_params);
3176 $link_attr{'-title'} .= ' (no merges)';
3177 print "<link ".
3178 "rel=\"$link_attr{'-rel'}\" ".
3179 "title=\"$link_attr{'-title'}\" ".
3180 "href=\"$link_attr{'-href'}\" ".
3181 "type=\"$link_attr{'-type'}\" ".
3182 "/>\n";
3185 } else {
3186 printf('<link rel="alternate" title="%s projects list" '.
3187 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3188 $site_name, href(project=>undef, action=>"project_index"));
3189 printf('<link rel="alternate" title="%s projects feeds" '.
3190 'href="%s" type="text/x-opml" />'."\n",
3191 $site_name, href(project=>undef, action=>"opml"));
3193 if (defined $favicon) {
3194 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3197 print "</head>\n" .
3198 "<body>\n";
3200 if (-f $site_header) {
3201 insert_file($site_header);
3204 print "<div class=\"page_header\">\n" .
3205 $cgi->a({-href => esc_url($logo_url),
3206 -title => $logo_label},
3207 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3208 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3209 if (defined $project) {
3210 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3211 if (defined $action) {
3212 print " / $action";
3214 print "\n";
3216 print "</div>\n";
3218 my $have_search = gitweb_check_feature('search');
3219 if (defined $project && $have_search) {
3220 if (!defined $searchtext) {
3221 $searchtext = "";
3223 my $search_hash;
3224 if (defined $hash_base) {
3225 $search_hash = $hash_base;
3226 } elsif (defined $hash) {
3227 $search_hash = $hash;
3228 } else {
3229 $search_hash = "HEAD";
3231 my $action = $my_uri;
3232 my $use_pathinfo = gitweb_check_feature('pathinfo');
3233 if ($use_pathinfo) {
3234 $action .= "/".esc_url($project);
3236 print $cgi->startform(-method => "get", -action => $action) .
3237 "<div class=\"search\">\n" .
3238 (!$use_pathinfo &&
3239 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3240 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3241 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3242 $cgi->popup_menu(-name => 'st', -default => 'commit',
3243 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3244 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3245 " search:\n",
3246 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3247 "<span title=\"Extended regular expression\">" .
3248 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3249 -checked => $search_use_regexp) .
3250 "</span>" .
3251 "</div>" .
3252 $cgi->end_form() . "\n";
3256 sub git_footer_html {
3257 my $feed_class = 'rss_logo';
3259 print "<div class=\"page_footer\">\n";
3260 if (defined $project) {
3261 my $descr = git_get_project_description($project);
3262 if (defined $descr) {
3263 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3266 my %href_params = get_feed_info();
3267 if (!%href_params) {
3268 $feed_class .= ' generic';
3270 $href_params{'-title'} ||= 'log';
3272 foreach my $format qw(RSS Atom) {
3273 $href_params{'action'} = lc($format);
3274 print $cgi->a({-href => href(%href_params),
3275 -title => "$href_params{'-title'} $format feed",
3276 -class => $feed_class}, $format)."\n";
3279 } else {
3280 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3281 -class => $feed_class}, "OPML") . " ";
3282 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3283 -class => $feed_class}, "TXT") . "\n";
3285 print "</div>\n"; # class="page_footer"
3287 if (-f $site_footer) {
3288 insert_file($site_footer);
3291 print "</body>\n" .
3292 "</html>";
3295 # die_error(<http_status_code>, <error_message>)
3296 # Example: die_error(404, 'Hash not found')
3297 # By convention, use the following status codes (as defined in RFC 2616):
3298 # 400: Invalid or missing CGI parameters, or
3299 # requested object exists but has wrong type.
3300 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3301 # this server or project.
3302 # 404: Requested object/revision/project doesn't exist.
3303 # 500: The server isn't configured properly, or
3304 # an internal error occurred (e.g. failed assertions caused by bugs), or
3305 # an unknown error occurred (e.g. the git binary died unexpectedly).
3306 sub die_error {
3307 my $status = shift || 500;
3308 my $error = shift || "Internal server error";
3310 my %http_responses = (400 => '400 Bad Request',
3311 403 => '403 Forbidden',
3312 404 => '404 Not Found',
3313 500 => '500 Internal Server Error');
3314 git_header_html($http_responses{$status});
3315 print <<EOF;
3316 <div class="page_body">
3317 <br /><br />
3318 $status - $error
3319 <br />
3320 </div>
3322 git_footer_html();
3323 exit;
3326 ## ----------------------------------------------------------------------
3327 ## functions printing or outputting HTML: navigation
3329 sub git_print_page_nav {
3330 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3331 $extra = '' if !defined $extra; # pager or formats
3333 my @navs = qw(summary shortlog log commit commitdiff tree);
3334 if ($suppress) {
3335 @navs = grep { $_ ne $suppress } @navs;
3338 my %arg = map { $_ => {action=>$_} } @navs;
3339 if (defined $head) {
3340 for (qw(commit commitdiff)) {
3341 $arg{$_}{'hash'} = $head;
3343 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3344 for (qw(shortlog log)) {
3345 $arg{$_}{'hash'} = $head;
3350 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3351 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3353 my @actions = gitweb_get_feature('actions');
3354 my %repl = (
3355 '%' => '%',
3356 'n' => $project, # project name
3357 'f' => $git_dir, # project path within filesystem
3358 'h' => $treehead || '', # current hash ('h' parameter)
3359 'b' => $treebase || '', # hash base ('hb' parameter)
3361 while (@actions) {
3362 my ($label, $link, $pos) = splice(@actions,0,3);
3363 # insert
3364 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3365 # munch munch
3366 $link =~ s/%([%nfhb])/$repl{$1}/g;
3367 $arg{$label}{'_href'} = $link;
3370 print "<div class=\"page_nav\">\n" .
3371 (join " | ",
3372 map { $_ eq $current ?
3373 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3374 } @navs);
3375 print "<br/>\n$extra<br/>\n" .
3376 "</div>\n";
3379 sub format_paging_nav {
3380 my ($action, $hash, $head, $page, $has_next_link) = @_;
3381 my $paging_nav;
3384 if ($hash ne $head || $page) {
3385 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3386 } else {
3387 $paging_nav .= "HEAD";
3390 if ($page > 0) {
3391 $paging_nav .= " &sdot; " .
3392 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3393 -accesskey => "p", -title => "Alt-p"}, "prev");
3394 } else {
3395 $paging_nav .= " &sdot; prev";
3398 if ($has_next_link) {
3399 $paging_nav .= " &sdot; " .
3400 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3401 -accesskey => "n", -title => "Alt-n"}, "next");
3402 } else {
3403 $paging_nav .= " &sdot; next";
3406 return $paging_nav;
3409 ## ......................................................................
3410 ## functions printing or outputting HTML: div
3412 sub git_print_header_div {
3413 my ($action, $title, $hash, $hash_base) = @_;
3414 my %args = ();
3416 $args{'action'} = $action;
3417 $args{'hash'} = $hash if $hash;
3418 $args{'hash_base'} = $hash_base if $hash_base;
3420 print "<div class=\"header\">\n" .
3421 $cgi->a({-href => href(%args), -class => "title"},
3422 $title ? $title : $action) .
3423 "\n</div>\n";
3426 sub print_local_time {
3427 my %date = @_;
3428 if ($date{'hour_local'} < 6) {
3429 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3430 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3431 } else {
3432 printf(" (%02d:%02d %s)",
3433 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3437 # Outputs the author name and date in long form
3438 sub git_print_authorship {
3439 my $co = shift;
3440 my %opts = @_;
3441 my $tag = $opts{-tag} || 'div';
3442 my $author = $co->{'author_name'};
3444 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3445 print "<$tag class=\"author_date\">" .
3446 format_search_author($author, "author", esc_html($author)) .
3447 " [$ad{'rfc2822'}";
3448 print_local_time(%ad) if ($opts{-localtime});
3449 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3450 . "</$tag>\n";
3453 # Outputs table rows containing the full author or committer information,
3454 # in the format expected for 'commit' view (& similia).
3455 # Parameters are a commit hash reference, followed by the list of people
3456 # to output information for. If the list is empty it defalts to both
3457 # author and committer.
3458 sub git_print_authorship_rows {
3459 my $co = shift;
3460 # too bad we can't use @people = @_ || ('author', 'committer')
3461 my @people = @_;
3462 @people = ('author', 'committer') unless @people;
3463 foreach my $who (@people) {
3464 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3465 print "<tr><td>$who</td><td>" .
3466 format_search_author($co->{"${who}_name"}, $who,
3467 esc_html($co->{"${who}_name"})) . " " .
3468 format_search_author($co->{"${who}_email"}, $who,
3469 esc_html("<" . $co->{"${who}_email"} . ">")) .
3470 "</td><td rowspan=\"2\">" .
3471 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3472 "</td></tr>\n" .
3473 "<tr>" .
3474 "<td></td><td> $wd{'rfc2822'}";
3475 print_local_time(%wd);
3476 print "</td>" .
3477 "</tr>\n";
3481 sub git_print_page_path {
3482 my $name = shift;
3483 my $type = shift;
3484 my $hb = shift;
3487 print "<div class=\"page_path\">";
3488 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3489 -title => 'tree root'}, to_utf8("[$project]"));
3490 print " / ";
3491 if (defined $name) {
3492 my @dirname = split '/', $name;
3493 my $basename = pop @dirname;
3494 my $fullname = '';
3496 foreach my $dir (@dirname) {
3497 $fullname .= ($fullname ? '/' : '') . $dir;
3498 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3499 hash_base=>$hb),
3500 -title => $fullname}, esc_path($dir));
3501 print " / ";
3503 if (defined $type && $type eq 'blob') {
3504 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3505 hash_base=>$hb),
3506 -title => $name}, esc_path($basename));
3507 } elsif (defined $type && $type eq 'tree') {
3508 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3509 hash_base=>$hb),
3510 -title => $name}, esc_path($basename));
3511 print " / ";
3512 } else {
3513 print esc_path($basename);
3516 print "<br/></div>\n";
3519 sub git_print_log {
3520 my $log = shift;
3521 my %opts = @_;
3523 if ($opts{'-remove_title'}) {
3524 # remove title, i.e. first line of log
3525 shift @$log;
3527 # remove leading empty lines
3528 while (defined $log->[0] && $log->[0] eq "") {
3529 shift @$log;
3532 # print log
3533 my $signoff = 0;
3534 my $empty = 0;
3535 foreach my $line (@$log) {
3536 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3537 $signoff = 1;
3538 $empty = 0;
3539 if (! $opts{'-remove_signoff'}) {
3540 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3541 next;
3542 } else {
3543 # remove signoff lines
3544 next;
3546 } else {
3547 $signoff = 0;
3550 # print only one empty line
3551 # do not print empty line after signoff
3552 if ($line eq "") {
3553 next if ($empty || $signoff);
3554 $empty = 1;
3555 } else {
3556 $empty = 0;
3559 print format_log_line_html($line) . "<br/>\n";
3562 if ($opts{'-final_empty_line'}) {
3563 # end with single empty line
3564 print "<br/>\n" unless $empty;
3568 # return link target (what link points to)
3569 sub git_get_link_target {
3570 my $hash = shift;
3571 my $link_target;
3573 # read link
3574 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3575 or return;
3577 local $/ = undef;
3578 $link_target = <$fd>;
3580 close $fd
3581 or return;
3583 return $link_target;
3586 # given link target, and the directory (basedir) the link is in,
3587 # return target of link relative to top directory (top tree);
3588 # return undef if it is not possible (including absolute links).
3589 sub normalize_link_target {
3590 my ($link_target, $basedir) = @_;
3592 # absolute symlinks (beginning with '/') cannot be normalized
3593 return if (substr($link_target, 0, 1) eq '/');
3595 # normalize link target to path from top (root) tree (dir)
3596 my $path;
3597 if ($basedir) {
3598 $path = $basedir . '/' . $link_target;
3599 } else {
3600 # we are in top (root) tree (dir)
3601 $path = $link_target;
3604 # remove //, /./, and /../
3605 my @path_parts;
3606 foreach my $part (split('/', $path)) {
3607 # discard '.' and ''
3608 next if (!$part || $part eq '.');
3609 # handle '..'
3610 if ($part eq '..') {
3611 if (@path_parts) {
3612 pop @path_parts;
3613 } else {
3614 # link leads outside repository (outside top dir)
3615 return;
3617 } else {
3618 push @path_parts, $part;
3621 $path = join('/', @path_parts);
3623 return $path;
3626 # print tree entry (row of git_tree), but without encompassing <tr> element
3627 sub git_print_tree_entry {
3628 my ($t, $basedir, $hash_base, $have_blame) = @_;
3630 my %base_key = ();
3631 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3633 # The format of a table row is: mode list link. Where mode is
3634 # the mode of the entry, list is the name of the entry, an href,
3635 # and link is the action links of the entry.
3637 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3638 if (exists $t->{'size'}) {
3639 print "<td class=\"size\">$t->{'size'}</td>\n";
3641 if ($t->{'type'} eq "blob") {
3642 print "<td class=\"list\">" .
3643 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3644 file_name=>"$basedir$t->{'name'}", %base_key),
3645 -class => "list"}, esc_path($t->{'name'}));
3646 if (S_ISLNK(oct $t->{'mode'})) {
3647 my $link_target = git_get_link_target($t->{'hash'});
3648 if ($link_target) {
3649 my $norm_target = normalize_link_target($link_target, $basedir);
3650 if (defined $norm_target) {
3651 print " -> " .
3652 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3653 file_name=>$norm_target),
3654 -title => $norm_target}, esc_path($link_target));
3655 } else {
3656 print " -> " . esc_path($link_target);
3660 print "</td>\n";
3661 print "<td class=\"link\">";
3662 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3663 file_name=>"$basedir$t->{'name'}", %base_key)},
3664 "blob");
3665 if ($have_blame) {
3666 print " | " .
3667 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3668 file_name=>"$basedir$t->{'name'}", %base_key)},
3669 "blame");
3671 if (defined $hash_base) {
3672 print " | " .
3673 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3674 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3675 "history");
3677 print " | " .
3678 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3679 file_name=>"$basedir$t->{'name'}")},
3680 "raw");
3681 print "</td>\n";
3683 } elsif ($t->{'type'} eq "tree") {
3684 print "<td class=\"list\">";
3685 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3686 file_name=>"$basedir$t->{'name'}",
3687 %base_key)},
3688 esc_path($t->{'name'}));
3689 print "</td>\n";
3690 print "<td class=\"link\">";
3691 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3692 file_name=>"$basedir$t->{'name'}",
3693 %base_key)},
3694 "tree");
3695 if (defined $hash_base) {
3696 print " | " .
3697 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3698 file_name=>"$basedir$t->{'name'}")},
3699 "history");
3701 print "</td>\n";
3702 } else {
3703 # unknown object: we can only present history for it
3704 # (this includes 'commit' object, i.e. submodule support)
3705 print "<td class=\"list\">" .
3706 esc_path($t->{'name'}) .
3707 "</td>\n";
3708 print "<td class=\"link\">";
3709 if (defined $hash_base) {
3710 print $cgi->a({-href => href(action=>"history",
3711 hash_base=>$hash_base,
3712 file_name=>"$basedir$t->{'name'}")},
3713 "history");
3715 print "</td>\n";
3719 ## ......................................................................
3720 ## functions printing large fragments of HTML
3722 # get pre-image filenames for merge (combined) diff
3723 sub fill_from_file_info {
3724 my ($diff, @parents) = @_;
3726 $diff->{'from_file'} = [ ];
3727 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3728 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3729 if ($diff->{'status'}[$i] eq 'R' ||
3730 $diff->{'status'}[$i] eq 'C') {
3731 $diff->{'from_file'}[$i] =
3732 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3736 return $diff;
3739 # is current raw difftree line of file deletion
3740 sub is_deleted {
3741 my $diffinfo = shift;
3743 return $diffinfo->{'to_id'} eq ('0' x 40);
3746 # does patch correspond to [previous] difftree raw line
3747 # $diffinfo - hashref of parsed raw diff format
3748 # $patchinfo - hashref of parsed patch diff format
3749 # (the same keys as in $diffinfo)
3750 sub is_patch_split {
3751 my ($diffinfo, $patchinfo) = @_;
3753 return defined $diffinfo && defined $patchinfo
3754 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3758 sub git_difftree_body {
3759 my ($difftree, $hash, @parents) = @_;
3760 my ($parent) = $parents[0];
3761 my $have_blame = gitweb_check_feature('blame');
3762 print "<div class=\"list_head\">\n";
3763 if ($#{$difftree} > 10) {
3764 print(($#{$difftree} + 1) . " files changed:\n");
3766 print "</div>\n";
3768 print "<table class=\"" .
3769 (@parents > 1 ? "combined " : "") .
3770 "diff_tree\">\n";
3772 # header only for combined diff in 'commitdiff' view
3773 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3774 if ($has_header) {
3775 # table header
3776 print "<thead><tr>\n" .
3777 "<th></th><th></th>\n"; # filename, patchN link
3778 for (my $i = 0; $i < @parents; $i++) {
3779 my $par = $parents[$i];
3780 print "<th>" .
3781 $cgi->a({-href => href(action=>"commitdiff",
3782 hash=>$hash, hash_parent=>$par),
3783 -title => 'commitdiff to parent number ' .
3784 ($i+1) . ': ' . substr($par,0,7)},
3785 $i+1) .
3786 "&nbsp;</th>\n";
3788 print "</tr></thead>\n<tbody>\n";
3791 my $alternate = 1;
3792 my $patchno = 0;
3793 foreach my $line (@{$difftree}) {
3794 my $diff = parsed_difftree_line($line);
3796 if ($alternate) {
3797 print "<tr class=\"dark\">\n";
3798 } else {
3799 print "<tr class=\"light\">\n";
3801 $alternate ^= 1;
3803 if (exists $diff->{'nparents'}) { # combined diff
3805 fill_from_file_info($diff, @parents)
3806 unless exists $diff->{'from_file'};
3808 if (!is_deleted($diff)) {
3809 # file exists in the result (child) commit
3810 print "<td>" .
3811 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3812 file_name=>$diff->{'to_file'},
3813 hash_base=>$hash),
3814 -class => "list"}, esc_path($diff->{'to_file'})) .
3815 "</td>\n";
3816 } else {
3817 print "<td>" .
3818 esc_path($diff->{'to_file'}) .
3819 "</td>\n";
3822 if ($action eq 'commitdiff') {
3823 # link to patch
3824 $patchno++;
3825 print "<td class=\"link\">" .
3826 $cgi->a({-href => "#patch$patchno"}, "patch") .
3827 " | " .
3828 "</td>\n";
3831 my $has_history = 0;
3832 my $not_deleted = 0;
3833 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3834 my $hash_parent = $parents[$i];
3835 my $from_hash = $diff->{'from_id'}[$i];
3836 my $from_path = $diff->{'from_file'}[$i];
3837 my $status = $diff->{'status'}[$i];
3839 $has_history ||= ($status ne 'A');
3840 $not_deleted ||= ($status ne 'D');
3842 if ($status eq 'A') {
3843 print "<td class=\"link\" align=\"right\"> | </td>\n";
3844 } elsif ($status eq 'D') {
3845 print "<td class=\"link\">" .
3846 $cgi->a({-href => href(action=>"blob",
3847 hash_base=>$hash,
3848 hash=>$from_hash,
3849 file_name=>$from_path)},
3850 "blob" . ($i+1)) .
3851 " | </td>\n";
3852 } else {
3853 if ($diff->{'to_id'} eq $from_hash) {
3854 print "<td class=\"link nochange\">";
3855 } else {
3856 print "<td class=\"link\">";
3858 print $cgi->a({-href => href(action=>"blobdiff",
3859 hash=>$diff->{'to_id'},
3860 hash_parent=>$from_hash,
3861 hash_base=>$hash,
3862 hash_parent_base=>$hash_parent,
3863 file_name=>$diff->{'to_file'},
3864 file_parent=>$from_path)},
3865 "diff" . ($i+1)) .
3866 " | </td>\n";
3870 print "<td class=\"link\">";
3871 if ($not_deleted) {
3872 print $cgi->a({-href => href(action=>"blob",
3873 hash=>$diff->{'to_id'},
3874 file_name=>$diff->{'to_file'},
3875 hash_base=>$hash)},
3876 "blob");
3877 print " | " if ($has_history);
3879 if ($has_history) {
3880 print $cgi->a({-href => href(action=>"history",
3881 file_name=>$diff->{'to_file'},
3882 hash_base=>$hash)},
3883 "history");
3885 print "</td>\n";
3887 print "</tr>\n";
3888 next; # instead of 'else' clause, to avoid extra indent
3890 # else ordinary diff
3892 my ($to_mode_oct, $to_mode_str, $to_file_type);
3893 my ($from_mode_oct, $from_mode_str, $from_file_type);
3894 if ($diff->{'to_mode'} ne ('0' x 6)) {
3895 $to_mode_oct = oct $diff->{'to_mode'};
3896 if (S_ISREG($to_mode_oct)) { # only for regular file
3897 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3899 $to_file_type = file_type($diff->{'to_mode'});
3901 if ($diff->{'from_mode'} ne ('0' x 6)) {
3902 $from_mode_oct = oct $diff->{'from_mode'};
3903 if (S_ISREG($to_mode_oct)) { # only for regular file
3904 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3906 $from_file_type = file_type($diff->{'from_mode'});
3909 if ($diff->{'status'} eq "A") { # created
3910 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3911 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3912 $mode_chng .= "]</span>";
3913 print "<td>";
3914 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3915 hash_base=>$hash, file_name=>$diff->{'file'}),
3916 -class => "list"}, esc_path($diff->{'file'}));
3917 print "</td>\n";
3918 print "<td>$mode_chng</td>\n";
3919 print "<td class=\"link\">";
3920 if ($action eq 'commitdiff') {
3921 # link to patch
3922 $patchno++;
3923 print $cgi->a({-href => "#patch$patchno"}, "patch");
3924 print " | ";
3926 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3927 hash_base=>$hash, file_name=>$diff->{'file'})},
3928 "blob");
3929 print "</td>\n";
3931 } elsif ($diff->{'status'} eq "D") { # deleted
3932 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3933 print "<td>";
3934 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3935 hash_base=>$parent, file_name=>$diff->{'file'}),
3936 -class => "list"}, esc_path($diff->{'file'}));
3937 print "</td>\n";
3938 print "<td>$mode_chng</td>\n";
3939 print "<td class=\"link\">";
3940 if ($action eq 'commitdiff') {
3941 # link to patch
3942 $patchno++;
3943 print $cgi->a({-href => "#patch$patchno"}, "patch");
3944 print " | ";
3946 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3947 hash_base=>$parent, file_name=>$diff->{'file'})},
3948 "blob") . " | ";
3949 if ($have_blame) {
3950 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3951 file_name=>$diff->{'file'})},
3952 "blame") . " | ";
3954 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3955 file_name=>$diff->{'file'})},
3956 "history");
3957 print "</td>\n";
3959 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3960 my $mode_chnge = "";
3961 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3962 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3963 if ($from_file_type ne $to_file_type) {
3964 $mode_chnge .= " from $from_file_type to $to_file_type";
3966 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3967 if ($from_mode_str && $to_mode_str) {
3968 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3969 } elsif ($to_mode_str) {
3970 $mode_chnge .= " mode: $to_mode_str";
3973 $mode_chnge .= "]</span>\n";
3975 print "<td>";
3976 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3977 hash_base=>$hash, file_name=>$diff->{'file'}),
3978 -class => "list"}, esc_path($diff->{'file'}));
3979 print "</td>\n";
3980 print "<td>$mode_chnge</td>\n";
3981 print "<td class=\"link\">";
3982 if ($action eq 'commitdiff') {
3983 # link to patch
3984 $patchno++;
3985 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3986 " | ";
3987 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3988 # "commit" view and modified file (not onlu mode changed)
3989 print $cgi->a({-href => href(action=>"blobdiff",
3990 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3991 hash_base=>$hash, hash_parent_base=>$parent,
3992 file_name=>$diff->{'file'})},
3993 "diff") .
3994 " | ";
3996 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3997 hash_base=>$hash, file_name=>$diff->{'file'})},
3998 "blob") . " | ";
3999 if ($have_blame) {
4000 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4001 file_name=>$diff->{'file'})},
4002 "blame") . " | ";
4004 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4005 file_name=>$diff->{'file'})},
4006 "history");
4007 print "</td>\n";
4009 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4010 my %status_name = ('R' => 'moved', 'C' => 'copied');
4011 my $nstatus = $status_name{$diff->{'status'}};
4012 my $mode_chng = "";
4013 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4014 # mode also for directories, so we cannot use $to_mode_str
4015 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4017 print "<td>" .
4018 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4019 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4020 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4021 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4022 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4023 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4024 -class => "list"}, esc_path($diff->{'from_file'})) .
4025 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4026 "<td class=\"link\">";
4027 if ($action eq 'commitdiff') {
4028 # link to patch
4029 $patchno++;
4030 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4031 " | ";
4032 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4033 # "commit" view and modified file (not only pure rename or copy)
4034 print $cgi->a({-href => href(action=>"blobdiff",
4035 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4036 hash_base=>$hash, hash_parent_base=>$parent,
4037 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4038 "diff") .
4039 " | ";
4041 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4042 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4043 "blob") . " | ";
4044 if ($have_blame) {
4045 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4046 file_name=>$diff->{'to_file'})},
4047 "blame") . " | ";
4049 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4050 file_name=>$diff->{'to_file'})},
4051 "history");
4052 print "</td>\n";
4054 } # we should not encounter Unmerged (U) or Unknown (X) status
4055 print "</tr>\n";
4057 print "</tbody>" if $has_header;
4058 print "</table>\n";
4061 sub git_patchset_body {
4062 my ($fd, $difftree, $hash, @hash_parents) = @_;
4063 my ($hash_parent) = $hash_parents[0];
4065 my $is_combined = (@hash_parents > 1);
4066 my $patch_idx = 0;
4067 my $patch_number = 0;
4068 my $patch_line;
4069 my $diffinfo;
4070 my $to_name;
4071 my (%from, %to);
4073 print "<div class=\"patchset\">\n";
4075 # skip to first patch
4076 while ($patch_line = <$fd>) {
4077 chomp $patch_line;
4079 last if ($patch_line =~ m/^diff /);
4082 PATCH:
4083 while ($patch_line) {
4085 # parse "git diff" header line
4086 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4087 # $1 is from_name, which we do not use
4088 $to_name = unquote($2);
4089 $to_name =~ s!^b/!!;
4090 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4091 # $1 is 'cc' or 'combined', which we do not use
4092 $to_name = unquote($2);
4093 } else {
4094 $to_name = undef;
4097 # check if current patch belong to current raw line
4098 # and parse raw git-diff line if needed
4099 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4100 # this is continuation of a split patch
4101 print "<div class=\"patch cont\">\n";
4102 } else {
4103 # advance raw git-diff output if needed
4104 $patch_idx++ if defined $diffinfo;
4106 # read and prepare patch information
4107 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4109 # compact combined diff output can have some patches skipped
4110 # find which patch (using pathname of result) we are at now;
4111 if ($is_combined) {
4112 while ($to_name ne $diffinfo->{'to_file'}) {
4113 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4114 format_diff_cc_simplified($diffinfo, @hash_parents) .
4115 "</div>\n"; # class="patch"
4117 $patch_idx++;
4118 $patch_number++;
4120 last if $patch_idx > $#$difftree;
4121 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4125 # modifies %from, %to hashes
4126 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4128 # this is first patch for raw difftree line with $patch_idx index
4129 # we index @$difftree array from 0, but number patches from 1
4130 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4133 # git diff header
4134 #assert($patch_line =~ m/^diff /) if DEBUG;
4135 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4136 $patch_number++;
4137 # print "git diff" header
4138 print format_git_diff_header_line($patch_line, $diffinfo,
4139 \%from, \%to);
4141 # print extended diff header
4142 print "<div class=\"diff extended_header\">\n";
4143 EXTENDED_HEADER:
4144 while ($patch_line = <$fd>) {
4145 chomp $patch_line;
4147 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4149 print format_extended_diff_header_line($patch_line, $diffinfo,
4150 \%from, \%to);
4152 print "</div>\n"; # class="diff extended_header"
4154 # from-file/to-file diff header
4155 if (! $patch_line) {
4156 print "</div>\n"; # class="patch"
4157 last PATCH;
4159 next PATCH if ($patch_line =~ m/^diff /);
4160 #assert($patch_line =~ m/^---/) if DEBUG;
4162 my $last_patch_line = $patch_line;
4163 $patch_line = <$fd>;
4164 chomp $patch_line;
4165 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4167 print format_diff_from_to_header($last_patch_line, $patch_line,
4168 $diffinfo, \%from, \%to,
4169 @hash_parents);
4171 # the patch itself
4172 LINE:
4173 while ($patch_line = <$fd>) {
4174 chomp $patch_line;
4176 next PATCH if ($patch_line =~ m/^diff /);
4178 print format_diff_line($patch_line, \%from, \%to);
4181 } continue {
4182 print "</div>\n"; # class="patch"
4185 # for compact combined (--cc) format, with chunk and patch simpliciaction
4186 # patchset might be empty, but there might be unprocessed raw lines
4187 for (++$patch_idx if $patch_number > 0;
4188 $patch_idx < @$difftree;
4189 ++$patch_idx) {
4190 # read and prepare patch information
4191 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4193 # generate anchor for "patch" links in difftree / whatchanged part
4194 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4195 format_diff_cc_simplified($diffinfo, @hash_parents) .
4196 "</div>\n"; # class="patch"
4198 $patch_number++;
4201 if ($patch_number == 0) {
4202 if (@hash_parents > 1) {
4203 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4204 } else {
4205 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4209 print "</div>\n"; # class="patchset"
4212 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4214 # fills project list info (age, description, owner, forks) for each
4215 # project in the list, removing invalid projects from returned list
4216 # NOTE: modifies $projlist, but does not remove entries from it
4217 sub fill_project_list_info {
4218 my ($projlist, $check_forks) = @_;
4219 my @projects;
4221 my $show_ctags = gitweb_check_feature('ctags');
4222 PROJECT:
4223 foreach my $pr (@$projlist) {
4224 my (@activity) = git_get_last_activity($pr->{'path'});
4225 unless (@activity) {
4226 next PROJECT;
4228 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4229 if (!defined $pr->{'descr'}) {
4230 my $descr = git_get_project_description($pr->{'path'}) || "";
4231 $descr = to_utf8($descr);
4232 $pr->{'descr_long'} = $descr;
4233 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4235 if (!defined $pr->{'owner'}) {
4236 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4238 if ($check_forks) {
4239 my $pname = $pr->{'path'};
4240 if (($pname =~ s/\.git$//) &&
4241 ($pname !~ /\/$/) &&
4242 (-d "$projectroot/$pname")) {
4243 $pr->{'forks'} = "-d $projectroot/$pname";
4244 } else {
4245 $pr->{'forks'} = 0;
4248 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4249 push @projects, $pr;
4252 return @projects;
4255 # print 'sort by' <th> element, generating 'sort by $name' replay link
4256 # if that order is not selected
4257 sub print_sort_th {
4258 my ($name, $order, $header) = @_;
4259 $header ||= ucfirst($name);
4261 if ($order eq $name) {
4262 print "<th>$header</th>\n";
4263 } else {
4264 print "<th>" .
4265 $cgi->a({-href => href(-replay=>1, order=>$name),
4266 -class => "header"}, $header) .
4267 "</th>\n";
4271 sub git_project_list_body {
4272 # actually uses global variable $project
4273 my ($projlist, $order, $from, $to, $extra, $no_header, $cache_lifetime) = @_;
4275 my $check_forks = gitweb_check_feature('forks');
4277 use File::stat;
4278 use POSIX qw(:fcntl_h);
4279 use Storable qw(store_fd retrieve);
4281 my $cache_file = "$cache_dir/$projlist_cache_name";
4283 my @projects;
4284 my $stale = 0;
4285 my $now = time();
4286 my $cache_mtime;
4287 if ($cache_lifetime && -f $cache_file) {
4288 $cache_mtime = stat($cache_file)->mtime;
4290 if (defined $cache_mtime && # caching is on and $cache_file exists
4291 $cache_mtime + $cache_lifetime*60 > $now &&
4292 (my $dump = retrieve($cache_file))) {
4293 $stale = $now - $cache_mtime;
4294 @projects = @$dump;
4295 } else {
4296 if (defined $cache_mtime) {
4297 # Postpone timeout by two minutes so that we get
4298 # enough time to do our job, or to be more exact
4299 # make cache expire after two minutes from now.
4300 my $time = $now - $cache_lifetime*60 + 120;
4301 utime $time, $time, $cache_file;
4303 @projects = fill_project_list_info($projlist, $check_forks);
4304 if ($cache_lifetime &&
4305 (-d $cache_dir || mkdir($cache_dir, 0700)) &&
4306 sysopen(my $fd, "$cache_file.lock", O_WRONLY|O_CREAT|O_EXCL, 0600)) {
4307 store_fd(\@projects, $fd);
4308 close $fd;
4309 rename "$cache_file.lock", $cache_file;
4313 $order ||= $default_projects_order;
4314 $from = 0 unless defined $from;
4315 $to = $#projects if (!defined $to || $#projects < $to);
4317 if ($cache_lifetime && $stale > 0) {
4318 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n";
4321 my %order_info = (
4322 project => { key => 'path', type => 'str' },
4323 descr => { key => 'descr_long', type => 'str' },
4324 owner => { key => 'owner', type => 'str' },
4325 age => { key => 'age', type => 'num' }
4327 my $oi = $order_info{$order};
4328 if ($oi->{'type'} eq 'str') {
4329 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4330 } else {
4331 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4334 my $show_ctags = gitweb_check_feature('ctags');
4335 if ($show_ctags) {
4336 my %ctags;
4337 foreach my $p (@projects) {
4338 foreach my $ct (keys %{$p->{'ctags'}}) {
4339 $ctags{$ct} += $p->{'ctags'}->{$ct};
4342 my $cloud = git_populate_project_tagcloud(\%ctags);
4343 print git_show_project_tagcloud($cloud, 64);
4346 print "<table class=\"project_list\">\n";
4347 unless ($no_header) {
4348 print "<tr>\n";
4349 if ($check_forks) {
4350 print "<th></th>\n";
4352 print_sort_th('project', $order, 'Project');
4353 print_sort_th('descr', $order, 'Description');
4354 print_sort_th('owner', $order, 'Owner');
4355 print_sort_th('age', $order, 'Last Change');
4356 print "<th></th>\n" . # for links
4357 "</tr>\n";
4359 my $alternate = 1;
4360 my $tagfilter = $cgi->param('by_tag');
4361 for (my $i = $from; $i <= $to; $i++) {
4362 my $pr = $projects[$i];
4364 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4365 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4366 and not $pr->{'descr_long'} =~ /$searchtext/;
4367 # Weed out forks or non-matching entries of search
4368 if ($check_forks) {
4369 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4370 $forkbase="^$forkbase" if $forkbase;
4371 next if not $searchtext and not $tagfilter and $show_ctags
4372 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4375 if ($alternate) {
4376 print "<tr class=\"dark\">\n";
4377 } else {
4378 print "<tr class=\"light\">\n";
4380 $alternate ^= 1;
4381 if ($check_forks) {
4382 print "<td>";
4383 if ($pr->{'forks'}) {
4384 print "<!-- $pr->{'forks'} -->\n";
4385 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4387 print "</td>\n";
4389 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4390 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4391 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4392 -class => "list", -title => $pr->{'descr_long'}},
4393 esc_html($pr->{'descr'})) . "</td>\n" .
4394 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4395 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4396 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4397 "<td class=\"link\">" .
4398 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4399 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4400 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4401 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4402 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4403 "</td>\n" .
4404 "</tr>\n";
4406 if (defined $extra) {
4407 print "<tr>\n";
4408 if ($check_forks) {
4409 print "<td></td>\n";
4411 print "<td colspan=\"5\">$extra</td>\n" .
4412 "</tr>\n";
4414 print "</table>\n";
4417 sub git_shortlog_body {
4418 # uses global variable $project
4419 my ($commitlist, $from, $to, $refs, $extra) = @_;
4421 $from = 0 unless defined $from;
4422 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4424 print "<table class=\"shortlog\">\n";
4425 my $alternate = 1;
4426 for (my $i = $from; $i <= $to; $i++) {
4427 my %co = %{$commitlist->[$i]};
4428 my $commit = $co{'id'};
4429 my $ref = format_ref_marker($refs, $commit);
4430 if ($alternate) {
4431 print "<tr class=\"dark\">\n";
4432 } else {
4433 print "<tr class=\"light\">\n";
4435 $alternate ^= 1;
4436 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4437 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4438 format_author_html('td', \%co, 10) . "<td>";
4439 print format_subject_html($co{'title'}, $co{'title_short'},
4440 href(action=>"commit", hash=>$commit), $ref);
4441 print "</td>\n" .
4442 "<td class=\"link\">" .
4443 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4444 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4445 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4446 my $snapshot_links = format_snapshot_links($commit);
4447 if (defined $snapshot_links) {
4448 print " | " . $snapshot_links;
4450 print "</td>\n" .
4451 "</tr>\n";
4453 if (defined $extra) {
4454 print "<tr>\n" .
4455 "<td colspan=\"4\">$extra</td>\n" .
4456 "</tr>\n";
4458 print "</table>\n";
4461 sub git_history_body {
4462 # Warning: assumes constant type (blob or tree) during history
4463 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4465 $from = 0 unless defined $from;
4466 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4468 print "<table class=\"history\">\n";
4469 my $alternate = 1;
4470 for (my $i = $from; $i <= $to; $i++) {
4471 my %co = %{$commitlist->[$i]};
4472 if (!%co) {
4473 next;
4475 my $commit = $co{'id'};
4477 my $ref = format_ref_marker($refs, $commit);
4479 if ($alternate) {
4480 print "<tr class=\"dark\">\n";
4481 } else {
4482 print "<tr class=\"light\">\n";
4484 $alternate ^= 1;
4485 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4486 # shortlog: format_author_html('td', \%co, 10)
4487 format_author_html('td', \%co, 15, 3) . "<td>";
4488 # originally git_history used chop_str($co{'title'}, 50)
4489 print format_subject_html($co{'title'}, $co{'title_short'},
4490 href(action=>"commit", hash=>$commit), $ref);
4491 print "</td>\n" .
4492 "<td class=\"link\">" .
4493 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4494 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4496 if ($ftype eq 'blob') {
4497 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4498 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4499 if (defined $blob_current && defined $blob_parent &&
4500 $blob_current ne $blob_parent) {
4501 print " | " .
4502 $cgi->a({-href => href(action=>"blobdiff",
4503 hash=>$blob_current, hash_parent=>$blob_parent,
4504 hash_base=>$hash_base, hash_parent_base=>$commit,
4505 file_name=>$file_name)},
4506 "diff to current");
4509 print "</td>\n" .
4510 "</tr>\n";
4512 if (defined $extra) {
4513 print "<tr>\n" .
4514 "<td colspan=\"4\">$extra</td>\n" .
4515 "</tr>\n";
4517 print "</table>\n";
4520 sub git_tags_body {
4521 # uses global variable $project
4522 my ($taglist, $from, $to, $extra) = @_;
4523 $from = 0 unless defined $from;
4524 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4526 print "<table class=\"tags\">\n";
4527 my $alternate = 1;
4528 for (my $i = $from; $i <= $to; $i++) {
4529 my $entry = $taglist->[$i];
4530 my %tag = %$entry;
4531 my $comment = $tag{'subject'};
4532 my $comment_short;
4533 if (defined $comment) {
4534 $comment_short = chop_str($comment, 30, 5);
4536 if ($alternate) {
4537 print "<tr class=\"dark\">\n";
4538 } else {
4539 print "<tr class=\"light\">\n";
4541 $alternate ^= 1;
4542 if (defined $tag{'age'}) {
4543 print "<td><i>$tag{'age'}</i></td>\n";
4544 } else {
4545 print "<td></td>\n";
4547 print "<td>" .
4548 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4549 -class => "list name"}, esc_html($tag{'name'})) .
4550 "</td>\n" .
4551 "<td>";
4552 if (defined $comment) {
4553 print format_subject_html($comment, $comment_short,
4554 href(action=>"tag", hash=>$tag{'id'}));
4556 print "</td>\n" .
4557 "<td class=\"selflink\">";
4558 if ($tag{'type'} eq "tag") {
4559 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4560 } else {
4561 print "&nbsp;";
4563 print "</td>\n" .
4564 "<td class=\"link\">" . " | " .
4565 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4566 if ($tag{'reftype'} eq "commit") {
4567 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4568 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4569 } elsif ($tag{'reftype'} eq "blob") {
4570 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4572 print "</td>\n" .
4573 "</tr>";
4575 if (defined $extra) {
4576 print "<tr>\n" .
4577 "<td colspan=\"5\">$extra</td>\n" .
4578 "</tr>\n";
4580 print "</table>\n";
4583 sub git_heads_body {
4584 # uses global variable $project
4585 my ($headlist, $head, $from, $to, $extra) = @_;
4586 $from = 0 unless defined $from;
4587 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4589 print "<table class=\"heads\">\n";
4590 my $alternate = 1;
4591 for (my $i = $from; $i <= $to; $i++) {
4592 my $entry = $headlist->[$i];
4593 my %ref = %$entry;
4594 my $curr = $ref{'id'} eq $head;
4595 if ($alternate) {
4596 print "<tr class=\"dark\">\n";
4597 } else {
4598 print "<tr class=\"light\">\n";
4600 $alternate ^= 1;
4601 print "<td><i>$ref{'age'}</i></td>\n" .
4602 ($curr ? "<td class=\"current_head\">" : "<td>") .
4603 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4604 -class => "list name"},esc_html($ref{'name'})) .
4605 "</td>\n" .
4606 "<td class=\"link\">" .
4607 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4608 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4609 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4610 "</td>\n" .
4611 "</tr>";
4613 if (defined $extra) {
4614 print "<tr>\n" .
4615 "<td colspan=\"3\">$extra</td>\n" .
4616 "</tr>\n";
4618 print "</table>\n";
4621 sub git_search_grep_body {
4622 my ($commitlist, $from, $to, $extra) = @_;
4623 $from = 0 unless defined $from;
4624 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4626 print "<table class=\"commit_search\">\n";
4627 my $alternate = 1;
4628 for (my $i = $from; $i <= $to; $i++) {
4629 my %co = %{$commitlist->[$i]};
4630 if (!%co) {
4631 next;
4633 my $commit = $co{'id'};
4634 if ($alternate) {
4635 print "<tr class=\"dark\">\n";
4636 } else {
4637 print "<tr class=\"light\">\n";
4639 $alternate ^= 1;
4640 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4641 format_author_html('td', \%co, 15, 5) .
4642 "<td>" .
4643 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4644 -class => "list subject"},
4645 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4646 my $comment = $co{'comment'};
4647 foreach my $line (@$comment) {
4648 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4649 my ($lead, $match, $trail) = ($1, $2, $3);
4650 $match = chop_str($match, 70, 5, 'center');
4651 my $contextlen = int((80 - length($match))/2);
4652 $contextlen = 30 if ($contextlen > 30);
4653 $lead = chop_str($lead, $contextlen, 10, 'left');
4654 $trail = chop_str($trail, $contextlen, 10, 'right');
4656 $lead = esc_html($lead);
4657 $match = esc_html($match);
4658 $trail = esc_html($trail);
4660 print "$lead<span class=\"match\">$match</span>$trail<br />";
4663 print "</td>\n" .
4664 "<td class=\"link\">" .
4665 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4666 " | " .
4667 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4668 " | " .
4669 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4670 print "</td>\n" .
4671 "</tr>\n";
4673 if (defined $extra) {
4674 print "<tr>\n" .
4675 "<td colspan=\"3\">$extra</td>\n" .
4676 "</tr>\n";
4678 print "</table>\n";
4681 ## ======================================================================
4682 ## ======================================================================
4683 ## actions
4685 sub git_project_list {
4686 my $order = $input_params{'order'};
4687 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4688 die_error(400, "Unknown order parameter");
4691 my @list = git_get_projects_list();
4692 if (!@list) {
4693 die_error(404, "No projects found");
4696 git_header_html();
4697 if (-f $home_text) {
4698 print "<div class=\"index_include\">\n";
4699 insert_file($home_text);
4700 print "</div>\n";
4702 print $cgi->startform(-method => "get") .
4703 "<p class=\"projsearch\">Search:\n" .
4704 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4705 "</p>" .
4706 $cgi->end_form() . "\n";
4707 git_project_list_body(\@list, $order, undef, undef, undef, undef, $projlist_cache_lifetime);
4708 git_footer_html();
4711 sub git_forks {
4712 my $order = $input_params{'order'};
4713 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4714 die_error(400, "Unknown order parameter");
4717 my @list = git_get_projects_list($project);
4718 if (!@list) {
4719 die_error(404, "No forks found");
4722 git_header_html();
4723 git_print_page_nav('','');
4724 git_print_header_div('summary', "$project forks");
4725 git_project_list_body(\@list, $order);
4726 git_footer_html();
4729 sub git_project_index {
4730 my @projects = git_get_projects_list($project);
4732 print $cgi->header(
4733 -type => 'text/plain',
4734 -charset => 'utf-8',
4735 -content_disposition => 'inline; filename="index.aux"');
4737 foreach my $pr (@projects) {
4738 if (!exists $pr->{'owner'}) {
4739 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4742 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4743 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4744 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4745 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4746 $path =~ s/ /\+/g;
4747 $owner =~ s/ /\+/g;
4749 print "$path $owner\n";
4753 sub git_summary {
4754 my $descr = git_get_project_description($project) || "none";
4755 my %co = parse_commit("HEAD");
4756 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4757 my $head = $co{'id'};
4759 my $owner = git_get_project_owner($project);
4761 my $refs = git_get_references();
4762 # These get_*_list functions return one more to allow us to see if
4763 # there are more ...
4764 my @taglist = git_get_tags_list(16);
4765 my @headlist = git_get_heads_list(16);
4766 my @forklist;
4767 my $check_forks = gitweb_check_feature('forks');
4769 if ($check_forks) {
4770 @forklist = git_get_projects_list($project);
4773 git_header_html();
4774 git_print_page_nav('summary','', $head);
4776 print "<div class=\"title\">&nbsp;</div>\n";
4777 print "<table class=\"projects_list\">\n" .
4778 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4779 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4780 if (defined $cd{'rfc2822'}) {
4781 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4784 # use per project git URL list in $projectroot/$project/cloneurl
4785 # or make project git URL from git base URL and project name
4786 my $url_tag = "URL";
4787 my @url_list = git_get_project_url_list($project);
4788 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4789 foreach my $git_url (@url_list) {
4790 next unless $git_url;
4791 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4792 $url_tag = "";
4795 # Tag cloud
4796 my $show_ctags = gitweb_check_feature('ctags');
4797 if ($show_ctags) {
4798 my $ctags = git_get_project_ctags($project);
4799 my $cloud = git_populate_project_tagcloud($ctags);
4800 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4801 print "</td>\n<td>" unless %$ctags;
4802 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4803 print "</td>\n<td>" if %$ctags;
4804 print git_show_project_tagcloud($cloud, 48);
4805 print "</td></tr>";
4808 print "</table>\n";
4810 # If XSS prevention is on, we don't include README.html.
4811 # TODO: Allow a readme in some safe format.
4812 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4813 print "<div class=\"title\">readme</div>\n" .
4814 "<div class=\"readme\">\n";
4815 insert_file("$projectroot/$project/README.html");
4816 print "\n</div>\n"; # class="readme"
4819 # we need to request one more than 16 (0..15) to check if
4820 # those 16 are all
4821 my @commitlist = $head ? parse_commits($head, 17) : ();
4822 if (@commitlist) {
4823 git_print_header_div('shortlog');
4824 git_shortlog_body(\@commitlist, 0, 15, $refs,
4825 $#commitlist <= 15 ? undef :
4826 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4829 if (@taglist) {
4830 git_print_header_div('tags');
4831 git_tags_body(\@taglist, 0, 15,
4832 $#taglist <= 15 ? undef :
4833 $cgi->a({-href => href(action=>"tags")}, "..."));
4836 if (@headlist) {
4837 git_print_header_div('heads');
4838 git_heads_body(\@headlist, $head, 0, 15,
4839 $#headlist <= 15 ? undef :
4840 $cgi->a({-href => href(action=>"heads")}, "..."));
4843 if (@forklist) {
4844 git_print_header_div('forks');
4845 git_project_list_body(\@forklist, 'age', 0, 15,
4846 $#forklist <= 15 ? undef :
4847 $cgi->a({-href => href(action=>"forks")}, "..."),
4848 'no_header');
4851 git_footer_html();
4854 sub git_tag {
4855 my $head = git_get_head_hash($project);
4856 git_header_html();
4857 git_print_page_nav('','', $head,undef,$head);
4858 my %tag = parse_tag($hash);
4860 if (! %tag) {
4861 die_error(404, "Unknown tag object");
4864 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4865 print "<div class=\"title_text\">\n" .
4866 "<table class=\"object_header\">\n" .
4867 "<tr>\n" .
4868 "<td>object</td>\n" .
4869 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4870 $tag{'object'}) . "</td>\n" .
4871 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4872 $tag{'type'}) . "</td>\n" .
4873 "</tr>\n";
4874 if (defined($tag{'author'})) {
4875 git_print_authorship_rows(\%tag, 'author');
4877 print "</table>\n\n" .
4878 "</div>\n";
4879 print "<div class=\"page_body\">";
4880 my $comment = $tag{'comment'};
4881 foreach my $line (@$comment) {
4882 chomp $line;
4883 print esc_html($line, -nbsp=>1) . "<br/>\n";
4885 print "</div>\n";
4886 git_footer_html();
4889 sub git_blame {
4890 # permissions
4891 gitweb_check_feature('blame')
4892 or die_error(403, "Blame view not allowed");
4894 # error checking
4895 die_error(400, "No file name given") unless $file_name;
4896 $hash_base ||= git_get_head_hash($project);
4897 die_error(404, "Couldn't find base commit") unless $hash_base;
4898 my %co = parse_commit($hash_base)
4899 or die_error(404, "Commit not found");
4900 my $ftype = "blob";
4901 if (!defined $hash) {
4902 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4903 or die_error(404, "Error looking up file");
4904 } else {
4905 $ftype = git_get_type($hash);
4906 if ($ftype !~ "blob") {
4907 die_error(400, "Object is not a blob");
4911 # run git-blame --porcelain
4912 open my $fd, "-|", git_cmd(), "blame", '-p',
4913 $hash_base, '--', $file_name
4914 or die_error(500, "Open git-blame failed");
4916 # page header
4917 git_header_html();
4918 my $formats_nav =
4919 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4920 "blob") .
4921 " | " .
4922 $cgi->a({-href => href(action=>"history", -replay=>1)},
4923 "history") .
4924 " | " .
4925 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4926 "HEAD");
4927 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4928 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4929 git_print_page_path($file_name, $ftype, $hash_base);
4931 # page body
4932 my @rev_color = qw(light dark);
4933 my $num_colors = scalar(@rev_color);
4934 my $current_color = 0;
4935 my %metainfo = ();
4937 print <<HTML;
4938 <div class="page_body">
4939 <table class="blame">
4940 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4941 HTML
4942 LINE:
4943 while (my $line = <$fd>) {
4944 chomp $line;
4945 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4946 # no <lines in group> for subsequent lines in group of lines
4947 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4948 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4949 if (!exists $metainfo{$full_rev}) {
4950 $metainfo{$full_rev} = { 'nprevious' => 0 };
4952 my $meta = $metainfo{$full_rev};
4953 my $data;
4954 while ($data = <$fd>) {
4955 chomp $data;
4956 last if ($data =~ s/^\t//); # contents of line
4957 if ($data =~ /^(\S+)(?: (.*))?$/) {
4958 $meta->{$1} = $2 unless exists $meta->{$1};
4960 if ($data =~ /^previous /) {
4961 $meta->{'nprevious'}++;
4964 my $short_rev = substr($full_rev, 0, 8);
4965 my $author = $meta->{'author'};
4966 my %date =
4967 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4968 my $date = $date{'iso-tz'};
4969 if ($group_size) {
4970 $current_color = ($current_color + 1) % $num_colors;
4972 my $tr_class = $rev_color[$current_color];
4973 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4974 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4975 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4976 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4977 if ($group_size) {
4978 print "<td class=\"sha1\"";
4979 print " title=\"". esc_html($author) . ", $date\"";
4980 print " rowspan=\"$group_size\"" if ($group_size > 1);
4981 print ">";
4982 print $cgi->a({-href => href(action=>"commit",
4983 hash=>$full_rev,
4984 file_name=>$file_name)},
4985 esc_html($short_rev));
4986 if ($group_size >= 2) {
4987 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4988 if (@author_initials) {
4989 print "<br />" .
4990 esc_html(join('', @author_initials));
4991 # or join('.', ...)
4994 print "</td>\n";
4996 # 'previous' <sha1 of parent commit> <filename at commit>
4997 if (exists $meta->{'previous'} &&
4998 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4999 $meta->{'parent'} = $1;
5000 $meta->{'file_parent'} = unquote($2);
5002 my $linenr_commit =
5003 exists($meta->{'parent'}) ?
5004 $meta->{'parent'} : $full_rev;
5005 my $linenr_filename =
5006 exists($meta->{'file_parent'}) ?
5007 $meta->{'file_parent'} : unquote($meta->{'filename'});
5008 my $blamed = href(action => 'blame',
5009 file_name => $linenr_filename,
5010 hash_base => $linenr_commit);
5011 print "<td class=\"linenr\">";
5012 print $cgi->a({ -href => "$blamed#l$orig_lineno",
5013 -class => "linenr" },
5014 esc_html($lineno));
5015 print "</td>";
5016 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5017 print "</tr>\n";
5019 print "</table>\n";
5020 print "</div>";
5021 close $fd
5022 or print "Reading blob failed\n";
5024 # page footer
5025 git_footer_html();
5028 sub git_tags {
5029 my $head = git_get_head_hash($project);
5030 git_header_html();
5031 git_print_page_nav('','', $head,undef,$head);
5032 git_print_header_div('summary', $project);
5034 my @tagslist = git_get_tags_list();
5035 if (@tagslist) {
5036 git_tags_body(\@tagslist);
5038 git_footer_html();
5041 sub git_heads {
5042 my $head = git_get_head_hash($project);
5043 git_header_html();
5044 git_print_page_nav('','', $head,undef,$head);
5045 git_print_header_div('summary', $project);
5047 my @headslist = git_get_heads_list();
5048 if (@headslist) {
5049 git_heads_body(\@headslist, $head);
5051 git_footer_html();
5054 sub git_blob_plain {
5055 my $type = shift;
5056 my $expires;
5058 if (!defined $hash) {
5059 if (defined $file_name) {
5060 my $base = $hash_base || git_get_head_hash($project);
5061 $hash = git_get_hash_by_path($base, $file_name, "blob")
5062 or die_error(404, "Cannot find file");
5063 } else {
5064 die_error(400, "No file name defined");
5066 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5067 # blobs defined by non-textual hash id's can be cached
5068 $expires = "+1d";
5071 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5072 or die_error(500, "Open git-cat-file blob '$hash' failed");
5074 # content-type (can include charset)
5075 $type = blob_contenttype($fd, $file_name, $type);
5077 # "save as" filename, even when no $file_name is given
5078 my $save_as = "$hash";
5079 if (defined $file_name) {
5080 $save_as = $file_name;
5081 } elsif ($type =~ m/^text\//) {
5082 $save_as .= '.txt';
5085 # With XSS prevention on, blobs of all types except a few known safe
5086 # ones are served with "Content-Disposition: attachment" to make sure
5087 # they don't run in our security domain. For certain image types,
5088 # blob view writes an <img> tag referring to blob_plain view, and we
5089 # want to be sure not to break that by serving the image as an
5090 # attachment (though Firefox 3 doesn't seem to care).
5091 my $sandbox = $prevent_xss &&
5092 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5094 print $cgi->header(
5095 -type => $type,
5096 -expires => $expires,
5097 -content_disposition =>
5098 ($sandbox ? 'attachment' : 'inline')
5099 . '; filename="' . $save_as . '"');
5100 local $/ = undef;
5101 binmode STDOUT, ':raw';
5102 print <$fd>;
5103 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5104 close $fd;
5107 sub git_blob {
5108 my $expires;
5110 if (!defined $hash) {
5111 if (defined $file_name) {
5112 my $base = $hash_base || git_get_head_hash($project);
5113 $hash = git_get_hash_by_path($base, $file_name, "blob")
5114 or die_error(404, "Cannot find file");
5115 } else {
5116 die_error(400, "No file name defined");
5118 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5119 # blobs defined by non-textual hash id's can be cached
5120 $expires = "+1d";
5123 my $have_blame = gitweb_check_feature('blame');
5124 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5125 or die_error(500, "Couldn't cat $file_name, $hash");
5126 my $mimetype = blob_mimetype($fd, $file_name);
5127 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5128 close $fd;
5129 return git_blob_plain($mimetype);
5131 # we can have blame only for text/* mimetype
5132 $have_blame &&= ($mimetype =~ m!^text/!);
5134 git_header_html(undef, $expires);
5135 my $formats_nav = '';
5136 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5137 if (defined $file_name) {
5138 if ($have_blame) {
5139 $formats_nav .=
5140 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5141 "blame") .
5142 " | ";
5144 $formats_nav .=
5145 $cgi->a({-href => href(action=>"history", -replay=>1)},
5146 "history") .
5147 " | " .
5148 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5149 "raw") .
5150 " | " .
5151 $cgi->a({-href => href(action=>"blob",
5152 hash_base=>"HEAD", file_name=>$file_name)},
5153 "HEAD");
5154 } else {
5155 $formats_nav .=
5156 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5157 "raw");
5159 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5160 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5161 } else {
5162 print "<div class=\"page_nav\">\n" .
5163 "<br/><br/></div>\n" .
5164 "<div class=\"title\">$hash</div>\n";
5166 git_print_page_path($file_name, "blob", $hash_base);
5167 print "<div class=\"page_body\">\n";
5168 if ($mimetype =~ m!^image/!) {
5169 print qq!<img type="$mimetype"!;
5170 if ($file_name) {
5171 print qq! alt="$file_name" title="$file_name"!;
5173 print qq! src="! .
5174 href(action=>"blob_plain", hash=>$hash,
5175 hash_base=>$hash_base, file_name=>$file_name) .
5176 qq!" />\n!;
5177 } else {
5178 my $nr;
5179 while (my $line = <$fd>) {
5180 chomp $line;
5181 $nr++;
5182 $line = untabify($line);
5183 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5184 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5187 close $fd
5188 or print "Reading blob failed.\n";
5189 print "</div>";
5190 git_footer_html();
5193 sub git_tree {
5194 if (!defined $hash_base) {
5195 $hash_base = "HEAD";
5197 if (!defined $hash) {
5198 if (defined $file_name) {
5199 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5200 } else {
5201 $hash = $hash_base;
5204 die_error(404, "No such tree") unless defined($hash);
5206 my $show_sizes = gitweb_check_feature('show-sizes');
5207 my $have_blame = gitweb_check_feature('blame');
5209 my @entries = ();
5211 local $/ = "\0";
5212 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5213 ($show_sizes ? '-l' : ()), @extra_options, $hash
5214 or die_error(500, "Open git-ls-tree failed");
5215 @entries = map { chomp; $_ } <$fd>;
5216 close $fd
5217 or die_error(404, "Reading tree failed");
5220 my $refs = git_get_references();
5221 my $ref = format_ref_marker($refs, $hash_base);
5222 git_header_html();
5223 my $basedir = '';
5224 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5225 my @views_nav = ();
5226 if (defined $file_name) {
5227 push @views_nav,
5228 $cgi->a({-href => href(action=>"history", -replay=>1)},
5229 "history"),
5230 $cgi->a({-href => href(action=>"tree",
5231 hash_base=>"HEAD", file_name=>$file_name)},
5232 "HEAD"),
5234 my $snapshot_links = format_snapshot_links($hash);
5235 if (defined $snapshot_links) {
5236 # FIXME: Should be available when we have no hash base as well.
5237 push @views_nav, $snapshot_links;
5239 git_print_page_nav('tree','', $hash_base, undef, undef,
5240 join(' | ', @views_nav));
5241 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5242 } else {
5243 undef $hash_base;
5244 print "<div class=\"page_nav\">\n";
5245 print "<br/><br/></div>\n";
5246 print "<div class=\"title\">$hash</div>\n";
5248 if (defined $file_name) {
5249 $basedir = $file_name;
5250 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5251 $basedir .= '/';
5253 git_print_page_path($file_name, 'tree', $hash_base);
5255 print "<div class=\"page_body\">\n";
5256 print "<table class=\"tree\">\n";
5257 my $alternate = 1;
5258 # '..' (top directory) link if possible
5259 if (defined $hash_base &&
5260 defined $file_name && $file_name =~ m![^/]+$!) {
5261 if ($alternate) {
5262 print "<tr class=\"dark\">\n";
5263 } else {
5264 print "<tr class=\"light\">\n";
5266 $alternate ^= 1;
5268 my $up = $file_name;
5269 $up =~ s!/?[^/]+$!!;
5270 undef $up unless $up;
5271 # based on git_print_tree_entry
5272 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5273 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
5274 print '<td class="list">';
5275 print $cgi->a({-href => href(action=>"tree",
5276 hash_base=>$hash_base,
5277 file_name=>$up)},
5278 "..");
5279 print "</td>\n";
5280 print "<td class=\"link\"></td>\n";
5282 print "</tr>\n";
5284 foreach my $line (@entries) {
5285 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5287 if ($alternate) {
5288 print "<tr class=\"dark\">\n";
5289 } else {
5290 print "<tr class=\"light\">\n";
5292 $alternate ^= 1;
5294 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5296 print "</tr>\n";
5298 print "</table>\n" .
5299 "</div>";
5300 git_footer_html();
5303 sub git_snapshot {
5304 my $format = $input_params{'snapshot_format'};
5305 if (!@snapshot_fmts) {
5306 die_error(403, "Snapshots not allowed");
5308 # default to first supported snapshot format
5309 $format ||= $snapshot_fmts[0];
5310 if ($format !~ m/^[a-z0-9]+$/) {
5311 die_error(400, "Invalid snapshot format parameter");
5312 } elsif (!exists($known_snapshot_formats{$format})) {
5313 die_error(400, "Unknown snapshot format");
5314 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5315 die_error(403, "Snapshot format not allowed");
5316 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5317 die_error(403, "Unsupported snapshot format");
5320 if (!defined $hash) {
5321 $hash = git_get_head_hash($project);
5324 my $name = $project;
5325 $name =~ s,([^/])/*\.git$,$1,;
5326 $name = basename($name);
5327 my $filename = to_utf8($name);
5328 $name =~ s/\047/\047\\\047\047/g;
5329 my $cmd;
5330 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5331 $cmd = quote_command(
5332 git_cmd(), 'archive',
5333 "--format=$known_snapshot_formats{$format}{'format'}",
5334 "--prefix=$name/", $hash);
5335 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5336 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5339 print $cgi->header(
5340 -type => $known_snapshot_formats{$format}{'type'},
5341 -content_disposition => 'inline; filename="' . "$filename" . '"',
5342 -status => '200 OK');
5344 open my $fd, "-|", $cmd
5345 or die_error(500, "Execute git-archive failed");
5346 binmode STDOUT, ':raw';
5347 print <$fd>;
5348 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5349 close $fd;
5352 sub git_log {
5353 my $head = git_get_head_hash($project);
5354 if (!defined $hash) {
5355 $hash = $head;
5357 if (!defined $page) {
5358 $page = 0;
5360 my $refs = git_get_references();
5362 my @commitlist = parse_commits($hash, 101, (100 * $page));
5364 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5366 my ($patch_max) = gitweb_get_feature('patches');
5367 if ($patch_max) {
5368 if ($patch_max < 0 || @commitlist <= $patch_max) {
5369 $paging_nav .= " &sdot; " .
5370 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5371 "patches");
5375 git_header_html();
5376 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5378 if (!@commitlist) {
5379 my %co = parse_commit($hash);
5381 git_print_header_div('summary', $project);
5382 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5384 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5385 for (my $i = 0; $i <= $to; $i++) {
5386 my %co = %{$commitlist[$i]};
5387 next if !%co;
5388 my $commit = $co{'id'};
5389 my $ref = format_ref_marker($refs, $commit);
5390 my %ad = parse_date($co{'author_epoch'});
5391 git_print_header_div('commit',
5392 "<span class=\"age\">$co{'age_string'}</span>" .
5393 esc_html($co{'title'}) . $ref,
5394 $commit);
5395 print "<div class=\"title_text\">\n" .
5396 "<div class=\"log_link\">\n" .
5397 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5398 " | " .
5399 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5400 " | " .
5401 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5402 "<br/>\n" .
5403 "</div>\n";
5404 git_print_authorship(\%co, -tag => 'span');
5405 print "<br/>\n</div>\n";
5407 print "<div class=\"log_body\">\n";
5408 git_print_log($co{'comment'}, -final_empty_line=> 1);
5409 print "</div>\n";
5411 if ($#commitlist >= 100) {
5412 print "<div class=\"page_nav\">\n";
5413 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5414 -accesskey => "n", -title => "Alt-n"}, "next");
5415 print "</div>\n";
5417 git_footer_html();
5420 sub git_commit {
5421 $hash ||= $hash_base || "HEAD";
5422 my %co = parse_commit($hash)
5423 or die_error(404, "Unknown commit object");
5425 my $parent = $co{'parent'};
5426 my $parents = $co{'parents'}; # listref
5428 # we need to prepare $formats_nav before any parameter munging
5429 my $formats_nav;
5430 if (!defined $parent) {
5431 # --root commitdiff
5432 $formats_nav .= '(initial)';
5433 } elsif (@$parents == 1) {
5434 # single parent commit
5435 $formats_nav .=
5436 '(parent: ' .
5437 $cgi->a({-href => href(action=>"commit",
5438 hash=>$parent)},
5439 esc_html(substr($parent, 0, 7))) .
5440 ')';
5441 } else {
5442 # merge commit
5443 $formats_nav .=
5444 '(merge: ' .
5445 join(' ', map {
5446 $cgi->a({-href => href(action=>"commit",
5447 hash=>$_)},
5448 esc_html(substr($_, 0, 7)));
5449 } @$parents ) .
5450 ')';
5452 if (gitweb_check_feature('patches') && @$parents <= 1) {
5453 $formats_nav .= " | " .
5454 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5455 "patch");
5458 if (!defined $parent) {
5459 $parent = "--root";
5461 my @difftree;
5462 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5463 @diff_opts,
5464 (@$parents <= 1 ? $parent : '-c'),
5465 $hash, "--"
5466 or die_error(500, "Open git-diff-tree failed");
5467 @difftree = map { chomp; $_ } <$fd>;
5468 close $fd or die_error(404, "Reading git-diff-tree failed");
5470 # non-textual hash id's can be cached
5471 my $expires;
5472 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5473 $expires = "+1d";
5475 my $refs = git_get_references();
5476 my $ref = format_ref_marker($refs, $co{'id'});
5478 git_header_html(undef, $expires);
5479 git_print_page_nav('commit', '',
5480 $hash, $co{'tree'}, $hash,
5481 $formats_nav);
5483 if (defined $co{'parent'}) {
5484 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5485 } else {
5486 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5488 print "<div class=\"title_text\">\n" .
5489 "<table class=\"object_header\">\n";
5490 git_print_authorship_rows(\%co);
5491 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5492 print "<tr>" .
5493 "<td>tree</td>" .
5494 "<td class=\"sha1\">" .
5495 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5496 class => "list"}, $co{'tree'}) .
5497 "</td>" .
5498 "<td class=\"link\">" .
5499 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5500 "tree");
5501 my $snapshot_links = format_snapshot_links($hash);
5502 if (defined $snapshot_links) {
5503 print " | " . $snapshot_links;
5505 print "</td>" .
5506 "</tr>\n";
5508 foreach my $par (@$parents) {
5509 print "<tr>" .
5510 "<td>parent</td>" .
5511 "<td class=\"sha1\">" .
5512 $cgi->a({-href => href(action=>"commit", hash=>$par),
5513 class => "list"}, $par) .
5514 "</td>" .
5515 "<td class=\"link\">" .
5516 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5517 " | " .
5518 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5519 "</td>" .
5520 "</tr>\n";
5522 print "</table>".
5523 "</div>\n";
5525 print "<div class=\"page_body\">\n";
5526 git_print_log($co{'comment'});
5527 print "</div>\n";
5529 git_difftree_body(\@difftree, $hash, @$parents);
5531 git_footer_html();
5534 sub git_object {
5535 # object is defined by:
5536 # - hash or hash_base alone
5537 # - hash_base and file_name
5538 my $type;
5540 # - hash or hash_base alone
5541 if ($hash || ($hash_base && !defined $file_name)) {
5542 my $object_id = $hash || $hash_base;
5544 open my $fd, "-|", quote_command(
5545 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5546 or die_error(404, "Object does not exist");
5547 $type = <$fd>;
5548 chomp $type;
5549 close $fd
5550 or die_error(404, "Object does not exist");
5552 # - hash_base and file_name
5553 } elsif ($hash_base && defined $file_name) {
5554 $file_name =~ s,/+$,,;
5556 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5557 or die_error(404, "Base object does not exist");
5559 # here errors should not hapen
5560 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5561 or die_error(500, "Open git-ls-tree failed");
5562 my $line = <$fd>;
5563 close $fd;
5565 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5566 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5567 die_error(404, "File or directory for given base does not exist");
5569 $type = $2;
5570 $hash = $3;
5571 } else {
5572 die_error(400, "Not enough information to find object");
5575 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5576 hash=>$hash, hash_base=>$hash_base,
5577 file_name=>$file_name),
5578 -status => '302 Found');
5581 sub git_blobdiff {
5582 my $format = shift || 'html';
5584 my $fd;
5585 my @difftree;
5586 my %diffinfo;
5587 my $expires;
5589 # preparing $fd and %diffinfo for git_patchset_body
5590 # new style URI
5591 if (defined $hash_base && defined $hash_parent_base) {
5592 if (defined $file_name) {
5593 # read raw output
5594 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5595 $hash_parent_base, $hash_base,
5596 "--", (defined $file_parent ? $file_parent : ()), $file_name
5597 or die_error(500, "Open git-diff-tree failed");
5598 @difftree = map { chomp; $_ } <$fd>;
5599 close $fd
5600 or die_error(404, "Reading git-diff-tree failed");
5601 @difftree
5602 or die_error(404, "Blob diff not found");
5604 } elsif (defined $hash &&
5605 $hash =~ /[0-9a-fA-F]{40}/) {
5606 # try to find filename from $hash
5608 # read filtered raw output
5609 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5610 $hash_parent_base, $hash_base, "--"
5611 or die_error(500, "Open git-diff-tree failed");
5612 @difftree =
5613 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5614 # $hash == to_id
5615 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5616 map { chomp; $_ } <$fd>;
5617 close $fd
5618 or die_error(404, "Reading git-diff-tree failed");
5619 @difftree
5620 or die_error(404, "Blob diff not found");
5622 } else {
5623 die_error(400, "Missing one of the blob diff parameters");
5626 if (@difftree > 1) {
5627 die_error(400, "Ambiguous blob diff specification");
5630 %diffinfo = parse_difftree_raw_line($difftree[0]);
5631 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5632 $file_name ||= $diffinfo{'to_file'};
5634 $hash_parent ||= $diffinfo{'from_id'};
5635 $hash ||= $diffinfo{'to_id'};
5637 # non-textual hash id's can be cached
5638 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5639 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5640 $expires = '+1d';
5643 # open patch output
5644 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5645 '-p', ($format eq 'html' ? "--full-index" : ()),
5646 $hash_parent_base, $hash_base,
5647 "--", (defined $file_parent ? $file_parent : ()), $file_name
5648 or die_error(500, "Open git-diff-tree failed");
5651 # old/legacy style URI -- not generated anymore since 1.4.3.
5652 if (!%diffinfo) {
5653 die_error('404 Not Found', "Missing one of the blob diff parameters")
5656 # header
5657 if ($format eq 'html') {
5658 my $formats_nav =
5659 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5660 "raw");
5661 git_header_html(undef, $expires);
5662 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5663 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5664 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5665 } else {
5666 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5667 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5669 if (defined $file_name) {
5670 git_print_page_path($file_name, "blob", $hash_base);
5671 } else {
5672 print "<div class=\"page_path\"></div>\n";
5675 } elsif ($format eq 'plain') {
5676 print $cgi->header(
5677 -type => 'text/plain',
5678 -charset => 'utf-8',
5679 -expires => $expires,
5680 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5682 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5684 } else {
5685 die_error(400, "Unknown blobdiff format");
5688 # patch
5689 if ($format eq 'html') {
5690 print "<div class=\"page_body\">\n";
5692 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5693 close $fd;
5695 print "</div>\n"; # class="page_body"
5696 git_footer_html();
5698 } else {
5699 while (my $line = <$fd>) {
5700 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5701 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5703 print $line;
5705 last if $line =~ m!^\+\+\+!;
5707 local $/ = undef;
5708 print <$fd>;
5709 close $fd;
5713 sub git_blobdiff_plain {
5714 git_blobdiff('plain');
5717 sub git_commitdiff {
5718 my %params = @_;
5719 my $format = $params{-format} || 'html';
5721 my ($patch_max) = gitweb_get_feature('patches');
5722 if ($format eq 'patch') {
5723 die_error(403, "Patch view not allowed") unless $patch_max;
5726 $hash ||= $hash_base || "HEAD";
5727 my %co = parse_commit($hash)
5728 or die_error(404, "Unknown commit object");
5730 # choose format for commitdiff for merge
5731 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5732 $hash_parent = '--cc';
5734 # we need to prepare $formats_nav before almost any parameter munging
5735 my $formats_nav;
5736 if ($format eq 'html') {
5737 $formats_nav =
5738 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5739 "raw");
5740 if ($patch_max && @{$co{'parents'}} <= 1) {
5741 $formats_nav .= " | " .
5742 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5743 "patch");
5746 if (defined $hash_parent &&
5747 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5748 # commitdiff with two commits given
5749 my $hash_parent_short = $hash_parent;
5750 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5751 $hash_parent_short = substr($hash_parent, 0, 7);
5753 $formats_nav .=
5754 ' (from';
5755 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5756 if ($co{'parents'}[$i] eq $hash_parent) {
5757 $formats_nav .= ' parent ' . ($i+1);
5758 last;
5761 $formats_nav .= ': ' .
5762 $cgi->a({-href => href(action=>"commitdiff",
5763 hash=>$hash_parent)},
5764 esc_html($hash_parent_short)) .
5765 ')';
5766 } elsif (!$co{'parent'}) {
5767 # --root commitdiff
5768 $formats_nav .= ' (initial)';
5769 } elsif (scalar @{$co{'parents'}} == 1) {
5770 # single parent commit
5771 $formats_nav .=
5772 ' (parent: ' .
5773 $cgi->a({-href => href(action=>"commitdiff",
5774 hash=>$co{'parent'})},
5775 esc_html(substr($co{'parent'}, 0, 7))) .
5776 ')';
5777 } else {
5778 # merge commit
5779 if ($hash_parent eq '--cc') {
5780 $formats_nav .= ' | ' .
5781 $cgi->a({-href => href(action=>"commitdiff",
5782 hash=>$hash, hash_parent=>'-c')},
5783 'combined');
5784 } else { # $hash_parent eq '-c'
5785 $formats_nav .= ' | ' .
5786 $cgi->a({-href => href(action=>"commitdiff",
5787 hash=>$hash, hash_parent=>'--cc')},
5788 'compact');
5790 $formats_nav .=
5791 ' (merge: ' .
5792 join(' ', map {
5793 $cgi->a({-href => href(action=>"commitdiff",
5794 hash=>$_)},
5795 esc_html(substr($_, 0, 7)));
5796 } @{$co{'parents'}} ) .
5797 ')';
5801 my $hash_parent_param = $hash_parent;
5802 if (!defined $hash_parent_param) {
5803 # --cc for multiple parents, --root for parentless
5804 $hash_parent_param =
5805 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5808 # read commitdiff
5809 my $fd;
5810 my @difftree;
5811 if ($format eq 'html') {
5812 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5813 "--no-commit-id", "--patch-with-raw", "--full-index",
5814 $hash_parent_param, $hash, "--"
5815 or die_error(500, "Open git-diff-tree failed");
5817 while (my $line = <$fd>) {
5818 chomp $line;
5819 # empty line ends raw part of diff-tree output
5820 last unless $line;
5821 push @difftree, scalar parse_difftree_raw_line($line);
5824 } elsif ($format eq 'plain') {
5825 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5826 '-p', $hash_parent_param, $hash, "--"
5827 or die_error(500, "Open git-diff-tree failed");
5828 } elsif ($format eq 'patch') {
5829 # For commit ranges, we limit the output to the number of
5830 # patches specified in the 'patches' feature.
5831 # For single commits, we limit the output to a single patch,
5832 # diverging from the git-format-patch default.
5833 my @commit_spec = ();
5834 if ($hash_parent) {
5835 if ($patch_max > 0) {
5836 push @commit_spec, "-$patch_max";
5838 push @commit_spec, '-n', "$hash_parent..$hash";
5839 } else {
5840 if ($params{-single}) {
5841 push @commit_spec, '-1';
5842 } else {
5843 if ($patch_max > 0) {
5844 push @commit_spec, "-$patch_max";
5846 push @commit_spec, "-n";
5848 push @commit_spec, '--root', $hash;
5850 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5851 '--stdout', @commit_spec
5852 or die_error(500, "Open git-format-patch failed");
5853 } else {
5854 die_error(400, "Unknown commitdiff format");
5857 # non-textual hash id's can be cached
5858 my $expires;
5859 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5860 $expires = "+1d";
5863 # write commit message
5864 if ($format eq 'html') {
5865 my $refs = git_get_references();
5866 my $ref = format_ref_marker($refs, $co{'id'});
5868 git_header_html(undef, $expires);
5869 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5870 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5871 print "<div class=\"title_text\">\n" .
5872 "<table class=\"object_header\">\n";
5873 git_print_authorship_rows(\%co);
5874 print "</table>".
5875 "</div>\n";
5876 print "<div class=\"page_body\">\n";
5877 if (@{$co{'comment'}} > 1) {
5878 print "<div class=\"log\">\n";
5879 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5880 print "</div>\n"; # class="log"
5883 } elsif ($format eq 'plain') {
5884 my $refs = git_get_references("tags");
5885 my $tagname = git_get_rev_name_tags($hash);
5886 my $filename = basename($project) . "-$hash.patch";
5888 print $cgi->header(
5889 -type => 'text/plain',
5890 -charset => 'utf-8',
5891 -expires => $expires,
5892 -content_disposition => 'inline; filename="' . "$filename" . '"');
5893 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5894 print "From: " . to_utf8($co{'author'}) . "\n";
5895 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5896 print "Subject: " . to_utf8($co{'title'}) . "\n";
5898 print "X-Git-Tag: $tagname\n" if $tagname;
5899 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5901 foreach my $line (@{$co{'comment'}}) {
5902 print to_utf8($line) . "\n";
5904 print "---\n\n";
5905 } elsif ($format eq 'patch') {
5906 my $filename = basename($project) . "-$hash.patch";
5908 print $cgi->header(
5909 -type => 'text/plain',
5910 -charset => 'utf-8',
5911 -expires => $expires,
5912 -content_disposition => 'inline; filename="' . "$filename" . '"');
5915 # write patch
5916 if ($format eq 'html') {
5917 my $use_parents = !defined $hash_parent ||
5918 $hash_parent eq '-c' || $hash_parent eq '--cc';
5919 git_difftree_body(\@difftree, $hash,
5920 $use_parents ? @{$co{'parents'}} : $hash_parent);
5921 print "<br/>\n";
5923 git_patchset_body($fd, \@difftree, $hash,
5924 $use_parents ? @{$co{'parents'}} : $hash_parent);
5925 close $fd;
5926 print "</div>\n"; # class="page_body"
5927 git_footer_html();
5929 } elsif ($format eq 'plain') {
5930 local $/ = undef;
5931 print <$fd>;
5932 close $fd
5933 or print "Reading git-diff-tree failed\n";
5934 } elsif ($format eq 'patch') {
5935 local $/ = undef;
5936 print <$fd>;
5937 close $fd
5938 or print "Reading git-format-patch failed\n";
5942 sub git_commitdiff_plain {
5943 git_commitdiff(-format => 'plain');
5946 # format-patch-style patches
5947 sub git_patch {
5948 git_commitdiff(-format => 'patch', -single => 1);
5951 sub git_patches {
5952 git_commitdiff(-format => 'patch');
5955 sub git_history {
5956 if (!defined $hash_base) {
5957 $hash_base = git_get_head_hash($project);
5959 if (!defined $page) {
5960 $page = 0;
5962 my $ftype;
5963 my %co = parse_commit($hash_base)
5964 or die_error(404, "Unknown commit object");
5966 my $refs = git_get_references();
5967 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5969 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5970 $file_name, "--full-history")
5971 or die_error(404, "No such file or directory on given branch");
5973 if (!defined $hash && defined $file_name) {
5974 # some commits could have deleted file in question,
5975 # and not have it in tree, but one of them has to have it
5976 for (my $i = 0; $i <= @commitlist; $i++) {
5977 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5978 last if defined $hash;
5981 if (defined $hash) {
5982 $ftype = git_get_type($hash);
5984 if (!defined $ftype) {
5985 die_error(500, "Unknown type of object");
5988 my $paging_nav = '';
5989 if ($page > 0) {
5990 $paging_nav .=
5991 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5992 file_name=>$file_name)},
5993 "first");
5994 $paging_nav .= " &sdot; " .
5995 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5996 -accesskey => "p", -title => "Alt-p"}, "prev");
5997 } else {
5998 $paging_nav .= "first";
5999 $paging_nav .= " &sdot; prev";
6001 my $next_link = '';
6002 if ($#commitlist >= 100) {
6003 $next_link =
6004 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6005 -accesskey => "n", -title => "Alt-n"}, "next");
6006 $paging_nav .= " &sdot; $next_link";
6007 } else {
6008 $paging_nav .= " &sdot; next";
6011 git_header_html();
6012 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
6013 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6014 git_print_page_path($file_name, $ftype, $hash_base);
6016 git_history_body(\@commitlist, 0, 99,
6017 $refs, $hash_base, $ftype, $next_link);
6019 git_footer_html();
6022 sub git_search {
6023 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6024 if (!defined $searchtext) {
6025 die_error(400, "Text field is empty");
6027 if (!defined $hash) {
6028 $hash = git_get_head_hash($project);
6030 my %co = parse_commit($hash);
6031 if (!%co) {
6032 die_error(404, "Unknown commit object");
6034 if (!defined $page) {
6035 $page = 0;
6038 $searchtype ||= 'commit';
6039 if ($searchtype eq 'pickaxe') {
6040 # pickaxe may take all resources of your box and run for several minutes
6041 # with every query - so decide by yourself how public you make this feature
6042 gitweb_check_feature('pickaxe')
6043 or die_error(403, "Pickaxe is disabled");
6045 if ($searchtype eq 'grep') {
6046 gitweb_check_feature('grep')
6047 or die_error(403, "Grep is disabled");
6050 git_header_html();
6052 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6053 my $greptype;
6054 if ($searchtype eq 'commit') {
6055 $greptype = "--grep=";
6056 } elsif ($searchtype eq 'author') {
6057 $greptype = "--author=";
6058 } elsif ($searchtype eq 'committer') {
6059 $greptype = "--committer=";
6061 $greptype .= $searchtext;
6062 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6063 $greptype, '--regexp-ignore-case',
6064 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6066 my $paging_nav = '';
6067 if ($page > 0) {
6068 $paging_nav .=
6069 $cgi->a({-href => href(action=>"search", hash=>$hash,
6070 searchtext=>$searchtext,
6071 searchtype=>$searchtype)},
6072 "first");
6073 $paging_nav .= " &sdot; " .
6074 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6075 -accesskey => "p", -title => "Alt-p"}, "prev");
6076 } else {
6077 $paging_nav .= "first";
6078 $paging_nav .= " &sdot; prev";
6080 my $next_link = '';
6081 if ($#commitlist >= 100) {
6082 $next_link =
6083 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6084 -accesskey => "n", -title => "Alt-n"}, "next");
6085 $paging_nav .= " &sdot; $next_link";
6086 } else {
6087 $paging_nav .= " &sdot; next";
6090 if ($#commitlist >= 100) {
6093 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6094 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6095 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6098 if ($searchtype eq 'pickaxe') {
6099 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6100 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6102 print "<table class=\"pickaxe search\">\n";
6103 my $alternate = 1;
6104 local $/ = "\n";
6105 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6106 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6107 ($search_use_regexp ? '--pickaxe-regex' : ());
6108 undef %co;
6109 my @files;
6110 while (my $line = <$fd>) {
6111 chomp $line;
6112 next unless $line;
6114 my %set = parse_difftree_raw_line($line);
6115 if (defined $set{'commit'}) {
6116 # finish previous commit
6117 if (%co) {
6118 print "</td>\n" .
6119 "<td class=\"link\">" .
6120 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6121 " | " .
6122 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6123 print "</td>\n" .
6124 "</tr>\n";
6127 if ($alternate) {
6128 print "<tr class=\"dark\">\n";
6129 } else {
6130 print "<tr class=\"light\">\n";
6132 $alternate ^= 1;
6133 %co = parse_commit($set{'commit'});
6134 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6135 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6136 "<td><i>$author</i></td>\n" .
6137 "<td>" .
6138 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6139 -class => "list subject"},
6140 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6141 } elsif (defined $set{'to_id'}) {
6142 next if ($set{'to_id'} =~ m/^0{40}$/);
6144 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6145 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6146 -class => "list"},
6147 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6148 "<br/>\n";
6151 close $fd;
6153 # finish last commit (warning: repetition!)
6154 if (%co) {
6155 print "</td>\n" .
6156 "<td class=\"link\">" .
6157 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6158 " | " .
6159 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6160 print "</td>\n" .
6161 "</tr>\n";
6164 print "</table>\n";
6167 if ($searchtype eq 'grep') {
6168 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6169 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6171 print "<table class=\"grep_search\">\n";
6172 my $alternate = 1;
6173 my $matches = 0;
6174 local $/ = "\n";
6175 open my $fd, "-|", git_cmd(), 'grep', '-n',
6176 $search_use_regexp ? ('-E', '-i') : '-F',
6177 $searchtext, $co{'tree'};
6178 my $lastfile = '';
6179 while (my $line = <$fd>) {
6180 chomp $line;
6181 my ($file, $lno, $ltext, $binary);
6182 last if ($matches++ > 1000);
6183 if ($line =~ /^Binary file (.+) matches$/) {
6184 $file = $1;
6185 $binary = 1;
6186 } else {
6187 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6189 if ($file ne $lastfile) {
6190 $lastfile and print "</td></tr>\n";
6191 if ($alternate++) {
6192 print "<tr class=\"dark\">\n";
6193 } else {
6194 print "<tr class=\"light\">\n";
6196 print "<td class=\"list\">".
6197 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6198 file_name=>"$file"),
6199 -class => "list"}, esc_path($file));
6200 print "</td><td>\n";
6201 $lastfile = $file;
6203 if ($binary) {
6204 print "<div class=\"binary\">Binary file</div>\n";
6205 } else {
6206 $ltext = untabify($ltext);
6207 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6208 $ltext = esc_html($1, -nbsp=>1);
6209 $ltext .= '<span class="match">';
6210 $ltext .= esc_html($2, -nbsp=>1);
6211 $ltext .= '</span>';
6212 $ltext .= esc_html($3, -nbsp=>1);
6213 } else {
6214 $ltext = esc_html($ltext, -nbsp=>1);
6216 print "<div class=\"pre\">" .
6217 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6218 file_name=>"$file").'#l'.$lno,
6219 -class => "linenr"}, sprintf('%4i', $lno))
6220 . ' ' . $ltext . "</div>\n";
6223 if ($lastfile) {
6224 print "</td></tr>\n";
6225 if ($matches > 1000) {
6226 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6228 } else {
6229 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6231 close $fd;
6233 print "</table>\n";
6235 git_footer_html();
6238 sub git_search_help {
6239 git_header_html();
6240 git_print_page_nav('','', $hash,$hash,$hash);
6241 print <<EOT;
6242 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6243 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6244 the pattern entered is recognized as the POSIX extended
6245 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6246 insensitive).</p>
6247 <dl>
6248 <dt><b>commit</b></dt>
6249 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6251 my $have_grep = gitweb_check_feature('grep');
6252 if ($have_grep) {
6253 print <<EOT;
6254 <dt><b>grep</b></dt>
6255 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6256 a different one) are searched for the given pattern. On large trees, this search can take
6257 a while and put some strain on the server, so please use it with some consideration. Note that
6258 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6259 case-sensitive.</dd>
6262 print <<EOT;
6263 <dt><b>author</b></dt>
6264 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6265 <dt><b>committer</b></dt>
6266 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6268 my $have_pickaxe = gitweb_check_feature('pickaxe');
6269 if ($have_pickaxe) {
6270 print <<EOT;
6271 <dt><b>pickaxe</b></dt>
6272 <dd>All commits that caused the string to appear or disappear from any file (changes that
6273 added, removed or "modified" the string) will be listed. This search can take a while and
6274 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6275 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6278 print "</dl>\n";
6279 git_footer_html();
6282 sub git_shortlog {
6283 my $head = git_get_head_hash($project);
6284 if (!defined $hash) {
6285 $hash = $head;
6287 if (!defined $page) {
6288 $page = 0;
6290 my $refs = git_get_references();
6292 my $commit_hash = $hash;
6293 if (defined $hash_parent) {
6294 $commit_hash = "$hash_parent..$hash";
6296 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6298 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6299 my $next_link = '';
6300 if ($#commitlist >= 100) {
6301 $next_link =
6302 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6303 -accesskey => "n", -title => "Alt-n"}, "next");
6305 my $patch_max = gitweb_check_feature('patches');
6306 if ($patch_max) {
6307 if ($patch_max < 0 || @commitlist <= $patch_max) {
6308 $paging_nav .= " &sdot; " .
6309 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6310 "patches");
6314 git_header_html();
6315 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6316 git_print_header_div('summary', $project);
6318 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6320 git_footer_html();
6323 ## ......................................................................
6324 ## feeds (RSS, Atom; OPML)
6326 sub git_feed {
6327 my $format = shift || 'atom';
6328 my $have_blame = gitweb_check_feature('blame');
6330 # Atom: http://www.atomenabled.org/developers/syndication/
6331 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6332 if ($format ne 'rss' && $format ne 'atom') {
6333 die_error(400, "Unknown web feed format");
6336 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6337 my $head = $hash || 'HEAD';
6338 my @commitlist = parse_commits($head, 150, 0, $file_name);
6340 my %latest_commit;
6341 my %latest_date;
6342 my $content_type = "application/$format+xml";
6343 if (defined $cgi->http('HTTP_ACCEPT') &&
6344 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6345 # browser (feed reader) prefers text/xml
6346 $content_type = 'text/xml';
6348 if (defined($commitlist[0])) {
6349 %latest_commit = %{$commitlist[0]};
6350 my $latest_epoch = $latest_commit{'committer_epoch'};
6351 %latest_date = parse_date($latest_epoch);
6352 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6353 if (defined $if_modified) {
6354 my $since;
6355 if (eval { require HTTP::Date; 1; }) {
6356 $since = HTTP::Date::str2time($if_modified);
6357 } elsif (eval { require Time::ParseDate; 1; }) {
6358 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6360 if (defined $since && $latest_epoch <= $since) {
6361 print $cgi->header(
6362 -type => $content_type,
6363 -charset => 'utf-8',
6364 -last_modified => $latest_date{'rfc2822'},
6365 -status => '304 Not Modified');
6366 return;
6369 print $cgi->header(
6370 -type => $content_type,
6371 -charset => 'utf-8',
6372 -last_modified => $latest_date{'rfc2822'});
6373 } else {
6374 print $cgi->header(
6375 -type => $content_type,
6376 -charset => 'utf-8');
6379 # Optimization: skip generating the body if client asks only
6380 # for Last-Modified date.
6381 return if ($cgi->request_method() eq 'HEAD');
6383 # header variables
6384 my $title = "$site_name - $project/$action";
6385 my $feed_type = 'log';
6386 if (defined $hash) {
6387 $title .= " - '$hash'";
6388 $feed_type = 'branch log';
6389 if (defined $file_name) {
6390 $title .= " :: $file_name";
6391 $feed_type = 'history';
6393 } elsif (defined $file_name) {
6394 $title .= " - $file_name";
6395 $feed_type = 'history';
6397 $title .= " $feed_type";
6398 my $descr = git_get_project_description($project);
6399 if (defined $descr) {
6400 $descr = esc_html($descr);
6401 } else {
6402 $descr = "$project " .
6403 ($format eq 'rss' ? 'RSS' : 'Atom') .
6404 " feed";
6406 my $owner = git_get_project_owner($project);
6407 $owner = esc_html($owner);
6409 #header
6410 my $alt_url;
6411 if (defined $file_name) {
6412 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6413 } elsif (defined $hash) {
6414 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6415 } else {
6416 $alt_url = href(-full=>1, action=>"summary");
6418 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6419 if ($format eq 'rss') {
6420 print <<XML;
6421 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6422 <channel>
6424 print "<title>$title</title>\n" .
6425 "<link>$alt_url</link>\n" .
6426 "<description>$descr</description>\n" .
6427 "<language>en</language>\n" .
6428 # project owner is responsible for 'editorial' content
6429 "<managingEditor>$owner</managingEditor>\n";
6430 if (defined $logo || defined $favicon) {
6431 # prefer the logo to the favicon, since RSS
6432 # doesn't allow both
6433 my $img = esc_url($logo || $favicon);
6434 print "<image>\n" .
6435 "<url>$img</url>\n" .
6436 "<title>$title</title>\n" .
6437 "<link>$alt_url</link>\n" .
6438 "</image>\n";
6440 if (%latest_date) {
6441 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6442 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6444 print "<generator>gitweb v.$version/$git_version</generator>\n";
6445 } elsif ($format eq 'atom') {
6446 print <<XML;
6447 <feed xmlns="http://www.w3.org/2005/Atom">
6449 print "<title>$title</title>\n" .
6450 "<subtitle>$descr</subtitle>\n" .
6451 '<link rel="alternate" type="text/html" href="' .
6452 $alt_url . '" />' . "\n" .
6453 '<link rel="self" type="' . $content_type . '" href="' .
6454 $cgi->self_url() . '" />' . "\n" .
6455 "<id>" . href(-full=>1) . "</id>\n" .
6456 # use project owner for feed author
6457 "<author><name>$owner</name></author>\n";
6458 if (defined $favicon) {
6459 print "<icon>" . esc_url($favicon) . "</icon>\n";
6461 if (defined $logo_url) {
6462 # not twice as wide as tall: 72 x 27 pixels
6463 print "<logo>" . esc_url($logo) . "</logo>\n";
6465 if (! %latest_date) {
6466 # dummy date to keep the feed valid until commits trickle in:
6467 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6468 } else {
6469 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6471 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6474 # contents
6475 for (my $i = 0; $i <= $#commitlist; $i++) {
6476 my %co = %{$commitlist[$i]};
6477 my $commit = $co{'id'};
6478 # we read 150, we always show 30 and the ones more recent than 48 hours
6479 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6480 last;
6482 my %cd = parse_date($co{'author_epoch'});
6484 # get list of changed files
6485 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6486 $co{'parent'} || "--root",
6487 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6488 or next;
6489 my @difftree = map { chomp; $_ } <$fd>;
6490 close $fd
6491 or next;
6493 # print element (entry, item)
6494 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6495 if ($format eq 'rss') {
6496 print "<item>\n" .
6497 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6498 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6499 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6500 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6501 "<link>$co_url</link>\n" .
6502 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6503 "<content:encoded>" .
6504 "<![CDATA[\n";
6505 } elsif ($format eq 'atom') {
6506 print "<entry>\n" .
6507 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6508 "<updated>$cd{'iso-8601'}</updated>\n" .
6509 "<author>\n" .
6510 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6511 if ($co{'author_email'}) {
6512 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6514 print "</author>\n" .
6515 # use committer for contributor
6516 "<contributor>\n" .
6517 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6518 if ($co{'committer_email'}) {
6519 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6521 print "</contributor>\n" .
6522 "<published>$cd{'iso-8601'}</published>\n" .
6523 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6524 "<id>$co_url</id>\n" .
6525 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6526 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6528 my $comment = $co{'comment'};
6529 print "<pre>\n";
6530 foreach my $line (@$comment) {
6531 $line = esc_html($line);
6532 print "$line\n";
6534 print "</pre><ul>\n";
6535 foreach my $difftree_line (@difftree) {
6536 my %difftree = parse_difftree_raw_line($difftree_line);
6537 next if !$difftree{'from_id'};
6539 my $file = $difftree{'file'} || $difftree{'to_file'};
6541 print "<li>" .
6542 "[" .
6543 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6544 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6545 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6546 file_name=>$file, file_parent=>$difftree{'from_file'}),
6547 -title => "diff"}, 'D');
6548 if ($have_blame) {
6549 print $cgi->a({-href => href(-full=>1, action=>"blame",
6550 file_name=>$file, hash_base=>$commit),
6551 -title => "blame"}, 'B');
6553 # if this is not a feed of a file history
6554 if (!defined $file_name || $file_name ne $file) {
6555 print $cgi->a({-href => href(-full=>1, action=>"history",
6556 file_name=>$file, hash=>$commit),
6557 -title => "history"}, 'H');
6559 $file = esc_path($file);
6560 print "] ".
6561 "$file</li>\n";
6563 if ($format eq 'rss') {
6564 print "</ul>]]>\n" .
6565 "</content:encoded>\n" .
6566 "</item>\n";
6567 } elsif ($format eq 'atom') {
6568 print "</ul>\n</div>\n" .
6569 "</content>\n" .
6570 "</entry>\n";
6574 # end of feed
6575 if ($format eq 'rss') {
6576 print "</channel>\n</rss>\n";
6577 } elsif ($format eq 'atom') {
6578 print "</feed>\n";
6582 sub git_rss {
6583 git_feed('rss');
6586 sub git_atom {
6587 git_feed('atom');
6590 sub git_opml {
6591 my @list = git_get_projects_list();
6593 print $cgi->header(
6594 -type => 'text/xml',
6595 -charset => 'utf-8',
6596 -content_disposition => 'inline; filename="opml.xml"');
6598 print <<XML;
6599 <?xml version="1.0" encoding="utf-8"?>
6600 <opml version="1.0">
6601 <head>
6602 <title>$site_name OPML Export</title>
6603 </head>
6604 <body>
6605 <outline text="git RSS feeds">
6608 foreach my $pr (@list) {
6609 my %proj = %$pr;
6610 my $head = git_get_head_hash($proj{'path'});
6611 if (!defined $head) {
6612 next;
6614 $git_dir = "$projectroot/$proj{'path'}";
6615 my %co = parse_commit($head);
6616 if (!%co) {
6617 next;
6620 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6621 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6622 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6623 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6625 print <<XML;
6626 </outline>
6627 </body>
6628 </opml>