Merge branch 'mk/gitweb-diff-hl'
[git/jnareb-git.git] / gitweb / gitweb.perl
blob49a2ec6c0fa316f96cebbdf1383ea7df3bceea2c
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 if ($my_url =~ s,\Q$path_info\E$,, &&
58 $my_uri =~ s,\Q$path_info\E$,, &&
59 defined $ENV{'SCRIPT_NAME'}) {
60 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
64 # target of the home link on top of all pages
65 our $home_link = $my_uri || "/";
68 # core git executable to use
69 # this can just be "git" if your webserver has a sensible PATH
70 our $GIT = "++GIT_BINDIR++/git";
72 # absolute fs-path which will be prepended to the project path
73 #our $projectroot = "/pub/scm";
74 our $projectroot = "++GITWEB_PROJECTROOT++";
76 # fs traversing limit for getting project list
77 # the number is relative to the projectroot
78 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
80 # string of the home link on top of all pages
81 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
83 # name of your site or organization to appear in page titles
84 # replace this with something more descriptive for clearer bookmarks
85 our $site_name = "++GITWEB_SITENAME++"
86 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
88 # html snippet to include in the <head> section of each page
89 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
90 # filename of html text to include at top of each page
91 our $site_header = "++GITWEB_SITE_HEADER++";
92 # html text to include at home page
93 our $home_text = "++GITWEB_HOMETEXT++";
94 # filename of html text to include at bottom of each page
95 our $site_footer = "++GITWEB_SITE_FOOTER++";
97 # URI of stylesheets
98 our @stylesheets = ("++GITWEB_CSS++");
99 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
100 our $stylesheet = undef;
101 # URI of GIT logo (72x27 size)
102 our $logo = "++GITWEB_LOGO++";
103 # URI of GIT favicon, assumed to be image/png type
104 our $favicon = "++GITWEB_FAVICON++";
105 # URI of gitweb.js (JavaScript code for gitweb)
106 our $javascript = "++GITWEB_JS++";
108 # URI and label (title) of GIT logo link
109 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
110 #our $logo_label = "git documentation";
111 our $logo_url = "http://git-scm.com/";
112 our $logo_label = "git homepage";
114 # source of projects list
115 our $projects_list = "++GITWEB_LIST++";
117 # the width (in characters) of the projects list "Description" column
118 our $projects_list_description_width = 25;
120 # group projects by category on the projects list
121 # (enabled if this variable evaluates to true)
122 our $projects_list_group_categories = 0;
124 # default category if none specified
125 # (leave the empty string for no category)
126 our $project_list_default_category = "";
128 # default order of projects list
129 # valid values are none, project, descr, owner, and age
130 our $default_projects_order = "project";
132 # show repository only if this file exists
133 # (only effective if this variable evaluates to true)
134 our $export_ok = "++GITWEB_EXPORT_OK++";
136 # show repository only if this subroutine returns true
137 # when given the path to the project, for example:
138 # sub { return -e "$_[0]/git-daemon-export-ok"; }
139 our $export_auth_hook = undef;
141 # only allow viewing of repositories also shown on the overview page
142 our $strict_export = "++GITWEB_STRICT_EXPORT++";
144 # list of git base URLs used for URL to where fetch project from,
145 # i.e. full URL is "$git_base_url/$project"
146 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
148 # default blob_plain mimetype and default charset for text/plain blob
149 our $default_blob_plain_mimetype = 'text/plain';
150 our $default_text_plain_charset = undef;
152 # file to use for guessing MIME types before trying /etc/mime.types
153 # (relative to the current git repository)
154 our $mimetypes_file = undef;
156 # assume this charset if line contains non-UTF-8 characters;
157 # it should be valid encoding (see Encoding::Supported(3pm) for list),
158 # for which encoding all byte sequences are valid, for example
159 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
160 # could be even 'utf-8' for the old behavior)
161 our $fallback_encoding = 'latin1';
163 # rename detection options for git-diff and git-diff-tree
164 # - default is '-M', with the cost proportional to
165 # (number of removed files) * (number of new files).
166 # - more costly is '-C' (which implies '-M'), with the cost proportional to
167 # (number of changed files + number of removed files) * (number of new files)
168 # - even more costly is '-C', '--find-copies-harder' with cost
169 # (number of files in the original tree) * (number of new files)
170 # - one might want to include '-B' option, e.g. '-B', '-M'
171 our @diff_opts = ('-M'); # taken from git_commit
173 # Disables features that would allow repository owners to inject script into
174 # the gitweb domain.
175 our $prevent_xss = 0;
177 # Path to the highlight executable to use (must be the one from
178 # http://www.andre-simon.de due to assumptions about parameters and output).
179 # Useful if highlight is not installed on your webserver's PATH.
180 # [Default: highlight]
181 our $highlight_bin = "++HIGHLIGHT_BIN++";
183 # information about snapshot formats that gitweb is capable of serving
184 our %known_snapshot_formats = (
185 # name => {
186 # 'display' => display name,
187 # 'type' => mime type,
188 # 'suffix' => filename suffix,
189 # 'format' => --format for git-archive,
190 # 'compressor' => [compressor command and arguments]
191 # (array reference, optional)
192 # 'disabled' => boolean (optional)}
194 'tgz' => {
195 'display' => 'tar.gz',
196 'type' => 'application/x-gzip',
197 'suffix' => '.tar.gz',
198 'format' => 'tar',
199 'compressor' => ['gzip', '-n']},
201 'tbz2' => {
202 'display' => 'tar.bz2',
203 'type' => 'application/x-bzip2',
204 'suffix' => '.tar.bz2',
205 'format' => 'tar',
206 'compressor' => ['bzip2']},
208 'txz' => {
209 'display' => 'tar.xz',
210 'type' => 'application/x-xz',
211 'suffix' => '.tar.xz',
212 'format' => 'tar',
213 'compressor' => ['xz'],
214 'disabled' => 1},
216 'zip' => {
217 'display' => 'zip',
218 'type' => 'application/x-zip',
219 'suffix' => '.zip',
220 'format' => 'zip'},
223 # Aliases so we understand old gitweb.snapshot values in repository
224 # configuration.
225 our %known_snapshot_format_aliases = (
226 'gzip' => 'tgz',
227 'bzip2' => 'tbz2',
228 'xz' => 'txz',
230 # backward compatibility: legacy gitweb config support
231 'x-gzip' => undef, 'gz' => undef,
232 'x-bzip2' => undef, 'bz2' => undef,
233 'x-zip' => undef, '' => undef,
236 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
237 # are changed, it may be appropriate to change these values too via
238 # $GITWEB_CONFIG.
239 our %avatar_size = (
240 'default' => 16,
241 'double' => 32
244 # Used to set the maximum load that we will still respond to gitweb queries.
245 # If server load exceed this value then return "503 server busy" error.
246 # If gitweb cannot determined server load, it is taken to be 0.
247 # Leave it undefined (or set to 'undef') to turn off load checking.
248 our $maxload = 300;
250 # configuration for 'highlight' (http://www.andre-simon.de/)
251 # match by basename
252 our %highlight_basename = (
253 #'Program' => 'py',
254 #'Library' => 'py',
255 'SConstruct' => 'py', # SCons equivalent of Makefile
256 'Makefile' => 'make',
258 # match by extension
259 our %highlight_ext = (
260 # main extensions, defining name of syntax;
261 # see files in /usr/share/highlight/langDefs/ directory
262 map { $_ => $_ }
263 qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
264 # alternate extensions, see /etc/highlight/filetypes.conf
265 'h' => 'c',
266 map { $_ => 'sh' } qw(bash zsh ksh),
267 map { $_ => 'cpp' } qw(cxx c++ cc),
268 map { $_ => 'php' } qw(php3 php4 php5 phps),
269 map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi'
270 map { $_ => 'make'} qw(mak mk),
271 map { $_ => 'xml' } qw(xhtml html htm),
274 # You define site-wide feature defaults here; override them with
275 # $GITWEB_CONFIG as necessary.
276 our %feature = (
277 # feature => {
278 # 'sub' => feature-sub (subroutine),
279 # 'override' => allow-override (boolean),
280 # 'default' => [ default options...] (array reference)}
282 # if feature is overridable (it means that allow-override has true value),
283 # then feature-sub will be called with default options as parameters;
284 # return value of feature-sub indicates if to enable specified feature
286 # if there is no 'sub' key (no feature-sub), then feature cannot be
287 # overridden
289 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
290 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
291 # is enabled
293 # Enable the 'blame' blob view, showing the last commit that modified
294 # each line in the file. This can be very CPU-intensive.
296 # To enable system wide have in $GITWEB_CONFIG
297 # $feature{'blame'}{'default'} = [1];
298 # To have project specific config enable override in $GITWEB_CONFIG
299 # $feature{'blame'}{'override'} = 1;
300 # and in project config gitweb.blame = 0|1;
301 'blame' => {
302 'sub' => sub { feature_bool('blame', @_) },
303 'override' => 0,
304 'default' => [0]},
306 # Enable the 'snapshot' link, providing a compressed archive of any
307 # tree. This can potentially generate high traffic if you have large
308 # project.
310 # Value is a list of formats defined in %known_snapshot_formats that
311 # you wish to offer.
312 # To disable system wide have in $GITWEB_CONFIG
313 # $feature{'snapshot'}{'default'} = [];
314 # To have project specific config enable override in $GITWEB_CONFIG
315 # $feature{'snapshot'}{'override'} = 1;
316 # and in project config, a comma-separated list of formats or "none"
317 # to disable. Example: gitweb.snapshot = tbz2,zip;
318 'snapshot' => {
319 'sub' => \&feature_snapshot,
320 'override' => 0,
321 'default' => ['tgz']},
323 # Enable text search, which will list the commits which match author,
324 # committer or commit text to a given string. Enabled by default.
325 # Project specific override is not supported.
327 # Note that this controls all search features, which means that if
328 # it is disabled, then 'grep' and 'pickaxe' search would also be
329 # disabled.
330 'search' => {
331 'override' => 0,
332 'default' => [1]},
334 # Enable grep search, which will list the files in currently selected
335 # tree containing the given string. Enabled by default. This can be
336 # potentially CPU-intensive, of course.
337 # Note that you need to have 'search' feature enabled too.
339 # To enable system wide have in $GITWEB_CONFIG
340 # $feature{'grep'}{'default'} = [1];
341 # To have project specific config enable override in $GITWEB_CONFIG
342 # $feature{'grep'}{'override'} = 1;
343 # and in project config gitweb.grep = 0|1;
344 'grep' => {
345 'sub' => sub { feature_bool('grep', @_) },
346 'override' => 0,
347 'default' => [1]},
349 # Enable the pickaxe search, which will list the commits that modified
350 # a given string in a file. This can be practical and quite faster
351 # alternative to 'blame', but still potentially CPU-intensive.
352 # Note that you need to have 'search' feature enabled too.
354 # To enable system wide have in $GITWEB_CONFIG
355 # $feature{'pickaxe'}{'default'} = [1];
356 # To have project specific config enable override in $GITWEB_CONFIG
357 # $feature{'pickaxe'}{'override'} = 1;
358 # and in project config gitweb.pickaxe = 0|1;
359 'pickaxe' => {
360 'sub' => sub { feature_bool('pickaxe', @_) },
361 'override' => 0,
362 'default' => [1]},
364 # Enable showing size of blobs in a 'tree' view, in a separate
365 # column, similar to what 'ls -l' does. This cost a bit of IO.
367 # To disable system wide have in $GITWEB_CONFIG
368 # $feature{'show-sizes'}{'default'} = [0];
369 # To have project specific config enable override in $GITWEB_CONFIG
370 # $feature{'show-sizes'}{'override'} = 1;
371 # and in project config gitweb.showsizes = 0|1;
372 'show-sizes' => {
373 'sub' => sub { feature_bool('showsizes', @_) },
374 'override' => 0,
375 'default' => [1]},
377 # Make gitweb use an alternative format of the URLs which can be
378 # more readable and natural-looking: project name is embedded
379 # directly in the path and the query string contains other
380 # auxiliary information. All gitweb installations recognize
381 # URL in either format; this configures in which formats gitweb
382 # generates links.
384 # To enable system wide have in $GITWEB_CONFIG
385 # $feature{'pathinfo'}{'default'} = [1];
386 # Project specific override is not supported.
388 # Note that you will need to change the default location of CSS,
389 # favicon, logo and possibly other files to an absolute URL. Also,
390 # if gitweb.cgi serves as your indexfile, you will need to force
391 # $my_uri to contain the script name in your $GITWEB_CONFIG.
392 'pathinfo' => {
393 'override' => 0,
394 'default' => [0]},
396 # Make gitweb consider projects in project root subdirectories
397 # to be forks of existing projects. Given project $projname.git,
398 # projects matching $projname/*.git will not be shown in the main
399 # projects list, instead a '+' mark will be added to $projname
400 # there and a 'forks' view will be enabled for the project, listing
401 # all the forks. If project list is taken from a file, forks have
402 # to be listed after the main project.
404 # To enable system wide have in $GITWEB_CONFIG
405 # $feature{'forks'}{'default'} = [1];
406 # Project specific override is not supported.
407 'forks' => {
408 'override' => 0,
409 'default' => [0]},
411 # Insert custom links to the action bar of all project pages.
412 # This enables you mainly to link to third-party scripts integrating
413 # into gitweb; e.g. git-browser for graphical history representation
414 # or custom web-based repository administration interface.
416 # The 'default' value consists of a list of triplets in the form
417 # (label, link, position) where position is the label after which
418 # to insert the link and link is a format string where %n expands
419 # to the project name, %f to the project path within the filesystem,
420 # %h to the current hash (h gitweb parameter) and %b to the current
421 # hash base (hb gitweb parameter); %% expands to %.
423 # To enable system wide have in $GITWEB_CONFIG e.g.
424 # $feature{'actions'}{'default'} = [('graphiclog',
425 # '/git-browser/by-commit.html?r=%n', 'summary')];
426 # Project specific override is not supported.
427 'actions' => {
428 'override' => 0,
429 'default' => []},
431 # Allow gitweb scan project content tags of project repository,
432 # and display the popular Web 2.0-ish "tag cloud" near the projects
433 # list. Note that this is something COMPLETELY different from the
434 # normal Git tags.
436 # gitweb by itself can show existing tags, but it does not handle
437 # tagging itself; you need to do it externally, outside gitweb.
438 # The format is described in git_get_project_ctags() subroutine.
439 # You may want to install the HTML::TagCloud Perl module to get
440 # a pretty tag cloud instead of just a list of tags.
442 # To enable system wide have in $GITWEB_CONFIG
443 # $feature{'ctags'}{'default'} = [1];
444 # Project specific override is not supported.
446 # In the future whether ctags editing is enabled might depend
447 # on the value, but using 1 should always mean no editing of ctags.
448 'ctags' => {
449 'override' => 0,
450 'default' => [0]},
452 # The maximum number of patches in a patchset generated in patch
453 # view. Set this to 0 or undef to disable patch view, or to a
454 # negative number to remove any limit.
456 # To disable system wide have in $GITWEB_CONFIG
457 # $feature{'patches'}{'default'} = [0];
458 # To have project specific config enable override in $GITWEB_CONFIG
459 # $feature{'patches'}{'override'} = 1;
460 # and in project config gitweb.patches = 0|n;
461 # where n is the maximum number of patches allowed in a patchset.
462 'patches' => {
463 'sub' => \&feature_patches,
464 'override' => 0,
465 'default' => [16]},
467 # Avatar support. When this feature is enabled, views such as
468 # shortlog or commit will display an avatar associated with
469 # the email of the committer(s) and/or author(s).
471 # Currently available providers are gravatar and picon.
472 # If an unknown provider is specified, the feature is disabled.
474 # Gravatar depends on Digest::MD5.
475 # Picon currently relies on the indiana.edu database.
477 # To enable system wide have in $GITWEB_CONFIG
478 # $feature{'avatar'}{'default'} = ['<provider>'];
479 # where <provider> is either gravatar or picon.
480 # To have project specific config enable override in $GITWEB_CONFIG
481 # $feature{'avatar'}{'override'} = 1;
482 # and in project config gitweb.avatar = <provider>;
483 'avatar' => {
484 'sub' => \&feature_avatar,
485 'override' => 0,
486 'default' => ['']},
488 # Enable displaying how much time and how many git commands
489 # it took to generate and display page. Disabled by default.
490 # Project specific override is not supported.
491 'timed' => {
492 'override' => 0,
493 'default' => [0]},
495 # Enable turning some links into links to actions which require
496 # JavaScript to run (like 'blame_incremental'). Not enabled by
497 # default. Project specific override is currently not supported.
498 'javascript-actions' => {
499 'override' => 0,
500 'default' => [0]},
502 # Enable and configure ability to change common timezone for dates
503 # in gitweb output via JavaScript. Enabled by default.
504 # Project specific override is not supported.
505 'javascript-timezone' => {
506 'override' => 0,
507 'default' => [
508 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
509 # or undef to turn off this feature
510 'gitweb_tz', # name of cookie where to store selected timezone
511 'datetime', # CSS class used to mark up dates for manipulation
514 # Syntax highlighting support. This is based on Daniel Svensson's
515 # and Sham Chukoury's work in gitweb-xmms2.git.
516 # It requires the 'highlight' program present in $PATH,
517 # and therefore is disabled by default.
519 # To enable system wide have in $GITWEB_CONFIG
520 # $feature{'highlight'}{'default'} = [1];
522 'highlight' => {
523 'sub' => sub { feature_bool('highlight', @_) },
524 'override' => 0,
525 'default' => [0]},
527 # Enable displaying of remote heads in the heads list
529 # To enable system wide have in $GITWEB_CONFIG
530 # $feature{'remote_heads'}{'default'} = [1];
531 # To have project specific config enable override in $GITWEB_CONFIG
532 # $feature{'remote_heads'}{'override'} = 1;
533 # and in project config gitweb.remote_heads = 0|1;
534 'remote_heads' => {
535 'sub' => sub { feature_bool('remote_heads', @_) },
536 'override' => 0,
537 'default' => [0]},
540 sub gitweb_get_feature {
541 my ($name) = @_;
542 return unless exists $feature{$name};
543 my ($sub, $override, @defaults) = (
544 $feature{$name}{'sub'},
545 $feature{$name}{'override'},
546 @{$feature{$name}{'default'}});
547 # project specific override is possible only if we have project
548 our $git_dir; # global variable, declared later
549 if (!$override || !defined $git_dir) {
550 return @defaults;
552 if (!defined $sub) {
553 warn "feature $name is not overridable";
554 return @defaults;
556 return $sub->(@defaults);
559 # A wrapper to check if a given feature is enabled.
560 # With this, you can say
562 # my $bool_feat = gitweb_check_feature('bool_feat');
563 # gitweb_check_feature('bool_feat') or somecode;
565 # instead of
567 # my ($bool_feat) = gitweb_get_feature('bool_feat');
568 # (gitweb_get_feature('bool_feat'))[0] or somecode;
570 sub gitweb_check_feature {
571 return (gitweb_get_feature(@_))[0];
575 sub feature_bool {
576 my $key = shift;
577 my ($val) = git_get_project_config($key, '--bool');
579 if (!defined $val) {
580 return ($_[0]);
581 } elsif ($val eq 'true') {
582 return (1);
583 } elsif ($val eq 'false') {
584 return (0);
588 sub feature_snapshot {
589 my (@fmts) = @_;
591 my ($val) = git_get_project_config('snapshot');
593 if ($val) {
594 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
597 return @fmts;
600 sub feature_patches {
601 my @val = (git_get_project_config('patches', '--int'));
603 if (@val) {
604 return @val;
607 return ($_[0]);
610 sub feature_avatar {
611 my @val = (git_get_project_config('avatar'));
613 return @val ? @val : @_;
616 # checking HEAD file with -e is fragile if the repository was
617 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
618 # and then pruned.
619 sub check_head_link {
620 my ($dir) = @_;
621 my $headfile = "$dir/HEAD";
622 return ((-e $headfile) ||
623 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
626 sub check_export_ok {
627 my ($dir) = @_;
628 return (check_head_link($dir) &&
629 (!$export_ok || -e "$dir/$export_ok") &&
630 (!$export_auth_hook || $export_auth_hook->($dir)));
633 # process alternate names for backward compatibility
634 # filter out unsupported (unknown) snapshot formats
635 sub filter_snapshot_fmts {
636 my @fmts = @_;
638 @fmts = map {
639 exists $known_snapshot_format_aliases{$_} ?
640 $known_snapshot_format_aliases{$_} : $_} @fmts;
641 @fmts = grep {
642 exists $known_snapshot_formats{$_} &&
643 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
646 # If it is set to code reference, it is code that it is to be run once per
647 # request, allowing updating configurations that change with each request,
648 # while running other code in config file only once.
650 # Otherwise, if it is false then gitweb would process config file only once;
651 # if it is true then gitweb config would be run for each request.
652 our $per_request_config = 1;
654 # read and parse gitweb config file given by its parameter.
655 # returns true on success, false on recoverable error, allowing
656 # to chain this subroutine, using first file that exists.
657 # dies on errors during parsing config file, as it is unrecoverable.
658 sub read_config_file {
659 my $filename = shift;
660 return unless defined $filename;
661 # die if there are errors parsing config file
662 if (-e $filename) {
663 do $filename;
664 die $@ if $@;
665 return 1;
667 return;
670 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
671 sub evaluate_gitweb_config {
672 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
673 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
674 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
676 # Protect agains duplications of file names, to not read config twice.
677 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
678 # there possibility of duplication of filename there doesn't matter.
679 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
680 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
682 # Common system-wide settings for convenience.
683 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
684 read_config_file($GITWEB_CONFIG_COMMON);
686 # Use first config file that exists. This means use the per-instance
687 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
688 read_config_file($GITWEB_CONFIG) and return;
689 read_config_file($GITWEB_CONFIG_SYSTEM);
692 # Get loadavg of system, to compare against $maxload.
693 # Currently it requires '/proc/loadavg' present to get loadavg;
694 # if it is not present it returns 0, which means no load checking.
695 sub get_loadavg {
696 if( -e '/proc/loadavg' ){
697 open my $fd, '<', '/proc/loadavg'
698 or return 0;
699 my @load = split(/\s+/, scalar <$fd>);
700 close $fd;
702 # The first three columns measure CPU and IO utilization of the last one,
703 # five, and 10 minute periods. The fourth column shows the number of
704 # currently running processes and the total number of processes in the m/n
705 # format. The last column displays the last process ID used.
706 return $load[0] || 0;
708 # additional checks for load average should go here for things that don't export
709 # /proc/loadavg
711 return 0;
714 # version of the core git binary
715 our $git_version;
716 sub evaluate_git_version {
717 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
718 $number_of_git_cmds++;
721 sub check_loadavg {
722 if (defined $maxload && get_loadavg() > $maxload) {
723 die_error(503, "The load average on the server is too high");
727 # ======================================================================
728 # input validation and dispatch
730 # input parameters can be collected from a variety of sources (presently, CGI
731 # and PATH_INFO), so we define an %input_params hash that collects them all
732 # together during validation: this allows subsequent uses (e.g. href()) to be
733 # agnostic of the parameter origin
735 our %input_params = ();
737 # input parameters are stored with the long parameter name as key. This will
738 # also be used in the href subroutine to convert parameters to their CGI
739 # equivalent, and since the href() usage is the most frequent one, we store
740 # the name -> CGI key mapping here, instead of the reverse.
742 # XXX: Warning: If you touch this, check the search form for updating,
743 # too.
745 our @cgi_param_mapping = (
746 project => "p",
747 action => "a",
748 file_name => "f",
749 file_parent => "fp",
750 hash => "h",
751 hash_parent => "hp",
752 hash_base => "hb",
753 hash_parent_base => "hpb",
754 page => "pg",
755 order => "o",
756 searchtext => "s",
757 searchtype => "st",
758 snapshot_format => "sf",
759 extra_options => "opt",
760 search_use_regexp => "sr",
761 ctag => "by_tag",
762 diff_style => "ds",
763 project_filter => "pf",
764 # this must be last entry (for manipulation from JavaScript)
765 javascript => "js"
767 our %cgi_param_mapping = @cgi_param_mapping;
769 # we will also need to know the possible actions, for validation
770 our %actions = (
771 "blame" => \&git_blame,
772 "blame_incremental" => \&git_blame_incremental,
773 "blame_data" => \&git_blame_data,
774 "blobdiff" => \&git_blobdiff,
775 "blobdiff_plain" => \&git_blobdiff_plain,
776 "blob" => \&git_blob,
777 "blob_plain" => \&git_blob_plain,
778 "commitdiff" => \&git_commitdiff,
779 "commitdiff_plain" => \&git_commitdiff_plain,
780 "commit" => \&git_commit,
781 "forks" => \&git_forks,
782 "heads" => \&git_heads,
783 "history" => \&git_history,
784 "log" => \&git_log,
785 "patch" => \&git_patch,
786 "patches" => \&git_patches,
787 "remotes" => \&git_remotes,
788 "rss" => \&git_rss,
789 "atom" => \&git_atom,
790 "search" => \&git_search,
791 "search_help" => \&git_search_help,
792 "shortlog" => \&git_shortlog,
793 "summary" => \&git_summary,
794 "tag" => \&git_tag,
795 "tags" => \&git_tags,
796 "tree" => \&git_tree,
797 "snapshot" => \&git_snapshot,
798 "object" => \&git_object,
799 # those below don't need $project
800 "opml" => \&git_opml,
801 "project_list" => \&git_project_list,
802 "project_index" => \&git_project_index,
805 # finally, we have the hash of allowed extra_options for the commands that
806 # allow them
807 our %allowed_options = (
808 "--no-merges" => [ qw(rss atom log shortlog history) ],
811 # fill %input_params with the CGI parameters. All values except for 'opt'
812 # should be single values, but opt can be an array. We should probably
813 # build an array of parameters that can be multi-valued, but since for the time
814 # being it's only this one, we just single it out
815 sub evaluate_query_params {
816 our $cgi;
818 while (my ($name, $symbol) = each %cgi_param_mapping) {
819 if ($symbol eq 'opt') {
820 $input_params{$name} = [ map { decode_utf8($_) } $cgi->param($symbol) ];
821 } else {
822 $input_params{$name} = decode_utf8($cgi->param($symbol));
827 # now read PATH_INFO and update the parameter list for missing parameters
828 sub evaluate_path_info {
829 return if defined $input_params{'project'};
830 return if !$path_info;
831 $path_info =~ s,^/+,,;
832 return if !$path_info;
834 # find which part of PATH_INFO is project
835 my $project = $path_info;
836 $project =~ s,/+$,,;
837 while ($project && !check_head_link("$projectroot/$project")) {
838 $project =~ s,/*[^/]*$,,;
840 return unless $project;
841 $input_params{'project'} = $project;
843 # do not change any parameters if an action is given using the query string
844 return if $input_params{'action'};
845 $path_info =~ s,^\Q$project\E/*,,;
847 # next, check if we have an action
848 my $action = $path_info;
849 $action =~ s,/.*$,,;
850 if (exists $actions{$action}) {
851 $path_info =~ s,^$action/*,,;
852 $input_params{'action'} = $action;
855 # list of actions that want hash_base instead of hash, but can have no
856 # pathname (f) parameter
857 my @wants_base = (
858 'tree',
859 'history',
862 # we want to catch, among others
863 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
864 my ($parentrefname, $parentpathname, $refname, $pathname) =
865 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
867 # first, analyze the 'current' part
868 if (defined $pathname) {
869 # we got "branch:filename" or "branch:dir/"
870 # we could use git_get_type(branch:pathname), but:
871 # - it needs $git_dir
872 # - it does a git() call
873 # - the convention of terminating directories with a slash
874 # makes it superfluous
875 # - embedding the action in the PATH_INFO would make it even
876 # more superfluous
877 $pathname =~ s,^/+,,;
878 if (!$pathname || substr($pathname, -1) eq "/") {
879 $input_params{'action'} ||= "tree";
880 $pathname =~ s,/$,,;
881 } else {
882 # the default action depends on whether we had parent info
883 # or not
884 if ($parentrefname) {
885 $input_params{'action'} ||= "blobdiff_plain";
886 } else {
887 $input_params{'action'} ||= "blob_plain";
890 $input_params{'hash_base'} ||= $refname;
891 $input_params{'file_name'} ||= $pathname;
892 } elsif (defined $refname) {
893 # we got "branch". In this case we have to choose if we have to
894 # set hash or hash_base.
896 # Most of the actions without a pathname only want hash to be
897 # set, except for the ones specified in @wants_base that want
898 # hash_base instead. It should also be noted that hand-crafted
899 # links having 'history' as an action and no pathname or hash
900 # set will fail, but that happens regardless of PATH_INFO.
901 if (defined $parentrefname) {
902 # if there is parent let the default be 'shortlog' action
903 # (for http://git.example.com/repo.git/A..B links); if there
904 # is no parent, dispatch will detect type of object and set
905 # action appropriately if required (if action is not set)
906 $input_params{'action'} ||= "shortlog";
908 if ($input_params{'action'} &&
909 grep { $_ eq $input_params{'action'} } @wants_base) {
910 $input_params{'hash_base'} ||= $refname;
911 } else {
912 $input_params{'hash'} ||= $refname;
916 # next, handle the 'parent' part, if present
917 if (defined $parentrefname) {
918 # a missing pathspec defaults to the 'current' filename, allowing e.g.
919 # someproject/blobdiff/oldrev..newrev:/filename
920 if ($parentpathname) {
921 $parentpathname =~ s,^/+,,;
922 $parentpathname =~ s,/$,,;
923 $input_params{'file_parent'} ||= $parentpathname;
924 } else {
925 $input_params{'file_parent'} ||= $input_params{'file_name'};
927 # we assume that hash_parent_base is wanted if a path was specified,
928 # or if the action wants hash_base instead of hash
929 if (defined $input_params{'file_parent'} ||
930 grep { $_ eq $input_params{'action'} } @wants_base) {
931 $input_params{'hash_parent_base'} ||= $parentrefname;
932 } else {
933 $input_params{'hash_parent'} ||= $parentrefname;
937 # for the snapshot action, we allow URLs in the form
938 # $project/snapshot/$hash.ext
939 # where .ext determines the snapshot and gets removed from the
940 # passed $refname to provide the $hash.
942 # To be able to tell that $refname includes the format extension, we
943 # require the following two conditions to be satisfied:
944 # - the hash input parameter MUST have been set from the $refname part
945 # of the URL (i.e. they must be equal)
946 # - the snapshot format MUST NOT have been defined already (e.g. from
947 # CGI parameter sf)
948 # It's also useless to try any matching unless $refname has a dot,
949 # so we check for that too
950 if (defined $input_params{'action'} &&
951 $input_params{'action'} eq 'snapshot' &&
952 defined $refname && index($refname, '.') != -1 &&
953 $refname eq $input_params{'hash'} &&
954 !defined $input_params{'snapshot_format'}) {
955 # We loop over the known snapshot formats, checking for
956 # extensions. Allowed extensions are both the defined suffix
957 # (which includes the initial dot already) and the snapshot
958 # format key itself, with a prepended dot
959 while (my ($fmt, $opt) = each %known_snapshot_formats) {
960 my $hash = $refname;
961 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
962 next;
964 my $sfx = $1;
965 # a valid suffix was found, so set the snapshot format
966 # and reset the hash parameter
967 $input_params{'snapshot_format'} = $fmt;
968 $input_params{'hash'} = $hash;
969 # we also set the format suffix to the one requested
970 # in the URL: this way a request for e.g. .tgz returns
971 # a .tgz instead of a .tar.gz
972 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
973 last;
978 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
979 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
980 $searchtext, $search_regexp, $project_filter);
981 sub evaluate_and_validate_params {
982 our $action = $input_params{'action'};
983 if (defined $action) {
984 if (!validate_action($action)) {
985 die_error(400, "Invalid action parameter");
989 # parameters which are pathnames
990 our $project = $input_params{'project'};
991 if (defined $project) {
992 if (!validate_project($project)) {
993 undef $project;
994 die_error(404, "No such project");
998 our $project_filter = $input_params{'project_filter'};
999 if (defined $project_filter) {
1000 if (!validate_pathname($project_filter)) {
1001 die_error(404, "Invalid project_filter parameter");
1005 our $file_name = $input_params{'file_name'};
1006 if (defined $file_name) {
1007 if (!validate_pathname($file_name)) {
1008 die_error(400, "Invalid file parameter");
1012 our $file_parent = $input_params{'file_parent'};
1013 if (defined $file_parent) {
1014 if (!validate_pathname($file_parent)) {
1015 die_error(400, "Invalid file parent parameter");
1019 # parameters which are refnames
1020 our $hash = $input_params{'hash'};
1021 if (defined $hash) {
1022 if (!validate_refname($hash)) {
1023 die_error(400, "Invalid hash parameter");
1027 our $hash_parent = $input_params{'hash_parent'};
1028 if (defined $hash_parent) {
1029 if (!validate_refname($hash_parent)) {
1030 die_error(400, "Invalid hash parent parameter");
1034 our $hash_base = $input_params{'hash_base'};
1035 if (defined $hash_base) {
1036 if (!validate_refname($hash_base)) {
1037 die_error(400, "Invalid hash base parameter");
1041 our @extra_options = @{$input_params{'extra_options'}};
1042 # @extra_options is always defined, since it can only be (currently) set from
1043 # CGI, and $cgi->param() returns the empty array in array context if the param
1044 # is not set
1045 foreach my $opt (@extra_options) {
1046 if (not exists $allowed_options{$opt}) {
1047 die_error(400, "Invalid option parameter");
1049 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1050 die_error(400, "Invalid option parameter for this action");
1054 our $hash_parent_base = $input_params{'hash_parent_base'};
1055 if (defined $hash_parent_base) {
1056 if (!validate_refname($hash_parent_base)) {
1057 die_error(400, "Invalid hash parent base parameter");
1061 # other parameters
1062 our $page = $input_params{'page'};
1063 if (defined $page) {
1064 if ($page =~ m/[^0-9]/) {
1065 die_error(400, "Invalid page parameter");
1069 our $searchtype = $input_params{'searchtype'};
1070 if (defined $searchtype) {
1071 if ($searchtype =~ m/[^a-z]/) {
1072 die_error(400, "Invalid searchtype parameter");
1076 our $search_use_regexp = $input_params{'search_use_regexp'};
1078 our $searchtext = $input_params{'searchtext'};
1079 our $search_regexp;
1080 if (defined $searchtext) {
1081 if (length($searchtext) < 2) {
1082 die_error(403, "At least two characters are required for search parameter");
1084 if ($search_use_regexp) {
1085 $search_regexp = $searchtext;
1086 if (!eval { qr/$search_regexp/; 1; }) {
1087 (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1088 die_error(400, "Invalid search regexp '$search_regexp'",
1089 esc_html($error));
1091 } else {
1092 $search_regexp = quotemeta $searchtext;
1097 # path to the current git repository
1098 our $git_dir;
1099 sub evaluate_git_dir {
1100 our $git_dir = "$projectroot/$project" if $project;
1103 our (@snapshot_fmts, $git_avatar);
1104 sub configure_gitweb_features {
1105 # list of supported snapshot formats
1106 our @snapshot_fmts = gitweb_get_feature('snapshot');
1107 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1109 # check that the avatar feature is set to a known provider name,
1110 # and for each provider check if the dependencies are satisfied.
1111 # if the provider name is invalid or the dependencies are not met,
1112 # reset $git_avatar to the empty string.
1113 our ($git_avatar) = gitweb_get_feature('avatar');
1114 if ($git_avatar eq 'gravatar') {
1115 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1116 } elsif ($git_avatar eq 'picon') {
1117 # no dependencies
1118 } else {
1119 $git_avatar = '';
1123 # custom error handler: 'die <message>' is Internal Server Error
1124 sub handle_errors_html {
1125 my $msg = shift; # it is already HTML escaped
1127 # to avoid infinite loop where error occurs in die_error,
1128 # change handler to default handler, disabling handle_errors_html
1129 set_message("Error occured when inside die_error:\n$msg");
1131 # you cannot jump out of die_error when called as error handler;
1132 # the subroutine set via CGI::Carp::set_message is called _after_
1133 # HTTP headers are already written, so it cannot write them itself
1134 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1136 set_message(\&handle_errors_html);
1138 # dispatch
1139 sub dispatch {
1140 if (!defined $action) {
1141 if (defined $hash) {
1142 $action = git_get_type($hash);
1143 $action or die_error(404, "Object does not exist");
1144 } elsif (defined $hash_base && defined $file_name) {
1145 $action = git_get_type("$hash_base:$file_name");
1146 $action or die_error(404, "File or directory does not exist");
1147 } elsif (defined $project) {
1148 $action = 'summary';
1149 } else {
1150 $action = 'project_list';
1153 if (!defined($actions{$action})) {
1154 die_error(400, "Unknown action");
1156 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1157 !$project) {
1158 die_error(400, "Project needed");
1160 $actions{$action}->();
1163 sub reset_timer {
1164 our $t0 = [ gettimeofday() ]
1165 if defined $t0;
1166 our $number_of_git_cmds = 0;
1169 our $first_request = 1;
1170 sub run_request {
1171 reset_timer();
1173 evaluate_uri();
1174 if ($first_request) {
1175 evaluate_gitweb_config();
1176 evaluate_git_version();
1178 if ($per_request_config) {
1179 if (ref($per_request_config) eq 'CODE') {
1180 $per_request_config->();
1181 } elsif (!$first_request) {
1182 evaluate_gitweb_config();
1185 check_loadavg();
1187 # $projectroot and $projects_list might be set in gitweb config file
1188 $projects_list ||= $projectroot;
1190 evaluate_query_params();
1191 evaluate_path_info();
1192 evaluate_and_validate_params();
1193 evaluate_git_dir();
1195 configure_gitweb_features();
1197 dispatch();
1200 our $is_last_request = sub { 1 };
1201 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1202 our $CGI = 'CGI';
1203 our $cgi;
1204 sub configure_as_fcgi {
1205 require CGI::Fast;
1206 our $CGI = 'CGI::Fast';
1208 my $request_number = 0;
1209 # let each child service 100 requests
1210 our $is_last_request = sub { ++$request_number > 100 };
1212 sub evaluate_argv {
1213 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1214 configure_as_fcgi()
1215 if $script_name =~ /\.fcgi$/;
1217 return unless (@ARGV);
1219 require Getopt::Long;
1220 Getopt::Long::GetOptions(
1221 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1222 'nproc|n=i' => sub {
1223 my ($arg, $val) = @_;
1224 return unless eval { require FCGI::ProcManager; 1; };
1225 my $proc_manager = FCGI::ProcManager->new({
1226 n_processes => $val,
1228 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1229 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1230 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1235 sub run {
1236 evaluate_argv();
1238 $first_request = 1;
1239 $pre_listen_hook->()
1240 if $pre_listen_hook;
1242 REQUEST:
1243 while ($cgi = $CGI->new()) {
1244 $pre_dispatch_hook->()
1245 if $pre_dispatch_hook;
1247 run_request();
1249 $post_dispatch_hook->()
1250 if $post_dispatch_hook;
1251 $first_request = 0;
1253 last REQUEST if ($is_last_request->());
1256 DONE_GITWEB:
1260 run();
1262 if (defined caller) {
1263 # wrapped in a subroutine processing requests,
1264 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1265 return;
1266 } else {
1267 # pure CGI script, serving single request
1268 exit;
1271 ## ======================================================================
1272 ## action links
1274 # possible values of extra options
1275 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1276 # -replay => 1 - start from a current view (replay with modifications)
1277 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1278 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1279 sub href {
1280 my %params = @_;
1281 # default is to use -absolute url() i.e. $my_uri
1282 my $href = $params{-full} ? $my_url : $my_uri;
1284 # implicit -replay, must be first of implicit params
1285 $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1287 $params{'project'} = $project unless exists $params{'project'};
1289 if ($params{-replay}) {
1290 while (my ($name, $symbol) = each %cgi_param_mapping) {
1291 if (!exists $params{$name}) {
1292 $params{$name} = $input_params{$name};
1297 my $use_pathinfo = gitweb_check_feature('pathinfo');
1298 if (defined $params{'project'} &&
1299 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1300 # try to put as many parameters as possible in PATH_INFO:
1301 # - project name
1302 # - action
1303 # - hash_parent or hash_parent_base:/file_parent
1304 # - hash or hash_base:/filename
1305 # - the snapshot_format as an appropriate suffix
1307 # When the script is the root DirectoryIndex for the domain,
1308 # $href here would be something like http://gitweb.example.com/
1309 # Thus, we strip any trailing / from $href, to spare us double
1310 # slashes in the final URL
1311 $href =~ s,/$,,;
1313 # Then add the project name, if present
1314 $href .= "/".esc_path_info($params{'project'});
1315 delete $params{'project'};
1317 # since we destructively absorb parameters, we keep this
1318 # boolean that remembers if we're handling a snapshot
1319 my $is_snapshot = $params{'action'} eq 'snapshot';
1321 # Summary just uses the project path URL, any other action is
1322 # added to the URL
1323 if (defined $params{'action'}) {
1324 $href .= "/".esc_path_info($params{'action'})
1325 unless $params{'action'} eq 'summary';
1326 delete $params{'action'};
1329 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1330 # stripping nonexistent or useless pieces
1331 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1332 || $params{'hash_parent'} || $params{'hash'});
1333 if (defined $params{'hash_base'}) {
1334 if (defined $params{'hash_parent_base'}) {
1335 $href .= esc_path_info($params{'hash_parent_base'});
1336 # skip the file_parent if it's the same as the file_name
1337 if (defined $params{'file_parent'}) {
1338 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1339 delete $params{'file_parent'};
1340 } elsif ($params{'file_parent'} !~ /\.\./) {
1341 $href .= ":/".esc_path_info($params{'file_parent'});
1342 delete $params{'file_parent'};
1345 $href .= "..";
1346 delete $params{'hash_parent'};
1347 delete $params{'hash_parent_base'};
1348 } elsif (defined $params{'hash_parent'}) {
1349 $href .= esc_path_info($params{'hash_parent'}). "..";
1350 delete $params{'hash_parent'};
1353 $href .= esc_path_info($params{'hash_base'});
1354 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1355 $href .= ":/".esc_path_info($params{'file_name'});
1356 delete $params{'file_name'};
1358 delete $params{'hash'};
1359 delete $params{'hash_base'};
1360 } elsif (defined $params{'hash'}) {
1361 $href .= esc_path_info($params{'hash'});
1362 delete $params{'hash'};
1365 # If the action was a snapshot, we can absorb the
1366 # snapshot_format parameter too
1367 if ($is_snapshot) {
1368 my $fmt = $params{'snapshot_format'};
1369 # snapshot_format should always be defined when href()
1370 # is called, but just in case some code forgets, we
1371 # fall back to the default
1372 $fmt ||= $snapshot_fmts[0];
1373 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1374 delete $params{'snapshot_format'};
1378 # now encode the parameters explicitly
1379 my @result = ();
1380 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1381 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1382 if (defined $params{$name}) {
1383 if (ref($params{$name}) eq "ARRAY") {
1384 foreach my $par (@{$params{$name}}) {
1385 push @result, $symbol . "=" . esc_param($par);
1387 } else {
1388 push @result, $symbol . "=" . esc_param($params{$name});
1392 $href .= "?" . join(';', @result) if scalar @result;
1394 # final transformation: trailing spaces must be escaped (URI-encoded)
1395 $href =~ s/(\s+)$/CGI::escape($1)/e;
1397 if ($params{-anchor}) {
1398 $href .= "#".esc_param($params{-anchor});
1401 return $href;
1405 ## ======================================================================
1406 ## validation, quoting/unquoting and escaping
1408 sub validate_action {
1409 my $input = shift || return undef;
1410 return undef unless exists $actions{$input};
1411 return $input;
1414 sub validate_project {
1415 my $input = shift || return undef;
1416 if (!validate_pathname($input) ||
1417 !(-d "$projectroot/$input") ||
1418 !check_export_ok("$projectroot/$input") ||
1419 ($strict_export && !project_in_list($input))) {
1420 return undef;
1421 } else {
1422 return $input;
1426 sub validate_pathname {
1427 my $input = shift || return undef;
1429 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1430 # at the beginning, at the end, and between slashes.
1431 # also this catches doubled slashes
1432 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1433 return undef;
1435 # no null characters
1436 if ($input =~ m!\0!) {
1437 return undef;
1439 return $input;
1442 sub validate_refname {
1443 my $input = shift || return undef;
1445 # textual hashes are O.K.
1446 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1447 return $input;
1449 # it must be correct pathname
1450 $input = validate_pathname($input)
1451 or return undef;
1452 # restrictions on ref name according to git-check-ref-format
1453 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1454 return undef;
1456 return $input;
1459 # decode sequences of octets in utf8 into Perl's internal form,
1460 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1461 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1462 sub to_utf8 {
1463 my $str = shift;
1464 return undef unless defined $str;
1466 if (utf8::is_utf8($str) || utf8::decode($str)) {
1467 return $str;
1468 } else {
1469 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1473 # quote unsafe chars, but keep the slash, even when it's not
1474 # correct, but quoted slashes look too horrible in bookmarks
1475 sub esc_param {
1476 my $str = shift;
1477 return undef unless defined $str;
1478 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1479 $str =~ s/ /\+/g;
1480 return $str;
1483 # the quoting rules for path_info fragment are slightly different
1484 sub esc_path_info {
1485 my $str = shift;
1486 return undef unless defined $str;
1488 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1489 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1491 return $str;
1494 # quote unsafe chars in whole URL, so some characters cannot be quoted
1495 sub esc_url {
1496 my $str = shift;
1497 return undef unless defined $str;
1498 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1499 $str =~ s/ /\+/g;
1500 return $str;
1503 # quote unsafe characters in HTML attributes
1504 sub esc_attr {
1506 # for XHTML conformance escaping '"' to '&quot;' is not enough
1507 return esc_html(@_);
1510 # replace invalid utf8 character with SUBSTITUTION sequence
1511 sub esc_html {
1512 my $str = shift;
1513 my %opts = @_;
1515 return undef unless defined $str;
1517 $str = to_utf8($str);
1518 $str = $cgi->escapeHTML($str);
1519 if ($opts{'-nbsp'}) {
1520 $str =~ s/ /&nbsp;/g;
1522 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1523 return $str;
1526 # quote control characters and escape filename to HTML
1527 sub esc_path {
1528 my $str = shift;
1529 my %opts = @_;
1531 return undef unless defined $str;
1533 $str = to_utf8($str);
1534 $str = $cgi->escapeHTML($str);
1535 if ($opts{'-nbsp'}) {
1536 $str =~ s/ /&nbsp;/g;
1538 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1539 return $str;
1542 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
1543 sub sanitize {
1544 my $str = shift;
1546 return undef unless defined $str;
1548 $str = to_utf8($str);
1549 $str =~ s|([[:cntrl:]])|($1 =~ /[\t\n\r]/ ? $1 : quot_cec($1))|eg;
1550 return $str;
1553 # Make control characters "printable", using character escape codes (CEC)
1554 sub quot_cec {
1555 my $cntrl = shift;
1556 my %opts = @_;
1557 my %es = ( # character escape codes, aka escape sequences
1558 "\t" => '\t', # tab (HT)
1559 "\n" => '\n', # line feed (LF)
1560 "\r" => '\r', # carrige return (CR)
1561 "\f" => '\f', # form feed (FF)
1562 "\b" => '\b', # backspace (BS)
1563 "\a" => '\a', # alarm (bell) (BEL)
1564 "\e" => '\e', # escape (ESC)
1565 "\013" => '\v', # vertical tab (VT)
1566 "\000" => '\0', # nul character (NUL)
1568 my $chr = ( (exists $es{$cntrl})
1569 ? $es{$cntrl}
1570 : sprintf('\%2x', ord($cntrl)) );
1571 if ($opts{-nohtml}) {
1572 return $chr;
1573 } else {
1574 return "<span class=\"cntrl\">$chr</span>";
1578 # Alternatively use unicode control pictures codepoints,
1579 # Unicode "printable representation" (PR)
1580 sub quot_upr {
1581 my $cntrl = shift;
1582 my %opts = @_;
1584 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1585 if ($opts{-nohtml}) {
1586 return $chr;
1587 } else {
1588 return "<span class=\"cntrl\">$chr</span>";
1592 # git may return quoted and escaped filenames
1593 sub unquote {
1594 my $str = shift;
1596 sub unq {
1597 my $seq = shift;
1598 my %es = ( # character escape codes, aka escape sequences
1599 't' => "\t", # tab (HT, TAB)
1600 'n' => "\n", # newline (NL)
1601 'r' => "\r", # return (CR)
1602 'f' => "\f", # form feed (FF)
1603 'b' => "\b", # backspace (BS)
1604 'a' => "\a", # alarm (bell) (BEL)
1605 'e' => "\e", # escape (ESC)
1606 'v' => "\013", # vertical tab (VT)
1609 if ($seq =~ m/^[0-7]{1,3}$/) {
1610 # octal char sequence
1611 return chr(oct($seq));
1612 } elsif (exists $es{$seq}) {
1613 # C escape sequence, aka character escape code
1614 return $es{$seq};
1616 # quoted ordinary character
1617 return $seq;
1620 if ($str =~ m/^"(.*)"$/) {
1621 # needs unquoting
1622 $str = $1;
1623 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1625 return $str;
1628 # escape tabs (convert tabs to spaces)
1629 sub untabify {
1630 my $line = shift;
1632 while ((my $pos = index($line, "\t")) != -1) {
1633 if (my $count = (8 - ($pos % 8))) {
1634 my $spaces = ' ' x $count;
1635 $line =~ s/\t/$spaces/;
1639 return $line;
1642 sub project_in_list {
1643 my $project = shift;
1644 my @list = git_get_projects_list();
1645 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1648 ## ----------------------------------------------------------------------
1649 ## HTML aware string manipulation
1651 # Try to chop given string on a word boundary between position
1652 # $len and $len+$add_len. If there is no word boundary there,
1653 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1654 # (marking chopped part) would be longer than given string.
1655 sub chop_str {
1656 my $str = shift;
1657 my $len = shift;
1658 my $add_len = shift || 10;
1659 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1661 # Make sure perl knows it is utf8 encoded so we don't
1662 # cut in the middle of a utf8 multibyte char.
1663 $str = to_utf8($str);
1665 # allow only $len chars, but don't cut a word if it would fit in $add_len
1666 # if it doesn't fit, cut it if it's still longer than the dots we would add
1667 # remove chopped character entities entirely
1669 # when chopping in the middle, distribute $len into left and right part
1670 # return early if chopping wouldn't make string shorter
1671 if ($where eq 'center') {
1672 return $str if ($len + 5 >= length($str)); # filler is length 5
1673 $len = int($len/2);
1674 } else {
1675 return $str if ($len + 4 >= length($str)); # filler is length 4
1678 # regexps: ending and beginning with word part up to $add_len
1679 my $endre = qr/.{$len}\w{0,$add_len}/;
1680 my $begre = qr/\w{0,$add_len}.{$len}/;
1682 if ($where eq 'left') {
1683 $str =~ m/^(.*?)($begre)$/;
1684 my ($lead, $body) = ($1, $2);
1685 if (length($lead) > 4) {
1686 $lead = " ...";
1688 return "$lead$body";
1690 } elsif ($where eq 'center') {
1691 $str =~ m/^($endre)(.*)$/;
1692 my ($left, $str) = ($1, $2);
1693 $str =~ m/^(.*?)($begre)$/;
1694 my ($mid, $right) = ($1, $2);
1695 if (length($mid) > 5) {
1696 $mid = " ... ";
1698 return "$left$mid$right";
1700 } else {
1701 $str =~ m/^($endre)(.*)$/;
1702 my $body = $1;
1703 my $tail = $2;
1704 if (length($tail) > 4) {
1705 $tail = "... ";
1707 return "$body$tail";
1711 # takes the same arguments as chop_str, but also wraps a <span> around the
1712 # result with a title attribute if it does get chopped. Additionally, the
1713 # string is HTML-escaped.
1714 sub chop_and_escape_str {
1715 my ($str) = @_;
1717 my $chopped = chop_str(@_);
1718 $str = to_utf8($str);
1719 if ($chopped eq $str) {
1720 return esc_html($chopped);
1721 } else {
1722 $str =~ s/[[:cntrl:]]/?/g;
1723 return $cgi->span({-title=>$str}, esc_html($chopped));
1727 # Highlight selected fragments of string, using given CSS class,
1728 # and escape HTML. It is assumed that fragments do not overlap.
1729 # Regions are passed as list of pairs (array references).
1731 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
1732 # '<span class="mark">foo</span>bar'
1733 sub esc_html_hl_regions {
1734 my ($str, $css_class, @sel) = @_;
1735 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
1736 @sel = grep { ref($_) eq 'ARRAY' } @sel;
1737 return esc_html($str, %opts) unless @sel;
1739 my $out = '';
1740 my $pos = 0;
1742 for my $s (@sel) {
1743 my ($begin, $end) = @$s;
1745 # Don't create empty <span> elements.
1746 next if $end <= $begin;
1748 my $escaped = esc_html(substr($str, $begin, $end - $begin),
1749 %opts);
1751 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
1752 if ($begin - $pos > 0);
1753 $out .= $cgi->span({-class => $css_class}, $escaped);
1755 $pos = $end;
1757 $out .= esc_html(substr($str, $pos), %opts)
1758 if ($pos < length($str));
1760 return $out;
1763 # return positions of beginning and end of each match
1764 sub matchpos_list {
1765 my ($str, $regexp) = @_;
1766 return unless (defined $str && defined $regexp);
1768 my @matches;
1769 while ($str =~ /$regexp/g) {
1770 push @matches, [$-[0], $+[0]];
1772 return @matches;
1775 # highlight match (if any), and escape HTML
1776 sub esc_html_match_hl {
1777 my ($str, $regexp) = @_;
1778 return esc_html($str) unless defined $regexp;
1780 my @matches = matchpos_list($str, $regexp);
1781 return esc_html($str) unless @matches;
1783 return esc_html_hl_regions($str, 'match', @matches);
1787 # highlight match (if any) of shortened string, and escape HTML
1788 sub esc_html_match_hl_chopped {
1789 my ($str, $chopped, $regexp) = @_;
1790 return esc_html_match_hl($str, $regexp) unless defined $chopped;
1792 my @matches = matchpos_list($str, $regexp);
1793 return esc_html($chopped) unless @matches;
1795 # filter matches so that we mark chopped string
1796 my $tail = "... "; # see chop_str
1797 unless ($chopped =~ s/\Q$tail\E$//) {
1798 $tail = '';
1800 my $chop_len = length($chopped);
1801 my $tail_len = length($tail);
1802 my @filtered;
1804 for my $m (@matches) {
1805 if ($m->[0] > $chop_len) {
1806 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
1807 last;
1808 } elsif ($m->[1] > $chop_len) {
1809 push @filtered, [ $m->[0], $chop_len + $tail_len ];
1810 last;
1812 push @filtered, $m;
1815 return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
1818 ## ----------------------------------------------------------------------
1819 ## functions returning short strings
1821 # CSS class for given age value (in seconds)
1822 sub age_class {
1823 my $age = shift;
1825 if (!defined $age) {
1826 return "noage";
1827 } elsif ($age < 60*60*2) {
1828 return "age0";
1829 } elsif ($age < 60*60*24*2) {
1830 return "age1";
1831 } else {
1832 return "age2";
1836 # convert age in seconds to "nn units ago" string
1837 sub age_string {
1838 my $age = shift;
1839 my $age_str;
1841 if ($age > 60*60*24*365*2) {
1842 $age_str = (int $age/60/60/24/365);
1843 $age_str .= " years ago";
1844 } elsif ($age > 60*60*24*(365/12)*2) {
1845 $age_str = int $age/60/60/24/(365/12);
1846 $age_str .= " months ago";
1847 } elsif ($age > 60*60*24*7*2) {
1848 $age_str = int $age/60/60/24/7;
1849 $age_str .= " weeks ago";
1850 } elsif ($age > 60*60*24*2) {
1851 $age_str = int $age/60/60/24;
1852 $age_str .= " days ago";
1853 } elsif ($age > 60*60*2) {
1854 $age_str = int $age/60/60;
1855 $age_str .= " hours ago";
1856 } elsif ($age > 60*2) {
1857 $age_str = int $age/60;
1858 $age_str .= " min ago";
1859 } elsif ($age > 2) {
1860 $age_str = int $age;
1861 $age_str .= " sec ago";
1862 } else {
1863 $age_str .= " right now";
1865 return $age_str;
1868 use constant {
1869 S_IFINVALID => 0030000,
1870 S_IFGITLINK => 0160000,
1873 # submodule/subproject, a commit object reference
1874 sub S_ISGITLINK {
1875 my $mode = shift;
1877 return (($mode & S_IFMT) == S_IFGITLINK)
1880 # convert file mode in octal to symbolic file mode string
1881 sub mode_str {
1882 my $mode = oct shift;
1884 if (S_ISGITLINK($mode)) {
1885 return 'm---------';
1886 } elsif (S_ISDIR($mode & S_IFMT)) {
1887 return 'drwxr-xr-x';
1888 } elsif (S_ISLNK($mode)) {
1889 return 'lrwxrwxrwx';
1890 } elsif (S_ISREG($mode)) {
1891 # git cares only about the executable bit
1892 if ($mode & S_IXUSR) {
1893 return '-rwxr-xr-x';
1894 } else {
1895 return '-rw-r--r--';
1897 } else {
1898 return '----------';
1902 # convert file mode in octal to file type string
1903 sub file_type {
1904 my $mode = shift;
1906 if ($mode !~ m/^[0-7]+$/) {
1907 return $mode;
1908 } else {
1909 $mode = oct $mode;
1912 if (S_ISGITLINK($mode)) {
1913 return "submodule";
1914 } elsif (S_ISDIR($mode & S_IFMT)) {
1915 return "directory";
1916 } elsif (S_ISLNK($mode)) {
1917 return "symlink";
1918 } elsif (S_ISREG($mode)) {
1919 return "file";
1920 } else {
1921 return "unknown";
1925 # convert file mode in octal to file type description string
1926 sub file_type_long {
1927 my $mode = shift;
1929 if ($mode !~ m/^[0-7]+$/) {
1930 return $mode;
1931 } else {
1932 $mode = oct $mode;
1935 if (S_ISGITLINK($mode)) {
1936 return "submodule";
1937 } elsif (S_ISDIR($mode & S_IFMT)) {
1938 return "directory";
1939 } elsif (S_ISLNK($mode)) {
1940 return "symlink";
1941 } elsif (S_ISREG($mode)) {
1942 if ($mode & S_IXUSR) {
1943 return "executable";
1944 } else {
1945 return "file";
1947 } else {
1948 return "unknown";
1953 ## ----------------------------------------------------------------------
1954 ## functions returning short HTML fragments, or transforming HTML fragments
1955 ## which don't belong to other sections
1957 # format line of commit message.
1958 sub format_log_line_html {
1959 my $line = shift;
1961 $line = esc_html($line, -nbsp=>1);
1962 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1963 $cgi->a({-href => href(action=>"object", hash=>$1),
1964 -class => "text"}, $1);
1965 }eg;
1967 return $line;
1970 # format marker of refs pointing to given object
1972 # the destination action is chosen based on object type and current context:
1973 # - for annotated tags, we choose the tag view unless it's the current view
1974 # already, in which case we go to shortlog view
1975 # - for other refs, we keep the current view if we're in history, shortlog or
1976 # log view, and select shortlog otherwise
1977 sub format_ref_marker {
1978 my ($refs, $id) = @_;
1979 my $markers = '';
1981 if (defined $refs->{$id}) {
1982 foreach my $ref (@{$refs->{$id}}) {
1983 # this code exploits the fact that non-lightweight tags are the
1984 # only indirect objects, and that they are the only objects for which
1985 # we want to use tag instead of shortlog as action
1986 my ($type, $name) = qw();
1987 my $indirect = ($ref =~ s/\^\{\}$//);
1988 # e.g. tags/v2.6.11 or heads/next
1989 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1990 $type = $1;
1991 $name = $2;
1992 } else {
1993 $type = "ref";
1994 $name = $ref;
1997 my $class = $type;
1998 $class .= " indirect" if $indirect;
2000 my $dest_action = "shortlog";
2002 if ($indirect) {
2003 $dest_action = "tag" unless $action eq "tag";
2004 } elsif ($action =~ /^(history|(short)?log)$/) {
2005 $dest_action = $action;
2008 my $dest = "";
2009 $dest .= "refs/" unless $ref =~ m!^refs/!;
2010 $dest .= $ref;
2012 my $link = $cgi->a({
2013 -href => href(
2014 action=>$dest_action,
2015 hash=>$dest
2016 )}, $name);
2018 $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2019 $link . "</span>";
2023 if ($markers) {
2024 return ' <span class="refs">'. $markers . '</span>';
2025 } else {
2026 return "";
2030 # format, perhaps shortened and with markers, title line
2031 sub format_subject_html {
2032 my ($long, $short, $href, $extra) = @_;
2033 $extra = '' unless defined($extra);
2035 if (length($short) < length($long)) {
2036 $long =~ s/[[:cntrl:]]/?/g;
2037 return $cgi->a({-href => $href, -class => "list subject",
2038 -title => to_utf8($long)},
2039 esc_html($short)) . $extra;
2040 } else {
2041 return $cgi->a({-href => $href, -class => "list subject"},
2042 esc_html($long)) . $extra;
2046 # Rather than recomputing the url for an email multiple times, we cache it
2047 # after the first hit. This gives a visible benefit in views where the avatar
2048 # for the same email is used repeatedly (e.g. shortlog).
2049 # The cache is shared by all avatar engines (currently gravatar only), which
2050 # are free to use it as preferred. Since only one avatar engine is used for any
2051 # given page, there's no risk for cache conflicts.
2052 our %avatar_cache = ();
2054 # Compute the picon url for a given email, by using the picon search service over at
2055 # http://www.cs.indiana.edu/picons/search.html
2056 sub picon_url {
2057 my $email = lc shift;
2058 if (!$avatar_cache{$email}) {
2059 my ($user, $domain) = split('@', $email);
2060 $avatar_cache{$email} =
2061 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2062 "$domain/$user/" .
2063 "users+domains+unknown/up/single";
2065 return $avatar_cache{$email};
2068 # Compute the gravatar url for a given email, if it's not in the cache already.
2069 # Gravatar stores only the part of the URL before the size, since that's the
2070 # one computationally more expensive. This also allows reuse of the cache for
2071 # different sizes (for this particular engine).
2072 sub gravatar_url {
2073 my $email = lc shift;
2074 my $size = shift;
2075 $avatar_cache{$email} ||=
2076 "http://www.gravatar.com/avatar/" .
2077 Digest::MD5::md5_hex($email) . "?s=";
2078 return $avatar_cache{$email} . $size;
2081 # Insert an avatar for the given $email at the given $size if the feature
2082 # is enabled.
2083 sub git_get_avatar {
2084 my ($email, %opts) = @_;
2085 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
2086 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
2087 $opts{-size} ||= 'default';
2088 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2089 my $url = "";
2090 if ($git_avatar eq 'gravatar') {
2091 $url = gravatar_url($email, $size);
2092 } elsif ($git_avatar eq 'picon') {
2093 $url = picon_url($email);
2095 # Other providers can be added by extending the if chain, defining $url
2096 # as needed. If no variant puts something in $url, we assume avatars
2097 # are completely disabled/unavailable.
2098 if ($url) {
2099 return $pre_white .
2100 "<img width=\"$size\" " .
2101 "class=\"avatar\" " .
2102 "src=\"".esc_url($url)."\" " .
2103 "alt=\"\" " .
2104 "/>" . $post_white;
2105 } else {
2106 return "";
2110 sub format_search_author {
2111 my ($author, $searchtype, $displaytext) = @_;
2112 my $have_search = gitweb_check_feature('search');
2114 if ($have_search) {
2115 my $performed = "";
2116 if ($searchtype eq 'author') {
2117 $performed = "authored";
2118 } elsif ($searchtype eq 'committer') {
2119 $performed = "committed";
2122 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2123 searchtext=>$author,
2124 searchtype=>$searchtype), class=>"list",
2125 title=>"Search for commits $performed by $author"},
2126 $displaytext);
2128 } else {
2129 return $displaytext;
2133 # format the author name of the given commit with the given tag
2134 # the author name is chopped and escaped according to the other
2135 # optional parameters (see chop_str).
2136 sub format_author_html {
2137 my $tag = shift;
2138 my $co = shift;
2139 my $author = chop_and_escape_str($co->{'author_name'}, @_);
2140 return "<$tag class=\"author\">" .
2141 format_search_author($co->{'author_name'}, "author",
2142 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2143 $author) .
2144 "</$tag>";
2147 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2148 sub format_git_diff_header_line {
2149 my $line = shift;
2150 my $diffinfo = shift;
2151 my ($from, $to) = @_;
2153 if ($diffinfo->{'nparents'}) {
2154 # combined diff
2155 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2156 if ($to->{'href'}) {
2157 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2158 esc_path($to->{'file'}));
2159 } else { # file was deleted (no href)
2160 $line .= esc_path($to->{'file'});
2162 } else {
2163 # "ordinary" diff
2164 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2165 if ($from->{'href'}) {
2166 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2167 'a/' . esc_path($from->{'file'}));
2168 } else { # file was added (no href)
2169 $line .= 'a/' . esc_path($from->{'file'});
2171 $line .= ' ';
2172 if ($to->{'href'}) {
2173 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2174 'b/' . esc_path($to->{'file'}));
2175 } else { # file was deleted
2176 $line .= 'b/' . esc_path($to->{'file'});
2180 return "<div class=\"diff header\">$line</div>\n";
2183 # format extended diff header line, before patch itself
2184 sub format_extended_diff_header_line {
2185 my $line = shift;
2186 my $diffinfo = shift;
2187 my ($from, $to) = @_;
2189 # match <path>
2190 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2191 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2192 esc_path($from->{'file'}));
2194 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2195 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2196 esc_path($to->{'file'}));
2198 # match single <mode>
2199 if ($line =~ m/\s(\d{6})$/) {
2200 $line .= '<span class="info"> (' .
2201 file_type_long($1) .
2202 ')</span>';
2204 # match <hash>
2205 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2206 # can match only for combined diff
2207 $line = 'index ';
2208 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2209 if ($from->{'href'}[$i]) {
2210 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2211 -class=>"hash"},
2212 substr($diffinfo->{'from_id'}[$i],0,7));
2213 } else {
2214 $line .= '0' x 7;
2216 # separator
2217 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2219 $line .= '..';
2220 if ($to->{'href'}) {
2221 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2222 substr($diffinfo->{'to_id'},0,7));
2223 } else {
2224 $line .= '0' x 7;
2227 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2228 # can match only for ordinary diff
2229 my ($from_link, $to_link);
2230 if ($from->{'href'}) {
2231 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2232 substr($diffinfo->{'from_id'},0,7));
2233 } else {
2234 $from_link = '0' x 7;
2236 if ($to->{'href'}) {
2237 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2238 substr($diffinfo->{'to_id'},0,7));
2239 } else {
2240 $to_link = '0' x 7;
2242 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2243 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2246 return $line . "<br/>\n";
2249 # format from-file/to-file diff header
2250 sub format_diff_from_to_header {
2251 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2252 my $line;
2253 my $result = '';
2255 $line = $from_line;
2256 #assert($line =~ m/^---/) if DEBUG;
2257 # no extra formatting for "^--- /dev/null"
2258 if (! $diffinfo->{'nparents'}) {
2259 # ordinary (single parent) diff
2260 if ($line =~ m!^--- "?a/!) {
2261 if ($from->{'href'}) {
2262 $line = '--- a/' .
2263 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2264 esc_path($from->{'file'}));
2265 } else {
2266 $line = '--- a/' .
2267 esc_path($from->{'file'});
2270 $result .= qq!<div class="diff from_file">$line</div>\n!;
2272 } else {
2273 # combined diff (merge commit)
2274 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2275 if ($from->{'href'}[$i]) {
2276 $line = '--- ' .
2277 $cgi->a({-href=>href(action=>"blobdiff",
2278 hash_parent=>$diffinfo->{'from_id'}[$i],
2279 hash_parent_base=>$parents[$i],
2280 file_parent=>$from->{'file'}[$i],
2281 hash=>$diffinfo->{'to_id'},
2282 hash_base=>$hash,
2283 file_name=>$to->{'file'}),
2284 -class=>"path",
2285 -title=>"diff" . ($i+1)},
2286 $i+1) .
2287 '/' .
2288 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2289 esc_path($from->{'file'}[$i]));
2290 } else {
2291 $line = '--- /dev/null';
2293 $result .= qq!<div class="diff from_file">$line</div>\n!;
2297 $line = $to_line;
2298 #assert($line =~ m/^\+\+\+/) if DEBUG;
2299 # no extra formatting for "^+++ /dev/null"
2300 if ($line =~ m!^\+\+\+ "?b/!) {
2301 if ($to->{'href'}) {
2302 $line = '+++ b/' .
2303 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2304 esc_path($to->{'file'}));
2305 } else {
2306 $line = '+++ b/' .
2307 esc_path($to->{'file'});
2310 $result .= qq!<div class="diff to_file">$line</div>\n!;
2312 return $result;
2315 # create note for patch simplified by combined diff
2316 sub format_diff_cc_simplified {
2317 my ($diffinfo, @parents) = @_;
2318 my $result = '';
2320 $result .= "<div class=\"diff header\">" .
2321 "diff --cc ";
2322 if (!is_deleted($diffinfo)) {
2323 $result .= $cgi->a({-href => href(action=>"blob",
2324 hash_base=>$hash,
2325 hash=>$diffinfo->{'to_id'},
2326 file_name=>$diffinfo->{'to_file'}),
2327 -class => "path"},
2328 esc_path($diffinfo->{'to_file'}));
2329 } else {
2330 $result .= esc_path($diffinfo->{'to_file'});
2332 $result .= "</div>\n" . # class="diff header"
2333 "<div class=\"diff nodifferences\">" .
2334 "Simple merge" .
2335 "</div>\n"; # class="diff nodifferences"
2337 return $result;
2340 sub diff_line_class {
2341 my ($line, $from, $to) = @_;
2343 # ordinary diff
2344 my $num_sign = 1;
2345 # combined diff
2346 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2347 $num_sign = scalar @{$from->{'href'}};
2350 my @diff_line_classifier = (
2351 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
2352 { regexp => qr/^\\/, class => "incomplete" },
2353 { regexp => qr/^ {$num_sign}/, class => "ctx" },
2354 # classifier for context must come before classifier add/rem,
2355 # or we would have to use more complicated regexp, for example
2356 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2357 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
2358 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
2360 for my $clsfy (@diff_line_classifier) {
2361 return $clsfy->{'class'}
2362 if ($line =~ $clsfy->{'regexp'});
2365 # fallback
2366 return "";
2369 # assumes that $from and $to are defined and correctly filled,
2370 # and that $line holds a line of chunk header for unified diff
2371 sub format_unidiff_chunk_header {
2372 my ($line, $from, $to) = @_;
2374 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2375 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2377 $from_lines = 0 unless defined $from_lines;
2378 $to_lines = 0 unless defined $to_lines;
2380 if ($from->{'href'}) {
2381 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2382 -class=>"list"}, $from_text);
2384 if ($to->{'href'}) {
2385 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2386 -class=>"list"}, $to_text);
2388 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2389 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2390 return $line;
2393 # assumes that $from and $to are defined and correctly filled,
2394 # and that $line holds a line of chunk header for combined diff
2395 sub format_cc_diff_chunk_header {
2396 my ($line, $from, $to) = @_;
2398 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2399 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2401 @from_text = split(' ', $ranges);
2402 for (my $i = 0; $i < @from_text; ++$i) {
2403 ($from_start[$i], $from_nlines[$i]) =
2404 (split(',', substr($from_text[$i], 1)), 0);
2407 $to_text = pop @from_text;
2408 $to_start = pop @from_start;
2409 $to_nlines = pop @from_nlines;
2411 $line = "<span class=\"chunk_info\">$prefix ";
2412 for (my $i = 0; $i < @from_text; ++$i) {
2413 if ($from->{'href'}[$i]) {
2414 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2415 -class=>"list"}, $from_text[$i]);
2416 } else {
2417 $line .= $from_text[$i];
2419 $line .= " ";
2421 if ($to->{'href'}) {
2422 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2423 -class=>"list"}, $to_text);
2424 } else {
2425 $line .= $to_text;
2427 $line .= " $prefix</span>" .
2428 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2429 return $line;
2432 # process patch (diff) line (not to be used for diff headers),
2433 # returning HTML-formatted (but not wrapped) line.
2434 # If the line is passed as a reference, it is treated as HTML and not
2435 # esc_html()'ed.
2436 sub format_diff_line {
2437 my ($line, $diff_class, $from, $to) = @_;
2439 if (ref($line)) {
2440 $line = $$line;
2441 } else {
2442 chomp $line;
2443 $line = untabify($line);
2445 if ($from && $to && $line =~ m/^\@{2} /) {
2446 $line = format_unidiff_chunk_header($line, $from, $to);
2447 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2448 $line = format_cc_diff_chunk_header($line, $from, $to);
2449 } else {
2450 $line = esc_html($line, -nbsp=>1);
2454 my $diff_classes = "diff";
2455 $diff_classes .= " $diff_class" if ($diff_class);
2456 $line = "<div class=\"$diff_classes\">$line</div>\n";
2458 return $line;
2461 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2462 # linked. Pass the hash of the tree/commit to snapshot.
2463 sub format_snapshot_links {
2464 my ($hash) = @_;
2465 my $num_fmts = @snapshot_fmts;
2466 if ($num_fmts > 1) {
2467 # A parenthesized list of links bearing format names.
2468 # e.g. "snapshot (_tar.gz_ _zip_)"
2469 return "snapshot (" . join(' ', map
2470 $cgi->a({
2471 -href => href(
2472 action=>"snapshot",
2473 hash=>$hash,
2474 snapshot_format=>$_
2476 }, $known_snapshot_formats{$_}{'display'})
2477 , @snapshot_fmts) . ")";
2478 } elsif ($num_fmts == 1) {
2479 # A single "snapshot" link whose tooltip bears the format name.
2480 # i.e. "_snapshot_"
2481 my ($fmt) = @snapshot_fmts;
2482 return
2483 $cgi->a({
2484 -href => href(
2485 action=>"snapshot",
2486 hash=>$hash,
2487 snapshot_format=>$fmt
2489 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2490 }, "snapshot");
2491 } else { # $num_fmts == 0
2492 return undef;
2496 ## ......................................................................
2497 ## functions returning values to be passed, perhaps after some
2498 ## transformation, to other functions; e.g. returning arguments to href()
2500 # returns hash to be passed to href to generate gitweb URL
2501 # in -title key it returns description of link
2502 sub get_feed_info {
2503 my $format = shift || 'Atom';
2504 my %res = (action => lc($format));
2506 # feed links are possible only for project views
2507 return unless (defined $project);
2508 # some views should link to OPML, or to generic project feed,
2509 # or don't have specific feed yet (so they should use generic)
2510 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2512 my $branch;
2513 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2514 # from tag links; this also makes possible to detect branch links
2515 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2516 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
2517 $branch = $1;
2519 # find log type for feed description (title)
2520 my $type = 'log';
2521 if (defined $file_name) {
2522 $type = "history of $file_name";
2523 $type .= "/" if ($action eq 'tree');
2524 $type .= " on '$branch'" if (defined $branch);
2525 } else {
2526 $type = "log of $branch" if (defined $branch);
2529 $res{-title} = $type;
2530 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2531 $res{'file_name'} = $file_name;
2533 return %res;
2536 ## ----------------------------------------------------------------------
2537 ## git utility subroutines, invoking git commands
2539 # returns path to the core git executable and the --git-dir parameter as list
2540 sub git_cmd {
2541 $number_of_git_cmds++;
2542 return $GIT, '--git-dir='.$git_dir;
2545 # quote the given arguments for passing them to the shell
2546 # quote_command("command", "arg 1", "arg with ' and ! characters")
2547 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2548 # Try to avoid using this function wherever possible.
2549 sub quote_command {
2550 return join(' ',
2551 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2554 # get HEAD ref of given project as hash
2555 sub git_get_head_hash {
2556 return git_get_full_hash(shift, 'HEAD');
2559 sub git_get_full_hash {
2560 return git_get_hash(@_);
2563 sub git_get_short_hash {
2564 return git_get_hash(@_, '--short=7');
2567 sub git_get_hash {
2568 my ($project, $hash, @options) = @_;
2569 my $o_git_dir = $git_dir;
2570 my $retval = undef;
2571 $git_dir = "$projectroot/$project";
2572 if (open my $fd, '-|', git_cmd(), 'rev-parse',
2573 '--verify', '-q', @options, $hash) {
2574 $retval = <$fd>;
2575 chomp $retval if defined $retval;
2576 close $fd;
2578 if (defined $o_git_dir) {
2579 $git_dir = $o_git_dir;
2581 return $retval;
2584 # get type of given object
2585 sub git_get_type {
2586 my $hash = shift;
2588 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2589 my $type = <$fd>;
2590 close $fd or return;
2591 chomp $type;
2592 return $type;
2595 # repository configuration
2596 our $config_file = '';
2597 our %config;
2599 # store multiple values for single key as anonymous array reference
2600 # single values stored directly in the hash, not as [ <value> ]
2601 sub hash_set_multi {
2602 my ($hash, $key, $value) = @_;
2604 if (!exists $hash->{$key}) {
2605 $hash->{$key} = $value;
2606 } elsif (!ref $hash->{$key}) {
2607 $hash->{$key} = [ $hash->{$key}, $value ];
2608 } else {
2609 push @{$hash->{$key}}, $value;
2613 # return hash of git project configuration
2614 # optionally limited to some section, e.g. 'gitweb'
2615 sub git_parse_project_config {
2616 my $section_regexp = shift;
2617 my %config;
2619 local $/ = "\0";
2621 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2622 or return;
2624 while (my $keyval = <$fh>) {
2625 chomp $keyval;
2626 my ($key, $value) = split(/\n/, $keyval, 2);
2628 hash_set_multi(\%config, $key, $value)
2629 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2631 close $fh;
2633 return %config;
2636 # convert config value to boolean: 'true' or 'false'
2637 # no value, number > 0, 'true' and 'yes' values are true
2638 # rest of values are treated as false (never as error)
2639 sub config_to_bool {
2640 my $val = shift;
2642 return 1 if !defined $val; # section.key
2644 # strip leading and trailing whitespace
2645 $val =~ s/^\s+//;
2646 $val =~ s/\s+$//;
2648 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2649 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2652 # convert config value to simple decimal number
2653 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2654 # to be multiplied by 1024, 1048576, or 1073741824
2655 sub config_to_int {
2656 my $val = shift;
2658 # strip leading and trailing whitespace
2659 $val =~ s/^\s+//;
2660 $val =~ s/\s+$//;
2662 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2663 $unit = lc($unit);
2664 # unknown unit is treated as 1
2665 return $num * ($unit eq 'g' ? 1073741824 :
2666 $unit eq 'm' ? 1048576 :
2667 $unit eq 'k' ? 1024 : 1);
2669 return $val;
2672 # convert config value to array reference, if needed
2673 sub config_to_multi {
2674 my $val = shift;
2676 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2679 sub git_get_project_config {
2680 my ($key, $type) = @_;
2682 return unless defined $git_dir;
2684 # key sanity check
2685 return unless ($key);
2686 # only subsection, if exists, is case sensitive,
2687 # and not lowercased by 'git config -z -l'
2688 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2689 $key = join(".", lc($hi), $mi, lc($lo));
2690 } else {
2691 $key = lc($key);
2693 $key =~ s/^gitweb\.//;
2694 return if ($key =~ m/\W/);
2696 # type sanity check
2697 if (defined $type) {
2698 $type =~ s/^--//;
2699 $type = undef
2700 unless ($type eq 'bool' || $type eq 'int');
2703 # get config
2704 if (!defined $config_file ||
2705 $config_file ne "$git_dir/config") {
2706 %config = git_parse_project_config('gitweb');
2707 $config_file = "$git_dir/config";
2710 # check if config variable (key) exists
2711 return unless exists $config{"gitweb.$key"};
2713 # ensure given type
2714 if (!defined $type) {
2715 return $config{"gitweb.$key"};
2716 } elsif ($type eq 'bool') {
2717 # backward compatibility: 'git config --bool' returns true/false
2718 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2719 } elsif ($type eq 'int') {
2720 return config_to_int($config{"gitweb.$key"});
2722 return $config{"gitweb.$key"};
2725 # get hash of given path at given ref
2726 sub git_get_hash_by_path {
2727 my $base = shift;
2728 my $path = shift || return undef;
2729 my $type = shift;
2731 $path =~ s,/+$,,;
2733 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2734 or die_error(500, "Open git-ls-tree failed");
2735 my $line = <$fd>;
2736 close $fd or return undef;
2738 if (!defined $line) {
2739 # there is no tree or hash given by $path at $base
2740 return undef;
2743 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2744 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2745 if (defined $type && $type ne $2) {
2746 # type doesn't match
2747 return undef;
2749 return $3;
2752 # get path of entry with given hash at given tree-ish (ref)
2753 # used to get 'from' filename for combined diff (merge commit) for renames
2754 sub git_get_path_by_hash {
2755 my $base = shift || return;
2756 my $hash = shift || return;
2758 local $/ = "\0";
2760 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2761 or return undef;
2762 while (my $line = <$fd>) {
2763 chomp $line;
2765 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2766 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2767 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2768 close $fd;
2769 return $1;
2772 close $fd;
2773 return undef;
2776 ## ......................................................................
2777 ## git utility functions, directly accessing git repository
2779 # get the value of config variable either from file named as the variable
2780 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
2781 # configuration variable in the repository config file.
2782 sub git_get_file_or_project_config {
2783 my ($path, $name) = @_;
2785 $git_dir = "$projectroot/$path";
2786 open my $fd, '<', "$git_dir/$name"
2787 or return git_get_project_config($name);
2788 my $conf = <$fd>;
2789 close $fd;
2790 if (defined $conf) {
2791 chomp $conf;
2793 return $conf;
2796 sub git_get_project_description {
2797 my $path = shift;
2798 return git_get_file_or_project_config($path, 'description');
2801 sub git_get_project_category {
2802 my $path = shift;
2803 return git_get_file_or_project_config($path, 'category');
2807 # supported formats:
2808 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2809 # - if its contents is a number, use it as tag weight,
2810 # - otherwise add a tag with weight 1
2811 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2812 # the same value multiple times increases tag weight
2813 # * `gitweb.ctag' multi-valued repo config variable
2814 sub git_get_project_ctags {
2815 my $project = shift;
2816 my $ctags = {};
2818 $git_dir = "$projectroot/$project";
2819 if (opendir my $dh, "$git_dir/ctags") {
2820 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2821 foreach my $tagfile (@files) {
2822 open my $ct, '<', $tagfile
2823 or next;
2824 my $val = <$ct>;
2825 chomp $val if $val;
2826 close $ct;
2828 (my $ctag = $tagfile) =~ s#.*/##;
2829 if ($val =~ /^\d+$/) {
2830 $ctags->{$ctag} = $val;
2831 } else {
2832 $ctags->{$ctag} = 1;
2835 closedir $dh;
2837 } elsif (open my $fh, '<', "$git_dir/ctags") {
2838 while (my $line = <$fh>) {
2839 chomp $line;
2840 $ctags->{$line}++ if $line;
2842 close $fh;
2844 } else {
2845 my $taglist = config_to_multi(git_get_project_config('ctag'));
2846 foreach my $tag (@$taglist) {
2847 $ctags->{$tag}++;
2851 return $ctags;
2854 # return hash, where keys are content tags ('ctags'),
2855 # and values are sum of weights of given tag in every project
2856 sub git_gather_all_ctags {
2857 my $projects = shift;
2858 my $ctags = {};
2860 foreach my $p (@$projects) {
2861 foreach my $ct (keys %{$p->{'ctags'}}) {
2862 $ctags->{$ct} += $p->{'ctags'}->{$ct};
2866 return $ctags;
2869 sub git_populate_project_tagcloud {
2870 my $ctags = shift;
2872 # First, merge different-cased tags; tags vote on casing
2873 my %ctags_lc;
2874 foreach (keys %$ctags) {
2875 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2876 if (not $ctags_lc{lc $_}->{topcount}
2877 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2878 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2879 $ctags_lc{lc $_}->{topname} = $_;
2883 my $cloud;
2884 my $matched = $input_params{'ctag'};
2885 if (eval { require HTML::TagCloud; 1; }) {
2886 $cloud = HTML::TagCloud->new;
2887 foreach my $ctag (sort keys %ctags_lc) {
2888 # Pad the title with spaces so that the cloud looks
2889 # less crammed.
2890 my $title = esc_html($ctags_lc{$ctag}->{topname});
2891 $title =~ s/ /&nbsp;/g;
2892 $title =~ s/^/&nbsp;/g;
2893 $title =~ s/$/&nbsp;/g;
2894 if (defined $matched && $matched eq $ctag) {
2895 $title = qq(<span class="match">$title</span>);
2897 $cloud->add($title, href(project=>undef, ctag=>$ctag),
2898 $ctags_lc{$ctag}->{count});
2900 } else {
2901 $cloud = {};
2902 foreach my $ctag (keys %ctags_lc) {
2903 my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
2904 if (defined $matched && $matched eq $ctag) {
2905 $title = qq(<span class="match">$title</span>);
2907 $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
2908 $cloud->{$ctag}{ctag} =
2909 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
2912 return $cloud;
2915 sub git_show_project_tagcloud {
2916 my ($cloud, $count) = @_;
2917 if (ref $cloud eq 'HTML::TagCloud') {
2918 return $cloud->html_and_css($count);
2919 } else {
2920 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
2921 return
2922 '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
2923 join (', ', map {
2924 $cloud->{$_}->{'ctag'}
2925 } splice(@tags, 0, $count)) .
2926 '</div>';
2930 sub git_get_project_url_list {
2931 my $path = shift;
2933 $git_dir = "$projectroot/$path";
2934 open my $fd, '<', "$git_dir/cloneurl"
2935 or return wantarray ?
2936 @{ config_to_multi(git_get_project_config('url')) } :
2937 config_to_multi(git_get_project_config('url'));
2938 my @git_project_url_list = map { chomp; $_ } <$fd>;
2939 close $fd;
2941 return wantarray ? @git_project_url_list : \@git_project_url_list;
2944 sub git_get_projects_list {
2945 my $filter = shift || '';
2946 my $paranoid = shift;
2947 my @list;
2949 if (-d $projects_list) {
2950 # search in directory
2951 my $dir = $projects_list;
2952 # remove the trailing "/"
2953 $dir =~ s!/+$!!;
2954 my $pfxlen = length("$dir");
2955 my $pfxdepth = ($dir =~ tr!/!!);
2956 # when filtering, search only given subdirectory
2957 if ($filter && !$paranoid) {
2958 $dir .= "/$filter";
2959 $dir =~ s!/+$!!;
2962 File::Find::find({
2963 follow_fast => 1, # follow symbolic links
2964 follow_skip => 2, # ignore duplicates
2965 dangling_symlinks => 0, # ignore dangling symlinks, silently
2966 wanted => sub {
2967 # global variables
2968 our $project_maxdepth;
2969 our $projectroot;
2970 # skip project-list toplevel, if we get it.
2971 return if (m!^[/.]$!);
2972 # only directories can be git repositories
2973 return unless (-d $_);
2974 # don't traverse too deep (Find is super slow on os x)
2975 # $project_maxdepth excludes depth of $projectroot
2976 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2977 $File::Find::prune = 1;
2978 return;
2981 my $path = substr($File::Find::name, $pfxlen + 1);
2982 # paranoidly only filter here
2983 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
2984 next;
2986 # we check related file in $projectroot
2987 if (check_export_ok("$projectroot/$path")) {
2988 push @list, { path => $path };
2989 $File::Find::prune = 1;
2992 }, "$dir");
2994 } elsif (-f $projects_list) {
2995 # read from file(url-encoded):
2996 # 'git%2Fgit.git Linus+Torvalds'
2997 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2998 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2999 open my $fd, '<', $projects_list or return;
3000 PROJECT:
3001 while (my $line = <$fd>) {
3002 chomp $line;
3003 my ($path, $owner) = split ' ', $line;
3004 $path = unescape($path);
3005 $owner = unescape($owner);
3006 if (!defined $path) {
3007 next;
3009 # if $filter is rpovided, check if $path begins with $filter
3010 if ($filter && $path !~ m!^\Q$filter\E/!) {
3011 next;
3013 if (check_export_ok("$projectroot/$path")) {
3014 my $pr = {
3015 path => $path,
3016 owner => to_utf8($owner),
3018 push @list, $pr;
3021 close $fd;
3023 return @list;
3026 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3027 # as side effects it sets 'forks' field to list of forks for forked projects
3028 sub filter_forks_from_projects_list {
3029 my $projects = shift;
3031 my %trie; # prefix tree of directories (path components)
3032 # generate trie out of those directories that might contain forks
3033 foreach my $pr (@$projects) {
3034 my $path = $pr->{'path'};
3035 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3036 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3037 next unless ($path); # skip '.git' repository: tests, git-instaweb
3038 next unless (-d "$projectroot/$path"); # containing directory exists
3039 $pr->{'forks'} = []; # there can be 0 or more forks of project
3041 # add to trie
3042 my @dirs = split('/', $path);
3043 # walk the trie, until either runs out of components or out of trie
3044 my $ref = \%trie;
3045 while (scalar @dirs &&
3046 exists($ref->{$dirs[0]})) {
3047 $ref = $ref->{shift @dirs};
3049 # create rest of trie structure from rest of components
3050 foreach my $dir (@dirs) {
3051 $ref = $ref->{$dir} = {};
3053 # create end marker, store $pr as a data
3054 $ref->{''} = $pr if (!exists $ref->{''});
3057 # filter out forks, by finding shortest prefix match for paths
3058 my @filtered;
3059 PROJECT:
3060 foreach my $pr (@$projects) {
3061 # trie lookup
3062 my $ref = \%trie;
3063 DIR:
3064 foreach my $dir (split('/', $pr->{'path'})) {
3065 if (exists $ref->{''}) {
3066 # found [shortest] prefix, is a fork - skip it
3067 push @{$ref->{''}{'forks'}}, $pr;
3068 next PROJECT;
3070 if (!exists $ref->{$dir}) {
3071 # not in trie, cannot have prefix, not a fork
3072 push @filtered, $pr;
3073 next PROJECT;
3075 # If the dir is there, we just walk one step down the trie.
3076 $ref = $ref->{$dir};
3078 # we ran out of trie
3079 # (shouldn't happen: it's either no match, or end marker)
3080 push @filtered, $pr;
3083 return @filtered;
3086 # note: fill_project_list_info must be run first,
3087 # for 'descr_long' and 'ctags' to be filled
3088 sub search_projects_list {
3089 my ($projlist, %opts) = @_;
3090 my $tagfilter = $opts{'tagfilter'};
3091 my $search_re = $opts{'search_regexp'};
3093 return @$projlist
3094 unless ($tagfilter || $search_re);
3096 # searching projects require filling to be run before it;
3097 fill_project_list_info($projlist,
3098 $tagfilter ? 'ctags' : (),
3099 $search_re ? ('path', 'descr') : ());
3100 my @projects;
3101 PROJECT:
3102 foreach my $pr (@$projlist) {
3104 if ($tagfilter) {
3105 next unless ref($pr->{'ctags'}) eq 'HASH';
3106 next unless
3107 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3110 if ($search_re) {
3111 next unless
3112 $pr->{'path'} =~ /$search_re/ ||
3113 $pr->{'descr_long'} =~ /$search_re/;
3116 push @projects, $pr;
3119 return @projects;
3122 our $gitweb_project_owner = undef;
3123 sub git_get_project_list_from_file {
3125 return if (defined $gitweb_project_owner);
3127 $gitweb_project_owner = {};
3128 # read from file (url-encoded):
3129 # 'git%2Fgit.git Linus+Torvalds'
3130 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3131 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3132 if (-f $projects_list) {
3133 open(my $fd, '<', $projects_list);
3134 while (my $line = <$fd>) {
3135 chomp $line;
3136 my ($pr, $ow) = split ' ', $line;
3137 $pr = unescape($pr);
3138 $ow = unescape($ow);
3139 $gitweb_project_owner->{$pr} = to_utf8($ow);
3141 close $fd;
3145 sub git_get_project_owner {
3146 my $project = shift;
3147 my $owner;
3149 return undef unless $project;
3150 $git_dir = "$projectroot/$project";
3152 if (!defined $gitweb_project_owner) {
3153 git_get_project_list_from_file();
3156 if (exists $gitweb_project_owner->{$project}) {
3157 $owner = $gitweb_project_owner->{$project};
3159 if (!defined $owner){
3160 $owner = git_get_project_config('owner');
3162 if (!defined $owner) {
3163 $owner = get_file_owner("$git_dir");
3166 return $owner;
3169 sub git_get_last_activity {
3170 my ($path) = @_;
3171 my $fd;
3173 $git_dir = "$projectroot/$path";
3174 open($fd, "-|", git_cmd(), 'for-each-ref',
3175 '--format=%(committer)',
3176 '--sort=-committerdate',
3177 '--count=1',
3178 'refs/heads') or return;
3179 my $most_recent = <$fd>;
3180 close $fd or return;
3181 if (defined $most_recent &&
3182 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
3183 my $timestamp = $1;
3184 my $age = time - $timestamp;
3185 return ($age, age_string($age));
3187 return (undef, undef);
3190 # Implementation note: when a single remote is wanted, we cannot use 'git
3191 # remote show -n' because that command always work (assuming it's a remote URL
3192 # if it's not defined), and we cannot use 'git remote show' because that would
3193 # try to make a network roundtrip. So the only way to find if that particular
3194 # remote is defined is to walk the list provided by 'git remote -v' and stop if
3195 # and when we find what we want.
3196 sub git_get_remotes_list {
3197 my $wanted = shift;
3198 my %remotes = ();
3200 open my $fd, '-|' , git_cmd(), 'remote', '-v';
3201 return unless $fd;
3202 while (my $remote = <$fd>) {
3203 chomp $remote;
3204 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3205 next if $wanted and not $remote eq $wanted;
3206 my ($url, $key) = ($1, $2);
3208 $remotes{$remote} ||= { 'heads' => () };
3209 $remotes{$remote}{$key} = $url;
3211 close $fd or return;
3212 return wantarray ? %remotes : \%remotes;
3215 # Takes a hash of remotes as first parameter and fills it by adding the
3216 # available remote heads for each of the indicated remotes.
3217 sub fill_remote_heads {
3218 my $remotes = shift;
3219 my @heads = map { "remotes/$_" } keys %$remotes;
3220 my @remoteheads = git_get_heads_list(undef, @heads);
3221 foreach my $remote (keys %$remotes) {
3222 $remotes->{$remote}{'heads'} = [ grep {
3223 $_->{'name'} =~ s!^$remote/!!
3224 } @remoteheads ];
3228 sub git_get_references {
3229 my $type = shift || "";
3230 my %refs;
3231 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3232 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3233 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3234 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
3235 or return;
3237 while (my $line = <$fd>) {
3238 chomp $line;
3239 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3240 if (defined $refs{$1}) {
3241 push @{$refs{$1}}, $2;
3242 } else {
3243 $refs{$1} = [ $2 ];
3247 close $fd or return;
3248 return \%refs;
3251 sub git_get_rev_name_tags {
3252 my $hash = shift || return undef;
3254 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3255 or return;
3256 my $name_rev = <$fd>;
3257 close $fd;
3259 if ($name_rev =~ m|^$hash tags/(.*)$|) {
3260 return $1;
3261 } else {
3262 # catches also '$hash undefined' output
3263 return undef;
3267 ## ----------------------------------------------------------------------
3268 ## parse to hash functions
3270 sub parse_date {
3271 my $epoch = shift;
3272 my $tz = shift || "-0000";
3274 my %date;
3275 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3276 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3277 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3278 $date{'hour'} = $hour;
3279 $date{'minute'} = $min;
3280 $date{'mday'} = $mday;
3281 $date{'day'} = $days[$wday];
3282 $date{'month'} = $months[$mon];
3283 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3284 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3285 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3286 $mday, $months[$mon], $hour ,$min;
3287 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3288 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3290 my ($tz_sign, $tz_hour, $tz_min) =
3291 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3292 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3293 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3294 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3295 $date{'hour_local'} = $hour;
3296 $date{'minute_local'} = $min;
3297 $date{'tz_local'} = $tz;
3298 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3299 1900+$year, $mon+1, $mday,
3300 $hour, $min, $sec, $tz);
3301 return %date;
3304 sub parse_tag {
3305 my $tag_id = shift;
3306 my %tag;
3307 my @comment;
3309 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3310 $tag{'id'} = $tag_id;
3311 while (my $line = <$fd>) {
3312 chomp $line;
3313 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3314 $tag{'object'} = $1;
3315 } elsif ($line =~ m/^type (.+)$/) {
3316 $tag{'type'} = $1;
3317 } elsif ($line =~ m/^tag (.+)$/) {
3318 $tag{'name'} = $1;
3319 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3320 $tag{'author'} = $1;
3321 $tag{'author_epoch'} = $2;
3322 $tag{'author_tz'} = $3;
3323 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3324 $tag{'author_name'} = $1;
3325 $tag{'author_email'} = $2;
3326 } else {
3327 $tag{'author_name'} = $tag{'author'};
3329 } elsif ($line =~ m/--BEGIN/) {
3330 push @comment, $line;
3331 last;
3332 } elsif ($line eq "") {
3333 last;
3336 push @comment, <$fd>;
3337 $tag{'comment'} = \@comment;
3338 close $fd or return;
3339 if (!defined $tag{'name'}) {
3340 return
3342 return %tag
3345 sub parse_commit_text {
3346 my ($commit_text, $withparents) = @_;
3347 my @commit_lines = split '\n', $commit_text;
3348 my %co;
3350 pop @commit_lines; # Remove '\0'
3352 if (! @commit_lines) {
3353 return;
3356 my $header = shift @commit_lines;
3357 if ($header !~ m/^[0-9a-fA-F]{40}/) {
3358 return;
3360 ($co{'id'}, my @parents) = split ' ', $header;
3361 while (my $line = shift @commit_lines) {
3362 last if $line eq "\n";
3363 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3364 $co{'tree'} = $1;
3365 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3366 push @parents, $1;
3367 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3368 $co{'author'} = to_utf8($1);
3369 $co{'author_epoch'} = $2;
3370 $co{'author_tz'} = $3;
3371 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3372 $co{'author_name'} = $1;
3373 $co{'author_email'} = $2;
3374 } else {
3375 $co{'author_name'} = $co{'author'};
3377 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3378 $co{'committer'} = to_utf8($1);
3379 $co{'committer_epoch'} = $2;
3380 $co{'committer_tz'} = $3;
3381 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3382 $co{'committer_name'} = $1;
3383 $co{'committer_email'} = $2;
3384 } else {
3385 $co{'committer_name'} = $co{'committer'};
3389 if (!defined $co{'tree'}) {
3390 return;
3392 $co{'parents'} = \@parents;
3393 $co{'parent'} = $parents[0];
3395 foreach my $title (@commit_lines) {
3396 $title =~ s/^ //;
3397 if ($title ne "") {
3398 $co{'title'} = chop_str($title, 80, 5);
3399 # remove leading stuff of merges to make the interesting part visible
3400 if (length($title) > 50) {
3401 $title =~ s/^Automatic //;
3402 $title =~ s/^merge (of|with) /Merge ... /i;
3403 if (length($title) > 50) {
3404 $title =~ s/(http|rsync):\/\///;
3406 if (length($title) > 50) {
3407 $title =~ s/(master|www|rsync)\.//;
3409 if (length($title) > 50) {
3410 $title =~ s/kernel.org:?//;
3412 if (length($title) > 50) {
3413 $title =~ s/\/pub\/scm//;
3416 $co{'title_short'} = chop_str($title, 50, 5);
3417 last;
3420 if (! defined $co{'title'} || $co{'title'} eq "") {
3421 $co{'title'} = $co{'title_short'} = '(no commit message)';
3423 # remove added spaces
3424 foreach my $line (@commit_lines) {
3425 $line =~ s/^ //;
3427 $co{'comment'} = \@commit_lines;
3429 my $age = time - $co{'committer_epoch'};
3430 $co{'age'} = $age;
3431 $co{'age_string'} = age_string($age);
3432 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3433 if ($age > 60*60*24*7*2) {
3434 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3435 $co{'age_string_age'} = $co{'age_string'};
3436 } else {
3437 $co{'age_string_date'} = $co{'age_string'};
3438 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3440 return %co;
3443 sub parse_commit {
3444 my ($commit_id) = @_;
3445 my %co;
3447 local $/ = "\0";
3449 open my $fd, "-|", git_cmd(), "rev-list",
3450 "--parents",
3451 "--header",
3452 "--max-count=1",
3453 $commit_id,
3454 "--",
3455 or die_error(500, "Open git-rev-list failed");
3456 %co = parse_commit_text(<$fd>, 1);
3457 close $fd;
3459 return %co;
3462 sub parse_commits {
3463 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3464 my @cos;
3466 $maxcount ||= 1;
3467 $skip ||= 0;
3469 local $/ = "\0";
3471 open my $fd, "-|", git_cmd(), "rev-list",
3472 "--header",
3473 @args,
3474 ("--max-count=" . $maxcount),
3475 ("--skip=" . $skip),
3476 @extra_options,
3477 $commit_id,
3478 "--",
3479 ($filename ? ($filename) : ())
3480 or die_error(500, "Open git-rev-list failed");
3481 while (my $line = <$fd>) {
3482 my %co = parse_commit_text($line);
3483 push @cos, \%co;
3485 close $fd;
3487 return wantarray ? @cos : \@cos;
3490 # parse line of git-diff-tree "raw" output
3491 sub parse_difftree_raw_line {
3492 my $line = shift;
3493 my %res;
3495 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
3496 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
3497 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3498 $res{'from_mode'} = $1;
3499 $res{'to_mode'} = $2;
3500 $res{'from_id'} = $3;
3501 $res{'to_id'} = $4;
3502 $res{'status'} = $5;
3503 $res{'similarity'} = $6;
3504 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3505 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3506 } else {
3507 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3510 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3511 # combined diff (for merge commit)
3512 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3513 $res{'nparents'} = length($1);
3514 $res{'from_mode'} = [ split(' ', $2) ];
3515 $res{'to_mode'} = pop @{$res{'from_mode'}};
3516 $res{'from_id'} = [ split(' ', $3) ];
3517 $res{'to_id'} = pop @{$res{'from_id'}};
3518 $res{'status'} = [ split('', $4) ];
3519 $res{'to_file'} = unquote($5);
3521 # 'c512b523472485aef4fff9e57b229d9d243c967f'
3522 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3523 $res{'commit'} = $1;
3526 return wantarray ? %res : \%res;
3529 # wrapper: return parsed line of git-diff-tree "raw" output
3530 # (the argument might be raw line, or parsed info)
3531 sub parsed_difftree_line {
3532 my $line_or_ref = shift;
3534 if (ref($line_or_ref) eq "HASH") {
3535 # pre-parsed (or generated by hand)
3536 return $line_or_ref;
3537 } else {
3538 return parse_difftree_raw_line($line_or_ref);
3542 # parse line of git-ls-tree output
3543 sub parse_ls_tree_line {
3544 my $line = shift;
3545 my %opts = @_;
3546 my %res;
3548 if ($opts{'-l'}) {
3549 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3550 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3552 $res{'mode'} = $1;
3553 $res{'type'} = $2;
3554 $res{'hash'} = $3;
3555 $res{'size'} = $4;
3556 if ($opts{'-z'}) {
3557 $res{'name'} = $5;
3558 } else {
3559 $res{'name'} = unquote($5);
3561 } else {
3562 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3563 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3565 $res{'mode'} = $1;
3566 $res{'type'} = $2;
3567 $res{'hash'} = $3;
3568 if ($opts{'-z'}) {
3569 $res{'name'} = $4;
3570 } else {
3571 $res{'name'} = unquote($4);
3575 return wantarray ? %res : \%res;
3578 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3579 sub parse_from_to_diffinfo {
3580 my ($diffinfo, $from, $to, @parents) = @_;
3582 if ($diffinfo->{'nparents'}) {
3583 # combined diff
3584 $from->{'file'} = [];
3585 $from->{'href'} = [];
3586 fill_from_file_info($diffinfo, @parents)
3587 unless exists $diffinfo->{'from_file'};
3588 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3589 $from->{'file'}[$i] =
3590 defined $diffinfo->{'from_file'}[$i] ?
3591 $diffinfo->{'from_file'}[$i] :
3592 $diffinfo->{'to_file'};
3593 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3594 $from->{'href'}[$i] = href(action=>"blob",
3595 hash_base=>$parents[$i],
3596 hash=>$diffinfo->{'from_id'}[$i],
3597 file_name=>$from->{'file'}[$i]);
3598 } else {
3599 $from->{'href'}[$i] = undef;
3602 } else {
3603 # ordinary (not combined) diff
3604 $from->{'file'} = $diffinfo->{'from_file'};
3605 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3606 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3607 hash=>$diffinfo->{'from_id'},
3608 file_name=>$from->{'file'});
3609 } else {
3610 delete $from->{'href'};
3614 $to->{'file'} = $diffinfo->{'to_file'};
3615 if (!is_deleted($diffinfo)) { # file exists in result
3616 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3617 hash=>$diffinfo->{'to_id'},
3618 file_name=>$to->{'file'});
3619 } else {
3620 delete $to->{'href'};
3624 ## ......................................................................
3625 ## parse to array of hashes functions
3627 sub git_get_heads_list {
3628 my ($limit, @classes) = @_;
3629 @classes = ('heads') unless @classes;
3630 my @patterns = map { "refs/$_" } @classes;
3631 my @headslist;
3633 open my $fd, '-|', git_cmd(), 'for-each-ref',
3634 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3635 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3636 @patterns
3637 or return;
3638 while (my $line = <$fd>) {
3639 my %ref_item;
3641 chomp $line;
3642 my ($refinfo, $committerinfo) = split(/\0/, $line);
3643 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3644 my ($committer, $epoch, $tz) =
3645 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3646 $ref_item{'fullname'} = $name;
3647 $name =~ s!^refs/(?:head|remote)s/!!;
3649 $ref_item{'name'} = $name;
3650 $ref_item{'id'} = $hash;
3651 $ref_item{'title'} = $title || '(no commit message)';
3652 $ref_item{'epoch'} = $epoch;
3653 if ($epoch) {
3654 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3655 } else {
3656 $ref_item{'age'} = "unknown";
3659 push @headslist, \%ref_item;
3661 close $fd;
3663 return wantarray ? @headslist : \@headslist;
3666 sub git_get_tags_list {
3667 my $limit = shift;
3668 my @tagslist;
3670 open my $fd, '-|', git_cmd(), 'for-each-ref',
3671 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3672 '--format=%(objectname) %(objecttype) %(refname) '.
3673 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3674 'refs/tags'
3675 or return;
3676 while (my $line = <$fd>) {
3677 my %ref_item;
3679 chomp $line;
3680 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3681 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3682 my ($creator, $epoch, $tz) =
3683 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3684 $ref_item{'fullname'} = $name;
3685 $name =~ s!^refs/tags/!!;
3687 $ref_item{'type'} = $type;
3688 $ref_item{'id'} = $id;
3689 $ref_item{'name'} = $name;
3690 if ($type eq "tag") {
3691 $ref_item{'subject'} = $title;
3692 $ref_item{'reftype'} = $reftype;
3693 $ref_item{'refid'} = $refid;
3694 } else {
3695 $ref_item{'reftype'} = $type;
3696 $ref_item{'refid'} = $id;
3699 if ($type eq "tag" || $type eq "commit") {
3700 $ref_item{'epoch'} = $epoch;
3701 if ($epoch) {
3702 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3703 } else {
3704 $ref_item{'age'} = "unknown";
3708 push @tagslist, \%ref_item;
3710 close $fd;
3712 return wantarray ? @tagslist : \@tagslist;
3715 ## ----------------------------------------------------------------------
3716 ## filesystem-related functions
3718 sub get_file_owner {
3719 my $path = shift;
3721 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3722 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3723 if (!defined $gcos) {
3724 return undef;
3726 my $owner = $gcos;
3727 $owner =~ s/[,;].*$//;
3728 return to_utf8($owner);
3731 # assume that file exists
3732 sub insert_file {
3733 my $filename = shift;
3735 open my $fd, '<', $filename;
3736 print map { to_utf8($_) } <$fd>;
3737 close $fd;
3740 ## ......................................................................
3741 ## mimetype related functions
3743 sub mimetype_guess_file {
3744 my $filename = shift;
3745 my $mimemap = shift;
3746 -r $mimemap or return undef;
3748 my %mimemap;
3749 open(my $mh, '<', $mimemap) or return undef;
3750 while (<$mh>) {
3751 next if m/^#/; # skip comments
3752 my ($mimetype, @exts) = split(/\s+/);
3753 foreach my $ext (@exts) {
3754 $mimemap{$ext} = $mimetype;
3757 close($mh);
3759 $filename =~ /\.([^.]*)$/;
3760 return $mimemap{$1};
3763 sub mimetype_guess {
3764 my $filename = shift;
3765 my $mime;
3766 $filename =~ /\./ or return undef;
3768 if ($mimetypes_file) {
3769 my $file = $mimetypes_file;
3770 if ($file !~ m!^/!) { # if it is relative path
3771 # it is relative to project
3772 $file = "$projectroot/$project/$file";
3774 $mime = mimetype_guess_file($filename, $file);
3776 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3777 return $mime;
3780 sub blob_mimetype {
3781 my $fd = shift;
3782 my $filename = shift;
3784 if ($filename) {
3785 my $mime = mimetype_guess($filename);
3786 $mime and return $mime;
3789 # just in case
3790 return $default_blob_plain_mimetype unless $fd;
3792 if (-T $fd) {
3793 return 'text/plain';
3794 } elsif (! $filename) {
3795 return 'application/octet-stream';
3796 } elsif ($filename =~ m/\.png$/i) {
3797 return 'image/png';
3798 } elsif ($filename =~ m/\.gif$/i) {
3799 return 'image/gif';
3800 } elsif ($filename =~ m/\.jpe?g$/i) {
3801 return 'image/jpeg';
3802 } else {
3803 return 'application/octet-stream';
3807 sub blob_contenttype {
3808 my ($fd, $file_name, $type) = @_;
3810 $type ||= blob_mimetype($fd, $file_name);
3811 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3812 $type .= "; charset=$default_text_plain_charset";
3815 return $type;
3818 # guess file syntax for syntax highlighting; return undef if no highlighting
3819 # the name of syntax can (in the future) depend on syntax highlighter used
3820 sub guess_file_syntax {
3821 my ($highlight, $mimetype, $file_name) = @_;
3822 return undef unless ($highlight && defined $file_name);
3823 my $basename = basename($file_name, '.in');
3824 return $highlight_basename{$basename}
3825 if exists $highlight_basename{$basename};
3827 $basename =~ /\.([^.]*)$/;
3828 my $ext = $1 or return undef;
3829 return $highlight_ext{$ext}
3830 if exists $highlight_ext{$ext};
3832 return undef;
3835 # run highlighter and return FD of its output,
3836 # or return original FD if no highlighting
3837 sub run_highlighter {
3838 my ($fd, $highlight, $syntax) = @_;
3839 return $fd unless ($highlight && defined $syntax);
3841 close $fd;
3842 open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3843 quote_command($highlight_bin).
3844 " --replace-tabs=8 --fragment --syntax $syntax |"
3845 or die_error(500, "Couldn't open file or run syntax highlighter");
3846 return $fd;
3849 ## ======================================================================
3850 ## functions printing HTML: header, footer, error page
3852 sub get_page_title {
3853 my $title = to_utf8($site_name);
3855 unless (defined $project) {
3856 if (defined $project_filter) {
3857 $title .= " - projects in '" . esc_path($project_filter) . "'";
3859 return $title;
3861 $title .= " - " . to_utf8($project);
3863 return $title unless (defined $action);
3864 $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3866 return $title unless (defined $file_name);
3867 $title .= " - " . esc_path($file_name);
3868 if ($action eq "tree" && $file_name !~ m|/$|) {
3869 $title .= "/";
3872 return $title;
3875 sub get_content_type_html {
3876 # require explicit support from the UA if we are to send the page as
3877 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3878 # we have to do this because MSIE sometimes globs '*/*', pretending to
3879 # support xhtml+xml but choking when it gets what it asked for.
3880 if (defined $cgi->http('HTTP_ACCEPT') &&
3881 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3882 $cgi->Accept('application/xhtml+xml') != 0) {
3883 return 'application/xhtml+xml';
3884 } else {
3885 return 'text/html';
3889 sub print_feed_meta {
3890 if (defined $project) {
3891 my %href_params = get_feed_info();
3892 if (!exists $href_params{'-title'}) {
3893 $href_params{'-title'} = 'log';
3896 foreach my $format (qw(RSS Atom)) {
3897 my $type = lc($format);
3898 my %link_attr = (
3899 '-rel' => 'alternate',
3900 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3901 '-type' => "application/$type+xml"
3904 $href_params{'extra_options'} = undef;
3905 $href_params{'action'} = $type;
3906 $link_attr{'-href'} = href(%href_params);
3907 print "<link ".
3908 "rel=\"$link_attr{'-rel'}\" ".
3909 "title=\"$link_attr{'-title'}\" ".
3910 "href=\"$link_attr{'-href'}\" ".
3911 "type=\"$link_attr{'-type'}\" ".
3912 "/>\n";
3914 $href_params{'extra_options'} = '--no-merges';
3915 $link_attr{'-href'} = href(%href_params);
3916 $link_attr{'-title'} .= ' (no merges)';
3917 print "<link ".
3918 "rel=\"$link_attr{'-rel'}\" ".
3919 "title=\"$link_attr{'-title'}\" ".
3920 "href=\"$link_attr{'-href'}\" ".
3921 "type=\"$link_attr{'-type'}\" ".
3922 "/>\n";
3925 } else {
3926 printf('<link rel="alternate" title="%s projects list" '.
3927 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3928 esc_attr($site_name), href(project=>undef, action=>"project_index"));
3929 printf('<link rel="alternate" title="%s projects feeds" '.
3930 'href="%s" type="text/x-opml" />'."\n",
3931 esc_attr($site_name), href(project=>undef, action=>"opml"));
3935 sub print_header_links {
3936 my $status = shift;
3938 # print out each stylesheet that exist, providing backwards capability
3939 # for those people who defined $stylesheet in a config file
3940 if (defined $stylesheet) {
3941 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3942 } else {
3943 foreach my $stylesheet (@stylesheets) {
3944 next unless $stylesheet;
3945 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3948 print_feed_meta()
3949 if ($status eq '200 OK');
3950 if (defined $favicon) {
3951 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3955 sub print_nav_breadcrumbs_path {
3956 my $dirprefix = undef;
3957 while (my $part = shift) {
3958 $dirprefix .= "/" if defined $dirprefix;
3959 $dirprefix .= $part;
3960 print $cgi->a({-href => href(project => undef,
3961 project_filter => $dirprefix,
3962 action => "project_list")},
3963 esc_html($part)) . " / ";
3967 sub print_nav_breadcrumbs {
3968 my %opts = @_;
3970 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3971 if (defined $project) {
3972 my @dirname = split '/', $project;
3973 my $projectbasename = pop @dirname;
3974 print_nav_breadcrumbs_path(@dirname);
3975 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
3976 if (defined $action) {
3977 my $action_print = $action ;
3978 if (defined $opts{-action_extra}) {
3979 $action_print = $cgi->a({-href => href(action=>$action)},
3980 $action);
3982 print " / $action_print";
3984 if (defined $opts{-action_extra}) {
3985 print " / $opts{-action_extra}";
3987 print "\n";
3988 } elsif (defined $project_filter) {
3989 print_nav_breadcrumbs_path(split '/', $project_filter);
3993 sub print_search_form {
3994 if (!defined $searchtext) {
3995 $searchtext = "";
3997 my $search_hash;
3998 if (defined $hash_base) {
3999 $search_hash = $hash_base;
4000 } elsif (defined $hash) {
4001 $search_hash = $hash;
4002 } else {
4003 $search_hash = "HEAD";
4005 my $action = $my_uri;
4006 my $use_pathinfo = gitweb_check_feature('pathinfo');
4007 if ($use_pathinfo) {
4008 $action .= "/".esc_url($project);
4010 print $cgi->startform(-method => "get", -action => $action) .
4011 "<div class=\"search\">\n" .
4012 (!$use_pathinfo &&
4013 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
4014 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
4015 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
4016 $cgi->popup_menu(-name => 'st', -default => 'commit',
4017 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
4018 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
4019 " search:\n",
4020 $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
4021 "<span title=\"Extended regular expression\">" .
4022 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
4023 -checked => $search_use_regexp) .
4024 "</span>" .
4025 "</div>" .
4026 $cgi->end_form() . "\n";
4029 sub git_header_html {
4030 my $status = shift || "200 OK";
4031 my $expires = shift;
4032 my %opts = @_;
4034 my $title = get_page_title();
4035 my $content_type = get_content_type_html();
4036 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
4037 -status=> $status, -expires => $expires)
4038 unless ($opts{'-no_http_header'});
4039 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
4040 print <<EOF;
4041 <?xml version="1.0" encoding="utf-8"?>
4042 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4043 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
4044 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
4045 <!-- git core binaries version $git_version -->
4046 <head>
4047 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
4048 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
4049 <meta name="robots" content="index, nofollow"/>
4050 <title>$title</title>
4052 # the stylesheet, favicon etc urls won't work correctly with path_info
4053 # unless we set the appropriate base URL
4054 if ($ENV{'PATH_INFO'}) {
4055 print "<base href=\"".esc_url($base_url)."\" />\n";
4057 print_header_links($status);
4059 if (defined $site_html_head_string) {
4060 print to_utf8($site_html_head_string);
4063 print "</head>\n" .
4064 "<body>\n";
4066 if (defined $site_header && -f $site_header) {
4067 insert_file($site_header);
4070 print "<div class=\"page_header\">\n";
4071 if (defined $logo) {
4072 print $cgi->a({-href => esc_url($logo_url),
4073 -title => $logo_label},
4074 $cgi->img({-src => esc_url($logo),
4075 -width => 72, -height => 27,
4076 -alt => "git",
4077 -class => "logo"}));
4079 print_nav_breadcrumbs(%opts);
4080 print "</div>\n";
4082 my $have_search = gitweb_check_feature('search');
4083 if (defined $project && $have_search) {
4084 print_search_form();
4088 sub git_footer_html {
4089 my $feed_class = 'rss_logo';
4091 print "<div class=\"page_footer\">\n";
4092 if (defined $project) {
4093 my $descr = git_get_project_description($project);
4094 if (defined $descr) {
4095 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
4098 my %href_params = get_feed_info();
4099 if (!%href_params) {
4100 $feed_class .= ' generic';
4102 $href_params{'-title'} ||= 'log';
4104 foreach my $format (qw(RSS Atom)) {
4105 $href_params{'action'} = lc($format);
4106 print $cgi->a({-href => href(%href_params),
4107 -title => "$href_params{'-title'} $format feed",
4108 -class => $feed_class}, $format)."\n";
4111 } else {
4112 print $cgi->a({-href => href(project=>undef, action=>"opml",
4113 project_filter => $project_filter),
4114 -class => $feed_class}, "OPML") . " ";
4115 print $cgi->a({-href => href(project=>undef, action=>"project_index",
4116 project_filter => $project_filter),
4117 -class => $feed_class}, "TXT") . "\n";
4119 print "</div>\n"; # class="page_footer"
4121 if (defined $t0 && gitweb_check_feature('timed')) {
4122 print "<div id=\"generating_info\">\n";
4123 print 'This page took '.
4124 '<span id="generating_time" class="time_span">'.
4125 tv_interval($t0, [ gettimeofday() ]).
4126 ' seconds </span>'.
4127 ' and '.
4128 '<span id="generating_cmd">'.
4129 $number_of_git_cmds.
4130 '</span> git commands '.
4131 " to generate.\n";
4132 print "</div>\n"; # class="page_footer"
4135 if (defined $site_footer && -f $site_footer) {
4136 insert_file($site_footer);
4139 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
4140 if (defined $action &&
4141 $action eq 'blame_incremental') {
4142 print qq!<script type="text/javascript">\n!.
4143 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
4144 qq! "!. href() .qq!");\n!.
4145 qq!</script>\n!;
4146 } else {
4147 my ($jstimezone, $tz_cookie, $datetime_class) =
4148 gitweb_get_feature('javascript-timezone');
4150 print qq!<script type="text/javascript">\n!.
4151 qq!window.onload = function () {\n!;
4152 if (gitweb_check_feature('javascript-actions')) {
4153 print qq! fixLinks();\n!;
4155 if ($jstimezone && $tz_cookie && $datetime_class) {
4156 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
4157 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
4159 print qq!};\n!.
4160 qq!</script>\n!;
4163 print "</body>\n" .
4164 "</html>";
4167 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
4168 # Example: die_error(404, 'Hash not found')
4169 # By convention, use the following status codes (as defined in RFC 2616):
4170 # 400: Invalid or missing CGI parameters, or
4171 # requested object exists but has wrong type.
4172 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4173 # this server or project.
4174 # 404: Requested object/revision/project doesn't exist.
4175 # 500: The server isn't configured properly, or
4176 # an internal error occurred (e.g. failed assertions caused by bugs), or
4177 # an unknown error occurred (e.g. the git binary died unexpectedly).
4178 # 503: The server is currently unavailable (because it is overloaded,
4179 # or down for maintenance). Generally, this is a temporary state.
4180 sub die_error {
4181 my $status = shift || 500;
4182 my $error = esc_html(shift) || "Internal Server Error";
4183 my $extra = shift;
4184 my %opts = @_;
4186 my %http_responses = (
4187 400 => '400 Bad Request',
4188 403 => '403 Forbidden',
4189 404 => '404 Not Found',
4190 500 => '500 Internal Server Error',
4191 503 => '503 Service Unavailable',
4193 git_header_html($http_responses{$status}, undef, %opts);
4194 print <<EOF;
4195 <div class="page_body">
4196 <br /><br />
4197 $status - $error
4198 <br />
4200 if (defined $extra) {
4201 print "<hr />\n" .
4202 "$extra\n";
4204 print "</div>\n";
4206 git_footer_html();
4207 goto DONE_GITWEB
4208 unless ($opts{'-error_handler'});
4211 ## ----------------------------------------------------------------------
4212 ## functions printing or outputting HTML: navigation
4214 sub git_print_page_nav {
4215 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4216 $extra = '' if !defined $extra; # pager or formats
4218 my @navs = qw(summary shortlog log commit commitdiff tree);
4219 if ($suppress) {
4220 @navs = grep { $_ ne $suppress } @navs;
4223 my %arg = map { $_ => {action=>$_} } @navs;
4224 if (defined $head) {
4225 for (qw(commit commitdiff)) {
4226 $arg{$_}{'hash'} = $head;
4228 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4229 for (qw(shortlog log)) {
4230 $arg{$_}{'hash'} = $head;
4235 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
4236 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
4238 my @actions = gitweb_get_feature('actions');
4239 my %repl = (
4240 '%' => '%',
4241 'n' => $project, # project name
4242 'f' => $git_dir, # project path within filesystem
4243 'h' => $treehead || '', # current hash ('h' parameter)
4244 'b' => $treebase || '', # hash base ('hb' parameter)
4246 while (@actions) {
4247 my ($label, $link, $pos) = splice(@actions,0,3);
4248 # insert
4249 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
4250 # munch munch
4251 $link =~ s/%([%nfhb])/$repl{$1}/g;
4252 $arg{$label}{'_href'} = $link;
4255 print "<div class=\"page_nav\">\n" .
4256 (join " | ",
4257 map { $_ eq $current ?
4258 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
4259 } @navs);
4260 print "<br/>\n$extra<br/>\n" .
4261 "</div>\n";
4264 # returns a submenu for the nagivation of the refs views (tags, heads,
4265 # remotes) with the current view disabled and the remotes view only
4266 # available if the feature is enabled
4267 sub format_ref_views {
4268 my ($current) = @_;
4269 my @ref_views = qw{tags heads};
4270 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
4271 return join " | ", map {
4272 $_ eq $current ? $_ :
4273 $cgi->a({-href => href(action=>$_)}, $_)
4274 } @ref_views
4277 sub format_paging_nav {
4278 my ($action, $page, $has_next_link) = @_;
4279 my $paging_nav;
4282 if ($page > 0) {
4283 $paging_nav .=
4284 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4285 " &sdot; " .
4286 $cgi->a({-href => href(-replay=>1, page=>$page-1),
4287 -accesskey => "p", -title => "Alt-p"}, "prev");
4288 } else {
4289 $paging_nav .= "first &sdot; prev";
4292 if ($has_next_link) {
4293 $paging_nav .= " &sdot; " .
4294 $cgi->a({-href => href(-replay=>1, page=>$page+1),
4295 -accesskey => "n", -title => "Alt-n"}, "next");
4296 } else {
4297 $paging_nav .= " &sdot; next";
4300 return $paging_nav;
4303 ## ......................................................................
4304 ## functions printing or outputting HTML: div
4306 sub git_print_header_div {
4307 my ($action, $title, $hash, $hash_base) = @_;
4308 my %args = ();
4310 $args{'action'} = $action;
4311 $args{'hash'} = $hash if $hash;
4312 $args{'hash_base'} = $hash_base if $hash_base;
4314 print "<div class=\"header\">\n" .
4315 $cgi->a({-href => href(%args), -class => "title"},
4316 $title ? $title : $action) .
4317 "\n</div>\n";
4320 sub format_repo_url {
4321 my ($name, $url) = @_;
4322 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4325 # Group output by placing it in a DIV element and adding a header.
4326 # Options for start_div() can be provided by passing a hash reference as the
4327 # first parameter to the function.
4328 # Options to git_print_header_div() can be provided by passing an array
4329 # reference. This must follow the options to start_div if they are present.
4330 # The content can be a scalar, which is output as-is, a scalar reference, which
4331 # is output after html escaping, an IO handle passed either as *handle or
4332 # *handle{IO}, or a function reference. In the latter case all following
4333 # parameters will be taken as argument to the content function call.
4334 sub git_print_section {
4335 my ($div_args, $header_args, $content);
4336 my $arg = shift;
4337 if (ref($arg) eq 'HASH') {
4338 $div_args = $arg;
4339 $arg = shift;
4341 if (ref($arg) eq 'ARRAY') {
4342 $header_args = $arg;
4343 $arg = shift;
4345 $content = $arg;
4347 print $cgi->start_div($div_args);
4348 git_print_header_div(@$header_args);
4350 if (ref($content) eq 'CODE') {
4351 $content->(@_);
4352 } elsif (ref($content) eq 'SCALAR') {
4353 print esc_html($$content);
4354 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4355 print <$content>;
4356 } elsif (!ref($content) && defined($content)) {
4357 print $content;
4360 print $cgi->end_div;
4363 sub format_timestamp_html {
4364 my $date = shift;
4365 my $strtime = $date->{'rfc2822'};
4367 my (undef, undef, $datetime_class) =
4368 gitweb_get_feature('javascript-timezone');
4369 if ($datetime_class) {
4370 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
4373 my $localtime_format = '(%02d:%02d %s)';
4374 if ($date->{'hour_local'} < 6) {
4375 $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
4377 $strtime .= ' ' .
4378 sprintf($localtime_format,
4379 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
4381 return $strtime;
4384 # Outputs the author name and date in long form
4385 sub git_print_authorship {
4386 my $co = shift;
4387 my %opts = @_;
4388 my $tag = $opts{-tag} || 'div';
4389 my $author = $co->{'author_name'};
4391 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4392 print "<$tag class=\"author_date\">" .
4393 format_search_author($author, "author", esc_html($author)) .
4394 " [".format_timestamp_html(\%ad)."]".
4395 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
4396 "</$tag>\n";
4399 # Outputs table rows containing the full author or committer information,
4400 # in the format expected for 'commit' view (& similar).
4401 # Parameters are a commit hash reference, followed by the list of people
4402 # to output information for. If the list is empty it defaults to both
4403 # author and committer.
4404 sub git_print_authorship_rows {
4405 my $co = shift;
4406 # too bad we can't use @people = @_ || ('author', 'committer')
4407 my @people = @_;
4408 @people = ('author', 'committer') unless @people;
4409 foreach my $who (@people) {
4410 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4411 print "<tr><td>$who</td><td>" .
4412 format_search_author($co->{"${who}_name"}, $who,
4413 esc_html($co->{"${who}_name"})) . " " .
4414 format_search_author($co->{"${who}_email"}, $who,
4415 esc_html("<" . $co->{"${who}_email"} . ">")) .
4416 "</td><td rowspan=\"2\">" .
4417 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4418 "</td></tr>\n" .
4419 "<tr>" .
4420 "<td></td><td>" .
4421 format_timestamp_html(\%wd) .
4422 "</td>" .
4423 "</tr>\n";
4427 sub git_print_page_path {
4428 my $name = shift;
4429 my $type = shift;
4430 my $hb = shift;
4433 print "<div class=\"page_path\">";
4434 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4435 -title => 'tree root'}, to_utf8("[$project]"));
4436 print " / ";
4437 if (defined $name) {
4438 my @dirname = split '/', $name;
4439 my $basename = pop @dirname;
4440 my $fullname = '';
4442 foreach my $dir (@dirname) {
4443 $fullname .= ($fullname ? '/' : '') . $dir;
4444 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4445 hash_base=>$hb),
4446 -title => $fullname}, esc_path($dir));
4447 print " / ";
4449 if (defined $type && $type eq 'blob') {
4450 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4451 hash_base=>$hb),
4452 -title => $name}, esc_path($basename));
4453 } elsif (defined $type && $type eq 'tree') {
4454 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4455 hash_base=>$hb),
4456 -title => $name}, esc_path($basename));
4457 print " / ";
4458 } else {
4459 print esc_path($basename);
4462 print "<br/></div>\n";
4465 sub git_print_log {
4466 my $log = shift;
4467 my %opts = @_;
4469 if ($opts{'-remove_title'}) {
4470 # remove title, i.e. first line of log
4471 shift @$log;
4473 # remove leading empty lines
4474 while (defined $log->[0] && $log->[0] eq "") {
4475 shift @$log;
4478 # print log
4479 my $signoff = 0;
4480 my $empty = 0;
4481 foreach my $line (@$log) {
4482 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4483 $signoff = 1;
4484 $empty = 0;
4485 if (! $opts{'-remove_signoff'}) {
4486 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4487 next;
4488 } else {
4489 # remove signoff lines
4490 next;
4492 } else {
4493 $signoff = 0;
4496 # print only one empty line
4497 # do not print empty line after signoff
4498 if ($line eq "") {
4499 next if ($empty || $signoff);
4500 $empty = 1;
4501 } else {
4502 $empty = 0;
4505 print format_log_line_html($line) . "<br/>\n";
4508 if ($opts{'-final_empty_line'}) {
4509 # end with single empty line
4510 print "<br/>\n" unless $empty;
4514 # return link target (what link points to)
4515 sub git_get_link_target {
4516 my $hash = shift;
4517 my $link_target;
4519 # read link
4520 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4521 or return;
4523 local $/ = undef;
4524 $link_target = <$fd>;
4526 close $fd
4527 or return;
4529 return $link_target;
4532 # given link target, and the directory (basedir) the link is in,
4533 # return target of link relative to top directory (top tree);
4534 # return undef if it is not possible (including absolute links).
4535 sub normalize_link_target {
4536 my ($link_target, $basedir) = @_;
4538 # absolute symlinks (beginning with '/') cannot be normalized
4539 return if (substr($link_target, 0, 1) eq '/');
4541 # normalize link target to path from top (root) tree (dir)
4542 my $path;
4543 if ($basedir) {
4544 $path = $basedir . '/' . $link_target;
4545 } else {
4546 # we are in top (root) tree (dir)
4547 $path = $link_target;
4550 # remove //, /./, and /../
4551 my @path_parts;
4552 foreach my $part (split('/', $path)) {
4553 # discard '.' and ''
4554 next if (!$part || $part eq '.');
4555 # handle '..'
4556 if ($part eq '..') {
4557 if (@path_parts) {
4558 pop @path_parts;
4559 } else {
4560 # link leads outside repository (outside top dir)
4561 return;
4563 } else {
4564 push @path_parts, $part;
4567 $path = join('/', @path_parts);
4569 return $path;
4572 # print tree entry (row of git_tree), but without encompassing <tr> element
4573 sub git_print_tree_entry {
4574 my ($t, $basedir, $hash_base, $have_blame) = @_;
4576 my %base_key = ();
4577 $base_key{'hash_base'} = $hash_base if defined $hash_base;
4579 # The format of a table row is: mode list link. Where mode is
4580 # the mode of the entry, list is the name of the entry, an href,
4581 # and link is the action links of the entry.
4583 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4584 if (exists $t->{'size'}) {
4585 print "<td class=\"size\">$t->{'size'}</td>\n";
4587 if ($t->{'type'} eq "blob") {
4588 print "<td class=\"list\">" .
4589 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4590 file_name=>"$basedir$t->{'name'}", %base_key),
4591 -class => "list"}, esc_path($t->{'name'}));
4592 if (S_ISLNK(oct $t->{'mode'})) {
4593 my $link_target = git_get_link_target($t->{'hash'});
4594 if ($link_target) {
4595 my $norm_target = normalize_link_target($link_target, $basedir);
4596 if (defined $norm_target) {
4597 print " -> " .
4598 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4599 file_name=>$norm_target),
4600 -title => $norm_target}, esc_path($link_target));
4601 } else {
4602 print " -> " . esc_path($link_target);
4606 print "</td>\n";
4607 print "<td class=\"link\">";
4608 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4609 file_name=>"$basedir$t->{'name'}", %base_key)},
4610 "blob");
4611 if ($have_blame) {
4612 print " | " .
4613 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4614 file_name=>"$basedir$t->{'name'}", %base_key)},
4615 "blame");
4617 if (defined $hash_base) {
4618 print " | " .
4619 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4620 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4621 "history");
4623 print " | " .
4624 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4625 file_name=>"$basedir$t->{'name'}")},
4626 "raw");
4627 print "</td>\n";
4629 } elsif ($t->{'type'} eq "tree") {
4630 print "<td class=\"list\">";
4631 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4632 file_name=>"$basedir$t->{'name'}",
4633 %base_key)},
4634 esc_path($t->{'name'}));
4635 print "</td>\n";
4636 print "<td class=\"link\">";
4637 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4638 file_name=>"$basedir$t->{'name'}",
4639 %base_key)},
4640 "tree");
4641 if (defined $hash_base) {
4642 print " | " .
4643 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4644 file_name=>"$basedir$t->{'name'}")},
4645 "history");
4647 print "</td>\n";
4648 } else {
4649 # unknown object: we can only present history for it
4650 # (this includes 'commit' object, i.e. submodule support)
4651 print "<td class=\"list\">" .
4652 esc_path($t->{'name'}) .
4653 "</td>\n";
4654 print "<td class=\"link\">";
4655 if (defined $hash_base) {
4656 print $cgi->a({-href => href(action=>"history",
4657 hash_base=>$hash_base,
4658 file_name=>"$basedir$t->{'name'}")},
4659 "history");
4661 print "</td>\n";
4665 ## ......................................................................
4666 ## functions printing large fragments of HTML
4668 # get pre-image filenames for merge (combined) diff
4669 sub fill_from_file_info {
4670 my ($diff, @parents) = @_;
4672 $diff->{'from_file'} = [ ];
4673 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4674 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4675 if ($diff->{'status'}[$i] eq 'R' ||
4676 $diff->{'status'}[$i] eq 'C') {
4677 $diff->{'from_file'}[$i] =
4678 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4682 return $diff;
4685 # is current raw difftree line of file deletion
4686 sub is_deleted {
4687 my $diffinfo = shift;
4689 return $diffinfo->{'to_id'} eq ('0' x 40);
4692 # does patch correspond to [previous] difftree raw line
4693 # $diffinfo - hashref of parsed raw diff format
4694 # $patchinfo - hashref of parsed patch diff format
4695 # (the same keys as in $diffinfo)
4696 sub is_patch_split {
4697 my ($diffinfo, $patchinfo) = @_;
4699 return defined $diffinfo && defined $patchinfo
4700 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4704 sub git_difftree_body {
4705 my ($difftree, $hash, @parents) = @_;
4706 my ($parent) = $parents[0];
4707 my $have_blame = gitweb_check_feature('blame');
4708 print "<div class=\"list_head\">\n";
4709 if ($#{$difftree} > 10) {
4710 print(($#{$difftree} + 1) . " files changed:\n");
4712 print "</div>\n";
4714 print "<table class=\"" .
4715 (@parents > 1 ? "combined " : "") .
4716 "diff_tree\">\n";
4718 # header only for combined diff in 'commitdiff' view
4719 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4720 if ($has_header) {
4721 # table header
4722 print "<thead><tr>\n" .
4723 "<th></th><th></th>\n"; # filename, patchN link
4724 for (my $i = 0; $i < @parents; $i++) {
4725 my $par = $parents[$i];
4726 print "<th>" .
4727 $cgi->a({-href => href(action=>"commitdiff",
4728 hash=>$hash, hash_parent=>$par),
4729 -title => 'commitdiff to parent number ' .
4730 ($i+1) . ': ' . substr($par,0,7)},
4731 $i+1) .
4732 "&nbsp;</th>\n";
4734 print "</tr></thead>\n<tbody>\n";
4737 my $alternate = 1;
4738 my $patchno = 0;
4739 foreach my $line (@{$difftree}) {
4740 my $diff = parsed_difftree_line($line);
4742 if ($alternate) {
4743 print "<tr class=\"dark\">\n";
4744 } else {
4745 print "<tr class=\"light\">\n";
4747 $alternate ^= 1;
4749 if (exists $diff->{'nparents'}) { # combined diff
4751 fill_from_file_info($diff, @parents)
4752 unless exists $diff->{'from_file'};
4754 if (!is_deleted($diff)) {
4755 # file exists in the result (child) commit
4756 print "<td>" .
4757 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4758 file_name=>$diff->{'to_file'},
4759 hash_base=>$hash),
4760 -class => "list"}, esc_path($diff->{'to_file'})) .
4761 "</td>\n";
4762 } else {
4763 print "<td>" .
4764 esc_path($diff->{'to_file'}) .
4765 "</td>\n";
4768 if ($action eq 'commitdiff') {
4769 # link to patch
4770 $patchno++;
4771 print "<td class=\"link\">" .
4772 $cgi->a({-href => href(-anchor=>"patch$patchno")},
4773 "patch") .
4774 " | " .
4775 "</td>\n";
4778 my $has_history = 0;
4779 my $not_deleted = 0;
4780 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4781 my $hash_parent = $parents[$i];
4782 my $from_hash = $diff->{'from_id'}[$i];
4783 my $from_path = $diff->{'from_file'}[$i];
4784 my $status = $diff->{'status'}[$i];
4786 $has_history ||= ($status ne 'A');
4787 $not_deleted ||= ($status ne 'D');
4789 if ($status eq 'A') {
4790 print "<td class=\"link\" align=\"right\"> | </td>\n";
4791 } elsif ($status eq 'D') {
4792 print "<td class=\"link\">" .
4793 $cgi->a({-href => href(action=>"blob",
4794 hash_base=>$hash,
4795 hash=>$from_hash,
4796 file_name=>$from_path)},
4797 "blob" . ($i+1)) .
4798 " | </td>\n";
4799 } else {
4800 if ($diff->{'to_id'} eq $from_hash) {
4801 print "<td class=\"link nochange\">";
4802 } else {
4803 print "<td class=\"link\">";
4805 print $cgi->a({-href => href(action=>"blobdiff",
4806 hash=>$diff->{'to_id'},
4807 hash_parent=>$from_hash,
4808 hash_base=>$hash,
4809 hash_parent_base=>$hash_parent,
4810 file_name=>$diff->{'to_file'},
4811 file_parent=>$from_path)},
4812 "diff" . ($i+1)) .
4813 " | </td>\n";
4817 print "<td class=\"link\">";
4818 if ($not_deleted) {
4819 print $cgi->a({-href => href(action=>"blob",
4820 hash=>$diff->{'to_id'},
4821 file_name=>$diff->{'to_file'},
4822 hash_base=>$hash)},
4823 "blob");
4824 print " | " if ($has_history);
4826 if ($has_history) {
4827 print $cgi->a({-href => href(action=>"history",
4828 file_name=>$diff->{'to_file'},
4829 hash_base=>$hash)},
4830 "history");
4832 print "</td>\n";
4834 print "</tr>\n";
4835 next; # instead of 'else' clause, to avoid extra indent
4837 # else ordinary diff
4839 my ($to_mode_oct, $to_mode_str, $to_file_type);
4840 my ($from_mode_oct, $from_mode_str, $from_file_type);
4841 if ($diff->{'to_mode'} ne ('0' x 6)) {
4842 $to_mode_oct = oct $diff->{'to_mode'};
4843 if (S_ISREG($to_mode_oct)) { # only for regular file
4844 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4846 $to_file_type = file_type($diff->{'to_mode'});
4848 if ($diff->{'from_mode'} ne ('0' x 6)) {
4849 $from_mode_oct = oct $diff->{'from_mode'};
4850 if (S_ISREG($from_mode_oct)) { # only for regular file
4851 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4853 $from_file_type = file_type($diff->{'from_mode'});
4856 if ($diff->{'status'} eq "A") { # created
4857 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4858 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
4859 $mode_chng .= "]</span>";
4860 print "<td>";
4861 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4862 hash_base=>$hash, file_name=>$diff->{'file'}),
4863 -class => "list"}, esc_path($diff->{'file'}));
4864 print "</td>\n";
4865 print "<td>$mode_chng</td>\n";
4866 print "<td class=\"link\">";
4867 if ($action eq 'commitdiff') {
4868 # link to patch
4869 $patchno++;
4870 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4871 "patch") .
4872 " | ";
4874 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4875 hash_base=>$hash, file_name=>$diff->{'file'})},
4876 "blob");
4877 print "</td>\n";
4879 } elsif ($diff->{'status'} eq "D") { # deleted
4880 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4881 print "<td>";
4882 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4883 hash_base=>$parent, file_name=>$diff->{'file'}),
4884 -class => "list"}, esc_path($diff->{'file'}));
4885 print "</td>\n";
4886 print "<td>$mode_chng</td>\n";
4887 print "<td class=\"link\">";
4888 if ($action eq 'commitdiff') {
4889 # link to patch
4890 $patchno++;
4891 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4892 "patch") .
4893 " | ";
4895 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4896 hash_base=>$parent, file_name=>$diff->{'file'})},
4897 "blob") . " | ";
4898 if ($have_blame) {
4899 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4900 file_name=>$diff->{'file'})},
4901 "blame") . " | ";
4903 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4904 file_name=>$diff->{'file'})},
4905 "history");
4906 print "</td>\n";
4908 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4909 my $mode_chnge = "";
4910 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4911 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4912 if ($from_file_type ne $to_file_type) {
4913 $mode_chnge .= " from $from_file_type to $to_file_type";
4915 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4916 if ($from_mode_str && $to_mode_str) {
4917 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4918 } elsif ($to_mode_str) {
4919 $mode_chnge .= " mode: $to_mode_str";
4922 $mode_chnge .= "]</span>\n";
4924 print "<td>";
4925 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4926 hash_base=>$hash, file_name=>$diff->{'file'}),
4927 -class => "list"}, esc_path($diff->{'file'}));
4928 print "</td>\n";
4929 print "<td>$mode_chnge</td>\n";
4930 print "<td class=\"link\">";
4931 if ($action eq 'commitdiff') {
4932 # link to patch
4933 $patchno++;
4934 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4935 "patch") .
4936 " | ";
4937 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4938 # "commit" view and modified file (not onlu mode changed)
4939 print $cgi->a({-href => href(action=>"blobdiff",
4940 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4941 hash_base=>$hash, hash_parent_base=>$parent,
4942 file_name=>$diff->{'file'})},
4943 "diff") .
4944 " | ";
4946 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4947 hash_base=>$hash, file_name=>$diff->{'file'})},
4948 "blob") . " | ";
4949 if ($have_blame) {
4950 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4951 file_name=>$diff->{'file'})},
4952 "blame") . " | ";
4954 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4955 file_name=>$diff->{'file'})},
4956 "history");
4957 print "</td>\n";
4959 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4960 my %status_name = ('R' => 'moved', 'C' => 'copied');
4961 my $nstatus = $status_name{$diff->{'status'}};
4962 my $mode_chng = "";
4963 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4964 # mode also for directories, so we cannot use $to_mode_str
4965 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4967 print "<td>" .
4968 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4969 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4970 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4971 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4972 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4973 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4974 -class => "list"}, esc_path($diff->{'from_file'})) .
4975 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4976 "<td class=\"link\">";
4977 if ($action eq 'commitdiff') {
4978 # link to patch
4979 $patchno++;
4980 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4981 "patch") .
4982 " | ";
4983 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4984 # "commit" view and modified file (not only pure rename or copy)
4985 print $cgi->a({-href => href(action=>"blobdiff",
4986 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4987 hash_base=>$hash, hash_parent_base=>$parent,
4988 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4989 "diff") .
4990 " | ";
4992 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4993 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4994 "blob") . " | ";
4995 if ($have_blame) {
4996 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4997 file_name=>$diff->{'to_file'})},
4998 "blame") . " | ";
5000 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
5001 file_name=>$diff->{'to_file'})},
5002 "history");
5003 print "</td>\n";
5005 } # we should not encounter Unmerged (U) or Unknown (X) status
5006 print "</tr>\n";
5008 print "</tbody>" if $has_header;
5009 print "</table>\n";
5012 # Print context lines and then rem/add lines in a side-by-side manner.
5013 sub print_sidebyside_diff_lines {
5014 my ($ctx, $rem, $add) = @_;
5016 # print context block before add/rem block
5017 if (@$ctx) {
5018 print join '',
5019 '<div class="chunk_block ctx">',
5020 '<div class="old">',
5021 @$ctx,
5022 '</div>',
5023 '<div class="new">',
5024 @$ctx,
5025 '</div>',
5026 '</div>';
5029 if (!@$add) {
5030 # pure removal
5031 print join '',
5032 '<div class="chunk_block rem">',
5033 '<div class="old">',
5034 @$rem,
5035 '</div>',
5036 '</div>';
5037 } elsif (!@$rem) {
5038 # pure addition
5039 print join '',
5040 '<div class="chunk_block add">',
5041 '<div class="new">',
5042 @$add,
5043 '</div>',
5044 '</div>';
5045 } else {
5046 print join '',
5047 '<div class="chunk_block chg">',
5048 '<div class="old">',
5049 @$rem,
5050 '</div>',
5051 '<div class="new">',
5052 @$add,
5053 '</div>',
5054 '</div>';
5058 # Print context lines and then rem/add lines in inline manner.
5059 sub print_inline_diff_lines {
5060 my ($ctx, $rem, $add) = @_;
5062 print @$ctx, @$rem, @$add;
5065 # Format removed and added line, mark changed part and HTML-format them.
5066 # Implementation is based on contrib/diff-highlight
5067 sub format_rem_add_lines_pair {
5068 my ($rem, $add, $num_parents) = @_;
5070 # We need to untabify lines before split()'ing them;
5071 # otherwise offsets would be invalid.
5072 chomp $rem;
5073 chomp $add;
5074 $rem = untabify($rem);
5075 $add = untabify($add);
5077 my @rem = split(//, $rem);
5078 my @add = split(//, $add);
5079 my ($esc_rem, $esc_add);
5080 # Ignore leading +/- characters for each parent.
5081 my ($prefix_len, $suffix_len) = ($num_parents, 0);
5082 my ($prefix_has_nonspace, $suffix_has_nonspace);
5084 my $shorter = (@rem < @add) ? @rem : @add;
5085 while ($prefix_len < $shorter) {
5086 last if ($rem[$prefix_len] ne $add[$prefix_len]);
5088 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
5089 $prefix_len++;
5092 while ($prefix_len + $suffix_len < $shorter) {
5093 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
5095 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
5096 $suffix_len++;
5099 # Mark lines that are different from each other, but have some common
5100 # part that isn't whitespace. If lines are completely different, don't
5101 # mark them because that would make output unreadable, especially if
5102 # diff consists of multiple lines.
5103 if ($prefix_has_nonspace || $suffix_has_nonspace) {
5104 $esc_rem = esc_html_hl_regions($rem, 'marked',
5105 [$prefix_len, @rem - $suffix_len], -nbsp=>1);
5106 $esc_add = esc_html_hl_regions($add, 'marked',
5107 [$prefix_len, @add - $suffix_len], -nbsp=>1);
5108 } else {
5109 $esc_rem = esc_html($rem, -nbsp=>1);
5110 $esc_add = esc_html($add, -nbsp=>1);
5113 return format_diff_line(\$esc_rem, 'rem'),
5114 format_diff_line(\$esc_add, 'add');
5117 # HTML-format diff context, removed and added lines.
5118 sub format_ctx_rem_add_lines {
5119 my ($ctx, $rem, $add, $num_parents) = @_;
5120 my (@new_ctx, @new_rem, @new_add);
5121 my $can_highlight = 0;
5122 my $is_combined = ($num_parents > 1);
5124 # Highlight if every removed line has a corresponding added line.
5125 if (@$add > 0 && @$add == @$rem) {
5126 $can_highlight = 1;
5128 # Highlight lines in combined diff only if the chunk contains
5129 # diff between the same version, e.g.
5131 # - a
5132 # - b
5133 # + c
5134 # + d
5136 # Otherwise the highlightling would be confusing.
5137 if ($is_combined) {
5138 for (my $i = 0; $i < @$add; $i++) {
5139 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
5140 my $prefix_add = substr($add->[$i], 0, $num_parents);
5142 $prefix_rem =~ s/-/+/g;
5144 if ($prefix_rem ne $prefix_add) {
5145 $can_highlight = 0;
5146 last;
5152 if ($can_highlight) {
5153 for (my $i = 0; $i < @$add; $i++) {
5154 my ($line_rem, $line_add) = format_rem_add_lines_pair(
5155 $rem->[$i], $add->[$i], $num_parents);
5156 push @new_rem, $line_rem;
5157 push @new_add, $line_add;
5159 } else {
5160 @new_rem = map { format_diff_line($_, 'rem') } @$rem;
5161 @new_add = map { format_diff_line($_, 'add') } @$add;
5164 @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
5166 return (\@new_ctx, \@new_rem, \@new_add);
5169 # Print context lines and then rem/add lines.
5170 sub print_diff_lines {
5171 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
5172 my $is_combined = $num_parents > 1;
5174 ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
5175 $num_parents);
5177 if ($diff_style eq 'sidebyside' && !$is_combined) {
5178 print_sidebyside_diff_lines($ctx, $rem, $add);
5179 } else {
5180 # default 'inline' style and unknown styles
5181 print_inline_diff_lines($ctx, $rem, $add);
5185 sub print_diff_chunk {
5186 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
5187 my (@ctx, @rem, @add);
5189 # The class of the previous line.
5190 my $prev_class = '';
5192 return unless @chunk;
5194 # incomplete last line might be among removed or added lines,
5195 # or both, or among context lines: find which
5196 for (my $i = 1; $i < @chunk; $i++) {
5197 if ($chunk[$i][0] eq 'incomplete') {
5198 $chunk[$i][0] = $chunk[$i-1][0];
5202 # guardian
5203 push @chunk, ["", ""];
5205 foreach my $line_info (@chunk) {
5206 my ($class, $line) = @$line_info;
5208 # print chunk headers
5209 if ($class && $class eq 'chunk_header') {
5210 print format_diff_line($line, $class, $from, $to);
5211 next;
5214 ## print from accumulator when have some add/rem lines or end
5215 # of chunk (flush context lines), or when have add and rem
5216 # lines and new block is reached (otherwise add/rem lines could
5217 # be reordered)
5218 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
5219 (@rem && @add && $class ne $prev_class)) {
5220 print_diff_lines(\@ctx, \@rem, \@add,
5221 $diff_style, $num_parents);
5222 @ctx = @rem = @add = ();
5225 ## adding lines to accumulator
5226 # guardian value
5227 last unless $line;
5228 # rem, add or change
5229 if ($class eq 'rem') {
5230 push @rem, $line;
5231 } elsif ($class eq 'add') {
5232 push @add, $line;
5234 # context line
5235 if ($class eq 'ctx') {
5236 push @ctx, $line;
5239 $prev_class = $class;
5243 sub git_patchset_body {
5244 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
5245 my ($hash_parent) = $hash_parents[0];
5247 my $is_combined = (@hash_parents > 1);
5248 my $patch_idx = 0;
5249 my $patch_number = 0;
5250 my $patch_line;
5251 my $diffinfo;
5252 my $to_name;
5253 my (%from, %to);
5254 my @chunk; # for side-by-side diff
5256 print "<div class=\"patchset\">\n";
5258 # skip to first patch
5259 while ($patch_line = <$fd>) {
5260 chomp $patch_line;
5262 last if ($patch_line =~ m/^diff /);
5265 PATCH:
5266 while ($patch_line) {
5268 # parse "git diff" header line
5269 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
5270 # $1 is from_name, which we do not use
5271 $to_name = unquote($2);
5272 $to_name =~ s!^b/!!;
5273 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
5274 # $1 is 'cc' or 'combined', which we do not use
5275 $to_name = unquote($2);
5276 } else {
5277 $to_name = undef;
5280 # check if current patch belong to current raw line
5281 # and parse raw git-diff line if needed
5282 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
5283 # this is continuation of a split patch
5284 print "<div class=\"patch cont\">\n";
5285 } else {
5286 # advance raw git-diff output if needed
5287 $patch_idx++ if defined $diffinfo;
5289 # read and prepare patch information
5290 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5292 # compact combined diff output can have some patches skipped
5293 # find which patch (using pathname of result) we are at now;
5294 if ($is_combined) {
5295 while ($to_name ne $diffinfo->{'to_file'}) {
5296 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5297 format_diff_cc_simplified($diffinfo, @hash_parents) .
5298 "</div>\n"; # class="patch"
5300 $patch_idx++;
5301 $patch_number++;
5303 last if $patch_idx > $#$difftree;
5304 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5308 # modifies %from, %to hashes
5309 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5311 # this is first patch for raw difftree line with $patch_idx index
5312 # we index @$difftree array from 0, but number patches from 1
5313 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
5316 # git diff header
5317 #assert($patch_line =~ m/^diff /) if DEBUG;
5318 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5319 $patch_number++;
5320 # print "git diff" header
5321 print format_git_diff_header_line($patch_line, $diffinfo,
5322 \%from, \%to);
5324 # print extended diff header
5325 print "<div class=\"diff extended_header\">\n";
5326 EXTENDED_HEADER:
5327 while ($patch_line = <$fd>) {
5328 chomp $patch_line;
5330 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
5332 print format_extended_diff_header_line($patch_line, $diffinfo,
5333 \%from, \%to);
5335 print "</div>\n"; # class="diff extended_header"
5337 # from-file/to-file diff header
5338 if (! $patch_line) {
5339 print "</div>\n"; # class="patch"
5340 last PATCH;
5342 next PATCH if ($patch_line =~ m/^diff /);
5343 #assert($patch_line =~ m/^---/) if DEBUG;
5345 my $last_patch_line = $patch_line;
5346 $patch_line = <$fd>;
5347 chomp $patch_line;
5348 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
5350 print format_diff_from_to_header($last_patch_line, $patch_line,
5351 $diffinfo, \%from, \%to,
5352 @hash_parents);
5354 # the patch itself
5355 LINE:
5356 while ($patch_line = <$fd>) {
5357 chomp $patch_line;
5359 next PATCH if ($patch_line =~ m/^diff /);
5361 my $class = diff_line_class($patch_line, \%from, \%to);
5363 if ($class eq 'chunk_header') {
5364 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5365 @chunk = ();
5368 push @chunk, [ $class, $patch_line ];
5371 } continue {
5372 if (@chunk) {
5373 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5374 @chunk = ();
5376 print "</div>\n"; # class="patch"
5379 # for compact combined (--cc) format, with chunk and patch simplification
5380 # the patchset might be empty, but there might be unprocessed raw lines
5381 for (++$patch_idx if $patch_number > 0;
5382 $patch_idx < @$difftree;
5383 ++$patch_idx) {
5384 # read and prepare patch information
5385 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5387 # generate anchor for "patch" links in difftree / whatchanged part
5388 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5389 format_diff_cc_simplified($diffinfo, @hash_parents) .
5390 "</div>\n"; # class="patch"
5392 $patch_number++;
5395 if ($patch_number == 0) {
5396 if (@hash_parents > 1) {
5397 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
5398 } else {
5399 print "<div class=\"diff nodifferences\">No differences found</div>\n";
5403 print "</div>\n"; # class="patchset"
5406 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5408 sub git_project_search_form {
5409 my ($searchtext, $search_use_regexp) = @_;
5411 my $limit = '';
5412 if ($project_filter) {
5413 $limit = " in '$project_filter/'";
5416 print "<div class=\"projsearch\">\n";
5417 print $cgi->startform(-method => 'get', -action => $my_uri) .
5418 $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
5419 print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
5420 if (defined $project_filter);
5421 print $cgi->textfield(-name => 's', -value => $searchtext,
5422 -title => "Search project by name and description$limit",
5423 -size => 60) . "\n" .
5424 "<span title=\"Extended regular expression\">" .
5425 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5426 -checked => $search_use_regexp) .
5427 "</span>\n" .
5428 $cgi->submit(-name => 'btnS', -value => 'Search') .
5429 $cgi->end_form() . "\n" .
5430 $cgi->a({-href => href(project => undef, searchtext => undef,
5431 project_filter => $project_filter)},
5432 esc_html("List all projects$limit")) . "<br />\n";
5433 print "</div>\n";
5436 # entry for given @keys needs filling if at least one of keys in list
5437 # is not present in %$project_info
5438 sub project_info_needs_filling {
5439 my ($project_info, @keys) = @_;
5441 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
5442 foreach my $key (@keys) {
5443 if (!exists $project_info->{$key}) {
5444 return 1;
5447 return;
5450 # fills project list info (age, description, owner, category, forks, etc.)
5451 # for each project in the list, removing invalid projects from
5452 # returned list, or fill only specified info.
5454 # Invalid projects are removed from the returned list if and only if you
5455 # ask 'age' or 'age_string' to be filled, because they are the only fields
5456 # that run unconditionally git command that requires repository, and
5457 # therefore do always check if project repository is invalid.
5459 # USAGE:
5460 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
5461 # ensures that 'descr_long' and 'ctags' fields are filled
5462 # * @project_list = fill_project_list_info(\@project_list)
5463 # ensures that all fields are filled (and invalid projects removed)
5465 # NOTE: modifies $projlist, but does not remove entries from it
5466 sub fill_project_list_info {
5467 my ($projlist, @wanted_keys) = @_;
5468 my @projects;
5469 my $filter_set = sub { return @_; };
5470 if (@wanted_keys) {
5471 my %wanted_keys = map { $_ => 1 } @wanted_keys;
5472 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
5475 my $show_ctags = gitweb_check_feature('ctags');
5476 PROJECT:
5477 foreach my $pr (@$projlist) {
5478 if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
5479 my (@activity) = git_get_last_activity($pr->{'path'});
5480 unless (@activity) {
5481 next PROJECT;
5483 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
5485 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
5486 my $descr = git_get_project_description($pr->{'path'}) || "";
5487 $descr = to_utf8($descr);
5488 $pr->{'descr_long'} = $descr;
5489 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
5491 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
5492 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
5494 if ($show_ctags &&
5495 project_info_needs_filling($pr, $filter_set->('ctags'))) {
5496 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
5498 if ($projects_list_group_categories &&
5499 project_info_needs_filling($pr, $filter_set->('category'))) {
5500 my $cat = git_get_project_category($pr->{'path'}) ||
5501 $project_list_default_category;
5502 $pr->{'category'} = to_utf8($cat);
5505 push @projects, $pr;
5508 return @projects;
5511 sub sort_projects_list {
5512 my ($projlist, $order) = @_;
5513 my @projects;
5515 my %order_info = (
5516 project => { key => 'path', type => 'str' },
5517 descr => { key => 'descr_long', type => 'str' },
5518 owner => { key => 'owner', type => 'str' },
5519 age => { key => 'age', type => 'num' }
5521 my $oi = $order_info{$order};
5522 return @$projlist unless defined $oi;
5523 if ($oi->{'type'} eq 'str') {
5524 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
5525 } else {
5526 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
5529 return @projects;
5532 # returns a hash of categories, containing the list of project
5533 # belonging to each category
5534 sub build_projlist_by_category {
5535 my ($projlist, $from, $to) = @_;
5536 my %categories;
5538 $from = 0 unless defined $from;
5539 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5541 for (my $i = $from; $i <= $to; $i++) {
5542 my $pr = $projlist->[$i];
5543 push @{$categories{ $pr->{'category'} }}, $pr;
5546 return wantarray ? %categories : \%categories;
5549 # print 'sort by' <th> element, generating 'sort by $name' replay link
5550 # if that order is not selected
5551 sub print_sort_th {
5552 print format_sort_th(@_);
5555 sub format_sort_th {
5556 my ($name, $order, $header) = @_;
5557 my $sort_th = "";
5558 $header ||= ucfirst($name);
5560 if ($order eq $name) {
5561 $sort_th .= "<th>$header</th>\n";
5562 } else {
5563 $sort_th .= "<th>" .
5564 $cgi->a({-href => href(-replay=>1, order=>$name),
5565 -class => "header"}, $header) .
5566 "</th>\n";
5569 return $sort_th;
5572 sub git_project_list_rows {
5573 my ($projlist, $from, $to, $check_forks) = @_;
5575 $from = 0 unless defined $from;
5576 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5578 my $alternate = 1;
5579 for (my $i = $from; $i <= $to; $i++) {
5580 my $pr = $projlist->[$i];
5582 if ($alternate) {
5583 print "<tr class=\"dark\">\n";
5584 } else {
5585 print "<tr class=\"light\">\n";
5587 $alternate ^= 1;
5589 if ($check_forks) {
5590 print "<td>";
5591 if ($pr->{'forks'}) {
5592 my $nforks = scalar @{$pr->{'forks'}};
5593 if ($nforks > 0) {
5594 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5595 -title => "$nforks forks"}, "+");
5596 } else {
5597 print $cgi->span({-title => "$nforks forks"}, "+");
5600 print "</td>\n";
5602 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5603 -class => "list"},
5604 esc_html_match_hl($pr->{'path'}, $search_regexp)) .
5605 "</td>\n" .
5606 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5607 -class => "list",
5608 -title => $pr->{'descr_long'}},
5609 $search_regexp
5610 ? esc_html_match_hl_chopped($pr->{'descr_long'},
5611 $pr->{'descr'}, $search_regexp)
5612 : esc_html($pr->{'descr'})) .
5613 "</td>\n" .
5614 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5615 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5616 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
5617 "<td class=\"link\">" .
5618 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
5619 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5620 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5621 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5622 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5623 "</td>\n" .
5624 "</tr>\n";
5628 sub git_project_list_body {
5629 # actually uses global variable $project
5630 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5631 my @projects = @$projlist;
5633 my $check_forks = gitweb_check_feature('forks');
5634 my $show_ctags = gitweb_check_feature('ctags');
5635 my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
5636 $check_forks = undef
5637 if ($tagfilter || $search_regexp);
5639 # filtering out forks before filling info allows to do less work
5640 @projects = filter_forks_from_projects_list(\@projects)
5641 if ($check_forks);
5642 # search_projects_list pre-fills required info
5643 @projects = search_projects_list(\@projects,
5644 'search_regexp' => $search_regexp,
5645 'tagfilter' => $tagfilter)
5646 if ($tagfilter || $search_regexp);
5647 # fill the rest
5648 @projects = fill_project_list_info(\@projects);
5650 $order ||= $default_projects_order;
5651 $from = 0 unless defined $from;
5652 $to = $#projects if (!defined $to || $#projects < $to);
5654 # short circuit
5655 if ($from > $to) {
5656 print "<center>\n".
5657 "<b>No such projects found</b><br />\n".
5658 "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5659 "</center>\n<br />\n";
5660 return;
5663 @projects = sort_projects_list(\@projects, $order);
5665 if ($show_ctags) {
5666 my $ctags = git_gather_all_ctags(\@projects);
5667 my $cloud = git_populate_project_tagcloud($ctags);
5668 print git_show_project_tagcloud($cloud, 64);
5671 print "<table class=\"project_list\">\n";
5672 unless ($no_header) {
5673 print "<tr>\n";
5674 if ($check_forks) {
5675 print "<th></th>\n";
5677 print_sort_th('project', $order, 'Project');
5678 print_sort_th('descr', $order, 'Description');
5679 print_sort_th('owner', $order, 'Owner');
5680 print_sort_th('age', $order, 'Last Change');
5681 print "<th></th>\n" . # for links
5682 "</tr>\n";
5685 if ($projects_list_group_categories) {
5686 # only display categories with projects in the $from-$to window
5687 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
5688 my %categories = build_projlist_by_category(\@projects, $from, $to);
5689 foreach my $cat (sort keys %categories) {
5690 unless ($cat eq "") {
5691 print "<tr>\n";
5692 if ($check_forks) {
5693 print "<td></td>\n";
5695 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
5696 print "</tr>\n";
5699 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
5701 } else {
5702 git_project_list_rows(\@projects, $from, $to, $check_forks);
5705 if (defined $extra) {
5706 print "<tr>\n";
5707 if ($check_forks) {
5708 print "<td></td>\n";
5710 print "<td colspan=\"5\">$extra</td>\n" .
5711 "</tr>\n";
5713 print "</table>\n";
5716 sub git_log_body {
5717 # uses global variable $project
5718 my ($commitlist, $from, $to, $refs, $extra) = @_;
5720 $from = 0 unless defined $from;
5721 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5723 for (my $i = 0; $i <= $to; $i++) {
5724 my %co = %{$commitlist->[$i]};
5725 next if !%co;
5726 my $commit = $co{'id'};
5727 my $ref = format_ref_marker($refs, $commit);
5728 git_print_header_div('commit',
5729 "<span class=\"age\">$co{'age_string'}</span>" .
5730 esc_html($co{'title'}) . $ref,
5731 $commit);
5732 print "<div class=\"title_text\">\n" .
5733 "<div class=\"log_link\">\n" .
5734 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5735 " | " .
5736 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5737 " | " .
5738 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5739 "<br/>\n" .
5740 "</div>\n";
5741 git_print_authorship(\%co, -tag => 'span');
5742 print "<br/>\n</div>\n";
5744 print "<div class=\"log_body\">\n";
5745 git_print_log($co{'comment'}, -final_empty_line=> 1);
5746 print "</div>\n";
5748 if ($extra) {
5749 print "<div class=\"page_nav\">\n";
5750 print "$extra\n";
5751 print "</div>\n";
5755 sub git_shortlog_body {
5756 # uses global variable $project
5757 my ($commitlist, $from, $to, $refs, $extra) = @_;
5759 $from = 0 unless defined $from;
5760 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5762 print "<table class=\"shortlog\">\n";
5763 my $alternate = 1;
5764 for (my $i = $from; $i <= $to; $i++) {
5765 my %co = %{$commitlist->[$i]};
5766 my $commit = $co{'id'};
5767 my $ref = format_ref_marker($refs, $commit);
5768 if ($alternate) {
5769 print "<tr class=\"dark\">\n";
5770 } else {
5771 print "<tr class=\"light\">\n";
5773 $alternate ^= 1;
5774 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5775 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5776 format_author_html('td', \%co, 10) . "<td>";
5777 print format_subject_html($co{'title'}, $co{'title_short'},
5778 href(action=>"commit", hash=>$commit), $ref);
5779 print "</td>\n" .
5780 "<td class=\"link\">" .
5781 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5782 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5783 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5784 my $snapshot_links = format_snapshot_links($commit);
5785 if (defined $snapshot_links) {
5786 print " | " . $snapshot_links;
5788 print "</td>\n" .
5789 "</tr>\n";
5791 if (defined $extra) {
5792 print "<tr>\n" .
5793 "<td colspan=\"4\">$extra</td>\n" .
5794 "</tr>\n";
5796 print "</table>\n";
5799 sub git_history_body {
5800 # Warning: assumes constant type (blob or tree) during history
5801 my ($commitlist, $from, $to, $refs, $extra,
5802 $file_name, $file_hash, $ftype) = @_;
5804 $from = 0 unless defined $from;
5805 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5807 print "<table class=\"history\">\n";
5808 my $alternate = 1;
5809 for (my $i = $from; $i <= $to; $i++) {
5810 my %co = %{$commitlist->[$i]};
5811 if (!%co) {
5812 next;
5814 my $commit = $co{'id'};
5816 my $ref = format_ref_marker($refs, $commit);
5818 if ($alternate) {
5819 print "<tr class=\"dark\">\n";
5820 } else {
5821 print "<tr class=\"light\">\n";
5823 $alternate ^= 1;
5824 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5825 # shortlog: format_author_html('td', \%co, 10)
5826 format_author_html('td', \%co, 15, 3) . "<td>";
5827 # originally git_history used chop_str($co{'title'}, 50)
5828 print format_subject_html($co{'title'}, $co{'title_short'},
5829 href(action=>"commit", hash=>$commit), $ref);
5830 print "</td>\n" .
5831 "<td class=\"link\">" .
5832 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5833 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
5835 if ($ftype eq 'blob') {
5836 my $blob_current = $file_hash;
5837 my $blob_parent = git_get_hash_by_path($commit, $file_name);
5838 if (defined $blob_current && defined $blob_parent &&
5839 $blob_current ne $blob_parent) {
5840 print " | " .
5841 $cgi->a({-href => href(action=>"blobdiff",
5842 hash=>$blob_current, hash_parent=>$blob_parent,
5843 hash_base=>$hash_base, hash_parent_base=>$commit,
5844 file_name=>$file_name)},
5845 "diff to current");
5848 print "</td>\n" .
5849 "</tr>\n";
5851 if (defined $extra) {
5852 print "<tr>\n" .
5853 "<td colspan=\"4\">$extra</td>\n" .
5854 "</tr>\n";
5856 print "</table>\n";
5859 sub git_tags_body {
5860 # uses global variable $project
5861 my ($taglist, $from, $to, $extra) = @_;
5862 $from = 0 unless defined $from;
5863 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5865 print "<table class=\"tags\">\n";
5866 my $alternate = 1;
5867 for (my $i = $from; $i <= $to; $i++) {
5868 my $entry = $taglist->[$i];
5869 my %tag = %$entry;
5870 my $comment = $tag{'subject'};
5871 my $comment_short;
5872 if (defined $comment) {
5873 $comment_short = chop_str($comment, 30, 5);
5875 if ($alternate) {
5876 print "<tr class=\"dark\">\n";
5877 } else {
5878 print "<tr class=\"light\">\n";
5880 $alternate ^= 1;
5881 if (defined $tag{'age'}) {
5882 print "<td><i>$tag{'age'}</i></td>\n";
5883 } else {
5884 print "<td></td>\n";
5886 print "<td>" .
5887 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
5888 -class => "list name"}, esc_html($tag{'name'})) .
5889 "</td>\n" .
5890 "<td>";
5891 if (defined $comment) {
5892 print format_subject_html($comment, $comment_short,
5893 href(action=>"tag", hash=>$tag{'id'}));
5895 print "</td>\n" .
5896 "<td class=\"selflink\">";
5897 if ($tag{'type'} eq "tag") {
5898 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
5899 } else {
5900 print "&nbsp;";
5902 print "</td>\n" .
5903 "<td class=\"link\">" . " | " .
5904 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
5905 if ($tag{'reftype'} eq "commit") {
5906 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5907 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
5908 } elsif ($tag{'reftype'} eq "blob") {
5909 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
5911 print "</td>\n" .
5912 "</tr>";
5914 if (defined $extra) {
5915 print "<tr>\n" .
5916 "<td colspan=\"5\">$extra</td>\n" .
5917 "</tr>\n";
5919 print "</table>\n";
5922 sub git_heads_body {
5923 # uses global variable $project
5924 my ($headlist, $head_at, $from, $to, $extra) = @_;
5925 $from = 0 unless defined $from;
5926 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
5928 print "<table class=\"heads\">\n";
5929 my $alternate = 1;
5930 for (my $i = $from; $i <= $to; $i++) {
5931 my $entry = $headlist->[$i];
5932 my %ref = %$entry;
5933 my $curr = defined $head_at && $ref{'id'} eq $head_at;
5934 if ($alternate) {
5935 print "<tr class=\"dark\">\n";
5936 } else {
5937 print "<tr class=\"light\">\n";
5939 $alternate ^= 1;
5940 print "<td><i>$ref{'age'}</i></td>\n" .
5941 ($curr ? "<td class=\"current_head\">" : "<td>") .
5942 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
5943 -class => "list name"},esc_html($ref{'name'})) .
5944 "</td>\n" .
5945 "<td class=\"link\">" .
5946 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5947 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
5948 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
5949 "</td>\n" .
5950 "</tr>";
5952 if (defined $extra) {
5953 print "<tr>\n" .
5954 "<td colspan=\"3\">$extra</td>\n" .
5955 "</tr>\n";
5957 print "</table>\n";
5960 # Display a single remote block
5961 sub git_remote_block {
5962 my ($remote, $rdata, $limit, $head) = @_;
5964 my $heads = $rdata->{'heads'};
5965 my $fetch = $rdata->{'fetch'};
5966 my $push = $rdata->{'push'};
5968 my $urls_table = "<table class=\"projects_list\">\n" ;
5970 if (defined $fetch) {
5971 if ($fetch eq $push) {
5972 $urls_table .= format_repo_url("URL", $fetch);
5973 } else {
5974 $urls_table .= format_repo_url("Fetch URL", $fetch);
5975 $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5977 } elsif (defined $push) {
5978 $urls_table .= format_repo_url("Push URL", $push);
5979 } else {
5980 $urls_table .= format_repo_url("", "No remote URL");
5983 $urls_table .= "</table>\n";
5985 my $dots;
5986 if (defined $limit && $limit < @$heads) {
5987 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5990 print $urls_table;
5991 git_heads_body($heads, $head, 0, $limit, $dots);
5994 # Display a list of remote names with the respective fetch and push URLs
5995 sub git_remotes_list {
5996 my ($remotedata, $limit) = @_;
5997 print "<table class=\"heads\">\n";
5998 my $alternate = 1;
5999 my @remotes = sort keys %$remotedata;
6001 my $limited = $limit && $limit < @remotes;
6003 $#remotes = $limit - 1 if $limited;
6005 while (my $remote = shift @remotes) {
6006 my $rdata = $remotedata->{$remote};
6007 my $fetch = $rdata->{'fetch'};
6008 my $push = $rdata->{'push'};
6009 if ($alternate) {
6010 print "<tr class=\"dark\">\n";
6011 } else {
6012 print "<tr class=\"light\">\n";
6014 $alternate ^= 1;
6015 print "<td>" .
6016 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
6017 -class=> "list name"},esc_html($remote)) .
6018 "</td>";
6019 print "<td class=\"link\">" .
6020 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
6021 " | " .
6022 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
6023 "</td>";
6025 print "</tr>\n";
6028 if ($limited) {
6029 print "<tr>\n" .
6030 "<td colspan=\"3\">" .
6031 $cgi->a({-href => href(action=>"remotes")}, "...") .
6032 "</td>\n" . "</tr>\n";
6035 print "</table>";
6038 # Display remote heads grouped by remote, unless there are too many
6039 # remotes, in which case we only display the remote names
6040 sub git_remotes_body {
6041 my ($remotedata, $limit, $head) = @_;
6042 if ($limit and $limit < keys %$remotedata) {
6043 git_remotes_list($remotedata, $limit);
6044 } else {
6045 fill_remote_heads($remotedata);
6046 while (my ($remote, $rdata) = each %$remotedata) {
6047 git_print_section({-class=>"remote", -id=>$remote},
6048 ["remotes", $remote, $remote], sub {
6049 git_remote_block($remote, $rdata, $limit, $head);
6055 sub git_search_message {
6056 my %co = @_;
6058 my $greptype;
6059 if ($searchtype eq 'commit') {
6060 $greptype = "--grep=";
6061 } elsif ($searchtype eq 'author') {
6062 $greptype = "--author=";
6063 } elsif ($searchtype eq 'committer') {
6064 $greptype = "--committer=";
6066 $greptype .= $searchtext;
6067 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6068 $greptype, '--regexp-ignore-case',
6069 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6071 my $paging_nav = '';
6072 if ($page > 0) {
6073 $paging_nav .=
6074 $cgi->a({-href => href(-replay=>1, page=>undef)},
6075 "first") .
6076 " &sdot; " .
6077 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6078 -accesskey => "p", -title => "Alt-p"}, "prev");
6079 } else {
6080 $paging_nav .= "first &sdot; prev";
6082 my $next_link = '';
6083 if ($#commitlist >= 100) {
6084 $next_link =
6085 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6086 -accesskey => "n", -title => "Alt-n"}, "next");
6087 $paging_nav .= " &sdot; $next_link";
6088 } else {
6089 $paging_nav .= " &sdot; next";
6092 git_header_html();
6094 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6095 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6096 if ($page == 0 && !@commitlist) {
6097 print "<p>No match.</p>\n";
6098 } else {
6099 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6102 git_footer_html();
6105 sub git_search_changes {
6106 my %co = @_;
6108 local $/ = "\n";
6109 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6110 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6111 ($search_use_regexp ? '--pickaxe-regex' : ())
6112 or die_error(500, "Open git-log failed");
6114 git_header_html();
6116 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6117 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6119 print "<table class=\"pickaxe search\">\n";
6120 my $alternate = 1;
6121 undef %co;
6122 my @files;
6123 while (my $line = <$fd>) {
6124 chomp $line;
6125 next unless $line;
6127 my %set = parse_difftree_raw_line($line);
6128 if (defined $set{'commit'}) {
6129 # finish previous commit
6130 if (%co) {
6131 print "</td>\n" .
6132 "<td class=\"link\">" .
6133 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6134 "commit") .
6135 " | " .
6136 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6137 hash_base=>$co{'id'})},
6138 "tree") .
6139 "</td>\n" .
6140 "</tr>\n";
6143 if ($alternate) {
6144 print "<tr class=\"dark\">\n";
6145 } else {
6146 print "<tr class=\"light\">\n";
6148 $alternate ^= 1;
6149 %co = parse_commit($set{'commit'});
6150 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6151 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6152 "<td><i>$author</i></td>\n" .
6153 "<td>" .
6154 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6155 -class => "list subject"},
6156 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6157 } elsif (defined $set{'to_id'}) {
6158 next if ($set{'to_id'} =~ m/^0{40}$/);
6160 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6161 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6162 -class => "list"},
6163 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6164 "<br/>\n";
6167 close $fd;
6169 # finish last commit (warning: repetition!)
6170 if (%co) {
6171 print "</td>\n" .
6172 "<td class=\"link\">" .
6173 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6174 "commit") .
6175 " | " .
6176 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6177 hash_base=>$co{'id'})},
6178 "tree") .
6179 "</td>\n" .
6180 "</tr>\n";
6183 print "</table>\n";
6185 git_footer_html();
6188 sub git_search_files {
6189 my %co = @_;
6191 local $/ = "\n";
6192 open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
6193 $search_use_regexp ? ('-E', '-i') : '-F',
6194 $searchtext, $co{'tree'}
6195 or die_error(500, "Open git-grep failed");
6197 git_header_html();
6199 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6200 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6202 print "<table class=\"grep_search\">\n";
6203 my $alternate = 1;
6204 my $matches = 0;
6205 my $lastfile = '';
6206 my $file_href;
6207 while (my $line = <$fd>) {
6208 chomp $line;
6209 my ($file, $lno, $ltext, $binary);
6210 last if ($matches++ > 1000);
6211 if ($line =~ /^Binary file (.+) matches$/) {
6212 $file = $1;
6213 $binary = 1;
6214 } else {
6215 ($file, $lno, $ltext) = split(/\0/, $line, 3);
6216 $file =~ s/^$co{'tree'}://;
6218 if ($file ne $lastfile) {
6219 $lastfile and print "</td></tr>\n";
6220 if ($alternate++) {
6221 print "<tr class=\"dark\">\n";
6222 } else {
6223 print "<tr class=\"light\">\n";
6225 $file_href = href(action=>"blob", hash_base=>$co{'id'},
6226 file_name=>$file);
6227 print "<td class=\"list\">".
6228 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
6229 print "</td><td>\n";
6230 $lastfile = $file;
6232 if ($binary) {
6233 print "<div class=\"binary\">Binary file</div>\n";
6234 } else {
6235 $ltext = untabify($ltext);
6236 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6237 $ltext = esc_html($1, -nbsp=>1);
6238 $ltext .= '<span class="match">';
6239 $ltext .= esc_html($2, -nbsp=>1);
6240 $ltext .= '</span>';
6241 $ltext .= esc_html($3, -nbsp=>1);
6242 } else {
6243 $ltext = esc_html($ltext, -nbsp=>1);
6245 print "<div class=\"pre\">" .
6246 $cgi->a({-href => $file_href.'#l'.$lno,
6247 -class => "linenr"}, sprintf('%4i', $lno)) .
6248 ' ' . $ltext . "</div>\n";
6251 if ($lastfile) {
6252 print "</td></tr>\n";
6253 if ($matches > 1000) {
6254 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6256 } else {
6257 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6259 close $fd;
6261 print "</table>\n";
6263 git_footer_html();
6266 sub git_search_grep_body {
6267 my ($commitlist, $from, $to, $extra) = @_;
6268 $from = 0 unless defined $from;
6269 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6271 print "<table class=\"commit_search\">\n";
6272 my $alternate = 1;
6273 for (my $i = $from; $i <= $to; $i++) {
6274 my %co = %{$commitlist->[$i]};
6275 if (!%co) {
6276 next;
6278 my $commit = $co{'id'};
6279 if ($alternate) {
6280 print "<tr class=\"dark\">\n";
6281 } else {
6282 print "<tr class=\"light\">\n";
6284 $alternate ^= 1;
6285 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6286 format_author_html('td', \%co, 15, 5) .
6287 "<td>" .
6288 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6289 -class => "list subject"},
6290 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6291 my $comment = $co{'comment'};
6292 foreach my $line (@$comment) {
6293 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
6294 my ($lead, $match, $trail) = ($1, $2, $3);
6295 $match = chop_str($match, 70, 5, 'center');
6296 my $contextlen = int((80 - length($match))/2);
6297 $contextlen = 30 if ($contextlen > 30);
6298 $lead = chop_str($lead, $contextlen, 10, 'left');
6299 $trail = chop_str($trail, $contextlen, 10, 'right');
6301 $lead = esc_html($lead);
6302 $match = esc_html($match);
6303 $trail = esc_html($trail);
6305 print "$lead<span class=\"match\">$match</span>$trail<br />";
6308 print "</td>\n" .
6309 "<td class=\"link\">" .
6310 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6311 " | " .
6312 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
6313 " | " .
6314 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6315 print "</td>\n" .
6316 "</tr>\n";
6318 if (defined $extra) {
6319 print "<tr>\n" .
6320 "<td colspan=\"3\">$extra</td>\n" .
6321 "</tr>\n";
6323 print "</table>\n";
6326 ## ======================================================================
6327 ## ======================================================================
6328 ## actions
6330 sub git_project_list {
6331 my $order = $input_params{'order'};
6332 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6333 die_error(400, "Unknown order parameter");
6336 my @list = git_get_projects_list($project_filter, $strict_export);
6337 if (!@list) {
6338 die_error(404, "No projects found");
6341 git_header_html();
6342 if (defined $home_text && -f $home_text) {
6343 print "<div class=\"index_include\">\n";
6344 insert_file($home_text);
6345 print "</div>\n";
6348 git_project_search_form($searchtext, $search_use_regexp);
6349 git_project_list_body(\@list, $order);
6350 git_footer_html();
6353 sub git_forks {
6354 my $order = $input_params{'order'};
6355 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6356 die_error(400, "Unknown order parameter");
6359 my $filter = $project;
6360 $filter =~ s/\.git$//;
6361 my @list = git_get_projects_list($filter);
6362 if (!@list) {
6363 die_error(404, "No forks found");
6366 git_header_html();
6367 git_print_page_nav('','');
6368 git_print_header_div('summary', "$project forks");
6369 git_project_list_body(\@list, $order);
6370 git_footer_html();
6373 sub git_project_index {
6374 my @projects = git_get_projects_list($project_filter, $strict_export);
6375 if (!@projects) {
6376 die_error(404, "No projects found");
6379 print $cgi->header(
6380 -type => 'text/plain',
6381 -charset => 'utf-8',
6382 -content_disposition => 'inline; filename="index.aux"');
6384 foreach my $pr (@projects) {
6385 if (!exists $pr->{'owner'}) {
6386 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
6389 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6390 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6391 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6392 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6393 $path =~ s/ /\+/g;
6394 $owner =~ s/ /\+/g;
6396 print "$path $owner\n";
6400 sub git_summary {
6401 my $descr = git_get_project_description($project) || "none";
6402 my %co = parse_commit("HEAD");
6403 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
6404 my $head = $co{'id'};
6405 my $remote_heads = gitweb_check_feature('remote_heads');
6407 my $owner = git_get_project_owner($project);
6409 my $refs = git_get_references();
6410 # These get_*_list functions return one more to allow us to see if
6411 # there are more ...
6412 my @taglist = git_get_tags_list(16);
6413 my @headlist = git_get_heads_list(16);
6414 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
6415 my @forklist;
6416 my $check_forks = gitweb_check_feature('forks');
6418 if ($check_forks) {
6419 # find forks of a project
6420 my $filter = $project;
6421 $filter =~ s/\.git$//;
6422 @forklist = git_get_projects_list($filter);
6423 # filter out forks of forks
6424 @forklist = filter_forks_from_projects_list(\@forklist)
6425 if (@forklist);
6428 git_header_html();
6429 git_print_page_nav('summary','', $head);
6431 print "<div class=\"title\">&nbsp;</div>\n";
6432 print "<table class=\"projects_list\">\n" .
6433 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
6434 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
6435 if (defined $cd{'rfc2822'}) {
6436 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6437 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
6440 # use per project git URL list in $projectroot/$project/cloneurl
6441 # or make project git URL from git base URL and project name
6442 my $url_tag = "URL";
6443 my @url_list = git_get_project_url_list($project);
6444 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6445 foreach my $git_url (@url_list) {
6446 next unless $git_url;
6447 print format_repo_url($url_tag, $git_url);
6448 $url_tag = "";
6451 # Tag cloud
6452 my $show_ctags = gitweb_check_feature('ctags');
6453 if ($show_ctags) {
6454 my $ctags = git_get_project_ctags($project);
6455 if (%$ctags) {
6456 # without ability to add tags, don't show if there are none
6457 my $cloud = git_populate_project_tagcloud($ctags);
6458 print "<tr id=\"metadata_ctags\">" .
6459 "<td>content tags</td>" .
6460 "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6461 "</tr>\n";
6465 print "</table>\n";
6467 # If XSS prevention is on, we don't include README.html.
6468 # TODO: Allow a readme in some safe format.
6469 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
6470 print "<div class=\"title\">readme</div>\n" .
6471 "<div class=\"readme\">\n";
6472 insert_file("$projectroot/$project/README.html");
6473 print "\n</div>\n"; # class="readme"
6476 # we need to request one more than 16 (0..15) to check if
6477 # those 16 are all
6478 my @commitlist = $head ? parse_commits($head, 17) : ();
6479 if (@commitlist) {
6480 git_print_header_div('shortlog');
6481 git_shortlog_body(\@commitlist, 0, 15, $refs,
6482 $#commitlist <= 15 ? undef :
6483 $cgi->a({-href => href(action=>"shortlog")}, "..."));
6486 if (@taglist) {
6487 git_print_header_div('tags');
6488 git_tags_body(\@taglist, 0, 15,
6489 $#taglist <= 15 ? undef :
6490 $cgi->a({-href => href(action=>"tags")}, "..."));
6493 if (@headlist) {
6494 git_print_header_div('heads');
6495 git_heads_body(\@headlist, $head, 0, 15,
6496 $#headlist <= 15 ? undef :
6497 $cgi->a({-href => href(action=>"heads")}, "..."));
6500 if (%remotedata) {
6501 git_print_header_div('remotes');
6502 git_remotes_body(\%remotedata, 15, $head);
6505 if (@forklist) {
6506 git_print_header_div('forks');
6507 git_project_list_body(\@forklist, 'age', 0, 15,
6508 $#forklist <= 15 ? undef :
6509 $cgi->a({-href => href(action=>"forks")}, "..."),
6510 'no_header');
6513 git_footer_html();
6516 sub git_tag {
6517 my %tag = parse_tag($hash);
6519 if (! %tag) {
6520 die_error(404, "Unknown tag object");
6523 my $head = git_get_head_hash($project);
6524 git_header_html();
6525 git_print_page_nav('','', $head,undef,$head);
6526 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
6527 print "<div class=\"title_text\">\n" .
6528 "<table class=\"object_header\">\n" .
6529 "<tr>\n" .
6530 "<td>object</td>\n" .
6531 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6532 $tag{'object'}) . "</td>\n" .
6533 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6534 $tag{'type'}) . "</td>\n" .
6535 "</tr>\n";
6536 if (defined($tag{'author'})) {
6537 git_print_authorship_rows(\%tag, 'author');
6539 print "</table>\n\n" .
6540 "</div>\n";
6541 print "<div class=\"page_body\">";
6542 my $comment = $tag{'comment'};
6543 foreach my $line (@$comment) {
6544 chomp $line;
6545 print esc_html($line, -nbsp=>1) . "<br/>\n";
6547 print "</div>\n";
6548 git_footer_html();
6551 sub git_blame_common {
6552 my $format = shift || 'porcelain';
6553 if ($format eq 'porcelain' && $input_params{'javascript'}) {
6554 $format = 'incremental';
6555 $action = 'blame_incremental'; # for page title etc
6558 # permissions
6559 gitweb_check_feature('blame')
6560 or die_error(403, "Blame view not allowed");
6562 # error checking
6563 die_error(400, "No file name given") unless $file_name;
6564 $hash_base ||= git_get_head_hash($project);
6565 die_error(404, "Couldn't find base commit") unless $hash_base;
6566 my %co = parse_commit($hash_base)
6567 or die_error(404, "Commit not found");
6568 my $ftype = "blob";
6569 if (!defined $hash) {
6570 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
6571 or die_error(404, "Error looking up file");
6572 } else {
6573 $ftype = git_get_type($hash);
6574 if ($ftype !~ "blob") {
6575 die_error(400, "Object is not a blob");
6579 my $fd;
6580 if ($format eq 'incremental') {
6581 # get file contents (as base)
6582 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
6583 or die_error(500, "Open git-cat-file failed");
6584 } elsif ($format eq 'data') {
6585 # run git-blame --incremental
6586 open $fd, "-|", git_cmd(), "blame", "--incremental",
6587 $hash_base, "--", $file_name
6588 or die_error(500, "Open git-blame --incremental failed");
6589 } else {
6590 # run git-blame --porcelain
6591 open $fd, "-|", git_cmd(), "blame", '-p',
6592 $hash_base, '--', $file_name
6593 or die_error(500, "Open git-blame --porcelain failed");
6596 # incremental blame data returns early
6597 if ($format eq 'data') {
6598 print $cgi->header(
6599 -type=>"text/plain", -charset => "utf-8",
6600 -status=> "200 OK");
6601 local $| = 1; # output autoflush
6602 while (my $line = <$fd>) {
6603 print to_utf8($line);
6605 close $fd
6606 or print "ERROR $!\n";
6608 print 'END';
6609 if (defined $t0 && gitweb_check_feature('timed')) {
6610 print ' '.
6611 tv_interval($t0, [ gettimeofday() ]).
6612 ' '.$number_of_git_cmds;
6614 print "\n";
6616 return;
6619 # page header
6620 git_header_html();
6621 my $formats_nav =
6622 $cgi->a({-href => href(action=>"blob", -replay=>1)},
6623 "blob") .
6624 " | ";
6625 if ($format eq 'incremental') {
6626 $formats_nav .=
6627 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6628 "blame") . " (non-incremental)";
6629 } else {
6630 $formats_nav .=
6631 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6632 "blame") . " (incremental)";
6634 $formats_nav .=
6635 " | " .
6636 $cgi->a({-href => href(action=>"history", -replay=>1)},
6637 "history") .
6638 " | " .
6639 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
6640 "HEAD");
6641 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6642 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6643 git_print_page_path($file_name, $ftype, $hash_base);
6645 # page body
6646 if ($format eq 'incremental') {
6647 print "<noscript>\n<div class=\"error\"><center><b>\n".
6648 "This page requires JavaScript to run.\n Use ".
6649 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
6650 'this page').
6651 " instead.\n".
6652 "</b></center></div>\n</noscript>\n";
6654 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6657 print qq!<div class="page_body">\n!;
6658 print qq!<div id="progress_info">... / ...</div>\n!
6659 if ($format eq 'incremental');
6660 print qq!<table id="blame_table" class="blame" width="100%">\n!.
6661 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6662 qq!<thead>\n!.
6663 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6664 qq!</thead>\n!.
6665 qq!<tbody>\n!;
6667 my @rev_color = qw(light dark);
6668 my $num_colors = scalar(@rev_color);
6669 my $current_color = 0;
6671 if ($format eq 'incremental') {
6672 my $color_class = $rev_color[$current_color];
6674 #contents of a file
6675 my $linenr = 0;
6676 LINE:
6677 while (my $line = <$fd>) {
6678 chomp $line;
6679 $linenr++;
6681 print qq!<tr id="l$linenr" class="$color_class">!.
6682 qq!<td class="sha1"><a href=""> </a></td>!.
6683 qq!<td class="linenr">!.
6684 qq!<a class="linenr" href="">$linenr</a></td>!;
6685 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
6686 print qq!</tr>\n!;
6689 } else { # porcelain, i.e. ordinary blame
6690 my %metainfo = (); # saves information about commits
6692 # blame data
6693 LINE:
6694 while (my $line = <$fd>) {
6695 chomp $line;
6696 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
6697 # no <lines in group> for subsequent lines in group of lines
6698 my ($full_rev, $orig_lineno, $lineno, $group_size) =
6699 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
6700 if (!exists $metainfo{$full_rev}) {
6701 $metainfo{$full_rev} = { 'nprevious' => 0 };
6703 my $meta = $metainfo{$full_rev};
6704 my $data;
6705 while ($data = <$fd>) {
6706 chomp $data;
6707 last if ($data =~ s/^\t//); # contents of line
6708 if ($data =~ /^(\S+)(?: (.*))?$/) {
6709 $meta->{$1} = $2 unless exists $meta->{$1};
6711 if ($data =~ /^previous /) {
6712 $meta->{'nprevious'}++;
6715 my $short_rev = substr($full_rev, 0, 8);
6716 my $author = $meta->{'author'};
6717 my %date =
6718 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
6719 my $date = $date{'iso-tz'};
6720 if ($group_size) {
6721 $current_color = ($current_color + 1) % $num_colors;
6723 my $tr_class = $rev_color[$current_color];
6724 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
6725 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
6726 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
6727 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
6728 if ($group_size) {
6729 print "<td class=\"sha1\"";
6730 print " title=\"". esc_html($author) . ", $date\"";
6731 print " rowspan=\"$group_size\"" if ($group_size > 1);
6732 print ">";
6733 print $cgi->a({-href => href(action=>"commit",
6734 hash=>$full_rev,
6735 file_name=>$file_name)},
6736 esc_html($short_rev));
6737 if ($group_size >= 2) {
6738 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
6739 if (@author_initials) {
6740 print "<br />" .
6741 esc_html(join('', @author_initials));
6742 # or join('.', ...)
6745 print "</td>\n";
6747 # 'previous' <sha1 of parent commit> <filename at commit>
6748 if (exists $meta->{'previous'} &&
6749 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
6750 $meta->{'parent'} = $1;
6751 $meta->{'file_parent'} = unquote($2);
6753 my $linenr_commit =
6754 exists($meta->{'parent'}) ?
6755 $meta->{'parent'} : $full_rev;
6756 my $linenr_filename =
6757 exists($meta->{'file_parent'}) ?
6758 $meta->{'file_parent'} : unquote($meta->{'filename'});
6759 my $blamed = href(action => 'blame',
6760 file_name => $linenr_filename,
6761 hash_base => $linenr_commit);
6762 print "<td class=\"linenr\">";
6763 print $cgi->a({ -href => "$blamed#l$orig_lineno",
6764 -class => "linenr" },
6765 esc_html($lineno));
6766 print "</td>";
6767 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
6768 print "</tr>\n";
6769 } # end while
6773 # footer
6774 print "</tbody>\n".
6775 "</table>\n"; # class="blame"
6776 print "</div>\n"; # class="blame_body"
6777 close $fd
6778 or print "Reading blob failed\n";
6780 git_footer_html();
6783 sub git_blame {
6784 git_blame_common();
6787 sub git_blame_incremental {
6788 git_blame_common('incremental');
6791 sub git_blame_data {
6792 git_blame_common('data');
6795 sub git_tags {
6796 my $head = git_get_head_hash($project);
6797 git_header_html();
6798 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
6799 git_print_header_div('summary', $project);
6801 my @tagslist = git_get_tags_list();
6802 if (@tagslist) {
6803 git_tags_body(\@tagslist);
6805 git_footer_html();
6808 sub git_heads {
6809 my $head = git_get_head_hash($project);
6810 git_header_html();
6811 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
6812 git_print_header_div('summary', $project);
6814 my @headslist = git_get_heads_list();
6815 if (@headslist) {
6816 git_heads_body(\@headslist, $head);
6818 git_footer_html();
6821 # used both for single remote view and for list of all the remotes
6822 sub git_remotes {
6823 gitweb_check_feature('remote_heads')
6824 or die_error(403, "Remote heads view is disabled");
6826 my $head = git_get_head_hash($project);
6827 my $remote = $input_params{'hash'};
6829 my $remotedata = git_get_remotes_list($remote);
6830 die_error(500, "Unable to get remote information") unless defined $remotedata;
6832 unless (%$remotedata) {
6833 die_error(404, defined $remote ?
6834 "Remote $remote not found" :
6835 "No remotes found");
6838 git_header_html(undef, undef, -action_extra => $remote);
6839 git_print_page_nav('', '', $head, undef, $head,
6840 format_ref_views($remote ? '' : 'remotes'));
6842 fill_remote_heads($remotedata);
6843 if (defined $remote) {
6844 git_print_header_div('remotes', "$remote remote for $project");
6845 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
6846 } else {
6847 git_print_header_div('summary', "$project remotes");
6848 git_remotes_body($remotedata, undef, $head);
6851 git_footer_html();
6854 sub git_blob_plain {
6855 my $type = shift;
6856 my $expires;
6858 if (!defined $hash) {
6859 if (defined $file_name) {
6860 my $base = $hash_base || git_get_head_hash($project);
6861 $hash = git_get_hash_by_path($base, $file_name, "blob")
6862 or die_error(404, "Cannot find file");
6863 } else {
6864 die_error(400, "No file name defined");
6866 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6867 # blobs defined by non-textual hash id's can be cached
6868 $expires = "+1d";
6871 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6872 or die_error(500, "Open git-cat-file blob '$hash' failed");
6874 # content-type (can include charset)
6875 $type = blob_contenttype($fd, $file_name, $type);
6877 # "save as" filename, even when no $file_name is given
6878 my $save_as = "$hash";
6879 if (defined $file_name) {
6880 $save_as = $file_name;
6881 } elsif ($type =~ m/^text\//) {
6882 $save_as .= '.txt';
6885 # With XSS prevention on, blobs of all types except a few known safe
6886 # ones are served with "Content-Disposition: attachment" to make sure
6887 # they don't run in our security domain. For certain image types,
6888 # blob view writes an <img> tag referring to blob_plain view, and we
6889 # want to be sure not to break that by serving the image as an
6890 # attachment (though Firefox 3 doesn't seem to care).
6891 my $sandbox = $prevent_xss &&
6892 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
6894 # serve text/* as text/plain
6895 if ($prevent_xss &&
6896 ($type =~ m!^text/[a-z]+\b(.*)$! ||
6897 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
6898 my $rest = $1;
6899 $rest = defined $rest ? $rest : '';
6900 $type = "text/plain$rest";
6903 print $cgi->header(
6904 -type => $type,
6905 -expires => $expires,
6906 -content_disposition =>
6907 ($sandbox ? 'attachment' : 'inline')
6908 . '; filename="' . $save_as . '"');
6909 local $/ = undef;
6910 binmode STDOUT, ':raw';
6911 print <$fd>;
6912 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6913 close $fd;
6916 sub git_blob {
6917 my $expires;
6919 if (!defined $hash) {
6920 if (defined $file_name) {
6921 my $base = $hash_base || git_get_head_hash($project);
6922 $hash = git_get_hash_by_path($base, $file_name, "blob")
6923 or die_error(404, "Cannot find file");
6924 } else {
6925 die_error(400, "No file name defined");
6927 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6928 # blobs defined by non-textual hash id's can be cached
6929 $expires = "+1d";
6932 my $have_blame = gitweb_check_feature('blame');
6933 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
6934 or die_error(500, "Couldn't cat $file_name, $hash");
6935 my $mimetype = blob_mimetype($fd, $file_name);
6936 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
6937 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
6938 close $fd;
6939 return git_blob_plain($mimetype);
6941 # we can have blame only for text/* mimetype
6942 $have_blame &&= ($mimetype =~ m!^text/!);
6944 my $highlight = gitweb_check_feature('highlight');
6945 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6946 $fd = run_highlighter($fd, $highlight, $syntax)
6947 if $syntax;
6949 git_header_html(undef, $expires);
6950 my $formats_nav = '';
6951 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6952 if (defined $file_name) {
6953 if ($have_blame) {
6954 $formats_nav .=
6955 $cgi->a({-href => href(action=>"blame", -replay=>1)},
6956 "blame") .
6957 " | ";
6959 $formats_nav .=
6960 $cgi->a({-href => href(action=>"history", -replay=>1)},
6961 "history") .
6962 " | " .
6963 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6964 "raw") .
6965 " | " .
6966 $cgi->a({-href => href(action=>"blob",
6967 hash_base=>"HEAD", file_name=>$file_name)},
6968 "HEAD");
6969 } else {
6970 $formats_nav .=
6971 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6972 "raw");
6974 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6975 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6976 } else {
6977 print "<div class=\"page_nav\">\n" .
6978 "<br/><br/></div>\n" .
6979 "<div class=\"title\">".esc_html($hash)."</div>\n";
6981 git_print_page_path($file_name, "blob", $hash_base);
6982 print "<div class=\"page_body\">\n";
6983 if ($mimetype =~ m!^image/!) {
6984 print qq!<img type="!.esc_attr($mimetype).qq!"!;
6985 if ($file_name) {
6986 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
6988 print qq! src="! .
6989 href(action=>"blob_plain", hash=>$hash,
6990 hash_base=>$hash_base, file_name=>$file_name) .
6991 qq!" />\n!;
6992 } else {
6993 my $nr;
6994 while (my $line = <$fd>) {
6995 chomp $line;
6996 $nr++;
6997 $line = untabify($line);
6998 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
6999 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
7000 $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);
7003 close $fd
7004 or print "Reading blob failed.\n";
7005 print "</div>";
7006 git_footer_html();
7009 sub git_tree {
7010 if (!defined $hash_base) {
7011 $hash_base = "HEAD";
7013 if (!defined $hash) {
7014 if (defined $file_name) {
7015 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
7016 } else {
7017 $hash = $hash_base;
7020 die_error(404, "No such tree") unless defined($hash);
7022 my $show_sizes = gitweb_check_feature('show-sizes');
7023 my $have_blame = gitweb_check_feature('blame');
7025 my @entries = ();
7027 local $/ = "\0";
7028 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
7029 ($show_sizes ? '-l' : ()), @extra_options, $hash
7030 or die_error(500, "Open git-ls-tree failed");
7031 @entries = map { chomp; $_ } <$fd>;
7032 close $fd
7033 or die_error(404, "Reading tree failed");
7036 my $refs = git_get_references();
7037 my $ref = format_ref_marker($refs, $hash_base);
7038 git_header_html();
7039 my $basedir = '';
7040 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7041 my @views_nav = ();
7042 if (defined $file_name) {
7043 push @views_nav,
7044 $cgi->a({-href => href(action=>"history", -replay=>1)},
7045 "history"),
7046 $cgi->a({-href => href(action=>"tree",
7047 hash_base=>"HEAD", file_name=>$file_name)},
7048 "HEAD"),
7050 my $snapshot_links = format_snapshot_links($hash);
7051 if (defined $snapshot_links) {
7052 # FIXME: Should be available when we have no hash base as well.
7053 push @views_nav, $snapshot_links;
7055 git_print_page_nav('tree','', $hash_base, undef, undef,
7056 join(' | ', @views_nav));
7057 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
7058 } else {
7059 undef $hash_base;
7060 print "<div class=\"page_nav\">\n";
7061 print "<br/><br/></div>\n";
7062 print "<div class=\"title\">".esc_html($hash)."</div>\n";
7064 if (defined $file_name) {
7065 $basedir = $file_name;
7066 if ($basedir ne '' && substr($basedir, -1) ne '/') {
7067 $basedir .= '/';
7069 git_print_page_path($file_name, 'tree', $hash_base);
7071 print "<div class=\"page_body\">\n";
7072 print "<table class=\"tree\">\n";
7073 my $alternate = 1;
7074 # '..' (top directory) link if possible
7075 if (defined $hash_base &&
7076 defined $file_name && $file_name =~ m![^/]+$!) {
7077 if ($alternate) {
7078 print "<tr class=\"dark\">\n";
7079 } else {
7080 print "<tr class=\"light\">\n";
7082 $alternate ^= 1;
7084 my $up = $file_name;
7085 $up =~ s!/?[^/]+$!!;
7086 undef $up unless $up;
7087 # based on git_print_tree_entry
7088 print '<td class="mode">' . mode_str('040000') . "</td>\n";
7089 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
7090 print '<td class="list">';
7091 print $cgi->a({-href => href(action=>"tree",
7092 hash_base=>$hash_base,
7093 file_name=>$up)},
7094 "..");
7095 print "</td>\n";
7096 print "<td class=\"link\"></td>\n";
7098 print "</tr>\n";
7100 foreach my $line (@entries) {
7101 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
7103 if ($alternate) {
7104 print "<tr class=\"dark\">\n";
7105 } else {
7106 print "<tr class=\"light\">\n";
7108 $alternate ^= 1;
7110 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
7112 print "</tr>\n";
7114 print "</table>\n" .
7115 "</div>";
7116 git_footer_html();
7119 sub snapshot_name {
7120 my ($project, $hash) = @_;
7122 # path/to/project.git -> project
7123 # path/to/project/.git -> project
7124 my $name = to_utf8($project);
7125 $name =~ s,([^/])/*\.git$,$1,;
7126 $name = basename($name);
7127 # sanitize name
7128 $name =~ s/[[:cntrl:]]/?/g;
7130 my $ver = $hash;
7131 if ($hash =~ /^[0-9a-fA-F]+$/) {
7132 # shorten SHA-1 hash
7133 my $full_hash = git_get_full_hash($project, $hash);
7134 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
7135 $ver = git_get_short_hash($project, $hash);
7137 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
7138 # tags don't need shortened SHA-1 hash
7139 $ver = $1;
7140 } else {
7141 # branches and other need shortened SHA-1 hash
7142 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
7143 $ver = $1;
7145 $ver .= '-' . git_get_short_hash($project, $hash);
7147 # in case of hierarchical branch names
7148 $ver =~ s!/!.!g;
7150 # name = project-version_string
7151 $name = "$name-$ver";
7153 return wantarray ? ($name, $name) : $name;
7156 sub exit_if_unmodified_since {
7157 my ($latest_epoch) = @_;
7158 our $cgi;
7160 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7161 if (defined $if_modified) {
7162 my $since;
7163 if (eval { require HTTP::Date; 1; }) {
7164 $since = HTTP::Date::str2time($if_modified);
7165 } elsif (eval { require Time::ParseDate; 1; }) {
7166 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7168 if (defined $since && $latest_epoch <= $since) {
7169 my %latest_date = parse_date($latest_epoch);
7170 print $cgi->header(
7171 -last_modified => $latest_date{'rfc2822'},
7172 -status => '304 Not Modified');
7173 goto DONE_GITWEB;
7178 sub git_snapshot {
7179 my $format = $input_params{'snapshot_format'};
7180 if (!@snapshot_fmts) {
7181 die_error(403, "Snapshots not allowed");
7183 # default to first supported snapshot format
7184 $format ||= $snapshot_fmts[0];
7185 if ($format !~ m/^[a-z0-9]+$/) {
7186 die_error(400, "Invalid snapshot format parameter");
7187 } elsif (!exists($known_snapshot_formats{$format})) {
7188 die_error(400, "Unknown snapshot format");
7189 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
7190 die_error(403, "Snapshot format not allowed");
7191 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
7192 die_error(403, "Unsupported snapshot format");
7195 my $type = git_get_type("$hash^{}");
7196 if (!$type) {
7197 die_error(404, 'Object does not exist');
7198 } elsif ($type eq 'blob') {
7199 die_error(400, 'Object is not a tree-ish');
7202 my ($name, $prefix) = snapshot_name($project, $hash);
7203 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
7205 my %co = parse_commit($hash);
7206 exit_if_unmodified_since($co{'committer_epoch'}) if %co;
7208 my $cmd = quote_command(
7209 git_cmd(), 'archive',
7210 "--format=$known_snapshot_formats{$format}{'format'}",
7211 "--prefix=$prefix/", $hash);
7212 if (exists $known_snapshot_formats{$format}{'compressor'}) {
7213 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
7216 $filename =~ s/(["\\])/\\$1/g;
7217 my %latest_date;
7218 if (%co) {
7219 %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
7222 print $cgi->header(
7223 -type => $known_snapshot_formats{$format}{'type'},
7224 -content_disposition => 'inline; filename="' . $filename . '"',
7225 %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
7226 -status => '200 OK');
7228 open my $fd, "-|", $cmd
7229 or die_error(500, "Execute git-archive failed");
7230 binmode STDOUT, ':raw';
7231 print <$fd>;
7232 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7233 close $fd;
7236 sub git_log_generic {
7237 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
7239 my $head = git_get_head_hash($project);
7240 if (!defined $base) {
7241 $base = $head;
7243 if (!defined $page) {
7244 $page = 0;
7246 my $refs = git_get_references();
7248 my $commit_hash = $base;
7249 if (defined $parent) {
7250 $commit_hash = "$parent..$base";
7252 my @commitlist =
7253 parse_commits($commit_hash, 101, (100 * $page),
7254 defined $file_name ? ($file_name, "--full-history") : ());
7256 my $ftype;
7257 if (!defined $file_hash && defined $file_name) {
7258 # some commits could have deleted file in question,
7259 # and not have it in tree, but one of them has to have it
7260 for (my $i = 0; $i < @commitlist; $i++) {
7261 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
7262 last if defined $file_hash;
7265 if (defined $file_hash) {
7266 $ftype = git_get_type($file_hash);
7268 if (defined $file_name && !defined $ftype) {
7269 die_error(500, "Unknown type of object");
7271 my %co;
7272 if (defined $file_name) {
7273 %co = parse_commit($base)
7274 or die_error(404, "Unknown commit object");
7278 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
7279 my $next_link = '';
7280 if ($#commitlist >= 100) {
7281 $next_link =
7282 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7283 -accesskey => "n", -title => "Alt-n"}, "next");
7285 my $patch_max = gitweb_get_feature('patches');
7286 if ($patch_max && !defined $file_name) {
7287 if ($patch_max < 0 || @commitlist <= $patch_max) {
7288 $paging_nav .= " &sdot; " .
7289 $cgi->a({-href => href(action=>"patches", -replay=>1)},
7290 "patches");
7294 git_header_html();
7295 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
7296 if (defined $file_name) {
7297 git_print_header_div('commit', esc_html($co{'title'}), $base);
7298 } else {
7299 git_print_header_div('summary', $project)
7301 git_print_page_path($file_name, $ftype, $hash_base)
7302 if (defined $file_name);
7304 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
7305 $file_name, $file_hash, $ftype);
7307 git_footer_html();
7310 sub git_log {
7311 git_log_generic('log', \&git_log_body,
7312 $hash, $hash_parent);
7315 sub git_commit {
7316 $hash ||= $hash_base || "HEAD";
7317 my %co = parse_commit($hash)
7318 or die_error(404, "Unknown commit object");
7320 my $parent = $co{'parent'};
7321 my $parents = $co{'parents'}; # listref
7323 # we need to prepare $formats_nav before any parameter munging
7324 my $formats_nav;
7325 if (!defined $parent) {
7326 # --root commitdiff
7327 $formats_nav .= '(initial)';
7328 } elsif (@$parents == 1) {
7329 # single parent commit
7330 $formats_nav .=
7331 '(parent: ' .
7332 $cgi->a({-href => href(action=>"commit",
7333 hash=>$parent)},
7334 esc_html(substr($parent, 0, 7))) .
7335 ')';
7336 } else {
7337 # merge commit
7338 $formats_nav .=
7339 '(merge: ' .
7340 join(' ', map {
7341 $cgi->a({-href => href(action=>"commit",
7342 hash=>$_)},
7343 esc_html(substr($_, 0, 7)));
7344 } @$parents ) .
7345 ')';
7347 if (gitweb_check_feature('patches') && @$parents <= 1) {
7348 $formats_nav .= " | " .
7349 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7350 "patch");
7353 if (!defined $parent) {
7354 $parent = "--root";
7356 my @difftree;
7357 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
7358 @diff_opts,
7359 (@$parents <= 1 ? $parent : '-c'),
7360 $hash, "--"
7361 or die_error(500, "Open git-diff-tree failed");
7362 @difftree = map { chomp; $_ } <$fd>;
7363 close $fd or die_error(404, "Reading git-diff-tree failed");
7365 # non-textual hash id's can be cached
7366 my $expires;
7367 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7368 $expires = "+1d";
7370 my $refs = git_get_references();
7371 my $ref = format_ref_marker($refs, $co{'id'});
7373 git_header_html(undef, $expires);
7374 git_print_page_nav('commit', '',
7375 $hash, $co{'tree'}, $hash,
7376 $formats_nav);
7378 if (defined $co{'parent'}) {
7379 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
7380 } else {
7381 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
7383 print "<div class=\"title_text\">\n" .
7384 "<table class=\"object_header\">\n";
7385 git_print_authorship_rows(\%co);
7386 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
7387 print "<tr>" .
7388 "<td>tree</td>" .
7389 "<td class=\"sha1\">" .
7390 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7391 class => "list"}, $co{'tree'}) .
7392 "</td>" .
7393 "<td class=\"link\">" .
7394 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7395 "tree");
7396 my $snapshot_links = format_snapshot_links($hash);
7397 if (defined $snapshot_links) {
7398 print " | " . $snapshot_links;
7400 print "</td>" .
7401 "</tr>\n";
7403 foreach my $par (@$parents) {
7404 print "<tr>" .
7405 "<td>parent</td>" .
7406 "<td class=\"sha1\">" .
7407 $cgi->a({-href => href(action=>"commit", hash=>$par),
7408 class => "list"}, $par) .
7409 "</td>" .
7410 "<td class=\"link\">" .
7411 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
7412 " | " .
7413 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
7414 "</td>" .
7415 "</tr>\n";
7417 print "</table>".
7418 "</div>\n";
7420 print "<div class=\"page_body\">\n";
7421 git_print_log($co{'comment'});
7422 print "</div>\n";
7424 git_difftree_body(\@difftree, $hash, @$parents);
7426 git_footer_html();
7429 sub git_object {
7430 # object is defined by:
7431 # - hash or hash_base alone
7432 # - hash_base and file_name
7433 my $type;
7435 # - hash or hash_base alone
7436 if ($hash || ($hash_base && !defined $file_name)) {
7437 my $object_id = $hash || $hash_base;
7439 open my $fd, "-|", quote_command(
7440 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
7441 or die_error(404, "Object does not exist");
7442 $type = <$fd>;
7443 chomp $type;
7444 close $fd
7445 or die_error(404, "Object does not exist");
7447 # - hash_base and file_name
7448 } elsif ($hash_base && defined $file_name) {
7449 $file_name =~ s,/+$,,;
7451 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
7452 or die_error(404, "Base object does not exist");
7454 # here errors should not hapen
7455 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
7456 or die_error(500, "Open git-ls-tree failed");
7457 my $line = <$fd>;
7458 close $fd;
7460 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
7461 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
7462 die_error(404, "File or directory for given base does not exist");
7464 $type = $2;
7465 $hash = $3;
7466 } else {
7467 die_error(400, "Not enough information to find object");
7470 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7471 hash=>$hash, hash_base=>$hash_base,
7472 file_name=>$file_name),
7473 -status => '302 Found');
7476 sub git_blobdiff {
7477 my $format = shift || 'html';
7478 my $diff_style = $input_params{'diff_style'} || 'inline';
7480 my $fd;
7481 my @difftree;
7482 my %diffinfo;
7483 my $expires;
7485 # preparing $fd and %diffinfo for git_patchset_body
7486 # new style URI
7487 if (defined $hash_base && defined $hash_parent_base) {
7488 if (defined $file_name) {
7489 # read raw output
7490 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7491 $hash_parent_base, $hash_base,
7492 "--", (defined $file_parent ? $file_parent : ()), $file_name
7493 or die_error(500, "Open git-diff-tree failed");
7494 @difftree = map { chomp; $_ } <$fd>;
7495 close $fd
7496 or die_error(404, "Reading git-diff-tree failed");
7497 @difftree
7498 or die_error(404, "Blob diff not found");
7500 } elsif (defined $hash &&
7501 $hash =~ /[0-9a-fA-F]{40}/) {
7502 # try to find filename from $hash
7504 # read filtered raw output
7505 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7506 $hash_parent_base, $hash_base, "--"
7507 or die_error(500, "Open git-diff-tree failed");
7508 @difftree =
7509 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
7510 # $hash == to_id
7511 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7512 map { chomp; $_ } <$fd>;
7513 close $fd
7514 or die_error(404, "Reading git-diff-tree failed");
7515 @difftree
7516 or die_error(404, "Blob diff not found");
7518 } else {
7519 die_error(400, "Missing one of the blob diff parameters");
7522 if (@difftree > 1) {
7523 die_error(400, "Ambiguous blob diff specification");
7526 %diffinfo = parse_difftree_raw_line($difftree[0]);
7527 $file_parent ||= $diffinfo{'from_file'} || $file_name;
7528 $file_name ||= $diffinfo{'to_file'};
7530 $hash_parent ||= $diffinfo{'from_id'};
7531 $hash ||= $diffinfo{'to_id'};
7533 # non-textual hash id's can be cached
7534 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
7535 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
7536 $expires = '+1d';
7539 # open patch output
7540 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7541 '-p', ($format eq 'html' ? "--full-index" : ()),
7542 $hash_parent_base, $hash_base,
7543 "--", (defined $file_parent ? $file_parent : ()), $file_name
7544 or die_error(500, "Open git-diff-tree failed");
7547 # old/legacy style URI -- not generated anymore since 1.4.3.
7548 if (!%diffinfo) {
7549 die_error('404 Not Found', "Missing one of the blob diff parameters")
7552 # header
7553 if ($format eq 'html') {
7554 my $formats_nav =
7555 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
7556 "raw");
7557 $formats_nav .= diff_style_nav($diff_style);
7558 git_header_html(undef, $expires);
7559 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7560 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7561 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7562 } else {
7563 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
7564 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
7566 if (defined $file_name) {
7567 git_print_page_path($file_name, "blob", $hash_base);
7568 } else {
7569 print "<div class=\"page_path\"></div>\n";
7572 } elsif ($format eq 'plain') {
7573 print $cgi->header(
7574 -type => 'text/plain',
7575 -charset => 'utf-8',
7576 -expires => $expires,
7577 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
7579 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7581 } else {
7582 die_error(400, "Unknown blobdiff format");
7585 # patch
7586 if ($format eq 'html') {
7587 print "<div class=\"page_body\">\n";
7589 git_patchset_body($fd, $diff_style,
7590 [ \%diffinfo ], $hash_base, $hash_parent_base);
7591 close $fd;
7593 print "</div>\n"; # class="page_body"
7594 git_footer_html();
7596 } else {
7597 while (my $line = <$fd>) {
7598 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7599 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
7601 print $line;
7603 last if $line =~ m!^\+\+\+!;
7605 local $/ = undef;
7606 print <$fd>;
7607 close $fd;
7611 sub git_blobdiff_plain {
7612 git_blobdiff('plain');
7615 # assumes that it is added as later part of already existing navigation,
7616 # so it returns "| foo | bar" rather than just "foo | bar"
7617 sub diff_style_nav {
7618 my ($diff_style, $is_combined) = @_;
7619 $diff_style ||= 'inline';
7621 return "" if ($is_combined);
7623 my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7624 my %styles = @styles;
7625 @styles =
7626 @styles[ map { $_ * 2 } 0..$#styles/2 ];
7628 return join '',
7629 map { " | ".$_ }
7630 map {
7631 $_ eq $diff_style ? $styles{$_} :
7632 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7633 } @styles;
7636 sub git_commitdiff {
7637 my %params = @_;
7638 my $format = $params{-format} || 'html';
7639 my $diff_style = $input_params{'diff_style'} || 'inline';
7641 my ($patch_max) = gitweb_get_feature('patches');
7642 if ($format eq 'patch') {
7643 die_error(403, "Patch view not allowed") unless $patch_max;
7646 $hash ||= $hash_base || "HEAD";
7647 my %co = parse_commit($hash)
7648 or die_error(404, "Unknown commit object");
7650 # choose format for commitdiff for merge
7651 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
7652 $hash_parent = '--cc';
7654 # we need to prepare $formats_nav before almost any parameter munging
7655 my $formats_nav;
7656 if ($format eq 'html') {
7657 $formats_nav =
7658 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
7659 "raw");
7660 if ($patch_max && @{$co{'parents'}} <= 1) {
7661 $formats_nav .= " | " .
7662 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7663 "patch");
7665 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
7667 if (defined $hash_parent &&
7668 $hash_parent ne '-c' && $hash_parent ne '--cc') {
7669 # commitdiff with two commits given
7670 my $hash_parent_short = $hash_parent;
7671 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
7672 $hash_parent_short = substr($hash_parent, 0, 7);
7674 $formats_nav .=
7675 ' (from';
7676 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
7677 if ($co{'parents'}[$i] eq $hash_parent) {
7678 $formats_nav .= ' parent ' . ($i+1);
7679 last;
7682 $formats_nav .= ': ' .
7683 $cgi->a({-href => href(-replay=>1,
7684 hash=>$hash_parent, hash_base=>undef)},
7685 esc_html($hash_parent_short)) .
7686 ')';
7687 } elsif (!$co{'parent'}) {
7688 # --root commitdiff
7689 $formats_nav .= ' (initial)';
7690 } elsif (scalar @{$co{'parents'}} == 1) {
7691 # single parent commit
7692 $formats_nav .=
7693 ' (parent: ' .
7694 $cgi->a({-href => href(-replay=>1,
7695 hash=>$co{'parent'}, hash_base=>undef)},
7696 esc_html(substr($co{'parent'}, 0, 7))) .
7697 ')';
7698 } else {
7699 # merge commit
7700 if ($hash_parent eq '--cc') {
7701 $formats_nav .= ' | ' .
7702 $cgi->a({-href => href(-replay=>1,
7703 hash=>$hash, hash_parent=>'-c')},
7704 'combined');
7705 } else { # $hash_parent eq '-c'
7706 $formats_nav .= ' | ' .
7707 $cgi->a({-href => href(-replay=>1,
7708 hash=>$hash, hash_parent=>'--cc')},
7709 'compact');
7711 $formats_nav .=
7712 ' (merge: ' .
7713 join(' ', map {
7714 $cgi->a({-href => href(-replay=>1,
7715 hash=>$_, hash_base=>undef)},
7716 esc_html(substr($_, 0, 7)));
7717 } @{$co{'parents'}} ) .
7718 ')';
7722 my $hash_parent_param = $hash_parent;
7723 if (!defined $hash_parent_param) {
7724 # --cc for multiple parents, --root for parentless
7725 $hash_parent_param =
7726 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
7729 # read commitdiff
7730 my $fd;
7731 my @difftree;
7732 if ($format eq 'html') {
7733 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7734 "--no-commit-id", "--patch-with-raw", "--full-index",
7735 $hash_parent_param, $hash, "--"
7736 or die_error(500, "Open git-diff-tree failed");
7738 while (my $line = <$fd>) {
7739 chomp $line;
7740 # empty line ends raw part of diff-tree output
7741 last unless $line;
7742 push @difftree, scalar parse_difftree_raw_line($line);
7745 } elsif ($format eq 'plain') {
7746 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7747 '-p', $hash_parent_param, $hash, "--"
7748 or die_error(500, "Open git-diff-tree failed");
7749 } elsif ($format eq 'patch') {
7750 # For commit ranges, we limit the output to the number of
7751 # patches specified in the 'patches' feature.
7752 # For single commits, we limit the output to a single patch,
7753 # diverging from the git-format-patch default.
7754 my @commit_spec = ();
7755 if ($hash_parent) {
7756 if ($patch_max > 0) {
7757 push @commit_spec, "-$patch_max";
7759 push @commit_spec, '-n', "$hash_parent..$hash";
7760 } else {
7761 if ($params{-single}) {
7762 push @commit_spec, '-1';
7763 } else {
7764 if ($patch_max > 0) {
7765 push @commit_spec, "-$patch_max";
7767 push @commit_spec, "-n";
7769 push @commit_spec, '--root', $hash;
7771 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
7772 '--encoding=utf8', '--stdout', @commit_spec
7773 or die_error(500, "Open git-format-patch failed");
7774 } else {
7775 die_error(400, "Unknown commitdiff format");
7778 # non-textual hash id's can be cached
7779 my $expires;
7780 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7781 $expires = "+1d";
7784 # write commit message
7785 if ($format eq 'html') {
7786 my $refs = git_get_references();
7787 my $ref = format_ref_marker($refs, $co{'id'});
7789 git_header_html(undef, $expires);
7790 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
7791 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
7792 print "<div class=\"title_text\">\n" .
7793 "<table class=\"object_header\">\n";
7794 git_print_authorship_rows(\%co);
7795 print "</table>".
7796 "</div>\n";
7797 print "<div class=\"page_body\">\n";
7798 if (@{$co{'comment'}} > 1) {
7799 print "<div class=\"log\">\n";
7800 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
7801 print "</div>\n"; # class="log"
7804 } elsif ($format eq 'plain') {
7805 my $refs = git_get_references("tags");
7806 my $tagname = git_get_rev_name_tags($hash);
7807 my $filename = basename($project) . "-$hash.patch";
7809 print $cgi->header(
7810 -type => 'text/plain',
7811 -charset => 'utf-8',
7812 -expires => $expires,
7813 -content_disposition => 'inline; filename="' . "$filename" . '"');
7814 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7815 print "From: " . to_utf8($co{'author'}) . "\n";
7816 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
7817 print "Subject: " . to_utf8($co{'title'}) . "\n";
7819 print "X-Git-Tag: $tagname\n" if $tagname;
7820 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7822 foreach my $line (@{$co{'comment'}}) {
7823 print to_utf8($line) . "\n";
7825 print "---\n\n";
7826 } elsif ($format eq 'patch') {
7827 my $filename = basename($project) . "-$hash.patch";
7829 print $cgi->header(
7830 -type => 'text/plain',
7831 -charset => 'utf-8',
7832 -expires => $expires,
7833 -content_disposition => 'inline; filename="' . "$filename" . '"');
7836 # write patch
7837 if ($format eq 'html') {
7838 my $use_parents = !defined $hash_parent ||
7839 $hash_parent eq '-c' || $hash_parent eq '--cc';
7840 git_difftree_body(\@difftree, $hash,
7841 $use_parents ? @{$co{'parents'}} : $hash_parent);
7842 print "<br/>\n";
7844 git_patchset_body($fd, $diff_style,
7845 \@difftree, $hash,
7846 $use_parents ? @{$co{'parents'}} : $hash_parent);
7847 close $fd;
7848 print "</div>\n"; # class="page_body"
7849 git_footer_html();
7851 } elsif ($format eq 'plain') {
7852 local $/ = undef;
7853 print <$fd>;
7854 close $fd
7855 or print "Reading git-diff-tree failed\n";
7856 } elsif ($format eq 'patch') {
7857 local $/ = undef;
7858 print <$fd>;
7859 close $fd
7860 or print "Reading git-format-patch failed\n";
7864 sub git_commitdiff_plain {
7865 git_commitdiff(-format => 'plain');
7868 # format-patch-style patches
7869 sub git_patch {
7870 git_commitdiff(-format => 'patch', -single => 1);
7873 sub git_patches {
7874 git_commitdiff(-format => 'patch');
7877 sub git_history {
7878 git_log_generic('history', \&git_history_body,
7879 $hash_base, $hash_parent_base,
7880 $file_name, $hash);
7883 sub git_search {
7884 $searchtype ||= 'commit';
7886 # check if appropriate features are enabled
7887 gitweb_check_feature('search')
7888 or die_error(403, "Search is disabled");
7889 if ($searchtype eq 'pickaxe') {
7890 # pickaxe may take all resources of your box and run for several minutes
7891 # with every query - so decide by yourself how public you make this feature
7892 gitweb_check_feature('pickaxe')
7893 or die_error(403, "Pickaxe search is disabled");
7895 if ($searchtype eq 'grep') {
7896 # grep search might be potentially CPU-intensive, too
7897 gitweb_check_feature('grep')
7898 or die_error(403, "Grep search is disabled");
7901 if (!defined $searchtext) {
7902 die_error(400, "Text field is empty");
7904 if (!defined $hash) {
7905 $hash = git_get_head_hash($project);
7907 my %co = parse_commit($hash);
7908 if (!%co) {
7909 die_error(404, "Unknown commit object");
7911 if (!defined $page) {
7912 $page = 0;
7915 if ($searchtype eq 'commit' ||
7916 $searchtype eq 'author' ||
7917 $searchtype eq 'committer') {
7918 git_search_message(%co);
7919 } elsif ($searchtype eq 'pickaxe') {
7920 git_search_changes(%co);
7921 } elsif ($searchtype eq 'grep') {
7922 git_search_files(%co);
7923 } else {
7924 die_error(400, "Unknown search type");
7928 sub git_search_help {
7929 git_header_html();
7930 git_print_page_nav('','', $hash,$hash,$hash);
7931 print <<EOT;
7932 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7933 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7934 the pattern entered is recognized as the POSIX extended
7935 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7936 insensitive).</p>
7937 <dl>
7938 <dt><b>commit</b></dt>
7939 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
7941 my $have_grep = gitweb_check_feature('grep');
7942 if ($have_grep) {
7943 print <<EOT;
7944 <dt><b>grep</b></dt>
7945 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
7946 a different one) are searched for the given pattern. On large trees, this search can take
7947 a while and put some strain on the server, so please use it with some consideration. Note that
7948 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7949 case-sensitive.</dd>
7952 print <<EOT;
7953 <dt><b>author</b></dt>
7954 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
7955 <dt><b>committer</b></dt>
7956 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
7958 my $have_pickaxe = gitweb_check_feature('pickaxe');
7959 if ($have_pickaxe) {
7960 print <<EOT;
7961 <dt><b>pickaxe</b></dt>
7962 <dd>All commits that caused the string to appear or disappear from any file (changes that
7963 added, removed or "modified" the string) will be listed. This search can take a while and
7964 takes a lot of strain on the server, so please use it wisely. Note that since you may be
7965 interested even in changes just changing the case as well, this search is case sensitive.</dd>
7968 print "</dl>\n";
7969 git_footer_html();
7972 sub git_shortlog {
7973 git_log_generic('shortlog', \&git_shortlog_body,
7974 $hash, $hash_parent);
7977 ## ......................................................................
7978 ## feeds (RSS, Atom; OPML)
7980 sub git_feed {
7981 my $format = shift || 'atom';
7982 my $have_blame = gitweb_check_feature('blame');
7984 # Atom: http://www.atomenabled.org/developers/syndication/
7985 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7986 if ($format ne 'rss' && $format ne 'atom') {
7987 die_error(400, "Unknown web feed format");
7990 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7991 my $head = $hash || 'HEAD';
7992 my @commitlist = parse_commits($head, 150, 0, $file_name);
7994 my %latest_commit;
7995 my %latest_date;
7996 my $content_type = "application/$format+xml";
7997 if (defined $cgi->http('HTTP_ACCEPT') &&
7998 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7999 # browser (feed reader) prefers text/xml
8000 $content_type = 'text/xml';
8002 if (defined($commitlist[0])) {
8003 %latest_commit = %{$commitlist[0]};
8004 my $latest_epoch = $latest_commit{'committer_epoch'};
8005 exit_if_unmodified_since($latest_epoch);
8006 %latest_date = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
8008 print $cgi->header(
8009 -type => $content_type,
8010 -charset => 'utf-8',
8011 %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
8012 -status => '200 OK');
8014 # Optimization: skip generating the body if client asks only
8015 # for Last-Modified date.
8016 return if ($cgi->request_method() eq 'HEAD');
8018 # header variables
8019 my $title = "$site_name - $project/$action";
8020 my $feed_type = 'log';
8021 if (defined $hash) {
8022 $title .= " - '$hash'";
8023 $feed_type = 'branch log';
8024 if (defined $file_name) {
8025 $title .= " :: $file_name";
8026 $feed_type = 'history';
8028 } elsif (defined $file_name) {
8029 $title .= " - $file_name";
8030 $feed_type = 'history';
8032 $title .= " $feed_type";
8033 my $descr = git_get_project_description($project);
8034 if (defined $descr) {
8035 $descr = esc_html($descr);
8036 } else {
8037 $descr = "$project " .
8038 ($format eq 'rss' ? 'RSS' : 'Atom') .
8039 " feed";
8041 my $owner = git_get_project_owner($project);
8042 $owner = esc_html($owner);
8044 #header
8045 my $alt_url;
8046 if (defined $file_name) {
8047 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
8048 } elsif (defined $hash) {
8049 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
8050 } else {
8051 $alt_url = href(-full=>1, action=>"summary");
8053 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
8054 if ($format eq 'rss') {
8055 print <<XML;
8056 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
8057 <channel>
8059 print "<title>$title</title>\n" .
8060 "<link>$alt_url</link>\n" .
8061 "<description>$descr</description>\n" .
8062 "<language>en</language>\n" .
8063 # project owner is responsible for 'editorial' content
8064 "<managingEditor>$owner</managingEditor>\n";
8065 if (defined $logo || defined $favicon) {
8066 # prefer the logo to the favicon, since RSS
8067 # doesn't allow both
8068 my $img = esc_url($logo || $favicon);
8069 print "<image>\n" .
8070 "<url>$img</url>\n" .
8071 "<title>$title</title>\n" .
8072 "<link>$alt_url</link>\n" .
8073 "</image>\n";
8075 if (%latest_date) {
8076 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
8077 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
8079 print "<generator>gitweb v.$version/$git_version</generator>\n";
8080 } elsif ($format eq 'atom') {
8081 print <<XML;
8082 <feed xmlns="http://www.w3.org/2005/Atom">
8084 print "<title>$title</title>\n" .
8085 "<subtitle>$descr</subtitle>\n" .
8086 '<link rel="alternate" type="text/html" href="' .
8087 $alt_url . '" />' . "\n" .
8088 '<link rel="self" type="' . $content_type . '" href="' .
8089 $cgi->self_url() . '" />' . "\n" .
8090 "<id>" . href(-full=>1) . "</id>\n" .
8091 # use project owner for feed author
8092 "<author><name>$owner</name></author>\n";
8093 if (defined $favicon) {
8094 print "<icon>" . esc_url($favicon) . "</icon>\n";
8096 if (defined $logo) {
8097 # not twice as wide as tall: 72 x 27 pixels
8098 print "<logo>" . esc_url($logo) . "</logo>\n";
8100 if (! %latest_date) {
8101 # dummy date to keep the feed valid until commits trickle in:
8102 print "<updated>1970-01-01T00:00:00Z</updated>\n";
8103 } else {
8104 print "<updated>$latest_date{'iso-8601'}</updated>\n";
8106 print "<generator version='$version/$git_version'>gitweb</generator>\n";
8109 # contents
8110 for (my $i = 0; $i <= $#commitlist; $i++) {
8111 my %co = %{$commitlist[$i]};
8112 my $commit = $co{'id'};
8113 # we read 150, we always show 30 and the ones more recent than 48 hours
8114 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
8115 last;
8117 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
8119 # get list of changed files
8120 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
8121 $co{'parent'} || "--root",
8122 $co{'id'}, "--", (defined $file_name ? $file_name : ())
8123 or next;
8124 my @difftree = map { chomp; $_ } <$fd>;
8125 close $fd
8126 or next;
8128 # print element (entry, item)
8129 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
8130 if ($format eq 'rss') {
8131 print "<item>\n" .
8132 "<title>" . esc_html($co{'title'}) . "</title>\n" .
8133 "<author>" . esc_html($co{'author'}) . "</author>\n" .
8134 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
8135 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
8136 "<link>$co_url</link>\n" .
8137 "<description>" . esc_html($co{'title'}) . "</description>\n" .
8138 "<content:encoded>" .
8139 "<![CDATA[\n";
8140 } elsif ($format eq 'atom') {
8141 print "<entry>\n" .
8142 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
8143 "<updated>$cd{'iso-8601'}</updated>\n" .
8144 "<author>\n" .
8145 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
8146 if ($co{'author_email'}) {
8147 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
8149 print "</author>\n" .
8150 # use committer for contributor
8151 "<contributor>\n" .
8152 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
8153 if ($co{'committer_email'}) {
8154 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
8156 print "</contributor>\n" .
8157 "<published>$cd{'iso-8601'}</published>\n" .
8158 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
8159 "<id>$co_url</id>\n" .
8160 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
8161 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
8163 my $comment = $co{'comment'};
8164 print "<pre>\n";
8165 foreach my $line (@$comment) {
8166 $line = esc_html($line);
8167 print "$line\n";
8169 print "</pre><ul>\n";
8170 foreach my $difftree_line (@difftree) {
8171 my %difftree = parse_difftree_raw_line($difftree_line);
8172 next if !$difftree{'from_id'};
8174 my $file = $difftree{'file'} || $difftree{'to_file'};
8176 print "<li>" .
8177 "[" .
8178 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
8179 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
8180 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
8181 file_name=>$file, file_parent=>$difftree{'from_file'}),
8182 -title => "diff"}, 'D');
8183 if ($have_blame) {
8184 print $cgi->a({-href => href(-full=>1, action=>"blame",
8185 file_name=>$file, hash_base=>$commit),
8186 -title => "blame"}, 'B');
8188 # if this is not a feed of a file history
8189 if (!defined $file_name || $file_name ne $file) {
8190 print $cgi->a({-href => href(-full=>1, action=>"history",
8191 file_name=>$file, hash=>$commit),
8192 -title => "history"}, 'H');
8194 $file = esc_path($file);
8195 print "] ".
8196 "$file</li>\n";
8198 if ($format eq 'rss') {
8199 print "</ul>]]>\n" .
8200 "</content:encoded>\n" .
8201 "</item>\n";
8202 } elsif ($format eq 'atom') {
8203 print "</ul>\n</div>\n" .
8204 "</content>\n" .
8205 "</entry>\n";
8209 # end of feed
8210 if ($format eq 'rss') {
8211 print "</channel>\n</rss>\n";
8212 } elsif ($format eq 'atom') {
8213 print "</feed>\n";
8217 sub git_rss {
8218 git_feed('rss');
8221 sub git_atom {
8222 git_feed('atom');
8225 sub git_opml {
8226 my @list = git_get_projects_list($project_filter, $strict_export);
8227 if (!@list) {
8228 die_error(404, "No projects found");
8231 print $cgi->header(
8232 -type => 'text/xml',
8233 -charset => 'utf-8',
8234 -content_disposition => 'inline; filename="opml.xml"');
8236 my $title = esc_html($site_name);
8237 my $filter = " within subdirectory ";
8238 if (defined $project_filter) {
8239 $filter .= esc_html($project_filter);
8240 } else {
8241 $filter = "";
8243 print <<XML;
8244 <?xml version="1.0" encoding="utf-8"?>
8245 <opml version="1.0">
8246 <head>
8247 <title>$title OPML Export$filter</title>
8248 </head>
8249 <body>
8250 <outline text="git RSS feeds">
8253 foreach my $pr (@list) {
8254 my %proj = %$pr;
8255 my $head = git_get_head_hash($proj{'path'});
8256 if (!defined $head) {
8257 next;
8259 $git_dir = "$projectroot/$proj{'path'}";
8260 my %co = parse_commit($head);
8261 if (!%co) {
8262 next;
8265 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
8266 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
8267 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
8268 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
8270 print <<XML;
8271 </outline>
8272 </body>
8273 </opml>