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