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