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