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