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