fixed coding fails from last commit
[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);
f01a1ea6 4640 my $count;
30c05d21
S
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++) {
f01a1ea6 4685 $count++;
30c05d21
S
4686 my $pr = $projects[$i];
4687
4688 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4689 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4690 and not $pr->{'descr_long'} =~ /$searchtext/;
4691 # Weed out forks or non-matching entries of search
4692 if ($check_forks) {
4693 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4694 $forkbase="^$forkbase" if $forkbase;
4695 next if not $searchtext and not $tagfilter and $show_ctags
4696 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4697 }
4698
4699 if ($alternate) {
4700 print "<tr class=\"dark\">\n";
4701 } else {
4702 print "<tr class=\"light\">\n";
4703 }
4704 $alternate ^= 1;
4705 if ($check_forks) {
4706 print "<td>";
4707 if ($pr->{'forks'}) {
4708 print "<!-- $pr->{'forks'} -->\n";
4709 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4710 }
4711 print "</td>\n";
4712 }
4713 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4714 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4715 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4716 -class => "list", -title => $pr->{'descr_long'}},
4717 esc_html($pr->{'descr'})) . "</td>\n" .
4718 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4719 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4720 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4721 "<td class=\"link\">" .
4722 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
8a1b4b56 4723 #$cgi->a({-href => href(project=>$pr->{'path'}, action=>"bugtrack")}, "bugtrack") . " | " .
30c05d21
S
4724 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4725 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
0c0738da 4726 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
30c05d21
S
4727 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4728 "</td>\n" .
4729 "</tr>\n";
4730 }
f01a1ea6 4731 print "<tr><td>&nbsp;</td><td>&nbsp;</td><td>".$count." projects found</td><td>&nbsp;</td><td>&nbsp;</td></tr>";
30c05d21
S
4732 if (defined $extra) {
4733 print "<tr>\n";
4734 if ($check_forks) {
4735 print "<td></td>\n";
4736 }
4737 print "<td colspan=\"5\">$extra</td>\n" .
4738 "</tr>\n";
4739 }
4740 print "</table>\n";
4741}
4742
4743sub git_log_body {
4744 # uses global variable $project
4745 my ($commitlist, $from, $to, $refs, $extra) = @_;
4746
4747 $from = 0 unless defined $from;
4748 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4749
4750 for (my $i = 0; $i <= $to; $i++) {
4751 my %co = %{$commitlist->[$i]};
4752 next if !%co;
4753 my $commit = $co{'id'};
4754 my $ref = format_ref_marker($refs, $commit);
4755 my %ad = parse_date($co{'author_epoch'});
4756 git_print_header_div('commit',
4757 "<span class=\"age\">$co{'age_string'}</span>" .
4758 esc_html($co{'title'}) . $ref,
4759 $commit);
4760 print "<div class=\"title_text\">\n" .
4761 "<div class=\"log_link\">\n" .
4762 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4763 " | " .
4764 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4765 " | " .
4766 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4767 "<br/>\n" .
4768 "</div>\n";
4769 git_print_authorship(\%co, -tag => 'span');
4770 print "<br/>\n</div>\n";
4771
4772 print "<div class=\"log_body\">\n";
4773 git_print_log($co{'comment'}, -final_empty_line=> 1);
4774 print "</div>\n";
4775 }
4776 if ($extra) {
4777 print "<div class=\"page_nav\">\n";
4778 print "$extra\n";
4779 print "</div>\n";
4780 }
4781}
4782
4783sub git_shortlog_body {
4784 # uses global variable $project
74867408 4785 my ($commitlist, $from, $to, $refs, $extra, $file_name, $file_hash, $ftype, $allrefs) = @_;
30c05d21
S
4786
4787 $from = 0 unless defined $from;
4788 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4789
8a1b4b56 4790 print "<table class=\"shortlog\" cellspacing=\"0\" cellpadding=\"0\">\n";
30c05d21 4791 my $alternate = 1;
74867408 4792 my $head = git_get_head_hash($project);
0c0738da 4793 my $graph_hash;
74867408 4794 if (defined $allrefs && $allrefs == 1) {
0c0738da 4795 $graph_hash = "all";
74867408
S
4796 }
4797 if (!defined $hash) {
4798 $hash = $head;
4799 }
0c0738da
S
4800 if(!defined $graph_hash) {
4801 $graph_hash = $hash;
4802 }
74867408
S
4803 if (!defined $page) {
4804 $page = 0;
4805 }
8a1b4b56 4806 my $graph_rand = int(rand(99999));
74867408 4807 print "<tr class=\"header\">\n";
0c0738da 4808 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
4809 print "<td valign=\"bottom\"><b>Author</b></td>\n";
4810 print "<td valign=\"bottom\"><b>Commit</b></td>\n";
4811 print "<td></td>\n";
4812 print "</tr>\n";
30c05d21
S
4813 for (my $i = $from; $i <= $to; $i++) {
4814 my %co = %{$commitlist->[$i]};
4815 my $commit = $co{'id'};
8a1b4b56 4816
30c05d21
S
4817 my $ref = format_ref_marker($refs, $commit);
4818 if ($alternate) {
4819 print "<tr class=\"dark\">\n";
4820 } else {
4821 print "<tr class=\"light\">\n";
4822 }
4823 $alternate ^= 1;
4824 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
0c0738da 4825 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 4826 print "<td class=\"". age_class($co{'age'}) . "\" title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
30c05d21
S
4827 format_author_html('td', \%co, 10) . "<td>";
4828 print format_subject_html($co{'title'}, $co{'title_short'},
4829 href(action=>"commit", hash=>$commit), $ref);
4830 print "</td>\n" .
4831 "<td class=\"link\">" .
4832 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4833 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
9086c806 4834 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree"). " | " .
7e90fff2 4835 "<a title='in format: tar.gz' href='http://git.nexus-irc.de/git_download.php?build2=".$project.";h=".$commit."'>snapshot</a>";
9086c806 4836
30c05d21
S
4837 print "</td>\n" .
4838 "</tr>\n";
4839 }
4840 if (defined $extra) {
4841 print "<tr>\n" .
4842 "<td colspan=\"4\">$extra</td>\n" .
4843 "</tr>\n";
4844 }
4845 print "</table>\n";
4846}
4847
4848sub git_history_body {
4849 # Warning: assumes constant type (blob or tree) during history
4850 my ($commitlist, $from, $to, $refs, $extra,
4851 $file_name, $file_hash, $ftype) = @_;
4852
4853 $from = 0 unless defined $from;
4854 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4855
4856 print "<table class=\"history\">\n";
4857 my $alternate = 1;
4858 for (my $i = $from; $i <= $to; $i++) {
4859 my %co = %{$commitlist->[$i]};
4860 if (!%co) {
4861 next;
4862 }
4863 my $commit = $co{'id'};
4864
4865 my $ref = format_ref_marker($refs, $commit);
4866
4867 if ($alternate) {
4868 print "<tr class=\"dark\">\n";
4869 } else {
4870 print "<tr class=\"light\">\n";
4871 }
4872 $alternate ^= 1;
8a1b4b56 4873 print "<td class=\"". age_class($co{'age'}) . "\" title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
30c05d21
S
4874 # shortlog: format_author_html('td', \%co, 10)
4875 format_author_html('td', \%co, 15, 3) . "<td>";
4876 # originally git_history used chop_str($co{'title'}, 50)
4877 print format_subject_html($co{'title'}, $co{'title_short'},
4878 href(action=>"commit", hash=>$commit), $ref);
4879 print "</td>\n" .
4880 "<td class=\"link\">" .
4881 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4882 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4883
4884 if ($ftype eq 'blob') {
4885 my $blob_current = $file_hash;
4886 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4887 if (defined $blob_current && defined $blob_parent &&
4888 $blob_current ne $blob_parent) {
4889 print " | " .
4890 $cgi->a({-href => href(action=>"blobdiff",
4891 hash=>$blob_current, hash_parent=>$blob_parent,
4892 hash_base=>$hash_base, hash_parent_base=>$commit,
4893 file_name=>$file_name)},
4894 "diff to current");
4895 }
4896 }
4897 print "</td>\n" .
4898 "</tr>\n";
4899 }
4900 if (defined $extra) {
4901 print "<tr>\n" .
4902 "<td colspan=\"4\">$extra</td>\n" .
4903 "</tr>\n";
4904 }
4905 print "</table>\n";
4906}
4907
4908sub git_tags_body {
4909 # uses global variable $project
4910 my ($taglist, $from, $to, $extra) = @_;
4911 $from = 0 unless defined $from;
4912 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4913
4914 print "<table class=\"tags\">\n";
4915 my $alternate = 1;
4916 for (my $i = $from; $i <= $to; $i++) {
4917 my $entry = $taglist->[$i];
4918 my %tag = %$entry;
4919 my $comment = $tag{'subject'};
4920 my $comment_short;
4921 if (defined $comment) {
4922 $comment_short = chop_str($comment, 30, 5);
4923 }
4924 if ($alternate) {
4925 print "<tr class=\"dark\">\n";
4926 } else {
4927 print "<tr class=\"light\">\n";
4928 }
4929 $alternate ^= 1;
4930 if (defined $tag{'age'}) {
4931 print "<td><i>$tag{'age'}</i></td>\n";
4932 } else {
4933 print "<td></td>\n";
4934 }
4935 print "<td>" .
4936 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4937 -class => "list name"}, esc_html($tag{'name'})) .
4938 "</td>\n" .
4939 "<td>";
4940 if (defined $comment) {
4941 print format_subject_html($comment, $comment_short,
4942 href(action=>"tag", hash=>$tag{'id'}));
4943 }
4944 print "</td>\n" .
4945 "<td class=\"selflink\">";
4946 if ($tag{'type'} eq "tag") {
4947 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4948 } else {
4949 print "&nbsp;";
4950 }
4951 print "</td>\n" .
4952 "<td class=\"link\">" . " | " .
4953 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4954 if ($tag{'reftype'} eq "commit") {
4955 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4956 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4957 } elsif ($tag{'reftype'} eq "blob") {
4958 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4959 }
4960 print "</td>\n" .
4961 "</tr>";
4962 }
4963 if (defined $extra) {
4964 print "<tr>\n" .
4965 "<td colspan=\"5\">$extra</td>\n" .
4966 "</tr>\n";
4967 }
4968 print "</table>\n";
4969}
4970
4971sub git_heads_body {
4972 # uses global variable $project
4973 my ($headlist, $head, $from, $to, $extra) = @_;
4974 $from = 0 unless defined $from;
4975 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4976
4977 print "<table class=\"heads\">\n";
4978 my $alternate = 1;
4979 for (my $i = $from; $i <= $to; $i++) {
4980 my $entry = $headlist->[$i];
4981 my %ref = %$entry;
4982 my $curr = $ref{'id'} eq $head;
4983 if ($alternate) {
4984 print "<tr class=\"dark\">\n";
4985 } else {
4986 print "<tr class=\"light\">\n";
4987 }
4988 $alternate ^= 1;
4989 print "<td><i>$ref{'age'}</i></td>\n" .
4990 ($curr ? "<td class=\"current_head\">" : "<td>") .
4991 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4992 -class => "list name"},esc_html($ref{'name'})) .
4993 "</td>\n" .
4994 "<td class=\"link\">" .
4995 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4996 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4997 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4998 "</td>\n" .
4999 "</tr>";
5000 }
5001 if (defined $extra) {
5002 print "<tr>\n" .
5003 "<td colspan=\"3\">$extra</td>\n" .
5004 "</tr>\n";
5005 }
5006 print "</table>\n";
5007}
5008
5009sub git_search_grep_body {
5010 my ($commitlist, $from, $to, $extra) = @_;
5011 $from = 0 unless defined $from;
5012 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5013
5014 print "<table class=\"commit_search\">\n";
5015 my $alternate = 1;
5016 for (my $i = $from; $i <= $to; $i++) {
5017 my %co = %{$commitlist->[$i]};
5018 if (!%co) {
5019 next;
5020 }
5021 my $commit = $co{'id'};
5022 if ($alternate) {
5023 print "<tr class=\"dark\">\n";
5024 } else {
5025 print "<tr class=\"light\">\n";
5026 }
5027 $alternate ^= 1;
5028 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5029 format_author_html('td', \%co, 15, 5) .
5030 "<td>" .
5031 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5032 -class => "list subject"},
5033 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5034 my $comment = $co{'comment'};
5035 foreach my $line (@$comment) {
5036 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
5037 my ($lead, $match, $trail) = ($1, $2, $3);
5038 $match = chop_str($match, 70, 5, 'center');
5039 my $contextlen = int((80 - length($match))/2);
5040 $contextlen = 30 if ($contextlen > 30);
5041 $lead = chop_str($lead, $contextlen, 10, 'left');
5042 $trail = chop_str($trail, $contextlen, 10, 'right');
5043
5044 $lead = esc_html($lead);
5045 $match = esc_html($match);
5046 $trail = esc_html($trail);
5047
5048 print "$lead<span class=\"match\">$match</span>$trail<br />";
5049 }
5050 }
5051 print "</td>\n" .
5052 "<td class=\"link\">" .
5053 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5054 " | " .
5055 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
5056 " | " .
5057 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5058 print "</td>\n" .
5059 "</tr>\n";
5060 }
5061 if (defined $extra) {
5062 print "<tr>\n" .
5063 "<td colspan=\"3\">$extra</td>\n" .
5064 "</tr>\n";
5065 }
5066 print "</table>\n";
5067}
5068
5069## ======================================================================
5070## ======================================================================
5071## actions
5072
5073sub git_project_list {
5074 my $order = $input_params{'order'};
5075 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5076 die_error(400, "Unknown order parameter");
5077 }
5078
5079 my @list = git_get_projects_list();
5080 if (!@list) {
5081 die_error(404, "No projects found");
5082 }
5083
5084 git_header_html();
5085 if (defined $home_text && -f $home_text) {
5086 print "<div class=\"index_include\">\n";
5087 insert_file($home_text);
5088 print "</div>\n";
5089 }
5090 print $cgi->startform(-method => "get") .
5091 "<p class=\"projsearch\">Search:\n" .
5092 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5093 "</p>" .
5094 $cgi->end_form() . "\n";
5095 git_project_list_body(\@list, $order);
5096 git_footer_html();
5097}
5098
5099sub git_forks {
5100 my $order = $input_params{'order'};
5101 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5102 die_error(400, "Unknown order parameter");
5103 }
5104
5105 my @list = git_get_projects_list($project);
5106 if (!@list) {
5107 die_error(404, "No forks found");
5108 }
5109
5110 git_header_html();
5111 git_print_page_nav('','');
5112 git_print_header_div('summary', "$project forks");
5113 git_project_list_body(\@list, $order);
5114 git_footer_html();
5115}
5116
5117sub git_project_index {
5118 my @projects = git_get_projects_list($project);
5119
5120 print $cgi->header(
5121 -type => 'text/plain',
5122 -charset => 'utf-8',
5123 -content_disposition => 'inline; filename="index.aux"');
5124
5125 foreach my $pr (@projects) {
5126 if (!exists $pr->{'owner'}) {
5127 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5128 }
5129
5130 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5131 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5132 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5133 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5134 $path =~ s/ /\+/g;
5135 $owner =~ s/ /\+/g;
5136
5137 print "$path $owner\n";
5138 }
5139}
8a1b4b56
S
5140sub git_project_index2 {
5141 my @projects = git_get_projects_list($project);
30c05d21 5142
8a1b4b56
S
5143 print $cgi->header(
5144 -type => 'text/plain',
5145 -charset => 'utf-8',
5146 -content_disposition => 'inline; filename="index.aux"');
5147
5148 foreach my $pr (@projects) {
5149 if (!exists $pr->{'owner'}) {
5150 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5151 }
5152
5153 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5154 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5155 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5156 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5157 $path =~ s/ /\+/g;
5158 $owner =~ s/ /\+/g;
5159
5160 print "$path\n";
5161 }
5162}
0c0738da 5163sub git_download {
8a1b4b56
S
5164 my $dl = get("http://git.nexus-irc.de/git_download.php");
5165 git_header_html();
5166 print "<div class=\"title\">Downloads</div>\n";
5167 print $dl;
5168 git_footer_html();
5169}
5170
8a1b4b56 5171sub git_project_bugtracker {
30c05d21 5172 my $descr = git_get_project_description($project) || "none";
8a1b4b56 5173 my $bugtrack = get("http://git.nexus-irc.de/git_bugtrack.php?p=".$project);
30c05d21
S
5174 my %co = parse_commit("HEAD");
5175 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5176 my $head = $co{'id'};
8a1b4b56
S
5177 my $owner = git_get_project_owner($project);
5178 my $version = get("http://git.nexus-irc.de/git_version.php?git=".$project);
5179 git_header_html();
5180 git_print_page_nav('bugtracker','', $head);
5181 print "<div class=\"title\">&nbsp;</div>\n";
5182 print "<table class=\"projects_list\">\n" .
5183 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5184 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5185 if (defined $cd{'rfc2822'}) {
5186 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5187 }
5188 my $url_tag = "URL";
5189 my @url_list = git_get_project_url_list($project);
5190 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5191 foreach my $git_url (@url_list) {
5192 next unless $git_url;
5193 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
5194 $url_tag = "";
5195 }
5196 print "<tr id=\"metadata_owner\"><td>version</td><td>" . esc_html($version) . "</td></tr>\n";
5197 print "</table>\n";
5198 git_print_header_div('bugtracker');
5199 print $bugtrack;
5200 git_footer_html();
5201}
30c05d21 5202
8a1b4b56
S
5203sub git_summary {
5204 my $descr = git_get_project_description($project) || "none";
5205 my %co = parse_commit("HEAD");
5206 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5207 my $head = $co{'id'};
5208
30c05d21 5209 my $owner = git_get_project_owner($project);
8a1b4b56
S
5210
5211 my $version = get("http://git.nexus-irc.de/git_version.php?git=".$project);
0c0738da 5212 my $download = get("http://git.nexus-irc.de/git_download.php?p=".$project);
8a1b4b56 5213
30c05d21
S
5214 my $refs = git_get_references();
5215 # These get_*_list functions return one more to allow us to see if
5216 # there are more ...
5217 my @taglist = git_get_tags_list(16);
5218 my @headlist = git_get_heads_list(16);
5219 my @forklist;
5220 my $check_forks = gitweb_check_feature('forks');
5221
5222 if ($check_forks) {
5223 @forklist = git_get_projects_list($project);
5224 }
5225
5226 git_header_html();
5227 git_print_page_nav('summary','', $head);
30c05d21
S
5228 print "<div class=\"title\">&nbsp;</div>\n";
5229 print "<table class=\"projects_list\">\n" .
5230 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5231 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5232 if (defined $cd{'rfc2822'}) {
5233 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5234 }
5235
5236 # use per project git URL list in $projectroot/$project/cloneurl
5237 # or make project git URL from git base URL and project name
5238 my $url_tag = "URL";
5239 my @url_list = git_get_project_url_list($project);
5240 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5241 foreach my $git_url (@url_list) {
5242 next unless $git_url;
5243 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
5244 $url_tag = "";
5245 }
8a1b4b56 5246 print "<tr id=\"metadata_owner\"><td>version</td><td>" . esc_html($version) . "</td></tr>\n";
0c0738da 5247 print "<tr id=\"metadata_owner\"><td>download</td><td>" . $download . "</td></tr>\n";
30c05d21
S
5248 # Tag cloud
5249 my $show_ctags = gitweb_check_feature('ctags');
5250 if ($show_ctags) {
5251 my $ctags = git_get_project_ctags($project);
5252 my $cloud = git_populate_project_tagcloud($ctags);
5253 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
5254 print "</td>\n<td>" unless %$ctags;
5255 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
5256 print "</td>\n<td>" if %$ctags;
5257 print git_show_project_tagcloud($cloud, 48);
5258 print "</td></tr>";
5259 }
5260
5261 print "</table>\n";
5262
5263 # If XSS prevention is on, we don't include README.html.
5264 # TODO: Allow a readme in some safe format.
5265 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5266 print "<div class=\"title\">readme</div>\n" .
5267 "<div class=\"readme\">\n";
5268 insert_file("$projectroot/$project/README.html");
5269 print "\n</div>\n"; # class="readme"
5270 }
5271
5272 # we need to request one more than 16 (0..15) to check if
5273 # those 16 are all
74867408 5274 my @commitlist = $head ? parse_commits("--all", 17) : ();
30c05d21
S
5275 if (@commitlist) {
5276 git_print_header_div('shortlog');
5277 git_shortlog_body(\@commitlist, 0, 15, $refs,
5278 $#commitlist <= 15 ? undef :
74867408 5279 $cgi->a({-href => href(action=>"shortlog")}, "..."), 0, 0, 0, 1);
30c05d21
S
5280 }
5281
5282 if (@taglist) {
5283 git_print_header_div('tags');
5284 git_tags_body(\@taglist, 0, 15,
5285 $#taglist <= 15 ? undef :
5286 $cgi->a({-href => href(action=>"tags")}, "..."));
5287 }
5288
5289 if (@headlist) {
5290 git_print_header_div('heads');
5291 git_heads_body(\@headlist, $head, 0, 15,
5292 $#headlist <= 15 ? undef :
5293 $cgi->a({-href => href(action=>"heads")}, "..."));
5294 }
5295
5296 if (@forklist) {
5297 git_print_header_div('forks');
5298 git_project_list_body(\@forklist, 'age', 0, 15,
5299 $#forklist <= 15 ? undef :
5300 $cgi->a({-href => href(action=>"forks")}, "..."),
5301 'no_header');
5302 }
5303
5304 git_footer_html();
5305}
5306
5307sub git_tag {
5308 my $head = git_get_head_hash($project);
5309 git_header_html();
5310 git_print_page_nav('','', $head,undef,$head);
5311 my %tag = parse_tag($hash);
5312
5313 if (! %tag) {
5314 die_error(404, "Unknown tag object");
5315 }
5316
5317 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5318 print "<div class=\"title_text\">\n" .
5319 "<table class=\"object_header\">\n" .
5320 "<tr>\n" .
5321 "<td>object</td>\n" .
5322 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5323 $tag{'object'}) . "</td>\n" .
5324 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5325 $tag{'type'}) . "</td>\n" .
5326 "</tr>\n";
5327 if (defined($tag{'author'})) {
5328 git_print_authorship_rows(\%tag, 'author');
5329 }
5330 print "</table>\n\n" .
5331 "</div>\n";
5332 print "<div class=\"page_body\">";
5333 my $comment = $tag{'comment'};
5334 foreach my $line (@$comment) {
5335 chomp $line;
5336 print esc_html($line, -nbsp=>1) . "<br/>\n";
5337 }
5338 print "</div>\n";
5339 git_footer_html();
5340}
5341
5342sub git_blame_common {
5343 my $format = shift || 'porcelain';
5344 if ($format eq 'porcelain' && $cgi->param('js')) {
5345 $format = 'incremental';
5346 $action = 'blame_incremental'; # for page title etc
5347 }
5348
5349 # permissions
5350 gitweb_check_feature('blame')
5351 or die_error(403, "Blame view not allowed");
5352
5353 # error checking
5354 die_error(400, "No file name given") unless $file_name;
5355 $hash_base ||= git_get_head_hash($project);
5356 die_error(404, "Couldn't find base commit") unless $hash_base;
5357 my %co = parse_commit($hash_base)
5358 or die_error(404, "Commit not found");
5359 my $ftype = "blob";
5360 if (!defined $hash) {
5361 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5362 or die_error(404, "Error looking up file");
5363 } else {
5364 $ftype = git_get_type($hash);
5365 if ($ftype !~ "blob") {
5366 die_error(400, "Object is not a blob");
5367 }
5368 }
5369
5370 my $fd;
5371 if ($format eq 'incremental') {
5372 # get file contents (as base)
5373 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5374 or die_error(500, "Open git-cat-file failed");
5375 } elsif ($format eq 'data') {
5376 # run git-blame --incremental
5377 open $fd, "-|", git_cmd(), "blame", "--incremental",
5378 $hash_base, "--", $file_name
5379 or die_error(500, "Open git-blame --incremental failed");
5380 } else {
5381 # run git-blame --porcelain
5382 open $fd, "-|", git_cmd(), "blame", '-p',
5383 $hash_base, '--', $file_name
5384 or die_error(500, "Open git-blame --porcelain failed");
5385 }
5386
5387 # incremental blame data returns early
5388 if ($format eq 'data') {
5389 print $cgi->header(
5390 -type=>"text/plain", -charset => "utf-8",
5391 -status=> "200 OK");
5392 local $| = 1; # output autoflush
5393 print while <$fd>;
5394 close $fd
5395 or print "ERROR $!\n";
5396
5397 print 'END';
5398 if (defined $t0 && gitweb_check_feature('timed')) {
5399 print ' '.
5400 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
5401 ' '.$number_of_git_cmds;
5402 }
5403 print "\n";
5404
5405 return;
5406 }
5407
5408 # page header
5409 git_header_html();
5410 my $formats_nav =
5411 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5412 "blob") .
5413 " | ";
5414 if ($format eq 'incremental') {
5415 $formats_nav .=
5416 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5417 "blame") . " (non-incremental)";
5418 } else {
5419 $formats_nav .=
5420 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5421 "blame") . " (incremental)";
5422 }
5423 $formats_nav .=
5424 " | " .
5425 $cgi->a({-href => href(action=>"history", -replay=>1)},
5426 "history") .
5427 " | " .
5428 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5429 "HEAD");
5430 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5431 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5432 git_print_page_path($file_name, $ftype, $hash_base);
5433
5434 # page body
5435 if ($format eq 'incremental') {
5436 print "<noscript>\n<div class=\"error\"><center><b>\n".
5437 "This page requires JavaScript to run.\n Use ".
5438 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5439 'this page').
5440 " instead.\n".
5441 "</b></center></div>\n</noscript>\n";
5442
5443 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5444 }
5445
5446 print qq!<div class="page_body">\n!;
5447 print qq!<div id="progress_info">... / ...</div>\n!
5448 if ($format eq 'incremental');
5449 print qq!<table id="blame_table" class="blame" width="100%">\n!.
5450 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5451 qq!<thead>\n!.
5452 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5453 qq!</thead>\n!.
5454 qq!<tbody>\n!;
5455
5456 my @rev_color = qw(light dark);
5457 my $num_colors = scalar(@rev_color);
5458 my $current_color = 0;
5459
5460 if ($format eq 'incremental') {
5461 my $color_class = $rev_color[$current_color];
5462
5463 #contents of a file
5464 my $linenr = 0;
5465 LINE:
5466 while (my $line = <$fd>) {
5467 chomp $line;
5468 $linenr++;
5469
5470 print qq!<tr id="l$linenr" class="$color_class">!.
5471 qq!<td class="sha1"><a href=""> </a></td>!.
5472 qq!<td class="linenr">!.
5473 qq!<a class="linenr" href="">$linenr</a></td>!;
5474 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5475 print qq!</tr>\n!;
5476 }
5477
5478 } else { # porcelain, i.e. ordinary blame
5479 my %metainfo = (); # saves information about commits
5480
5481 # blame data
5482 LINE:
5483 while (my $line = <$fd>) {
5484 chomp $line;
5485 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5486 # no <lines in group> for subsequent lines in group of lines
5487 my ($full_rev, $orig_lineno, $lineno, $group_size) =
5488 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5489 if (!exists $metainfo{$full_rev}) {
5490 $metainfo{$full_rev} = { 'nprevious' => 0 };
5491 }
5492 my $meta = $metainfo{$full_rev};
5493 my $data;
5494 while ($data = <$fd>) {
5495 chomp $data;
5496 last if ($data =~ s/^\t//); # contents of line
5497 if ($data =~ /^(\S+)(?: (.*))?$/) {
5498 $meta->{$1} = $2 unless exists $meta->{$1};
5499 }
5500 if ($data =~ /^previous /) {
5501 $meta->{'nprevious'}++;
5502 }
5503 }
5504 my $short_rev = substr($full_rev, 0, 8);
5505 my $author = $meta->{'author'};
5506 my %date =
5507 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5508 my $date = $date{'iso-tz'};
5509 if ($group_size) {
5510 $current_color = ($current_color + 1) % $num_colors;
5511 }
5512 my $tr_class = $rev_color[$current_color];
5513 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5514 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5515 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5516 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5517 if ($group_size) {
5518 print "<td class=\"sha1\"";
5519 print " title=\"". esc_html($author) . ", $date\"";
5520 print " rowspan=\"$group_size\"" if ($group_size > 1);
5521 print ">";
5522 print $cgi->a({-href => href(action=>"commit",
5523 hash=>$full_rev,
5524 file_name=>$file_name)},
5525 esc_html($short_rev));
5526 if ($group_size >= 2) {
5527 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5528 if (@author_initials) {
5529 print "<br />" .
5530 esc_html(join('', @author_initials));
5531 # or join('.', ...)
5532 }
5533 }
5534 print "</td>\n";
5535 }
5536 # 'previous' <sha1 of parent commit> <filename at commit>
5537 if (exists $meta->{'previous'} &&
5538 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5539 $meta->{'parent'} = $1;
5540 $meta->{'file_parent'} = unquote($2);
5541 }
5542 my $linenr_commit =
5543 exists($meta->{'parent'}) ?
5544 $meta->{'parent'} : $full_rev;
5545 my $linenr_filename =
5546 exists($meta->{'file_parent'}) ?
5547 $meta->{'file_parent'} : unquote($meta->{'filename'});
5548 my $blamed = href(action => 'blame',
5549 file_name => $linenr_filename,
5550 hash_base => $linenr_commit);
5551 print "<td class=\"linenr\">";
5552 print $cgi->a({ -href => "$blamed#l$orig_lineno",
5553 -class => "linenr" },
5554 esc_html($lineno));
5555 print "</td>";
5556 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5557 print "</tr>\n";
5558 } # end while
5559
5560 }
5561
5562 # footer
5563 print "</tbody>\n".
5564 "</table>\n"; # class="blame"
5565 print "</div>\n"; # class="blame_body"
5566 close $fd
5567 or print "Reading blob failed\n";
5568
5569 git_footer_html();
5570}
5571
5572sub git_blame {
5573 git_blame_common();
5574}
5575
5576sub git_blame_incremental {
5577 git_blame_common('incremental');
5578}
5579
5580sub git_blame_data {
5581 git_blame_common('data');
5582}
5583
5584sub git_tags {
5585 my $head = git_get_head_hash($project);
5586 git_header_html();
5587 git_print_page_nav('','', $head,undef,$head);
5588 git_print_header_div('summary', $project);
5589
5590 my @tagslist = git_get_tags_list();
5591 if (@tagslist) {
5592 git_tags_body(\@tagslist);
5593 }
5594 git_footer_html();
5595}
5596
5597sub git_heads {
5598 my $head = git_get_head_hash($project);
5599 git_header_html();
5600 git_print_page_nav('','', $head,undef,$head);
5601 git_print_header_div('summary', $project);
5602
5603 my @headslist = git_get_heads_list();
5604 if (@headslist) {
5605 git_heads_body(\@headslist, $head);
5606 }
5607 git_footer_html();
5608}
5609
5610sub git_blob_plain {
5611 my $type = shift;
5612 my $expires;
5613
5614 if (!defined $hash) {
5615 if (defined $file_name) {
5616 my $base = $hash_base || git_get_head_hash($project);
5617 $hash = git_get_hash_by_path($base, $file_name, "blob")
5618 or die_error(404, "Cannot find file");
5619 } else {
5620 die_error(400, "No file name defined");
5621 }
5622 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5623 # blobs defined by non-textual hash id's can be cached
5624 $expires = "+1d";
5625 }
5626
5627 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5628 or die_error(500, "Open git-cat-file blob '$hash' failed");
5629
5630 # content-type (can include charset)
5631 $type = blob_contenttype($fd, $file_name, $type);
5632
5633 # "save as" filename, even when no $file_name is given
5634 my $save_as = "$hash";
5635 if (defined $file_name) {
5636 $save_as = $file_name;
5637 } elsif ($type =~ m/^text\//) {
5638 $save_as .= '.txt';
5639 }
5640
5641 # With XSS prevention on, blobs of all types except a few known safe
5642 # ones are served with "Content-Disposition: attachment" to make sure
5643 # they don't run in our security domain. For certain image types,
5644 # blob view writes an <img> tag referring to blob_plain view, and we
5645 # want to be sure not to break that by serving the image as an
5646 # attachment (though Firefox 3 doesn't seem to care).
5647 my $sandbox = $prevent_xss &&
5648 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5649
5650 print $cgi->header(
5651 -type => $type,
5652 -expires => $expires,
5653 -content_disposition =>
5654 ($sandbox ? 'attachment' : 'inline')
5655 . '; filename="' . $save_as . '"');
5656 local $/ = undef;
5657 binmode STDOUT, ':raw';
5658 print <$fd>;
5659 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5660 close $fd;
5661}
5662
5663sub git_blob {
5664 my $expires;
5665
5666 if (!defined $hash) {
5667 if (defined $file_name) {
5668 my $base = $hash_base || git_get_head_hash($project);
5669 $hash = git_get_hash_by_path($base, $file_name, "blob")
5670 or die_error(404, "Cannot find file");
5671 } else {
5672 die_error(400, "No file name defined");
5673 }
5674 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5675 # blobs defined by non-textual hash id's can be cached
5676 $expires = "+1d";
5677 }
5678
5679 my $have_blame = gitweb_check_feature('blame');
5680 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5681 or die_error(500, "Couldn't cat $file_name, $hash");
5682 my $mimetype = blob_mimetype($fd, $file_name);
5683 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
5684 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5685 close $fd;
5686 return git_blob_plain($mimetype);
5687 }
5688 # we can have blame only for text/* mimetype
5689 $have_blame &&= ($mimetype =~ m!^text/!);
5690
5691 my $highlight = gitweb_check_feature('highlight');
5692 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
5693 $fd = run_highlighter($fd, $highlight, $syntax)
5694 if $syntax;
5695
5696 git_header_html(undef, $expires);
5697 my $formats_nav = '';
5698 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5699 if (defined $file_name) {
5700 if ($have_blame) {
5701 $formats_nav .=
5702 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5703 "blame") .
5704 " | ";
5705 }
5706 $formats_nav .=
5707 $cgi->a({-href => href(action=>"history", -replay=>1)},
5708 "history") .
5709 " | " .
5710 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5711 "raw") .
5712 " | " .
5713 $cgi->a({-href => href(action=>"blob",
5714 hash_base=>"HEAD", file_name=>$file_name)},
5715 "HEAD");
5716 } else {
5717 $formats_nav .=
5718 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5719 "raw");
5720 }
5721 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5722 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5723 } else {
5724 print "<div class=\"page_nav\">\n" .
5725 "<br/><br/></div>\n" .
5726 "<div class=\"title\">".esc_html($hash)."</div>\n";
5727 }
5728 git_print_page_path($file_name, "blob", $hash_base);
5729 print "<div class=\"page_body\">\n";
5730 if ($mimetype =~ m!^image/!) {
5731 print qq!<img type="!.esc_attr($mimetype).qq!"!;
5732 if ($file_name) {
5733 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
5734 }
5735 print qq! src="! .
5736 href(action=>"blob_plain", hash=>$hash,
5737 hash_base=>$hash_base, file_name=>$file_name) .
5738 qq!" />\n!;
5739 } else {
5740 my $nr;
5741 while (my $line = <$fd>) {
5742 chomp $line;
5743 $nr++;
5744 $line = untabify($line);
5745 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
5746 $nr, esc_attr(href(-replay => 1)), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
5747 }
5748 }
5749 close $fd
5750 or print "Reading blob failed.\n";
5751 print "</div>";
5752 git_footer_html();
5753}
5754
5755sub git_tree {
5756 if (!defined $hash_base) {
5757 $hash_base = "HEAD";
5758 }
5759 if (!defined $hash) {
5760 if (defined $file_name) {
5761 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5762 } else {
5763 $hash = $hash_base;
5764 }
5765 }
5766 die_error(404, "No such tree") unless defined($hash);
5767
5768 my $show_sizes = gitweb_check_feature('show-sizes');
5769 my $have_blame = gitweb_check_feature('blame');
5770
5771 my @entries = ();
5772 {
5773 local $/ = "\0";
5774 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5775 ($show_sizes ? '-l' : ()), @extra_options, $hash
5776 or die_error(500, "Open git-ls-tree failed");
5777 @entries = map { chomp; $_ } <$fd>;
5778 close $fd
5779 or die_error(404, "Reading tree failed");
5780 }
5781
5782 my $refs = git_get_references();
5783 my $ref = format_ref_marker($refs, $hash_base);
5784 git_header_html();
5785 my $basedir = '';
5786 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5787 my @views_nav = ();
5788 if (defined $file_name) {
5789 push @views_nav,
5790 $cgi->a({-href => href(action=>"history", -replay=>1)},
5791 "history"),
5792 $cgi->a({-href => href(action=>"tree",
5793 hash_base=>"HEAD", file_name=>$file_name)},
5794 "HEAD"),
5795 }
9086c806 5796 my $snapshot_links = "1";
30c05d21
S
5797 if (defined $snapshot_links) {
5798 # FIXME: Should be available when we have no hash base as well.
7e90fff2 5799 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
5800 }
5801 git_print_page_nav('tree','', $hash_base, undef, undef,
5802 join(' | ', @views_nav));
5803 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5804 } else {
5805 undef $hash_base;
5806 print "<div class=\"page_nav\">\n";
5807 print "<br/><br/></div>\n";
5808 print "<div class=\"title\">".esc_html($hash)."</div>\n";
5809 }
5810 if (defined $file_name) {
5811 $basedir = $file_name;
5812 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5813 $basedir .= '/';
5814 }
5815 git_print_page_path($file_name, 'tree', $hash_base);
5816 }
5817 print "<div class=\"page_body\">\n";
5818 print "<table class=\"tree\">\n";
5819 my $alternate = 1;
5820 # '..' (top directory) link if possible
5821 if (defined $hash_base &&
5822 defined $file_name && $file_name =~ m![^/]+$!) {
5823 if ($alternate) {
5824 print "<tr class=\"dark\">\n";
5825 } else {
5826 print "<tr class=\"light\">\n";
5827 }
5828 $alternate ^= 1;
5829
5830 my $up = $file_name;
5831 $up =~ s!/?[^/]+$!!;
5832 undef $up unless $up;
5833 # based on git_print_tree_entry
5834 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5835 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
5836 print '<td class="list">';
5837 print $cgi->a({-href => href(action=>"tree",
5838 hash_base=>$hash_base,
5839 file_name=>$up)},
5840 "..");
5841 print "</td>\n";
5842 print "<td class=\"link\"></td>\n";
5843
5844 print "</tr>\n";
5845 }
5846 foreach my $line (@entries) {
5847 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5848
5849 if ($alternate) {
5850 print "<tr class=\"dark\">\n";
5851 } else {
5852 print "<tr class=\"light\">\n";
5853 }
5854 $alternate ^= 1;
5855
5856 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5857
5858 print "</tr>\n";
5859 }
5860 print "</table>\n" .
5861 "</div>";
5862 git_footer_html();
5863}
5864
5865sub snapshot_name {
5866 my ($project, $hash) = @_;
5867
5868 # path/to/project.git -> project
5869 # path/to/project/.git -> project
5870 my $name = to_utf8($project);
5871 $name =~ s,([^/])/*\.git$,$1,;
5872 $name = basename($name);
5873 # sanitize name
5874 $name =~ s/[[:cntrl:]]/?/g;
5875
5876 my $ver = $hash;
5877 if ($hash =~ /^[0-9a-fA-F]+$/) {
5878 # shorten SHA-1 hash
5879 my $full_hash = git_get_full_hash($project, $hash);
5880 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
5881 $ver = git_get_short_hash($project, $hash);
5882 }
5883 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
5884 # tags don't need shortened SHA-1 hash
5885 $ver = $1;
5886 } else {
5887 # branches and other need shortened SHA-1 hash
5888 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
5889 $ver = $1;
5890 }
5891 $ver .= '-' . git_get_short_hash($project, $hash);
5892 }
5893 # in case of hierarchical branch names
5894 $ver =~ s!/!.!g;
5895
5896 # name = project-version_string
5897 $name = "$name-$ver";
5898
5899 return wantarray ? ($name, $name) : $name;
5900}
5901
5902sub git_snapshot {
5903 my $format = $input_params{'snapshot_format'};
5904 if (!@snapshot_fmts) {
5905 die_error(403, "Snapshots not allowed");
5906 }
5907 # default to first supported snapshot format
5908 $format ||= $snapshot_fmts[0];
5909 if ($format !~ m/^[a-z0-9]+$/) {
5910 die_error(400, "Invalid snapshot format parameter");
5911 } elsif (!exists($known_snapshot_formats{$format})) {
5912 die_error(400, "Unknown snapshot format");
5913 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5914 die_error(403, "Snapshot format not allowed");
5915 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5916 die_error(403, "Unsupported snapshot format");
5917 }
5918
5919 my $type = git_get_type("$hash^{}");
5920 if (!$type) {
5921 die_error(404, 'Object does not exist');
5922 } elsif ($type eq 'blob') {
5923 die_error(400, 'Object is not a tree-ish');
5924 }
5925
5926 my ($name, $prefix) = snapshot_name($project, $hash);
5927 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
5928 my $cmd = quote_command(
5929 git_cmd(), 'archive',
5930 "--format=$known_snapshot_formats{$format}{'format'}",
5931 "--prefix=$prefix/", $hash);
5932 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5933 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5934 }
5935
5936 $filename =~ s/(["\\])/\\$1/g;
5937 print $cgi->header(
5938 -type => $known_snapshot_formats{$format}{'type'},
5939 -content_disposition => 'inline; filename="' . $filename . '"',
5940 -status => '200 OK');
5941
5942 open my $fd, "-|", $cmd
5943 or die_error(500, "Execute git-archive failed");
5944 binmode STDOUT, ':raw';
5945 print <$fd>;
5946 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5947 close $fd;
5948}
5949
5950sub git_log_generic {
5951 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
5952
5953 my $head = git_get_head_hash($project);
74867408 5954 my $allrefs;
30c05d21
S
5955 if (!defined $base) {
5956 $base = $head;
74867408 5957 $allrefs = 1;
30c05d21
S
5958 }
5959 if (!defined $page) {
5960 $page = 0;
5961 }
5962 my $refs = git_get_references();
5963
5964 my $commit_hash = $base;
74867408
S
5965 if (defined $allrefs) {
5966 $commit_hash = "--all";
5967 }
30c05d21
S
5968 if (defined $parent) {
5969 $commit_hash = "$parent..$base";
5970 }
5971 my @commitlist =
5972 parse_commits($commit_hash, 101, (100 * $page),
5973 defined $file_name ? ($file_name, "--full-history") : ());
5974
5975 my $ftype;
5976 if (!defined $file_hash && defined $file_name) {
5977 # some commits could have deleted file in question,
5978 # and not have it in tree, but one of them has to have it
5979 for (my $i = 0; $i < @commitlist; $i++) {
5980 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5981 last if defined $file_hash;
5982 }
5983 }
5984 if (defined $file_hash) {
5985 $ftype = git_get_type($file_hash);
5986 }
5987 if (defined $file_name && !defined $ftype) {
5988 die_error(500, "Unknown type of object");
5989 }
5990 my %co;
5991 if (defined $file_name) {
5992 %co = parse_commit($base)
5993 or die_error(404, "Unknown commit object");
5994 }
5995
5996
5997 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
5998 my $next_link = '';
5999 if ($#commitlist >= 100) {
6000 $next_link =
6001 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6002 -accesskey => "n", -title => "Alt-n"}, "next");
6003 }
6004 my $patch_max = gitweb_get_feature('patches');
6005 if ($patch_max && !defined $file_name) {
6006 if ($patch_max < 0 || @commitlist <= $patch_max) {
6007 $paging_nav .= " &sdot; " .
6008 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6009 "patches");
6010 }
6011 }
6012
6013 git_header_html();
6014 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
6015 if (defined $file_name) {
6016 git_print_header_div('commit', esc_html($co{'title'}), $base);
6017 } else {
6018 git_print_header_div('summary', $project)
6019 }
6020 git_print_page_path($file_name, $ftype, $hash_base)
6021 if (defined $file_name);
6022
6023 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
74867408 6024 $file_name, $file_hash, $ftype, $allrefs);
30c05d21
S
6025
6026 git_footer_html();
6027}
6028
6029sub git_log {
6030 git_log_generic('log', \&git_log_body,
6031 $hash, $hash_parent);
6032}
6033
6034sub git_commit {
6035 $hash ||= $hash_base || "HEAD";
6036 my %co = parse_commit($hash)
6037 or die_error(404, "Unknown commit object");
6038
6039 my $parent = $co{'parent'};
6040 my $parents = $co{'parents'}; # listref
6041
6042 # we need to prepare $formats_nav before any parameter munging
6043 my $formats_nav;
6044 if (!defined $parent) {
6045 # --root commitdiff
6046 $formats_nav .= '(initial)';
6047 } elsif (@$parents == 1) {
6048 # single parent commit
6049 $formats_nav .=
6050 '(parent: ' .
6051 $cgi->a({-href => href(action=>"commit",
6052 hash=>$parent)},
6053 esc_html(substr($parent, 0, 7))) .
6054 ')';
6055 } else {
6056 # merge commit
6057 $formats_nav .=
6058 '(merge: ' .
6059 join(' ', map {
6060 $cgi->a({-href => href(action=>"commit",
6061 hash=>$_)},
6062 esc_html(substr($_, 0, 7)));
6063 } @$parents ) .
6064 ')';
6065 }
6066 if (gitweb_check_feature('patches') && @$parents <= 1) {
6067 $formats_nav .= " | " .
6068 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6069 "patch");
6070 }
6071
6072 if (!defined $parent) {
6073 $parent = "--root";
6074 }
6075 my @difftree;
6076 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
6077 @diff_opts,
6078 (@$parents <= 1 ? $parent : '-c'),
6079 $hash, "--"
6080 or die_error(500, "Open git-diff-tree failed");
6081 @difftree = map { chomp; $_ } <$fd>;
6082 close $fd or die_error(404, "Reading git-diff-tree failed");
6083
6084 # non-textual hash id's can be cached
6085 my $expires;
6086 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6087 $expires = "+1d";
6088 }
6089 my $refs = git_get_references();
6090 my $ref = format_ref_marker($refs, $co{'id'});
6091
6092 git_header_html(undef, $expires);
6093 git_print_page_nav('commit', '',
6094 $hash, $co{'tree'}, $hash,
6095 $formats_nav);
6096
6097 if (defined $co{'parent'}) {
6098 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
6099 } else {
6100 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
6101 }
6102 print "<div class=\"title_text\">\n" .
6103 "<table class=\"object_header\">\n";
6104 git_print_authorship_rows(\%co);
6105 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
6106 print "<tr>" .
6107 "<td>tree</td>" .
6108 "<td class=\"sha1\">" .
6109 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
6110 class => "list"}, $co{'tree'}) .
6111 "</td>" .
6112 "<td class=\"link\">" .
6113 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
6114 "tree");
9086c806 6115 my $snapshot_links = "1";
30c05d21 6116 if (defined $snapshot_links) {
7e90fff2 6117 print " | " . "<a title='in format: tar.gz' href='http://git.nexus-irc.de/git_download.php?build2=".$project.";h=".$hash."'>snapshot</a>";
30c05d21
S
6118 }
6119 print "</td>" .
6120 "</tr>\n";
6121
6122 foreach my $par (@$parents) {
6123 print "<tr>" .
6124 "<td>parent</td>" .
6125 "<td class=\"sha1\">" .
6126 $cgi->a({-href => href(action=>"commit", hash=>$par),
6127 class => "list"}, $par) .
6128 "</td>" .
6129 "<td class=\"link\">" .
6130 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
6131 " | " .
6132 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
6133 "</td>" .
6134 "</tr>\n";
6135 }
6136 print "</table>".
6137 "</div>\n";
6138
6139 print "<div class=\"page_body\">\n";
6140 git_print_log($co{'comment'});
6141 print "</div>\n";
6142
6143 git_difftree_body(\@difftree, $hash, @$parents);
6144
6145 git_footer_html();
6146}
6147
6148sub git_object {
6149 # object is defined by:
6150 # - hash or hash_base alone
6151 # - hash_base and file_name
6152 my $type;
6153
6154 # - hash or hash_base alone
6155 if ($hash || ($hash_base && !defined $file_name)) {
6156 my $object_id = $hash || $hash_base;
6157
6158 open my $fd, "-|", quote_command(
6159 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
6160 or die_error(404, "Object does not exist");
6161 $type = <$fd>;
6162 chomp $type;
6163 close $fd
6164 or die_error(404, "Object does not exist");
6165
6166 # - hash_base and file_name
6167 } elsif ($hash_base && defined $file_name) {
6168 $file_name =~ s,/+$,,;
6169
6170 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
6171 or die_error(404, "Base object does not exist");
6172
6173 # here errors should not hapen
6174 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
6175 or die_error(500, "Open git-ls-tree failed");
6176 my $line = <$fd>;
6177 close $fd;
6178
6179 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
6180 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
6181 die_error(404, "File or directory for given base does not exist");
6182 }
6183 $type = $2;
6184 $hash = $3;
6185 } else {
6186 die_error(400, "Not enough information to find object");
6187 }
6188
6189 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
6190 hash=>$hash, hash_base=>$hash_base,
6191 file_name=>$file_name),
6192 -status => '302 Found');
6193}
6194
6195sub git_blobdiff {
6196 my $format = shift || 'html';
6197
6198 my $fd;
6199 my @difftree;
6200 my %diffinfo;
6201 my $expires;
6202
6203 # preparing $fd and %diffinfo for git_patchset_body
6204 # new style URI
6205 if (defined $hash_base && defined $hash_parent_base) {
6206 if (defined $file_name) {
6207 # read raw output
6208 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6209 $hash_parent_base, $hash_base,
6210 "--", (defined $file_parent ? $file_parent : ()), $file_name
6211 or die_error(500, "Open git-diff-tree failed");
6212 @difftree = map { chomp; $_ } <$fd>;
6213 close $fd
6214 or die_error(404, "Reading git-diff-tree failed");
6215 @difftree
6216 or die_error(404, "Blob diff not found");
6217
6218 } elsif (defined $hash &&
6219 $hash =~ /[0-9a-fA-F]{40}/) {
6220 # try to find filename from $hash
6221
6222 # read filtered raw output
6223 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6224 $hash_parent_base, $hash_base, "--"
6225 or die_error(500, "Open git-diff-tree failed");
6226 @difftree =
6227 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
6228 # $hash == to_id
6229 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
6230 map { chomp; $_ } <$fd>;
6231 close $fd
6232 or die_error(404, "Reading git-diff-tree failed");
6233 @difftree
6234 or die_error(404, "Blob diff not found");
6235
6236 } else {
6237 die_error(400, "Missing one of the blob diff parameters");
6238 }
6239
6240 if (@difftree > 1) {
6241 die_error(400, "Ambiguous blob diff specification");
6242 }
6243
6244 %diffinfo = parse_difftree_raw_line($difftree[0]);
6245 $file_parent ||= $diffinfo{'from_file'} || $file_name;
6246 $file_name ||= $diffinfo{'to_file'};
6247
6248 $hash_parent ||= $diffinfo{'from_id'};
6249 $hash ||= $diffinfo{'to_id'};
6250
6251 # non-textual hash id's can be cached
6252 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6253 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6254 $expires = '+1d';
6255 }
6256
6257 # open patch output
6258 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6259 '-p', ($format eq 'html' ? "--full-index" : ()),
6260 $hash_parent_base, $hash_base,
6261 "--", (defined $file_parent ? $file_parent : ()), $file_name
6262 or die_error(500, "Open git-diff-tree failed");
6263 }
6264
6265 # old/legacy style URI -- not generated anymore since 1.4.3.
6266 if (!%diffinfo) {
6267 die_error('404 Not Found', "Missing one of the blob diff parameters")
6268 }
6269
6270 # header
6271 if ($format eq 'html') {
6272 my $formats_nav =
6273 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6274 "raw");
6275 git_header_html(undef, $expires);
6276 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6277 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6278 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6279 } else {
6280 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6281 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
6282 }
6283 if (defined $file_name) {
6284 git_print_page_path($file_name, "blob", $hash_base);
6285 } else {
6286 print "<div class=\"page_path\"></div>\n";
6287 }
6288
6289 } elsif ($format eq 'plain') {
6290 print $cgi->header(
6291 -type => 'text/plain',
6292 -charset => 'utf-8',
6293 -expires => $expires,
6294 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6295
6296 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6297
6298 } else {
6299 die_error(400, "Unknown blobdiff format");
6300 }
6301
6302 # patch
6303 if ($format eq 'html') {
6304 print "<div class=\"page_body\">\n";
6305
6306 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6307 close $fd;
6308
6309 print "</div>\n"; # class="page_body"
6310 git_footer_html();
6311
6312 } else {
6313 while (my $line = <$fd>) {
6314 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6315 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6316
6317 print $line;
6318
6319 last if $line =~ m!^\+\+\+!;
6320 }
6321 local $/ = undef;
6322 print <$fd>;
6323 close $fd;
6324 }
6325}
6326
6327sub git_blobdiff_plain {
6328 git_blobdiff('plain');
6329}
6330
6331sub git_commitdiff {
6332 my %params = @_;
6333 my $format = $params{-format} || 'html';
6334
6335 my ($patch_max) = gitweb_get_feature('patches');
6336 if ($format eq 'patch') {
6337 die_error(403, "Patch view not allowed") unless $patch_max;
6338 }
6339
6340 $hash ||= $hash_base || "HEAD";
6341 my %co = parse_commit($hash)
6342 or die_error(404, "Unknown commit object");
6343
6344 # choose format for commitdiff for merge
6345 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6346 $hash_parent = '--cc';
6347 }
6348 # we need to prepare $formats_nav before almost any parameter munging
6349 my $formats_nav;
6350 if ($format eq 'html') {
6351 $formats_nav =
6352 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6353 "raw");
6354 if ($patch_max && @{$co{'parents'}} <= 1) {
6355 $formats_nav .= " | " .
6356 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6357 "patch");
6358 }
6359
6360 if (defined $hash_parent &&
6361 $hash_parent ne '-c' && $hash_parent ne '--cc') {
6362 # commitdiff with two commits given
6363 my $hash_parent_short = $hash_parent;
6364 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6365 $hash_parent_short = substr($hash_parent, 0, 7);
6366 }
6367 $formats_nav .=
6368 ' (from';
6369 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6370 if ($co{'parents'}[$i] eq $hash_parent) {
6371 $formats_nav .= ' parent ' . ($i+1);
6372 last;
6373 }
6374 }
6375 $formats_nav .= ': ' .
6376 $cgi->a({-href => href(action=>"commitdiff",
6377 hash=>$hash_parent)},
6378 esc_html($hash_parent_short)) .
6379 ')';
6380 } elsif (!$co{'parent'}) {
6381 # --root commitdiff
6382 $formats_nav .= ' (initial)';
6383 } elsif (scalar @{$co{'parents'}} == 1) {
6384 # single parent commit
6385 $formats_nav .=
6386 ' (parent: ' .
6387 $cgi->a({-href => href(action=>"commitdiff",
6388 hash=>$co{'parent'})},
6389 esc_html(substr($co{'parent'}, 0, 7))) .
6390 ')';
6391 } else {
6392 # merge commit
6393 if ($hash_parent eq '--cc') {
6394 $formats_nav .= ' | ' .
6395 $cgi->a({-href => href(action=>"commitdiff",
6396 hash=>$hash, hash_parent=>'-c')},
6397 'combined');
6398 } else { # $hash_parent eq '-c'
6399 $formats_nav .= ' | ' .
6400 $cgi->a({-href => href(action=>"commitdiff",
6401 hash=>$hash, hash_parent=>'--cc')},
6402 'compact');
6403 }
6404 $formats_nav .=
6405 ' (merge: ' .
6406 join(' ', map {
6407 $cgi->a({-href => href(action=>"commitdiff",
6408 hash=>$_)},
6409 esc_html(substr($_, 0, 7)));
6410 } @{$co{'parents'}} ) .
6411 ')';
6412 }
6413 }
6414
6415 my $hash_parent_param = $hash_parent;
6416 if (!defined $hash_parent_param) {
6417 # --cc for multiple parents, --root for parentless
6418 $hash_parent_param =
6419 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6420 }
6421
6422 # read commitdiff
6423 my $fd;
6424 my @difftree;
6425 if ($format eq 'html') {
6426 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6427 "--no-commit-id", "--patch-with-raw", "--full-index",
6428 $hash_parent_param, $hash, "--"
6429 or die_error(500, "Open git-diff-tree failed");
6430
6431 while (my $line = <$fd>) {
6432 chomp $line;
6433 # empty line ends raw part of diff-tree output
6434 last unless $line;
6435 push @difftree, scalar parse_difftree_raw_line($line);
6436 }
6437
6438 } elsif ($format eq 'plain') {
6439 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6440 '-p', $hash_parent_param, $hash, "--"
6441 or die_error(500, "Open git-diff-tree failed");
6442 } elsif ($format eq 'patch') {
6443 # For commit ranges, we limit the output to the number of
6444 # patches specified in the 'patches' feature.
6445 # For single commits, we limit the output to a single patch,
6446 # diverging from the git-format-patch default.
6447 my @commit_spec = ();
6448 if ($hash_parent) {
6449 if ($patch_max > 0) {
6450 push @commit_spec, "-$patch_max";
6451 }
6452 push @commit_spec, '-n', "$hash_parent..$hash";
6453 } else {
6454 if ($params{-single}) {
6455 push @commit_spec, '-1';
6456 } else {
6457 if ($patch_max > 0) {
6458 push @commit_spec, "-$patch_max";
6459 }
6460 push @commit_spec, "-n";
6461 }
6462 push @commit_spec, '--root', $hash;
6463 }
6464 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
6465 '--stdout', @commit_spec
6466 or die_error(500, "Open git-format-patch failed");
6467 } else {
6468 die_error(400, "Unknown commitdiff format");
6469 }
6470
6471 # non-textual hash id's can be cached
6472 my $expires;
6473 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6474 $expires = "+1d";
6475 }
6476
6477 # write commit message
6478 if ($format eq 'html') {
6479 my $refs = git_get_references();
6480 my $ref = format_ref_marker($refs, $co{'id'});
6481
6482 git_header_html(undef, $expires);
6483 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6484 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6485 print "<div class=\"title_text\">\n" .
6486 "<table class=\"object_header\">\n";
6487 git_print_authorship_rows(\%co);
6488 print "</table>".
6489 "</div>\n";
6490 print "<div class=\"page_body\">\n";
6491 if (@{$co{'comment'}} > 1) {
6492 print "<div class=\"log\">\n";
6493 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6494 print "</div>\n"; # class="log"
6495 }
6496
6497 } elsif ($format eq 'plain') {
6498 my $refs = git_get_references("tags");
6499 my $tagname = git_get_rev_name_tags($hash);
6500 my $filename = basename($project) . "-$hash.patch";
6501
6502 print $cgi->header(
6503 -type => 'text/plain',
6504 -charset => 'utf-8',
6505 -expires => $expires,
6506 -content_disposition => 'inline; filename="' . "$filename" . '"');
6507 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6508 print "From: " . to_utf8($co{'author'}) . "\n";
6509 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6510 print "Subject: " . to_utf8($co{'title'}) . "\n";
6511
6512 print "X-Git-Tag: $tagname\n" if $tagname;
6513 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6514
6515 foreach my $line (@{$co{'comment'}}) {
6516 print to_utf8($line) . "\n";
6517 }
6518 print "---\n\n";
6519 } elsif ($format eq 'patch') {
6520 my $filename = basename($project) . "-$hash.patch";
6521
6522 print $cgi->header(
6523 -type => 'text/plain',
6524 -charset => 'utf-8',
6525 -expires => $expires,
6526 -content_disposition => 'inline; filename="' . "$filename" . '"');
6527 }
6528
6529 # write patch
6530 if ($format eq 'html') {
6531 my $use_parents = !defined $hash_parent ||
6532 $hash_parent eq '-c' || $hash_parent eq '--cc';
6533 git_difftree_body(\@difftree, $hash,
6534 $use_parents ? @{$co{'parents'}} : $hash_parent);
6535 print "<br/>\n";
6536
6537 git_patchset_body($fd, \@difftree, $hash,
6538 $use_parents ? @{$co{'parents'}} : $hash_parent);
6539 close $fd;
6540 print "</div>\n"; # class="page_body"
6541 git_footer_html();
6542
6543 } elsif ($format eq 'plain') {
6544 local $/ = undef;
6545 print <$fd>;
6546 close $fd
6547 or print "Reading git-diff-tree failed\n";
6548 } elsif ($format eq 'patch') {
6549 local $/ = undef;
6550 print <$fd>;
6551 close $fd
6552 or print "Reading git-format-patch failed\n";
6553 }
6554}
6555
6556sub git_commitdiff_plain {
6557 git_commitdiff(-format => 'plain');
6558}
6559
6560# format-patch-style patches
6561sub git_patch {
6562 git_commitdiff(-format => 'patch', -single => 1);
6563}
6564
6565sub git_patches {
6566 git_commitdiff(-format => 'patch');
6567}
6568
6569sub git_history {
6570 git_log_generic('history', \&git_history_body,
6571 $hash_base, $hash_parent_base,
6572 $file_name, $hash);
6573}
6574
6575sub git_search {
6576 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6577 if (!defined $searchtext) {
6578 die_error(400, "Text field is empty");
6579 }
6580 if (!defined $hash) {
6581 $hash = git_get_head_hash($project);
6582 }
6583 my %co = parse_commit($hash);
6584 if (!%co) {
6585 die_error(404, "Unknown commit object");
6586 }
6587 if (!defined $page) {
6588 $page = 0;
6589 }
6590
6591 $searchtype ||= 'commit';
6592 if ($searchtype eq 'pickaxe') {
6593 # pickaxe may take all resources of your box and run for several minutes
6594 # with every query - so decide by yourself how public you make this feature
6595 gitweb_check_feature('pickaxe')
6596 or die_error(403, "Pickaxe is disabled");
6597 }
6598 if ($searchtype eq 'grep') {
6599 gitweb_check_feature('grep')
6600 or die_error(403, "Grep is disabled");
6601 }
6602
6603 git_header_html();
6604
6605 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6606 my $greptype;
6607 if ($searchtype eq 'commit') {
6608 $greptype = "--grep=";
6609 } elsif ($searchtype eq 'author') {
6610 $greptype = "--author=";
6611 } elsif ($searchtype eq 'committer') {
6612 $greptype = "--committer=";
6613 }
6614 $greptype .= $searchtext;
6615 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6616 $greptype, '--regexp-ignore-case',
6617 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6618
6619 my $paging_nav = '';
6620 if ($page > 0) {
6621 $paging_nav .=
6622 $cgi->a({-href => href(action=>"search", hash=>$hash,
6623 searchtext=>$searchtext,
6624 searchtype=>$searchtype)},
6625 "first");
6626 $paging_nav .= " &sdot; " .
6627 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6628 -accesskey => "p", -title => "Alt-p"}, "prev");
6629 } else {
6630 $paging_nav .= "first";
6631 $paging_nav .= " &sdot; prev";
6632 }
6633 my $next_link = '';
6634 if ($#commitlist >= 100) {
6635 $next_link =
6636 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6637 -accesskey => "n", -title => "Alt-n"}, "next");
6638 $paging_nav .= " &sdot; $next_link";
6639 } else {
6640 $paging_nav .= " &sdot; next";
6641 }
6642
6643 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6644 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6645 if ($page == 0 && !@commitlist) {
6646 print "<p>No match.</p>\n";
6647 } else {
6648 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6649 }
6650 }
6651
6652 if ($searchtype eq 'pickaxe') {
6653 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6654 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6655
6656 print "<table class=\"pickaxe search\">\n";
6657 my $alternate = 1;
6658 local $/ = "\n";
6659 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6660 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6661 ($search_use_regexp ? '--pickaxe-regex' : ());
6662 undef %co;
6663 my @files;
6664 while (my $line = <$fd>) {
6665 chomp $line;
6666 next unless $line;
6667
6668 my %set = parse_difftree_raw_line($line);
6669 if (defined $set{'commit'}) {
6670 # finish previous commit
6671 if (%co) {
6672 print "</td>\n" .
6673 "<td class=\"link\">" .
6674 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6675 " | " .
6676 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6677 print "</td>\n" .
6678 "</tr>\n";
6679 }
6680
6681 if ($alternate) {
6682 print "<tr class=\"dark\">\n";
6683 } else {
6684 print "<tr class=\"light\">\n";
6685 }
6686 $alternate ^= 1;
6687 %co = parse_commit($set{'commit'});
6688 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6689 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6690 "<td><i>$author</i></td>\n" .
6691 "<td>" .
6692 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6693 -class => "list subject"},
6694 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6695 } elsif (defined $set{'to_id'}) {
6696 next if ($set{'to_id'} =~ m/^0{40}$/);
6697
6698 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6699 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6700 -class => "list"},
6701 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6702 "<br/>\n";
6703 }
6704 }
6705 close $fd;
6706
6707 # finish last commit (warning: repetition!)
6708 if (%co) {
6709 print "</td>\n" .
6710 "<td class=\"link\">" .
6711 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6712 " | " .
6713 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6714 print "</td>\n" .
6715 "</tr>\n";
6716 }
6717
6718 print "</table>\n";
6719 }
6720
6721 if ($searchtype eq 'grep') {
6722 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6723 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6724
6725 print "<table class=\"grep_search\">\n";
6726 my $alternate = 1;
6727 my $matches = 0;
6728 local $/ = "\n";
6729 open my $fd, "-|", git_cmd(), 'grep', '-n',
6730 $search_use_regexp ? ('-E', '-i') : '-F',
6731 $searchtext, $co{'tree'};
6732 my $lastfile = '';
6733 while (my $line = <$fd>) {
6734 chomp $line;
6735 my ($file, $lno, $ltext, $binary);
6736 last if ($matches++ > 1000);
6737 if ($line =~ /^Binary file (.+) matches$/) {
6738 $file = $1;
6739 $binary = 1;
6740 } else {
6741 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6742 }
6743 if ($file ne $lastfile) {
6744 $lastfile and print "</td></tr>\n";
6745 if ($alternate++) {
6746 print "<tr class=\"dark\">\n";
6747 } else {
6748 print "<tr class=\"light\">\n";
6749 }
6750 print "<td class=\"list\">".
6751 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6752 file_name=>"$file"),
6753 -class => "list"}, esc_path($file));
6754 print "</td><td>\n";
6755 $lastfile = $file;
6756 }
6757 if ($binary) {
6758 print "<div class=\"binary\">Binary file</div>\n";
6759 } else {
6760 $ltext = untabify($ltext);
6761 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6762 $ltext = esc_html($1, -nbsp=>1);
6763 $ltext .= '<span class="match">';
6764 $ltext .= esc_html($2, -nbsp=>1);
6765 $ltext .= '</span>';
6766 $ltext .= esc_html($3, -nbsp=>1);
6767 } else {
6768 $ltext = esc_html($ltext, -nbsp=>1);
6769 }
6770 print "<div class=\"pre\">" .
6771 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6772 file_name=>"$file").'#l'.$lno,
6773 -class => "linenr"}, sprintf('%4i', $lno))
6774 . ' ' . $ltext . "</div>\n";
6775 }
6776 }
6777 if ($lastfile) {
6778 print "</td></tr>\n";
6779 if ($matches > 1000) {
6780 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6781 }
6782 } else {
6783 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6784 }
6785 close $fd;
6786
6787 print "</table>\n";
6788 }
6789 git_footer_html();
6790}
6791
6792sub git_search_help {
6793 git_header_html();
6794 git_print_page_nav('','', $hash,$hash,$hash);
6795 print <<EOT;
6796<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6797regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6798the pattern entered is recognized as the POSIX extended
6799<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6800insensitive).</p>
6801<dl>
6802<dt><b>commit</b></dt>
6803<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6804EOT
6805 my $have_grep = gitweb_check_feature('grep');
6806 if ($have_grep) {
6807 print <<EOT;
6808<dt><b>grep</b></dt>
6809<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6810 a different one) are searched for the given pattern. On large trees, this search can take
6811a while and put some strain on the server, so please use it with some consideration. Note that
6812due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6813case-sensitive.</dd>
6814EOT
6815 }
6816 print <<EOT;
6817<dt><b>author</b></dt>
6818<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6819<dt><b>committer</b></dt>
6820<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6821EOT
6822 my $have_pickaxe = gitweb_check_feature('pickaxe');
6823 if ($have_pickaxe) {
6824 print <<EOT;
6825<dt><b>pickaxe</b></dt>
6826<dd>All commits that caused the string to appear or disappear from any file (changes that
6827added, removed or "modified" the string) will be listed. This search can take a while and
6828takes a lot of strain on the server, so please use it wisely. Note that since you may be
6829interested even in changes just changing the case as well, this search is case sensitive.</dd>
6830EOT
6831 }
6832 print "</dl>\n";
6833 git_footer_html();
6834}
6835
6836sub git_shortlog {
6837 git_log_generic('shortlog', \&git_shortlog_body,
6838 $hash, $hash_parent);
6839}
6840
6841## ......................................................................
6842## feeds (RSS, Atom; OPML)
6843
6844sub git_feed {
6845 my $format = shift || 'atom';
6846 my $have_blame = gitweb_check_feature('blame');
6847
6848 # Atom: http://www.atomenabled.org/developers/syndication/
6849 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6850 if ($format ne 'rss' && $format ne 'atom') {
6851 die_error(400, "Unknown web feed format");
6852 }
6853
6854 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6855 my $head = $hash || 'HEAD';
6856 my @commitlist = parse_commits($head, 150, 0, $file_name);
6857
6858 my %latest_commit;
6859 my %latest_date;
6860 my $content_type = "application/$format+xml";
6861 if (defined $cgi->http('HTTP_ACCEPT') &&
6862 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6863 # browser (feed reader) prefers text/xml
6864 $content_type = 'text/xml';
6865 }
6866 if (defined($commitlist[0])) {
6867 %latest_commit = %{$commitlist[0]};
6868 my $latest_epoch = $latest_commit{'committer_epoch'};
6869 %latest_date = parse_date($latest_epoch);
6870 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6871 if (defined $if_modified) {
6872 my $since;
6873 if (eval { require HTTP::Date; 1; }) {
6874 $since = HTTP::Date::str2time($if_modified);
6875 } elsif (eval { require Time::ParseDate; 1; }) {
6876 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6877 }
6878 if (defined $since && $latest_epoch <= $since) {
6879 print $cgi->header(
6880 -type => $content_type,
6881 -charset => 'utf-8',
6882 -last_modified => $latest_date{'rfc2822'},
6883 -status => '304 Not Modified');
6884 return;
6885 }
6886 }
6887 print $cgi->header(
6888 -type => $content_type,
6889 -charset => 'utf-8',
6890 -last_modified => $latest_date{'rfc2822'});
6891 } else {
6892 print $cgi->header(
6893 -type => $content_type,
6894 -charset => 'utf-8');
6895 }
6896
6897 # Optimization: skip generating the body if client asks only
6898 # for Last-Modified date.
6899 return if ($cgi->request_method() eq 'HEAD');
6900
6901 # header variables
6902 my $title = "$site_name - $project/$action";
6903 my $feed_type = 'log';
6904 if (defined $hash) {
6905 $title .= " - '$hash'";
6906 $feed_type = 'branch log';
6907 if (defined $file_name) {
6908 $title .= " :: $file_name";
6909 $feed_type = 'history';
6910 }
6911 } elsif (defined $file_name) {
6912 $title .= " - $file_name";
6913 $feed_type = 'history';
6914 }
6915 $title .= " $feed_type";
6916 my $descr = git_get_project_description($project);
6917 if (defined $descr) {
6918 $descr = esc_html($descr);
6919 } else {
6920 $descr = "$project " .
6921 ($format eq 'rss' ? 'RSS' : 'Atom') .
6922 " feed";
6923 }
6924 my $owner = git_get_project_owner($project);
6925 $owner = esc_html($owner);
6926
6927 #header
6928 my $alt_url;
6929 if (defined $file_name) {
6930 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6931 } elsif (defined $hash) {
6932 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6933 } else {
6934 $alt_url = href(-full=>1, action=>"summary");
6935 }
6936 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6937 if ($format eq 'rss') {
6938 print <<XML;
6939<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6940<channel>
6941XML
6942 print "<title>$title</title>\n" .
6943 "<link>$alt_url</link>\n" .
6944 "<description>$descr</description>\n" .
6945 "<language>en</language>\n" .
6946 # project owner is responsible for 'editorial' content
6947 "<managingEditor>$owner</managingEditor>\n";
6948 if (defined $logo || defined $favicon) {
6949 # prefer the logo to the favicon, since RSS
6950 # doesn't allow both
6951 my $img = esc_url($logo || $favicon);
6952 print "<image>\n" .
6953 "<url>$img</url>\n" .
6954 "<title>$title</title>\n" .
6955 "<link>$alt_url</link>\n" .
6956 "</image>\n";
6957 }
6958 if (%latest_date) {
6959 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6960 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6961 }
6962 print "<generator>gitweb v.$version/$git_version</generator>\n";
6963 } elsif ($format eq 'atom') {
6964 print <<XML;
6965<feed xmlns="http://www.w3.org/2005/Atom">
6966XML
6967 print "<title>$title</title>\n" .
6968 "<subtitle>$descr</subtitle>\n" .
6969 '<link rel="alternate" type="text/html" href="' .
6970 $alt_url . '" />' . "\n" .
6971 '<link rel="self" type="' . $content_type . '" href="' .
6972 $cgi->self_url() . '" />' . "\n" .
6973 "<id>" . href(-full=>1) . "</id>\n" .
6974 # use project owner for feed author
6975 "<author><name>$owner</name></author>\n";
6976 if (defined $favicon) {
6977 print "<icon>" . esc_url($favicon) . "</icon>\n";
6978 }
6979 if (defined $logo) {
6980 # not twice as wide as tall: 72 x 27 pixels
6981 print "<logo>" . esc_url($logo) . "</logo>\n";
6982 }
6983 if (! %latest_date) {
6984 # dummy date to keep the feed valid until commits trickle in:
6985 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6986 } else {
6987 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6988 }
6989 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6990 }
6991
6992 # contents
6993 for (my $i = 0; $i <= $#commitlist; $i++) {
6994 my %co = %{$commitlist[$i]};
6995 my $commit = $co{'id'};
6996 # we read 150, we always show 30 and the ones more recent than 48 hours
6997 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6998 last;
6999 }
7000 my %cd = parse_date($co{'author_epoch'});
7001
7002 # get list of changed files
7003 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7004 $co{'parent'} || "--root",
7005 $co{'id'}, "--", (defined $file_name ? $file_name : ())
7006 or next;
7007 my @difftree = map { chomp; $_ } <$fd>;
7008 close $fd
7009 or next;
7010
7011 # print element (entry, item)
7012 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
7013 if ($format eq 'rss') {
7014 print "<item>\n" .
7015 "<title>" . esc_html($co{'title'}) . "</title>\n" .
7016 "<author>" . esc_html($co{'author'}) . "</author>\n" .
7017 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7018 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7019 "<link>$co_url</link>\n" .
7020 "<description>" . esc_html($co{'title'}) . "</description>\n" .
7021 "<content:encoded>" .
7022 "<![CDATA[\n";
7023 } elsif ($format eq 'atom') {
7024 print "<entry>\n" .
7025 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7026 "<updated>$cd{'iso-8601'}</updated>\n" .
7027 "<author>\n" .
7028 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
7029 if ($co{'author_email'}) {
7030 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
7031 }
7032 print "</author>\n" .
7033 # use committer for contributor
7034 "<contributor>\n" .
7035 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7036 if ($co{'committer_email'}) {
7037 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7038 }
7039 print "</contributor>\n" .
7040 "<published>$cd{'iso-8601'}</published>\n" .
7041 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
7042 "<id>$co_url</id>\n" .
7043 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
7044 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
7045 }
7046 my $comment = $co{'comment'};
7047 print "<pre>\n";
7048 foreach my $line (@$comment) {
7049 $line = esc_html($line);
7050 print "$line\n";
7051 }
7052 print "</pre><ul>\n";
7053 foreach my $difftree_line (@difftree) {
7054 my %difftree = parse_difftree_raw_line($difftree_line);
7055 next if !$difftree{'from_id'};
7056
7057 my $file = $difftree{'file'} || $difftree{'to_file'};
7058
7059 print "<li>" .
7060 "[" .
7061 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
7062 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
7063 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
7064 file_name=>$file, file_parent=>$difftree{'from_file'}),
7065 -title => "diff"}, 'D');
7066 if ($have_blame) {
7067 print $cgi->a({-href => href(-full=>1, action=>"blame",
7068 file_name=>$file, hash_base=>$commit),
7069 -title => "blame"}, 'B');
7070 }
7071 # if this is not a feed of a file history
7072 if (!defined $file_name || $file_name ne $file) {
7073 print $cgi->a({-href => href(-full=>1, action=>"history",
7074 file_name=>$file, hash=>$commit),
7075 -title => "history"}, 'H');
7076 }
7077 $file = esc_path($file);
7078 print "] ".
7079 "$file</li>\n";
7080 }
7081 if ($format eq 'rss') {
7082 print "</ul>]]>\n" .
7083 "</content:encoded>\n" .
7084 "</item>\n";
7085 } elsif ($format eq 'atom') {
7086 print "</ul>\n</div>\n" .
7087 "</content>\n" .
7088 "</entry>\n";
7089 }
7090 }
7091
7092 # end of feed
7093 if ($format eq 'rss') {
7094 print "</channel>\n</rss>\n";
7095 } elsif ($format eq 'atom') {
7096 print "</feed>\n";
7097 }
7098}
7099
7100sub git_rss {
7101 git_feed('rss');
7102}
7103
7104sub git_atom {
7105 git_feed('atom');
7106}
7107
7108sub git_opml {
7109 my @list = git_get_projects_list();
7110
7111 print $cgi->header(
7112 -type => 'text/xml',
7113 -charset => 'utf-8',
7114 -content_disposition => 'inline; filename="opml.xml"');
7115
7116 print <<XML;
7117<?xml version="1.0" encoding="utf-8"?>
7118<opml version="1.0">
7119<head>
7120 <title>$site_name OPML Export</title>
7121</head>
7122<body>
7123<outline text="git RSS feeds">
7124XML
7125
7126 foreach my $pr (@list) {
7127 my %proj = %$pr;
7128 my $head = git_get_head_hash($proj{'path'});
7129 if (!defined $head) {
7130 next;
7131 }
7132 $git_dir = "$projectroot/$proj{'path'}";
7133 my %co = parse_commit($head);
7134 if (!%co) {
7135 next;
7136 }
7137
7138 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
7139 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7140 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
7141 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7142 }
7143 print <<XML;
7144</outline>
7145</body>
7146</opml>
7147XML
7148}