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