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