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