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