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