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