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