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