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