gitweb: URL-decode $my_url/$my_uri when stripping PATH_INFO
[git/jnareb-git.git] / gitweb / gitweb.perl
blob126d3ef4bd4137e51ed0c4e58445937aa795b966
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 5.008;
11 use strict;
12 use warnings;
13 use CGI qw(:standard :escapeHTML -nosticky);
14 use CGI::Util qw(unescape);
15 use CGI::Carp qw(fatalsToBrowser set_message);
16 use Encode;
17 use Fcntl ':mode';
18 use File::Find qw();
19 use File::Basename qw(basename);
20 use Time::HiRes qw(gettimeofday tv_interval);
21 binmode STDOUT, ':utf8';
23 our $t0 = [ gettimeofday() ];
24 our $number_of_git_cmds = 0;
26 BEGIN {
27 CGI->compile() if $ENV{'MOD_PERL'};
30 our $version = "++GIT_VERSION++";
32 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
33 sub evaluate_uri {
34 our $cgi;
36 our $my_url = $cgi->url();
37 our $my_uri = $cgi->url(-absolute => 1);
39 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
40 # needed and used only for URLs with nonempty PATH_INFO
41 our $base_url = $my_url;
43 # When the script is used as DirectoryIndex, the URL does not contain the name
44 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
45 # have to do it ourselves. We make $path_info global because it's also used
46 # later on.
48 # Another issue with the script being the DirectoryIndex is that the resulting
49 # $my_url data is not the full script URL: this is good, because we want
50 # generated links to keep implying the script name if it wasn't explicitly
51 # indicated in the URL we're handling, but it means that $my_url cannot be used
52 # as base URL.
53 # Therefore, if we needed to strip PATH_INFO, then we know that we have
54 # to build the base URL ourselves:
55 our $path_info = decode_utf8($ENV{"PATH_INFO"});
56 if ($path_info) {
57 # $path_info has already been URL-decoded by the web server, but
58 # $my_url and $my_uri have not. URL-decode them so we can properly
59 # strip $path_info.
60 $my_url = unescape($my_url);
61 $my_uri = unescape($my_uri);
62 if ($my_url =~ s,\Q$path_info\E$,, &&
63 $my_uri =~ s,\Q$path_info\E$,, &&
64 defined $ENV{'SCRIPT_NAME'}) {
65 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
69 # target of the home link on top of all pages
70 our $home_link = $my_uri || "/";
73 # core git executable to use
74 # this can just be "git" if your webserver has a sensible PATH
75 our $GIT = "++GIT_BINDIR++/git";
77 # absolute fs-path which will be prepended to the project path
78 #our $projectroot = "/pub/scm";
79 our $projectroot = "++GITWEB_PROJECTROOT++";
81 # fs traversing limit for getting project list
82 # the number is relative to the projectroot
83 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
85 # string of the home link on top of all pages
86 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
88 # name of your site or organization to appear in page titles
89 # replace this with something more descriptive for clearer bookmarks
90 our $site_name = "++GITWEB_SITENAME++"
91 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
93 # html snippet to include in the <head> section of each page
94 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
95 # filename of html text to include at top of each page
96 our $site_header = "++GITWEB_SITE_HEADER++";
97 # html text to include at home page
98 our $home_text = "++GITWEB_HOMETEXT++";
99 # filename of html text to include at bottom of each page
100 our $site_footer = "++GITWEB_SITE_FOOTER++";
102 # URI of stylesheets
103 our @stylesheets = ("++GITWEB_CSS++");
104 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
105 our $stylesheet = undef;
106 # URI of GIT logo (72x27 size)
107 our $logo = "++GITWEB_LOGO++";
108 # URI of GIT favicon, assumed to be image/png type
109 our $favicon = "++GITWEB_FAVICON++";
110 # URI of gitweb.js (JavaScript code for gitweb)
111 our $javascript = "++GITWEB_JS++";
113 # URI and label (title) of GIT logo link
114 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
115 #our $logo_label = "git documentation";
116 our $logo_url = "http://git-scm.com/";
117 our $logo_label = "git homepage";
119 # source of projects list
120 our $projects_list = "++GITWEB_LIST++";
122 # the width (in characters) of the projects list "Description" column
123 our $projects_list_description_width = 25;
125 # group projects by category on the projects list
126 # (enabled if this variable evaluates to true)
127 our $projects_list_group_categories = 0;
129 # default category if none specified
130 # (leave the empty string for no category)
131 our $project_list_default_category = "";
133 # default order of projects list
134 # valid values are none, project, descr, owner, and age
135 our $default_projects_order = "project";
137 # show repository only if this file exists
138 # (only effective if this variable evaluates to true)
139 our $export_ok = "++GITWEB_EXPORT_OK++";
141 # show repository only if this subroutine returns true
142 # when given the path to the project, for example:
143 # sub { return -e "$_[0]/git-daemon-export-ok"; }
144 our $export_auth_hook = undef;
146 # only allow viewing of repositories also shown on the overview page
147 our $strict_export = "++GITWEB_STRICT_EXPORT++";
149 # list of git base URLs used for URL to where fetch project from,
150 # i.e. full URL is "$git_base_url/$project"
151 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
153 # default blob_plain mimetype and default charset for text/plain blob
154 our $default_blob_plain_mimetype = 'text/plain';
155 our $default_text_plain_charset = undef;
157 # file to use for guessing MIME types before trying /etc/mime.types
158 # (relative to the current git repository)
159 our $mimetypes_file = undef;
161 # assume this charset if line contains non-UTF-8 characters;
162 # it should be valid encoding (see Encoding::Supported(3pm) for list),
163 # for which encoding all byte sequences are valid, for example
164 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
165 # could be even 'utf-8' for the old behavior)
166 our $fallback_encoding = 'latin1';
168 # rename detection options for git-diff and git-diff-tree
169 # - default is '-M', with the cost proportional to
170 # (number of removed files) * (number of new files).
171 # - more costly is '-C' (which implies '-M'), with the cost proportional to
172 # (number of changed files + number of removed files) * (number of new files)
173 # - even more costly is '-C', '--find-copies-harder' with cost
174 # (number of files in the original tree) * (number of new files)
175 # - one might want to include '-B' option, e.g. '-B', '-M'
176 our @diff_opts = ('-M'); # taken from git_commit
178 # Disables features that would allow repository owners to inject script into
179 # the gitweb domain.
180 our $prevent_xss = 0;
182 # Path to the highlight executable to use (must be the one from
183 # http://www.andre-simon.de due to assumptions about parameters and output).
184 # Useful if highlight is not installed on your webserver's PATH.
185 # [Default: highlight]
186 our $highlight_bin = "++HIGHLIGHT_BIN++";
188 # information about snapshot formats that gitweb is capable of serving
189 our %known_snapshot_formats = (
190 # name => {
191 # 'display' => display name,
192 # 'type' => mime type,
193 # 'suffix' => filename suffix,
194 # 'format' => --format for git-archive,
195 # 'compressor' => [compressor command and arguments]
196 # (array reference, optional)
197 # 'disabled' => boolean (optional)}
199 'tgz' => {
200 'display' => 'tar.gz',
201 'type' => 'application/x-gzip',
202 'suffix' => '.tar.gz',
203 'format' => 'tar',
204 'compressor' => ['gzip', '-n']},
206 'tbz2' => {
207 'display' => 'tar.bz2',
208 'type' => 'application/x-bzip2',
209 'suffix' => '.tar.bz2',
210 'format' => 'tar',
211 'compressor' => ['bzip2']},
213 'txz' => {
214 'display' => 'tar.xz',
215 'type' => 'application/x-xz',
216 'suffix' => '.tar.xz',
217 'format' => 'tar',
218 'compressor' => ['xz'],
219 'disabled' => 1},
221 'zip' => {
222 'display' => 'zip',
223 'type' => 'application/x-zip',
224 'suffix' => '.zip',
225 'format' => 'zip'},
228 # Aliases so we understand old gitweb.snapshot values in repository
229 # configuration.
230 our %known_snapshot_format_aliases = (
231 'gzip' => 'tgz',
232 'bzip2' => 'tbz2',
233 'xz' => 'txz',
235 # backward compatibility: legacy gitweb config support
236 'x-gzip' => undef, 'gz' => undef,
237 'x-bzip2' => undef, 'bz2' => undef,
238 'x-zip' => undef, '' => undef,
241 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
242 # are changed, it may be appropriate to change these values too via
243 # $GITWEB_CONFIG.
244 our %avatar_size = (
245 'default' => 16,
246 'double' => 32
249 # Used to set the maximum load that we will still respond to gitweb queries.
250 # If server load exceed this value then return "503 server busy" error.
251 # If gitweb cannot determined server load, it is taken to be 0.
252 # Leave it undefined (or set to 'undef') to turn off load checking.
253 our $maxload = 300;
255 # configuration for 'highlight' (http://www.andre-simon.de/)
256 # match by basename
257 our %highlight_basename = (
258 #'Program' => 'py',
259 #'Library' => 'py',
260 'SConstruct' => 'py', # SCons equivalent of Makefile
261 'Makefile' => 'make',
263 # match by extension
264 our %highlight_ext = (
265 # main extensions, defining name of syntax;
266 # see files in /usr/share/highlight/langDefs/ directory
267 map { $_ => $_ }
268 qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
269 # alternate extensions, see /etc/highlight/filetypes.conf
270 'h' => 'c',
271 map { $_ => 'sh' } qw(bash zsh ksh),
272 map { $_ => 'cpp' } qw(cxx c++ cc),
273 map { $_ => 'php' } qw(php3 php4 php5 phps),
274 map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi'
275 map { $_ => 'make'} qw(mak mk),
276 map { $_ => 'xml' } qw(xhtml html htm),
279 # You define site-wide feature defaults here; override them with
280 # $GITWEB_CONFIG as necessary.
281 our %feature = (
282 # feature => {
283 # 'sub' => feature-sub (subroutine),
284 # 'override' => allow-override (boolean),
285 # 'default' => [ default options...] (array reference)}
287 # if feature is overridable (it means that allow-override has true value),
288 # then feature-sub will be called with default options as parameters;
289 # return value of feature-sub indicates if to enable specified feature
291 # if there is no 'sub' key (no feature-sub), then feature cannot be
292 # overridden
294 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
295 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
296 # is enabled
298 # Enable the 'blame' blob view, showing the last commit that modified
299 # each line in the file. This can be very CPU-intensive.
301 # To enable system wide have in $GITWEB_CONFIG
302 # $feature{'blame'}{'default'} = [1];
303 # To have project specific config enable override in $GITWEB_CONFIG
304 # $feature{'blame'}{'override'} = 1;
305 # and in project config gitweb.blame = 0|1;
306 'blame' => {
307 'sub' => sub { feature_bool('blame', @_) },
308 'override' => 0,
309 'default' => [0]},
311 # Enable the 'snapshot' link, providing a compressed archive of any
312 # tree. This can potentially generate high traffic if you have large
313 # project.
315 # Value is a list of formats defined in %known_snapshot_formats that
316 # you wish to offer.
317 # To disable system wide have in $GITWEB_CONFIG
318 # $feature{'snapshot'}{'default'} = [];
319 # To have project specific config enable override in $GITWEB_CONFIG
320 # $feature{'snapshot'}{'override'} = 1;
321 # and in project config, a comma-separated list of formats or "none"
322 # to disable. Example: gitweb.snapshot = tbz2,zip;
323 'snapshot' => {
324 'sub' => \&feature_snapshot,
325 'override' => 0,
326 'default' => ['tgz']},
328 # Enable text search, which will list the commits which match author,
329 # committer or commit text to a given string. Enabled by default.
330 # Project specific override is not supported.
332 # Note that this controls all search features, which means that if
333 # it is disabled, then 'grep' and 'pickaxe' search would also be
334 # disabled.
335 'search' => {
336 'override' => 0,
337 'default' => [1]},
339 # Enable grep search, which will list the files in currently selected
340 # tree containing the given string. Enabled by default. This can be
341 # potentially CPU-intensive, of course.
342 # Note that you need to have 'search' feature enabled too.
344 # To enable system wide have in $GITWEB_CONFIG
345 # $feature{'grep'}{'default'} = [1];
346 # To have project specific config enable override in $GITWEB_CONFIG
347 # $feature{'grep'}{'override'} = 1;
348 # and in project config gitweb.grep = 0|1;
349 'grep' => {
350 'sub' => sub { feature_bool('grep', @_) },
351 'override' => 0,
352 'default' => [1]},
354 # Enable the pickaxe search, which will list the commits that modified
355 # a given string in a file. This can be practical and quite faster
356 # alternative to 'blame', but still potentially CPU-intensive.
357 # Note that you need to have 'search' feature enabled too.
359 # To enable system wide have in $GITWEB_CONFIG
360 # $feature{'pickaxe'}{'default'} = [1];
361 # To have project specific config enable override in $GITWEB_CONFIG
362 # $feature{'pickaxe'}{'override'} = 1;
363 # and in project config gitweb.pickaxe = 0|1;
364 'pickaxe' => {
365 'sub' => sub { feature_bool('pickaxe', @_) },
366 'override' => 0,
367 'default' => [1]},
369 # Enable showing size of blobs in a 'tree' view, in a separate
370 # column, similar to what 'ls -l' does. This cost a bit of IO.
372 # To disable system wide have in $GITWEB_CONFIG
373 # $feature{'show-sizes'}{'default'} = [0];
374 # To have project specific config enable override in $GITWEB_CONFIG
375 # $feature{'show-sizes'}{'override'} = 1;
376 # and in project config gitweb.showsizes = 0|1;
377 'show-sizes' => {
378 'sub' => sub { feature_bool('showsizes', @_) },
379 'override' => 0,
380 'default' => [1]},
382 # Make gitweb use an alternative format of the URLs which can be
383 # more readable and natural-looking: project name is embedded
384 # directly in the path and the query string contains other
385 # auxiliary information. All gitweb installations recognize
386 # URL in either format; this configures in which formats gitweb
387 # generates links.
389 # To enable system wide have in $GITWEB_CONFIG
390 # $feature{'pathinfo'}{'default'} = [1];
391 # Project specific override is not supported.
393 # Note that you will need to change the default location of CSS,
394 # favicon, logo and possibly other files to an absolute URL. Also,
395 # if gitweb.cgi serves as your indexfile, you will need to force
396 # $my_uri to contain the script name in your $GITWEB_CONFIG.
397 'pathinfo' => {
398 'override' => 0,
399 'default' => [0]},
401 # Make gitweb consider projects in project root subdirectories
402 # to be forks of existing projects. Given project $projname.git,
403 # projects matching $projname/*.git will not be shown in the main
404 # projects list, instead a '+' mark will be added to $projname
405 # there and a 'forks' view will be enabled for the project, listing
406 # all the forks. If project list is taken from a file, forks have
407 # to be listed after the main project.
409 # To enable system wide have in $GITWEB_CONFIG
410 # $feature{'forks'}{'default'} = [1];
411 # Project specific override is not supported.
412 'forks' => {
413 'override' => 0,
414 'default' => [0]},
416 # Insert custom links to the action bar of all project pages.
417 # This enables you mainly to link to third-party scripts integrating
418 # into gitweb; e.g. git-browser for graphical history representation
419 # or custom web-based repository administration interface.
421 # The 'default' value consists of a list of triplets in the form
422 # (label, link, position) where position is the label after which
423 # to insert the link and link is a format string where %n expands
424 # to the project name, %f to the project path within the filesystem,
425 # %h to the current hash (h gitweb parameter) and %b to the current
426 # hash base (hb gitweb parameter); %% expands to %.
428 # To enable system wide have in $GITWEB_CONFIG e.g.
429 # $feature{'actions'}{'default'} = [('graphiclog',
430 # '/git-browser/by-commit.html?r=%n', 'summary')];
431 # Project specific override is not supported.
432 'actions' => {
433 'override' => 0,
434 'default' => []},
436 # Allow gitweb scan project content tags of project repository,
437 # and display the popular Web 2.0-ish "tag cloud" near the projects
438 # list. Note that this is something COMPLETELY different from the
439 # normal Git tags.
441 # gitweb by itself can show existing tags, but it does not handle
442 # tagging itself; you need to do it externally, outside gitweb.
443 # The format is described in git_get_project_ctags() subroutine.
444 # You may want to install the HTML::TagCloud Perl module to get
445 # a pretty tag cloud instead of just a list of tags.
447 # To enable system wide have in $GITWEB_CONFIG
448 # $feature{'ctags'}{'default'} = [1];
449 # Project specific override is not supported.
451 # In the future whether ctags editing is enabled might depend
452 # on the value, but using 1 should always mean no editing of ctags.
453 'ctags' => {
454 'override' => 0,
455 'default' => [0]},
457 # The maximum number of patches in a patchset generated in patch
458 # view. Set this to 0 or undef to disable patch view, or to a
459 # negative number to remove any limit.
461 # To disable system wide have in $GITWEB_CONFIG
462 # $feature{'patches'}{'default'} = [0];
463 # To have project specific config enable override in $GITWEB_CONFIG
464 # $feature{'patches'}{'override'} = 1;
465 # and in project config gitweb.patches = 0|n;
466 # where n is the maximum number of patches allowed in a patchset.
467 'patches' => {
468 'sub' => \&feature_patches,
469 'override' => 0,
470 'default' => [16]},
472 # Avatar support. When this feature is enabled, views such as
473 # shortlog or commit will display an avatar associated with
474 # the email of the committer(s) and/or author(s).
476 # Currently available providers are gravatar and picon.
477 # If an unknown provider is specified, the feature is disabled.
479 # Gravatar depends on Digest::MD5.
480 # Picon currently relies on the indiana.edu database.
482 # To enable system wide have in $GITWEB_CONFIG
483 # $feature{'avatar'}{'default'} = ['<provider>'];
484 # where <provider> is either gravatar or picon.
485 # To have project specific config enable override in $GITWEB_CONFIG
486 # $feature{'avatar'}{'override'} = 1;
487 # and in project config gitweb.avatar = <provider>;
488 'avatar' => {
489 'sub' => \&feature_avatar,
490 'override' => 0,
491 'default' => ['']},
493 # Enable displaying how much time and how many git commands
494 # it took to generate and display page. Disabled by default.
495 # Project specific override is not supported.
496 'timed' => {
497 'override' => 0,
498 'default' => [0]},
500 # Enable turning some links into links to actions which require
501 # JavaScript to run (like 'blame_incremental'). Not enabled by
502 # default. Project specific override is currently not supported.
503 'javascript-actions' => {
504 'override' => 0,
505 'default' => [0]},
507 # Enable and configure ability to change common timezone for dates
508 # in gitweb output via JavaScript. Enabled by default.
509 # Project specific override is not supported.
510 'javascript-timezone' => {
511 'override' => 0,
512 'default' => [
513 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
514 # or undef to turn off this feature
515 'gitweb_tz', # name of cookie where to store selected timezone
516 'datetime', # CSS class used to mark up dates for manipulation
519 # Syntax highlighting support. This is based on Daniel Svensson's
520 # and Sham Chukoury's work in gitweb-xmms2.git.
521 # It requires the 'highlight' program present in $PATH,
522 # and therefore is disabled by default.
524 # To enable system wide have in $GITWEB_CONFIG
525 # $feature{'highlight'}{'default'} = [1];
527 'highlight' => {
528 'sub' => sub { feature_bool('highlight', @_) },
529 'override' => 0,
530 'default' => [0]},
532 # Enable displaying of remote heads in the heads list
534 # To enable system wide have in $GITWEB_CONFIG
535 # $feature{'remote_heads'}{'default'} = [1];
536 # To have project specific config enable override in $GITWEB_CONFIG
537 # $feature{'remote_heads'}{'override'} = 1;
538 # and in project config gitweb.remote_heads = 0|1;
539 'remote_heads' => {
540 'sub' => sub { feature_bool('remote_heads', @_) },
541 'override' => 0,
542 'default' => [0]},
545 sub gitweb_get_feature {
546 my ($name) = @_;
547 return unless exists $feature{$name};
548 my ($sub, $override, @defaults) = (
549 $feature{$name}{'sub'},
550 $feature{$name}{'override'},
551 @{$feature{$name}{'default'}});
552 # project specific override is possible only if we have project
553 our $git_dir; # global variable, declared later
554 if (!$override || !defined $git_dir) {
555 return @defaults;
557 if (!defined $sub) {
558 warn "feature $name is not overridable";
559 return @defaults;
561 return $sub->(@defaults);
564 # A wrapper to check if a given feature is enabled.
565 # With this, you can say
567 # my $bool_feat = gitweb_check_feature('bool_feat');
568 # gitweb_check_feature('bool_feat') or somecode;
570 # instead of
572 # my ($bool_feat) = gitweb_get_feature('bool_feat');
573 # (gitweb_get_feature('bool_feat'))[0] or somecode;
575 sub gitweb_check_feature {
576 return (gitweb_get_feature(@_))[0];
580 sub feature_bool {
581 my $key = shift;
582 my ($val) = git_get_project_config($key, '--bool');
584 if (!defined $val) {
585 return ($_[0]);
586 } elsif ($val eq 'true') {
587 return (1);
588 } elsif ($val eq 'false') {
589 return (0);
593 sub feature_snapshot {
594 my (@fmts) = @_;
596 my ($val) = git_get_project_config('snapshot');
598 if ($val) {
599 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
602 return @fmts;
605 sub feature_patches {
606 my @val = (git_get_project_config('patches', '--int'));
608 if (@val) {
609 return @val;
612 return ($_[0]);
615 sub feature_avatar {
616 my @val = (git_get_project_config('avatar'));
618 return @val ? @val : @_;
621 # checking HEAD file with -e is fragile if the repository was
622 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
623 # and then pruned.
624 sub check_head_link {
625 my ($dir) = @_;
626 my $headfile = "$dir/HEAD";
627 return ((-e $headfile) ||
628 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
631 sub check_export_ok {
632 my ($dir) = @_;
633 return (check_head_link($dir) &&
634 (!$export_ok || -e "$dir/$export_ok") &&
635 (!$export_auth_hook || $export_auth_hook->($dir)));
638 # process alternate names for backward compatibility
639 # filter out unsupported (unknown) snapshot formats
640 sub filter_snapshot_fmts {
641 my @fmts = @_;
643 @fmts = map {
644 exists $known_snapshot_format_aliases{$_} ?
645 $known_snapshot_format_aliases{$_} : $_} @fmts;
646 @fmts = grep {
647 exists $known_snapshot_formats{$_} &&
648 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
651 # If it is set to code reference, it is code that it is to be run once per
652 # request, allowing updating configurations that change with each request,
653 # while running other code in config file only once.
655 # Otherwise, if it is false then gitweb would process config file only once;
656 # if it is true then gitweb config would be run for each request.
657 our $per_request_config = 1;
659 # read and parse gitweb config file given by its parameter.
660 # returns true on success, false on recoverable error, allowing
661 # to chain this subroutine, using first file that exists.
662 # dies on errors during parsing config file, as it is unrecoverable.
663 sub read_config_file {
664 my $filename = shift;
665 return unless defined $filename;
666 # die if there are errors parsing config file
667 if (-e $filename) {
668 do $filename;
669 die $@ if $@;
670 return 1;
672 return;
675 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
676 sub evaluate_gitweb_config {
677 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
678 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
679 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
681 # Protect agains duplications of file names, to not read config twice.
682 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
683 # there possibility of duplication of filename there doesn't matter.
684 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
685 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
687 # Common system-wide settings for convenience.
688 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
689 read_config_file($GITWEB_CONFIG_COMMON);
691 # Use first config file that exists. This means use the per-instance
692 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
693 read_config_file($GITWEB_CONFIG) and return;
694 read_config_file($GITWEB_CONFIG_SYSTEM);
697 # Get loadavg of system, to compare against $maxload.
698 # Currently it requires '/proc/loadavg' present to get loadavg;
699 # if it is not present it returns 0, which means no load checking.
700 sub get_loadavg {
701 if( -e '/proc/loadavg' ){
702 open my $fd, '<', '/proc/loadavg'
703 or return 0;
704 my @load = split(/\s+/, scalar <$fd>);
705 close $fd;
707 # The first three columns measure CPU and IO utilization of the last one,
708 # five, and 10 minute periods. The fourth column shows the number of
709 # currently running processes and the total number of processes in the m/n
710 # format. The last column displays the last process ID used.
711 return $load[0] || 0;
713 # additional checks for load average should go here for things that don't export
714 # /proc/loadavg
716 return 0;
719 # version of the core git binary
720 our $git_version;
721 sub evaluate_git_version {
722 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
723 $number_of_git_cmds++;
726 sub check_loadavg {
727 if (defined $maxload && get_loadavg() > $maxload) {
728 die_error(503, "The load average on the server is too high");
732 # ======================================================================
733 # input validation and dispatch
735 # input parameters can be collected from a variety of sources (presently, CGI
736 # and PATH_INFO), so we define an %input_params hash that collects them all
737 # together during validation: this allows subsequent uses (e.g. href()) to be
738 # agnostic of the parameter origin
740 our %input_params = ();
742 # input parameters are stored with the long parameter name as key. This will
743 # also be used in the href subroutine to convert parameters to their CGI
744 # equivalent, and since the href() usage is the most frequent one, we store
745 # the name -> CGI key mapping here, instead of the reverse.
747 # XXX: Warning: If you touch this, check the search form for updating,
748 # too.
750 our @cgi_param_mapping = (
751 project => "p",
752 action => "a",
753 file_name => "f",
754 file_parent => "fp",
755 hash => "h",
756 hash_parent => "hp",
757 hash_base => "hb",
758 hash_parent_base => "hpb",
759 page => "pg",
760 order => "o",
761 searchtext => "s",
762 searchtype => "st",
763 snapshot_format => "sf",
764 extra_options => "opt",
765 search_use_regexp => "sr",
766 ctag => "by_tag",
767 diff_style => "ds",
768 project_filter => "pf",
769 # this must be last entry (for manipulation from JavaScript)
770 javascript => "js"
772 our %cgi_param_mapping = @cgi_param_mapping;
774 # we will also need to know the possible actions, for validation
775 our %actions = (
776 "blame" => \&git_blame,
777 "blame_incremental" => \&git_blame_incremental,
778 "blame_data" => \&git_blame_data,
779 "blobdiff" => \&git_blobdiff,
780 "blobdiff_plain" => \&git_blobdiff_plain,
781 "blob" => \&git_blob,
782 "blob_plain" => \&git_blob_plain,
783 "commitdiff" => \&git_commitdiff,
784 "commitdiff_plain" => \&git_commitdiff_plain,
785 "commit" => \&git_commit,
786 "forks" => \&git_forks,
787 "heads" => \&git_heads,
788 "history" => \&git_history,
789 "log" => \&git_log,
790 "patch" => \&git_patch,
791 "patches" => \&git_patches,
792 "remotes" => \&git_remotes,
793 "rss" => \&git_rss,
794 "atom" => \&git_atom,
795 "search" => \&git_search,
796 "search_help" => \&git_search_help,
797 "shortlog" => \&git_shortlog,
798 "summary" => \&git_summary,
799 "tag" => \&git_tag,
800 "tags" => \&git_tags,
801 "tree" => \&git_tree,
802 "snapshot" => \&git_snapshot,
803 "object" => \&git_object,
804 # those below don't need $project
805 "opml" => \&git_opml,
806 "project_list" => \&git_project_list,
807 "project_index" => \&git_project_index,
810 # finally, we have the hash of allowed extra_options for the commands that
811 # allow them
812 our %allowed_options = (
813 "--no-merges" => [ qw(rss atom log shortlog history) ],
816 # fill %input_params with the CGI parameters. All values except for 'opt'
817 # should be single values, but opt can be an array. We should probably
818 # build an array of parameters that can be multi-valued, but since for the time
819 # being it's only this one, we just single it out
820 sub evaluate_query_params {
821 our $cgi;
823 while (my ($name, $symbol) = each %cgi_param_mapping) {
824 if ($symbol eq 'opt') {
825 $input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ];
826 } else {
827 $input_params{$name} = decode_utf8($cgi->param($symbol));
832 # now read PATH_INFO and update the parameter list for missing parameters
833 sub evaluate_path_info {
834 return if defined $input_params{'project'};
835 return if !$path_info;
836 $path_info =~ s,^/+,,;
837 return if !$path_info;
839 # find which part of PATH_INFO is project
840 my $project = $path_info;
841 $project =~ s,/+$,,;
842 while ($project && !check_head_link("$projectroot/$project")) {
843 $project =~ s,/*[^/]*$,,;
845 return unless $project;
846 $input_params{'project'} = $project;
848 # do not change any parameters if an action is given using the query string
849 return if $input_params{'action'};
850 $path_info =~ s,^\Q$project\E/*,,;
852 # next, check if we have an action
853 my $action = $path_info;
854 $action =~ s,/.*$,,;
855 if (exists $actions{$action}) {
856 $path_info =~ s,^$action/*,,;
857 $input_params{'action'} = $action;
860 # list of actions that want hash_base instead of hash, but can have no
861 # pathname (f) parameter
862 my @wants_base = (
863 'tree',
864 'history',
867 # we want to catch, among others
868 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
869 my ($parentrefname, $parentpathname, $refname, $pathname) =
870 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
872 # first, analyze the 'current' part
873 if (defined $pathname) {
874 # we got "branch:filename" or "branch:dir/"
875 # we could use git_get_type(branch:pathname), but:
876 # - it needs $git_dir
877 # - it does a git() call
878 # - the convention of terminating directories with a slash
879 # makes it superfluous
880 # - embedding the action in the PATH_INFO would make it even
881 # more superfluous
882 $pathname =~ s,^/+,,;
883 if (!$pathname || substr($pathname, -1) eq "/") {
884 $input_params{'action'} ||= "tree";
885 $pathname =~ s,/$,,;
886 } else {
887 # the default action depends on whether we had parent info
888 # or not
889 if ($parentrefname) {
890 $input_params{'action'} ||= "blobdiff_plain";
891 } else {
892 $input_params{'action'} ||= "blob_plain";
895 $input_params{'hash_base'} ||= $refname;
896 $input_params{'file_name'} ||= $pathname;
897 } elsif (defined $refname) {
898 # we got "branch". In this case we have to choose if we have to
899 # set hash or hash_base.
901 # Most of the actions without a pathname only want hash to be
902 # set, except for the ones specified in @wants_base that want
903 # hash_base instead. It should also be noted that hand-crafted
904 # links having 'history' as an action and no pathname or hash
905 # set will fail, but that happens regardless of PATH_INFO.
906 if (defined $parentrefname) {
907 # if there is parent let the default be 'shortlog' action
908 # (for http://git.example.com/repo.git/A..B links); if there
909 # is no parent, dispatch will detect type of object and set
910 # action appropriately if required (if action is not set)
911 $input_params{'action'} ||= "shortlog";
913 if ($input_params{'action'} &&
914 grep { $_ eq $input_params{'action'} } @wants_base) {
915 $input_params{'hash_base'} ||= $refname;
916 } else {
917 $input_params{'hash'} ||= $refname;
921 # next, handle the 'parent' part, if present
922 if (defined $parentrefname) {
923 # a missing pathspec defaults to the 'current' filename, allowing e.g.
924 # someproject/blobdiff/oldrev..newrev:/filename
925 if ($parentpathname) {
926 $parentpathname =~ s,^/+,,;
927 $parentpathname =~ s,/$,,;
928 $input_params{'file_parent'} ||= $parentpathname;
929 } else {
930 $input_params{'file_parent'} ||= $input_params{'file_name'};
932 # we assume that hash_parent_base is wanted if a path was specified,
933 # or if the action wants hash_base instead of hash
934 if (defined $input_params{'file_parent'} ||
935 grep { $_ eq $input_params{'action'} } @wants_base) {
936 $input_params{'hash_parent_base'} ||= $parentrefname;
937 } else {
938 $input_params{'hash_parent'} ||= $parentrefname;
942 # for the snapshot action, we allow URLs in the form
943 # $project/snapshot/$hash.ext
944 # where .ext determines the snapshot and gets removed from the
945 # passed $refname to provide the $hash.
947 # To be able to tell that $refname includes the format extension, we
948 # require the following two conditions to be satisfied:
949 # - the hash input parameter MUST have been set from the $refname part
950 # of the URL (i.e. they must be equal)
951 # - the snapshot format MUST NOT have been defined already (e.g. from
952 # CGI parameter sf)
953 # It's also useless to try any matching unless $refname has a dot,
954 # so we check for that too
955 if (defined $input_params{'action'} &&
956 $input_params{'action'} eq 'snapshot' &&
957 defined $refname && index($refname, '.') != -1 &&
958 $refname eq $input_params{'hash'} &&
959 !defined $input_params{'snapshot_format'}) {
960 # We loop over the known snapshot formats, checking for
961 # extensions. Allowed extensions are both the defined suffix
962 # (which includes the initial dot already) and the snapshot
963 # format key itself, with a prepended dot
964 while (my ($fmt, $opt) = each %known_snapshot_formats) {
965 my $hash = $refname;
966 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
967 next;
969 my $sfx = $1;
970 # a valid suffix was found, so set the snapshot format
971 # and reset the hash parameter
972 $input_params{'snapshot_format'} = $fmt;
973 $input_params{'hash'} = $hash;
974 # we also set the format suffix to the one requested
975 # in the URL: this way a request for e.g. .tgz returns
976 # a .tgz instead of a .tar.gz
977 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
978 last;
983 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
984 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
985 $searchtext, $search_regexp, $project_filter);
986 sub evaluate_and_validate_params {
987 our $action = $input_params{'action'};
988 if (defined $action) {
989 if (!validate_action($action)) {
990 die_error(400, "Invalid action parameter");
994 # parameters which are pathnames
995 our $project = $input_params{'project'};
996 if (defined $project) {
997 if (!validate_project($project)) {
998 undef $project;
999 die_error(404, "No such project");
1003 our $project_filter = $input_params{'project_filter'};
1004 if (defined $project_filter) {
1005 if (!validate_pathname($project_filter)) {
1006 die_error(404, "Invalid project_filter parameter");
1010 our $file_name = $input_params{'file_name'};
1011 if (defined $file_name) {
1012 if (!validate_pathname($file_name)) {
1013 die_error(400, "Invalid file parameter");
1017 our $file_parent = $input_params{'file_parent'};
1018 if (defined $file_parent) {
1019 if (!validate_pathname($file_parent)) {
1020 die_error(400, "Invalid file parent parameter");
1024 # parameters which are refnames
1025 our $hash = $input_params{'hash'};
1026 if (defined $hash) {
1027 if (!validate_refname($hash)) {
1028 die_error(400, "Invalid hash parameter");
1032 our $hash_parent = $input_params{'hash_parent'};
1033 if (defined $hash_parent) {
1034 if (!validate_refname($hash_parent)) {
1035 die_error(400, "Invalid hash parent parameter");
1039 our $hash_base = $input_params{'hash_base'};
1040 if (defined $hash_base) {
1041 if (!validate_refname($hash_base)) {
1042 die_error(400, "Invalid hash base parameter");
1046 our @extra_options = @{$input_params{'extra_options'}};
1047 # @extra_options is always defined, since it can only be (currently) set from
1048 # CGI, and $cgi->param() returns the empty array in array context if the param
1049 # is not set
1050 foreach my $opt (@extra_options) {
1051 if (not exists $allowed_options{$opt}) {
1052 die_error(400, "Invalid option parameter");
1054 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1055 die_error(400, "Invalid option parameter for this action");
1059 our $hash_parent_base = $input_params{'hash_parent_base'};
1060 if (defined $hash_parent_base) {
1061 if (!validate_refname($hash_parent_base)) {
1062 die_error(400, "Invalid hash parent base parameter");
1066 # other parameters
1067 our $page = $input_params{'page'};
1068 if (defined $page) {
1069 if ($page =~ m/[^0-9]/) {
1070 die_error(400, "Invalid page parameter");
1074 our $searchtype = $input_params{'searchtype'};
1075 if (defined $searchtype) {
1076 if ($searchtype =~ m/[^a-z]/) {
1077 die_error(400, "Invalid searchtype parameter");
1081 our $search_use_regexp = $input_params{'search_use_regexp'};
1083 our $searchtext = $input_params{'searchtext'};
1084 our $search_regexp;
1085 if (defined $searchtext) {
1086 if (length($searchtext) < 2) {
1087 die_error(403, "At least two characters are required for search parameter");
1089 if ($search_use_regexp) {
1090 $search_regexp = $searchtext;
1091 if (!eval { qr/$search_regexp/; 1; }) {
1092 (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1093 die_error(400, "Invalid search regexp '$search_regexp'",
1094 esc_html($error));
1096 } else {
1097 $search_regexp = quotemeta $searchtext;
1102 # path to the current git repository
1103 our $git_dir;
1104 sub evaluate_git_dir {
1105 our $git_dir = "$projectroot/$project" if $project;
1108 our (@snapshot_fmts, $git_avatar);
1109 sub configure_gitweb_features {
1110 # list of supported snapshot formats
1111 our @snapshot_fmts = gitweb_get_feature('snapshot');
1112 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1114 # check that the avatar feature is set to a known provider name,
1115 # and for each provider check if the dependencies are satisfied.
1116 # if the provider name is invalid or the dependencies are not met,
1117 # reset $git_avatar to the empty string.
1118 our ($git_avatar) = gitweb_get_feature('avatar');
1119 if ($git_avatar eq 'gravatar') {
1120 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1121 } elsif ($git_avatar eq 'picon') {
1122 # no dependencies
1123 } else {
1124 $git_avatar = '';
1128 # custom error handler: 'die <message>' is Internal Server Error
1129 sub handle_errors_html {
1130 my $msg = shift; # it is already HTML escaped
1132 # to avoid infinite loop where error occurs in die_error,
1133 # change handler to default handler, disabling handle_errors_html
1134 set_message("Error occured when inside die_error:\n$msg");
1136 # you cannot jump out of die_error when called as error handler;
1137 # the subroutine set via CGI::Carp::set_message is called _after_
1138 # HTTP headers are already written, so it cannot write them itself
1139 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1141 set_message(\&handle_errors_html);
1143 # dispatch
1144 sub dispatch {
1145 if (!defined $action) {
1146 if (defined $hash) {
1147 $action = git_get_type($hash);
1148 $action or die_error(404, "Object does not exist");
1149 } elsif (defined $hash_base && defined $file_name) {
1150 $action = git_get_type("$hash_base:$file_name");
1151 $action or die_error(404, "File or directory does not exist");
1152 } elsif (defined $project) {
1153 $action = 'summary';
1154 } else {
1155 $action = 'project_list';
1158 if (!defined($actions{$action})) {
1159 die_error(400, "Unknown action");
1161 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1162 !$project) {
1163 die_error(400, "Project needed");
1165 $actions{$action}->();
1168 sub reset_timer {
1169 our $t0 = [ gettimeofday() ]
1170 if defined $t0;
1171 our $number_of_git_cmds = 0;
1174 our $first_request = 1;
1175 sub run_request {
1176 reset_timer();
1178 evaluate_uri();
1179 if ($first_request) {
1180 evaluate_gitweb_config();
1181 evaluate_git_version();
1183 if ($per_request_config) {
1184 if (ref($per_request_config) eq 'CODE') {
1185 $per_request_config->();
1186 } elsif (!$first_request) {
1187 evaluate_gitweb_config();
1190 check_loadavg();
1192 # $projectroot and $projects_list might be set in gitweb config file
1193 $projects_list ||= $projectroot;
1195 evaluate_query_params();
1196 evaluate_path_info();
1197 evaluate_and_validate_params();
1198 evaluate_git_dir();
1200 configure_gitweb_features();
1202 dispatch();
1205 our $is_last_request = sub { 1 };
1206 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1207 our $CGI = 'CGI';
1208 our $cgi;
1209 sub configure_as_fcgi {
1210 require CGI::Fast;
1211 our $CGI = 'CGI::Fast';
1213 my $request_number = 0;
1214 # let each child service 100 requests
1215 our $is_last_request = sub { ++$request_number > 100 };
1217 sub evaluate_argv {
1218 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1219 configure_as_fcgi()
1220 if $script_name =~ /\.fcgi$/;
1222 return unless (@ARGV);
1224 require Getopt::Long;
1225 Getopt::Long::GetOptions(
1226 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1227 'nproc|n=i' => sub {
1228 my ($arg, $val) = @_;
1229 return unless eval { require FCGI::ProcManager; 1; };
1230 my $proc_manager = FCGI::ProcManager->new({
1231 n_processes => $val,
1233 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1234 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1235 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1240 sub run {
1241 evaluate_argv();
1243 $first_request = 1;
1244 $pre_listen_hook->()
1245 if $pre_listen_hook;
1247 REQUEST:
1248 while ($cgi = $CGI->new()) {
1249 $pre_dispatch_hook->()
1250 if $pre_dispatch_hook;
1252 run_request();
1254 $post_dispatch_hook->()
1255 if $post_dispatch_hook;
1256 $first_request = 0;
1258 last REQUEST if ($is_last_request->());
1261 DONE_GITWEB:
1265 run();
1267 if (defined caller) {
1268 # wrapped in a subroutine processing requests,
1269 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1270 return;
1271 } else {
1272 # pure CGI script, serving single request
1273 exit;
1276 ## ======================================================================
1277 ## action links
1279 # possible values of extra options
1280 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1281 # -replay => 1 - start from a current view (replay with modifications)
1282 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1283 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1284 sub href {
1285 my %params = @_;
1286 # default is to use -absolute url() i.e. $my_uri
1287 my $href = $params{-full} ? $my_url : $my_uri;
1289 # implicit -replay, must be first of implicit params
1290 $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1292 $params{'project'} = $project unless exists $params{'project'};
1294 if ($params{-replay}) {
1295 while (my ($name, $symbol) = each %cgi_param_mapping) {
1296 if (!exists $params{$name}) {
1297 $params{$name} = $input_params{$name};
1302 my $use_pathinfo = gitweb_check_feature('pathinfo');
1303 if (defined $params{'project'} &&
1304 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1305 # try to put as many parameters as possible in PATH_INFO:
1306 # - project name
1307 # - action
1308 # - hash_parent or hash_parent_base:/file_parent
1309 # - hash or hash_base:/filename
1310 # - the snapshot_format as an appropriate suffix
1312 # When the script is the root DirectoryIndex for the domain,
1313 # $href here would be something like http://gitweb.example.com/
1314 # Thus, we strip any trailing / from $href, to spare us double
1315 # slashes in the final URL
1316 $href =~ s,/$,,;
1318 # Then add the project name, if present
1319 $href .= "/".esc_path_info($params{'project'});
1320 delete $params{'project'};
1322 # since we destructively absorb parameters, we keep this
1323 # boolean that remembers if we're handling a snapshot
1324 my $is_snapshot = $params{'action'} eq 'snapshot';
1326 # Summary just uses the project path URL, any other action is
1327 # added to the URL
1328 if (defined $params{'action'}) {
1329 $href .= "/".esc_path_info($params{'action'})
1330 unless $params{'action'} eq 'summary';
1331 delete $params{'action'};
1334 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1335 # stripping nonexistent or useless pieces
1336 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1337 || $params{'hash_parent'} || $params{'hash'});
1338 if (defined $params{'hash_base'}) {
1339 if (defined $params{'hash_parent_base'}) {
1340 $href .= esc_path_info($params{'hash_parent_base'});
1341 # skip the file_parent if it's the same as the file_name
1342 if (defined $params{'file_parent'}) {
1343 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1344 delete $params{'file_parent'};
1345 } elsif ($params{'file_parent'} !~ /\.\./) {
1346 $href .= ":/".esc_path_info($params{'file_parent'});
1347 delete $params{'file_parent'};
1350 $href .= "..";
1351 delete $params{'hash_parent'};
1352 delete $params{'hash_parent_base'};
1353 } elsif (defined $params{'hash_parent'}) {
1354 $href .= esc_path_info($params{'hash_parent'}). "..";
1355 delete $params{'hash_parent'};
1358 $href .= esc_path_info($params{'hash_base'});
1359 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1360 $href .= ":/".esc_path_info($params{'file_name'});
1361 delete $params{'file_name'};
1363 delete $params{'hash'};
1364 delete $params{'hash_base'};
1365 } elsif (defined $params{'hash'}) {
1366 $href .= esc_path_info($params{'hash'});
1367 delete $params{'hash'};
1370 # If the action was a snapshot, we can absorb the
1371 # snapshot_format parameter too
1372 if ($is_snapshot) {
1373 my $fmt = $params{'snapshot_format'};
1374 # snapshot_format should always be defined when href()
1375 # is called, but just in case some code forgets, we
1376 # fall back to the default
1377 $fmt ||= $snapshot_fmts[0];
1378 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1379 delete $params{'snapshot_format'};
1383 # now encode the parameters explicitly
1384 my @result = ();
1385 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1386 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1387 if (defined $params{$name}) {
1388 if (ref($params{$name}) eq "ARRAY") {
1389 foreach my $par (@{$params{$name}}) {
1390 push @result, $symbol . "=" . esc_param($par);
1392 } else {
1393 push @result, $symbol . "=" . esc_param($params{$name});
1397 $href .= "?" . join(';', @result) if scalar @result;
1399 # final transformation: trailing spaces must be escaped (URI-encoded)
1400 $href =~ s/(\s+)$/CGI::escape($1)/e;
1402 if ($params{-anchor}) {
1403 $href .= "#".esc_param($params{-anchor});
1406 return $href;
1410 ## ======================================================================
1411 ## validation, quoting/unquoting and escaping
1413 sub validate_action {
1414 my $input = shift || return undef;
1415 return undef unless exists $actions{$input};
1416 return $input;
1419 sub validate_project {
1420 my $input = shift || return undef;
1421 if (!validate_pathname($input) ||
1422 !(-d "$projectroot/$input") ||
1423 !check_export_ok("$projectroot/$input") ||
1424 ($strict_export && !project_in_list($input))) {
1425 return undef;
1426 } else {
1427 return $input;
1431 sub validate_pathname {
1432 my $input = shift || return undef;
1434 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1435 # at the beginning, at the end, and between slashes.
1436 # also this catches doubled slashes
1437 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1438 return undef;
1440 # no null characters
1441 if ($input =~ m!\0!) {
1442 return undef;
1444 return $input;
1447 sub validate_refname {
1448 my $input = shift || return undef;
1450 # textual hashes are O.K.
1451 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1452 return $input;
1454 # it must be correct pathname
1455 $input = validate_pathname($input)
1456 or return undef;
1457 # restrictions on ref name according to git-check-ref-format
1458 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1459 return undef;
1461 return $input;
1464 # decode sequences of octets in utf8 into Perl's internal form,
1465 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1466 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1467 sub to_utf8 {
1468 my $str = shift;
1469 return undef unless defined $str;
1471 if (utf8::is_utf8($str) || utf8::decode($str)) {
1472 return $str;
1473 } else {
1474 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1478 # quote unsafe chars, but keep the slash, even when it's not
1479 # correct, but quoted slashes look too horrible in bookmarks
1480 sub esc_param {
1481 my $str = shift;
1482 return undef unless defined $str;
1483 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1484 $str =~ s/ /\+/g;
1485 return $str;
1488 # the quoting rules for path_info fragment are slightly different
1489 sub esc_path_info {
1490 my $str = shift;
1491 return undef unless defined $str;
1493 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1494 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1496 return $str;
1499 # quote unsafe chars in whole URL, so some characters cannot be quoted
1500 sub esc_url {
1501 my $str = shift;
1502 return undef unless defined $str;
1503 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1504 $str =~ s/ /\+/g;
1505 return $str;
1508 # quote unsafe characters in HTML attributes
1509 sub esc_attr {
1511 # for XHTML conformance escaping '"' to '&quot;' is not enough
1512 return esc_html(@_);
1515 # replace invalid utf8 character with SUBSTITUTION sequence
1516 sub esc_html {
1517 my $str = shift;
1518 my %opts = @_;
1520 return undef unless defined $str;
1522 $str = to_utf8($str);
1523 $str = $cgi->escapeHTML($str);
1524 if ($opts{'-nbsp'}) {
1525 $str =~ s/ /&nbsp;/g;
1527 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1528 return $str;
1531 # quote control characters and escape filename to HTML
1532 sub esc_path {
1533 my $str = shift;
1534 my %opts = @_;
1536 return undef unless defined $str;
1538 $str = to_utf8($str);
1539 $str = $cgi->escapeHTML($str);
1540 if ($opts{'-nbsp'}) {
1541 $str =~ s/ /&nbsp;/g;
1543 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1544 return $str;
1547 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
1548 sub sanitize {
1549 my $str = shift;
1551 return undef unless defined $str;
1553 $str = to_utf8($str);
1554 $str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg;
1555 return $str;
1558 # Make control characters "printable", using character escape codes (CEC)
1559 sub quot_cec {
1560 my $cntrl = shift;
1561 my %opts = @_;
1562 my %es = ( # character escape codes, aka escape sequences
1563 "\t" => '\t', # tab (HT)
1564 "\n" => '\n', # line feed (LF)
1565 "\r" => '\r', # carrige return (CR)
1566 "\f" => '\f', # form feed (FF)
1567 "\b" => '\b', # backspace (BS)
1568 "\a" => '\a', # alarm (bell) (BEL)
1569 "\e" => '\e', # escape (ESC)
1570 "\013" => '\v', # vertical tab (VT)
1571 "\000" => '\0', # nul character (NUL)
1573 my $chr = ( (exists $es{$cntrl})
1574 ? $es{$cntrl}
1575 : sprintf('\%2x', ord($cntrl)) );
1576 if ($opts{-nohtml}) {
1577 return $chr;
1578 } else {
1579 return "<span class=\"cntrl\">$chr</span>";
1583 # Alternatively use unicode control pictures codepoints,
1584 # Unicode "printable representation" (PR)
1585 sub quot_upr {
1586 my $cntrl = shift;
1587 my %opts = @_;
1589 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1590 if ($opts{-nohtml}) {
1591 return $chr;
1592 } else {
1593 return "<span class=\"cntrl\">$chr</span>";
1597 # git may return quoted and escaped filenames
1598 sub unquote {
1599 my $str = shift;
1601 sub unq {
1602 my $seq = shift;
1603 my %es = ( # character escape codes, aka escape sequences
1604 't' => "\t", # tab (HT, TAB)
1605 'n' => "\n", # newline (NL)
1606 'r' => "\r", # return (CR)
1607 'f' => "\f", # form feed (FF)
1608 'b' => "\b", # backspace (BS)
1609 'a' => "\a", # alarm (bell) (BEL)
1610 'e' => "\e", # escape (ESC)
1611 'v' => "\013", # vertical tab (VT)
1614 if ($seq =~ m/^[0-7]{1,3}$/) {
1615 # octal char sequence
1616 return chr(oct($seq));
1617 } elsif (exists $es{$seq}) {
1618 # C escape sequence, aka character escape code
1619 return $es{$seq};
1621 # quoted ordinary character
1622 return $seq;
1625 if ($str =~ m/^"(.*)"$/) {
1626 # needs unquoting
1627 $str = $1;
1628 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1630 return $str;
1633 # escape tabs (convert tabs to spaces)
1634 sub untabify {
1635 my $line = shift;
1637 while ((my $pos = index($line, "\t")) != -1) {
1638 if (my $count = (8 - ($pos % 8))) {
1639 my $spaces = ' ' x $count;
1640 $line =~ s/\t/$spaces/;
1644 return $line;
1647 sub project_in_list {
1648 my $project = shift;
1649 my @list = git_get_projects_list();
1650 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1653 ## ----------------------------------------------------------------------
1654 ## HTML aware string manipulation
1656 # Try to chop given string on a word boundary between position
1657 # $len and $len+$add_len. If there is no word boundary there,
1658 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1659 # (marking chopped part) would be longer than given string.
1660 sub chop_str {
1661 my $str = shift;
1662 my $len = shift;
1663 my $add_len = shift || 10;
1664 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1666 # Make sure perl knows it is utf8 encoded so we don't
1667 # cut in the middle of a utf8 multibyte char.
1668 $str = to_utf8($str);
1670 # allow only $len chars, but don't cut a word if it would fit in $add_len
1671 # if it doesn't fit, cut it if it's still longer than the dots we would add
1672 # remove chopped character entities entirely
1674 # when chopping in the middle, distribute $len into left and right part
1675 # return early if chopping wouldn't make string shorter
1676 if ($where eq 'center') {
1677 return $str if ($len + 5 >= length($str)); # filler is length 5
1678 $len = int($len/2);
1679 } else {
1680 return $str if ($len + 4 >= length($str)); # filler is length 4
1683 # regexps: ending and beginning with word part up to $add_len
1684 my $endre = qr/.{$len}\w{0,$add_len}/;
1685 my $begre = qr/\w{0,$add_len}.{$len}/;
1687 if ($where eq 'left') {
1688 $str =~ m/^(.*?)($begre)$/;
1689 my ($lead, $body) = ($1, $2);
1690 if (length($lead) > 4) {
1691 $lead = " ...";
1693 return "$lead$body";
1695 } elsif ($where eq 'center') {
1696 $str =~ m/^($endre)(.*)$/;
1697 my ($left, $str) = ($1, $2);
1698 $str =~ m/^(.*?)($begre)$/;
1699 my ($mid, $right) = ($1, $2);
1700 if (length($mid) > 5) {
1701 $mid = " ... ";
1703 return "$left$mid$right";
1705 } else {
1706 $str =~ m/^($endre)(.*)$/;
1707 my $body = $1;
1708 my $tail = $2;
1709 if (length($tail) > 4) {
1710 $tail = "... ";
1712 return "$body$tail";
1716 # takes the same arguments as chop_str, but also wraps a <span> around the
1717 # result with a title attribute if it does get chopped. Additionally, the
1718 # string is HTML-escaped.
1719 sub chop_and_escape_str {
1720 my ($str) = @_;
1722 my $chopped = chop_str(@_);
1723 $str = to_utf8($str);
1724 if ($chopped eq $str) {
1725 return esc_html($chopped);
1726 } else {
1727 $str =~ s/[[:cntrl:]]/?/g;
1728 return $cgi->span({-title=>$str}, esc_html($chopped));
1732 # Highlight selected fragments of string, using given CSS class,
1733 # and escape HTML. It is assumed that fragments do not overlap.
1734 # Regions are passed as list of pairs (array references).
1736 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
1737 # '<span class="mark">foo</span>bar'
1738 sub esc_html_hl_regions {
1739 my ($str, $css_class, @sel) = @_;
1740 return esc_html($str) unless @sel;
1742 my $out = '';
1743 my $pos = 0;
1745 for my $s (@sel) {
1746 $out .= esc_html(substr($str, $pos, $s->[0] - $pos))
1747 if ($s->[0] - $pos > 0);
1748 $out .= $cgi->span({-class => $css_class},
1749 esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));
1751 $pos = $s->[1];
1753 $out .= esc_html(substr($str, $pos))
1754 if ($pos < length($str));
1756 return $out;
1759 # return positions of beginning and end of each match
1760 sub matchpos_list {
1761 my ($str, $regexp) = @_;
1762 return unless (defined $str && defined $regexp);
1764 my @matches;
1765 while ($str =~ /$regexp/g) {
1766 push @matches, [$-[0], $+[0]];
1768 return @matches;
1771 # highlight match (if any), and escape HTML
1772 sub esc_html_match_hl {
1773 my ($str, $regexp) = @_;
1774 return esc_html($str) unless defined $regexp;
1776 my @matches = matchpos_list($str, $regexp);
1777 return esc_html($str) unless @matches;
1779 return esc_html_hl_regions($str, 'match', @matches);
1783 # highlight match (if any) of shortened string, and escape HTML
1784 sub esc_html_match_hl_chopped {
1785 my ($str, $chopped, $regexp) = @_;
1786 return esc_html_match_hl($str, $regexp) unless defined $chopped;
1788 my @matches = matchpos_list($str, $regexp);
1789 return esc_html($chopped) unless @matches;
1791 # filter matches so that we mark chopped string
1792 my $tail = "... "; # see chop_str
1793 unless ($chopped =~ s/\Q$tail\E$//) {
1794 $tail = '';
1796 my $chop_len = length($chopped);
1797 my $tail_len = length($tail);
1798 my @filtered;
1800 for my $m (@matches) {
1801 if ($m->[0] > $chop_len) {
1802 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
1803 last;
1804 } elsif ($m->[1] > $chop_len) {
1805 push @filtered, [ $m->[0], $chop_len + $tail_len ];
1806 last;
1808 push @filtered, $m;
1811 return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
1814 ## ----------------------------------------------------------------------
1815 ## functions returning short strings
1817 # CSS class for given age value (in seconds)
1818 sub age_class {
1819 my $age = shift;
1821 if (!defined $age) {
1822 return "noage";
1823 } elsif ($age < 60*60*2) {
1824 return "age0";
1825 } elsif ($age < 60*60*24*2) {
1826 return "age1";
1827 } else {
1828 return "age2";
1832 # convert age in seconds to "nn units ago" string
1833 sub age_string {
1834 my $age = shift;
1835 my $age_str;
1837 if ($age > 60*60*24*365*2) {
1838 $age_str = (int $age/60/60/24/365);
1839 $age_str .= " years ago";
1840 } elsif ($age > 60*60*24*(365/12)*2) {
1841 $age_str = int $age/60/60/24/(365/12);
1842 $age_str .= " months ago";
1843 } elsif ($age > 60*60*24*7*2) {
1844 $age_str = int $age/60/60/24/7;
1845 $age_str .= " weeks ago";
1846 } elsif ($age > 60*60*24*2) {
1847 $age_str = int $age/60/60/24;
1848 $age_str .= " days ago";
1849 } elsif ($age > 60*60*2) {
1850 $age_str = int $age/60/60;
1851 $age_str .= " hours ago";
1852 } elsif ($age > 60*2) {
1853 $age_str = int $age/60;
1854 $age_str .= " min ago";
1855 } elsif ($age > 2) {
1856 $age_str = int $age;
1857 $age_str .= " sec ago";
1858 } else {
1859 $age_str .= " right now";
1861 return $age_str;
1864 use constant {
1865 S_IFINVALID => 0030000,
1866 S_IFGITLINK => 0160000,
1869 # submodule/subproject, a commit object reference
1870 sub S_ISGITLINK {
1871 my $mode = shift;
1873 return (($mode & S_IFMT) == S_IFGITLINK)
1876 # convert file mode in octal to symbolic file mode string
1877 sub mode_str {
1878 my $mode = oct shift;
1880 if (S_ISGITLINK($mode)) {
1881 return 'm---------';
1882 } elsif (S_ISDIR($mode & S_IFMT)) {
1883 return 'drwxr-xr-x';
1884 } elsif (S_ISLNK($mode)) {
1885 return 'lrwxrwxrwx';
1886 } elsif (S_ISREG($mode)) {
1887 # git cares only about the executable bit
1888 if ($mode & S_IXUSR) {
1889 return '-rwxr-xr-x';
1890 } else {
1891 return '-rw-r--r--';
1893 } else {
1894 return '----------';
1898 # convert file mode in octal to file type string
1899 sub file_type {
1900 my $mode = shift;
1902 if ($mode !~ m/^[0-7]+$/) {
1903 return $mode;
1904 } else {
1905 $mode = oct $mode;
1908 if (S_ISGITLINK($mode)) {
1909 return "submodule";
1910 } elsif (S_ISDIR($mode & S_IFMT)) {
1911 return "directory";
1912 } elsif (S_ISLNK($mode)) {
1913 return "symlink";
1914 } elsif (S_ISREG($mode)) {
1915 return "file";
1916 } else {
1917 return "unknown";
1921 # convert file mode in octal to file type description string
1922 sub file_type_long {
1923 my $mode = shift;
1925 if ($mode !~ m/^[0-7]+$/) {
1926 return $mode;
1927 } else {
1928 $mode = oct $mode;
1931 if (S_ISGITLINK($mode)) {
1932 return "submodule";
1933 } elsif (S_ISDIR($mode & S_IFMT)) {
1934 return "directory";
1935 } elsif (S_ISLNK($mode)) {
1936 return "symlink";
1937 } elsif (S_ISREG($mode)) {
1938 if ($mode & S_IXUSR) {
1939 return "executable";
1940 } else {
1941 return "file";
1943 } else {
1944 return "unknown";
1949 ## ----------------------------------------------------------------------
1950 ## functions returning short HTML fragments, or transforming HTML fragments
1951 ## which don't belong to other sections
1953 # format line of commit message.
1954 sub format_log_line_html {
1955 my $line = shift;
1957 $line = esc_html($line, -nbsp=>1);
1958 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1959 $cgi->a({-href => href(action=>"object", hash=>$1),
1960 -class => "text"}, $1);
1961 }eg;
1963 return $line;
1966 # format marker of refs pointing to given object
1968 # the destination action is chosen based on object type and current context:
1969 # - for annotated tags, we choose the tag view unless it's the current view
1970 # already, in which case we go to shortlog view
1971 # - for other refs, we keep the current view if we're in history, shortlog or
1972 # log view, and select shortlog otherwise
1973 sub format_ref_marker {
1974 my ($refs, $id) = @_;
1975 my $markers = '';
1977 if (defined $refs->{$id}) {
1978 foreach my $ref (@{$refs->{$id}}) {
1979 # this code exploits the fact that non-lightweight tags are the
1980 # only indirect objects, and that they are the only objects for which
1981 # we want to use tag instead of shortlog as action
1982 my ($type, $name) = qw();
1983 my $indirect = ($ref =~ s/\^\{\}$//);
1984 # e.g. tags/v2.6.11 or heads/next
1985 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1986 $type = $1;
1987 $name = $2;
1988 } else {
1989 $type = "ref";
1990 $name = $ref;
1993 my $class = $type;
1994 $class .= " indirect" if $indirect;
1996 my $dest_action = "shortlog";
1998 if ($indirect) {
1999 $dest_action = "tag" unless $action eq "tag";
2000 } elsif ($action =~ /^(history|(short)?log)$/) {
2001 $dest_action = $action;
2004 my $dest = "";
2005 $dest .= "refs/" unless $ref =~ m!^refs/!;
2006 $dest .= $ref;
2008 my $link = $cgi->a({
2009 -href => href(
2010 action=>$dest_action,
2011 hash=>$dest
2012 )}, $name);
2014 $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2015 $link . "</span>";
2019 if ($markers) {
2020 return ' <span class="refs">'. $markers . '</span>';
2021 } else {
2022 return "";
2026 # format, perhaps shortened and with markers, title line
2027 sub format_subject_html {
2028 my ($long, $short, $href, $extra) = @_;
2029 $extra = '' unless defined($extra);
2031 if (length($short) < length($long)) {
2032 $long =~ s/[[:cntrl:]]/?/g;
2033 return $cgi->a({-href => $href, -class => "list subject",
2034 -title => to_utf8($long)},
2035 esc_html($short)) . $extra;
2036 } else {
2037 return $cgi->a({-href => $href, -class => "list subject"},
2038 esc_html($long)) . $extra;
2042 # Rather than recomputing the url for an email multiple times, we cache it
2043 # after the first hit. This gives a visible benefit in views where the avatar
2044 # for the same email is used repeatedly (e.g. shortlog).
2045 # The cache is shared by all avatar engines (currently gravatar only), which
2046 # are free to use it as preferred. Since only one avatar engine is used for any
2047 # given page, there's no risk for cache conflicts.
2048 our %avatar_cache = ();
2050 # Compute the picon url for a given email, by using the picon search service over at
2051 # http://www.cs.indiana.edu/picons/search.html
2052 sub picon_url {
2053 my $email = lc shift;
2054 if (!$avatar_cache{$email}) {
2055 my ($user, $domain) = split('@', $email);
2056 $avatar_cache{$email} =
2057 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2058 "$domain/$user/" .
2059 "users+domains+unknown/up/single";
2061 return $avatar_cache{$email};
2064 # Compute the gravatar url for a given email, if it's not in the cache already.
2065 # Gravatar stores only the part of the URL before the size, since that's the
2066 # one computationally more expensive. This also allows reuse of the cache for
2067 # different sizes (for this particular engine).
2068 sub gravatar_url {
2069 my $email = lc shift;
2070 my $size = shift;
2071 $avatar_cache{$email} ||=
2072 "http://www.gravatar.com/avatar/" .
2073 Digest::MD5::md5_hex($email) . "?s=";
2074 return $avatar_cache{$email} . $size;
2077 # Insert an avatar for the given $email at the given $size if the feature
2078 # is enabled.
2079 sub git_get_avatar {
2080 my ($email, %opts) = @_;
2081 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
2082 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
2083 $opts{-size} ||= 'default';
2084 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2085 my $url = "";
2086 if ($git_avatar eq 'gravatar') {
2087 $url = gravatar_url($email, $size);
2088 } elsif ($git_avatar eq 'picon') {
2089 $url = picon_url($email);
2091 # Other providers can be added by extending the if chain, defining $url
2092 # as needed. If no variant puts something in $url, we assume avatars
2093 # are completely disabled/unavailable.
2094 if ($url) {
2095 return $pre_white .
2096 "<img width=\"$size\" " .
2097 "class=\"avatar\" " .
2098 "src=\"".esc_url($url)."\" " .
2099 "alt=\"\" " .
2100 "/>" . $post_white;
2101 } else {
2102 return "";
2106 sub format_search_author {
2107 my ($author, $searchtype, $displaytext) = @_;
2108 my $have_search = gitweb_check_feature('search');
2110 if ($have_search) {
2111 my $performed = "";
2112 if ($searchtype eq 'author') {
2113 $performed = "authored";
2114 } elsif ($searchtype eq 'committer') {
2115 $performed = "committed";
2118 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2119 searchtext=>$author,
2120 searchtype=>$searchtype), class=>"list",
2121 title=>"Search for commits $performed by $author"},
2122 $displaytext);
2124 } else {
2125 return $displaytext;
2129 # format the author name of the given commit with the given tag
2130 # the author name is chopped and escaped according to the other
2131 # optional parameters (see chop_str).
2132 sub format_author_html {
2133 my $tag = shift;
2134 my $co = shift;
2135 my $author = chop_and_escape_str($co->{'author_name'}, @_);
2136 return "<$tag class=\"author\">" .
2137 format_search_author($co->{'author_name'}, "author",
2138 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2139 $author) .
2140 "</$tag>";
2143 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2144 sub format_git_diff_header_line {
2145 my $line = shift;
2146 my $diffinfo = shift;
2147 my ($from, $to) = @_;
2149 if ($diffinfo->{'nparents'}) {
2150 # combined diff
2151 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2152 if ($to->{'href'}) {
2153 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2154 esc_path($to->{'file'}));
2155 } else { # file was deleted (no href)
2156 $line .= esc_path($to->{'file'});
2158 } else {
2159 # "ordinary" diff
2160 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2161 if ($from->{'href'}) {
2162 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2163 'a/' . esc_path($from->{'file'}));
2164 } else { # file was added (no href)
2165 $line .= 'a/' . esc_path($from->{'file'});
2167 $line .= ' ';
2168 if ($to->{'href'}) {
2169 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2170 'b/' . esc_path($to->{'file'}));
2171 } else { # file was deleted
2172 $line .= 'b/' . esc_path($to->{'file'});
2176 return "<div class=\"diff header\">$line</div>\n";
2179 # format extended diff header line, before patch itself
2180 sub format_extended_diff_header_line {
2181 my $line = shift;
2182 my $diffinfo = shift;
2183 my ($from, $to) = @_;
2185 # match <path>
2186 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2187 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2188 esc_path($from->{'file'}));
2190 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2191 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2192 esc_path($to->{'file'}));
2194 # match single <mode>
2195 if ($line =~ m/\s(\d{6})$/) {
2196 $line .= '<span class="info"> (' .
2197 file_type_long($1) .
2198 ')</span>';
2200 # match <hash>
2201 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2202 # can match only for combined diff
2203 $line = 'index ';
2204 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2205 if ($from->{'href'}[$i]) {
2206 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2207 -class=>"hash"},
2208 substr($diffinfo->{'from_id'}[$i],0,7));
2209 } else {
2210 $line .= '0' x 7;
2212 # separator
2213 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2215 $line .= '..';
2216 if ($to->{'href'}) {
2217 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2218 substr($diffinfo->{'to_id'},0,7));
2219 } else {
2220 $line .= '0' x 7;
2223 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2224 # can match only for ordinary diff
2225 my ($from_link, $to_link);
2226 if ($from->{'href'}) {
2227 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2228 substr($diffinfo->{'from_id'},0,7));
2229 } else {
2230 $from_link = '0' x 7;
2232 if ($to->{'href'}) {
2233 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2234 substr($diffinfo->{'to_id'},0,7));
2235 } else {
2236 $to_link = '0' x 7;
2238 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2239 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2242 return $line . "<br/>\n";
2245 # format from-file/to-file diff header
2246 sub format_diff_from_to_header {
2247 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2248 my $line;
2249 my $result = '';
2251 $line = $from_line;
2252 #assert($line =~ m/^---/) if DEBUG;
2253 # no extra formatting for "^--- /dev/null"
2254 if (! $diffinfo->{'nparents'}) {
2255 # ordinary (single parent) diff
2256 if ($line =~ m!^--- "?a/!) {
2257 if ($from->{'href'}) {
2258 $line = '--- a/' .
2259 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2260 esc_path($from->{'file'}));
2261 } else {
2262 $line = '--- a/' .
2263 esc_path($from->{'file'});
2266 $result .= qq!<div class="diff from_file">$line</div>\n!;
2268 } else {
2269 # combined diff (merge commit)
2270 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2271 if ($from->{'href'}[$i]) {
2272 $line = '--- ' .
2273 $cgi->a({-href=>href(action=>"blobdiff",
2274 hash_parent=>$diffinfo->{'from_id'}[$i],
2275 hash_parent_base=>$parents[$i],
2276 file_parent=>$from->{'file'}[$i],
2277 hash=>$diffinfo->{'to_id'},
2278 hash_base=>$hash,
2279 file_name=>$to->{'file'}),
2280 -class=>"path",
2281 -title=>"diff" . ($i+1)},
2282 $i+1) .
2283 '/' .
2284 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2285 esc_path($from->{'file'}[$i]));
2286 } else {
2287 $line = '--- /dev/null';
2289 $result .= qq!<div class="diff from_file">$line</div>\n!;
2293 $line = $to_line;
2294 #assert($line =~ m/^\+\+\+/) if DEBUG;
2295 # no extra formatting for "^+++ /dev/null"
2296 if ($line =~ m!^\+\+\+ "?b/!) {
2297 if ($to->{'href'}) {
2298 $line = '+++ b/' .
2299 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2300 esc_path($to->{'file'}));
2301 } else {
2302 $line = '+++ b/' .
2303 esc_path($to->{'file'});
2306 $result .= qq!<div class="diff to_file">$line</div>\n!;
2308 return $result;
2311 # create note for patch simplified by combined diff
2312 sub format_diff_cc_simplified {
2313 my ($diffinfo, @parents) = @_;
2314 my $result = '';
2316 $result .= "<div class=\"diff header\">" .
2317 "diff --cc ";
2318 if (!is_deleted($diffinfo)) {
2319 $result .= $cgi->a({-href => href(action=>"blob",
2320 hash_base=>$hash,
2321 hash=>$diffinfo->{'to_id'},
2322 file_name=>$diffinfo->{'to_file'}),
2323 -class => "path"},
2324 esc_path($diffinfo->{'to_file'}));
2325 } else {
2326 $result .= esc_path($diffinfo->{'to_file'});
2328 $result .= "</div>\n" . # class="diff header"
2329 "<div class=\"diff nodifferences\">" .
2330 "Simple merge" .
2331 "</div>\n"; # class="diff nodifferences"
2333 return $result;
2336 sub diff_line_class {
2337 my ($line, $from, $to) = @_;
2339 # ordinary diff
2340 my $num_sign = 1;
2341 # combined diff
2342 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2343 $num_sign = scalar @{$from->{'href'}};
2346 my @diff_line_classifier = (
2347 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
2348 { regexp => qr/^\\/, class => "incomplete" },
2349 { regexp => qr/^ {$num_sign}/, class => "ctx" },
2350 # classifier for context must come before classifier add/rem,
2351 # or we would have to use more complicated regexp, for example
2352 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2353 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
2354 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
2356 for my $clsfy (@diff_line_classifier) {
2357 return $clsfy->{'class'}
2358 if ($line =~ $clsfy->{'regexp'});
2361 # fallback
2362 return "";
2365 # assumes that $from and $to are defined and correctly filled,
2366 # and that $line holds a line of chunk header for unified diff
2367 sub format_unidiff_chunk_header {
2368 my ($line, $from, $to) = @_;
2370 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2371 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2373 $from_lines = 0 unless defined $from_lines;
2374 $to_lines = 0 unless defined $to_lines;
2376 if ($from->{'href'}) {
2377 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2378 -class=>"list"}, $from_text);
2380 if ($to->{'href'}) {
2381 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2382 -class=>"list"}, $to_text);
2384 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2385 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2386 return $line;
2389 # assumes that $from and $to are defined and correctly filled,
2390 # and that $line holds a line of chunk header for combined diff
2391 sub format_cc_diff_chunk_header {
2392 my ($line, $from, $to) = @_;
2394 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2395 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2397 @from_text = split(' ', $ranges);
2398 for (my $i = 0; $i < @from_text; ++$i) {
2399 ($from_start[$i], $from_nlines[$i]) =
2400 (split(',', substr($from_text[$i], 1)), 0);
2403 $to_text = pop @from_text;
2404 $to_start = pop @from_start;
2405 $to_nlines = pop @from_nlines;
2407 $line = "<span class=\"chunk_info\">$prefix ";
2408 for (my $i = 0; $i < @from_text; ++$i) {
2409 if ($from->{'href'}[$i]) {
2410 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2411 -class=>"list"}, $from_text[$i]);
2412 } else {
2413 $line .= $from_text[$i];
2415 $line .= " ";
2417 if ($to->{'href'}) {
2418 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2419 -class=>"list"}, $to_text);
2420 } else {
2421 $line .= $to_text;
2423 $line .= " $prefix</span>" .
2424 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2425 return $line;
2428 # process patch (diff) line (not to be used for diff headers),
2429 # returning class and HTML-formatted (but not wrapped) line
2430 sub process_diff_line {
2431 my $line = shift;
2432 my ($from, $to) = @_;
2434 my $diff_class = diff_line_class($line, $from, $to);
2436 chomp $line;
2437 $line = untabify($line);
2439 if ($from && $to && $line =~ m/^\@{2} /) {
2440 $line = format_unidiff_chunk_header($line, $from, $to);
2441 return $diff_class, $line;
2443 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2444 $line = format_cc_diff_chunk_header($line, $from, $to);
2445 return $diff_class, $line;
2448 return $diff_class, esc_html($line, -nbsp=>1);
2451 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2452 # linked. Pass the hash of the tree/commit to snapshot.
2453 sub format_snapshot_links {
2454 my ($hash) = @_;
2455 my $num_fmts = @snapshot_fmts;
2456 if ($num_fmts > 1) {
2457 # A parenthesized list of links bearing format names.
2458 # e.g. "snapshot (_tar.gz_ _zip_)"
2459 return "snapshot (" . join(' ', map
2460 $cgi->a({
2461 -href => href(
2462 action=>"snapshot",
2463 hash=>$hash,
2464 snapshot_format=>$_
2466 }, $known_snapshot_formats{$_}{'display'})
2467 , @snapshot_fmts) . ")";
2468 } elsif ($num_fmts == 1) {
2469 # A single "snapshot" link whose tooltip bears the format name.
2470 # i.e. "_snapshot_"
2471 my ($fmt) = @snapshot_fmts;
2472 return
2473 $cgi->a({
2474 -href => href(
2475 action=>"snapshot",
2476 hash=>$hash,
2477 snapshot_format=>$fmt
2479 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2480 }, "snapshot");
2481 } else { # $num_fmts == 0
2482 return undef;
2486 ## ......................................................................
2487 ## functions returning values to be passed, perhaps after some
2488 ## transformation, to other functions; e.g. returning arguments to href()
2490 # returns hash to be passed to href to generate gitweb URL
2491 # in -title key it returns description of link
2492 sub get_feed_info {
2493 my $format = shift || 'Atom';
2494 my %res = (action => lc($format));
2496 # feed links are possible only for project views
2497 return unless (defined $project);
2498 # some views should link to OPML, or to generic project feed,
2499 # or don't have specific feed yet (so they should use generic)
2500 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2502 my $branch;
2503 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2504 # from tag links; this also makes possible to detect branch links
2505 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2506 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
2507 $branch = $1;
2509 # find log type for feed description (title)
2510 my $type = 'log';
2511 if (defined $file_name) {
2512 $type = "history of $file_name";
2513 $type .= "/" if ($action eq 'tree');
2514 $type .= " on '$branch'" if (defined $branch);
2515 } else {
2516 $type = "log of $branch" if (defined $branch);
2519 $res{-title} = $type;
2520 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2521 $res{'file_name'} = $file_name;
2523 return %res;
2526 ## ----------------------------------------------------------------------
2527 ## git utility subroutines, invoking git commands
2529 # returns path to the core git executable and the --git-dir parameter as list
2530 sub git_cmd {
2531 $number_of_git_cmds++;
2532 return $GIT, '--git-dir='.$git_dir;
2535 # quote the given arguments for passing them to the shell
2536 # quote_command("command", "arg 1", "arg with ' and ! characters")
2537 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2538 # Try to avoid using this function wherever possible.
2539 sub quote_command {
2540 return join(' ',
2541 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2544 # get HEAD ref of given project as hash
2545 sub git_get_head_hash {
2546 return git_get_full_hash(shift, 'HEAD');
2549 sub git_get_full_hash {
2550 return git_get_hash(@_);
2553 sub git_get_short_hash {
2554 return git_get_hash(@_, '--short=7');
2557 sub git_get_hash {
2558 my ($project, $hash, @options) = @_;
2559 my $o_git_dir = $git_dir;
2560 my $retval = undef;
2561 $git_dir = "$projectroot/$project";
2562 if (open my $fd, '-|', git_cmd(), 'rev-parse',
2563 '--verify', '-q', @options, $hash) {
2564 $retval = <$fd>;
2565 chomp $retval if defined $retval;
2566 close $fd;
2568 if (defined $o_git_dir) {
2569 $git_dir = $o_git_dir;
2571 return $retval;
2574 # get type of given object
2575 sub git_get_type {
2576 my $hash = shift;
2578 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2579 my $type = <$fd>;
2580 close $fd or return;
2581 chomp $type;
2582 return $type;
2585 # repository configuration
2586 our $config_file = '';
2587 our %config;
2589 # store multiple values for single key as anonymous array reference
2590 # single values stored directly in the hash, not as [ <value> ]
2591 sub hash_set_multi {
2592 my ($hash, $key, $value) = @_;
2594 if (!exists $hash->{$key}) {
2595 $hash->{$key} = $value;
2596 } elsif (!ref $hash->{$key}) {
2597 $hash->{$key} = [ $hash->{$key}, $value ];
2598 } else {
2599 push @{$hash->{$key}}, $value;
2603 # return hash of git project configuration
2604 # optionally limited to some section, e.g. 'gitweb'
2605 sub git_parse_project_config {
2606 my $section_regexp = shift;
2607 my %config;
2609 local $/ = "\0";
2611 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2612 or return;
2614 while (my $keyval = <$fh>) {
2615 chomp $keyval;
2616 my ($key, $value) = split(/\n/, $keyval, 2);
2618 hash_set_multi(\%config, $key, $value)
2619 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2621 close $fh;
2623 return %config;
2626 # convert config value to boolean: 'true' or 'false'
2627 # no value, number > 0, 'true' and 'yes' values are true
2628 # rest of values are treated as false (never as error)
2629 sub config_to_bool {
2630 my $val = shift;
2632 return 1 if !defined $val; # section.key
2634 # strip leading and trailing whitespace
2635 $val =~ s/^\s+//;
2636 $val =~ s/\s+$//;
2638 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2639 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2642 # convert config value to simple decimal number
2643 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2644 # to be multiplied by 1024, 1048576, or 1073741824
2645 sub config_to_int {
2646 my $val = shift;
2648 # strip leading and trailing whitespace
2649 $val =~ s/^\s+//;
2650 $val =~ s/\s+$//;
2652 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2653 $unit = lc($unit);
2654 # unknown unit is treated as 1
2655 return $num * ($unit eq 'g' ? 1073741824 :
2656 $unit eq 'm' ? 1048576 :
2657 $unit eq 'k' ? 1024 : 1);
2659 return $val;
2662 # convert config value to array reference, if needed
2663 sub config_to_multi {
2664 my $val = shift;
2666 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2669 sub git_get_project_config {
2670 my ($key, $type) = @_;
2672 return unless defined $git_dir;
2674 # key sanity check
2675 return unless ($key);
2676 # only subsection, if exists, is case sensitive,
2677 # and not lowercased by 'git config -z -l'
2678 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2679 $key = join(".", lc($hi), $mi, lc($lo));
2680 } else {
2681 $key = lc($key);
2683 $key =~ s/^gitweb\.//;
2684 return if ($key =~ m/\W/);
2686 # type sanity check
2687 if (defined $type) {
2688 $type =~ s/^--//;
2689 $type = undef
2690 unless ($type eq 'bool' || $type eq 'int');
2693 # get config
2694 if (!defined $config_file ||
2695 $config_file ne "$git_dir/config") {
2696 %config = git_parse_project_config('gitweb');
2697 $config_file = "$git_dir/config";
2700 # check if config variable (key) exists
2701 return unless exists $config{"gitweb.$key"};
2703 # ensure given type
2704 if (!defined $type) {
2705 return $config{"gitweb.$key"};
2706 } elsif ($type eq 'bool') {
2707 # backward compatibility: 'git config --bool' returns true/false
2708 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2709 } elsif ($type eq 'int') {
2710 return config_to_int($config{"gitweb.$key"});
2712 return $config{"gitweb.$key"};
2715 # get hash of given path at given ref
2716 sub git_get_hash_by_path {
2717 my $base = shift;
2718 my $path = shift || return undef;
2719 my $type = shift;
2721 $path =~ s,/+$,,;
2723 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2724 or die_error(500, "Open git-ls-tree failed");
2725 my $line = <$fd>;
2726 close $fd or return undef;
2728 if (!defined $line) {
2729 # there is no tree or hash given by $path at $base
2730 return undef;
2733 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2734 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2735 if (defined $type && $type ne $2) {
2736 # type doesn't match
2737 return undef;
2739 return $3;
2742 # get path of entry with given hash at given tree-ish (ref)
2743 # used to get 'from' filename for combined diff (merge commit) for renames
2744 sub git_get_path_by_hash {
2745 my $base = shift || return;
2746 my $hash = shift || return;
2748 local $/ = "\0";
2750 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2751 or return undef;
2752 while (my $line = <$fd>) {
2753 chomp $line;
2755 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2756 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2757 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2758 close $fd;
2759 return $1;
2762 close $fd;
2763 return undef;
2766 ## ......................................................................
2767 ## git utility functions, directly accessing git repository
2769 # get the value of config variable either from file named as the variable
2770 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
2771 # configuration variable in the repository config file.
2772 sub git_get_file_or_project_config {
2773 my ($path, $name) = @_;
2775 $git_dir = "$projectroot/$path";
2776 open my $fd, '<', "$git_dir/$name"
2777 or return git_get_project_config($name);
2778 my $conf = <$fd>;
2779 close $fd;
2780 if (defined $conf) {
2781 chomp $conf;
2783 return $conf;
2786 sub git_get_project_description {
2787 my $path = shift;
2788 return git_get_file_or_project_config($path, 'description');
2791 sub git_get_project_category {
2792 my $path = shift;
2793 return git_get_file_or_project_config($path, 'category');
2797 # supported formats:
2798 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2799 # - if its contents is a number, use it as tag weight,
2800 # - otherwise add a tag with weight 1
2801 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2802 # the same value multiple times increases tag weight
2803 # * `gitweb.ctag' multi-valued repo config variable
2804 sub git_get_project_ctags {
2805 my $project = shift;
2806 my $ctags = {};
2808 $git_dir = "$projectroot/$project";
2809 if (opendir my $dh, "$git_dir/ctags") {
2810 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2811 foreach my $tagfile (@files) {
2812 open my $ct, '<', $tagfile
2813 or next;
2814 my $val = <$ct>;
2815 chomp $val if $val;
2816 close $ct;
2818 (my $ctag = $tagfile) =~ s#.*/##;
2819 if ($val =~ /^\d+$/) {
2820 $ctags->{$ctag} = $val;
2821 } else {
2822 $ctags->{$ctag} = 1;
2825 closedir $dh;
2827 } elsif (open my $fh, '<', "$git_dir/ctags") {
2828 while (my $line = <$fh>) {
2829 chomp $line;
2830 $ctags->{$line}++ if $line;
2832 close $fh;
2834 } else {
2835 my $taglist = config_to_multi(git_get_project_config('ctag'));
2836 foreach my $tag (@$taglist) {
2837 $ctags->{$tag}++;
2841 return $ctags;
2844 # return hash, where keys are content tags ('ctags'),
2845 # and values are sum of weights of given tag in every project
2846 sub git_gather_all_ctags {
2847 my $projects = shift;
2848 my $ctags = {};
2850 foreach my $p (@$projects) {
2851 foreach my $ct (keys %{$p->{'ctags'}}) {
2852 $ctags->{$ct} += $p->{'ctags'}->{$ct};
2856 return $ctags;
2859 sub git_populate_project_tagcloud {
2860 my $ctags = shift;
2862 # First, merge different-cased tags; tags vote on casing
2863 my %ctags_lc;
2864 foreach (keys %$ctags) {
2865 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2866 if (not $ctags_lc{lc $_}->{topcount}
2867 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2868 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2869 $ctags_lc{lc $_}->{topname} = $_;
2873 my $cloud;
2874 my $matched = $input_params{'ctag'};
2875 if (eval { require HTML::TagCloud; 1; }) {
2876 $cloud = HTML::TagCloud->new;
2877 foreach my $ctag (sort keys %ctags_lc) {
2878 # Pad the title with spaces so that the cloud looks
2879 # less crammed.
2880 my $title = esc_html($ctags_lc{$ctag}->{topname});
2881 $title =~ s/ /&nbsp;/g;
2882 $title =~ s/^/&nbsp;/g;
2883 $title =~ s/$/&nbsp;/g;
2884 if (defined $matched && $matched eq $ctag) {
2885 $title = qq(<span class="match">$title</span>);
2887 $cloud->add($title, href(project=>undef, ctag=>$ctag),
2888 $ctags_lc{$ctag}->{count});
2890 } else {
2891 $cloud = {};
2892 foreach my $ctag (keys %ctags_lc) {
2893 my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
2894 if (defined $matched && $matched eq $ctag) {
2895 $title = qq(<span class="match">$title</span>);
2897 $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2898 $cloud->{$ctag}{ctag} =
2899 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
2902 return $cloud;
2905 sub git_show_project_tagcloud {
2906 my ($cloud, $count) = @_;
2907 if (ref $cloud eq 'HTML::TagCloud') {
2908 return $cloud->html_and_css($count);
2909 } else {
2910 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2911 return
2912 '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2913 join (', ', map {
2914 $cloud->{$_}->{'ctag'}
2915 } splice(@tags, 0, $count)) .
2916 '</div>';
2920 sub git_get_project_url_list {
2921 my $path = shift;
2923 $git_dir = "$projectroot/$path";
2924 open my $fd, '<', "$git_dir/cloneurl"
2925 or return wantarray ?
2926 @{ config_to_multi(git_get_project_config('url')) } :
2927 config_to_multi(git_get_project_config('url'));
2928 my @git_project_url_list = map { chomp; $_ } <$fd>;
2929 close $fd;
2931 return wantarray ? @git_project_url_list : \@git_project_url_list;
2934 sub git_get_projects_list {
2935 my $filter = shift || '';
2936 my $paranoid = shift;
2937 my @list;
2939 if (-d $projects_list) {
2940 # search in directory
2941 my $dir = $projects_list;
2942 # remove the trailing "/"
2943 $dir =~ s!/+$!!;
2944 my $pfxlen = length("$dir");
2945 my $pfxdepth = ($dir =~ tr!/!!);
2946 # when filtering, search only given subdirectory
2947 if ($filter && !$paranoid) {
2948 $dir .= "/$filter";
2949 $dir =~ s!/+$!!;
2952 File::Find::find({
2953 follow_fast => 1, # follow symbolic links
2954 follow_skip => 2, # ignore duplicates
2955 dangling_symlinks => 0, # ignore dangling symlinks, silently
2956 wanted => sub {
2957 # global variables
2958 our $project_maxdepth;
2959 our $projectroot;
2960 # skip project-list toplevel, if we get it.
2961 return if (m!^[/.]$!);
2962 # only directories can be git repositories
2963 return unless (-d $_);
2964 # don't traverse too deep (Find is super slow on os x)
2965 # $project_maxdepth excludes depth of $projectroot
2966 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2967 $File::Find::prune = 1;
2968 return;
2971 my $path = substr($File::Find::name, $pfxlen + 1);
2972 # paranoidly only filter here
2973 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
2974 next;
2976 # we check related file in $projectroot
2977 if (check_export_ok("$projectroot/$path")) {
2978 push @list, { path => $path };
2979 $File::Find::prune = 1;
2982 }, "$dir");
2984 } elsif (-f $projects_list) {
2985 # read from file(url-encoded):
2986 # 'git%2Fgit.git Linus+Torvalds'
2987 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2988 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2989 open my $fd, '<', $projects_list or return;
2990 PROJECT:
2991 while (my $line = <$fd>) {
2992 chomp $line;
2993 my ($path, $owner) = split ' ', $line;
2994 $path = unescape($path);
2995 $owner = unescape($owner);
2996 if (!defined $path) {
2997 next;
2999 # if $filter is rpovided, check if $path begins with $filter
3000 if ($filter && $path !~ m!^\Q$filter\E/!) {
3001 next;
3003 if (check_export_ok("$projectroot/$path")) {
3004 my $pr = {
3005 path => $path,
3006 owner => to_utf8($owner),
3008 push @list, $pr;
3011 close $fd;
3013 return @list;
3016 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3017 # as side effects it sets 'forks' field to list of forks for forked projects
3018 sub filter_forks_from_projects_list {
3019 my $projects = shift;
3021 my %trie; # prefix tree of directories (path components)
3022 # generate trie out of those directories that might contain forks
3023 foreach my $pr (@$projects) {
3024 my $path = $pr->{'path'};
3025 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3026 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3027 next unless ($path); # skip '.git' repository: tests, git-instaweb
3028 next unless (-d "$projectroot/$path"); # containing directory exists
3029 $pr->{'forks'} = []; # there can be 0 or more forks of project
3031 # add to trie
3032 my @dirs = split('/', $path);
3033 # walk the trie, until either runs out of components or out of trie
3034 my $ref = \%trie;
3035 while (scalar @dirs &&
3036 exists($ref->{$dirs[0]})) {
3037 $ref = $ref->{shift @dirs};
3039 # create rest of trie structure from rest of components
3040 foreach my $dir (@dirs) {
3041 $ref = $ref->{$dir} = {};
3043 # create end marker, store $pr as a data
3044 $ref->{''} = $pr if (!exists $ref->{''});
3047 # filter out forks, by finding shortest prefix match for paths
3048 my @filtered;
3049 PROJECT:
3050 foreach my $pr (@$projects) {
3051 # trie lookup
3052 my $ref = \%trie;
3053 DIR:
3054 foreach my $dir (split('/', $pr->{'path'})) {
3055 if (exists $ref->{''}) {
3056 # found [shortest] prefix, is a fork - skip it
3057 push @{$ref->{''}{'forks'}}, $pr;
3058 next PROJECT;
3060 if (!exists $ref->{$dir}) {
3061 # not in trie, cannot have prefix, not a fork
3062 push @filtered, $pr;
3063 next PROJECT;
3065 # If the dir is there, we just walk one step down the trie.
3066 $ref = $ref->{$dir};
3068 # we ran out of trie
3069 # (shouldn't happen: it's either no match, or end marker)
3070 push @filtered, $pr;
3073 return @filtered;
3076 # note: fill_project_list_info must be run first,
3077 # for 'descr_long' and 'ctags' to be filled
3078 sub search_projects_list {
3079 my ($projlist, %opts) = @_;
3080 my $tagfilter = $opts{'tagfilter'};
3081 my $search_re = $opts{'search_regexp'};
3083 return @$projlist
3084 unless ($tagfilter || $search_re);
3086 # searching projects require filling to be run before it;
3087 fill_project_list_info($projlist,
3088 $tagfilter ? 'ctags' : (),
3089 $search_re ? ('path', 'descr') : ());
3090 my @projects;
3091 PROJECT:
3092 foreach my $pr (@$projlist) {
3094 if ($tagfilter) {
3095 next unless ref($pr->{'ctags'}) eq 'HASH';
3096 next unless
3097 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3100 if ($search_re) {
3101 next unless
3102 $pr->{'path'} =~ /$search_re/ ||
3103 $pr->{'descr_long'} =~ /$search_re/;
3106 push @projects, $pr;
3109 return @projects;
3112 our $gitweb_project_owner = undef;
3113 sub git_get_project_list_from_file {
3115 return if (defined $gitweb_project_owner);
3117 $gitweb_project_owner = {};
3118 # read from file (url-encoded):
3119 # 'git%2Fgit.git Linus+Torvalds'
3120 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3121 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3122 if (-f $projects_list) {
3123 open(my $fd, '<', $projects_list);
3124 while (my $line = <$fd>) {
3125 chomp $line;
3126 my ($pr, $ow) = split ' ', $line;
3127 $pr = unescape($pr);
3128 $ow = unescape($ow);
3129 $gitweb_project_owner->{$pr} = to_utf8($ow);
3131 close $fd;
3135 sub git_get_project_owner {
3136 my $project = shift;
3137 my $owner;
3139 return undef unless $project;
3140 $git_dir = "$projectroot/$project";
3142 if (!defined $gitweb_project_owner) {
3143 git_get_project_list_from_file();
3146 if (exists $gitweb_project_owner->{$project}) {
3147 $owner = $gitweb_project_owner->{$project};
3149 if (!defined $owner){
3150 $owner = git_get_project_config('owner');
3152 if (!defined $owner) {
3153 $owner = get_file_owner("$git_dir");
3156 return $owner;
3159 sub git_get_last_activity {
3160 my ($path) = @_;
3161 my $fd;
3163 $git_dir = "$projectroot/$path";
3164 open($fd, "-|", git_cmd(), 'for-each-ref',
3165 '--format=%(committer)',
3166 '--sort=-committerdate',
3167 '--count=1',
3168 'refs/heads') or return;
3169 my $most_recent = <$fd>;
3170 close $fd or return;
3171 if (defined $most_recent &&
3172 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
3173 my $timestamp = $1;
3174 my $age = time - $timestamp;
3175 return ($age, age_string($age));
3177 return (undef, undef);
3180 # Implementation note: when a single remote is wanted, we cannot use 'git
3181 # remote show -n' because that command always work (assuming it's a remote URL
3182 # if it's not defined), and we cannot use 'git remote show' because that would
3183 # try to make a network roundtrip. So the only way to find if that particular
3184 # remote is defined is to walk the list provided by 'git remote -v' and stop if
3185 # and when we find what we want.
3186 sub git_get_remotes_list {
3187 my $wanted = shift;
3188 my %remotes = ();
3190 open my $fd, '-|' , git_cmd(), 'remote', '-v';
3191 return unless $fd;
3192 while (my $remote = <$fd>) {
3193 chomp $remote;
3194 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3195 next if $wanted and not $remote eq $wanted;
3196 my ($url, $key) = ($1, $2);
3198 $remotes{$remote} ||= { 'heads' => () };
3199 $remotes{$remote}{$key} = $url;
3201 close $fd or return;
3202 return wantarray ? %remotes : \%remotes;
3205 # Takes a hash of remotes as first parameter and fills it by adding the
3206 # available remote heads for each of the indicated remotes.
3207 sub fill_remote_heads {
3208 my $remotes = shift;
3209 my @heads = map { "remotes/$_" } keys %$remotes;
3210 my @remoteheads = git_get_heads_list(undef, @heads);
3211 foreach my $remote (keys %$remotes) {
3212 $remotes->{$remote}{'heads'} = [ grep {
3213 $_->{'name'} =~ s!^$remote/!!
3214 } @remoteheads ];
3218 sub git_get_references {
3219 my $type = shift || "";
3220 my %refs;
3221 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3222 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3223 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3224 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
3225 or return;
3227 while (my $line = <$fd>) {
3228 chomp $line;
3229 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3230 if (defined $refs{$1}) {
3231 push @{$refs{$1}}, $2;
3232 } else {
3233 $refs{$1} = [ $2 ];
3237 close $fd or return;
3238 return \%refs;
3241 sub git_get_rev_name_tags {
3242 my $hash = shift || return undef;
3244 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3245 or return;
3246 my $name_rev = <$fd>;
3247 close $fd;
3249 if ($name_rev =~ m|^$hash tags/(.*)$|) {
3250 return $1;
3251 } else {
3252 # catches also '$hash undefined' output
3253 return undef;
3257 ## ----------------------------------------------------------------------
3258 ## parse to hash functions
3260 sub parse_date {
3261 my $epoch = shift;
3262 my $tz = shift || "-0000";
3264 my %date;
3265 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3266 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3267 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3268 $date{'hour'} = $hour;
3269 $date{'minute'} = $min;
3270 $date{'mday'} = $mday;
3271 $date{'day'} = $days[$wday];
3272 $date{'month'} = $months[$mon];
3273 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3274 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3275 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3276 $mday, $months[$mon], $hour ,$min;
3277 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3278 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3280 my ($tz_sign, $tz_hour, $tz_min) =
3281 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3282 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3283 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3284 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3285 $date{'hour_local'} = $hour;
3286 $date{'minute_local'} = $min;
3287 $date{'tz_local'} = $tz;
3288 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3289 1900+$year, $mon+1, $mday,
3290 $hour, $min, $sec, $tz);
3291 return %date;
3294 sub parse_tag {
3295 my $tag_id = shift;
3296 my %tag;
3297 my @comment;
3299 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3300 $tag{'id'} = $tag_id;
3301 while (my $line = <$fd>) {
3302 chomp $line;
3303 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3304 $tag{'object'} = $1;
3305 } elsif ($line =~ m/^type (.+)$/) {
3306 $tag{'type'} = $1;
3307 } elsif ($line =~ m/^tag (.+)$/) {
3308 $tag{'name'} = $1;
3309 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3310 $tag{'author'} = $1;
3311 $tag{'author_epoch'} = $2;
3312 $tag{'author_tz'} = $3;
3313 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3314 $tag{'author_name'} = $1;
3315 $tag{'author_email'} = $2;
3316 } else {
3317 $tag{'author_name'} = $tag{'author'};
3319 } elsif ($line =~ m/--BEGIN/) {
3320 push @comment, $line;
3321 last;
3322 } elsif ($line eq "") {
3323 last;
3326 push @comment, <$fd>;
3327 $tag{'comment'} = \@comment;
3328 close $fd or return;
3329 if (!defined $tag{'name'}) {
3330 return
3332 return %tag
3335 sub parse_commit_text {
3336 my ($commit_text, $withparents) = @_;
3337 my @commit_lines = split '\n', $commit_text;
3338 my %co;
3340 pop @commit_lines; # Remove '\0'
3342 if (! @commit_lines) {
3343 return;
3346 my $header = shift @commit_lines;
3347 if ($header !~ m/^[0-9a-fA-F]{40}/) {
3348 return;
3350 ($co{'id'}, my @parents) = split ' ', $header;
3351 while (my $line = shift @commit_lines) {
3352 last if $line eq "\n";
3353 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3354 $co{'tree'} = $1;
3355 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3356 push @parents, $1;
3357 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3358 $co{'author'} = to_utf8($1);
3359 $co{'author_epoch'} = $2;
3360 $co{'author_tz'} = $3;
3361 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3362 $co{'author_name'} = $1;
3363 $co{'author_email'} = $2;
3364 } else {
3365 $co{'author_name'} = $co{'author'};
3367 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3368 $co{'committer'} = to_utf8($1);
3369 $co{'committer_epoch'} = $2;
3370 $co{'committer_tz'} = $3;
3371 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3372 $co{'committer_name'} = $1;
3373 $co{'committer_email'} = $2;
3374 } else {
3375 $co{'committer_name'} = $co{'committer'};
3379 if (!defined $co{'tree'}) {
3380 return;
3382 $co{'parents'} = \@parents;
3383 $co{'parent'} = $parents[0];
3385 foreach my $title (@commit_lines) {
3386 $title =~ s/^ //;
3387 if ($title ne "") {
3388 $co{'title'} = chop_str($title, 80, 5);
3389 # remove leading stuff of merges to make the interesting part visible
3390 if (length($title) > 50) {
3391 $title =~ s/^Automatic //;
3392 $title =~ s/^merge (of|with) /Merge ... /i;
3393 if (length($title) > 50) {
3394 $title =~ s/(http|rsync):\/\///;
3396 if (length($title) > 50) {
3397 $title =~ s/(master|www|rsync)\.//;
3399 if (length($title) > 50) {
3400 $title =~ s/kernel.org:?//;
3402 if (length($title) > 50) {
3403 $title =~ s/\/pub\/scm//;
3406 $co{'title_short'} = chop_str($title, 50, 5);
3407 last;
3410 if (! defined $co{'title'} || $co{'title'} eq "") {
3411 $co{'title'} = $co{'title_short'} = '(no commit message)';
3413 # remove added spaces
3414 foreach my $line (@commit_lines) {
3415 $line =~ s/^ //;
3417 $co{'comment'} = \@commit_lines;
3419 my $age = time - $co{'committer_epoch'};
3420 $co{'age'} = $age;
3421 $co{'age_string'} = age_string($age);
3422 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3423 if ($age > 60*60*24*7*2) {
3424 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3425 $co{'age_string_age'} = $co{'age_string'};
3426 } else {
3427 $co{'age_string_date'} = $co{'age_string'};
3428 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3430 return %co;
3433 sub parse_commit {
3434 my ($commit_id) = @_;
3435 my %co;
3437 local $/ = "\0";
3439 open my $fd, "-|", git_cmd(), "rev-list",
3440 "--parents",
3441 "--header",
3442 "--max-count=1",
3443 $commit_id,
3444 "--",
3445 or die_error(500, "Open git-rev-list failed");
3446 %co = parse_commit_text(<$fd>, 1);
3447 close $fd;
3449 return %co;
3452 sub parse_commits {
3453 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3454 my @cos;
3456 $maxcount ||= 1;
3457 $skip ||= 0;
3459 local $/ = "\0";
3461 open my $fd, "-|", git_cmd(), "rev-list",
3462 "--header",
3463 @args,
3464 ("--max-count=" . $maxcount),
3465 ("--skip=" . $skip),
3466 @extra_options,
3467 $commit_id,
3468 "--",
3469 ($filename ? ($filename) : ())
3470 or die_error(500, "Open git-rev-list failed");
3471 while (my $line = <$fd>) {
3472 my %co = parse_commit_text($line);
3473 push @cos, \%co;
3475 close $fd;
3477 return wantarray ? @cos : \@cos;
3480 # parse line of git-diff-tree "raw" output
3481 sub parse_difftree_raw_line {
3482 my $line = shift;
3483 my %res;
3485 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
3486 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
3487 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3488 $res{'from_mode'} = $1;
3489 $res{'to_mode'} = $2;
3490 $res{'from_id'} = $3;
3491 $res{'to_id'} = $4;
3492 $res{'status'} = $5;
3493 $res{'similarity'} = $6;
3494 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3495 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3496 } else {
3497 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3500 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3501 # combined diff (for merge commit)
3502 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3503 $res{'nparents'} = length($1);
3504 $res{'from_mode'} = [ split(' ', $2) ];
3505 $res{'to_mode'} = pop @{$res{'from_mode'}};
3506 $res{'from_id'} = [ split(' ', $3) ];
3507 $res{'to_id'} = pop @{$res{'from_id'}};
3508 $res{'status'} = [ split('', $4) ];
3509 $res{'to_file'} = unquote($5);
3511 # 'c512b523472485aef4fff9e57b229d9d243c967f'
3512 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3513 $res{'commit'} = $1;
3516 return wantarray ? %res : \%res;
3519 # wrapper: return parsed line of git-diff-tree "raw" output
3520 # (the argument might be raw line, or parsed info)
3521 sub parsed_difftree_line {
3522 my $line_or_ref = shift;
3524 if (ref($line_or_ref) eq "HASH") {
3525 # pre-parsed (or generated by hand)
3526 return $line_or_ref;
3527 } else {
3528 return parse_difftree_raw_line($line_or_ref);
3532 # parse line of git-ls-tree output
3533 sub parse_ls_tree_line {
3534 my $line = shift;
3535 my %opts = @_;
3536 my %res;
3538 if ($opts{'-l'}) {
3539 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3540 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3542 $res{'mode'} = $1;
3543 $res{'type'} = $2;
3544 $res{'hash'} = $3;
3545 $res{'size'} = $4;
3546 if ($opts{'-z'}) {
3547 $res{'name'} = $5;
3548 } else {
3549 $res{'name'} = unquote($5);
3551 } else {
3552 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3553 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3555 $res{'mode'} = $1;
3556 $res{'type'} = $2;
3557 $res{'hash'} = $3;
3558 if ($opts{'-z'}) {
3559 $res{'name'} = $4;
3560 } else {
3561 $res{'name'} = unquote($4);
3565 return wantarray ? %res : \%res;
3568 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3569 sub parse_from_to_diffinfo {
3570 my ($diffinfo, $from, $to, @parents) = @_;
3572 if ($diffinfo->{'nparents'}) {
3573 # combined diff
3574 $from->{'file'} = [];
3575 $from->{'href'} = [];
3576 fill_from_file_info($diffinfo, @parents)
3577 unless exists $diffinfo->{'from_file'};
3578 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3579 $from->{'file'}[$i] =
3580 defined $diffinfo->{'from_file'}[$i] ?
3581 $diffinfo->{'from_file'}[$i] :
3582 $diffinfo->{'to_file'};
3583 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3584 $from->{'href'}[$i] = href(action=>"blob",
3585 hash_base=>$parents[$i],
3586 hash=>$diffinfo->{'from_id'}[$i],
3587 file_name=>$from->{'file'}[$i]);
3588 } else {
3589 $from->{'href'}[$i] = undef;
3592 } else {
3593 # ordinary (not combined) diff
3594 $from->{'file'} = $diffinfo->{'from_file'};
3595 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3596 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3597 hash=>$diffinfo->{'from_id'},
3598 file_name=>$from->{'file'});
3599 } else {
3600 delete $from->{'href'};
3604 $to->{'file'} = $diffinfo->{'to_file'};
3605 if (!is_deleted($diffinfo)) { # file exists in result
3606 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3607 hash=>$diffinfo->{'to_id'},
3608 file_name=>$to->{'file'});
3609 } else {
3610 delete $to->{'href'};
3614 ## ......................................................................
3615 ## parse to array of hashes functions
3617 sub git_get_heads_list {
3618 my ($limit, @classes) = @_;
3619 @classes = ('heads') unless @classes;
3620 my @patterns = map { "refs/$_" } @classes;
3621 my @headslist;
3623 open my $fd, '-|', git_cmd(), 'for-each-ref',
3624 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3625 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3626 @patterns
3627 or return;
3628 while (my $line = <$fd>) {
3629 my %ref_item;
3631 chomp $line;
3632 my ($refinfo, $committerinfo) = split(/\0/, $line);
3633 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3634 my ($committer, $epoch, $tz) =
3635 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3636 $ref_item{'fullname'} = $name;
3637 $name =~ s!^refs/(?:head|remote)s/!!;
3639 $ref_item{'name'} = $name;
3640 $ref_item{'id'} = $hash;
3641 $ref_item{'title'} = $title || '(no commit message)';
3642 $ref_item{'epoch'} = $epoch;
3643 if ($epoch) {
3644 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3645 } else {
3646 $ref_item{'age'} = "unknown";
3649 push @headslist, \%ref_item;
3651 close $fd;
3653 return wantarray ? @headslist : \@headslist;
3656 sub git_get_tags_list {
3657 my $limit = shift;
3658 my @tagslist;
3660 open my $fd, '-|', git_cmd(), 'for-each-ref',
3661 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3662 '--format=%(objectname) %(objecttype) %(refname) '.
3663 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3664 'refs/tags'
3665 or return;
3666 while (my $line = <$fd>) {
3667 my %ref_item;
3669 chomp $line;
3670 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3671 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3672 my ($creator, $epoch, $tz) =
3673 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3674 $ref_item{'fullname'} = $name;
3675 $name =~ s!^refs/tags/!!;
3677 $ref_item{'type'} = $type;
3678 $ref_item{'id'} = $id;
3679 $ref_item{'name'} = $name;
3680 if ($type eq "tag") {
3681 $ref_item{'subject'} = $title;
3682 $ref_item{'reftype'} = $reftype;
3683 $ref_item{'refid'} = $refid;
3684 } else {
3685 $ref_item{'reftype'} = $type;
3686 $ref_item{'refid'} = $id;
3689 if ($type eq "tag" || $type eq "commit") {
3690 $ref_item{'epoch'} = $epoch;
3691 if ($epoch) {
3692 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3693 } else {
3694 $ref_item{'age'} = "unknown";
3698 push @tagslist, \%ref_item;
3700 close $fd;
3702 return wantarray ? @tagslist : \@tagslist;
3705 ## ----------------------------------------------------------------------
3706 ## filesystem-related functions
3708 sub get_file_owner {
3709 my $path = shift;
3711 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3712 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3713 if (!defined $gcos) {
3714 return undef;
3716 my $owner = $gcos;
3717 $owner =~ s/[,;].*$//;
3718 return to_utf8($owner);
3721 # assume that file exists
3722 sub insert_file {
3723 my $filename = shift;
3725 open my $fd, '<', $filename;
3726 print map { to_utf8($_) } <$fd>;
3727 close $fd;
3730 ## ......................................................................
3731 ## mimetype related functions
3733 sub mimetype_guess_file {
3734 my $filename = shift;
3735 my $mimemap = shift;
3736 -r $mimemap or return undef;
3738 my %mimemap;
3739 open(my $mh, '<', $mimemap) or return undef;
3740 while (<$mh>) {
3741 next if m/^#/; # skip comments
3742 my ($mimetype, @exts) = split(/\s+/);
3743 foreach my $ext (@exts) {
3744 $mimemap{$ext} = $mimetype;
3747 close($mh);
3749 $filename =~ /\.([^.]*)$/;
3750 return $mimemap{$1};
3753 sub mimetype_guess {
3754 my $filename = shift;
3755 my $mime;
3756 $filename =~ /\./ or return undef;
3758 if ($mimetypes_file) {
3759 my $file = $mimetypes_file;
3760 if ($file !~ m!^/!) { # if it is relative path
3761 # it is relative to project
3762 $file = "$projectroot/$project/$file";
3764 $mime = mimetype_guess_file($filename, $file);
3766 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3767 return $mime;
3770 sub blob_mimetype {
3771 my $fd = shift;
3772 my $filename = shift;
3774 if ($filename) {
3775 my $mime = mimetype_guess($filename);
3776 $mime and return $mime;
3779 # just in case
3780 return $default_blob_plain_mimetype unless $fd;
3782 if (-T $fd) {
3783 return 'text/plain';
3784 } elsif (! $filename) {
3785 return 'application/octet-stream';
3786 } elsif ($filename =~ m/\.png$/i) {
3787 return 'image/png';
3788 } elsif ($filename =~ m/\.gif$/i) {
3789 return 'image/gif';
3790 } elsif ($filename =~ m/\.jpe?g$/i) {
3791 return 'image/jpeg';
3792 } else {
3793 return 'application/octet-stream';
3797 sub blob_contenttype {
3798 my ($fd, $file_name, $type) = @_;
3800 $type ||= blob_mimetype($fd, $file_name);
3801 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3802 $type .= "; charset=$default_text_plain_charset";
3805 return $type;
3808 # guess file syntax for syntax highlighting; return undef if no highlighting
3809 # the name of syntax can (in the future) depend on syntax highlighter used
3810 sub guess_file_syntax {
3811 my ($highlight, $mimetype, $file_name) = @_;
3812 return undef unless ($highlight && defined $file_name);
3813 my $basename = basename($file_name, '.in');
3814 return $highlight_basename{$basename}
3815 if exists $highlight_basename{$basename};
3817 $basename =~ /\.([^.]*)$/;
3818 my $ext = $1 or return undef;
3819 return $highlight_ext{$ext}
3820 if exists $highlight_ext{$ext};
3822 return undef;
3825 # run highlighter and return FD of its output,
3826 # or return original FD if no highlighting
3827 sub run_highlighter {
3828 my ($fd, $highlight, $syntax) = @_;
3829 return $fd unless ($highlight && defined $syntax);
3831 close $fd;
3832 open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3833 quote_command($highlight_bin).
3834 " --replace-tabs=8 --fragment --syntax $syntax |"
3835 or die_error(500, "Couldn't open file or run syntax highlighter");
3836 return $fd;
3839 ## ======================================================================
3840 ## functions printing HTML: header, footer, error page
3842 sub get_page_title {
3843 my $title = to_utf8($site_name);
3845 unless (defined $project) {
3846 if (defined $project_filter) {
3847 $title .= " - projects in '" . esc_path($project_filter) . "'";
3849 return $title;
3851 $title .= " - " . to_utf8($project);
3853 return $title unless (defined $action);
3854 $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3856 return $title unless (defined $file_name);
3857 $title .= " - " . esc_path($file_name);
3858 if ($action eq "tree" && $file_name !~ m|/$|) {
3859 $title .= "/";
3862 return $title;
3865 sub get_content_type_html {
3866 # require explicit support from the UA if we are to send the page as
3867 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3868 # we have to do this because MSIE sometimes globs '*/*', pretending to
3869 # support xhtml+xml but choking when it gets what it asked for.
3870 if (defined $cgi->http('HTTP_ACCEPT') &&
3871 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3872 $cgi->Accept('application/xhtml+xml') != 0) {
3873 return 'application/xhtml+xml';
3874 } else {
3875 return 'text/html';
3879 sub print_feed_meta {
3880 if (defined $project) {
3881 my %href_params = get_feed_info();
3882 if (!exists $href_params{'-title'}) {
3883 $href_params{'-title'} = 'log';
3886 foreach my $format (qw(RSS Atom)) {
3887 my $type = lc($format);
3888 my %link_attr = (
3889 '-rel' => 'alternate',
3890 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3891 '-type' => "application/$type+xml"
3894 $href_params{'action'} = $type;
3895 $link_attr{'-href'} = href(%href_params);
3896 print "<link ".
3897 "rel=\"$link_attr{'-rel'}\" ".
3898 "title=\"$link_attr{'-title'}\" ".
3899 "href=\"$link_attr{'-href'}\" ".
3900 "type=\"$link_attr{'-type'}\" ".
3901 "/>\n";
3903 $href_params{'extra_options'} = '--no-merges';
3904 $link_attr{'-href'} = href(%href_params);
3905 $link_attr{'-title'} .= ' (no merges)';
3906 print "<link ".
3907 "rel=\"$link_attr{'-rel'}\" ".
3908 "title=\"$link_attr{'-title'}\" ".
3909 "href=\"$link_attr{'-href'}\" ".
3910 "type=\"$link_attr{'-type'}\" ".
3911 "/>\n";
3914 } else {
3915 printf('<link rel="alternate" title="%s projects list" '.
3916 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3917 esc_attr($site_name), href(project=>undef, action=>"project_index"));
3918 printf('<link rel="alternate" title="%s projects feeds" '.
3919 'href="%s" type="text/x-opml" />'."\n",
3920 esc_attr($site_name), href(project=>undef, action=>"opml"));
3924 sub print_header_links {
3925 my $status = shift;
3927 # print out each stylesheet that exist, providing backwards capability
3928 # for those people who defined $stylesheet in a config file
3929 if (defined $stylesheet) {
3930 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3931 } else {
3932 foreach my $stylesheet (@stylesheets) {
3933 next unless $stylesheet;
3934 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3937 print_feed_meta()
3938 if ($status eq '200 OK');
3939 if (defined $favicon) {
3940 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3944 sub print_nav_breadcrumbs_path {
3945 my $dirprefix = undef;
3946 while (my $part = shift) {
3947 $dirprefix .= "/" if defined $dirprefix;
3948 $dirprefix .= $part;
3949 print $cgi->a({-href => href(project => undef,
3950 project_filter => $dirprefix,
3951 action => "project_list")},
3952 esc_html($part)) . " / ";
3956 sub print_nav_breadcrumbs {
3957 my %opts = @_;
3959 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3960 if (defined $project) {
3961 my @dirname = split '/', $project;
3962 my $projectbasename = pop @dirname;
3963 print_nav_breadcrumbs_path(@dirname);
3964 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
3965 if (defined $action) {
3966 my $action_print = $action ;
3967 if (defined $opts{-action_extra}) {
3968 $action_print = $cgi->a({-href => href(action=>$action)},
3969 $action);
3971 print " / $action_print";
3973 if (defined $opts{-action_extra}) {
3974 print " / $opts{-action_extra}";
3976 print "\n";
3977 } elsif (defined $project_filter) {
3978 print_nav_breadcrumbs_path(split '/', $project_filter);
3982 sub print_search_form {
3983 if (!defined $searchtext) {
3984 $searchtext = "";
3986 my $search_hash;
3987 if (defined $hash_base) {
3988 $search_hash = $hash_base;
3989 } elsif (defined $hash) {
3990 $search_hash = $hash;
3991 } else {
3992 $search_hash = "HEAD";
3994 my $action = $my_uri;
3995 my $use_pathinfo = gitweb_check_feature('pathinfo');
3996 if ($use_pathinfo) {
3997 $action .= "/".esc_url($project);
3999 print $cgi->startform(-method => "get", -action => $action) .
4000 "<div class=\"search\">\n" .
4001 (!$use_pathinfo &&
4002 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
4003 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
4004 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
4005 $cgi->popup_menu(-name => 'st', -default => 'commit',
4006 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
4007 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
4008 " search:\n",
4009 $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
4010 "<span title=\"Extended regular expression\">" .
4011 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
4012 -checked => $search_use_regexp) .
4013 "</span>" .
4014 "</div>" .
4015 $cgi->end_form() . "\n";
4018 sub git_header_html {
4019 my $status = shift || "200 OK";
4020 my $expires = shift;
4021 my %opts = @_;
4023 my $title = get_page_title();
4024 my $content_type = get_content_type_html();
4025 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
4026 -status=> $status, -expires => $expires)
4027 unless ($opts{'-no_http_header'});
4028 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
4029 print <<EOF;
4030 <?xml version="1.0" encoding="utf-8"?>
4031 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4032 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
4033 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
4034 <!-- git core binaries version $git_version -->
4035 <head>
4036 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
4037 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
4038 <meta name="robots" content="index, nofollow"/>
4039 <title>$title</title>
4041 # the stylesheet, favicon etc urls won't work correctly with path_info
4042 # unless we set the appropriate base URL
4043 if ($ENV{'PATH_INFO'}) {
4044 print "<base href=\"".esc_url($base_url)."\" />\n";
4046 print_header_links($status);
4048 if (defined $site_html_head_string) {
4049 print to_utf8($site_html_head_string);
4052 print "</head>\n" .
4053 "<body>\n";
4055 if (defined $site_header && -f $site_header) {
4056 insert_file($site_header);
4059 print "<div class=\"page_header\">\n";
4060 if (defined $logo) {
4061 print $cgi->a({-href => esc_url($logo_url),
4062 -title => $logo_label},
4063 $cgi->img({-src => esc_url($logo),
4064 -width => 72, -height => 27,
4065 -alt => "git",
4066 -class => "logo"}));
4068 print_nav_breadcrumbs(%opts);
4069 print "</div>\n";
4071 my $have_search = gitweb_check_feature('search');
4072 if (defined $project && $have_search) {
4073 print_search_form();
4077 sub git_footer_html {
4078 my $feed_class = 'rss_logo';
4080 print "<div class=\"page_footer\">\n";
4081 if (defined $project) {
4082 my $descr = git_get_project_description($project);
4083 if (defined $descr) {
4084 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
4087 my %href_params = get_feed_info();
4088 if (!%href_params) {
4089 $feed_class .= ' generic';
4091 $href_params{'-title'} ||= 'log';
4093 foreach my $format (qw(RSS Atom)) {
4094 $href_params{'action'} = lc($format);
4095 print $cgi->a({-href => href(%href_params),
4096 -title => "$href_params{'-title'} $format feed",
4097 -class => $feed_class}, $format)."\n";
4100 } else {
4101 print $cgi->a({-href => href(project=>undef, action=>"opml",
4102 project_filter => $project_filter),
4103 -class => $feed_class}, "OPML") . " ";
4104 print $cgi->a({-href => href(project=>undef, action=>"project_index",
4105 project_filter => $project_filter),
4106 -class => $feed_class}, "TXT") . "\n";
4108 print "</div>\n"; # class="page_footer"
4110 if (defined $t0 && gitweb_check_feature('timed')) {
4111 print "<div id=\"generating_info\">\n";
4112 print 'This page took '.
4113 '<span id="generating_time" class="time_span">'.
4114 tv_interval($t0, [ gettimeofday() ]).
4115 ' seconds </span>'.
4116 ' and '.
4117 '<span id="generating_cmd">'.
4118 $number_of_git_cmds.
4119 '</span> git commands '.
4120 " to generate.\n";
4121 print "</div>\n"; # class="page_footer"
4124 if (defined $site_footer && -f $site_footer) {
4125 insert_file($site_footer);
4128 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
4129 if (defined $action &&
4130 $action eq 'blame_incremental') {
4131 print qq!<script type="text/javascript">\n!.
4132 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
4133 qq! "!. href() .qq!");\n!.
4134 qq!</script>\n!;
4135 } else {
4136 my ($jstimezone, $tz_cookie, $datetime_class) =
4137 gitweb_get_feature('javascript-timezone');
4139 print qq!<script type="text/javascript">\n!.
4140 qq!window.onload = function () {\n!;
4141 if (gitweb_check_feature('javascript-actions')) {
4142 print qq! fixLinks();\n!;
4144 if ($jstimezone && $tz_cookie && $datetime_class) {
4145 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
4146 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
4148 print qq!};\n!.
4149 qq!</script>\n!;
4152 print "</body>\n" .
4153 "</html>";
4156 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
4157 # Example: die_error(404, 'Hash not found')
4158 # By convention, use the following status codes (as defined in RFC 2616):
4159 # 400: Invalid or missing CGI parameters, or
4160 # requested object exists but has wrong type.
4161 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4162 # this server or project.
4163 # 404: Requested object/revision/project doesn't exist.
4164 # 500: The server isn't configured properly, or
4165 # an internal error occurred (e.g. failed assertions caused by bugs), or
4166 # an unknown error occurred (e.g. the git binary died unexpectedly).
4167 # 503: The server is currently unavailable (because it is overloaded,
4168 # or down for maintenance). Generally, this is a temporary state.
4169 sub die_error {
4170 my $status = shift || 500;
4171 my $error = esc_html(shift) || "Internal Server Error";
4172 my $extra = shift;
4173 my %opts = @_;
4175 my %http_responses = (
4176 400 => '400 Bad Request',
4177 403 => '403 Forbidden',
4178 404 => '404 Not Found',
4179 500 => '500 Internal Server Error',
4180 503 => '503 Service Unavailable',
4182 git_header_html($http_responses{$status}, undef, %opts);
4183 print <<EOF;
4184 <div class="page_body">
4185 <br /><br />
4186 $status - $error
4187 <br />
4189 if (defined $extra) {
4190 print "<hr />\n" .
4191 "$extra\n";
4193 print "</div>\n";
4195 git_footer_html();
4196 goto DONE_GITWEB
4197 unless ($opts{'-error_handler'});
4200 ## ----------------------------------------------------------------------
4201 ## functions printing or outputting HTML: navigation
4203 sub git_print_page_nav {
4204 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4205 $extra = '' if !defined $extra; # pager or formats
4207 my @navs = qw(summary shortlog log commit commitdiff tree);
4208 if ($suppress) {
4209 @navs = grep { $_ ne $suppress } @navs;
4212 my %arg = map { $_ => {action=>$_} } @navs;
4213 if (defined $head) {
4214 for (qw(commit commitdiff)) {
4215 $arg{$_}{'hash'} = $head;
4217 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4218 for (qw(shortlog log)) {
4219 $arg{$_}{'hash'} = $head;
4224 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
4225 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
4227 my @actions = gitweb_get_feature('actions');
4228 my %repl = (
4229 '%' => '%',
4230 'n' => $project, # project name
4231 'f' => $git_dir, # project path within filesystem
4232 'h' => $treehead || '', # current hash ('h' parameter)
4233 'b' => $treebase || '', # hash base ('hb' parameter)
4235 while (@actions) {
4236 my ($label, $link, $pos) = splice(@actions,0,3);
4237 # insert
4238 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
4239 # munch munch
4240 $link =~ s/%([%nfhb])/$repl{$1}/g;
4241 $arg{$label}{'_href'} = $link;
4244 print "<div class=\"page_nav\">\n" .
4245 (join " | ",
4246 map { $_ eq $current ?
4247 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
4248 } @navs);
4249 print "<br/>\n$extra<br/>\n" .
4250 "</div>\n";
4253 # returns a submenu for the nagivation of the refs views (tags, heads,
4254 # remotes) with the current view disabled and the remotes view only
4255 # available if the feature is enabled
4256 sub format_ref_views {
4257 my ($current) = @_;
4258 my @ref_views = qw{tags heads};
4259 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
4260 return join " | ", map {
4261 $_ eq $current ? $_ :
4262 $cgi->a({-href => href(action=>$_)}, $_)
4263 } @ref_views
4266 sub format_paging_nav {
4267 my ($action, $page, $has_next_link) = @_;
4268 my $paging_nav;
4271 if ($page > 0) {
4272 $paging_nav .=
4273 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4274 " &sdot; " .
4275 $cgi->a({-href => href(-replay=>1, page=>$page-1),
4276 -accesskey => "p", -title => "Alt-p"}, "prev");
4277 } else {
4278 $paging_nav .= "first &sdot; prev";
4281 if ($has_next_link) {
4282 $paging_nav .= " &sdot; " .
4283 $cgi->a({-href => href(-replay=>1, page=>$page+1),
4284 -accesskey => "n", -title => "Alt-n"}, "next");
4285 } else {
4286 $paging_nav .= " &sdot; next";
4289 return $paging_nav;
4292 ## ......................................................................
4293 ## functions printing or outputting HTML: div
4295 sub git_print_header_div {
4296 my ($action, $title, $hash, $hash_base) = @_;
4297 my %args = ();
4299 $args{'action'} = $action;
4300 $args{'hash'} = $hash if $hash;
4301 $args{'hash_base'} = $hash_base if $hash_base;
4303 print "<div class=\"header\">\n" .
4304 $cgi->a({-href => href(%args), -class => "title"},
4305 $title ? $title : $action) .
4306 "\n</div>\n";
4309 sub format_repo_url {
4310 my ($name, $url) = @_;
4311 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4314 # Group output by placing it in a DIV element and adding a header.
4315 # Options for start_div() can be provided by passing a hash reference as the
4316 # first parameter to the function.
4317 # Options to git_print_header_div() can be provided by passing an array
4318 # reference. This must follow the options to start_div if they are present.
4319 # The content can be a scalar, which is output as-is, a scalar reference, which
4320 # is output after html escaping, an IO handle passed either as *handle or
4321 # *handle{IO}, or a function reference. In the latter case all following
4322 # parameters will be taken as argument to the content function call.
4323 sub git_print_section {
4324 my ($div_args, $header_args, $content);
4325 my $arg = shift;
4326 if (ref($arg) eq 'HASH') {
4327 $div_args = $arg;
4328 $arg = shift;
4330 if (ref($arg) eq 'ARRAY') {
4331 $header_args = $arg;
4332 $arg = shift;
4334 $content = $arg;
4336 print $cgi->start_div($div_args);
4337 git_print_header_div(@$header_args);
4339 if (ref($content) eq 'CODE') {
4340 $content->(@_);
4341 } elsif (ref($content) eq 'SCALAR') {
4342 print esc_html($$content);
4343 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4344 print <$content>;
4345 } elsif (!ref($content) && defined($content)) {
4346 print $content;
4349 print $cgi->end_div;
4352 sub format_timestamp_html {
4353 my $date = shift;
4354 my $strtime = $date->{'rfc2822'};
4356 my (undef, undef, $datetime_class) =
4357 gitweb_get_feature('javascript-timezone');
4358 if ($datetime_class) {
4359 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
4362 my $localtime_format = '(%02d:%02d %s)';
4363 if ($date->{'hour_local'} < 6) {
4364 $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
4366 $strtime .= ' ' .
4367 sprintf($localtime_format,
4368 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
4370 return $strtime;
4373 # Outputs the author name and date in long form
4374 sub git_print_authorship {
4375 my $co = shift;
4376 my %opts = @_;
4377 my $tag = $opts{-tag} || 'div';
4378 my $author = $co->{'author_name'};
4380 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4381 print "<$tag class=\"author_date\">" .
4382 format_search_author($author, "author", esc_html($author)) .
4383 " [".format_timestamp_html(\%ad)."]".
4384 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
4385 "</$tag>\n";
4388 # Outputs table rows containing the full author or committer information,
4389 # in the format expected for 'commit' view (& similar).
4390 # Parameters are a commit hash reference, followed by the list of people
4391 # to output information for. If the list is empty it defaults to both
4392 # author and committer.
4393 sub git_print_authorship_rows {
4394 my $co = shift;
4395 # too bad we can't use @people = @_ || ('author', 'committer')
4396 my @people = @_;
4397 @people = ('author', 'committer') unless @people;
4398 foreach my $who (@people) {
4399 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4400 print "<tr><td>$who</td><td>" .
4401 format_search_author($co->{"${who}_name"}, $who,
4402 esc_html($co->{"${who}_name"})) . " " .
4403 format_search_author($co->{"${who}_email"}, $who,
4404 esc_html("<" . $co->{"${who}_email"} . ">")) .
4405 "</td><td rowspan=\"2\">" .
4406 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4407 "</td></tr>\n" .
4408 "<tr>" .
4409 "<td></td><td>" .
4410 format_timestamp_html(\%wd) .
4411 "</td>" .
4412 "</tr>\n";
4416 sub git_print_page_path {
4417 my $name = shift;
4418 my $type = shift;
4419 my $hb = shift;
4422 print "<div class=\"page_path\">";
4423 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4424 -title => 'tree root'}, to_utf8("[$project]"));
4425 print " / ";
4426 if (defined $name) {
4427 my @dirname = split '/', $name;
4428 my $basename = pop @dirname;
4429 my $fullname = '';
4431 foreach my $dir (@dirname) {
4432 $fullname .= ($fullname ? '/' : '') . $dir;
4433 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4434 hash_base=>$hb),
4435 -title => $fullname}, esc_path($dir));
4436 print " / ";
4438 if (defined $type && $type eq 'blob') {
4439 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4440 hash_base=>$hb),
4441 -title => $name}, esc_path($basename));
4442 } elsif (defined $type && $type eq 'tree') {
4443 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4444 hash_base=>$hb),
4445 -title => $name}, esc_path($basename));
4446 print " / ";
4447 } else {
4448 print esc_path($basename);
4451 print "<br/></div>\n";
4454 sub git_print_log {
4455 my $log = shift;
4456 my %opts = @_;
4458 if ($opts{'-remove_title'}) {
4459 # remove title, i.e. first line of log
4460 shift @$log;
4462 # remove leading empty lines
4463 while (defined $log->[0] && $log->[0] eq "") {
4464 shift @$log;
4467 # print log
4468 my $signoff = 0;
4469 my $empty = 0;
4470 foreach my $line (@$log) {
4471 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4472 $signoff = 1;
4473 $empty = 0;
4474 if (! $opts{'-remove_signoff'}) {
4475 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4476 next;
4477 } else {
4478 # remove signoff lines
4479 next;
4481 } else {
4482 $signoff = 0;
4485 # print only one empty line
4486 # do not print empty line after signoff
4487 if ($line eq "") {
4488 next if ($empty || $signoff);
4489 $empty = 1;
4490 } else {
4491 $empty = 0;
4494 print format_log_line_html($line) . "<br/>\n";
4497 if ($opts{'-final_empty_line'}) {
4498 # end with single empty line
4499 print "<br/>\n" unless $empty;
4503 # return link target (what link points to)
4504 sub git_get_link_target {
4505 my $hash = shift;
4506 my $link_target;
4508 # read link
4509 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4510 or return;
4512 local $/ = undef;
4513 $link_target = <$fd>;
4515 close $fd
4516 or return;
4518 return $link_target;
4521 # given link target, and the directory (basedir) the link is in,
4522 # return target of link relative to top directory (top tree);
4523 # return undef if it is not possible (including absolute links).
4524 sub normalize_link_target {
4525 my ($link_target, $basedir) = @_;
4527 # absolute symlinks (beginning with '/') cannot be normalized
4528 return if (substr($link_target, 0, 1) eq '/');
4530 # normalize link target to path from top (root) tree (dir)
4531 my $path;
4532 if ($basedir) {
4533 $path = $basedir . '/' . $link_target;
4534 } else {
4535 # we are in top (root) tree (dir)
4536 $path = $link_target;
4539 # remove //, /./, and /../
4540 my @path_parts;
4541 foreach my $part (split('/', $path)) {
4542 # discard '.' and ''
4543 next if (!$part || $part eq '.');
4544 # handle '..'
4545 if ($part eq '..') {
4546 if (@path_parts) {
4547 pop @path_parts;
4548 } else {
4549 # link leads outside repository (outside top dir)
4550 return;
4552 } else {
4553 push @path_parts, $part;
4556 $path = join('/', @path_parts);
4558 return $path;
4561 # print tree entry (row of git_tree), but without encompassing <tr> element
4562 sub git_print_tree_entry {
4563 my ($t, $basedir, $hash_base, $have_blame) = @_;
4565 my %base_key = ();
4566 $base_key{'hash_base'} = $hash_base if defined $hash_base;
4568 # The format of a table row is: mode list link. Where mode is
4569 # the mode of the entry, list is the name of the entry, an href,
4570 # and link is the action links of the entry.
4572 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4573 if (exists $t->{'size'}) {
4574 print "<td class=\"size\">$t->{'size'}</td>\n";
4576 if ($t->{'type'} eq "blob") {
4577 print "<td class=\"list\">" .
4578 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4579 file_name=>"$basedir$t->{'name'}", %base_key),
4580 -class => "list"}, esc_path($t->{'name'}));
4581 if (S_ISLNK(oct $t->{'mode'})) {
4582 my $link_target = git_get_link_target($t->{'hash'});
4583 if ($link_target) {
4584 my $norm_target = normalize_link_target($link_target, $basedir);
4585 if (defined $norm_target) {
4586 print " -> " .
4587 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4588 file_name=>$norm_target),
4589 -title => $norm_target}, esc_path($link_target));
4590 } else {
4591 print " -> " . esc_path($link_target);
4595 print "</td>\n";
4596 print "<td class=\"link\">";
4597 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4598 file_name=>"$basedir$t->{'name'}", %base_key)},
4599 "blob");
4600 if ($have_blame) {
4601 print " | " .
4602 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4603 file_name=>"$basedir$t->{'name'}", %base_key)},
4604 "blame");
4606 if (defined $hash_base) {
4607 print " | " .
4608 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4609 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4610 "history");
4612 print " | " .
4613 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4614 file_name=>"$basedir$t->{'name'}")},
4615 "raw");
4616 print "</td>\n";
4618 } elsif ($t->{'type'} eq "tree") {
4619 print "<td class=\"list\">";
4620 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4621 file_name=>"$basedir$t->{'name'}",
4622 %base_key)},
4623 esc_path($t->{'name'}));
4624 print "</td>\n";
4625 print "<td class=\"link\">";
4626 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4627 file_name=>"$basedir$t->{'name'}",
4628 %base_key)},
4629 "tree");
4630 if (defined $hash_base) {
4631 print " | " .
4632 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4633 file_name=>"$basedir$t->{'name'}")},
4634 "history");
4636 print "</td>\n";
4637 } else {
4638 # unknown object: we can only present history for it
4639 # (this includes 'commit' object, i.e. submodule support)
4640 print "<td class=\"list\">" .
4641 esc_path($t->{'name'}) .
4642 "</td>\n";
4643 print "<td class=\"link\">";
4644 if (defined $hash_base) {
4645 print $cgi->a({-href => href(action=>"history",
4646 hash_base=>$hash_base,
4647 file_name=>"$basedir$t->{'name'}")},
4648 "history");
4650 print "</td>\n";
4654 ## ......................................................................
4655 ## functions printing large fragments of HTML
4657 # get pre-image filenames for merge (combined) diff
4658 sub fill_from_file_info {
4659 my ($diff, @parents) = @_;
4661 $diff->{'from_file'} = [ ];
4662 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4663 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4664 if ($diff->{'status'}[$i] eq 'R' ||
4665 $diff->{'status'}[$i] eq 'C') {
4666 $diff->{'from_file'}[$i] =
4667 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4671 return $diff;
4674 # is current raw difftree line of file deletion
4675 sub is_deleted {
4676 my $diffinfo = shift;
4678 return $diffinfo->{'to_id'} eq ('0' x 40);
4681 # does patch correspond to [previous] difftree raw line
4682 # $diffinfo - hashref of parsed raw diff format
4683 # $patchinfo - hashref of parsed patch diff format
4684 # (the same keys as in $diffinfo)
4685 sub is_patch_split {
4686 my ($diffinfo, $patchinfo) = @_;
4688 return defined $diffinfo && defined $patchinfo
4689 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4693 sub git_difftree_body {
4694 my ($difftree, $hash, @parents) = @_;
4695 my ($parent) = $parents[0];
4696 my $have_blame = gitweb_check_feature('blame');
4697 print "<div class=\"list_head\">\n";
4698 if ($#{$difftree} > 10) {
4699 print(($#{$difftree} + 1) . " files changed:\n");
4701 print "</div>\n";
4703 print "<table class=\"" .
4704 (@parents > 1 ? "combined " : "") .
4705 "diff_tree\">\n";
4707 # header only for combined diff in 'commitdiff' view
4708 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4709 if ($has_header) {
4710 # table header
4711 print "<thead><tr>\n" .
4712 "<th></th><th></th>\n"; # filename, patchN link
4713 for (my $i = 0; $i < @parents; $i++) {
4714 my $par = $parents[$i];
4715 print "<th>" .
4716 $cgi->a({-href => href(action=>"commitdiff",
4717 hash=>$hash, hash_parent=>$par),
4718 -title => 'commitdiff to parent number ' .
4719 ($i+1) . ': ' . substr($par,0,7)},
4720 $i+1) .
4721 "&nbsp;</th>\n";
4723 print "</tr></thead>\n<tbody>\n";
4726 my $alternate = 1;
4727 my $patchno = 0;
4728 foreach my $line (@{$difftree}) {
4729 my $diff = parsed_difftree_line($line);
4731 if ($alternate) {
4732 print "<tr class=\"dark\">\n";
4733 } else {
4734 print "<tr class=\"light\">\n";
4736 $alternate ^= 1;
4738 if (exists $diff->{'nparents'}) { # combined diff
4740 fill_from_file_info($diff, @parents)
4741 unless exists $diff->{'from_file'};
4743 if (!is_deleted($diff)) {
4744 # file exists in the result (child) commit
4745 print "<td>" .
4746 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4747 file_name=>$diff->{'to_file'},
4748 hash_base=>$hash),
4749 -class => "list"}, esc_path($diff->{'to_file'})) .
4750 "</td>\n";
4751 } else {
4752 print "<td>" .
4753 esc_path($diff->{'to_file'}) .
4754 "</td>\n";
4757 if ($action eq 'commitdiff') {
4758 # link to patch
4759 $patchno++;
4760 print "<td class=\"link\">" .
4761 $cgi->a({-href => href(-anchor=>"patch$patchno")},
4762 "patch") .
4763 " | " .
4764 "</td>\n";
4767 my $has_history = 0;
4768 my $not_deleted = 0;
4769 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4770 my $hash_parent = $parents[$i];
4771 my $from_hash = $diff->{'from_id'}[$i];
4772 my $from_path = $diff->{'from_file'}[$i];
4773 my $status = $diff->{'status'}[$i];
4775 $has_history ||= ($status ne 'A');
4776 $not_deleted ||= ($status ne 'D');
4778 if ($status eq 'A') {
4779 print "<td class=\"link\" align=\"right\"> | </td>\n";
4780 } elsif ($status eq 'D') {
4781 print "<td class=\"link\">" .
4782 $cgi->a({-href => href(action=>"blob",
4783 hash_base=>$hash,
4784 hash=>$from_hash,
4785 file_name=>$from_path)},
4786 "blob" . ($i+1)) .
4787 " | </td>\n";
4788 } else {
4789 if ($diff->{'to_id'} eq $from_hash) {
4790 print "<td class=\"link nochange\">";
4791 } else {
4792 print "<td class=\"link\">";
4794 print $cgi->a({-href => href(action=>"blobdiff",
4795 hash=>$diff->{'to_id'},
4796 hash_parent=>$from_hash,
4797 hash_base=>$hash,
4798 hash_parent_base=>$hash_parent,
4799 file_name=>$diff->{'to_file'},
4800 file_parent=>$from_path)},
4801 "diff" . ($i+1)) .
4802 " | </td>\n";
4806 print "<td class=\"link\">";
4807 if ($not_deleted) {
4808 print $cgi->a({-href => href(action=>"blob",
4809 hash=>$diff->{'to_id'},
4810 file_name=>$diff->{'to_file'},
4811 hash_base=>$hash)},
4812 "blob");
4813 print " | " if ($has_history);
4815 if ($has_history) {
4816 print $cgi->a({-href => href(action=>"history",
4817 file_name=>$diff->{'to_file'},
4818 hash_base=>$hash)},
4819 "history");
4821 print "</td>\n";
4823 print "</tr>\n";
4824 next; # instead of 'else' clause, to avoid extra indent
4826 # else ordinary diff
4828 my ($to_mode_oct, $to_mode_str, $to_file_type);
4829 my ($from_mode_oct, $from_mode_str, $from_file_type);
4830 if ($diff->{'to_mode'} ne ('0' x 6)) {
4831 $to_mode_oct = oct $diff->{'to_mode'};
4832 if (S_ISREG($to_mode_oct)) { # only for regular file
4833 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4835 $to_file_type = file_type($diff->{'to_mode'});
4837 if ($diff->{'from_mode'} ne ('0' x 6)) {
4838 $from_mode_oct = oct $diff->{'from_mode'};
4839 if (S_ISREG($from_mode_oct)) { # only for regular file
4840 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4842 $from_file_type = file_type($diff->{'from_mode'});
4845 if ($diff->{'status'} eq "A") { # created
4846 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4847 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
4848 $mode_chng .= "]</span>";
4849 print "<td>";
4850 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4851 hash_base=>$hash, file_name=>$diff->{'file'}),
4852 -class => "list"}, esc_path($diff->{'file'}));
4853 print "</td>\n";
4854 print "<td>$mode_chng</td>\n";
4855 print "<td class=\"link\">";
4856 if ($action eq 'commitdiff') {
4857 # link to patch
4858 $patchno++;
4859 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4860 "patch") .
4861 " | ";
4863 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4864 hash_base=>$hash, file_name=>$diff->{'file'})},
4865 "blob");
4866 print "</td>\n";
4868 } elsif ($diff->{'status'} eq "D") { # deleted
4869 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4870 print "<td>";
4871 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4872 hash_base=>$parent, file_name=>$diff->{'file'}),
4873 -class => "list"}, esc_path($diff->{'file'}));
4874 print "</td>\n";
4875 print "<td>$mode_chng</td>\n";
4876 print "<td class=\"link\">";
4877 if ($action eq 'commitdiff') {
4878 # link to patch
4879 $patchno++;
4880 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4881 "patch") .
4882 " | ";
4884 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4885 hash_base=>$parent, file_name=>$diff->{'file'})},
4886 "blob") . " | ";
4887 if ($have_blame) {
4888 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4889 file_name=>$diff->{'file'})},
4890 "blame") . " | ";
4892 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4893 file_name=>$diff->{'file'})},
4894 "history");
4895 print "</td>\n";
4897 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4898 my $mode_chnge = "";
4899 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4900 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4901 if ($from_file_type ne $to_file_type) {
4902 $mode_chnge .= " from $from_file_type to $to_file_type";
4904 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4905 if ($from_mode_str && $to_mode_str) {
4906 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4907 } elsif ($to_mode_str) {
4908 $mode_chnge .= " mode: $to_mode_str";
4911 $mode_chnge .= "]</span>\n";
4913 print "<td>";
4914 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4915 hash_base=>$hash, file_name=>$diff->{'file'}),
4916 -class => "list"}, esc_path($diff->{'file'}));
4917 print "</td>\n";
4918 print "<td>$mode_chnge</td>\n";
4919 print "<td class=\"link\">";
4920 if ($action eq 'commitdiff') {
4921 # link to patch
4922 $patchno++;
4923 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4924 "patch") .
4925 " | ";
4926 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4927 # "commit" view and modified file (not onlu mode changed)
4928 print $cgi->a({-href => href(action=>"blobdiff",
4929 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4930 hash_base=>$hash, hash_parent_base=>$parent,
4931 file_name=>$diff->{'file'})},
4932 "diff") .
4933 " | ";
4935 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4936 hash_base=>$hash, file_name=>$diff->{'file'})},
4937 "blob") . " | ";
4938 if ($have_blame) {
4939 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4940 file_name=>$diff->{'file'})},
4941 "blame") . " | ";
4943 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4944 file_name=>$diff->{'file'})},
4945 "history");
4946 print "</td>\n";
4948 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4949 my %status_name = ('R' => 'moved', 'C' => 'copied');
4950 my $nstatus = $status_name{$diff->{'status'}};
4951 my $mode_chng = "";
4952 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4953 # mode also for directories, so we cannot use $to_mode_str
4954 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4956 print "<td>" .
4957 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4958 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4959 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4960 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4961 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4962 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4963 -class => "list"}, esc_path($diff->{'from_file'})) .
4964 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4965 "<td class=\"link\">";
4966 if ($action eq 'commitdiff') {
4967 # link to patch
4968 $patchno++;
4969 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4970 "patch") .
4971 " | ";
4972 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4973 # "commit" view and modified file (not only pure rename or copy)
4974 print $cgi->a({-href => href(action=>"blobdiff",
4975 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4976 hash_base=>$hash, hash_parent_base=>$parent,
4977 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4978 "diff") .
4979 " | ";
4981 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4982 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4983 "blob") . " | ";
4984 if ($have_blame) {
4985 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4986 file_name=>$diff->{'to_file'})},
4987 "blame") . " | ";
4989 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4990 file_name=>$diff->{'to_file'})},
4991 "history");
4992 print "</td>\n";
4994 } # we should not encounter Unmerged (U) or Unknown (X) status
4995 print "</tr>\n";
4997 print "</tbody>" if $has_header;
4998 print "</table>\n";
5001 sub print_sidebyside_diff_chunk {
5002 my @chunk = @_;
5003 my (@ctx, @rem, @add);
5005 return unless @chunk;
5007 # incomplete last line might be among removed or added lines,
5008 # or both, or among context lines: find which
5009 for (my $i = 1; $i < @chunk; $i++) {
5010 if ($chunk[$i][0] eq 'incomplete') {
5011 $chunk[$i][0] = $chunk[$i-1][0];
5015 # guardian
5016 push @chunk, ["", ""];
5018 foreach my $line_info (@chunk) {
5019 my ($class, $line) = @$line_info;
5021 # print chunk headers
5022 if ($class && $class eq 'chunk_header') {
5023 print $line;
5024 next;
5027 ## print from accumulator when type of class of lines change
5028 # empty contents block on start rem/add block, or end of chunk
5029 if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
5030 print join '',
5031 '<div class="chunk_block ctx">',
5032 '<div class="old">',
5033 @ctx,
5034 '</div>',
5035 '<div class="new">',
5036 @ctx,
5037 '</div>',
5038 '</div>';
5039 @ctx = ();
5041 # empty add/rem block on start context block, or end of chunk
5042 if ((@rem || @add) && (!$class || $class eq 'ctx')) {
5043 if (!@add) {
5044 # pure removal
5045 print join '',
5046 '<div class="chunk_block rem">',
5047 '<div class="old">',
5048 @rem,
5049 '</div>',
5050 '</div>';
5051 } elsif (!@rem) {
5052 # pure addition
5053 print join '',
5054 '<div class="chunk_block add">',
5055 '<div class="new">',
5056 @add,
5057 '</div>',
5058 '</div>';
5059 } else {
5060 # assume that it is change
5061 print join '',
5062 '<div class="chunk_block chg">',
5063 '<div class="old">',
5064 @rem,
5065 '</div>',
5066 '<div class="new">',
5067 @add,
5068 '</div>',
5069 '</div>';
5071 @rem = @add = ();
5074 ## adding lines to accumulator
5075 # guardian value
5076 last unless $line;
5077 # rem, add or change
5078 if ($class eq 'rem') {
5079 push @rem, $line;
5080 } elsif ($class eq 'add') {
5081 push @add, $line;
5083 # context line
5084 if ($class eq 'ctx') {
5085 push @ctx, $line;
5090 sub git_patchset_body {
5091 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
5092 my ($hash_parent) = $hash_parents[0];
5094 my $is_combined = (@hash_parents > 1);
5095 my $patch_idx = 0;
5096 my $patch_number = 0;
5097 my $patch_line;
5098 my $diffinfo;
5099 my $to_name;
5100 my (%from, %to);
5101 my @chunk; # for side-by-side diff
5103 print "<div class=\"patchset\">\n";
5105 # skip to first patch
5106 while ($patch_line = <$fd>) {
5107 chomp $patch_line;
5109 last if ($patch_line =~ m/^diff /);
5112 PATCH:
5113 while ($patch_line) {
5115 # parse "git diff" header line
5116 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
5117 # $1 is from_name, which we do not use
5118 $to_name = unquote($2);
5119 $to_name =~ s!^b/!!;
5120 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
5121 # $1 is 'cc' or 'combined', which we do not use
5122 $to_name = unquote($2);
5123 } else {
5124 $to_name = undef;
5127 # check if current patch belong to current raw line
5128 # and parse raw git-diff line if needed
5129 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
5130 # this is continuation of a split patch
5131 print "<div class=\"patch cont\">\n";
5132 } else {
5133 # advance raw git-diff output if needed
5134 $patch_idx++ if defined $diffinfo;
5136 # read and prepare patch information
5137 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5139 # compact combined diff output can have some patches skipped
5140 # find which patch (using pathname of result) we are at now;
5141 if ($is_combined) {
5142 while ($to_name ne $diffinfo->{'to_file'}) {
5143 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5144 format_diff_cc_simplified($diffinfo, @hash_parents) .
5145 "</div>\n"; # class="patch"
5147 $patch_idx++;
5148 $patch_number++;
5150 last if $patch_idx > $#$difftree;
5151 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5155 # modifies %from, %to hashes
5156 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5158 # this is first patch for raw difftree line with $patch_idx index
5159 # we index @$difftree array from 0, but number patches from 1
5160 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
5163 # git diff header
5164 #assert($patch_line =~ m/^diff /) if DEBUG;
5165 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5166 $patch_number++;
5167 # print "git diff" header
5168 print format_git_diff_header_line($patch_line, $diffinfo,
5169 \%from, \%to);
5171 # print extended diff header
5172 print "<div class=\"diff extended_header\">\n";
5173 EXTENDED_HEADER:
5174 while ($patch_line = <$fd>) {
5175 chomp $patch_line;
5177 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
5179 print format_extended_diff_header_line($patch_line, $diffinfo,
5180 \%from, \%to);
5182 print "</div>\n"; # class="diff extended_header"
5184 # from-file/to-file diff header
5185 if (! $patch_line) {
5186 print "</div>\n"; # class="patch"
5187 last PATCH;
5189 next PATCH if ($patch_line =~ m/^diff /);
5190 #assert($patch_line =~ m/^---/) if DEBUG;
5192 my $last_patch_line = $patch_line;
5193 $patch_line = <$fd>;
5194 chomp $patch_line;
5195 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
5197 print format_diff_from_to_header($last_patch_line, $patch_line,
5198 $diffinfo, \%from, \%to,
5199 @hash_parents);
5201 # the patch itself
5202 LINE:
5203 while ($patch_line = <$fd>) {
5204 chomp $patch_line;
5206 next PATCH if ($patch_line =~ m/^diff /);
5208 my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
5209 my $diff_classes = "diff";
5210 $diff_classes .= " $class" if ($class);
5211 $line = "<div class=\"$diff_classes\">$line</div>\n";
5213 if ($diff_style eq 'sidebyside' && !$is_combined) {
5214 if ($class eq 'chunk_header') {
5215 print_sidebyside_diff_chunk(@chunk);
5216 @chunk = ( [ $class, $line ] );
5217 } else {
5218 push @chunk, [ $class, $line ];
5220 } else {
5221 # default 'inline' style and unknown styles
5222 print $line;
5226 } continue {
5227 if (@chunk) {
5228 print_sidebyside_diff_chunk(@chunk);
5229 @chunk = ();
5231 print "</div>\n"; # class="patch"
5234 # for compact combined (--cc) format, with chunk and patch simplification
5235 # the patchset might be empty, but there might be unprocessed raw lines
5236 for (++$patch_idx if $patch_number > 0;
5237 $patch_idx < @$difftree;
5238 ++$patch_idx) {
5239 # read and prepare patch information
5240 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5242 # generate anchor for "patch" links in difftree / whatchanged part
5243 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5244 format_diff_cc_simplified($diffinfo, @hash_parents) .
5245 "</div>\n"; # class="patch"
5247 $patch_number++;
5250 if ($patch_number == 0) {
5251 if (@hash_parents > 1) {
5252 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
5253 } else {
5254 print "<div class=\"diff nodifferences\">No differences found</div>\n";
5258 print "</div>\n"; # class="patchset"
5261 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5263 sub git_project_search_form {
5264 my ($searchtext, $search_use_regexp) = @_;
5266 my $limit = '';
5267 if ($project_filter) {
5268 $limit = " in '$project_filter/'";
5271 print "<div class=\"projsearch\">\n";
5272 print $cgi->startform(-method => 'get', -action => $my_uri) .
5273 $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
5274 print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
5275 if (defined $project_filter);
5276 print $cgi->textfield(-name => 's', -value => $searchtext,
5277 -title => "Search project by name and description$limit",
5278 -size => 60) . "\n" .
5279 "<span title=\"Extended regular expression\">" .
5280 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5281 -checked => $search_use_regexp) .
5282 "</span>\n" .
5283 $cgi->submit(-name => 'btnS', -value => 'Search') .
5284 $cgi->end_form() . "\n" .
5285 $cgi->a({-href => href(project => undef, searchtext => undef,
5286 project_filter => $project_filter)},
5287 esc_html("List all projects$limit")) . "<br />\n";
5288 print "</div>\n";
5291 # entry for given @keys needs filling if at least one of keys in list
5292 # is not present in %$project_info
5293 sub project_info_needs_filling {
5294 my ($project_info, @keys) = @_;
5296 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
5297 foreach my $key (@keys) {
5298 if (!exists $project_info->{$key}) {
5299 return 1;
5302 return;
5305 # fills project list info (age, description, owner, category, forks, etc.)
5306 # for each project in the list, removing invalid projects from
5307 # returned list, or fill only specified info.
5309 # Invalid projects are removed from the returned list if and only if you
5310 # ask 'age' or 'age_string' to be filled, because they are the only fields
5311 # that run unconditionally git command that requires repository, and
5312 # therefore do always check if project repository is invalid.
5314 # USAGE:
5315 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
5316 # ensures that 'descr_long' and 'ctags' fields are filled
5317 # * @project_list = fill_project_list_info(\@project_list)
5318 # ensures that all fields are filled (and invalid projects removed)
5320 # NOTE: modifies $projlist, but does not remove entries from it
5321 sub fill_project_list_info {
5322 my ($projlist, @wanted_keys) = @_;
5323 my @projects;
5324 my $filter_set = sub { return @_; };
5325 if (@wanted_keys) {
5326 my %wanted_keys = map { $_ => 1 } @wanted_keys;
5327 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
5330 my $show_ctags = gitweb_check_feature('ctags');
5331 PROJECT:
5332 foreach my $pr (@$projlist) {
5333 if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
5334 my (@activity) = git_get_last_activity($pr->{'path'});
5335 unless (@activity) {
5336 next PROJECT;
5338 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
5340 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
5341 my $descr = git_get_project_description($pr->{'path'}) || "";
5342 $descr = to_utf8($descr);
5343 $pr->{'descr_long'} = $descr;
5344 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
5346 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
5347 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
5349 if ($show_ctags &&
5350 project_info_needs_filling($pr, $filter_set->('ctags'))) {
5351 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
5353 if ($projects_list_group_categories &&
5354 project_info_needs_filling($pr, $filter_set->('category'))) {
5355 my $cat = git_get_project_category($pr->{'path'}) ||
5356 $project_list_default_category;
5357 $pr->{'category'} = to_utf8($cat);
5360 push @projects, $pr;
5363 return @projects;
5366 sub sort_projects_list {
5367 my ($projlist, $order) = @_;
5368 my @projects;
5370 my %order_info = (
5371 project => { key => 'path', type => 'str' },
5372 descr => { key => 'descr_long', type => 'str' },
5373 owner => { key => 'owner', type => 'str' },
5374 age => { key => 'age', type => 'num' }
5376 my $oi = $order_info{$order};
5377 return @$projlist unless defined $oi;
5378 if ($oi->{'type'} eq 'str') {
5379 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
5380 } else {
5381 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
5384 return @projects;
5387 # returns a hash of categories, containing the list of project
5388 # belonging to each category
5389 sub build_projlist_by_category {
5390 my ($projlist, $from, $to) = @_;
5391 my %categories;
5393 $from = 0 unless defined $from;
5394 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5396 for (my $i = $from; $i <= $to; $i++) {
5397 my $pr = $projlist->[$i];
5398 push @{$categories{ $pr->{'category'} }}, $pr;
5401 return wantarray ? %categories : \%categories;
5404 # print 'sort by' <th> element, generating 'sort by $name' replay link
5405 # if that order is not selected
5406 sub print_sort_th {
5407 print format_sort_th(@_);
5410 sub format_sort_th {
5411 my ($name, $order, $header) = @_;
5412 my $sort_th = "";
5413 $header ||= ucfirst($name);
5415 if ($order eq $name) {
5416 $sort_th .= "<th>$header</th>\n";
5417 } else {
5418 $sort_th .= "<th>" .
5419 $cgi->a({-href => href(-replay=>1, order=>$name),
5420 -class => "header"}, $header) .
5421 "</th>\n";
5424 return $sort_th;
5427 sub git_project_list_rows {
5428 my ($projlist, $from, $to, $check_forks) = @_;
5430 $from = 0 unless defined $from;
5431 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5433 my $alternate = 1;
5434 for (my $i = $from; $i <= $to; $i++) {
5435 my $pr = $projlist->[$i];
5437 if ($alternate) {
5438 print "<tr class=\"dark\">\n";
5439 } else {
5440 print "<tr class=\"light\">\n";
5442 $alternate ^= 1;
5444 if ($check_forks) {
5445 print "<td>";
5446 if ($pr->{'forks'}) {
5447 my $nforks = scalar @{$pr->{'forks'}};
5448 if ($nforks > 0) {
5449 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5450 -title => "$nforks forks"}, "+");
5451 } else {
5452 print $cgi->span({-title => "$nforks forks"}, "+");
5455 print "</td>\n";
5457 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5458 -class => "list"},
5459 esc_html_match_hl($pr->{'path'}, $search_regexp)) .
5460 "</td>\n" .
5461 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5462 -class => "list",
5463 -title => $pr->{'descr_long'}},
5464 $search_regexp
5465 ? esc_html_match_hl_chopped($pr->{'descr_long'},
5466 $pr->{'descr'}, $search_regexp)
5467 : esc_html($pr->{'descr'})) .
5468 "</td>\n" .
5469 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5470 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5471 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
5472 "<td class=\"link\">" .
5473 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
5474 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5475 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5476 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5477 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5478 "</td>\n" .
5479 "</tr>\n";
5483 sub git_project_list_body {
5484 # actually uses global variable $project
5485 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5486 my @projects = @$projlist;
5488 my $check_forks = gitweb_check_feature('forks');
5489 my $show_ctags = gitweb_check_feature('ctags');
5490 my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
5491 $check_forks = undef
5492 if ($tagfilter || $search_regexp);
5494 # filtering out forks before filling info allows to do less work
5495 @projects = filter_forks_from_projects_list(\@projects)
5496 if ($check_forks);
5497 # search_projects_list pre-fills required info
5498 @projects = search_projects_list(\@projects,
5499 'search_regexp' => $search_regexp,
5500 'tagfilter' => $tagfilter)
5501 if ($tagfilter || $search_regexp);
5502 # fill the rest
5503 @projects = fill_project_list_info(\@projects);
5505 $order ||= $default_projects_order;
5506 $from = 0 unless defined $from;
5507 $to = $#projects if (!defined $to || $#projects < $to);
5509 # short circuit
5510 if ($from > $to) {
5511 print "<center>\n".
5512 "<b>No such projects found</b><br />\n".
5513 "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5514 "</center>\n<br />\n";
5515 return;
5518 @projects = sort_projects_list(\@projects, $order);
5520 if ($show_ctags) {
5521 my $ctags = git_gather_all_ctags(\@projects);
5522 my $cloud = git_populate_project_tagcloud($ctags);
5523 print git_show_project_tagcloud($cloud, 64);
5526 print "<table class=\"project_list\">\n";
5527 unless ($no_header) {
5528 print "<tr>\n";
5529 if ($check_forks) {
5530 print "<th></th>\n";
5532 print_sort_th('project', $order, 'Project');
5533 print_sort_th('descr', $order, 'Description');
5534 print_sort_th('owner', $order, 'Owner');
5535 print_sort_th('age', $order, 'Last Change');
5536 print "<th></th>\n" . # for links
5537 "</tr>\n";
5540 if ($projects_list_group_categories) {
5541 # only display categories with projects in the $from-$to window
5542 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
5543 my %categories = build_projlist_by_category(\@projects, $from, $to);
5544 foreach my $cat (sort keys %categories) {
5545 unless ($cat eq "") {
5546 print "<tr>\n";
5547 if ($check_forks) {
5548 print "<td></td>\n";
5550 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
5551 print "</tr>\n";
5554 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
5556 } else {
5557 git_project_list_rows(\@projects, $from, $to, $check_forks);
5560 if (defined $extra) {
5561 print "<tr>\n";
5562 if ($check_forks) {
5563 print "<td></td>\n";
5565 print "<td colspan=\"5\">$extra</td>\n" .
5566 "</tr>\n";
5568 print "</table>\n";
5571 sub git_log_body {
5572 # uses global variable $project
5573 my ($commitlist, $from, $to, $refs, $extra) = @_;
5575 $from = 0 unless defined $from;
5576 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5578 for (my $i = 0; $i <= $to; $i++) {
5579 my %co = %{$commitlist->[$i]};
5580 next if !%co;
5581 my $commit = $co{'id'};
5582 my $ref = format_ref_marker($refs, $commit);
5583 git_print_header_div('commit',
5584 "<span class=\"age\">$co{'age_string'}</span>" .
5585 esc_html($co{'title'}) . $ref,
5586 $commit);
5587 print "<div class=\"title_text\">\n" .
5588 "<div class=\"log_link\">\n" .
5589 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5590 " | " .
5591 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5592 " | " .
5593 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5594 "<br/>\n" .
5595 "</div>\n";
5596 git_print_authorship(\%co, -tag => 'span');
5597 print "<br/>\n</div>\n";
5599 print "<div class=\"log_body\">\n";
5600 git_print_log($co{'comment'}, -final_empty_line=> 1);
5601 print "</div>\n";
5603 if ($extra) {
5604 print "<div class=\"page_nav\">\n";
5605 print "$extra\n";
5606 print "</div>\n";
5610 sub git_shortlog_body {
5611 # uses global variable $project
5612 my ($commitlist, $from, $to, $refs, $extra) = @_;
5614 $from = 0 unless defined $from;
5615 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5617 print "<table class=\"shortlog\">\n";
5618 my $alternate = 1;
5619 for (my $i = $from; $i <= $to; $i++) {
5620 my %co = %{$commitlist->[$i]};
5621 my $commit = $co{'id'};
5622 my $ref = format_ref_marker($refs, $commit);
5623 if ($alternate) {
5624 print "<tr class=\"dark\">\n";
5625 } else {
5626 print "<tr class=\"light\">\n";
5628 $alternate ^= 1;
5629 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5630 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5631 format_author_html('td', \%co, 10) . "<td>";
5632 print format_subject_html($co{'title'}, $co{'title_short'},
5633 href(action=>"commit", hash=>$commit), $ref);
5634 print "</td>\n" .
5635 "<td class=\"link\">" .
5636 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5637 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5638 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5639 my $snapshot_links = format_snapshot_links($commit);
5640 if (defined $snapshot_links) {
5641 print " | " . $snapshot_links;
5643 print "</td>\n" .
5644 "</tr>\n";
5646 if (defined $extra) {
5647 print "<tr>\n" .
5648 "<td colspan=\"4\">$extra</td>\n" .
5649 "</tr>\n";
5651 print "</table>\n";
5654 sub git_history_body {
5655 # Warning: assumes constant type (blob or tree) during history
5656 my ($commitlist, $from, $to, $refs, $extra,
5657 $file_name, $file_hash, $ftype) = @_;
5659 $from = 0 unless defined $from;
5660 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5662 print "<table class=\"history\">\n";
5663 my $alternate = 1;
5664 for (my $i = $from; $i <= $to; $i++) {
5665 my %co = %{$commitlist->[$i]};
5666 if (!%co) {
5667 next;
5669 my $commit = $co{'id'};
5671 my $ref = format_ref_marker($refs, $commit);
5673 if ($alternate) {
5674 print "<tr class=\"dark\">\n";
5675 } else {
5676 print "<tr class=\"light\">\n";
5678 $alternate ^= 1;
5679 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5680 # shortlog: format_author_html('td', \%co, 10)
5681 format_author_html('td', \%co, 15, 3) . "<td>";
5682 # originally git_history used chop_str($co{'title'}, 50)
5683 print format_subject_html($co{'title'}, $co{'title_short'},
5684 href(action=>"commit", hash=>$commit), $ref);
5685 print "</td>\n" .
5686 "<td class=\"link\">" .
5687 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5688 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5690 if ($ftype eq 'blob') {
5691 my $blob_current = $file_hash;
5692 my $blob_parent = git_get_hash_by_path($commit, $file_name);
5693 if (defined $blob_current && defined $blob_parent &&
5694 $blob_current ne $blob_parent) {
5695 print " | " .
5696 $cgi->a({-href => href(action=>"blobdiff",
5697 hash=>$blob_current, hash_parent=>$blob_parent,
5698 hash_base=>$hash_base, hash_parent_base=>$commit,
5699 file_name=>$file_name)},
5700 "diff to current");
5703 print "</td>\n" .
5704 "</tr>\n";
5706 if (defined $extra) {
5707 print "<tr>\n" .
5708 "<td colspan=\"4\">$extra</td>\n" .
5709 "</tr>\n";
5711 print "</table>\n";
5714 sub git_tags_body {
5715 # uses global variable $project
5716 my ($taglist, $from, $to, $extra) = @_;
5717 $from = 0 unless defined $from;
5718 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5720 print "<table class=\"tags\">\n";
5721 my $alternate = 1;
5722 for (my $i = $from; $i <= $to; $i++) {
5723 my $entry = $taglist->[$i];
5724 my %tag = %$entry;
5725 my $comment = $tag{'subject'};
5726 my $comment_short;
5727 if (defined $comment) {
5728 $comment_short = chop_str($comment, 30, 5);
5730 if ($alternate) {
5731 print "<tr class=\"dark\">\n";
5732 } else {
5733 print "<tr class=\"light\">\n";
5735 $alternate ^= 1;
5736 if (defined $tag{'age'}) {
5737 print "<td><i>$tag{'age'}</i></td>\n";
5738 } else {
5739 print "<td></td>\n";
5741 print "<td>" .
5742 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5743 -class => "list name"}, esc_html($tag{'name'})) .
5744 "</td>\n" .
5745 "<td>";
5746 if (defined $comment) {
5747 print format_subject_html($comment, $comment_short,
5748 href(action=>"tag", hash=>$tag{'id'}));
5750 print "</td>\n" .
5751 "<td class=\"selflink\">";
5752 if ($tag{'type'} eq "tag") {
5753 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5754 } else {
5755 print "&nbsp;";
5757 print "</td>\n" .
5758 "<td class=\"link\">" . " | " .
5759 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5760 if ($tag{'reftype'} eq "commit") {
5761 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5762 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5763 } elsif ($tag{'reftype'} eq "blob") {
5764 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5766 print "</td>\n" .
5767 "</tr>";
5769 if (defined $extra) {
5770 print "<tr>\n" .
5771 "<td colspan=\"5\">$extra</td>\n" .
5772 "</tr>\n";
5774 print "</table>\n";
5777 sub git_heads_body {
5778 # uses global variable $project
5779 my ($headlist, $head_at, $from, $to, $extra) = @_;
5780 $from = 0 unless defined $from;
5781 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5783 print "<table class=\"heads\">\n";
5784 my $alternate = 1;
5785 for (my $i = $from; $i <= $to; $i++) {
5786 my $entry = $headlist->[$i];
5787 my %ref = %$entry;
5788 my $curr = defined $head_at && $ref{'id'} eq $head_at;
5789 if ($alternate) {
5790 print "<tr class=\"dark\">\n";
5791 } else {
5792 print "<tr class=\"light\">\n";
5794 $alternate ^= 1;
5795 print "<td><i>$ref{'age'}</i></td>\n" .
5796 ($curr ? "<td class=\"current_head\">" : "<td>") .
5797 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5798 -class => "list name"},esc_html($ref{'name'})) .
5799 "</td>\n" .
5800 "<td class=\"link\">" .
5801 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5802 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5803 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5804 "</td>\n" .
5805 "</tr>";
5807 if (defined $extra) {
5808 print "<tr>\n" .
5809 "<td colspan=\"3\">$extra</td>\n" .
5810 "</tr>\n";
5812 print "</table>\n";
5815 # Display a single remote block
5816 sub git_remote_block {
5817 my ($remote, $rdata, $limit, $head) = @_;
5819 my $heads = $rdata->{'heads'};
5820 my $fetch = $rdata->{'fetch'};
5821 my $push = $rdata->{'push'};
5823 my $urls_table = "<table class=\"projects_list\">\n" ;
5825 if (defined $fetch) {
5826 if ($fetch eq $push) {
5827 $urls_table .= format_repo_url("URL", $fetch);
5828 } else {
5829 $urls_table .= format_repo_url("Fetch URL", $fetch);
5830 $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5832 } elsif (defined $push) {
5833 $urls_table .= format_repo_url("Push URL", $push);
5834 } else {
5835 $urls_table .= format_repo_url("", "No remote URL");
5838 $urls_table .= "</table>\n";
5840 my $dots;
5841 if (defined $limit && $limit < @$heads) {
5842 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5845 print $urls_table;
5846 git_heads_body($heads, $head, 0, $limit, $dots);
5849 # Display a list of remote names with the respective fetch and push URLs
5850 sub git_remotes_list {
5851 my ($remotedata, $limit) = @_;
5852 print "<table class=\"heads\">\n";
5853 my $alternate = 1;
5854 my @remotes = sort keys %$remotedata;
5856 my $limited = $limit && $limit < @remotes;
5858 $#remotes = $limit - 1 if $limited;
5860 while (my $remote = shift @remotes) {
5861 my $rdata = $remotedata->{$remote};
5862 my $fetch = $rdata->{'fetch'};
5863 my $push = $rdata->{'push'};
5864 if ($alternate) {
5865 print "<tr class=\"dark\">\n";
5866 } else {
5867 print "<tr class=\"light\">\n";
5869 $alternate ^= 1;
5870 print "<td>" .
5871 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5872 -class=> "list name"},esc_html($remote)) .
5873 "</td>";
5874 print "<td class=\"link\">" .
5875 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5876 " | " .
5877 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5878 "</td>";
5880 print "</tr>\n";
5883 if ($limited) {
5884 print "<tr>\n" .
5885 "<td colspan=\"3\">" .
5886 $cgi->a({-href => href(action=>"remotes")}, "...") .
5887 "</td>\n" . "</tr>\n";
5890 print "</table>";
5893 # Display remote heads grouped by remote, unless there are too many
5894 # remotes, in which case we only display the remote names
5895 sub git_remotes_body {
5896 my ($remotedata, $limit, $head) = @_;
5897 if ($limit and $limit < keys %$remotedata) {
5898 git_remotes_list($remotedata, $limit);
5899 } else {
5900 fill_remote_heads($remotedata);
5901 while (my ($remote, $rdata) = each %$remotedata) {
5902 git_print_section({-class=>"remote", -id=>$remote},
5903 ["remotes", $remote, $remote], sub {
5904 git_remote_block($remote, $rdata, $limit, $head);
5910 sub git_search_message {
5911 my %co = @_;
5913 my $greptype;
5914 if ($searchtype eq 'commit') {
5915 $greptype = "--grep=";
5916 } elsif ($searchtype eq 'author') {
5917 $greptype = "--author=";
5918 } elsif ($searchtype eq 'committer') {
5919 $greptype = "--committer=";
5921 $greptype .= $searchtext;
5922 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5923 $greptype, '--regexp-ignore-case',
5924 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5926 my $paging_nav = '';
5927 if ($page > 0) {
5928 $paging_nav .=
5929 $cgi->a({-href => href(-replay=>1, page=>undef)},
5930 "first") .
5931 " &sdot; " .
5932 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5933 -accesskey => "p", -title => "Alt-p"}, "prev");
5934 } else {
5935 $paging_nav .= "first &sdot; prev";
5937 my $next_link = '';
5938 if ($#commitlist >= 100) {
5939 $next_link =
5940 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5941 -accesskey => "n", -title => "Alt-n"}, "next");
5942 $paging_nav .= " &sdot; $next_link";
5943 } else {
5944 $paging_nav .= " &sdot; next";
5947 git_header_html();
5949 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5950 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5951 if ($page == 0 && !@commitlist) {
5952 print "<p>No match.</p>\n";
5953 } else {
5954 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5957 git_footer_html();
5960 sub git_search_changes {
5961 my %co = @_;
5963 local $/ = "\n";
5964 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5965 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5966 ($search_use_regexp ? '--pickaxe-regex' : ())
5967 or die_error(500, "Open git-log failed");
5969 git_header_html();
5971 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5972 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5974 print "<table class=\"pickaxe search\">\n";
5975 my $alternate = 1;
5976 undef %co;
5977 my @files;
5978 while (my $line = <$fd>) {
5979 chomp $line;
5980 next unless $line;
5982 my %set = parse_difftree_raw_line($line);
5983 if (defined $set{'commit'}) {
5984 # finish previous commit
5985 if (%co) {
5986 print "</td>\n" .
5987 "<td class=\"link\">" .
5988 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
5989 "commit") .
5990 " | " .
5991 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
5992 hash_base=>$co{'id'})},
5993 "tree") .
5994 "</td>\n" .
5995 "</tr>\n";
5998 if ($alternate) {
5999 print "<tr class=\"dark\">\n";
6000 } else {
6001 print "<tr class=\"light\">\n";
6003 $alternate ^= 1;
6004 %co = parse_commit($set{'commit'});
6005 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6006 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6007 "<td><i>$author</i></td>\n" .
6008 "<td>" .
6009 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6010 -class => "list subject"},
6011 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6012 } elsif (defined $set{'to_id'}) {
6013 next if ($set{'to_id'} =~ m/^0{40}$/);
6015 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6016 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6017 -class => "list"},
6018 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6019 "<br/>\n";
6022 close $fd;
6024 # finish last commit (warning: repetition!)
6025 if (%co) {
6026 print "</td>\n" .
6027 "<td class=\"link\">" .
6028 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6029 "commit") .
6030 " | " .
6031 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6032 hash_base=>$co{'id'})},
6033 "tree") .
6034 "</td>\n" .
6035 "</tr>\n";
6038 print "</table>\n";
6040 git_footer_html();
6043 sub git_search_files {
6044 my %co = @_;
6046 local $/ = "\n";
6047 open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
6048 $search_use_regexp ? ('-E', '-i') : '-F',
6049 $searchtext, $co{'tree'}
6050 or die_error(500, "Open git-grep failed");
6052 git_header_html();
6054 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6055 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6057 print "<table class=\"grep_search\">\n";
6058 my $alternate = 1;
6059 my $matches = 0;
6060 my $lastfile = '';
6061 my $file_href;
6062 while (my $line = <$fd>) {
6063 chomp $line;
6064 my ($file, $lno, $ltext, $binary);
6065 last if ($matches++ > 1000);
6066 if ($line =~ /^Binary file (.+) matches$/) {
6067 $file = $1;
6068 $binary = 1;
6069 } else {
6070 ($file, $lno, $ltext) = split(/\0/, $line, 3);
6071 $file =~ s/^$co{'tree'}://;
6073 if ($file ne $lastfile) {
6074 $lastfile and print "</td></tr>\n";
6075 if ($alternate++) {
6076 print "<tr class=\"dark\">\n";
6077 } else {
6078 print "<tr class=\"light\">\n";
6080 $file_href = href(action=>"blob", hash_base=>$co{'id'},
6081 file_name=>$file);
6082 print "<td class=\"list\">".
6083 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
6084 print "</td><td>\n";
6085 $lastfile = $file;
6087 if ($binary) {
6088 print "<div class=\"binary\">Binary file</div>\n";
6089 } else {
6090 $ltext = untabify($ltext);
6091 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6092 $ltext = esc_html($1, -nbsp=>1);
6093 $ltext .= '<span class="match">';
6094 $ltext .= esc_html($2, -nbsp=>1);
6095 $ltext .= '</span>';
6096 $ltext .= esc_html($3, -nbsp=>1);
6097 } else {
6098 $ltext = esc_html($ltext, -nbsp=>1);
6100 print "<div class=\"pre\">" .
6101 $cgi->a({-href => $file_href.'#l'.$lno,
6102 -class => "linenr"}, sprintf('%4i', $lno)) .
6103 ' ' . $ltext . "</div>\n";
6106 if ($lastfile) {
6107 print "</td></tr>\n";
6108 if ($matches > 1000) {
6109 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6111 } else {
6112 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6114 close $fd;
6116 print "</table>\n";
6118 git_footer_html();
6121 sub git_search_grep_body {
6122 my ($commitlist, $from, $to, $extra) = @_;
6123 $from = 0 unless defined $from;
6124 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6126 print "<table class=\"commit_search\">\n";
6127 my $alternate = 1;
6128 for (my $i = $from; $i <= $to; $i++) {
6129 my %co = %{$commitlist->[$i]};
6130 if (!%co) {
6131 next;
6133 my $commit = $co{'id'};
6134 if ($alternate) {
6135 print "<tr class=\"dark\">\n";
6136 } else {
6137 print "<tr class=\"light\">\n";
6139 $alternate ^= 1;
6140 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6141 format_author_html('td', \%co, 15, 5) .
6142 "<td>" .
6143 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6144 -class => "list subject"},
6145 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6146 my $comment = $co{'comment'};
6147 foreach my $line (@$comment) {
6148 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
6149 my ($lead, $match, $trail) = ($1, $2, $3);
6150 $match = chop_str($match, 70, 5, 'center');
6151 my $contextlen = int((80 - length($match))/2);
6152 $contextlen = 30 if ($contextlen > 30);
6153 $lead = chop_str($lead, $contextlen, 10, 'left');
6154 $trail = chop_str($trail, $contextlen, 10, 'right');
6156 $lead = esc_html($lead);
6157 $match = esc_html($match);
6158 $trail = esc_html($trail);
6160 print "$lead<span class=\"match\">$match</span>$trail<br />";
6163 print "</td>\n" .
6164 "<td class=\"link\">" .
6165 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6166 " | " .
6167 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
6168 " | " .
6169 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6170 print "</td>\n" .
6171 "</tr>\n";
6173 if (defined $extra) {
6174 print "<tr>\n" .
6175 "<td colspan=\"3\">$extra</td>\n" .
6176 "</tr>\n";
6178 print "</table>\n";
6181 ## ======================================================================
6182 ## ======================================================================
6183 ## actions
6185 sub git_project_list {
6186 my $order = $input_params{'order'};
6187 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6188 die_error(400, "Unknown order parameter");
6191 my @list = git_get_projects_list($project_filter, $strict_export);
6192 if (!@list) {
6193 die_error(404, "No projects found");
6196 git_header_html();
6197 if (defined $home_text && -f $home_text) {
6198 print "<div class=\"index_include\">\n";
6199 insert_file($home_text);
6200 print "</div>\n";
6203 git_project_search_form($searchtext, $search_use_regexp);
6204 git_project_list_body(\@list, $order);
6205 git_footer_html();
6208 sub git_forks {
6209 my $order = $input_params{'order'};
6210 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6211 die_error(400, "Unknown order parameter");
6214 my $filter = $project;
6215 $filter =~ s/\.git$//;
6216 my @list = git_get_projects_list($filter);
6217 if (!@list) {
6218 die_error(404, "No forks found");
6221 git_header_html();
6222 git_print_page_nav('','');
6223 git_print_header_div('summary', "$project forks");
6224 git_project_list_body(\@list, $order);
6225 git_footer_html();
6228 sub git_project_index {
6229 my @projects = git_get_projects_list($project_filter, $strict_export);
6230 if (!@projects) {
6231 die_error(404, "No projects found");
6234 print $cgi->header(
6235 -type => 'text/plain',
6236 -charset => 'utf-8',
6237 -content_disposition => 'inline; filename="index.aux"');
6239 foreach my $pr (@projects) {
6240 if (!exists $pr->{'owner'}) {
6241 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
6244 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6245 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6246 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6247 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6248 $path =~ s/ /\+/g;
6249 $owner =~ s/ /\+/g;
6251 print "$path $owner\n";
6255 sub git_summary {
6256 my $descr = git_get_project_description($project) || "none";
6257 my %co = parse_commit("HEAD");
6258 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
6259 my $head = $co{'id'};
6260 my $remote_heads = gitweb_check_feature('remote_heads');
6262 my $owner = git_get_project_owner($project);
6264 my $refs = git_get_references();
6265 # These get_*_list functions return one more to allow us to see if
6266 # there are more ...
6267 my @taglist = git_get_tags_list(16);
6268 my @headlist = git_get_heads_list(16);
6269 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
6270 my @forklist;
6271 my $check_forks = gitweb_check_feature('forks');
6273 if ($check_forks) {
6274 # find forks of a project
6275 my $filter = $project;
6276 $filter =~ s/\.git$//;
6277 @forklist = git_get_projects_list($filter);
6278 # filter out forks of forks
6279 @forklist = filter_forks_from_projects_list(\@forklist)
6280 if (@forklist);
6283 git_header_html();
6284 git_print_page_nav('summary','', $head);
6286 print "<div class=\"title\">&nbsp;</div>\n";
6287 print "<table class=\"projects_list\">\n" .
6288 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
6289 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
6290 if (defined $cd{'rfc2822'}) {
6291 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6292 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
6295 # use per project git URL list in $projectroot/$project/cloneurl
6296 # or make project git URL from git base URL and project name
6297 my $url_tag = "URL";
6298 my @url_list = git_get_project_url_list($project);
6299 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6300 foreach my $git_url (@url_list) {
6301 next unless $git_url;
6302 print format_repo_url($url_tag, $git_url);
6303 $url_tag = "";
6306 # Tag cloud
6307 my $show_ctags = gitweb_check_feature('ctags');
6308 if ($show_ctags) {
6309 my $ctags = git_get_project_ctags($project);
6310 if (%$ctags) {
6311 # without ability to add tags, don't show if there are none
6312 my $cloud = git_populate_project_tagcloud($ctags);
6313 print "<tr id=\"metadata_ctags\">" .
6314 "<td>content tags</td>" .
6315 "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6316 "</tr>\n";
6320 print "</table>\n";
6322 # If XSS prevention is on, we don't include README.html.
6323 # TODO: Allow a readme in some safe format.
6324 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
6325 print "<div class=\"title\">readme</div>\n" .
6326 "<div class=\"readme\">\n";
6327 insert_file("$projectroot/$project/README.html");
6328 print "\n</div>\n"; # class="readme"
6331 # we need to request one more than 16 (0..15) to check if
6332 # those 16 are all
6333 my @commitlist = $head ? parse_commits($head, 17) : ();
6334 if (@commitlist) {
6335 git_print_header_div('shortlog');
6336 git_shortlog_body(\@commitlist, 0, 15, $refs,
6337 $#commitlist <= 15 ? undef :
6338 $cgi->a({-href => href(action=>"shortlog")}, "..."));
6341 if (@taglist) {
6342 git_print_header_div('tags');
6343 git_tags_body(\@taglist, 0, 15,
6344 $#taglist <= 15 ? undef :
6345 $cgi->a({-href => href(action=>"tags")}, "..."));
6348 if (@headlist) {
6349 git_print_header_div('heads');
6350 git_heads_body(\@headlist, $head, 0, 15,
6351 $#headlist <= 15 ? undef :
6352 $cgi->a({-href => href(action=>"heads")}, "..."));
6355 if (%remotedata) {
6356 git_print_header_div('remotes');
6357 git_remotes_body(\%remotedata, 15, $head);
6360 if (@forklist) {
6361 git_print_header_div('forks');
6362 git_project_list_body(\@forklist, 'age', 0, 15,
6363 $#forklist <= 15 ? undef :
6364 $cgi->a({-href => href(action=>"forks")}, "..."),
6365 'no_header');
6368 git_footer_html();
6371 sub git_tag {
6372 my %tag = parse_tag($hash);
6374 if (! %tag) {
6375 die_error(404, "Unknown tag object");
6378 my $head = git_get_head_hash($project);
6379 git_header_html();
6380 git_print_page_nav('','', $head,undef,$head);
6381 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
6382 print "<div class=\"title_text\">\n" .
6383 "<table class=\"object_header\">\n" .
6384 "<tr>\n" .
6385 "<td>object</td>\n" .
6386 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6387 $tag{'object'}) . "</td>\n" .
6388 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6389 $tag{'type'}) . "</td>\n" .
6390 "</tr>\n";
6391 if (defined($tag{'author'})) {
6392 git_print_authorship_rows(\%tag, 'author');
6394 print "</table>\n\n" .
6395 "</div>\n";
6396 print "<div class=\"page_body\">";
6397 my $comment = $tag{'comment'};
6398 foreach my $line (@$comment) {
6399 chomp $line;
6400 print esc_html($line, -nbsp=>1) . "<br/>\n";
6402 print "</div>\n";
6403 git_footer_html();
6406 sub git_blame_common {
6407 my $format = shift || 'porcelain';
6408 if ($format eq 'porcelain' && $input_params{'javascript'}) {
6409 $format = 'incremental';
6410 $action = 'blame_incremental'; # for page title etc
6413 # permissions
6414 gitweb_check_feature('blame')
6415 or die_error(403, "Blame view not allowed");
6417 # error checking
6418 die_error(400, "No file name given") unless $file_name;
6419 $hash_base ||= git_get_head_hash($project);
6420 die_error(404, "Couldn't find base commit") unless $hash_base;
6421 my %co = parse_commit($hash_base)
6422 or die_error(404, "Commit not found");
6423 my $ftype = "blob";
6424 if (!defined $hash) {
6425 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
6426 or die_error(404, "Error looking up file");
6427 } else {
6428 $ftype = git_get_type($hash);
6429 if ($ftype !~ "blob") {
6430 die_error(400, "Object is not a blob");
6434 my $fd;
6435 if ($format eq 'incremental') {
6436 # get file contents (as base)
6437 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
6438 or die_error(500, "Open git-cat-file failed");
6439 } elsif ($format eq 'data') {
6440 # run git-blame --incremental
6441 open $fd, "-|", git_cmd(), "blame", "--incremental",
6442 $hash_base, "--", $file_name
6443 or die_error(500, "Open git-blame --incremental failed");
6444 } else {
6445 # run git-blame --porcelain
6446 open $fd, "-|", git_cmd(), "blame", '-p',
6447 $hash_base, '--', $file_name
6448 or die_error(500, "Open git-blame --porcelain failed");
6451 # incremental blame data returns early
6452 if ($format eq 'data') {
6453 print $cgi->header(
6454 -type=>"text/plain", -charset => "utf-8",
6455 -status=> "200 OK");
6456 local $| = 1; # output autoflush
6457 while (my $line = <$fd>) {
6458 print to_utf8($line);
6460 close $fd
6461 or print "ERROR $!\n";
6463 print 'END';
6464 if (defined $t0 && gitweb_check_feature('timed')) {
6465 print ' '.
6466 tv_interval($t0, [ gettimeofday() ]).
6467 ' '.$number_of_git_cmds;
6469 print "\n";
6471 return;
6474 # page header
6475 git_header_html();
6476 my $formats_nav =
6477 $cgi->a({-href => href(action=>"blob", -replay=>1)},
6478 "blob") .
6479 " | ";
6480 if ($format eq 'incremental') {
6481 $formats_nav .=
6482 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6483 "blame") . " (non-incremental)";
6484 } else {
6485 $formats_nav .=
6486 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6487 "blame") . " (incremental)";
6489 $formats_nav .=
6490 " | " .
6491 $cgi->a({-href => href(action=>"history", -replay=>1)},
6492 "history") .
6493 " | " .
6494 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
6495 "HEAD");
6496 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6497 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6498 git_print_page_path($file_name, $ftype, $hash_base);
6500 # page body
6501 if ($format eq 'incremental') {
6502 print "<noscript>\n<div class=\"error\"><center><b>\n".
6503 "This page requires JavaScript to run.\n Use ".
6504 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
6505 'this page').
6506 " instead.\n".
6507 "</b></center></div>\n</noscript>\n";
6509 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6512 print qq!<div class="page_body">\n!;
6513 print qq!<div id="progress_info">... / ...</div>\n!
6514 if ($format eq 'incremental');
6515 print qq!<table id="blame_table" class="blame" width="100%">\n!.
6516 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6517 qq!<thead>\n!.
6518 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6519 qq!</thead>\n!.
6520 qq!<tbody>\n!;
6522 my @rev_color = qw(light dark);
6523 my $num_colors = scalar(@rev_color);
6524 my $current_color = 0;
6526 if ($format eq 'incremental') {
6527 my $color_class = $rev_color[$current_color];
6529 #contents of a file
6530 my $linenr = 0;
6531 LINE:
6532 while (my $line = <$fd>) {
6533 chomp $line;
6534 $linenr++;
6536 print qq!<tr id="l$linenr" class="$color_class">!.
6537 qq!<td class="sha1"><a href=""> </a></td>!.
6538 qq!<td class="linenr">!.
6539 qq!<a class="linenr" href="">$linenr</a></td>!;
6540 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
6541 print qq!</tr>\n!;
6544 } else { # porcelain, i.e. ordinary blame
6545 my %metainfo = (); # saves information about commits
6547 # blame data
6548 LINE:
6549 while (my $line = <$fd>) {
6550 chomp $line;
6551 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
6552 # no <lines in group> for subsequent lines in group of lines
6553 my ($full_rev, $orig_lineno, $lineno, $group_size) =
6554 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
6555 if (!exists $metainfo{$full_rev}) {
6556 $metainfo{$full_rev} = { 'nprevious' => 0 };
6558 my $meta = $metainfo{$full_rev};
6559 my $data;
6560 while ($data = <$fd>) {
6561 chomp $data;
6562 last if ($data =~ s/^\t//); # contents of line
6563 if ($data =~ /^(\S+)(?: (.*))?$/) {
6564 $meta->{$1} = $2 unless exists $meta->{$1};
6566 if ($data =~ /^previous /) {
6567 $meta->{'nprevious'}++;
6570 my $short_rev = substr($full_rev, 0, 8);
6571 my $author = $meta->{'author'};
6572 my %date =
6573 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
6574 my $date = $date{'iso-tz'};
6575 if ($group_size) {
6576 $current_color = ($current_color + 1) % $num_colors;
6578 my $tr_class = $rev_color[$current_color];
6579 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
6580 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
6581 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
6582 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
6583 if ($group_size) {
6584 print "<td class=\"sha1\"";
6585 print " title=\"". esc_html($author) . ", $date\"";
6586 print " rowspan=\"$group_size\"" if ($group_size > 1);
6587 print ">";
6588 print $cgi->a({-href => href(action=>"commit",
6589 hash=>$full_rev,
6590 file_name=>$file_name)},
6591 esc_html($short_rev));
6592 if ($group_size >= 2) {
6593 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
6594 if (@author_initials) {
6595 print "<br />" .
6596 esc_html(join('', @author_initials));
6597 # or join('.', ...)
6600 print "</td>\n";
6602 # 'previous' <sha1 of parent commit> <filename at commit>
6603 if (exists $meta->{'previous'} &&
6604 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
6605 $meta->{'parent'} = $1;
6606 $meta->{'file_parent'} = unquote($2);
6608 my $linenr_commit =
6609 exists($meta->{'parent'}) ?
6610 $meta->{'parent'} : $full_rev;
6611 my $linenr_filename =
6612 exists($meta->{'file_parent'}) ?
6613 $meta->{'file_parent'} : unquote($meta->{'filename'});
6614 my $blamed = href(action => 'blame',
6615 file_name => $linenr_filename,
6616 hash_base => $linenr_commit);
6617 print "<td class=\"linenr\">";
6618 print $cgi->a({ -href => "$blamed#l$orig_lineno",
6619 -class => "linenr" },
6620 esc_html($lineno));
6621 print "</td>";
6622 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
6623 print "</tr>\n";
6624 } # end while
6628 # footer
6629 print "</tbody>\n".
6630 "</table>\n"; # class="blame"
6631 print "</div>\n"; # class="blame_body"
6632 close $fd
6633 or print "Reading blob failed\n";
6635 git_footer_html();
6638 sub git_blame {
6639 git_blame_common();
6642 sub git_blame_incremental {
6643 git_blame_common('incremental');
6646 sub git_blame_data {
6647 git_blame_common('data');
6650 sub git_tags {
6651 my $head = git_get_head_hash($project);
6652 git_header_html();
6653 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
6654 git_print_header_div('summary', $project);
6656 my @tagslist = git_get_tags_list();
6657 if (@tagslist) {
6658 git_tags_body(\@tagslist);
6660 git_footer_html();
6663 sub git_heads {
6664 my $head = git_get_head_hash($project);
6665 git_header_html();
6666 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
6667 git_print_header_div('summary', $project);
6669 my @headslist = git_get_heads_list();
6670 if (@headslist) {
6671 git_heads_body(\@headslist, $head);
6673 git_footer_html();
6676 # used both for single remote view and for list of all the remotes
6677 sub git_remotes {
6678 gitweb_check_feature('remote_heads')
6679 or die_error(403, "Remote heads view is disabled");
6681 my $head = git_get_head_hash($project);
6682 my $remote = $input_params{'hash'};
6684 my $remotedata = git_get_remotes_list($remote);
6685 die_error(500, "Unable to get remote information") unless defined $remotedata;
6687 unless (%$remotedata) {
6688 die_error(404, defined $remote ?
6689 "Remote $remote not found" :
6690 "No remotes found");
6693 git_header_html(undef, undef, -action_extra => $remote);
6694 git_print_page_nav('', '', $head, undef, $head,
6695 format_ref_views($remote ? '' : 'remotes'));
6697 fill_remote_heads($remotedata);
6698 if (defined $remote) {
6699 git_print_header_div('remotes', "$remote remote for $project");
6700 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
6701 } else {
6702 git_print_header_div('summary', "$project remotes");
6703 git_remotes_body($remotedata, undef, $head);
6706 git_footer_html();
6709 sub git_blob_plain {
6710 my $type = shift;
6711 my $expires;
6713 if (!defined $hash) {
6714 if (defined $file_name) {
6715 my $base = $hash_base || git_get_head_hash($project);
6716 $hash = git_get_hash_by_path($base, $file_name, "blob")
6717 or die_error(404, "Cannot find file");
6718 } else {
6719 die_error(400, "No file name defined");
6721 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6722 # blobs defined by non-textual hash id's can be cached
6723 $expires = "+1d";
6726 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6727 or die_error(500, "Open git-cat-file blob '$hash' failed");
6729 # content-type (can include charset)
6730 $type = blob_contenttype($fd, $file_name, $type);
6732 # "save as" filename, even when no $file_name is given
6733 my $save_as = "$hash";
6734 if (defined $file_name) {
6735 $save_as = $file_name;
6736 } elsif ($type =~ m/^text\//) {
6737 $save_as .= '.txt';
6740 # With XSS prevention on, blobs of all types except a few known safe
6741 # ones are served with "Content-Disposition: attachment" to make sure
6742 # they don't run in our security domain. For certain image types,
6743 # blob view writes an <img> tag referring to blob_plain view, and we
6744 # want to be sure not to break that by serving the image as an
6745 # attachment (though Firefox 3 doesn't seem to care).
6746 my $sandbox = $prevent_xss &&
6747 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
6749 # serve text/* as text/plain
6750 if ($prevent_xss &&
6751 ($type =~ m!^text/[a-z]+\b(.*)$! ||
6752 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
6753 my $rest = $1;
6754 $rest = defined $rest ? $rest : '';
6755 $type = "text/plain$rest";
6758 print $cgi->header(
6759 -type => $type,
6760 -expires => $expires,
6761 -content_disposition =>
6762 ($sandbox ? 'attachment' : 'inline')
6763 . '; filename="' . $save_as . '"');
6764 local $/ = undef;
6765 binmode STDOUT, ':raw';
6766 print <$fd>;
6767 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6768 close $fd;
6771 sub git_blob {
6772 my $expires;
6774 if (!defined $hash) {
6775 if (defined $file_name) {
6776 my $base = $hash_base || git_get_head_hash($project);
6777 $hash = git_get_hash_by_path($base, $file_name, "blob")
6778 or die_error(404, "Cannot find file");
6779 } else {
6780 die_error(400, "No file name defined");
6782 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6783 # blobs defined by non-textual hash id's can be cached
6784 $expires = "+1d";
6787 my $have_blame = gitweb_check_feature('blame');
6788 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6789 or die_error(500, "Couldn't cat $file_name, $hash");
6790 my $mimetype = blob_mimetype($fd, $file_name);
6791 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6792 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6793 close $fd;
6794 return git_blob_plain($mimetype);
6796 # we can have blame only for text/* mimetype
6797 $have_blame &&= ($mimetype =~ m!^text/!);
6799 my $highlight = gitweb_check_feature('highlight');
6800 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6801 $fd = run_highlighter($fd, $highlight, $syntax)
6802 if $syntax;
6804 git_header_html(undef, $expires);
6805 my $formats_nav = '';
6806 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6807 if (defined $file_name) {
6808 if ($have_blame) {
6809 $formats_nav .=
6810 $cgi->a({-href => href(action=>"blame", -replay=>1)},
6811 "blame") .
6812 " | ";
6814 $formats_nav .=
6815 $cgi->a({-href => href(action=>"history", -replay=>1)},
6816 "history") .
6817 " | " .
6818 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6819 "raw") .
6820 " | " .
6821 $cgi->a({-href => href(action=>"blob",
6822 hash_base=>"HEAD", file_name=>$file_name)},
6823 "HEAD");
6824 } else {
6825 $formats_nav .=
6826 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6827 "raw");
6829 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6830 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6831 } else {
6832 print "<div class=\"page_nav\">\n" .
6833 "<br/><br/></div>\n" .
6834 "<div class=\"title\">".esc_html($hash)."</div>\n";
6836 git_print_page_path($file_name, "blob", $hash_base);
6837 print "<div class=\"page_body\">\n";
6838 if ($mimetype =~ m!^image/!) {
6839 print qq!<img type="!.esc_attr($mimetype).qq!"!;
6840 if ($file_name) {
6841 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6843 print qq! src="! .
6844 href(action=>"blob_plain", hash=>$hash,
6845 hash_base=>$hash_base, file_name=>$file_name) .
6846 qq!" />\n!;
6847 } else {
6848 my $nr;
6849 while (my $line = <$fd>) {
6850 chomp $line;
6851 $nr++;
6852 $line = untabify($line);
6853 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6854 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
6855 $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);
6858 close $fd
6859 or print "Reading blob failed.\n";
6860 print "</div>";
6861 git_footer_html();
6864 sub git_tree {
6865 if (!defined $hash_base) {
6866 $hash_base = "HEAD";
6868 if (!defined $hash) {
6869 if (defined $file_name) {
6870 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6871 } else {
6872 $hash = $hash_base;
6875 die_error(404, "No such tree") unless defined($hash);
6877 my $show_sizes = gitweb_check_feature('show-sizes');
6878 my $have_blame = gitweb_check_feature('blame');
6880 my @entries = ();
6882 local $/ = "\0";
6883 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6884 ($show_sizes ? '-l' : ()), @extra_options, $hash
6885 or die_error(500, "Open git-ls-tree failed");
6886 @entries = map { chomp; $_ } <$fd>;
6887 close $fd
6888 or die_error(404, "Reading tree failed");
6891 my $refs = git_get_references();
6892 my $ref = format_ref_marker($refs, $hash_base);
6893 git_header_html();
6894 my $basedir = '';
6895 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6896 my @views_nav = ();
6897 if (defined $file_name) {
6898 push @views_nav,
6899 $cgi->a({-href => href(action=>"history", -replay=>1)},
6900 "history"),
6901 $cgi->a({-href => href(action=>"tree",
6902 hash_base=>"HEAD", file_name=>$file_name)},
6903 "HEAD"),
6905 my $snapshot_links = format_snapshot_links($hash);
6906 if (defined $snapshot_links) {
6907 # FIXME: Should be available when we have no hash base as well.
6908 push @views_nav, $snapshot_links;
6910 git_print_page_nav('tree','', $hash_base, undef, undef,
6911 join(' | ', @views_nav));
6912 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
6913 } else {
6914 undef $hash_base;
6915 print "<div class=\"page_nav\">\n";
6916 print "<br/><br/></div>\n";
6917 print "<div class=\"title\">".esc_html($hash)."</div>\n";
6919 if (defined $file_name) {
6920 $basedir = $file_name;
6921 if ($basedir ne '' && substr($basedir, -1) ne '/') {
6922 $basedir .= '/';
6924 git_print_page_path($file_name, 'tree', $hash_base);
6926 print "<div class=\"page_body\">\n";
6927 print "<table class=\"tree\">\n";
6928 my $alternate = 1;
6929 # '..' (top directory) link if possible
6930 if (defined $hash_base &&
6931 defined $file_name && $file_name =~ m![^/]+$!) {
6932 if ($alternate) {
6933 print "<tr class=\"dark\">\n";
6934 } else {
6935 print "<tr class=\"light\">\n";
6937 $alternate ^= 1;
6939 my $up = $file_name;
6940 $up =~ s!/?[^/]+$!!;
6941 undef $up unless $up;
6942 # based on git_print_tree_entry
6943 print '<td class="mode">' . mode_str('040000') . "</td>\n";
6944 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
6945 print '<td class="list">';
6946 print $cgi->a({-href => href(action=>"tree",
6947 hash_base=>$hash_base,
6948 file_name=>$up)},
6949 "..");
6950 print "</td>\n";
6951 print "<td class=\"link\"></td>\n";
6953 print "</tr>\n";
6955 foreach my $line (@entries) {
6956 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
6958 if ($alternate) {
6959 print "<tr class=\"dark\">\n";
6960 } else {
6961 print "<tr class=\"light\">\n";
6963 $alternate ^= 1;
6965 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
6967 print "</tr>\n";
6969 print "</table>\n" .
6970 "</div>";
6971 git_footer_html();
6974 sub snapshot_name {
6975 my ($project, $hash) = @_;
6977 # path/to/project.git -> project
6978 # path/to/project/.git -> project
6979 my $name = to_utf8($project);
6980 $name =~ s,([^/])/*\.git$,$1,;
6981 $name = basename($name);
6982 # sanitize name
6983 $name =~ s/[[:cntrl:]]/?/g;
6985 my $ver = $hash;
6986 if ($hash =~ /^[0-9a-fA-F]+$/) {
6987 # shorten SHA-1 hash
6988 my $full_hash = git_get_full_hash($project, $hash);
6989 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6990 $ver = git_get_short_hash($project, $hash);
6992 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6993 # tags don't need shortened SHA-1 hash
6994 $ver = $1;
6995 } else {
6996 # branches and other need shortened SHA-1 hash
6997 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
6998 $ver = $1;
7000 $ver .= '-' . git_get_short_hash($project, $hash);
7002 # in case of hierarchical branch names
7003 $ver =~ s!/!.!g;
7005 # name = project-version_string
7006 $name = "$name-$ver";
7008 return wantarray ? ($name, $name) : $name;
7011 sub git_snapshot {
7012 my $format = $input_params{'snapshot_format'};
7013 if (!@snapshot_fmts) {
7014 die_error(403, "Snapshots not allowed");
7016 # default to first supported snapshot format
7017 $format ||= $snapshot_fmts[0];
7018 if ($format !~ m/^[a-z0-9]+$/) {
7019 die_error(400, "Invalid snapshot format parameter");
7020 } elsif (!exists($known_snapshot_formats{$format})) {
7021 die_error(400, "Unknown snapshot format");
7022 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
7023 die_error(403, "Snapshot format not allowed");
7024 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
7025 die_error(403, "Unsupported snapshot format");
7028 my $type = git_get_type("$hash^{}");
7029 if (!$type) {
7030 die_error(404, 'Object does not exist');
7031 } elsif ($type eq 'blob') {
7032 die_error(400, 'Object is not a tree-ish');
7035 my ($name, $prefix) = snapshot_name($project, $hash);
7036 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
7037 my $cmd = quote_command(
7038 git_cmd(), 'archive',
7039 "--format=$known_snapshot_formats{$format}{'format'}",
7040 "--prefix=$prefix/", $hash);
7041 if (exists $known_snapshot_formats{$format}{'compressor'}) {
7042 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
7045 $filename =~ s/(["\\])/\\$1/g;
7046 print $cgi->header(
7047 -type => $known_snapshot_formats{$format}{'type'},
7048 -content_disposition => 'inline; filename="' . $filename . '"',
7049 -status => '200 OK');
7051 open my $fd, "-|", $cmd
7052 or die_error(500, "Execute git-archive failed");
7053 binmode STDOUT, ':raw';
7054 print <$fd>;
7055 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7056 close $fd;
7059 sub git_log_generic {
7060 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
7062 my $head = git_get_head_hash($project);
7063 if (!defined $base) {
7064 $base = $head;
7066 if (!defined $page) {
7067 $page = 0;
7069 my $refs = git_get_references();
7071 my $commit_hash = $base;
7072 if (defined $parent) {
7073 $commit_hash = "$parent..$base";
7075 my @commitlist =
7076 parse_commits($commit_hash, 101, (100 * $page),
7077 defined $file_name ? ($file_name, "--full-history") : ());
7079 my $ftype;
7080 if (!defined $file_hash && defined $file_name) {
7081 # some commits could have deleted file in question,
7082 # and not have it in tree, but one of them has to have it
7083 for (my $i = 0; $i < @commitlist; $i++) {
7084 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
7085 last if defined $file_hash;
7088 if (defined $file_hash) {
7089 $ftype = git_get_type($file_hash);
7091 if (defined $file_name && !defined $ftype) {
7092 die_error(500, "Unknown type of object");
7094 my %co;
7095 if (defined $file_name) {
7096 %co = parse_commit($base)
7097 or die_error(404, "Unknown commit object");
7101 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
7102 my $next_link = '';
7103 if ($#commitlist >= 100) {
7104 $next_link =
7105 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7106 -accesskey => "n", -title => "Alt-n"}, "next");
7108 my $patch_max = gitweb_get_feature('patches');
7109 if ($patch_max && !defined $file_name) {
7110 if ($patch_max < 0 || @commitlist <= $patch_max) {
7111 $paging_nav .= " &sdot; " .
7112 $cgi->a({-href => href(action=>"patches", -replay=>1)},
7113 "patches");
7117 git_header_html();
7118 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
7119 if (defined $file_name) {
7120 git_print_header_div('commit', esc_html($co{'title'}), $base);
7121 } else {
7122 git_print_header_div('summary', $project)
7124 git_print_page_path($file_name, $ftype, $hash_base)
7125 if (defined $file_name);
7127 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
7128 $file_name, $file_hash, $ftype);
7130 git_footer_html();
7133 sub git_log {
7134 git_log_generic('log', \&git_log_body,
7135 $hash, $hash_parent);
7138 sub git_commit {
7139 $hash ||= $hash_base || "HEAD";
7140 my %co = parse_commit($hash)
7141 or die_error(404, "Unknown commit object");
7143 my $parent = $co{'parent'};
7144 my $parents = $co{'parents'}; # listref
7146 # we need to prepare $formats_nav before any parameter munging
7147 my $formats_nav;
7148 if (!defined $parent) {
7149 # --root commitdiff
7150 $formats_nav .= '(initial)';
7151 } elsif (@$parents == 1) {
7152 # single parent commit
7153 $formats_nav .=
7154 '(parent: ' .
7155 $cgi->a({-href => href(action=>"commit",
7156 hash=>$parent)},
7157 esc_html(substr($parent, 0, 7))) .
7158 ')';
7159 } else {
7160 # merge commit
7161 $formats_nav .=
7162 '(merge: ' .
7163 join(' ', map {
7164 $cgi->a({-href => href(action=>"commit",
7165 hash=>$_)},
7166 esc_html(substr($_, 0, 7)));
7167 } @$parents ) .
7168 ')';
7170 if (gitweb_check_feature('patches') && @$parents <= 1) {
7171 $formats_nav .= " | " .
7172 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7173 "patch");
7176 if (!defined $parent) {
7177 $parent = "--root";
7179 my @difftree;
7180 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
7181 @diff_opts,
7182 (@$parents <= 1 ? $parent : '-c'),
7183 $hash, "--"
7184 or die_error(500, "Open git-diff-tree failed");
7185 @difftree = map { chomp; $_ } <$fd>;
7186 close $fd or die_error(404, "Reading git-diff-tree failed");
7188 # non-textual hash id's can be cached
7189 my $expires;
7190 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7191 $expires = "+1d";
7193 my $refs = git_get_references();
7194 my $ref = format_ref_marker($refs, $co{'id'});
7196 git_header_html(undef, $expires);
7197 git_print_page_nav('commit', '',
7198 $hash, $co{'tree'}, $hash,
7199 $formats_nav);
7201 if (defined $co{'parent'}) {
7202 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
7203 } else {
7204 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
7206 print "<div class=\"title_text\">\n" .
7207 "<table class=\"object_header\">\n";
7208 git_print_authorship_rows(\%co);
7209 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
7210 print "<tr>" .
7211 "<td>tree</td>" .
7212 "<td class=\"sha1\">" .
7213 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7214 class => "list"}, $co{'tree'}) .
7215 "</td>" .
7216 "<td class=\"link\">" .
7217 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7218 "tree");
7219 my $snapshot_links = format_snapshot_links($hash);
7220 if (defined $snapshot_links) {
7221 print " | " . $snapshot_links;
7223 print "</td>" .
7224 "</tr>\n";
7226 foreach my $par (@$parents) {
7227 print "<tr>" .
7228 "<td>parent</td>" .
7229 "<td class=\"sha1\">" .
7230 $cgi->a({-href => href(action=>"commit", hash=>$par),
7231 class => "list"}, $par) .
7232 "</td>" .
7233 "<td class=\"link\">" .
7234 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
7235 " | " .
7236 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
7237 "</td>" .
7238 "</tr>\n";
7240 print "</table>".
7241 "</div>\n";
7243 print "<div class=\"page_body\">\n";
7244 git_print_log($co{'comment'});
7245 print "</div>\n";
7247 git_difftree_body(\@difftree, $hash, @$parents);
7249 git_footer_html();
7252 sub git_object {
7253 # object is defined by:
7254 # - hash or hash_base alone
7255 # - hash_base and file_name
7256 my $type;
7258 # - hash or hash_base alone
7259 if ($hash || ($hash_base && !defined $file_name)) {
7260 my $object_id = $hash || $hash_base;
7262 open my $fd, "-|", quote_command(
7263 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
7264 or die_error(404, "Object does not exist");
7265 $type = <$fd>;
7266 chomp $type;
7267 close $fd
7268 or die_error(404, "Object does not exist");
7270 # - hash_base and file_name
7271 } elsif ($hash_base && defined $file_name) {
7272 $file_name =~ s,/+$,,;
7274 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
7275 or die_error(404, "Base object does not exist");
7277 # here errors should not hapen
7278 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
7279 or die_error(500, "Open git-ls-tree failed");
7280 my $line = <$fd>;
7281 close $fd;
7283 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
7284 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
7285 die_error(404, "File or directory for given base does not exist");
7287 $type = $2;
7288 $hash = $3;
7289 } else {
7290 die_error(400, "Not enough information to find object");
7293 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7294 hash=>$hash, hash_base=>$hash_base,
7295 file_name=>$file_name),
7296 -status => '302 Found');
7299 sub git_blobdiff {
7300 my $format = shift || 'html';
7301 my $diff_style = $input_params{'diff_style'} || 'inline';
7303 my $fd;
7304 my @difftree;
7305 my %diffinfo;
7306 my $expires;
7308 # preparing $fd and %diffinfo for git_patchset_body
7309 # new style URI
7310 if (defined $hash_base && defined $hash_parent_base) {
7311 if (defined $file_name) {
7312 # read raw output
7313 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7314 $hash_parent_base, $hash_base,
7315 "--", (defined $file_parent ? $file_parent : ()), $file_name
7316 or die_error(500, "Open git-diff-tree failed");
7317 @difftree = map { chomp; $_ } <$fd>;
7318 close $fd
7319 or die_error(404, "Reading git-diff-tree failed");
7320 @difftree
7321 or die_error(404, "Blob diff not found");
7323 } elsif (defined $hash &&
7324 $hash =~ /[0-9a-fA-F]{40}/) {
7325 # try to find filename from $hash
7327 # read filtered raw output
7328 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7329 $hash_parent_base, $hash_base, "--"
7330 or die_error(500, "Open git-diff-tree failed");
7331 @difftree =
7332 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
7333 # $hash == to_id
7334 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7335 map { chomp; $_ } <$fd>;
7336 close $fd
7337 or die_error(404, "Reading git-diff-tree failed");
7338 @difftree
7339 or die_error(404, "Blob diff not found");
7341 } else {
7342 die_error(400, "Missing one of the blob diff parameters");
7345 if (@difftree > 1) {
7346 die_error(400, "Ambiguous blob diff specification");
7349 %diffinfo = parse_difftree_raw_line($difftree[0]);
7350 $file_parent ||= $diffinfo{'from_file'} || $file_name;
7351 $file_name ||= $diffinfo{'to_file'};
7353 $hash_parent ||= $diffinfo{'from_id'};
7354 $hash ||= $diffinfo{'to_id'};
7356 # non-textual hash id's can be cached
7357 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
7358 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
7359 $expires = '+1d';
7362 # open patch output
7363 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7364 '-p', ($format eq 'html' ? "--full-index" : ()),
7365 $hash_parent_base, $hash_base,
7366 "--", (defined $file_parent ? $file_parent : ()), $file_name
7367 or die_error(500, "Open git-diff-tree failed");
7370 # old/legacy style URI -- not generated anymore since 1.4.3.
7371 if (!%diffinfo) {
7372 die_error('404 Not Found', "Missing one of the blob diff parameters")
7375 # header
7376 if ($format eq 'html') {
7377 my $formats_nav =
7378 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
7379 "raw");
7380 $formats_nav .= diff_style_nav($diff_style);
7381 git_header_html(undef, $expires);
7382 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7383 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7384 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7385 } else {
7386 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
7387 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
7389 if (defined $file_name) {
7390 git_print_page_path($file_name, "blob", $hash_base);
7391 } else {
7392 print "<div class=\"page_path\"></div>\n";
7395 } elsif ($format eq 'plain') {
7396 print $cgi->header(
7397 -type => 'text/plain',
7398 -charset => 'utf-8',
7399 -expires => $expires,
7400 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
7402 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7404 } else {
7405 die_error(400, "Unknown blobdiff format");
7408 # patch
7409 if ($format eq 'html') {
7410 print "<div class=\"page_body\">\n";
7412 git_patchset_body($fd, $diff_style,
7413 [ \%diffinfo ], $hash_base, $hash_parent_base);
7414 close $fd;
7416 print "</div>\n"; # class="page_body"
7417 git_footer_html();
7419 } else {
7420 while (my $line = <$fd>) {
7421 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7422 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
7424 print $line;
7426 last if $line =~ m!^\+\+\+!;
7428 local $/ = undef;
7429 print <$fd>;
7430 close $fd;
7434 sub git_blobdiff_plain {
7435 git_blobdiff('plain');
7438 # assumes that it is added as later part of already existing navigation,
7439 # so it returns "| foo | bar" rather than just "foo | bar"
7440 sub diff_style_nav {
7441 my ($diff_style, $is_combined) = @_;
7442 $diff_style ||= 'inline';
7444 return "" if ($is_combined);
7446 my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7447 my %styles = @styles;
7448 @styles =
7449 @styles[ map { $_ * 2 } 0..$#styles/2 ];
7451 return join '',
7452 map { " | ".$_ }
7453 map {
7454 $_ eq $diff_style ? $styles{$_} :
7455 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7456 } @styles;
7459 sub git_commitdiff {
7460 my %params = @_;
7461 my $format = $params{-format} || 'html';
7462 my $diff_style = $input_params{'diff_style'} || 'inline';
7464 my ($patch_max) = gitweb_get_feature('patches');
7465 if ($format eq 'patch') {
7466 die_error(403, "Patch view not allowed") unless $patch_max;
7469 $hash ||= $hash_base || "HEAD";
7470 my %co = parse_commit($hash)
7471 or die_error(404, "Unknown commit object");
7473 # choose format for commitdiff for merge
7474 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
7475 $hash_parent = '--cc';
7477 # we need to prepare $formats_nav before almost any parameter munging
7478 my $formats_nav;
7479 if ($format eq 'html') {
7480 $formats_nav =
7481 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
7482 "raw");
7483 if ($patch_max && @{$co{'parents'}} <= 1) {
7484 $formats_nav .= " | " .
7485 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7486 "patch");
7488 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
7490 if (defined $hash_parent &&
7491 $hash_parent ne '-c' && $hash_parent ne '--cc') {
7492 # commitdiff with two commits given
7493 my $hash_parent_short = $hash_parent;
7494 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
7495 $hash_parent_short = substr($hash_parent, 0, 7);
7497 $formats_nav .=
7498 ' (from';
7499 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
7500 if ($co{'parents'}[$i] eq $hash_parent) {
7501 $formats_nav .= ' parent ' . ($i+1);
7502 last;
7505 $formats_nav .= ': ' .
7506 $cgi->a({-href => href(-replay=>1,
7507 hash=>$hash_parent, hash_base=>undef)},
7508 esc_html($hash_parent_short)) .
7509 ')';
7510 } elsif (!$co{'parent'}) {
7511 # --root commitdiff
7512 $formats_nav .= ' (initial)';
7513 } elsif (scalar @{$co{'parents'}} == 1) {
7514 # single parent commit
7515 $formats_nav .=
7516 ' (parent: ' .
7517 $cgi->a({-href => href(-replay=>1,
7518 hash=>$co{'parent'}, hash_base=>undef)},
7519 esc_html(substr($co{'parent'}, 0, 7))) .
7520 ')';
7521 } else {
7522 # merge commit
7523 if ($hash_parent eq '--cc') {
7524 $formats_nav .= ' | ' .
7525 $cgi->a({-href => href(-replay=>1,
7526 hash=>$hash, hash_parent=>'-c')},
7527 'combined');
7528 } else { # $hash_parent eq '-c'
7529 $formats_nav .= ' | ' .
7530 $cgi->a({-href => href(-replay=>1,
7531 hash=>$hash, hash_parent=>'--cc')},
7532 'compact');
7534 $formats_nav .=
7535 ' (merge: ' .
7536 join(' ', map {
7537 $cgi->a({-href => href(-replay=>1,
7538 hash=>$_, hash_base=>undef)},
7539 esc_html(substr($_, 0, 7)));
7540 } @{$co{'parents'}} ) .
7541 ')';
7545 my $hash_parent_param = $hash_parent;
7546 if (!defined $hash_parent_param) {
7547 # --cc for multiple parents, --root for parentless
7548 $hash_parent_param =
7549 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
7552 # read commitdiff
7553 my $fd;
7554 my @difftree;
7555 if ($format eq 'html') {
7556 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7557 "--no-commit-id", "--patch-with-raw", "--full-index",
7558 $hash_parent_param, $hash, "--"
7559 or die_error(500, "Open git-diff-tree failed");
7561 while (my $line = <$fd>) {
7562 chomp $line;
7563 # empty line ends raw part of diff-tree output
7564 last unless $line;
7565 push @difftree, scalar parse_difftree_raw_line($line);
7568 } elsif ($format eq 'plain') {
7569 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7570 '-p', $hash_parent_param, $hash, "--"
7571 or die_error(500, "Open git-diff-tree failed");
7572 } elsif ($format eq 'patch') {
7573 # For commit ranges, we limit the output to the number of
7574 # patches specified in the 'patches' feature.
7575 # For single commits, we limit the output to a single patch,
7576 # diverging from the git-format-patch default.
7577 my @commit_spec = ();
7578 if ($hash_parent) {
7579 if ($patch_max > 0) {
7580 push @commit_spec, "-$patch_max";
7582 push @commit_spec, '-n', "$hash_parent..$hash";
7583 } else {
7584 if ($params{-single}) {
7585 push @commit_spec, '-1';
7586 } else {
7587 if ($patch_max > 0) {
7588 push @commit_spec, "-$patch_max";
7590 push @commit_spec, "-n";
7592 push @commit_spec, '--root', $hash;
7594 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
7595 '--encoding=utf8', '--stdout', @commit_spec
7596 or die_error(500, "Open git-format-patch failed");
7597 } else {
7598 die_error(400, "Unknown commitdiff format");
7601 # non-textual hash id's can be cached
7602 my $expires;
7603 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7604 $expires = "+1d";
7607 # write commit message
7608 if ($format eq 'html') {
7609 my $refs = git_get_references();
7610 my $ref = format_ref_marker($refs, $co{'id'});
7612 git_header_html(undef, $expires);
7613 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
7614 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
7615 print "<div class=\"title_text\">\n" .
7616 "<table class=\"object_header\">\n";
7617 git_print_authorship_rows(\%co);
7618 print "</table>".
7619 "</div>\n";
7620 print "<div class=\"page_body\">\n";
7621 if (@{$co{'comment'}} > 1) {
7622 print "<div class=\"log\">\n";
7623 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
7624 print "</div>\n"; # class="log"
7627 } elsif ($format eq 'plain') {
7628 my $refs = git_get_references("tags");
7629 my $tagname = git_get_rev_name_tags($hash);
7630 my $filename = basename($project) . "-$hash.patch";
7632 print $cgi->header(
7633 -type => 'text/plain',
7634 -charset => 'utf-8',
7635 -expires => $expires,
7636 -content_disposition => 'inline; filename="' . "$filename" . '"');
7637 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7638 print "From: " . to_utf8($co{'author'}) . "\n";
7639 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
7640 print "Subject: " . to_utf8($co{'title'}) . "\n";
7642 print "X-Git-Tag: $tagname\n" if $tagname;
7643 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7645 foreach my $line (@{$co{'comment'}}) {
7646 print to_utf8($line) . "\n";
7648 print "---\n\n";
7649 } elsif ($format eq 'patch') {
7650 my $filename = basename($project) . "-$hash.patch";
7652 print $cgi->header(
7653 -type => 'text/plain',
7654 -charset => 'utf-8',
7655 -expires => $expires,
7656 -content_disposition => 'inline; filename="' . "$filename" . '"');
7659 # write patch
7660 if ($format eq 'html') {
7661 my $use_parents = !defined $hash_parent ||
7662 $hash_parent eq '-c' || $hash_parent eq '--cc';
7663 git_difftree_body(\@difftree, $hash,
7664 $use_parents ? @{$co{'parents'}} : $hash_parent);
7665 print "<br/>\n";
7667 git_patchset_body($fd, $diff_style,
7668 \@difftree, $hash,
7669 $use_parents ? @{$co{'parents'}} : $hash_parent);
7670 close $fd;
7671 print "</div>\n"; # class="page_body"
7672 git_footer_html();
7674 } elsif ($format eq 'plain') {
7675 local $/ = undef;
7676 print <$fd>;
7677 close $fd
7678 or print "Reading git-diff-tree failed\n";
7679 } elsif ($format eq 'patch') {
7680 local $/ = undef;
7681 print <$fd>;
7682 close $fd
7683 or print "Reading git-format-patch failed\n";
7687 sub git_commitdiff_plain {
7688 git_commitdiff(-format => 'plain');
7691 # format-patch-style patches
7692 sub git_patch {
7693 git_commitdiff(-format => 'patch', -single => 1);
7696 sub git_patches {
7697 git_commitdiff(-format => 'patch');
7700 sub git_history {
7701 git_log_generic('history', \&git_history_body,
7702 $hash_base, $hash_parent_base,
7703 $file_name, $hash);
7706 sub git_search {
7707 $searchtype ||= 'commit';
7709 # check if appropriate features are enabled
7710 gitweb_check_feature('search')
7711 or die_error(403, "Search is disabled");
7712 if ($searchtype eq 'pickaxe') {
7713 # pickaxe may take all resources of your box and run for several minutes
7714 # with every query - so decide by yourself how public you make this feature
7715 gitweb_check_feature('pickaxe')
7716 or die_error(403, "Pickaxe search is disabled");
7718 if ($searchtype eq 'grep') {
7719 # grep search might be potentially CPU-intensive, too
7720 gitweb_check_feature('grep')
7721 or die_error(403, "Grep search is disabled");
7724 if (!defined $searchtext) {
7725 die_error(400, "Text field is empty");
7727 if (!defined $hash) {
7728 $hash = git_get_head_hash($project);
7730 my %co = parse_commit($hash);
7731 if (!%co) {
7732 die_error(404, "Unknown commit object");
7734 if (!defined $page) {
7735 $page = 0;
7738 if ($searchtype eq 'commit' ||
7739 $searchtype eq 'author' ||
7740 $searchtype eq 'committer') {
7741 git_search_message(%co);
7742 } elsif ($searchtype eq 'pickaxe') {
7743 git_search_changes(%co);
7744 } elsif ($searchtype eq 'grep') {
7745 git_search_files(%co);
7746 } else {
7747 die_error(400, "Unknown search type");
7751 sub git_search_help {
7752 git_header_html();
7753 git_print_page_nav('','', $hash,$hash,$hash);
7754 print <<EOT;
7755 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7756 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7757 the pattern entered is recognized as the POSIX extended
7758 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7759 insensitive).</p>
7760 <dl>
7761 <dt><b>commit</b></dt>
7762 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7764 my $have_grep = gitweb_check_feature('grep');
7765 if ($have_grep) {
7766 print <<EOT;
7767 <dt><b>grep</b></dt>
7768 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7769 a different one) are searched for the given pattern. On large trees, this search can take
7770 a while and put some strain on the server, so please use it with some consideration. Note that
7771 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7772 case-sensitive.</dd>
7775 print <<EOT;
7776 <dt><b>author</b></dt>
7777 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7778 <dt><b>committer</b></dt>
7779 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7781 my $have_pickaxe = gitweb_check_feature('pickaxe');
7782 if ($have_pickaxe) {
7783 print <<EOT;
7784 <dt><b>pickaxe</b></dt>
7785 <dd>All commits that caused the string to appear or disappear from any file (changes that
7786 added, removed or "modified" the string) will be listed. This search can take a while and
7787 takes a lot of strain on the server, so please use it wisely. Note that since you may be
7788 interested even in changes just changing the case as well, this search is case sensitive.</dd>
7791 print "</dl>\n";
7792 git_footer_html();
7795 sub git_shortlog {
7796 git_log_generic('shortlog', \&git_shortlog_body,
7797 $hash, $hash_parent);
7800 ## ......................................................................
7801 ## feeds (RSS, Atom; OPML)
7803 sub git_feed {
7804 my $format = shift || 'atom';
7805 my $have_blame = gitweb_check_feature('blame');
7807 # Atom: http://www.atomenabled.org/developers/syndication/
7808 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7809 if ($format ne 'rss' && $format ne 'atom') {
7810 die_error(400, "Unknown web feed format");
7813 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7814 my $head = $hash || 'HEAD';
7815 my @commitlist = parse_commits($head, 150, 0, $file_name);
7817 my %latest_commit;
7818 my %latest_date;
7819 my $content_type = "application/$format+xml";
7820 if (defined $cgi->http('HTTP_ACCEPT') &&
7821 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7822 # browser (feed reader) prefers text/xml
7823 $content_type = 'text/xml';
7825 if (defined($commitlist[0])) {
7826 %latest_commit = %{$commitlist[0]};
7827 my $latest_epoch = $latest_commit{'committer_epoch'};
7828 %latest_date = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
7829 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7830 if (defined $if_modified) {
7831 my $since;
7832 if (eval { require HTTP::Date; 1; }) {
7833 $since = HTTP::Date::str2time($if_modified);
7834 } elsif (eval { require Time::ParseDate; 1; }) {
7835 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7837 if (defined $since && $latest_epoch <= $since) {
7838 print $cgi->header(
7839 -type => $content_type,
7840 -charset => 'utf-8',
7841 -last_modified => $latest_date{'rfc2822'},
7842 -status => '304 Not Modified');
7843 return;
7846 print $cgi->header(
7847 -type => $content_type,
7848 -charset => 'utf-8',
7849 -last_modified => $latest_date{'rfc2822'});
7850 } else {
7851 print $cgi->header(
7852 -type => $content_type,
7853 -charset => 'utf-8');
7856 # Optimization: skip generating the body if client asks only
7857 # for Last-Modified date.
7858 return if ($cgi->request_method() eq 'HEAD');
7860 # header variables
7861 my $title = "$site_name - $project/$action";
7862 my $feed_type = 'log';
7863 if (defined $hash) {
7864 $title .= " - '$hash'";
7865 $feed_type = 'branch log';
7866 if (defined $file_name) {
7867 $title .= " :: $file_name";
7868 $feed_type = 'history';
7870 } elsif (defined $file_name) {
7871 $title .= " - $file_name";
7872 $feed_type = 'history';
7874 $title .= " $feed_type";
7875 my $descr = git_get_project_description($project);
7876 if (defined $descr) {
7877 $descr = esc_html($descr);
7878 } else {
7879 $descr = "$project " .
7880 ($format eq 'rss' ? 'RSS' : 'Atom') .
7881 " feed";
7883 my $owner = git_get_project_owner($project);
7884 $owner = esc_html($owner);
7886 #header
7887 my $alt_url;
7888 if (defined $file_name) {
7889 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7890 } elsif (defined $hash) {
7891 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7892 } else {
7893 $alt_url = href(-full=>1, action=>"summary");
7895 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7896 if ($format eq 'rss') {
7897 print <<XML;
7898 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7899 <channel>
7901 print "<title>$title</title>\n" .
7902 "<link>$alt_url</link>\n" .
7903 "<description>$descr</description>\n" .
7904 "<language>en</language>\n" .
7905 # project owner is responsible for 'editorial' content
7906 "<managingEditor>$owner</managingEditor>\n";
7907 if (defined $logo || defined $favicon) {
7908 # prefer the logo to the favicon, since RSS
7909 # doesn't allow both
7910 my $img = esc_url($logo || $favicon);
7911 print "<image>\n" .
7912 "<url>$img</url>\n" .
7913 "<title>$title</title>\n" .
7914 "<link>$alt_url</link>\n" .
7915 "</image>\n";
7917 if (%latest_date) {
7918 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7919 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7921 print "<generator>gitweb v.$version/$git_version</generator>\n";
7922 } elsif ($format eq 'atom') {
7923 print <<XML;
7924 <feed xmlns="http://www.w3.org/2005/Atom">
7926 print "<title>$title</title>\n" .
7927 "<subtitle>$descr</subtitle>\n" .
7928 '<link rel="alternate" type="text/html" href="' .
7929 $alt_url . '" />' . "\n" .
7930 '<link rel="self" type="' . $content_type . '" href="' .
7931 $cgi->self_url() . '" />' . "\n" .
7932 "<id>" . href(-full=>1) . "</id>\n" .
7933 # use project owner for feed author
7934 "<author><name>$owner</name></author>\n";
7935 if (defined $favicon) {
7936 print "<icon>" . esc_url($favicon) . "</icon>\n";
7938 if (defined $logo) {
7939 # not twice as wide as tall: 72 x 27 pixels
7940 print "<logo>" . esc_url($logo) . "</logo>\n";
7942 if (! %latest_date) {
7943 # dummy date to keep the feed valid until commits trickle in:
7944 print "<updated>1970-01-01T00:00:00Z</updated>\n";
7945 } else {
7946 print "<updated>$latest_date{'iso-8601'}</updated>\n";
7948 print "<generator version='$version/$git_version'>gitweb</generator>\n";
7951 # contents
7952 for (my $i = 0; $i <= $#commitlist; $i++) {
7953 my %co = %{$commitlist[$i]};
7954 my $commit = $co{'id'};
7955 # we read 150, we always show 30 and the ones more recent than 48 hours
7956 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
7957 last;
7959 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
7961 # get list of changed files
7962 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7963 $co{'parent'} || "--root",
7964 $co{'id'}, "--", (defined $file_name ? $file_name : ())
7965 or next;
7966 my @difftree = map { chomp; $_ } <$fd>;
7967 close $fd
7968 or next;
7970 # print element (entry, item)
7971 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7972 if ($format eq 'rss') {
7973 print "<item>\n" .
7974 "<title>" . esc_html($co{'title'}) . "</title>\n" .
7975 "<author>" . esc_html($co{'author'}) . "</author>\n" .
7976 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7977 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7978 "<link>$co_url</link>\n" .
7979 "<description>" . esc_html($co{'title'}) . "</description>\n" .
7980 "<content:encoded>" .
7981 "<![CDATA[\n";
7982 } elsif ($format eq 'atom') {
7983 print "<entry>\n" .
7984 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7985 "<updated>$cd{'iso-8601'}</updated>\n" .
7986 "<author>\n" .
7987 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
7988 if ($co{'author_email'}) {
7989 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
7991 print "</author>\n" .
7992 # use committer for contributor
7993 "<contributor>\n" .
7994 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7995 if ($co{'committer_email'}) {
7996 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7998 print "</contributor>\n" .
7999 "<published>$cd{'iso-8601'}</published>\n" .
8000 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
8001 "<id>$co_url</id>\n" .
8002 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
8003 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
8005 my $comment = $co{'comment'};
8006 print "<pre>\n";
8007 foreach my $line (@$comment) {
8008 $line = esc_html($line);
8009 print "$line\n";
8011 print "</pre><ul>\n";
8012 foreach my $difftree_line (@difftree) {
8013 my %difftree = parse_difftree_raw_line($difftree_line);
8014 next if !$difftree{'from_id'};
8016 my $file = $difftree{'file'} || $difftree{'to_file'};
8018 print "<li>" .
8019 "[" .
8020 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
8021 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
8022 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
8023 file_name=>$file, file_parent=>$difftree{'from_file'}),
8024 -title => "diff"}, 'D');
8025 if ($have_blame) {
8026 print $cgi->a({-href => href(-full=>1, action=>"blame",
8027 file_name=>$file, hash_base=>$commit),
8028 -title => "blame"}, 'B');
8030 # if this is not a feed of a file history
8031 if (!defined $file_name || $file_name ne $file) {
8032 print $cgi->a({-href => href(-full=>1, action=>"history",
8033 file_name=>$file, hash=>$commit),
8034 -title => "history"}, 'H');
8036 $file = esc_path($file);
8037 print "] ".
8038 "$file</li>\n";
8040 if ($format eq 'rss') {
8041 print "</ul>]]>\n" .
8042 "</content:encoded>\n" .
8043 "</item>\n";
8044 } elsif ($format eq 'atom') {
8045 print "</ul>\n</div>\n" .
8046 "</content>\n" .
8047 "</entry>\n";
8051 # end of feed
8052 if ($format eq 'rss') {
8053 print "</channel>\n</rss>\n";
8054 } elsif ($format eq 'atom') {
8055 print "</feed>\n";
8059 sub git_rss {
8060 git_feed('rss');
8063 sub git_atom {
8064 git_feed('atom');
8067 sub git_opml {
8068 my @list = git_get_projects_list($project_filter, $strict_export);
8069 if (!@list) {
8070 die_error(404, "No projects found");
8073 print $cgi->header(
8074 -type => 'text/xml',
8075 -charset => 'utf-8',
8076 -content_disposition => 'inline; filename="opml.xml"');
8078 my $title = esc_html($site_name);
8079 my $filter = " within subdirectory ";
8080 if (defined $project_filter) {
8081 $filter .= esc_html($project_filter);
8082 } else {
8083 $filter = "";
8085 print <<XML;
8086 <?xml version="1.0" encoding="utf-8"?>
8087 <opml version="1.0">
8088 <head>
8089 <title>$title OPML Export$filter</title>
8090 </head>
8091 <body>
8092 <outline text="git RSS feeds">
8095 foreach my $pr (@list) {
8096 my %proj = %$pr;
8097 my $head = git_get_head_hash($proj{'path'});
8098 if (!defined $head) {
8099 next;
8101 $git_dir = "$projectroot/$proj{'path'}";
8102 my %co = parse_commit($head);
8103 if (!%co) {
8104 next;
8107 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
8108 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
8109 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
8110 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
8112 print <<XML;
8113 </outline>
8114 </body>
8115 </opml>