]> git.ipfire.org Git - thirdparty/git.git/blame - gitweb/gitweb.perl
perl: bump the required Perl version to 5.8 from 5.6.[21]
[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 9
d48b2841 10use 5.008;
161332a5
KS
11use strict;
12use warnings;
19806691 13use CGI qw(:standard :escapeHTML -nosticky);
7403d50b 14use CGI::Util qw(unescape);
7a597457 15use CGI::Carp qw(fatalsToBrowser set_message);
40c13813 16use Encode;
b87d78d6 17use Fcntl ':mode';
7a13b999 18use File::Find qw();
cb9c6e5b 19use File::Basename qw(basename);
10bb9036 20binmode STDOUT, ':utf8';
161332a5 21
aa7dd05e
JN
22our $t0;
23if (eval { require Time::HiRes; 1; }) {
24 $t0 = [Time::HiRes::gettimeofday()];
25}
26our $number_of_git_cmds = 0;
27
b1f5f64f 28BEGIN {
3be8e720 29 CGI->compile() if $ENV{'MOD_PERL'};
b1f5f64f
JN
30}
31
06c084d2 32our $version = "++GIT_VERSION++";
3e029299 33
c2394fe9
JN
34our ($my_url, $my_uri, $base_url, $path_info, $home_link);
35sub evaluate_uri {
36 our $cgi;
81d3fe9f 37
c2394fe9
JN
38 our $my_url = $cgi->url();
39 our $my_uri = $cgi->url(-absolute => 1);
40
41 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
42 # needed and used only for URLs with nonempty PATH_INFO
43 our $base_url = $my_url;
44
45 # When the script is used as DirectoryIndex, the URL does not contain the name
46 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
47 # have to do it ourselves. We make $path_info global because it's also used
48 # later on.
49 #
50 # Another issue with the script being the DirectoryIndex is that the resulting
51 # $my_url data is not the full script URL: this is good, because we want
52 # generated links to keep implying the script name if it wasn't explicitly
53 # indicated in the URL we're handling, but it means that $my_url cannot be used
54 # as base URL.
55 # Therefore, if we needed to strip PATH_INFO, then we know that we have
56 # to build the base URL ourselves:
57 our $path_info = $ENV{"PATH_INFO"};
58 if ($path_info) {
59 if ($my_url =~ s,\Q$path_info\E$,, &&
60 $my_uri =~ s,\Q$path_info\E$,, &&
61 defined $ENV{'SCRIPT_NAME'}) {
62 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
63 }
81d3fe9f 64 }
c2394fe9
JN
65
66 # target of the home link on top of all pages
67 our $home_link = $my_uri || "/";
b65910fe
GB
68}
69
e130ddaa
AT
70# core git executable to use
71# this can just be "git" if your webserver has a sensible PATH
06c084d2 72our $GIT = "++GIT_BINDIR++/git";
3f7f2710 73
b87d78d6 74# absolute fs-path which will be prepended to the project path
4a87b43e 75#our $projectroot = "/pub/scm";
06c084d2 76our $projectroot = "++GITWEB_PROJECTROOT++";
b87d78d6 77
ca5e9495
LL
78# fs traversing limit for getting project list
79# the number is relative to the projectroot
80our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
81
2de21fac
YS
82# string of the home link on top of all pages
83our $home_link_str = "++GITWEB_HOME_LINK_STR++";
84
49da1daf
AT
85# name of your site or organization to appear in page titles
86# replace this with something more descriptive for clearer bookmarks
8be2890c
PB
87our $site_name = "++GITWEB_SITENAME++"
88 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
49da1daf 89
b2d3476e
AC
90# filename of html text to include at top of each page
91our $site_header = "++GITWEB_SITE_HEADER++";
8ab1da2c 92# html text to include at home page
06c084d2 93our $home_text = "++GITWEB_HOMETEXT++";
b2d3476e
AC
94# filename of html text to include at bottom of each page
95our $site_footer = "++GITWEB_SITE_FOOTER++";
96
97# URI of stylesheets
98our @stylesheets = ("++GITWEB_CSS++");
887a612f
PB
99# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
100our $stylesheet = undef;
9a7a62ff 101# URI of GIT logo (72x27 size)
06c084d2 102our $logo = "++GITWEB_LOGO++";
0b5deba1
JN
103# URI of GIT favicon, assumed to be image/png type
104our $favicon = "++GITWEB_FAVICON++";
4af819d4
JN
105# URI of gitweb.js (JavaScript code for gitweb)
106our $javascript = "++GITWEB_JS++";
aedd9425 107
9a7a62ff
JN
108# URI and label (title) of GIT logo link
109#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
110#our $logo_label = "git documentation";
69fb8283 111our $logo_url = "http://git-scm.com/";
9a7a62ff 112our $logo_label = "git homepage";
51a7c66a 113
09bd7898 114# source of projects list
06c084d2 115our $projects_list = "++GITWEB_LIST++";
b87d78d6 116
55feb120
MH
117# the width (in characters) of the projects list "Description" column
118our $projects_list_description_width = 25;
119
b06dcf8c
FL
120# default order of projects list
121# valid values are none, project, descr, owner, and age
122our $default_projects_order = "project";
123
32f4aacc
ML
124# show repository only if this file exists
125# (only effective if this variable evaluates to true)
126our $export_ok = "++GITWEB_EXPORT_OK++";
127
dd7f5f10
AG
128# show repository only if this subroutine returns true
129# when given the path to the project, for example:
130# sub { return -e "$_[0]/git-daemon-export-ok"; }
131our $export_auth_hook = undef;
132
32f4aacc
ML
133# only allow viewing of repositories also shown on the overview page
134our $strict_export = "++GITWEB_STRICT_EXPORT++";
135
19a8721e
JN
136# list of git base URLs used for URL to where fetch project from,
137# i.e. full URL is "$git_base_url/$project"
d6b7e0b9 138our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
19a8721e 139
f5aa79d9 140# default blob_plain mimetype and default charset for text/plain blob
4a87b43e
DS
141our $default_blob_plain_mimetype = 'text/plain';
142our $default_text_plain_charset = undef;
f5aa79d9 143
2d007374
PB
144# file to use for guessing MIME types before trying /etc/mime.types
145# (relative to the current git repository)
4a87b43e 146our $mimetypes_file = undef;
2d007374 147
00f429af
MK
148# assume this charset if line contains non-UTF-8 characters;
149# it should be valid encoding (see Encoding::Supported(3pm) for list),
150# for which encoding all byte sequences are valid, for example
151# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
152# could be even 'utf-8' for the old behavior)
153our $fallback_encoding = 'latin1';
154
69a9b41c
JN
155# rename detection options for git-diff and git-diff-tree
156# - default is '-M', with the cost proportional to
157# (number of removed files) * (number of new files).
158# - more costly is '-C' (which implies '-M'), with the cost proportional to
159# (number of changed files + number of removed files) * (number of new files)
160# - even more costly is '-C', '--find-copies-harder' with cost
161# (number of files in the original tree) * (number of new files)
162# - one might want to include '-B' option, e.g. '-B', '-M'
163our @diff_opts = ('-M'); # taken from git_commit
164
7e1100e9
MM
165# Disables features that would allow repository owners to inject script into
166# the gitweb domain.
167our $prevent_xss = 0;
168
a3c8ab30
MM
169# information about snapshot formats that gitweb is capable of serving
170our %known_snapshot_formats = (
171 # name => {
172 # 'display' => display name,
173 # 'type' => mime type,
174 # 'suffix' => filename suffix,
175 # 'format' => --format for git-archive,
176 # 'compressor' => [compressor command and arguments]
1bfd3631
MR
177 # (array reference, optional)
178 # 'disabled' => boolean (optional)}
a3c8ab30
MM
179 #
180 'tgz' => {
181 'display' => 'tar.gz',
182 'type' => 'application/x-gzip',
183 'suffix' => '.tar.gz',
184 'format' => 'tar',
185 'compressor' => ['gzip']},
186
187 'tbz2' => {
188 'display' => 'tar.bz2',
189 'type' => 'application/x-bzip2',
190 'suffix' => '.tar.bz2',
191 'format' => 'tar',
192 'compressor' => ['bzip2']},
193
cbdefb5a
MR
194 'txz' => {
195 'display' => 'tar.xz',
196 'type' => 'application/x-xz',
197 'suffix' => '.tar.xz',
198 'format' => 'tar',
199 'compressor' => ['xz'],
200 'disabled' => 1},
201
a3c8ab30
MM
202 'zip' => {
203 'display' => 'zip',
204 'type' => 'application/x-zip',
205 'suffix' => '.zip',
206 'format' => 'zip'},
207);
208
209# Aliases so we understand old gitweb.snapshot values in repository
210# configuration.
211our %known_snapshot_format_aliases = (
212 'gzip' => 'tgz',
213 'bzip2' => 'tbz2',
cbdefb5a 214 'xz' => 'txz',
a3c8ab30
MM
215
216 # backward compatibility: legacy gitweb config support
217 'x-gzip' => undef, 'gz' => undef,
218 'x-bzip2' => undef, 'bz2' => undef,
219 'x-zip' => undef, '' => undef,
220);
221
e9fdd74e
GB
222# Pixel sizes for icons and avatars. If the default font sizes or lineheights
223# are changed, it may be appropriate to change these values too via
224# $GITWEB_CONFIG.
225our %avatar_size = (
226 'default' => 16,
227 'double' => 32
228);
229
b62a1a98
JWH
230# Used to set the maximum load that we will still respond to gitweb queries.
231# If server load exceed this value then return "503 server busy" error.
232# If gitweb cannot determined server load, it is taken to be 0.
233# Leave it undefined (or set to 'undef') to turn off load checking.
234our $maxload = 300;
235
61bf126e
AS
236# configuration for 'highlight' (http://www.andre-simon.de/)
237# match by basename
238our %highlight_basename = (
239 #'Program' => 'py',
240 #'Library' => 'py',
241 'SConstruct' => 'py', # SCons equivalent of Makefile
242 'Makefile' => 'make',
243);
244# match by extension
245our %highlight_ext = (
246 # main extensions, defining name of syntax;
247 # see files in /usr/share/highlight/langDefs/ directory
248 map { $_ => $_ }
249 qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl),
250 # alternate extensions, see /etc/highlight/filetypes.conf
251 'h' => 'c',
252 map { $_ => 'cpp' } qw(cxx c++ cc),
253 map { $_ => 'php' } qw(php3 php4),
254 map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi'
255 'mak' => 'make',
256 map { $_ => 'xml' } qw(xhtml html htm),
257);
258
ddb8d900
AK
259# You define site-wide feature defaults here; override them with
260# $GITWEB_CONFIG as necessary.
952c65fc 261our %feature = (
17848fc6
JN
262 # feature => {
263 # 'sub' => feature-sub (subroutine),
264 # 'override' => allow-override (boolean),
265 # 'default' => [ default options...] (array reference)}
266 #
b4b20b21 267 # if feature is overridable (it means that allow-override has true value),
17848fc6
JN
268 # then feature-sub will be called with default options as parameters;
269 # return value of feature-sub indicates if to enable specified feature
270 #
b4b20b21 271 # if there is no 'sub' key (no feature-sub), then feature cannot be
22e5e58a 272 # overridden
b4b20b21 273 #
ff3c0ff2
GB
274 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
275 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
276 # is enabled
952c65fc 277
45a3b12c
PB
278 # Enable the 'blame' blob view, showing the last commit that modified
279 # each line in the file. This can be very CPU-intensive.
280
281 # To enable system wide have in $GITWEB_CONFIG
282 # $feature{'blame'}{'default'} = [1];
283 # To have project specific config enable override in $GITWEB_CONFIG
284 # $feature{'blame'}{'override'} = 1;
285 # and in project config gitweb.blame = 0|1;
952c65fc 286 'blame' => {
cdad8170 287 'sub' => sub { feature_bool('blame', @_) },
952c65fc
JN
288 'override' => 0,
289 'default' => [0]},
290
a3c8ab30 291 # Enable the 'snapshot' link, providing a compressed archive of any
45a3b12c
PB
292 # tree. This can potentially generate high traffic if you have large
293 # project.
294
a3c8ab30
MM
295 # Value is a list of formats defined in %known_snapshot_formats that
296 # you wish to offer.
45a3b12c 297 # To disable system wide have in $GITWEB_CONFIG
a3c8ab30 298 # $feature{'snapshot'}{'default'} = [];
45a3b12c 299 # To have project specific config enable override in $GITWEB_CONFIG
bbee1d97 300 # $feature{'snapshot'}{'override'} = 1;
a3c8ab30
MM
301 # and in project config, a comma-separated list of formats or "none"
302 # to disable. Example: gitweb.snapshot = tbz2,zip;
952c65fc
JN
303 'snapshot' => {
304 'sub' => \&feature_snapshot,
305 'override' => 0,
a3c8ab30 306 'default' => ['tgz']},
04f7a94f 307
6be93511
RF
308 # Enable text search, which will list the commits which match author,
309 # committer or commit text to a given string. Enabled by default.
b4b20b21 310 # Project specific override is not supported.
6be93511
RF
311 'search' => {
312 'override' => 0,
313 'default' => [1]},
314
e7738553
PB
315 # Enable grep search, which will list the files in currently selected
316 # tree containing the given string. Enabled by default. This can be
317 # potentially CPU-intensive, of course.
318
319 # To enable system wide have in $GITWEB_CONFIG
320 # $feature{'grep'}{'default'} = [1];
321 # To have project specific config enable override in $GITWEB_CONFIG
322 # $feature{'grep'}{'override'} = 1;
323 # and in project config gitweb.grep = 0|1;
324 'grep' => {
cdad8170 325 'sub' => sub { feature_bool('grep', @_) },
e7738553
PB
326 'override' => 0,
327 'default' => [1]},
328
45a3b12c
PB
329 # Enable the pickaxe search, which will list the commits that modified
330 # a given string in a file. This can be practical and quite faster
331 # alternative to 'blame', but still potentially CPU-intensive.
332
333 # To enable system wide have in $GITWEB_CONFIG
334 # $feature{'pickaxe'}{'default'} = [1];
335 # To have project specific config enable override in $GITWEB_CONFIG
336 # $feature{'pickaxe'}{'override'} = 1;
337 # and in project config gitweb.pickaxe = 0|1;
04f7a94f 338 'pickaxe' => {
cdad8170 339 'sub' => sub { feature_bool('pickaxe', @_) },
04f7a94f
JN
340 'override' => 0,
341 'default' => [1]},
9e756904 342
e4b48eaa
JN
343 # Enable showing size of blobs in a 'tree' view, in a separate
344 # column, similar to what 'ls -l' does. This cost a bit of IO.
345
346 # To disable system wide have in $GITWEB_CONFIG
347 # $feature{'show-sizes'}{'default'} = [0];
348 # To have project specific config enable override in $GITWEB_CONFIG
349 # $feature{'show-sizes'}{'override'} = 1;
350 # and in project config gitweb.showsizes = 0|1;
351 'show-sizes' => {
352 'sub' => sub { feature_bool('showsizes', @_) },
353 'override' => 0,
354 'default' => [1]},
355
45a3b12c
PB
356 # Make gitweb use an alternative format of the URLs which can be
357 # more readable and natural-looking: project name is embedded
358 # directly in the path and the query string contains other
359 # auxiliary information. All gitweb installations recognize
360 # URL in either format; this configures in which formats gitweb
361 # generates links.
362
363 # To enable system wide have in $GITWEB_CONFIG
364 # $feature{'pathinfo'}{'default'} = [1];
365 # Project specific override is not supported.
366
367 # Note that you will need to change the default location of CSS,
368 # favicon, logo and possibly other files to an absolute URL. Also,
369 # if gitweb.cgi serves as your indexfile, you will need to force
370 # $my_uri to contain the script name in your $GITWEB_CONFIG.
9e756904
MW
371 'pathinfo' => {
372 'override' => 0,
373 'default' => [0]},
e30496df
PB
374
375 # Make gitweb consider projects in project root subdirectories
376 # to be forks of existing projects. Given project $projname.git,
377 # projects matching $projname/*.git will not be shown in the main
378 # projects list, instead a '+' mark will be added to $projname
379 # there and a 'forks' view will be enabled for the project, listing
c2b8b134
FL
380 # all the forks. If project list is taken from a file, forks have
381 # to be listed after the main project.
e30496df
PB
382
383 # To enable system wide have in $GITWEB_CONFIG
384 # $feature{'forks'}{'default'} = [1];
385 # Project specific override is not supported.
386 'forks' => {
387 'override' => 0,
388 'default' => [0]},
d627f68f
PB
389
390 # Insert custom links to the action bar of all project pages.
391 # This enables you mainly to link to third-party scripts integrating
392 # into gitweb; e.g. git-browser for graphical history representation
393 # or custom web-based repository administration interface.
394
395 # The 'default' value consists of a list of triplets in the form
396 # (label, link, position) where position is the label after which
2b11e059 397 # to insert the link and link is a format string where %n expands
d627f68f
PB
398 # to the project name, %f to the project path within the filesystem,
399 # %h to the current hash (h gitweb parameter) and %b to the current
2b11e059 400 # hash base (hb gitweb parameter); %% expands to %.
d627f68f
PB
401
402 # To enable system wide have in $GITWEB_CONFIG e.g.
403 # $feature{'actions'}{'default'} = [('graphiclog',
404 # '/git-browser/by-commit.html?r=%n', 'summary')];
405 # Project specific override is not supported.
406 'actions' => {
407 'override' => 0,
408 'default' => []},
3e3d4ee7 409
aed93de4
PB
410 # Allow gitweb scan project content tags described in ctags/
411 # of project repository, and display the popular Web 2.0-ish
412 # "tag cloud" near the project list. Note that this is something
413 # COMPLETELY different from the normal Git tags.
414
415 # gitweb by itself can show existing tags, but it does not handle
416 # tagging itself; you need an external application for that.
417 # For an example script, check Girocco's cgi/tagproj.cgi.
418 # You may want to install the HTML::TagCloud Perl module to get
419 # a pretty tag cloud instead of just a list of tags.
420
421 # To enable system wide have in $GITWEB_CONFIG
422 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
423 # Project specific override is not supported.
424 'ctags' => {
425 'override' => 0,
426 'default' => [0]},
9872cd6f
GB
427
428 # The maximum number of patches in a patchset generated in patch
429 # view. Set this to 0 or undef to disable patch view, or to a
430 # negative number to remove any limit.
431
432 # To disable system wide have in $GITWEB_CONFIG
433 # $feature{'patches'}{'default'} = [0];
434 # To have project specific config enable override in $GITWEB_CONFIG
435 # $feature{'patches'}{'override'} = 1;
436 # and in project config gitweb.patches = 0|n;
437 # where n is the maximum number of patches allowed in a patchset.
438 'patches' => {
439 'sub' => \&feature_patches,
440 'override' => 0,
441 'default' => [16]},
e9fdd74e
GB
442
443 # Avatar support. When this feature is enabled, views such as
444 # shortlog or commit will display an avatar associated with
445 # the email of the committer(s) and/or author(s).
446
679a1a1d
GB
447 # Currently available providers are gravatar and picon.
448 # If an unknown provider is specified, the feature is disabled.
449
450 # Gravatar depends on Digest::MD5.
451 # Picon currently relies on the indiana.edu database.
e9fdd74e
GB
452
453 # To enable system wide have in $GITWEB_CONFIG
679a1a1d
GB
454 # $feature{'avatar'}{'default'} = ['<provider>'];
455 # where <provider> is either gravatar or picon.
e9fdd74e
GB
456 # To have project specific config enable override in $GITWEB_CONFIG
457 # $feature{'avatar'}{'override'} = 1;
679a1a1d 458 # and in project config gitweb.avatar = <provider>;
e9fdd74e
GB
459 'avatar' => {
460 'sub' => \&feature_avatar,
461 'override' => 0,
462 'default' => ['']},
aa7dd05e
JN
463
464 # Enable displaying how much time and how many git commands
465 # it took to generate and display page. Disabled by default.
466 # Project specific override is not supported.
467 'timed' => {
468 'override' => 0,
469 'default' => [0]},
e627e50a
JN
470
471 # Enable turning some links into links to actions which require
472 # JavaScript to run (like 'blame_incremental'). Not enabled by
473 # default. Project specific override is currently not supported.
474 'javascript-actions' => {
475 'override' => 0,
476 'default' => [0]},
b331fe54
JS
477
478 # Syntax highlighting support. This is based on Daniel Svensson's
479 # and Sham Chukoury's work in gitweb-xmms2.git.
592ea417
JN
480 # It requires the 'highlight' program present in $PATH,
481 # and therefore is disabled by default.
b331fe54
JS
482
483 # To enable system wide have in $GITWEB_CONFIG
484 # $feature{'highlight'}{'default'} = [1];
485
486 'highlight' => {
487 'sub' => sub { feature_bool('highlight', @_) },
488 'override' => 0,
489 'default' => [0]},
ddb8d900
AK
490);
491
a7c5a283 492sub gitweb_get_feature {
ddb8d900 493 my ($name) = @_;
dd1ad5f1 494 return unless exists $feature{$name};
952c65fc
JN
495 my ($sub, $override, @defaults) = (
496 $feature{$name}{'sub'},
497 $feature{$name}{'override'},
498 @{$feature{$name}{'default'}});
9be3614e
JN
499 # project specific override is possible only if we have project
500 our $git_dir; # global variable, declared later
501 if (!$override || !defined $git_dir) {
502 return @defaults;
503 }
a9455919 504 if (!defined $sub) {
93197898 505 warn "feature $name is not overridable";
a9455919
MW
506 return @defaults;
507 }
ddb8d900
AK
508 return $sub->(@defaults);
509}
510
25b2790f
GB
511# A wrapper to check if a given feature is enabled.
512# With this, you can say
513#
514# my $bool_feat = gitweb_check_feature('bool_feat');
515# gitweb_check_feature('bool_feat') or somecode;
516#
517# instead of
518#
519# my ($bool_feat) = gitweb_get_feature('bool_feat');
520# (gitweb_get_feature('bool_feat'))[0] or somecode;
521#
522sub gitweb_check_feature {
523 return (gitweb_get_feature(@_))[0];
524}
525
526
cdad8170
MK
527sub feature_bool {
528 my $key = shift;
529 my ($val) = git_get_project_config($key, '--bool');
ddb8d900 530
df5d10a3
MC
531 if (!defined $val) {
532 return ($_[0]);
533 } elsif ($val eq 'true') {
cdad8170 534 return (1);
ddb8d900 535 } elsif ($val eq 'false') {
cdad8170 536 return (0);
ddb8d900 537 }
ddb8d900
AK
538}
539
ddb8d900 540sub feature_snapshot {
a3c8ab30 541 my (@fmts) = @_;
ddb8d900
AK
542
543 my ($val) = git_get_project_config('snapshot');
544
a3c8ab30
MM
545 if ($val) {
546 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
ddb8d900
AK
547 }
548
a3c8ab30 549 return @fmts;
de9272f4
LT
550}
551
9872cd6f
GB
552sub feature_patches {
553 my @val = (git_get_project_config('patches', '--int'));
554
555 if (@val) {
556 return @val;
557 }
558
559 return ($_[0]);
560}
561
e9fdd74e
GB
562sub feature_avatar {
563 my @val = (git_get_project_config('avatar'));
564
565 return @val ? @val : @_;
566}
567
2172ce4b
JH
568# checking HEAD file with -e is fragile if the repository was
569# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
570# and then pruned.
571sub check_head_link {
572 my ($dir) = @_;
573 my $headfile = "$dir/HEAD";
574 return ((-e $headfile) ||
575 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
576}
577
578sub check_export_ok {
579 my ($dir) = @_;
580 return (check_head_link($dir) &&
dd7f5f10
AG
581 (!$export_ok || -e "$dir/$export_ok") &&
582 (!$export_auth_hook || $export_auth_hook->($dir)));
2172ce4b
JH
583}
584
a781785d
JN
585# process alternate names for backward compatibility
586# filter out unsupported (unknown) snapshot formats
587sub filter_snapshot_fmts {
588 my @fmts = @_;
589
590 @fmts = map {
591 exists $known_snapshot_format_aliases{$_} ?
592 $known_snapshot_format_aliases{$_} : $_} @fmts;
68cedb1f 593 @fmts = grep {
1bfd3631
MR
594 exists $known_snapshot_formats{$_} &&
595 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
a781785d
JN
596}
597
c2394fe9
JN
598our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
599sub evaluate_gitweb_config {
600 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
601 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
602 # die if there are errors parsing config file
603 if (-e $GITWEB_CONFIG) {
604 do $GITWEB_CONFIG;
605 die $@ if $@;
606 } elsif (-e $GITWEB_CONFIG_SYSTEM) {
607 do $GITWEB_CONFIG_SYSTEM;
608 die $@ if $@;
609 }
17a8b250 610}
c8d138a8 611
b62a1a98
JWH
612# Get loadavg of system, to compare against $maxload.
613# Currently it requires '/proc/loadavg' present to get loadavg;
614# if it is not present it returns 0, which means no load checking.
615sub get_loadavg {
616 if( -e '/proc/loadavg' ){
617 open my $fd, '<', '/proc/loadavg'
618 or return 0;
619 my @load = split(/\s+/, scalar <$fd>);
620 close $fd;
621
622 # The first three columns measure CPU and IO utilization of the last one,
623 # five, and 10 minute periods. The fourth column shows the number of
624 # currently running processes and the total number of processes in the m/n
625 # format. The last column displays the last process ID used.
626 return $load[0] || 0;
627 }
628 # additional checks for load average should go here for things that don't export
629 # /proc/loadavg
630
631 return 0;
632}
633
c8d138a8 634# version of the core git binary
c2394fe9
JN
635our $git_version;
636sub evaluate_git_version {
637 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
638 $number_of_git_cmds++;
639}
c8d138a8 640
c2394fe9
JN
641sub check_loadavg {
642 if (defined $maxload && get_loadavg() > $maxload) {
643 die_error(503, "The load average on the server is too high");
644 }
b62a1a98
JWH
645}
646
154b4d78 647# ======================================================================
09bd7898 648# input validation and dispatch
1b2d297e
GB
649
650# input parameters can be collected from a variety of sources (presently, CGI
651# and PATH_INFO), so we define an %input_params hash that collects them all
652# together during validation: this allows subsequent uses (e.g. href()) to be
653# agnostic of the parameter origin
654
dde80d9c 655our %input_params = ();
1b2d297e
GB
656
657# input parameters are stored with the long parameter name as key. This will
658# also be used in the href subroutine to convert parameters to their CGI
659# equivalent, and since the href() usage is the most frequent one, we store
660# the name -> CGI key mapping here, instead of the reverse.
661#
662# XXX: Warning: If you touch this, check the search form for updating,
663# too.
664
dde80d9c 665our @cgi_param_mapping = (
1b2d297e
GB
666 project => "p",
667 action => "a",
668 file_name => "f",
669 file_parent => "fp",
670 hash => "h",
671 hash_parent => "hp",
672 hash_base => "hb",
673 hash_parent_base => "hpb",
674 page => "pg",
675 order => "o",
676 searchtext => "s",
677 searchtype => "st",
678 snapshot_format => "sf",
679 extra_options => "opt",
680 search_use_regexp => "sr",
c4ccf61f
JN
681 # this must be last entry (for manipulation from JavaScript)
682 javascript => "js"
1b2d297e 683);
dde80d9c 684our %cgi_param_mapping = @cgi_param_mapping;
1b2d297e
GB
685
686# we will also need to know the possible actions, for validation
dde80d9c 687our %actions = (
1b2d297e 688 "blame" => \&git_blame,
4af819d4
JN
689 "blame_incremental" => \&git_blame_incremental,
690 "blame_data" => \&git_blame_data,
1b2d297e
GB
691 "blobdiff" => \&git_blobdiff,
692 "blobdiff_plain" => \&git_blobdiff_plain,
693 "blob" => \&git_blob,
694 "blob_plain" => \&git_blob_plain,
695 "commitdiff" => \&git_commitdiff,
696 "commitdiff_plain" => \&git_commitdiff_plain,
697 "commit" => \&git_commit,
698 "forks" => \&git_forks,
699 "heads" => \&git_heads,
700 "history" => \&git_history,
701 "log" => \&git_log,
9872cd6f 702 "patch" => \&git_patch,
a3411f8a 703 "patches" => \&git_patches,
1b2d297e
GB
704 "rss" => \&git_rss,
705 "atom" => \&git_atom,
706 "search" => \&git_search,
707 "search_help" => \&git_search_help,
708 "shortlog" => \&git_shortlog,
709 "summary" => \&git_summary,
710 "tag" => \&git_tag,
711 "tags" => \&git_tags,
712 "tree" => \&git_tree,
713 "snapshot" => \&git_snapshot,
714 "object" => \&git_object,
715 # those below don't need $project
716 "opml" => \&git_opml,
717 "project_list" => \&git_project_list,
718 "project_index" => \&git_project_index,
719);
720
721# finally, we have the hash of allowed extra_options for the commands that
722# allow them
dde80d9c 723our %allowed_options = (
1b2d297e
GB
724 "--no-merges" => [ qw(rss atom log shortlog history) ],
725);
726
727# fill %input_params with the CGI parameters. All values except for 'opt'
728# should be single values, but opt can be an array. We should probably
729# build an array of parameters that can be multi-valued, but since for the time
730# being it's only this one, we just single it out
c2394fe9
JN
731sub evaluate_query_params {
732 our $cgi;
733
734 while (my ($name, $symbol) = each %cgi_param_mapping) {
735 if ($symbol eq 'opt') {
736 $input_params{$name} = [ $cgi->param($symbol) ];
737 } else {
738 $input_params{$name} = $cgi->param($symbol);
739 }
1b2d297e
GB
740 }
741}
742
743# now read PATH_INFO and update the parameter list for missing parameters
744sub evaluate_path_info {
745 return if defined $input_params{'project'};
746 return if !$path_info;
747 $path_info =~ s,^/+,,;
748 return if !$path_info;
749
750 # find which part of PATH_INFO is project
751 my $project = $path_info;
752 $project =~ s,/+$,,;
753 while ($project && !check_head_link("$projectroot/$project")) {
754 $project =~ s,/*[^/]*$,,;
755 }
756 return unless $project;
757 $input_params{'project'} = $project;
758
759 # do not change any parameters if an action is given using the query string
760 return if $input_params{'action'};
761 $path_info =~ s,^\Q$project\E/*,,;
762
d8c28822
GB
763 # next, check if we have an action
764 my $action = $path_info;
765 $action =~ s,/.*$,,;
766 if (exists $actions{$action}) {
767 $path_info =~ s,^$action/*,,;
768 $input_params{'action'} = $action;
769 }
770
771 # list of actions that want hash_base instead of hash, but can have no
772 # pathname (f) parameter
773 my @wants_base = (
774 'tree',
775 'history',
776 );
777
b0be3838
GB
778 # we want to catch
779 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
780 my ($parentrefname, $parentpathname, $refname, $pathname) =
781 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
782
783 # first, analyze the 'current' part
1b2d297e 784 if (defined $pathname) {
d8c28822
GB
785 # we got "branch:filename" or "branch:dir/"
786 # we could use git_get_type(branch:pathname), but:
787 # - it needs $git_dir
788 # - it does a git() call
789 # - the convention of terminating directories with a slash
790 # makes it superfluous
791 # - embedding the action in the PATH_INFO would make it even
792 # more superfluous
1b2d297e
GB
793 $pathname =~ s,^/+,,;
794 if (!$pathname || substr($pathname, -1) eq "/") {
d8c28822 795 $input_params{'action'} ||= "tree";
1b2d297e
GB
796 $pathname =~ s,/$,,;
797 } else {
b0be3838
GB
798 # the default action depends on whether we had parent info
799 # or not
800 if ($parentrefname) {
801 $input_params{'action'} ||= "blobdiff_plain";
802 } else {
803 $input_params{'action'} ||= "blob_plain";
804 }
1b2d297e
GB
805 }
806 $input_params{'hash_base'} ||= $refname;
807 $input_params{'file_name'} ||= $pathname;
808 } elsif (defined $refname) {
d8c28822
GB
809 # we got "branch". In this case we have to choose if we have to
810 # set hash or hash_base.
811 #
812 # Most of the actions without a pathname only want hash to be
813 # set, except for the ones specified in @wants_base that want
814 # hash_base instead. It should also be noted that hand-crafted
815 # links having 'history' as an action and no pathname or hash
816 # set will fail, but that happens regardless of PATH_INFO.
817 $input_params{'action'} ||= "shortlog";
818 if (grep { $_ eq $input_params{'action'} } @wants_base) {
819 $input_params{'hash_base'} ||= $refname;
820 } else {
821 $input_params{'hash'} ||= $refname;
822 }
1b2d297e 823 }
b0be3838
GB
824
825 # next, handle the 'parent' part, if present
826 if (defined $parentrefname) {
827 # a missing pathspec defaults to the 'current' filename, allowing e.g.
828 # someproject/blobdiff/oldrev..newrev:/filename
829 if ($parentpathname) {
830 $parentpathname =~ s,^/+,,;
831 $parentpathname =~ s,/$,,;
832 $input_params{'file_parent'} ||= $parentpathname;
833 } else {
834 $input_params{'file_parent'} ||= $input_params{'file_name'};
835 }
836 # we assume that hash_parent_base is wanted if a path was specified,
837 # or if the action wants hash_base instead of hash
838 if (defined $input_params{'file_parent'} ||
839 grep { $_ eq $input_params{'action'} } @wants_base) {
840 $input_params{'hash_parent_base'} ||= $parentrefname;
841 } else {
842 $input_params{'hash_parent'} ||= $parentrefname;
843 }
844 }
1ec2fb5f
GB
845
846 # for the snapshot action, we allow URLs in the form
847 # $project/snapshot/$hash.ext
848 # where .ext determines the snapshot and gets removed from the
849 # passed $refname to provide the $hash.
850 #
851 # To be able to tell that $refname includes the format extension, we
852 # require the following two conditions to be satisfied:
853 # - the hash input parameter MUST have been set from the $refname part
854 # of the URL (i.e. they must be equal)
855 # - the snapshot format MUST NOT have been defined already (e.g. from
856 # CGI parameter sf)
857 # It's also useless to try any matching unless $refname has a dot,
858 # so we check for that too
859 if (defined $input_params{'action'} &&
860 $input_params{'action'} eq 'snapshot' &&
861 defined $refname && index($refname, '.') != -1 &&
862 $refname eq $input_params{'hash'} &&
863 !defined $input_params{'snapshot_format'}) {
864 # We loop over the known snapshot formats, checking for
865 # extensions. Allowed extensions are both the defined suffix
866 # (which includes the initial dot already) and the snapshot
867 # format key itself, with a prepended dot
ccb4b539 868 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1ec2fb5f 869 my $hash = $refname;
095e9142
JN
870 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
871 next;
872 }
873 my $sfx = $1;
1ec2fb5f
GB
874 # a valid suffix was found, so set the snapshot format
875 # and reset the hash parameter
876 $input_params{'snapshot_format'} = $fmt;
877 $input_params{'hash'} = $hash;
878 # we also set the format suffix to the one requested
879 # in the URL: this way a request for e.g. .tgz returns
880 # a .tgz instead of a .tar.gz
881 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
882 last;
883 }
884 }
1b2d297e 885}
1b2d297e 886
c2394fe9
JN
887our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
888 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
889 $searchtext, $search_regexp);
890sub evaluate_and_validate_params {
891 our $action = $input_params{'action'};
892 if (defined $action) {
893 if (!validate_action($action)) {
894 die_error(400, "Invalid action parameter");
895 }
b87d78d6 896 }
44ad2978 897
c2394fe9
JN
898 # parameters which are pathnames
899 our $project = $input_params{'project'};
900 if (defined $project) {
901 if (!validate_project($project)) {
902 undef $project;
903 die_error(404, "No such project");
904 }
9cd3d988 905 }
6191f8e1 906
c2394fe9
JN
907 our $file_name = $input_params{'file_name'};
908 if (defined $file_name) {
909 if (!validate_pathname($file_name)) {
910 die_error(400, "Invalid file parameter");
911 }
24d0693a 912 }
24d0693a 913
c2394fe9
JN
914 our $file_parent = $input_params{'file_parent'};
915 if (defined $file_parent) {
916 if (!validate_pathname($file_parent)) {
917 die_error(400, "Invalid file parent parameter");
918 }
24d0693a 919 }
5c95fab0 920
c2394fe9
JN
921 # parameters which are refnames
922 our $hash = $input_params{'hash'};
923 if (defined $hash) {
924 if (!validate_refname($hash)) {
925 die_error(400, "Invalid hash parameter");
926 }
4fac5294 927 }
6191f8e1 928
c2394fe9
JN
929 our $hash_parent = $input_params{'hash_parent'};
930 if (defined $hash_parent) {
931 if (!validate_refname($hash_parent)) {
932 die_error(400, "Invalid hash parent parameter");
933 }
c91da262 934 }
09bd7898 935
c2394fe9
JN
936 our $hash_base = $input_params{'hash_base'};
937 if (defined $hash_base) {
938 if (!validate_refname($hash_base)) {
939 die_error(400, "Invalid hash base parameter");
940 }
c91da262 941 }
6191f8e1 942
c2394fe9
JN
943 our @extra_options = @{$input_params{'extra_options'}};
944 # @extra_options is always defined, since it can only be (currently) set from
945 # CGI, and $cgi->param() returns the empty array in array context if the param
946 # is not set
947 foreach my $opt (@extra_options) {
948 if (not exists $allowed_options{$opt}) {
949 die_error(400, "Invalid option parameter");
950 }
951 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
952 die_error(400, "Invalid option parameter for this action");
953 }
868bc068 954 }
868bc068 955
c2394fe9
JN
956 our $hash_parent_base = $input_params{'hash_parent_base'};
957 if (defined $hash_parent_base) {
958 if (!validate_refname($hash_parent_base)) {
959 die_error(400, "Invalid hash parent base parameter");
960 }
420e92f2 961 }
420e92f2 962
c2394fe9
JN
963 # other parameters
964 our $page = $input_params{'page'};
965 if (defined $page) {
966 if ($page =~ m/[^0-9]/) {
967 die_error(400, "Invalid page parameter");
968 }
b87d78d6 969 }
823d5dc8 970
c2394fe9
JN
971 our $searchtype = $input_params{'searchtype'};
972 if (defined $searchtype) {
973 if ($searchtype =~ m/[^a-z]/) {
974 die_error(400, "Invalid searchtype parameter");
975 }
e7738553 976 }
e7738553 977
c2394fe9 978 our $search_use_regexp = $input_params{'search_use_regexp'};
0e559919 979
c2394fe9
JN
980 our $searchtext = $input_params{'searchtext'};
981 our $search_regexp;
982 if (defined $searchtext) {
983 if (length($searchtext) < 2) {
984 die_error(403, "At least two characters are required for search parameter");
985 }
986 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
9d032c72 987 }
19806691
KS
988}
989
645927ce
ML
990# path to the current git repository
991our $git_dir;
c2394fe9
JN
992sub evaluate_git_dir {
993 our $git_dir = "$projectroot/$project" if $project;
e9fdd74e
GB
994}
995
c2394fe9
JN
996our (@snapshot_fmts, $git_avatar);
997sub configure_gitweb_features {
998 # list of supported snapshot formats
999 our @snapshot_fmts = gitweb_get_feature('snapshot');
1000 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1001
1002 # check that the avatar feature is set to a known provider name,
1003 # and for each provider check if the dependencies are satisfied.
1004 # if the provider name is invalid or the dependencies are not met,
1005 # reset $git_avatar to the empty string.
1006 our ($git_avatar) = gitweb_get_feature('avatar');
1007 if ($git_avatar eq 'gravatar') {
1008 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1009 } elsif ($git_avatar eq 'picon') {
1010 # no dependencies
7f9778b1 1011 } else {
c2394fe9 1012 $git_avatar = '';
7f9778b1 1013 }
e9fdd74e
GB
1014}
1015
7a597457
JN
1016# custom error handler: 'die <message>' is Internal Server Error
1017sub handle_errors_html {
1018 my $msg = shift; # it is already HTML escaped
1019
1020 # to avoid infinite loop where error occurs in die_error,
1021 # change handler to default handler, disabling handle_errors_html
1022 set_message("Error occured when inside die_error:\n$msg");
1023
1024 # you cannot jump out of die_error when called as error handler;
1025 # the subroutine set via CGI::Carp::set_message is called _after_
1026 # HTTP headers are already written, so it cannot write them itself
1027 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1028}
1029set_message(\&handle_errors_html);
1030
717b8311 1031# dispatch
c2394fe9
JN
1032sub dispatch {
1033 if (!defined $action) {
1034 if (defined $hash) {
1035 $action = git_get_type($hash);
1036 } elsif (defined $hash_base && defined $file_name) {
1037 $action = git_get_type("$hash_base:$file_name");
1038 } elsif (defined $project) {
1039 $action = 'summary';
1040 } else {
1041 $action = 'project_list';
1042 }
7f9778b1 1043 }
c2394fe9
JN
1044 if (!defined($actions{$action})) {
1045 die_error(400, "Unknown action");
1046 }
1047 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1048 !$project) {
1049 die_error(400, "Project needed");
1050 }
1051 $actions{$action}->();
77a153fd 1052}
c2394fe9 1053
869d5881 1054sub reset_timer {
c2394fe9
JN
1055 our $t0 = [Time::HiRes::gettimeofday()]
1056 if defined $t0;
869d5881
JN
1057 our $number_of_git_cmds = 0;
1058}
1059
1060sub run_request {
1061 reset_timer();
c2394fe9
JN
1062
1063 evaluate_uri();
7f425db9 1064 evaluate_gitweb_config();
c2394fe9
JN
1065 check_loadavg();
1066
7f425db9
JN
1067 # $projectroot and $projects_list might be set in gitweb config file
1068 $projects_list ||= $projectroot;
1069
c2394fe9
JN
1070 evaluate_query_params();
1071 evaluate_path_info();
1072 evaluate_and_validate_params();
1073 evaluate_git_dir();
1074
1075 configure_gitweb_features();
1076
1077 dispatch();
09bd7898 1078}
a0446e7b
SV
1079
1080our $is_last_request = sub { 1 };
1081our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1082our $CGI = 'CGI';
1083our $cgi;
45aa9895
JN
1084sub configure_as_fcgi {
1085 require CGI::Fast;
1086 our $CGI = 'CGI::Fast';
1087
1088 my $request_number = 0;
1089 # let each child service 100 requests
1090 our $is_last_request = sub { ++$request_number > 100 };
d04d3d42 1091}
a0446e7b 1092sub evaluate_argv {
45aa9895
JN
1093 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1094 configure_as_fcgi()
1095 if $script_name =~ /\.fcgi$/;
1096
a0446e7b
SV
1097 return unless (@ARGV);
1098
1099 require Getopt::Long;
1100 Getopt::Long::GetOptions(
45aa9895 1101 'fastcgi|fcgi|f' => \&configure_as_fcgi,
a0446e7b
SV
1102 'nproc|n=i' => sub {
1103 my ($arg, $val) = @_;
1104 return unless eval { require FCGI::ProcManager; 1; };
1105 my $proc_manager = FCGI::ProcManager->new({
1106 n_processes => $val,
1107 });
1108 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1109 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1110 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1111 },
1112 );
1113}
1114
1115sub run {
1116 evaluate_argv();
869d5881
JN
1117 evaluate_git_version();
1118
a0446e7b
SV
1119 $pre_listen_hook->()
1120 if $pre_listen_hook;
1121
1122 REQUEST:
1123 while ($cgi = $CGI->new()) {
1124 $pre_dispatch_hook->()
1125 if $pre_dispatch_hook;
1126
1127 run_request();
1128
0b45010e 1129 $post_dispatch_hook->()
a0446e7b
SV
1130 if $post_dispatch_hook;
1131
1132 last REQUEST if ($is_last_request->());
1133 }
c2394fe9
JN
1134
1135 DONE_GITWEB:
1136 1;
d04d3d42 1137}
a0446e7b 1138
c2394fe9 1139run();
09bd7898 1140
5ed2ec10
JN
1141if (defined caller) {
1142 # wrapped in a subroutine processing requests,
1143 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1144 return;
1145} else {
1146 # pure CGI script, serving single request
1147 exit;
1148}
09bd7898 1149
06a9d86b
MW
1150## ======================================================================
1151## action links
1152
377bee34
JN
1153# possible values of extra options
1154# -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1155# -replay => 1 - start from a current view (replay with modifications)
1156# -path_info => 0|1 - don't use/use path_info URL (if possible)
74fd8728 1157sub href {
498fe002 1158 my %params = @_;
bd5d1e42
JN
1159 # default is to use -absolute url() i.e. $my_uri
1160 my $href = $params{-full} ? $my_url : $my_uri;
498fe002 1161
afa9b620
JN
1162 $params{'project'} = $project unless exists $params{'project'};
1163
1cad283a 1164 if ($params{-replay}) {
1b2d297e 1165 while (my ($name, $symbol) = each %cgi_param_mapping) {
1cad283a 1166 if (!exists $params{$name}) {
1b2d297e 1167 $params{$name} = $input_params{$name};
1cad283a
JN
1168 }
1169 }
1170 }
1171
25b2790f 1172 my $use_pathinfo = gitweb_check_feature('pathinfo');
377bee34
JN
1173 if (defined $params{'project'} &&
1174 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
b02bd7a6
GB
1175 # try to put as many parameters as possible in PATH_INFO:
1176 # - project name
1177 # - action
8db49a7f 1178 # - hash_parent or hash_parent_base:/file_parent
3550ea71 1179 # - hash or hash_base:/filename
c752a0e0 1180 # - the snapshot_format as an appropriate suffix
b02bd7a6
GB
1181
1182 # When the script is the root DirectoryIndex for the domain,
1183 # $href here would be something like http://gitweb.example.com/
1184 # Thus, we strip any trailing / from $href, to spare us double
1185 # slashes in the final URL
1186 $href =~ s,/$,,;
1187
1188 # Then add the project name, if present
fb098a94 1189 $href .= "/".esc_url($params{'project'});
9e756904
MW
1190 delete $params{'project'};
1191
c752a0e0
GB
1192 # since we destructively absorb parameters, we keep this
1193 # boolean that remembers if we're handling a snapshot
1194 my $is_snapshot = $params{'action'} eq 'snapshot';
1195
b02bd7a6
GB
1196 # Summary just uses the project path URL, any other action is
1197 # added to the URL
1198 if (defined $params{'action'}) {
1199 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
9e756904
MW
1200 delete $params{'action'};
1201 }
b02bd7a6 1202
8db49a7f
GB
1203 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1204 # stripping nonexistent or useless pieces
1205 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1206 || $params{'hash_parent'} || $params{'hash'});
b02bd7a6 1207 if (defined $params{'hash_base'}) {
8db49a7f
GB
1208 if (defined $params{'hash_parent_base'}) {
1209 $href .= esc_url($params{'hash_parent_base'});
1210 # skip the file_parent if it's the same as the file_name
b7da721f
GB
1211 if (defined $params{'file_parent'}) {
1212 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1213 delete $params{'file_parent'};
1214 } elsif ($params{'file_parent'} !~ /\.\./) {
1215 $href .= ":/".esc_url($params{'file_parent'});
1216 delete $params{'file_parent'};
1217 }
8db49a7f
GB
1218 }
1219 $href .= "..";
1220 delete $params{'hash_parent'};
1221 delete $params{'hash_parent_base'};
1222 } elsif (defined $params{'hash_parent'}) {
1223 $href .= esc_url($params{'hash_parent'}). "..";
1224 delete $params{'hash_parent'};
1225 }
1226
1227 $href .= esc_url($params{'hash_base'});
1228 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
3550ea71 1229 $href .= ":/".esc_url($params{'file_name'});
b02bd7a6
GB
1230 delete $params{'file_name'};
1231 }
1232 delete $params{'hash'};
1233 delete $params{'hash_base'};
1234 } elsif (defined $params{'hash'}) {
8db49a7f 1235 $href .= esc_url($params{'hash'});
b02bd7a6
GB
1236 delete $params{'hash'};
1237 }
c752a0e0
GB
1238
1239 # If the action was a snapshot, we can absorb the
1240 # snapshot_format parameter too
1241 if ($is_snapshot) {
1242 my $fmt = $params{'snapshot_format'};
1243 # snapshot_format should always be defined when href()
1244 # is called, but just in case some code forgets, we
1245 # fall back to the default
1246 $fmt ||= $snapshot_fmts[0];
1247 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1248 delete $params{'snapshot_format'};
1249 }
9e756904
MW
1250 }
1251
1252 # now encode the parameters explicitly
498fe002 1253 my @result = ();
1b2d297e
GB
1254 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1255 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
498fe002 1256 if (defined $params{$name}) {
f22cca44
JN
1257 if (ref($params{$name}) eq "ARRAY") {
1258 foreach my $par (@{$params{$name}}) {
1259 push @result, $symbol . "=" . esc_param($par);
1260 }
1261 } else {
1262 push @result, $symbol . "=" . esc_param($params{$name});
1263 }
498fe002
JN
1264 }
1265 }
9e756904
MW
1266 $href .= "?" . join(';', @result) if scalar @result;
1267
1268 return $href;
06a9d86b
MW
1269}
1270
1271
717b8311
JN
1272## ======================================================================
1273## validation, quoting/unquoting and escaping
1274
1b2d297e
GB
1275sub validate_action {
1276 my $input = shift || return undef;
1277 return undef unless exists $actions{$input};
1278 return $input;
1279}
1280
1281sub validate_project {
1282 my $input = shift || return undef;
1283 if (!validate_pathname($input) ||
1284 !(-d "$projectroot/$input") ||
ec26f098 1285 !check_export_ok("$projectroot/$input") ||
1b2d297e
GB
1286 ($strict_export && !project_in_list($input))) {
1287 return undef;
1288 } else {
1289 return $input;
1290 }
1291}
1292
24d0693a
JN
1293sub validate_pathname {
1294 my $input = shift || return undef;
717b8311 1295
24d0693a
JN
1296 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1297 # at the beginning, at the end, and between slashes.
1298 # also this catches doubled slashes
1299 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1300 return undef;
717b8311 1301 }
24d0693a
JN
1302 # no null characters
1303 if ($input =~ m!\0!) {
717b8311
JN
1304 return undef;
1305 }
24d0693a
JN
1306 return $input;
1307}
1308
1309sub validate_refname {
1310 my $input = shift || return undef;
1311
1312 # textual hashes are O.K.
1313 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1314 return $input;
1315 }
1316 # it must be correct pathname
1317 $input = validate_pathname($input)
1318 or return undef;
1319 # restrictions on ref name according to git-check-ref-format
1320 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
717b8311
JN
1321 return undef;
1322 }
1323 return $input;
1324}
1325
00f429af
MK
1326# decode sequences of octets in utf8 into Perl's internal form,
1327# which is utf-8 with utf8 flag set if needed. gitweb writes out
1328# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1329sub to_utf8 {
1330 my $str = shift;
1df48766 1331 return undef unless defined $str;
e5d3de5c
İD
1332 if (utf8::valid($str)) {
1333 utf8::decode($str);
1334 return $str;
00f429af
MK
1335 } else {
1336 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1337 }
1338}
1339
232ff553
KS
1340# quote unsafe chars, but keep the slash, even when it's not
1341# correct, but quoted slashes look too horrible in bookmarks
1342sub esc_param {
353347b0 1343 my $str = shift;
1df48766 1344 return undef unless defined $str;
452e2256 1345 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
a9e60b7d 1346 $str =~ s/ /\+/g;
353347b0
KS
1347 return $str;
1348}
1349
22e5e58a 1350# quote unsafe chars in whole URL, so some characters cannot be quoted
f93bff8d
JN
1351sub esc_url {
1352 my $str = shift;
1df48766 1353 return undef unless defined $str;
109988f2 1354 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
f93bff8d
JN
1355 $str =~ s/ /\+/g;
1356 return $str;
1357}
1358
232ff553 1359# replace invalid utf8 character with SUBSTITUTION sequence
74fd8728 1360sub esc_html {
40c13813 1361 my $str = shift;
6255ef08
JN
1362 my %opts = @_;
1363
1df48766
JN
1364 return undef unless defined $str;
1365
00f429af 1366 $str = to_utf8($str);
c390ae97 1367 $str = $cgi->escapeHTML($str);
6255ef08
JN
1368 if ($opts{'-nbsp'}) {
1369 $str =~ s/ /&nbsp;/g;
1370 }
25ffbb27 1371 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
40c13813
KS
1372 return $str;
1373}
1374
391862e3
JN
1375# quote control characters and escape filename to HTML
1376sub esc_path {
1377 my $str = shift;
1378 my %opts = @_;
1379
1df48766
JN
1380 return undef unless defined $str;
1381
00f429af 1382 $str = to_utf8($str);
c390ae97 1383 $str = $cgi->escapeHTML($str);
391862e3
JN
1384 if ($opts{'-nbsp'}) {
1385 $str =~ s/ /&nbsp;/g;
1386 }
1387 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1388 return $str;
1389}
1390
1391# Make control characters "printable", using character escape codes (CEC)
1d3bc0cc
JN
1392sub quot_cec {
1393 my $cntrl = shift;
c84c483f 1394 my %opts = @_;
1d3bc0cc 1395 my %es = ( # character escape codes, aka escape sequences
c84c483f
JN
1396 "\t" => '\t', # tab (HT)
1397 "\n" => '\n', # line feed (LF)
1398 "\r" => '\r', # carrige return (CR)
1399 "\f" => '\f', # form feed (FF)
1400 "\b" => '\b', # backspace (BS)
1401 "\a" => '\a', # alarm (bell) (BEL)
1402 "\e" => '\e', # escape (ESC)
1403 "\013" => '\v', # vertical tab (VT)
1404 "\000" => '\0', # nul character (NUL)
1405 );
1d3bc0cc
JN
1406 my $chr = ( (exists $es{$cntrl})
1407 ? $es{$cntrl}
25dfd171 1408 : sprintf('\%2x', ord($cntrl)) );
c84c483f
JN
1409 if ($opts{-nohtml}) {
1410 return $chr;
1411 } else {
1412 return "<span class=\"cntrl\">$chr</span>";
1413 }
1d3bc0cc
JN
1414}
1415
391862e3
JN
1416# Alternatively use unicode control pictures codepoints,
1417# Unicode "printable representation" (PR)
1d3bc0cc
JN
1418sub quot_upr {
1419 my $cntrl = shift;
c84c483f
JN
1420 my %opts = @_;
1421
1d3bc0cc 1422 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
c84c483f
JN
1423 if ($opts{-nohtml}) {
1424 return $chr;
1425 } else {
1426 return "<span class=\"cntrl\">$chr</span>";
1427 }
1d3bc0cc
JN
1428}
1429
232ff553
KS
1430# git may return quoted and escaped filenames
1431sub unquote {
1432 my $str = shift;
403d0906
JN
1433
1434 sub unq {
1435 my $seq = shift;
1436 my %es = ( # character escape codes, aka escape sequences
1437 't' => "\t", # tab (HT, TAB)
1438 'n' => "\n", # newline (NL)
1439 'r' => "\r", # return (CR)
1440 'f' => "\f", # form feed (FF)
1441 'b' => "\b", # backspace (BS)
1442 'a' => "\a", # alarm (bell) (BEL)
1443 'e' => "\e", # escape (ESC)
1444 'v' => "\013", # vertical tab (VT)
1445 );
1446
1447 if ($seq =~ m/^[0-7]{1,3}$/) {
1448 # octal char sequence
1449 return chr(oct($seq));
1450 } elsif (exists $es{$seq}) {
1451 # C escape sequence, aka character escape code
c84c483f 1452 return $es{$seq};
403d0906
JN
1453 }
1454 # quoted ordinary character
1455 return $seq;
1456 }
1457
232ff553 1458 if ($str =~ m/^"(.*)"$/) {
403d0906 1459 # needs unquoting
232ff553 1460 $str = $1;
403d0906 1461 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
232ff553
KS
1462 }
1463 return $str;
1464}
1465
f16db173
JN
1466# escape tabs (convert tabs to spaces)
1467sub untabify {
1468 my $line = shift;
1469
1470 while ((my $pos = index($line, "\t")) != -1) {
1471 if (my $count = (8 - ($pos % 8))) {
1472 my $spaces = ' ' x $count;
1473 $line =~ s/\t/$spaces/;
1474 }
1475 }
1476
1477 return $line;
1478}
1479
32f4aacc
ML
1480sub project_in_list {
1481 my $project = shift;
1482 my @list = git_get_projects_list();
1483 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1484}
1485
717b8311
JN
1486## ----------------------------------------------------------------------
1487## HTML aware string manipulation
1488
b8d97d07
JN
1489# Try to chop given string on a word boundary between position
1490# $len and $len+$add_len. If there is no word boundary there,
1491# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1492# (marking chopped part) would be longer than given string.
717b8311
JN
1493sub chop_str {
1494 my $str = shift;
1495 my $len = shift;
1496 my $add_len = shift || 10;
b8d97d07 1497 my $where = shift || 'right'; # 'left' | 'center' | 'right'
717b8311 1498
dee2775a
AW
1499 # Make sure perl knows it is utf8 encoded so we don't
1500 # cut in the middle of a utf8 multibyte char.
1501 $str = to_utf8($str);
1502
717b8311
JN
1503 # allow only $len chars, but don't cut a word if it would fit in $add_len
1504 # if it doesn't fit, cut it if it's still longer than the dots we would add
b8d97d07
JN
1505 # remove chopped character entities entirely
1506
1507 # when chopping in the middle, distribute $len into left and right part
1508 # return early if chopping wouldn't make string shorter
1509 if ($where eq 'center') {
1510 return $str if ($len + 5 >= length($str)); # filler is length 5
1511 $len = int($len/2);
1512 } else {
1513 return $str if ($len + 4 >= length($str)); # filler is length 4
1514 }
1515
1516 # regexps: ending and beginning with word part up to $add_len
1517 my $endre = qr/.{$len}\w{0,$add_len}/;
1518 my $begre = qr/\w{0,$add_len}.{$len}/;
1519
1520 if ($where eq 'left') {
1521 $str =~ m/^(.*?)($begre)$/;
1522 my ($lead, $body) = ($1, $2);
1523 if (length($lead) > 4) {
b8d97d07
JN
1524 $lead = " ...";
1525 }
1526 return "$lead$body";
1527
1528 } elsif ($where eq 'center') {
1529 $str =~ m/^($endre)(.*)$/;
1530 my ($left, $str) = ($1, $2);
1531 $str =~ m/^(.*?)($begre)$/;
1532 my ($mid, $right) = ($1, $2);
1533 if (length($mid) > 5) {
b8d97d07
JN
1534 $mid = " ... ";
1535 }
1536 return "$left$mid$right";
1537
1538 } else {
1539 $str =~ m/^($endre)(.*)$/;
1540 my $body = $1;
1541 my $tail = $2;
1542 if (length($tail) > 4) {
b8d97d07
JN
1543 $tail = "... ";
1544 }
1545 return "$body$tail";
717b8311 1546 }
717b8311
JN
1547}
1548
ce58ec91
DS
1549# takes the same arguments as chop_str, but also wraps a <span> around the
1550# result with a title attribute if it does get chopped. Additionally, the
1551# string is HTML-escaped.
1552sub chop_and_escape_str {
b8d97d07 1553 my ($str) = @_;
ce58ec91 1554
b8d97d07 1555 my $chopped = chop_str(@_);
ce58ec91
DS
1556 if ($chopped eq $str) {
1557 return esc_html($chopped);
1558 } else {
14afe774 1559 $str =~ s/[[:cntrl:]]/?/g;
850b90a5 1560 return $cgi->span({-title=>$str}, esc_html($chopped));
ce58ec91
DS
1561 }
1562}
1563
717b8311
JN
1564## ----------------------------------------------------------------------
1565## functions returning short strings
1566
1f1ab5f0
JN
1567# CSS class for given age value (in seconds)
1568sub age_class {
1569 my $age = shift;
1570
785cdea9
JN
1571 if (!defined $age) {
1572 return "noage";
1573 } elsif ($age < 60*60*2) {
1f1ab5f0
JN
1574 return "age0";
1575 } elsif ($age < 60*60*24*2) {
1576 return "age1";
1577 } else {
1578 return "age2";
1579 }
1580}
1581
717b8311
JN
1582# convert age in seconds to "nn units ago" string
1583sub age_string {
1584 my $age = shift;
1585 my $age_str;
a59d4afd 1586
717b8311
JN
1587 if ($age > 60*60*24*365*2) {
1588 $age_str = (int $age/60/60/24/365);
1589 $age_str .= " years ago";
1590 } elsif ($age > 60*60*24*(365/12)*2) {
1591 $age_str = int $age/60/60/24/(365/12);
1592 $age_str .= " months ago";
1593 } elsif ($age > 60*60*24*7*2) {
1594 $age_str = int $age/60/60/24/7;
1595 $age_str .= " weeks ago";
1596 } elsif ($age > 60*60*24*2) {
1597 $age_str = int $age/60/60/24;
1598 $age_str .= " days ago";
1599 } elsif ($age > 60*60*2) {
1600 $age_str = int $age/60/60;
1601 $age_str .= " hours ago";
1602 } elsif ($age > 60*2) {
1603 $age_str = int $age/60;
1604 $age_str .= " min ago";
1605 } elsif ($age > 2) {
1606 $age_str = int $age;
1607 $age_str .= " sec ago";
f6801d66 1608 } else {
717b8311 1609 $age_str .= " right now";
4c02e3c5 1610 }
717b8311 1611 return $age_str;
161332a5
KS
1612}
1613
01ac1e38
JN
1614use constant {
1615 S_IFINVALID => 0030000,
1616 S_IFGITLINK => 0160000,
1617};
1618
1619# submodule/subproject, a commit object reference
74fd8728 1620sub S_ISGITLINK {
01ac1e38
JN
1621 my $mode = shift;
1622
1623 return (($mode & S_IFMT) == S_IFGITLINK)
1624}
1625
717b8311
JN
1626# convert file mode in octal to symbolic file mode string
1627sub mode_str {
1628 my $mode = oct shift;
1629
01ac1e38
JN
1630 if (S_ISGITLINK($mode)) {
1631 return 'm---------';
1632 } elsif (S_ISDIR($mode & S_IFMT)) {
717b8311
JN
1633 return 'drwxr-xr-x';
1634 } elsif (S_ISLNK($mode)) {
1635 return 'lrwxrwxrwx';
1636 } elsif (S_ISREG($mode)) {
1637 # git cares only about the executable bit
1638 if ($mode & S_IXUSR) {
1639 return '-rwxr-xr-x';
1640 } else {
1641 return '-rw-r--r--';
1642 };
c994d620 1643 } else {
717b8311 1644 return '----------';
ff7669a5 1645 }
161332a5
KS
1646}
1647
717b8311
JN
1648# convert file mode in octal to file type string
1649sub file_type {
7c5e2ebb
JN
1650 my $mode = shift;
1651
1652 if ($mode !~ m/^[0-7]+$/) {
1653 return $mode;
1654 } else {
1655 $mode = oct $mode;
1656 }
664f4cc5 1657
01ac1e38
JN
1658 if (S_ISGITLINK($mode)) {
1659 return "submodule";
1660 } elsif (S_ISDIR($mode & S_IFMT)) {
717b8311
JN
1661 return "directory";
1662 } elsif (S_ISLNK($mode)) {
1663 return "symlink";
1664 } elsif (S_ISREG($mode)) {
1665 return "file";
1666 } else {
1667 return "unknown";
1668 }
a59d4afd
KS
1669}
1670
744d0ac3
JN
1671# convert file mode in octal to file type description string
1672sub file_type_long {
1673 my $mode = shift;
1674
1675 if ($mode !~ m/^[0-7]+$/) {
1676 return $mode;
1677 } else {
1678 $mode = oct $mode;
1679 }
1680
01ac1e38
JN
1681 if (S_ISGITLINK($mode)) {
1682 return "submodule";
1683 } elsif (S_ISDIR($mode & S_IFMT)) {
744d0ac3
JN
1684 return "directory";
1685 } elsif (S_ISLNK($mode)) {
1686 return "symlink";
1687 } elsif (S_ISREG($mode)) {
1688 if ($mode & S_IXUSR) {
1689 return "executable";
1690 } else {
1691 return "file";
1692 };
1693 } else {
1694 return "unknown";
1695 }
1696}
1697
1698
717b8311
JN
1699## ----------------------------------------------------------------------
1700## functions returning short HTML fragments, or transforming HTML fragments
3dff5379 1701## which don't belong to other sections
b18f9bf4 1702
225932ed 1703# format line of commit message.
717b8311
JN
1704sub format_log_line_html {
1705 my $line = shift;
b18f9bf4 1706
225932ed 1707 $line = esc_html($line, -nbsp=>1);
7d233dea
MC
1708 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1709 $cgi->a({-href => href(action=>"object", hash=>$1),
1710 -class => "text"}, $1);
1711 }eg;
1712
717b8311 1713 return $line;
b18f9bf4
JN
1714}
1715
717b8311 1716# format marker of refs pointing to given object
4afbaeff
GB
1717
1718# the destination action is chosen based on object type and current context:
1719# - for annotated tags, we choose the tag view unless it's the current view
1720# already, in which case we go to shortlog view
1721# - for other refs, we keep the current view if we're in history, shortlog or
1722# log view, and select shortlog otherwise
847e01fb 1723sub format_ref_marker {
717b8311 1724 my ($refs, $id) = @_;
d294e1ca 1725 my $markers = '';
27fb8c40 1726
717b8311 1727 if (defined $refs->{$id}) {
d294e1ca 1728 foreach my $ref (@{$refs->{$id}}) {
4afbaeff
GB
1729 # this code exploits the fact that non-lightweight tags are the
1730 # only indirect objects, and that they are the only objects for which
1731 # we want to use tag instead of shortlog as action
d294e1ca 1732 my ($type, $name) = qw();
4afbaeff 1733 my $indirect = ($ref =~ s/\^\{\}$//);
d294e1ca
JN
1734 # e.g. tags/v2.6.11 or heads/next
1735 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1736 $type = $1;
1737 $name = $2;
1738 } else {
1739 $type = "ref";
1740 $name = $ref;
1741 }
1742
4afbaeff
GB
1743 my $class = $type;
1744 $class .= " indirect" if $indirect;
1745
1746 my $dest_action = "shortlog";
1747
1748 if ($indirect) {
1749 $dest_action = "tag" unless $action eq "tag";
1750 } elsif ($action =~ /^(history|(short)?log)$/) {
1751 $dest_action = $action;
1752 }
1753
1754 my $dest = "";
1755 $dest .= "refs/" unless $ref =~ m!^refs/!;
1756 $dest .= $ref;
1757
1758 my $link = $cgi->a({
1759 -href => href(
1760 action=>$dest_action,
1761 hash=>$dest
1762 )}, $name);
1763
1764 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1765 $link . "</span>";
d294e1ca
JN
1766 }
1767 }
1768
1769 if ($markers) {
1770 return ' <span class="refs">'. $markers . '</span>';
717b8311
JN
1771 } else {
1772 return "";
1773 }
27fb8c40
JN
1774}
1775
17d07443
JN
1776# format, perhaps shortened and with markers, title line
1777sub format_subject_html {
1c2a4f5a 1778 my ($long, $short, $href, $extra) = @_;
17d07443
JN
1779 $extra = '' unless defined($extra);
1780
1781 if (length($short) < length($long)) {
14afe774 1782 $long =~ s/[[:cntrl:]]/?/g;
7c278014 1783 return $cgi->a({-href => $href, -class => "list subject",
00f429af 1784 -title => to_utf8($long)},
01b89f0c 1785 esc_html($short)) . $extra;
17d07443 1786 } else {
7c278014 1787 return $cgi->a({-href => $href, -class => "list subject"},
01b89f0c 1788 esc_html($long)) . $extra;
17d07443
JN
1789 }
1790}
1791
5a371b7b
GB
1792# Rather than recomputing the url for an email multiple times, we cache it
1793# after the first hit. This gives a visible benefit in views where the avatar
1794# for the same email is used repeatedly (e.g. shortlog).
1795# The cache is shared by all avatar engines (currently gravatar only), which
1796# are free to use it as preferred. Since only one avatar engine is used for any
1797# given page, there's no risk for cache conflicts.
1798our %avatar_cache = ();
1799
679a1a1d
GB
1800# Compute the picon url for a given email, by using the picon search service over at
1801# http://www.cs.indiana.edu/picons/search.html
1802sub picon_url {
1803 my $email = lc shift;
1804 if (!$avatar_cache{$email}) {
1805 my ($user, $domain) = split('@', $email);
1806 $avatar_cache{$email} =
1807 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1808 "$domain/$user/" .
1809 "users+domains+unknown/up/single";
1810 }
1811 return $avatar_cache{$email};
1812}
1813
5a371b7b
GB
1814# Compute the gravatar url for a given email, if it's not in the cache already.
1815# Gravatar stores only the part of the URL before the size, since that's the
1816# one computationally more expensive. This also allows reuse of the cache for
1817# different sizes (for this particular engine).
1818sub gravatar_url {
1819 my $email = lc shift;
1820 my $size = shift;
1821 $avatar_cache{$email} ||=
1822 "http://www.gravatar.com/avatar/" .
1823 Digest::MD5::md5_hex($email) . "?s=";
1824 return $avatar_cache{$email} . $size;
1825}
1826
e9fdd74e
GB
1827# Insert an avatar for the given $email at the given $size if the feature
1828# is enabled.
1829sub git_get_avatar {
1830 my ($email, %opts) = @_;
1831 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
1832 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
1833 $opts{-size} ||= 'default';
1834 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1835 my $url = "";
1836 if ($git_avatar eq 'gravatar') {
5a371b7b 1837 $url = gravatar_url($email, $size);
679a1a1d
GB
1838 } elsif ($git_avatar eq 'picon') {
1839 $url = picon_url($email);
e9fdd74e 1840 }
679a1a1d 1841 # Other providers can be added by extending the if chain, defining $url
e9fdd74e
GB
1842 # as needed. If no variant puts something in $url, we assume avatars
1843 # are completely disabled/unavailable.
1844 if ($url) {
1845 return $pre_white .
1846 "<img width=\"$size\" " .
1847 "class=\"avatar\" " .
1848 "src=\"$url\" " .
7d25ef41 1849 "alt=\"\" " .
e9fdd74e
GB
1850 "/>" . $post_white;
1851 } else {
1852 return "";
1853 }
1854}
1855
e133d65c
SB
1856sub format_search_author {
1857 my ($author, $searchtype, $displaytext) = @_;
1858 my $have_search = gitweb_check_feature('search');
1859
1860 if ($have_search) {
1861 my $performed = "";
1862 if ($searchtype eq 'author') {
1863 $performed = "authored";
1864 } elsif ($searchtype eq 'committer') {
1865 $performed = "committed";
1866 }
1867
1868 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1869 searchtext=>$author,
1870 searchtype=>$searchtype), class=>"list",
1871 title=>"Search for commits $performed by $author"},
1872 $displaytext);
1873
1874 } else {
1875 return $displaytext;
1876 }
1877}
1878
1c49a4e1
GB
1879# format the author name of the given commit with the given tag
1880# the author name is chopped and escaped according to the other
1881# optional parameters (see chop_str).
1882sub format_author_html {
1883 my $tag = shift;
1884 my $co = shift;
1885 my $author = chop_and_escape_str($co->{'author_name'}, @_);
e9fdd74e 1886 return "<$tag class=\"author\">" .
e133d65c
SB
1887 format_search_author($co->{'author_name'}, "author",
1888 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1889 $author) .
1890 "</$tag>";
1c49a4e1
GB
1891}
1892
90921740
JN
1893# format git diff header line, i.e. "diff --(git|combined|cc) ..."
1894sub format_git_diff_header_line {
1895 my $line = shift;
1896 my $diffinfo = shift;
1897 my ($from, $to) = @_;
1898
1899 if ($diffinfo->{'nparents'}) {
1900 # combined diff
1901 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1902 if ($to->{'href'}) {
1903 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1904 esc_path($to->{'file'}));
1905 } else { # file was deleted (no href)
1906 $line .= esc_path($to->{'file'});
1907 }
1908 } else {
1909 # "ordinary" diff
1910 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1911 if ($from->{'href'}) {
1912 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1913 'a/' . esc_path($from->{'file'}));
1914 } else { # file was added (no href)
1915 $line .= 'a/' . esc_path($from->{'file'});
1916 }
1917 $line .= ' ';
1918 if ($to->{'href'}) {
1919 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1920 'b/' . esc_path($to->{'file'}));
1921 } else { # file was deleted
1922 $line .= 'b/' . esc_path($to->{'file'});
1923 }
1924 }
1925
1926 return "<div class=\"diff header\">$line</div>\n";
1927}
1928
1929# format extended diff header line, before patch itself
1930sub format_extended_diff_header_line {
1931 my $line = shift;
1932 my $diffinfo = shift;
1933 my ($from, $to) = @_;
1934
1935 # match <path>
1936 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1937 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1938 esc_path($from->{'file'}));
1939 }
1940 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1941 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1942 esc_path($to->{'file'}));
1943 }
1944 # match single <mode>
1945 if ($line =~ m/\s(\d{6})$/) {
1946 $line .= '<span class="info"> (' .
1947 file_type_long($1) .
1948 ')</span>';
1949 }
1950 # match <hash>
1951 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1952 # can match only for combined diff
1953 $line = 'index ';
1954 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1955 if ($from->{'href'}[$i]) {
1956 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1957 -class=>"hash"},
1958 substr($diffinfo->{'from_id'}[$i],0,7));
1959 } else {
1960 $line .= '0' x 7;
1961 }
1962 # separator
1963 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1964 }
1965 $line .= '..';
1966 if ($to->{'href'}) {
1967 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1968 substr($diffinfo->{'to_id'},0,7));
1969 } else {
1970 $line .= '0' x 7;
1971 }
1972
1973 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1974 # can match only for ordinary diff
1975 my ($from_link, $to_link);
1976 if ($from->{'href'}) {
1977 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1978 substr($diffinfo->{'from_id'},0,7));
1979 } else {
1980 $from_link = '0' x 7;
1981 }
1982 if ($to->{'href'}) {
1983 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1984 substr($diffinfo->{'to_id'},0,7));
1985 } else {
1986 $to_link = '0' x 7;
1987 }
1988 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1989 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1990 }
1991
1992 return $line . "<br/>\n";
1993}
1994
1995# format from-file/to-file diff header
1996sub format_diff_from_to_header {
91af4ce4 1997 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
90921740
JN
1998 my $line;
1999 my $result = '';
2000
2001 $line = $from_line;
2002 #assert($line =~ m/^---/) if DEBUG;
deaa01a9
JN
2003 # no extra formatting for "^--- /dev/null"
2004 if (! $diffinfo->{'nparents'}) {
2005 # ordinary (single parent) diff
2006 if ($line =~ m!^--- "?a/!) {
2007 if ($from->{'href'}) {
2008 $line = '--- a/' .
2009 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2010 esc_path($from->{'file'}));
2011 } else {
2012 $line = '--- a/' .
2013 esc_path($from->{'file'});
2014 }
2015 }
2016 $result .= qq!<div class="diff from_file">$line</div>\n!;
2017
2018 } else {
2019 # combined diff (merge commit)
2020 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2021 if ($from->{'href'}[$i]) {
2022 $line = '--- ' .
91af4ce4
JN
2023 $cgi->a({-href=>href(action=>"blobdiff",
2024 hash_parent=>$diffinfo->{'from_id'}[$i],
2025 hash_parent_base=>$parents[$i],
2026 file_parent=>$from->{'file'}[$i],
2027 hash=>$diffinfo->{'to_id'},
2028 hash_base=>$hash,
2029 file_name=>$to->{'file'}),
2030 -class=>"path",
2031 -title=>"diff" . ($i+1)},
2032 $i+1) .
2033 '/' .
deaa01a9
JN
2034 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2035 esc_path($from->{'file'}[$i]));
2036 } else {
2037 $line = '--- /dev/null';
2038 }
2039 $result .= qq!<div class="diff from_file">$line</div>\n!;
90921740
JN
2040 }
2041 }
90921740
JN
2042
2043 $line = $to_line;
2044 #assert($line =~ m/^\+\+\+/) if DEBUG;
2045 # no extra formatting for "^+++ /dev/null"
2046 if ($line =~ m!^\+\+\+ "?b/!) {
2047 if ($to->{'href'}) {
2048 $line = '+++ b/' .
2049 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2050 esc_path($to->{'file'}));
2051 } else {
2052 $line = '+++ b/' .
2053 esc_path($to->{'file'});
2054 }
2055 }
2056 $result .= qq!<div class="diff to_file">$line</div>\n!;
2057
2058 return $result;
2059}
2060
cd030c3a
JN
2061# create note for patch simplified by combined diff
2062sub format_diff_cc_simplified {
2063 my ($diffinfo, @parents) = @_;
2064 my $result = '';
2065
2066 $result .= "<div class=\"diff header\">" .
2067 "diff --cc ";
2068 if (!is_deleted($diffinfo)) {
2069 $result .= $cgi->a({-href => href(action=>"blob",
2070 hash_base=>$hash,
2071 hash=>$diffinfo->{'to_id'},
2072 file_name=>$diffinfo->{'to_file'}),
2073 -class => "path"},
2074 esc_path($diffinfo->{'to_file'}));
2075 } else {
2076 $result .= esc_path($diffinfo->{'to_file'});
2077 }
2078 $result .= "</div>\n" . # class="diff header"
2079 "<div class=\"diff nodifferences\">" .
2080 "Simple merge" .
2081 "</div>\n"; # class="diff nodifferences"
2082
2083 return $result;
2084}
2085
90921740 2086# format patch (diff) line (not to be used for diff headers)
eee08903
JN
2087sub format_diff_line {
2088 my $line = shift;
59e3b14e 2089 my ($from, $to) = @_;
eee08903
JN
2090 my $diff_class = "";
2091
2092 chomp $line;
2093
e72c0eaf
JN
2094 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2095 # combined diff
2096 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
2097 if ($line =~ m/^\@{3}/) {
2098 $diff_class = " chunk_header";
2099 } elsif ($line =~ m/^\\/) {
2100 $diff_class = " incomplete";
2101 } elsif ($prefix =~ tr/+/+/) {
2102 $diff_class = " add";
2103 } elsif ($prefix =~ tr/-/-/) {
2104 $diff_class = " rem";
2105 }
2106 } else {
2107 # assume ordinary diff
2108 my $char = substr($line, 0, 1);
2109 if ($char eq '+') {
2110 $diff_class = " add";
2111 } elsif ($char eq '-') {
2112 $diff_class = " rem";
2113 } elsif ($char eq '@') {
2114 $diff_class = " chunk_header";
2115 } elsif ($char eq "\\") {
2116 $diff_class = " incomplete";
2117 }
eee08903
JN
2118 }
2119 $line = untabify($line);
59e3b14e
JN
2120 if ($from && $to && $line =~ m/^\@{2} /) {
2121 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2122 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2123
2124 $from_lines = 0 unless defined $from_lines;
2125 $to_lines = 0 unless defined $to_lines;
2126
2127 if ($from->{'href'}) {
2128 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2129 -class=>"list"}, $from_text);
2130 }
2131 if ($to->{'href'}) {
2132 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2133 -class=>"list"}, $to_text);
2134 }
2135 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2136 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2137 return "<div class=\"diff$diff_class\">$line</div>\n";
e72c0eaf
JN
2138 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2139 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2140 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2141
2142 @from_text = split(' ', $ranges);
2143 for (my $i = 0; $i < @from_text; ++$i) {
2144 ($from_start[$i], $from_nlines[$i]) =
2145 (split(',', substr($from_text[$i], 1)), 0);
2146 }
2147
2148 $to_text = pop @from_text;
2149 $to_start = pop @from_start;
2150 $to_nlines = pop @from_nlines;
2151
2152 $line = "<span class=\"chunk_info\">$prefix ";
2153 for (my $i = 0; $i < @from_text; ++$i) {
2154 if ($from->{'href'}[$i]) {
2155 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2156 -class=>"list"}, $from_text[$i]);
2157 } else {
2158 $line .= $from_text[$i];
2159 }
2160 $line .= " ";
2161 }
2162 if ($to->{'href'}) {
2163 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2164 -class=>"list"}, $to_text);
2165 } else {
2166 $line .= $to_text;
2167 }
2168 $line .= " $prefix</span>" .
2169 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2170 return "<div class=\"diff$diff_class\">$line</div>\n";
59e3b14e 2171 }
6255ef08 2172 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
eee08903
JN
2173}
2174
a3c8ab30
MM
2175# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2176# linked. Pass the hash of the tree/commit to snapshot.
2177sub format_snapshot_links {
2178 my ($hash) = @_;
a3c8ab30
MM
2179 my $num_fmts = @snapshot_fmts;
2180 if ($num_fmts > 1) {
2181 # A parenthesized list of links bearing format names.
a781785d 2182 # e.g. "snapshot (_tar.gz_ _zip_)"
a3c8ab30
MM
2183 return "snapshot (" . join(' ', map
2184 $cgi->a({
2185 -href => href(
2186 action=>"snapshot",
2187 hash=>$hash,
2188 snapshot_format=>$_
2189 )
2190 }, $known_snapshot_formats{$_}{'display'})
2191 , @snapshot_fmts) . ")";
2192 } elsif ($num_fmts == 1) {
2193 # A single "snapshot" link whose tooltip bears the format name.
a781785d 2194 # i.e. "_snapshot_"
a3c8ab30 2195 my ($fmt) = @snapshot_fmts;
a781785d
JN
2196 return
2197 $cgi->a({
a3c8ab30
MM
2198 -href => href(
2199 action=>"snapshot",
2200 hash=>$hash,
2201 snapshot_format=>$fmt
2202 ),
2203 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2204 }, "snapshot");
2205 } else { # $num_fmts == 0
2206 return undef;
2207 }
2208}
2209
3562198b
JN
2210## ......................................................................
2211## functions returning values to be passed, perhaps after some
2212## transformation, to other functions; e.g. returning arguments to href()
2213
2214# returns hash to be passed to href to generate gitweb URL
2215# in -title key it returns description of link
2216sub get_feed_info {
2217 my $format = shift || 'Atom';
2218 my %res = (action => lc($format));
2219
2220 # feed links are possible only for project views
2221 return unless (defined $project);
2222 # some views should link to OPML, or to generic project feed,
2223 # or don't have specific feed yet (so they should use generic)
2224 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
2225
2226 my $branch;
2227 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2228 # from tag links; this also makes possible to detect branch links
2229 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2230 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
2231 $branch = $1;
2232 }
2233 # find log type for feed description (title)
2234 my $type = 'log';
2235 if (defined $file_name) {
2236 $type = "history of $file_name";
2237 $type .= "/" if ($action eq 'tree');
2238 $type .= " on '$branch'" if (defined $branch);
2239 } else {
2240 $type = "log of $branch" if (defined $branch);
2241 }
2242
2243 $res{-title} = $type;
2244 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2245 $res{'file_name'} = $file_name;
2246
2247 return %res;
2248}
2249
717b8311
JN
2250## ----------------------------------------------------------------------
2251## git utility subroutines, invoking git commands
42f7eb94 2252
25691fbe
DS
2253# returns path to the core git executable and the --git-dir parameter as list
2254sub git_cmd {
aa7dd05e 2255 $number_of_git_cmds++;
25691fbe
DS
2256 return $GIT, '--git-dir='.$git_dir;
2257}
2258
516381d5
LW
2259# quote the given arguments for passing them to the shell
2260# quote_command("command", "arg 1", "arg with ' and ! characters")
2261# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2262# Try to avoid using this function wherever possible.
2263sub quote_command {
2264 return join(' ',
68cedb1f 2265 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
25691fbe
DS
2266}
2267
717b8311 2268# get HEAD ref of given project as hash
847e01fb 2269sub git_get_head_hash {
b629275f
MR
2270 return git_get_full_hash(shift, 'HEAD');
2271}
2272
2273sub git_get_full_hash {
2274 return git_get_hash(@_);
2275}
2276
2277sub git_get_short_hash {
2278 return git_get_hash(@_, '--short=7');
2279}
2280
2281sub git_get_hash {
2282 my ($project, $hash, @options) = @_;
25691fbe 2283 my $o_git_dir = $git_dir;
df2c37a5 2284 my $retval = undef;
25691fbe 2285 $git_dir = "$projectroot/$project";
b629275f
MR
2286 if (open my $fd, '-|', git_cmd(), 'rev-parse',
2287 '--verify', '-q', @options, $hash) {
2288 $retval = <$fd>;
2289 chomp $retval if defined $retval;
df2c37a5 2290 close $fd;
df2c37a5 2291 }
25691fbe
DS
2292 if (defined $o_git_dir) {
2293 $git_dir = $o_git_dir;
2c5c008b 2294 }
df2c37a5
JH
2295 return $retval;
2296}
2297
717b8311
JN
2298# get type of given object
2299sub git_get_type {
2300 my $hash = shift;
2301
25691fbe 2302 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
717b8311
JN
2303 my $type = <$fd>;
2304 close $fd or return;
2305 chomp $type;
2306 return $type;
2307}
2308
b201927a
JN
2309# repository configuration
2310our $config_file = '';
2311our %config;
2312
2313# store multiple values for single key as anonymous array reference
2314# single values stored directly in the hash, not as [ <value> ]
2315sub hash_set_multi {
2316 my ($hash, $key, $value) = @_;
2317
2318 if (!exists $hash->{$key}) {
2319 $hash->{$key} = $value;
2320 } elsif (!ref $hash->{$key}) {
2321 $hash->{$key} = [ $hash->{$key}, $value ];
2322 } else {
2323 push @{$hash->{$key}}, $value;
2324 }
2325}
2326
2327# return hash of git project configuration
2328# optionally limited to some section, e.g. 'gitweb'
2329sub git_parse_project_config {
2330 my $section_regexp = shift;
2331 my %config;
2332
2333 local $/ = "\0";
2334
2335 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2336 or return;
2337
2338 while (my $keyval = <$fh>) {
2339 chomp $keyval;
2340 my ($key, $value) = split(/\n/, $keyval, 2);
2341
2342 hash_set_multi(\%config, $key, $value)
2343 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2344 }
2345 close $fh;
2346
2347 return %config;
2348}
2349
df5d10a3 2350# convert config value to boolean: 'true' or 'false'
b201927a
JN
2351# no value, number > 0, 'true' and 'yes' values are true
2352# rest of values are treated as false (never as error)
2353sub config_to_bool {
2354 my $val = shift;
2355
df5d10a3
MC
2356 return 1 if !defined $val; # section.key
2357
b201927a
JN
2358 # strip leading and trailing whitespace
2359 $val =~ s/^\s+//;
2360 $val =~ s/\s+$//;
2361
df5d10a3 2362 return (($val =~ /^\d+$/ && $val) || # section.key = 1
b201927a
JN
2363 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2364}
2365
2366# convert config value to simple decimal number
2367# an optional value suffix of 'k', 'm', or 'g' will cause the value
2368# to be multiplied by 1024, 1048576, or 1073741824
2369sub config_to_int {
2370 my $val = shift;
2371
2372 # strip leading and trailing whitespace
2373 $val =~ s/^\s+//;
2374 $val =~ s/\s+$//;
2375
2376 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2377 $unit = lc($unit);
2378 # unknown unit is treated as 1
2379 return $num * ($unit eq 'g' ? 1073741824 :
2380 $unit eq 'm' ? 1048576 :
2381 $unit eq 'k' ? 1024 : 1);
2382 }
2383 return $val;
2384}
2385
2386# convert config value to array reference, if needed
2387sub config_to_multi {
2388 my $val = shift;
2389
d76a585d 2390 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
b201927a
JN
2391}
2392
717b8311 2393sub git_get_project_config {
ddb8d900 2394 my ($key, $type) = @_;
717b8311 2395
7a49c254 2396 return unless defined $git_dir;
9be3614e 2397
b201927a 2398 # key sanity check
717b8311
JN
2399 return unless ($key);
2400 $key =~ s/^gitweb\.//;
2401 return if ($key =~ m/\W/);
2402
b201927a
JN
2403 # type sanity check
2404 if (defined $type) {
2405 $type =~ s/^--//;
2406 $type = undef
2407 unless ($type eq 'bool' || $type eq 'int');
2408 }
2409
2410 # get config
2411 if (!defined $config_file ||
2412 $config_file ne "$git_dir/config") {
2413 %config = git_parse_project_config('gitweb');
2414 $config_file = "$git_dir/config";
2415 }
2416
df5d10a3
MC
2417 # check if config variable (key) exists
2418 return unless exists $config{"gitweb.$key"};
2419
b201927a
JN
2420 # ensure given type
2421 if (!defined $type) {
2422 return $config{"gitweb.$key"};
2423 } elsif ($type eq 'bool') {
2424 # backward compatibility: 'git config --bool' returns true/false
2425 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2426 } elsif ($type eq 'int') {
2427 return config_to_int($config{"gitweb.$key"});
2428 }
2429 return $config{"gitweb.$key"};
717b8311
JN
2430}
2431
717b8311
JN
2432# get hash of given path at given ref
2433sub git_get_hash_by_path {
2434 my $base = shift;
2435 my $path = shift || return undef;
1d782b03 2436 my $type = shift;
717b8311 2437
4b02f483 2438 $path =~ s,/+$,,;
717b8311 2439
25691fbe 2440 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
074afaa0 2441 or die_error(500, "Open git-ls-tree failed");
717b8311
JN
2442 my $line = <$fd>;
2443 close $fd or return undef;
2444
198a2a8a
JN
2445 if (!defined $line) {
2446 # there is no tree or hash given by $path at $base
2447 return undef;
2448 }
2449
717b8311 2450 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8b4b94cc 2451 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1d782b03
JN
2452 if (defined $type && $type ne $2) {
2453 # type doesn't match
2454 return undef;
2455 }
717b8311
JN
2456 return $3;
2457}
2458
ed224dea
JN
2459# get path of entry with given hash at given tree-ish (ref)
2460# used to get 'from' filename for combined diff (merge commit) for renames
2461sub git_get_path_by_hash {
2462 my $base = shift || return;
2463 my $hash = shift || return;
2464
2465 local $/ = "\0";
2466
2467 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2468 or return undef;
2469 while (my $line = <$fd>) {
2470 chomp $line;
2471
2472 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2473 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2474 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2475 close $fd;
2476 return $1;
2477 }
2478 }
2479 close $fd;
2480 return undef;
2481}
2482
717b8311
JN
2483## ......................................................................
2484## git utility functions, directly accessing git repository
2485
847e01fb 2486sub git_get_project_description {
b87d78d6 2487 my $path = shift;
09bd7898 2488
0e121a2c 2489 $git_dir = "$projectroot/$path";
dff2b6d4 2490 open my $fd, '<', "$git_dir/description"
0e121a2c 2491 or return git_get_project_config('description');
b87d78d6
KS
2492 my $descr = <$fd>;
2493 close $fd;
2eb54efc
JH
2494 if (defined $descr) {
2495 chomp $descr;
2496 }
b87d78d6 2497 return $descr;
12a88f2f
KS
2498}
2499
aed93de4
PB
2500sub git_get_project_ctags {
2501 my $path = shift;
2502 my $ctags = {};
2503
2504 $git_dir = "$projectroot/$path";
ad87e4f6
JN
2505 opendir my $dh, "$git_dir/ctags"
2506 or return $ctags;
2507 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
dff2b6d4 2508 open my $ct, '<', $_ or next;
ad87e4f6 2509 my $val = <$ct>;
aed93de4 2510 chomp $val;
ad87e4f6 2511 close $ct;
aed93de4
PB
2512 my $ctag = $_; $ctag =~ s#.*/##;
2513 $ctags->{$ctag} = $val;
2514 }
ad87e4f6 2515 closedir $dh;
aed93de4
PB
2516 $ctags;
2517}
2518
2519sub git_populate_project_tagcloud {
2520 my $ctags = shift;
2521
2522 # First, merge different-cased tags; tags vote on casing
2523 my %ctags_lc;
2524 foreach (keys %$ctags) {
2525 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2526 if (not $ctags_lc{lc $_}->{topcount}
2527 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2528 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2529 $ctags_lc{lc $_}->{topname} = $_;
2530 }
2531 }
2532
2533 my $cloud;
2534 if (eval { require HTML::TagCloud; 1; }) {
2535 $cloud = HTML::TagCloud->new;
2536 foreach (sort keys %ctags_lc) {
2537 # Pad the title with spaces so that the cloud looks
2538 # less crammed.
2539 my $title = $ctags_lc{$_}->{topname};
2540 $title =~ s/ /&nbsp;/g;
2541 $title =~ s/^/&nbsp;/g;
2542 $title =~ s/$/&nbsp;/g;
2543 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2544 }
2545 } else {
2546 $cloud = \%ctags_lc;
2547 }
2548 $cloud;
2549}
2550
2551sub git_show_project_tagcloud {
2552 my ($cloud, $count) = @_;
2553 print STDERR ref($cloud)."..\n";
2554 if (ref $cloud eq 'HTML::TagCloud') {
2555 return $cloud->html_and_css($count);
2556 } else {
2557 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2558 return '<p align="center">' . join (', ', map {
2559 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2560 } splice(@tags, 0, $count)) . '</p>';
2561 }
2562}
2563
e79ca7cc
JN
2564sub git_get_project_url_list {
2565 my $path = shift;
2566
0e121a2c 2567 $git_dir = "$projectroot/$path";
dff2b6d4 2568 open my $fd, '<', "$git_dir/cloneurl"
0e121a2c
JN
2569 or return wantarray ?
2570 @{ config_to_multi(git_get_project_config('url')) } :
2571 config_to_multi(git_get_project_config('url'));
e79ca7cc
JN
2572 my @git_project_url_list = map { chomp; $_ } <$fd>;
2573 close $fd;
2574
2575 return wantarray ? @git_project_url_list : \@git_project_url_list;
2576}
2577
847e01fb 2578sub git_get_projects_list {
e30496df 2579 my ($filter) = @_;
717b8311
JN
2580 my @list;
2581
e30496df
PB
2582 $filter ||= '';
2583 $filter =~ s/\.git$//;
2584
25b2790f 2585 my $check_forks = gitweb_check_feature('forks');
c2b8b134 2586
717b8311
JN
2587 if (-d $projects_list) {
2588 # search in directory
e30496df 2589 my $dir = $projects_list . ($filter ? "/$filter" : '');
6768d6b8
AK
2590 # remove the trailing "/"
2591 $dir =~ s!/+$!!;
c0011ff8 2592 my $pfxlen = length("$dir");
ca5e9495 2593 my $pfxdepth = ($dir =~ tr!/!!);
c0011ff8
JN
2594
2595 File::Find::find({
2596 follow_fast => 1, # follow symbolic links
d20602ee 2597 follow_skip => 2, # ignore duplicates
c0011ff8
JN
2598 dangling_symlinks => 0, # ignore dangling symlinks, silently
2599 wanted => sub {
ee1d8ee0
JN
2600 # global variables
2601 our $project_maxdepth;
2602 our $projectroot;
c0011ff8
JN
2603 # skip project-list toplevel, if we get it.
2604 return if (m!^[/.]$!);
2605 # only directories can be git repositories
2606 return unless (-d $_);
ca5e9495
LL
2607 # don't traverse too deep (Find is super slow on os x)
2608 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2609 $File::Find::prune = 1;
2610 return;
2611 }
c0011ff8
JN
2612
2613 my $subdir = substr($File::Find::name, $pfxlen + 1);
2614 # we check related file in $projectroot
fb3bb3d1
DD
2615 my $path = ($filter ? "$filter/" : '') . $subdir;
2616 if (check_export_ok("$projectroot/$path")) {
2617 push @list, { path => $path };
c0011ff8
JN
2618 $File::Find::prune = 1;
2619 }
2620 },
2621 }, "$dir");
2622
717b8311
JN
2623 } elsif (-f $projects_list) {
2624 # read from file(url-encoded):
2625 # 'git%2Fgit.git Linus+Torvalds'
2626 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2627 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
c2b8b134 2628 my %paths;
dff2b6d4 2629 open my $fd, '<', $projects_list or return;
c2b8b134 2630 PROJECT:
717b8311
JN
2631 while (my $line = <$fd>) {
2632 chomp $line;
2633 my ($path, $owner) = split ' ', $line;
2634 $path = unescape($path);
2635 $owner = unescape($owner);
2636 if (!defined $path) {
2637 next;
2638 }
83ee94c1
JH
2639 if ($filter ne '') {
2640 # looking for forks;
2641 my $pfx = substr($path, 0, length($filter));
2642 if ($pfx ne $filter) {
c2b8b134 2643 next PROJECT;
83ee94c1
JH
2644 }
2645 my $sfx = substr($path, length($filter));
2646 if ($sfx !~ /^\/.*\.git$/) {
c2b8b134
FL
2647 next PROJECT;
2648 }
2649 } elsif ($check_forks) {
2650 PATH:
2651 foreach my $filter (keys %paths) {
2652 # looking for forks;
2653 my $pfx = substr($path, 0, length($filter));
2654 if ($pfx ne $filter) {
2655 next PATH;
2656 }
2657 my $sfx = substr($path, length($filter));
2658 if ($sfx !~ /^\/.*\.git$/) {
2659 next PATH;
2660 }
2661 # is a fork, don't include it in
2662 # the list
2663 next PROJECT;
83ee94c1
JH
2664 }
2665 }
2172ce4b 2666 if (check_export_ok("$projectroot/$path")) {
717b8311
JN
2667 my $pr = {
2668 path => $path,
00f429af 2669 owner => to_utf8($owner),
717b8311 2670 };
c2b8b134
FL
2671 push @list, $pr;
2672 (my $forks_path = $path) =~ s/\.git$//;
2673 $paths{$forks_path}++;
717b8311
JN
2674 }
2675 }
2676 close $fd;
2677 }
717b8311
JN
2678 return @list;
2679}
2680
47852450
JH
2681our $gitweb_project_owner = undef;
2682sub git_get_project_list_from_file {
1e0cf030 2683
47852450 2684 return if (defined $gitweb_project_owner);
1e0cf030 2685
47852450 2686 $gitweb_project_owner = {};
1e0cf030
JN
2687 # read from file (url-encoded):
2688 # 'git%2Fgit.git Linus+Torvalds'
2689 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2690 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2691 if (-f $projects_list) {
dff2b6d4 2692 open(my $fd, '<', $projects_list);
1e0cf030
JN
2693 while (my $line = <$fd>) {
2694 chomp $line;
2695 my ($pr, $ow) = split ' ', $line;
2696 $pr = unescape($pr);
2697 $ow = unescape($ow);
47852450 2698 $gitweb_project_owner->{$pr} = to_utf8($ow);
1e0cf030
JN
2699 }
2700 close $fd;
2701 }
47852450
JH
2702}
2703
2704sub git_get_project_owner {
2705 my $project = shift;
2706 my $owner;
2707
2708 return undef unless $project;
b59012ef 2709 $git_dir = "$projectroot/$project";
47852450
JH
2710
2711 if (!defined $gitweb_project_owner) {
2712 git_get_project_list_from_file();
2713 }
2714
2715 if (exists $gitweb_project_owner->{$project}) {
2716 $owner = $gitweb_project_owner->{$project};
2717 }
b59012ef
BR
2718 if (!defined $owner){
2719 $owner = git_get_project_config('owner');
2720 }
1e0cf030 2721 if (!defined $owner) {
b59012ef 2722 $owner = get_file_owner("$git_dir");
1e0cf030
JN
2723 }
2724
2725 return $owner;
2726}
2727
c60c56cc
JN
2728sub git_get_last_activity {
2729 my ($path) = @_;
2730 my $fd;
2731
2732 $git_dir = "$projectroot/$path";
2733 open($fd, "-|", git_cmd(), 'for-each-ref',
0ff5ec70 2734 '--format=%(committer)',
c60c56cc 2735 '--sort=-committerdate',
0ff5ec70 2736 '--count=1',
c60c56cc
JN
2737 'refs/heads') or return;
2738 my $most_recent = <$fd>;
2739 close $fd or return;
785cdea9
JN
2740 if (defined $most_recent &&
2741 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
c60c56cc
JN
2742 my $timestamp = $1;
2743 my $age = time - $timestamp;
2744 return ($age, age_string($age));
2745 }
c956395e 2746 return (undef, undef);
c60c56cc
JN
2747}
2748
847e01fb 2749sub git_get_references {
717b8311
JN
2750 my $type = shift || "";
2751 my %refs;
28b9d9f7
JN
2752 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2753 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2754 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2755 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
9704d75d 2756 or return;
d294e1ca 2757
717b8311
JN
2758 while (my $line = <$fd>) {
2759 chomp $line;
4afbaeff 2760 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
717b8311 2761 if (defined $refs{$1}) {
d294e1ca 2762 push @{$refs{$1}}, $2;
717b8311 2763 } else {
d294e1ca 2764 $refs{$1} = [ $2 ];
717b8311
JN
2765 }
2766 }
2767 }
2768 close $fd or return;
2769 return \%refs;
2770}
2771
56a322f1
JN
2772sub git_get_rev_name_tags {
2773 my $hash = shift || return undef;
2774
25691fbe 2775 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
56a322f1
JN
2776 or return;
2777 my $name_rev = <$fd>;
2778 close $fd;
2779
2780 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2781 return $1;
2782 } else {
2783 # catches also '$hash undefined' output
2784 return undef;
2785 }
2786}
2787
717b8311
JN
2788## ----------------------------------------------------------------------
2789## parse to hash functions
2790
847e01fb 2791sub parse_date {
717b8311
JN
2792 my $epoch = shift;
2793 my $tz = shift || "-0000";
2794
2795 my %date;
2796 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2797 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2798 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2799 $date{'hour'} = $hour;
2800 $date{'minute'} = $min;
2801 $date{'mday'} = $mday;
2802 $date{'day'} = $days[$wday];
2803 $date{'month'} = $months[$mon];
af6feeb2
JN
2804 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2805 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
952c65fc
JN
2806 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2807 $mday, $months[$mon], $hour ,$min;
af6feeb2 2808 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
a62d6d84 2809 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
717b8311
JN
2810
2811 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2812 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2813 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2814 $date{'hour_local'} = $hour;
2815 $date{'minute_local'} = $min;
2816 $date{'tz_local'} = $tz;
af6feeb2
JN
2817 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2818 1900+$year, $mon+1, $mday,
2819 $hour, $min, $sec, $tz);
717b8311
JN
2820 return %date;
2821}
2822
847e01fb 2823sub parse_tag {
ede5e100
KS
2824 my $tag_id = shift;
2825 my %tag;
d8a20ba9 2826 my @comment;
ede5e100 2827
25691fbe 2828 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
d8a20ba9 2829 $tag{'id'} = $tag_id;
ede5e100
KS
2830 while (my $line = <$fd>) {
2831 chomp $line;
2832 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2833 $tag{'object'} = $1;
7ab0d2b6 2834 } elsif ($line =~ m/^type (.+)$/) {
ede5e100 2835 $tag{'type'} = $1;
7ab0d2b6 2836 } elsif ($line =~ m/^tag (.+)$/) {
ede5e100 2837 $tag{'name'} = $1;
d8a20ba9
KS
2838 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2839 $tag{'author'} = $1;
ba924733
GB
2840 $tag{'author_epoch'} = $2;
2841 $tag{'author_tz'} = $3;
2842 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2843 $tag{'author_name'} = $1;
2844 $tag{'author_email'} = $2;
2845 } else {
2846 $tag{'author_name'} = $tag{'author'};
2847 }
d8a20ba9
KS
2848 } elsif ($line =~ m/--BEGIN/) {
2849 push @comment, $line;
2850 last;
2851 } elsif ($line eq "") {
2852 last;
ede5e100
KS
2853 }
2854 }
d8a20ba9
KS
2855 push @comment, <$fd>;
2856 $tag{'comment'} = \@comment;
19806691 2857 close $fd or return;
ede5e100
KS
2858 if (!defined $tag{'name'}) {
2859 return
2860 };
2861 return %tag
2862}
2863
756bbf54 2864sub parse_commit_text {
ccdfdea0 2865 my ($commit_text, $withparents) = @_;
756bbf54 2866 my @commit_lines = split '\n', $commit_text;
703ac710 2867 my %co;
703ac710 2868
756bbf54
RF
2869 pop @commit_lines; # Remove '\0'
2870
198a2a8a
JN
2871 if (! @commit_lines) {
2872 return;
2873 }
2874
25f422fb 2875 my $header = shift @commit_lines;
198a2a8a 2876 if ($header !~ m/^[0-9a-fA-F]{40}/) {
25f422fb
KS
2877 return;
2878 }
ccdfdea0 2879 ($co{'id'}, my @parents) = split ' ', $header;
19806691 2880 while (my $line = shift @commit_lines) {
b87d78d6 2881 last if $line eq "\n";
7ab0d2b6 2882 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
703ac710 2883 $co{'tree'} = $1;
ccdfdea0 2884 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
208b2dff 2885 push @parents, $1;
022be3d0 2886 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
5ed5bbc7 2887 $co{'author'} = to_utf8($1);
185f09e5
KS
2888 $co{'author_epoch'} = $2;
2889 $co{'author_tz'} = $3;
ba00b8c1
JN
2890 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2891 $co{'author_name'} = $1;
2892 $co{'author_email'} = $2;
2bf7a52c
KS
2893 } else {
2894 $co{'author_name'} = $co{'author'};
2895 }
86eed32d 2896 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
5ed5bbc7 2897 $co{'committer'} = to_utf8($1);
185f09e5
KS
2898 $co{'committer_epoch'} = $2;
2899 $co{'committer_tz'} = $3;
ba00b8c1
JN
2900 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2901 $co{'committer_name'} = $1;
2902 $co{'committer_email'} = $2;
2903 } else {
2904 $co{'committer_name'} = $co{'committer'};
2905 }
703ac710
KS
2906 }
2907 }
ede5e100 2908 if (!defined $co{'tree'}) {
25f422fb 2909 return;
ede5e100 2910 };
208b2dff
RF
2911 $co{'parents'} = \@parents;
2912 $co{'parent'} = $parents[0];
25f422fb 2913
19806691 2914 foreach my $title (@commit_lines) {
c2488d06 2915 $title =~ s/^ //;
19806691 2916 if ($title ne "") {
48c771f4 2917 $co{'title'} = chop_str($title, 80, 5);
19806691
KS
2918 # remove leading stuff of merges to make the interesting part visible
2919 if (length($title) > 50) {
2920 $title =~ s/^Automatic //;
2921 $title =~ s/^merge (of|with) /Merge ... /i;
2922 if (length($title) > 50) {
2923 $title =~ s/(http|rsync):\/\///;
2924 }
2925 if (length($title) > 50) {
2926 $title =~ s/(master|www|rsync)\.//;
2927 }
2928 if (length($title) > 50) {
2929 $title =~ s/kernel.org:?//;
2930 }
2931 if (length($title) > 50) {
2932 $title =~ s/\/pub\/scm//;
2933 }
2934 }
48c771f4 2935 $co{'title_short'} = chop_str($title, 50, 5);
19806691
KS
2936 last;
2937 }
2938 }
53c39676 2939 if (! defined $co{'title'} || $co{'title'} eq "") {
7e0fe5c9
PB
2940 $co{'title'} = $co{'title_short'} = '(no commit message)';
2941 }
25f422fb
KS
2942 # remove added spaces
2943 foreach my $line (@commit_lines) {
2944 $line =~ s/^ //;
2945 }
2946 $co{'comment'} = \@commit_lines;
2ae100df
KS
2947
2948 my $age = time - $co{'committer_epoch'};
2949 $co{'age'} = $age;
d263a6bd 2950 $co{'age_string'} = age_string($age);
71be1e79
KS
2951 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2952 if ($age > 60*60*24*7*2) {
1b1cd421 2953 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79
KS
2954 $co{'age_string_age'} = $co{'age_string'};
2955 } else {
2956 $co{'age_string_date'} = $co{'age_string'};
1b1cd421 2957 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79 2958 }
703ac710
KS
2959 return %co;
2960}
2961
756bbf54
RF
2962sub parse_commit {
2963 my ($commit_id) = @_;
2964 my %co;
2965
2966 local $/ = "\0";
2967
2968 open my $fd, "-|", git_cmd(), "rev-list",
ccdfdea0 2969 "--parents",
756bbf54 2970 "--header",
756bbf54
RF
2971 "--max-count=1",
2972 $commit_id,
2973 "--",
074afaa0 2974 or die_error(500, "Open git-rev-list failed");
ccdfdea0 2975 %co = parse_commit_text(<$fd>, 1);
756bbf54
RF
2976 close $fd;
2977
2978 return %co;
2979}
2980
2981sub parse_commits {
311e552e 2982 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
756bbf54
RF
2983 my @cos;
2984
2985 $maxcount ||= 1;
2986 $skip ||= 0;
2987
756bbf54
RF
2988 local $/ = "\0";
2989
2990 open my $fd, "-|", git_cmd(), "rev-list",
2991 "--header",
311e552e 2992 @args,
756bbf54 2993 ("--max-count=" . $maxcount),
f47efbb7 2994 ("--skip=" . $skip),
868bc068 2995 @extra_options,
756bbf54
RF
2996 $commit_id,
2997 "--",
2998 ($filename ? ($filename) : ())
074afaa0 2999 or die_error(500, "Open git-rev-list failed");
756bbf54
RF
3000 while (my $line = <$fd>) {
3001 my %co = parse_commit_text($line);
3002 push @cos, \%co;
3003 }
3004 close $fd;
3005
3006 return wantarray ? @cos : \@cos;
3007}
3008
e8e41a93 3009# parse line of git-diff-tree "raw" output
740e67f9
JN
3010sub parse_difftree_raw_line {
3011 my $line = shift;
3012 my %res;
3013
3014 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
3015 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
3016 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3017 $res{'from_mode'} = $1;
3018 $res{'to_mode'} = $2;
3019 $res{'from_id'} = $3;
3020 $res{'to_id'} = $4;
4ed4a347 3021 $res{'status'} = $5;
740e67f9
JN
3022 $res{'similarity'} = $6;
3023 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
e8e41a93 3024 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
740e67f9 3025 } else {
9d301456 3026 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
740e67f9
JN
3027 }
3028 }
78bc403a
JN
3029 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3030 # combined diff (for merge commit)
3031 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3032 $res{'nparents'} = length($1);
3033 $res{'from_mode'} = [ split(' ', $2) ];
3034 $res{'to_mode'} = pop @{$res{'from_mode'}};
3035 $res{'from_id'} = [ split(' ', $3) ];
3036 $res{'to_id'} = pop @{$res{'from_id'}};
3037 $res{'status'} = [ split('', $4) ];
3038 $res{'to_file'} = unquote($5);
3039 }
740e67f9 3040 # 'c512b523472485aef4fff9e57b229d9d243c967f'
0edcb37d
JN
3041 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3042 $res{'commit'} = $1;
3043 }
740e67f9
JN
3044
3045 return wantarray ? %res : \%res;
3046}
3047
0cec6db5
JN
3048# wrapper: return parsed line of git-diff-tree "raw" output
3049# (the argument might be raw line, or parsed info)
3050sub parsed_difftree_line {
3051 my $line_or_ref = shift;
3052
3053 if (ref($line_or_ref) eq "HASH") {
3054 # pre-parsed (or generated by hand)
3055 return $line_or_ref;
3056 } else {
3057 return parse_difftree_raw_line($line_or_ref);
3058 }
3059}
3060
cb849b46 3061# parse line of git-ls-tree output
74fd8728 3062sub parse_ls_tree_line {
cb849b46
JN
3063 my $line = shift;
3064 my %opts = @_;
3065 my %res;
3066
e4b48eaa
JN
3067 if ($opts{'-l'}) {
3068 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3069 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
cb849b46 3070
e4b48eaa
JN
3071 $res{'mode'} = $1;
3072 $res{'type'} = $2;
3073 $res{'hash'} = $3;
3074 $res{'size'} = $4;
3075 if ($opts{'-z'}) {
3076 $res{'name'} = $5;
3077 } else {
3078 $res{'name'} = unquote($5);
3079 }
cb849b46 3080 } else {
e4b48eaa
JN
3081 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3082 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3083
3084 $res{'mode'} = $1;
3085 $res{'type'} = $2;
3086 $res{'hash'} = $3;
3087 if ($opts{'-z'}) {
3088 $res{'name'} = $4;
3089 } else {
3090 $res{'name'} = unquote($4);
3091 }
cb849b46
JN
3092 }
3093
3094 return wantarray ? %res : \%res;
3095}
3096
90921740
JN
3097# generates _two_ hashes, references to which are passed as 2 and 3 argument
3098sub parse_from_to_diffinfo {
3099 my ($diffinfo, $from, $to, @parents) = @_;
3100
3101 if ($diffinfo->{'nparents'}) {
3102 # combined diff
3103 $from->{'file'} = [];
3104 $from->{'href'} = [];
3105 fill_from_file_info($diffinfo, @parents)
3106 unless exists $diffinfo->{'from_file'};
3107 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
9d301456
JN
3108 $from->{'file'}[$i] =
3109 defined $diffinfo->{'from_file'}[$i] ?
3110 $diffinfo->{'from_file'}[$i] :
3111 $diffinfo->{'to_file'};
90921740
JN
3112 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3113 $from->{'href'}[$i] = href(action=>"blob",
3114 hash_base=>$parents[$i],
3115 hash=>$diffinfo->{'from_id'}[$i],
3116 file_name=>$from->{'file'}[$i]);
3117 } else {
3118 $from->{'href'}[$i] = undef;
3119 }
3120 }
3121 } else {
0cec6db5 3122 # ordinary (not combined) diff
9d301456 3123 $from->{'file'} = $diffinfo->{'from_file'};
90921740
JN
3124 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3125 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3126 hash=>$diffinfo->{'from_id'},
3127 file_name=>$from->{'file'});
3128 } else {
3129 delete $from->{'href'};
3130 }
3131 }
3132
9d301456 3133 $to->{'file'} = $diffinfo->{'to_file'};
90921740
JN
3134 if (!is_deleted($diffinfo)) { # file exists in result
3135 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3136 hash=>$diffinfo->{'to_id'},
3137 file_name=>$to->{'file'});
3138 } else {
3139 delete $to->{'href'};
3140 }
3141}
3142
717b8311
JN
3143## ......................................................................
3144## parse to array of hashes functions
4c02e3c5 3145
cd146408
JN
3146sub git_get_heads_list {
3147 my $limit = shift;
3148 my @headslist;
3149
3150 open my $fd, '-|', git_cmd(), 'for-each-ref',
3151 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3152 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3153 'refs/heads'
c83a77e4
JN
3154 or return;
3155 while (my $line = <$fd>) {
cd146408 3156 my %ref_item;
120ddde2 3157
cd146408
JN
3158 chomp $line;
3159 my ($refinfo, $committerinfo) = split(/\0/, $line);
3160 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3161 my ($committer, $epoch, $tz) =
3162 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 3163 $ref_item{'fullname'} = $name;
cd146408
JN
3164 $name =~ s!^refs/heads/!!;
3165
3166 $ref_item{'name'} = $name;
3167 $ref_item{'id'} = $hash;
3168 $ref_item{'title'} = $title || '(no commit message)';
3169 $ref_item{'epoch'} = $epoch;
3170 if ($epoch) {
3171 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3172 } else {
3173 $ref_item{'age'} = "unknown";
717b8311 3174 }
cd146408
JN
3175
3176 push @headslist, \%ref_item;
c83a77e4
JN
3177 }
3178 close $fd;
3179
cd146408
JN
3180 return wantarray ? @headslist : \@headslist;
3181}
3182
3183sub git_get_tags_list {
3184 my $limit = shift;
3185 my @tagslist;
3186
3187 open my $fd, '-|', git_cmd(), 'for-each-ref',
3188 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3189 '--format=%(objectname) %(objecttype) %(refname) '.
3190 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3191 'refs/tags'
3192 or return;
3193 while (my $line = <$fd>) {
3194 my %ref_item;
7a13b999 3195
cd146408
JN
3196 chomp $line;
3197 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3198 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3199 my ($creator, $epoch, $tz) =
3200 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 3201 $ref_item{'fullname'} = $name;
cd146408
JN
3202 $name =~ s!^refs/tags/!!;
3203
3204 $ref_item{'type'} = $type;
3205 $ref_item{'id'} = $id;
3206 $ref_item{'name'} = $name;
3207 if ($type eq "tag") {
3208 $ref_item{'subject'} = $title;
3209 $ref_item{'reftype'} = $reftype;
3210 $ref_item{'refid'} = $refid;
3211 } else {
3212 $ref_item{'reftype'} = $type;
3213 $ref_item{'refid'} = $id;
3214 }
3215
3216 if ($type eq "tag" || $type eq "commit") {
3217 $ref_item{'epoch'} = $epoch;
3218 if ($epoch) {
3219 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3220 } else {
3221 $ref_item{'age'} = "unknown";
3222 }
3223 }
991910a9 3224
cd146408 3225 push @tagslist, \%ref_item;
717b8311 3226 }
cd146408
JN
3227 close $fd;
3228
3229 return wantarray ? @tagslist : \@tagslist;
86eed32d
KS
3230}
3231
717b8311
JN
3232## ----------------------------------------------------------------------
3233## filesystem-related functions
022be3d0 3234
c07ad4b9
KS
3235sub get_file_owner {
3236 my $path = shift;
3237
3238 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3239 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3240 if (!defined $gcos) {
3241 return undef;
3242 }
3243 my $owner = $gcos;
3244 $owner =~ s/[,;].*$//;
00f429af 3245 return to_utf8($owner);
c07ad4b9
KS
3246}
3247
2dcb5e1a
JN
3248# assume that file exists
3249sub insert_file {
3250 my $filename = shift;
3251
3252 open my $fd, '<', $filename;
4586864a 3253 print map { to_utf8($_) } <$fd>;
2dcb5e1a
JN
3254 close $fd;
3255}
3256
717b8311
JN
3257## ......................................................................
3258## mimetype related functions
09bd7898 3259
717b8311
JN
3260sub mimetype_guess_file {
3261 my $filename = shift;
3262 my $mimemap = shift;
3263 -r $mimemap or return undef;
3264
3265 my %mimemap;
dff2b6d4 3266 open(my $mh, '<', $mimemap) or return undef;
ad87e4f6 3267 while (<$mh>) {
618918e5 3268 next if m/^#/; # skip comments
ad87e4f6 3269 my ($mimetype, $exts) = split(/\t+/);
46b059d7
JH
3270 if (defined $exts) {
3271 my @exts = split(/\s+/, $exts);
3272 foreach my $ext (@exts) {
ad87e4f6 3273 $mimemap{$ext} = $mimetype;
46b059d7 3274 }
09bd7898 3275 }
09bd7898 3276 }
ad87e4f6 3277 close($mh);
09bd7898 3278
8059319a 3279 $filename =~ /\.([^.]*)$/;
717b8311
JN
3280 return $mimemap{$1};
3281}
5996ca08 3282
717b8311
JN
3283sub mimetype_guess {
3284 my $filename = shift;
3285 my $mime;
3286 $filename =~ /\./ or return undef;
5996ca08 3287
717b8311
JN
3288 if ($mimetypes_file) {
3289 my $file = $mimetypes_file;
d5aa50de
JN
3290 if ($file !~ m!^/!) { # if it is relative path
3291 # it is relative to project
3292 $file = "$projectroot/$project/$file";
3293 }
717b8311
JN
3294 $mime = mimetype_guess_file($filename, $file);
3295 }
3296 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3297 return $mime;
5996ca08
FF
3298}
3299
847e01fb 3300sub blob_mimetype {
717b8311
JN
3301 my $fd = shift;
3302 my $filename = shift;
5996ca08 3303
717b8311
JN
3304 if ($filename) {
3305 my $mime = mimetype_guess($filename);
3306 $mime and return $mime;
d8d17b5d 3307 }
717b8311
JN
3308
3309 # just in case
3310 return $default_blob_plain_mimetype unless $fd;
3311
3312 if (-T $fd) {
7f718e8b 3313 return 'text/plain';
717b8311
JN
3314 } elsif (! $filename) {
3315 return 'application/octet-stream';
3316 } elsif ($filename =~ m/\.png$/i) {
3317 return 'image/png';
3318 } elsif ($filename =~ m/\.gif$/i) {
3319 return 'image/gif';
3320 } elsif ($filename =~ m/\.jpe?g$/i) {
3321 return 'image/jpeg';
d8d17b5d 3322 } else {
717b8311 3323 return 'application/octet-stream';
f7ab660c 3324 }
717b8311
JN
3325}
3326
7f718e8b
JN
3327sub blob_contenttype {
3328 my ($fd, $file_name, $type) = @_;
3329
3330 $type ||= blob_mimetype($fd, $file_name);
3331 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3332 $type .= "; charset=$default_text_plain_charset";
3333 }
3334
3335 return $type;
3336}
3337
592ea417
JN
3338# guess file syntax for syntax highlighting; return undef if no highlighting
3339# the name of syntax can (in the future) depend on syntax highlighter used
3340sub guess_file_syntax {
3341 my ($highlight, $mimetype, $file_name) = @_;
3342 return undef unless ($highlight && defined $file_name);
592ea417
JN
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,
22e5e58a 3785# in the format expected for 'commit' view (& similar).
1c49a4e1 3786# Parameters are a commit hash reference, followed by the list of people
22e5e58a 3787# to output information for. If the list is empty it defaults to both
1c49a4e1
GB
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
22e5e58a
RW
4516 # for compact combined (--cc) format, with chunk and patch simplification
4517 # the 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 %tag = parse_tag($hash);
198a2a8a
JN
5196
5197 if (! %tag) {
074afaa0 5198 die_error(404, "Unknown tag object");
198a2a8a
JN
5199 }
5200
d8a94803
AK
5201 my $head = git_get_head_hash($project);
5202 git_header_html();
5203 git_print_page_nav('','', $head,undef,$head);
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
8dbc0fce
RF
6525 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6526 git_print_header_div('commit', esc_html($co{'title'}), $hash);
497d9c34
JN
6527 if ($page == 0 && !@commitlist) {
6528 print "<p>No match.</p>\n";
6529 } else {
6530 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6531 }
c994d620
KS
6532 }
6533
88ad729b 6534 if ($searchtype eq 'pickaxe') {
8dbc0fce
RF
6535 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6536 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6537
591ebf65 6538 print "<table class=\"pickaxe search\">\n";
8dbc0fce 6539 my $alternate = 1;
34122b57 6540 local $/ = "\n";
c582abae
JN
6541 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6542 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6543 ($search_use_regexp ? '--pickaxe-regex' : ());
c994d620
KS
6544 undef %co;
6545 my @files;
6546 while (my $line = <$fd>) {
c582abae
JN
6547 chomp $line;
6548 next unless $line;
6549
6550 my %set = parse_difftree_raw_line($line);
6551 if (defined $set{'commit'}) {
6552 # finish previous commit
c994d620 6553 if (%co) {
c994d620
KS
6554 print "</td>\n" .
6555 "<td class=\"link\">" .
756d2f06 6556 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
952c65fc
JN
6557 " | " .
6558 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
c994d620
KS
6559 print "</td>\n" .
6560 "</tr>\n";
6561 }
c582abae
JN
6562
6563 if ($alternate) {
6564 print "<tr class=\"dark\">\n";
6565 } else {
6566 print "<tr class=\"light\">\n";
6567 }
6568 $alternate ^= 1;
6569 %co = parse_commit($set{'commit'});
6570 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6571 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6572 "<td><i>$author</i></td>\n" .
6573 "<td>" .
6574 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6575 -class => "list subject"},
6576 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6577 } elsif (defined $set{'to_id'}) {
6578 next if ($set{'to_id'} =~ m/^0{40}$/);
6579
6580 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6581 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6582 -class => "list"},
6583 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6584 "<br/>\n";
19806691 6585 }
19806691 6586 }
c994d620 6587 close $fd;
8dbc0fce 6588
c582abae
JN
6589 # finish last commit (warning: repetition!)
6590 if (%co) {
6591 print "</td>\n" .
6592 "<td class=\"link\">" .
6593 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6594 " | " .
6595 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6596 print "</td>\n" .
6597 "</tr>\n";
6598 }
6599
8dbc0fce 6600 print "</table>\n";
19806691 6601 }
e7738553
PB
6602
6603 if ($searchtype eq 'grep') {
6604 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6605 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6606
591ebf65 6607 print "<table class=\"grep_search\">\n";
e7738553
PB
6608 my $alternate = 1;
6609 my $matches = 0;
34122b57 6610 local $/ = "\n";
0e559919
PB
6611 open my $fd, "-|", git_cmd(), 'grep', '-n',
6612 $search_use_regexp ? ('-E', '-i') : '-F',
6613 $searchtext, $co{'tree'};
e7738553
PB
6614 my $lastfile = '';
6615 while (my $line = <$fd>) {
6616 chomp $line;
6617 my ($file, $lno, $ltext, $binary);
6618 last if ($matches++ > 1000);
6619 if ($line =~ /^Binary file (.+) matches$/) {
6620 $file = $1;
6621 $binary = 1;
6622 } else {
6623 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6624 }
6625 if ($file ne $lastfile) {
6626 $lastfile and print "</td></tr>\n";
6627 if ($alternate++) {
6628 print "<tr class=\"dark\">\n";
6629 } else {
6630 print "<tr class=\"light\">\n";
6631 }
6632 print "<td class=\"list\">".
6633 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6634 file_name=>"$file"),
6635 -class => "list"}, esc_path($file));
6636 print "</td><td>\n";
6637 $lastfile = $file;
6638 }
6639 if ($binary) {
6640 print "<div class=\"binary\">Binary file</div>\n";
6641 } else {
6642 $ltext = untabify($ltext);
0e559919 6643 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
e7738553
PB
6644 $ltext = esc_html($1, -nbsp=>1);
6645 $ltext .= '<span class="match">';
6646 $ltext .= esc_html($2, -nbsp=>1);
6647 $ltext .= '</span>';
6648 $ltext .= esc_html($3, -nbsp=>1);
6649 } else {
6650 $ltext = esc_html($ltext, -nbsp=>1);
6651 }
6652 print "<div class=\"pre\">" .
6653 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6654 file_name=>"$file").'#l'.$lno,
6655 -class => "linenr"}, sprintf('%4i', $lno))
6656 . ' ' . $ltext . "</div>\n";
6657 }
6658 }
6659 if ($lastfile) {
6660 print "</td></tr>\n";
6661 if ($matches > 1000) {
6662 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6663 }
6664 } else {
6665 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6666 }
6667 close $fd;
6668
6669 print "</table>\n";
6670 }
19806691
KS
6671 git_footer_html();
6672}
6673
88ad729b
PB
6674sub git_search_help {
6675 git_header_html();
6676 git_print_page_nav('','', $hash,$hash,$hash);
6677 print <<EOT;
0e559919
PB
6678<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6679regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6680the pattern entered is recognized as the POSIX extended
6681<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6682insensitive).</p>
88ad729b
PB
6683<dl>
6684<dt><b>commit</b></dt>
0e559919 6685<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
e7738553 6686EOT
25b2790f 6687 my $have_grep = gitweb_check_feature('grep');
e7738553
PB
6688 if ($have_grep) {
6689 print <<EOT;
6690<dt><b>grep</b></dt>
6691<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
0e559919
PB
6692 a different one) are searched for the given pattern. On large trees, this search can take
6693a while and put some strain on the server, so please use it with some consideration. Note that
6694due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6695case-sensitive.</dd>
e7738553
PB
6696EOT
6697 }
6698 print <<EOT;
88ad729b 6699<dt><b>author</b></dt>
0e559919 6700<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 6701<dt><b>committer</b></dt>
0e559919 6702<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
88ad729b 6703EOT
25b2790f 6704 my $have_pickaxe = gitweb_check_feature('pickaxe');
88ad729b
PB
6705 if ($have_pickaxe) {
6706 print <<EOT;
6707<dt><b>pickaxe</b></dt>
6708<dd>All commits that caused the string to appear or disappear from any file (changes that
6709added, removed or "modified" the string) will be listed. This search can take a while and
0e559919
PB
6710takes a lot of strain on the server, so please use it wisely. Note that since you may be
6711interested even in changes just changing the case as well, this search is case sensitive.</dd>
88ad729b
PB
6712EOT
6713 }
6714 print "</dl>\n";
6715 git_footer_html();
6716}
6717
19806691 6718sub git_shortlog {
69ca37d2
JN
6719 git_log_generic('shortlog', \&git_shortlog_body,
6720 $hash, $hash_parent);
19806691 6721}
717b8311
JN
6722
6723## ......................................................................
af6feeb2 6724## feeds (RSS, Atom; OPML)
717b8311 6725
af6feeb2
JN
6726sub git_feed {
6727 my $format = shift || 'atom';
25b2790f 6728 my $have_blame = gitweb_check_feature('blame');
af6feeb2
JN
6729
6730 # Atom: http://www.atomenabled.org/developers/syndication/
6731 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6732 if ($format ne 'rss' && $format ne 'atom') {
074afaa0 6733 die_error(400, "Unknown web feed format");
af6feeb2
JN
6734 }
6735
6736 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6737 my $head = $hash || 'HEAD';
311e552e 6738 my @commitlist = parse_commits($head, 150, 0, $file_name);
af6feeb2
JN
6739
6740 my %latest_commit;
6741 my %latest_date;
6742 my $content_type = "application/$format+xml";
6743 if (defined $cgi->http('HTTP_ACCEPT') &&
6744 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6745 # browser (feed reader) prefers text/xml
6746 $content_type = 'text/xml';
6747 }
b6093a5c
RF
6748 if (defined($commitlist[0])) {
6749 %latest_commit = %{$commitlist[0]};
cd956c73
GB
6750 my $latest_epoch = $latest_commit{'committer_epoch'};
6751 %latest_date = parse_date($latest_epoch);
6752 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6753 if (defined $if_modified) {
6754 my $since;
6755 if (eval { require HTTP::Date; 1; }) {
6756 $since = HTTP::Date::str2time($if_modified);
6757 } elsif (eval { require Time::ParseDate; 1; }) {
6758 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6759 }
6760 if (defined $since && $latest_epoch <= $since) {
6761 print $cgi->header(
6762 -type => $content_type,
6763 -charset => 'utf-8',
6764 -last_modified => $latest_date{'rfc2822'},
6765 -status => '304 Not Modified');
6766 return;
6767 }
6768 }
af6feeb2
JN
6769 print $cgi->header(
6770 -type => $content_type,
6771 -charset => 'utf-8',
6772 -last_modified => $latest_date{'rfc2822'});
6773 } else {
6774 print $cgi->header(
6775 -type => $content_type,
6776 -charset => 'utf-8');
6777 }
6778
6779 # Optimization: skip generating the body if client asks only
6780 # for Last-Modified date.
6781 return if ($cgi->request_method() eq 'HEAD');
6782
6783 # header variables
6784 my $title = "$site_name - $project/$action";
6785 my $feed_type = 'log';
6786 if (defined $hash) {
6787 $title .= " - '$hash'";
6788 $feed_type = 'branch log';
6789 if (defined $file_name) {
6790 $title .= " :: $file_name";
6791 $feed_type = 'history';
6792 }
6793 } elsif (defined $file_name) {
6794 $title .= " - $file_name";
6795 $feed_type = 'history';
6796 }
6797 $title .= " $feed_type";
6798 my $descr = git_get_project_description($project);
6799 if (defined $descr) {
6800 $descr = esc_html($descr);
6801 } else {
6802 $descr = "$project " .
6803 ($format eq 'rss' ? 'RSS' : 'Atom') .
6804 " feed";
6805 }
6806 my $owner = git_get_project_owner($project);
6807 $owner = esc_html($owner);
6808
6809 #header
6810 my $alt_url;
6811 if (defined $file_name) {
6812 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6813 } elsif (defined $hash) {
6814 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6815 } else {
6816 $alt_url = href(-full=>1, action=>"summary");
6817 }
6818 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6819 if ($format eq 'rss') {
6820 print <<XML;
59b9f61a
JN
6821<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6822<channel>
59b9f61a 6823XML
af6feeb2
JN
6824 print "<title>$title</title>\n" .
6825 "<link>$alt_url</link>\n" .
6826 "<description>$descr</description>\n" .
3ac109ae
GB
6827 "<language>en</language>\n" .
6828 # project owner is responsible for 'editorial' content
6829 "<managingEditor>$owner</managingEditor>\n";
1ba68ce2
GB
6830 if (defined $logo || defined $favicon) {
6831 # prefer the logo to the favicon, since RSS
6832 # doesn't allow both
6833 my $img = esc_url($logo || $favicon);
6834 print "<image>\n" .
6835 "<url>$img</url>\n" .
6836 "<title>$title</title>\n" .
6837 "<link>$alt_url</link>\n" .
6838 "</image>\n";
6839 }
0cf31285
GB
6840 if (%latest_date) {
6841 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6842 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6843 }
ad59a7a3 6844 print "<generator>gitweb v.$version/$git_version</generator>\n";
af6feeb2
JN
6845 } elsif ($format eq 'atom') {
6846 print <<XML;
6847<feed xmlns="http://www.w3.org/2005/Atom">
6848XML
6849 print "<title>$title</title>\n" .
6850 "<subtitle>$descr</subtitle>\n" .
6851 '<link rel="alternate" type="text/html" href="' .
6852 $alt_url . '" />' . "\n" .
6853 '<link rel="self" type="' . $content_type . '" href="' .
6854 $cgi->self_url() . '" />' . "\n" .
6855 "<id>" . href(-full=>1) . "</id>\n" .
6856 # use project owner for feed author
6857 "<author><name>$owner</name></author>\n";
6858 if (defined $favicon) {
6859 print "<icon>" . esc_url($favicon) . "</icon>\n";
6860 }
6861 if (defined $logo_url) {
6862 # not twice as wide as tall: 72 x 27 pixels
e1147267 6863 print "<logo>" . esc_url($logo) . "</logo>\n";
af6feeb2
JN
6864 }
6865 if (! %latest_date) {
6866 # dummy date to keep the feed valid until commits trickle in:
6867 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6868 } else {
6869 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6870 }
ad59a7a3 6871 print "<generator version='$version/$git_version'>gitweb</generator>\n";
af6feeb2 6872 }
717b8311 6873
af6feeb2 6874 # contents
b6093a5c
RF
6875 for (my $i = 0; $i <= $#commitlist; $i++) {
6876 my %co = %{$commitlist[$i]};
6877 my $commit = $co{'id'};
717b8311 6878 # we read 150, we always show 30 and the ones more recent than 48 hours
91fd2bf3 6879 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
717b8311
JN
6880 last;
6881 }
91fd2bf3 6882 my %cd = parse_date($co{'author_epoch'});
af6feeb2
JN
6883
6884 # get list of changed files
b6093a5c 6885 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
c906b181
JN
6886 $co{'parent'} || "--root",
6887 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6bcf4b46 6888 or next;
717b8311 6889 my @difftree = map { chomp; $_ } <$fd>;
6bcf4b46
JN
6890 close $fd
6891 or next;
af6feeb2
JN
6892
6893 # print element (entry, item)
e62a641d 6894 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
af6feeb2
JN
6895 if ($format eq 'rss') {
6896 print "<item>\n" .
6897 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6898 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6899 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6900 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6901 "<link>$co_url</link>\n" .
6902 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6903 "<content:encoded>" .
6904 "<![CDATA[\n";
6905 } elsif ($format eq 'atom') {
6906 print "<entry>\n" .
6907 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6908 "<updated>$cd{'iso-8601'}</updated>\n" .
ab23c19d
JN
6909 "<author>\n" .
6910 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6911 if ($co{'author_email'}) {
6912 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6913 }
6914 print "</author>\n" .
af6feeb2 6915 # use committer for contributor
ab23c19d
JN
6916 "<contributor>\n" .
6917 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6918 if ($co{'committer_email'}) {
6919 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6920 }
6921 print "</contributor>\n" .
af6feeb2
JN
6922 "<published>$cd{'iso-8601'}</published>\n" .
6923 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6924 "<id>$co_url</id>\n" .
6925 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6926 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6927 }
717b8311 6928 my $comment = $co{'comment'};
af6feeb2 6929 print "<pre>\n";
717b8311 6930 foreach my $line (@$comment) {
af6feeb2
JN
6931 $line = esc_html($line);
6932 print "$line\n";
717b8311 6933 }
af6feeb2
JN
6934 print "</pre><ul>\n";
6935 foreach my $difftree_line (@difftree) {
6936 my %difftree = parse_difftree_raw_line($difftree_line);
6937 next if !$difftree{'from_id'};
6938
6939 my $file = $difftree{'file'} || $difftree{'to_file'};
6940
6941 print "<li>" .
6942 "[" .
6943 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6944 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6945 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6946 file_name=>$file, file_parent=>$difftree{'from_file'}),
6947 -title => "diff"}, 'D');
6948 if ($have_blame) {
6949 print $cgi->a({-href => href(-full=>1, action=>"blame",
6950 file_name=>$file, hash_base=>$commit),
6951 -title => "blame"}, 'B');
717b8311 6952 }
af6feeb2
JN
6953 # if this is not a feed of a file history
6954 if (!defined $file_name || $file_name ne $file) {
6955 print $cgi->a({-href => href(-full=>1, action=>"history",
6956 file_name=>$file, hash=>$commit),
6957 -title => "history"}, 'H');
6958 }
6959 $file = esc_path($file);
6960 print "] ".
6961 "$file</li>\n";
6962 }
6963 if ($format eq 'rss') {
6964 print "</ul>]]>\n" .
6965 "</content:encoded>\n" .
6966 "</item>\n";
6967 } elsif ($format eq 'atom') {
6968 print "</ul>\n</div>\n" .
6969 "</content>\n" .
6970 "</entry>\n";
717b8311 6971 }
717b8311 6972 }
af6feeb2
JN
6973
6974 # end of feed
6975 if ($format eq 'rss') {
6976 print "</channel>\n</rss>\n";
3278fbc5 6977 } elsif ($format eq 'atom') {
af6feeb2
JN
6978 print "</feed>\n";
6979 }
6980}
6981
6982sub git_rss {
6983 git_feed('rss');
6984}
6985
6986sub git_atom {
6987 git_feed('atom');
717b8311
JN
6988}
6989
6990sub git_opml {
847e01fb 6991 my @list = git_get_projects_list();
717b8311 6992
ae35785e
GB
6993 print $cgi->header(
6994 -type => 'text/xml',
6995 -charset => 'utf-8',
6996 -content_disposition => 'inline; filename="opml.xml"');
6997
59b9f61a
JN
6998 print <<XML;
6999<?xml version="1.0" encoding="utf-8"?>
7000<opml version="1.0">
7001<head>
8be2890c 7002 <title>$site_name OPML Export</title>
59b9f61a
JN
7003</head>
7004<body>
7005<outline text="git RSS feeds">
7006XML
717b8311
JN
7007
7008 foreach my $pr (@list) {
7009 my %proj = %$pr;
847e01fb 7010 my $head = git_get_head_hash($proj{'path'});
717b8311
JN
7011 if (!defined $head) {
7012 next;
7013 }
25691fbe 7014 $git_dir = "$projectroot/$proj{'path'}";
847e01fb 7015 my %co = parse_commit($head);
717b8311
JN
7016 if (!%co) {
7017 next;
7018 }
7019
7020 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
df63fbbf
GB
7021 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7022 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
717b8311
JN
7023 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7024 }
59b9f61a
JN
7025 print <<XML;
7026</outline>
7027</body>
7028</opml>
7029XML
717b8311 7030}