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