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