]> git.ipfire.org Git - thirdparty/git.git/blame - gitweb/gitweb.perl
gitweb: prepare git_get_projects_list for use outside 'forks'.
[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 || '';
348a6589 2830 my $paranoid = shift;
717b8311
JN
2831 my @list;
2832
2833 if (-d $projects_list) {
2834 # search in directory
12b1443c 2835 my $dir = $projects_list;
6768d6b8
AK
2836 # remove the trailing "/"
2837 $dir =~ s!/+$!!;
ac593b76
MM
2838 my $pfxlen = length("$dir");
2839 my $pfxdepth = ($dir =~ tr!/!!);
12b1443c 2840 # when filtering, search only given subdirectory
348a6589 2841 if ($filter && !$paranoid) {
12b1443c
JN
2842 $dir .= "/$filter";
2843 $dir =~ s!/+$!!;
2844 }
c0011ff8
JN
2845
2846 File::Find::find({
2847 follow_fast => 1, # follow symbolic links
d20602ee 2848 follow_skip => 2, # ignore duplicates
c0011ff8
JN
2849 dangling_symlinks => 0, # ignore dangling symlinks, silently
2850 wanted => sub {
ee1d8ee0
JN
2851 # global variables
2852 our $project_maxdepth;
2853 our $projectroot;
c0011ff8
JN
2854 # skip project-list toplevel, if we get it.
2855 return if (m!^[/.]$!);
2856 # only directories can be git repositories
2857 return unless (-d $_);
ca5e9495 2858 # don't traverse too deep (Find is super slow on os x)
12b1443c 2859 # $project_maxdepth excludes depth of $projectroot
ca5e9495
LL
2860 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2861 $File::Find::prune = 1;
2862 return;
2863 }
c0011ff8 2864
12b1443c 2865 my $path = substr($File::Find::name, $pfxlen + 1);
348a6589
BL
2866 # paranoidly only filter here
2867 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
2868 next;
2869 }
c0011ff8 2870 # we check related file in $projectroot
fb3bb3d1
DD
2871 if (check_export_ok("$projectroot/$path")) {
2872 push @list, { path => $path };
c0011ff8
JN
2873 $File::Find::prune = 1;
2874 }
2875 },
2876 }, "$dir");
2877
717b8311
JN
2878 } elsif (-f $projects_list) {
2879 # read from file(url-encoded):
2880 # 'git%2Fgit.git Linus+Torvalds'
2881 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2882 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
dff2b6d4 2883 open my $fd, '<', $projects_list or return;
c2b8b134 2884 PROJECT:
717b8311
JN
2885 while (my $line = <$fd>) {
2886 chomp $line;
2887 my ($path, $owner) = split ' ', $line;
2888 $path = unescape($path);
2889 $owner = unescape($owner);
2890 if (!defined $path) {
2891 next;
2892 }
12b1443c
JN
2893 # if $filter is rpovided, check if $path begins with $filter
2894 if ($filter && $path !~ m!^\Q$filter\E/!) {
2895 next;
83ee94c1 2896 }
2172ce4b 2897 if (check_export_ok("$projectroot/$path")) {
717b8311
JN
2898 my $pr = {
2899 path => $path,
00f429af 2900 owner => to_utf8($owner),
717b8311 2901 };
c2b8b134 2902 push @list, $pr;
717b8311
JN
2903 }
2904 }
2905 close $fd;
2906 }
717b8311
JN
2907 return @list;
2908}
2909
12b1443c
JN
2910# written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
2911# as side effects it sets 'forks' field to list of forks for forked projects
2912sub filter_forks_from_projects_list {
2913 my $projects = shift;
2914
2915 my %trie; # prefix tree of directories (path components)
2916 # generate trie out of those directories that might contain forks
2917 foreach my $pr (@$projects) {
2918 my $path = $pr->{'path'};
2919 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
2920 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
2921 next unless ($path); # skip '.git' repository: tests, git-instaweb
53c632fa 2922 next unless (-d "$projectroot/$path"); # containing directory exists
12b1443c
JN
2923 $pr->{'forks'} = []; # there can be 0 or more forks of project
2924
2925 # add to trie
2926 my @dirs = split('/', $path);
2927 # walk the trie, until either runs out of components or out of trie
2928 my $ref = \%trie;
2929 while (scalar @dirs &&
2930 exists($ref->{$dirs[0]})) {
2931 $ref = $ref->{shift @dirs};
2932 }
2933 # create rest of trie structure from rest of components
2934 foreach my $dir (@dirs) {
2935 $ref = $ref->{$dir} = {};
2936 }
2937 # create end marker, store $pr as a data
2938 $ref->{''} = $pr if (!exists $ref->{''});
2939 }
2940
2941 # filter out forks, by finding shortest prefix match for paths
2942 my @filtered;
2943 PROJECT:
2944 foreach my $pr (@$projects) {
2945 # trie lookup
2946 my $ref = \%trie;
2947 DIR:
2948 foreach my $dir (split('/', $pr->{'path'})) {
2949 if (exists $ref->{''}) {
2950 # found [shortest] prefix, is a fork - skip it
2951 push @{$ref->{''}{'forks'}}, $pr;
2952 next PROJECT;
2953 }
2954 if (!exists $ref->{$dir}) {
2955 # not in trie, cannot have prefix, not a fork
2956 push @filtered, $pr;
2957 next PROJECT;
2958 }
2959 # If the dir is there, we just walk one step down the trie.
2960 $ref = $ref->{$dir};
2961 }
2962 # we ran out of trie
2963 # (shouldn't happen: it's either no match, or end marker)
2964 push @filtered, $pr;
2965 }
2966
2967 return @filtered;
2968}
2969
2970# note: fill_project_list_info must be run first,
2971# for 'descr_long' and 'ctags' to be filled
2972sub search_projects_list {
2973 my ($projlist, %opts) = @_;
2974 my $tagfilter = $opts{'tagfilter'};
2975 my $searchtext = $opts{'searchtext'};
2976
2977 return @$projlist
2978 unless ($tagfilter || $searchtext);
2979
2980 my @projects;
2981 PROJECT:
2982 foreach my $pr (@$projlist) {
2983
2984 if ($tagfilter) {
2985 next unless ref($pr->{'ctags'}) eq 'HASH';
2986 next unless
2987 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
2988 }
2989
2990 if ($searchtext) {
2991 next unless
2992 $pr->{'path'} =~ /$searchtext/ ||
2993 $pr->{'descr_long'} =~ /$searchtext/;
2994 }
2995
2996 push @projects, $pr;
2997 }
2998
2999 return @projects;
3000}
3001
47852450
JH
3002our $gitweb_project_owner = undef;
3003sub git_get_project_list_from_file {
1e0cf030 3004
47852450 3005 return if (defined $gitweb_project_owner);
1e0cf030 3006
47852450 3007 $gitweb_project_owner = {};
1e0cf030
JN
3008 # read from file (url-encoded):
3009 # 'git%2Fgit.git Linus+Torvalds'
3010 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3011 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3012 if (-f $projects_list) {
dff2b6d4 3013 open(my $fd, '<', $projects_list);
1e0cf030
JN
3014 while (my $line = <$fd>) {
3015 chomp $line;
3016 my ($pr, $ow) = split ' ', $line;
3017 $pr = unescape($pr);
3018 $ow = unescape($ow);
47852450 3019 $gitweb_project_owner->{$pr} = to_utf8($ow);
1e0cf030
JN
3020 }
3021 close $fd;
3022 }
47852450
JH
3023}
3024
3025sub git_get_project_owner {
3026 my $project = shift;
3027 my $owner;
3028
3029 return undef unless $project;
b59012ef 3030 $git_dir = "$projectroot/$project";
47852450
JH
3031
3032 if (!defined $gitweb_project_owner) {
3033 git_get_project_list_from_file();
3034 }
3035
3036 if (exists $gitweb_project_owner->{$project}) {
3037 $owner = $gitweb_project_owner->{$project};
3038 }
b59012ef
BR
3039 if (!defined $owner){
3040 $owner = git_get_project_config('owner');
3041 }
1e0cf030 3042 if (!defined $owner) {
b59012ef 3043 $owner = get_file_owner("$git_dir");
1e0cf030
JN
3044 }
3045
3046 return $owner;
3047}
3048
c60c56cc
JN
3049sub git_get_last_activity {
3050 my ($path) = @_;
3051 my $fd;
3052
3053 $git_dir = "$projectroot/$path";
3054 open($fd, "-|", git_cmd(), 'for-each-ref',
0ff5ec70 3055 '--format=%(committer)',
c60c56cc 3056 '--sort=-committerdate',
0ff5ec70 3057 '--count=1',
c60c56cc
JN
3058 'refs/heads') or return;
3059 my $most_recent = <$fd>;
3060 close $fd or return;
785cdea9
JN
3061 if (defined $most_recent &&
3062 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
c60c56cc
JN
3063 my $timestamp = $1;
3064 my $age = time - $timestamp;
3065 return ($age, age_string($age));
3066 }
c956395e 3067 return (undef, undef);
c60c56cc
JN
3068}
3069
9d0d42f3
GB
3070# Implementation note: when a single remote is wanted, we cannot use 'git
3071# remote show -n' because that command always work (assuming it's a remote URL
3072# if it's not defined), and we cannot use 'git remote show' because that would
3073# try to make a network roundtrip. So the only way to find if that particular
3074# remote is defined is to walk the list provided by 'git remote -v' and stop if
3075# and when we find what we want.
3076sub git_get_remotes_list {
3077 my $wanted = shift;
3078 my %remotes = ();
3079
3080 open my $fd, '-|' , git_cmd(), 'remote', '-v';
3081 return unless $fd;
3082 while (my $remote = <$fd>) {
3083 chomp $remote;
3084 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3085 next if $wanted and not $remote eq $wanted;
3086 my ($url, $key) = ($1, $2);
3087
3088 $remotes{$remote} ||= { 'heads' => () };
3089 $remotes{$remote}{$key} = $url;
3090 }
3091 close $fd or return;
3092 return wantarray ? %remotes : \%remotes;
3093}
3094
3095# Takes a hash of remotes as first parameter and fills it by adding the
3096# available remote heads for each of the indicated remotes.
3097sub fill_remote_heads {
3098 my $remotes = shift;
3099 my @heads = map { "remotes/$_" } keys %$remotes;
3100 my @remoteheads = git_get_heads_list(undef, @heads);
3101 foreach my $remote (keys %$remotes) {
3102 $remotes->{$remote}{'heads'} = [ grep {
3103 $_->{'name'} =~ s!^$remote/!!
3104 } @remoteheads ];
3105 }
3106}
3107
847e01fb 3108sub git_get_references {
717b8311
JN
3109 my $type = shift || "";
3110 my %refs;
28b9d9f7
JN
3111 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3112 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3113 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3114 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
9704d75d 3115 or return;
d294e1ca 3116
717b8311
JN
3117 while (my $line = <$fd>) {
3118 chomp $line;
4afbaeff 3119 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
717b8311 3120 if (defined $refs{$1}) {
d294e1ca 3121 push @{$refs{$1}}, $2;
717b8311 3122 } else {
d294e1ca 3123 $refs{$1} = [ $2 ];
717b8311
JN
3124 }
3125 }
3126 }
3127 close $fd or return;
3128 return \%refs;
3129}
3130
56a322f1
JN
3131sub git_get_rev_name_tags {
3132 my $hash = shift || return undef;
3133
25691fbe 3134 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
56a322f1
JN
3135 or return;
3136 my $name_rev = <$fd>;
3137 close $fd;
3138
3139 if ($name_rev =~ m|^$hash tags/(.*)$|) {
3140 return $1;
3141 } else {
3142 # catches also '$hash undefined' output
3143 return undef;
3144 }
3145}
3146
717b8311
JN
3147## ----------------------------------------------------------------------
3148## parse to hash functions
3149
847e01fb 3150sub parse_date {
717b8311
JN
3151 my $epoch = shift;
3152 my $tz = shift || "-0000";
3153
3154 my %date;
3155 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3156 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3157 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3158 $date{'hour'} = $hour;
3159 $date{'minute'} = $min;
3160 $date{'mday'} = $mday;
3161 $date{'day'} = $days[$wday];
3162 $date{'month'} = $months[$mon];
af6feeb2
JN
3163 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3164 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
952c65fc
JN
3165 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3166 $mday, $months[$mon], $hour ,$min;
af6feeb2 3167 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
a62d6d84 3168 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
717b8311 3169
2b1e1723
JN
3170 my ($tz_sign, $tz_hour, $tz_min) =
3171 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3172 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3173 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
717b8311
JN
3174 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3175 $date{'hour_local'} = $hour;
3176 $date{'minute_local'} = $min;
3177 $date{'tz_local'} = $tz;
af6feeb2
JN
3178 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3179 1900+$year, $mon+1, $mday,
3180 $hour, $min, $sec, $tz);
717b8311
JN
3181 return %date;
3182}
3183
847e01fb 3184sub parse_tag {
ede5e100
KS
3185 my $tag_id = shift;
3186 my %tag;
d8a20ba9 3187 my @comment;
ede5e100 3188
25691fbe 3189 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
d8a20ba9 3190 $tag{'id'} = $tag_id;
ede5e100
KS
3191 while (my $line = <$fd>) {
3192 chomp $line;
3193 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3194 $tag{'object'} = $1;
7ab0d2b6 3195 } elsif ($line =~ m/^type (.+)$/) {
ede5e100 3196 $tag{'type'} = $1;
7ab0d2b6 3197 } elsif ($line =~ m/^tag (.+)$/) {
ede5e100 3198 $tag{'name'} = $1;
d8a20ba9
KS
3199 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3200 $tag{'author'} = $1;
ba924733
GB
3201 $tag{'author_epoch'} = $2;
3202 $tag{'author_tz'} = $3;
3203 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3204 $tag{'author_name'} = $1;
3205 $tag{'author_email'} = $2;
3206 } else {
3207 $tag{'author_name'} = $tag{'author'};
3208 }
d8a20ba9
KS
3209 } elsif ($line =~ m/--BEGIN/) {
3210 push @comment, $line;
3211 last;
3212 } elsif ($line eq "") {
3213 last;
ede5e100
KS
3214 }
3215 }
d8a20ba9
KS
3216 push @comment, <$fd>;
3217 $tag{'comment'} = \@comment;
19806691 3218 close $fd or return;
ede5e100
KS
3219 if (!defined $tag{'name'}) {
3220 return
3221 };
3222 return %tag
3223}
3224
756bbf54 3225sub parse_commit_text {
ccdfdea0 3226 my ($commit_text, $withparents) = @_;
756bbf54 3227 my @commit_lines = split '\n', $commit_text;
703ac710 3228 my %co;
703ac710 3229
756bbf54
RF
3230 pop @commit_lines; # Remove '\0'
3231
198a2a8a
JN
3232 if (! @commit_lines) {
3233 return;
3234 }
3235
25f422fb 3236 my $header = shift @commit_lines;
198a2a8a 3237 if ($header !~ m/^[0-9a-fA-F]{40}/) {
25f422fb
KS
3238 return;
3239 }
ccdfdea0 3240 ($co{'id'}, my @parents) = split ' ', $header;
19806691 3241 while (my $line = shift @commit_lines) {
b87d78d6 3242 last if $line eq "\n";
7ab0d2b6 3243 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
703ac710 3244 $co{'tree'} = $1;
ccdfdea0 3245 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
208b2dff 3246 push @parents, $1;
022be3d0 3247 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
5ed5bbc7 3248 $co{'author'} = to_utf8($1);
185f09e5
KS
3249 $co{'author_epoch'} = $2;
3250 $co{'author_tz'} = $3;
ba00b8c1
JN
3251 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3252 $co{'author_name'} = $1;
3253 $co{'author_email'} = $2;
2bf7a52c
KS
3254 } else {
3255 $co{'author_name'} = $co{'author'};
3256 }
86eed32d 3257 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
5ed5bbc7 3258 $co{'committer'} = to_utf8($1);
185f09e5
KS
3259 $co{'committer_epoch'} = $2;
3260 $co{'committer_tz'} = $3;
ba00b8c1
JN
3261 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3262 $co{'committer_name'} = $1;
3263 $co{'committer_email'} = $2;
3264 } else {
3265 $co{'committer_name'} = $co{'committer'};
3266 }
703ac710
KS
3267 }
3268 }
ede5e100 3269 if (!defined $co{'tree'}) {
25f422fb 3270 return;
ede5e100 3271 };
208b2dff
RF
3272 $co{'parents'} = \@parents;
3273 $co{'parent'} = $parents[0];
25f422fb 3274
19806691 3275 foreach my $title (@commit_lines) {
c2488d06 3276 $title =~ s/^ //;
19806691 3277 if ($title ne "") {
48c771f4 3278 $co{'title'} = chop_str($title, 80, 5);
19806691
KS
3279 # remove leading stuff of merges to make the interesting part visible
3280 if (length($title) > 50) {
3281 $title =~ s/^Automatic //;
3282 $title =~ s/^merge (of|with) /Merge ... /i;
3283 if (length($title) > 50) {
3284 $title =~ s/(http|rsync):\/\///;
3285 }
3286 if (length($title) > 50) {
3287 $title =~ s/(master|www|rsync)\.//;
3288 }
3289 if (length($title) > 50) {
3290 $title =~ s/kernel.org:?//;
3291 }
3292 if (length($title) > 50) {
3293 $title =~ s/\/pub\/scm//;
3294 }
3295 }
48c771f4 3296 $co{'title_short'} = chop_str($title, 50, 5);
19806691
KS
3297 last;
3298 }
3299 }
53c39676 3300 if (! defined $co{'title'} || $co{'title'} eq "") {
7e0fe5c9
PB
3301 $co{'title'} = $co{'title_short'} = '(no commit message)';
3302 }
25f422fb
KS
3303 # remove added spaces
3304 foreach my $line (@commit_lines) {
3305 $line =~ s/^ //;
3306 }
3307 $co{'comment'} = \@commit_lines;
2ae100df
KS
3308
3309 my $age = time - $co{'committer_epoch'};
3310 $co{'age'} = $age;
d263a6bd 3311 $co{'age_string'} = age_string($age);
71be1e79
KS
3312 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3313 if ($age > 60*60*24*7*2) {
1b1cd421 3314 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79
KS
3315 $co{'age_string_age'} = $co{'age_string'};
3316 } else {
3317 $co{'age_string_date'} = $co{'age_string'};
1b1cd421 3318 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79 3319 }
703ac710
KS
3320 return %co;
3321}
3322
756bbf54
RF
3323sub parse_commit {
3324 my ($commit_id) = @_;
3325 my %co;
3326
3327 local $/ = "\0";
3328
3329 open my $fd, "-|", git_cmd(), "rev-list",
ccdfdea0 3330 "--parents",
756bbf54 3331 "--header",
756bbf54
RF
3332 "--max-count=1",
3333 $commit_id,
3334 "--",
074afaa0 3335 or die_error(500, "Open git-rev-list failed");
ccdfdea0 3336 %co = parse_commit_text(<$fd>, 1);
756bbf54
RF
3337 close $fd;
3338
3339 return %co;
3340}
3341
3342sub parse_commits {
311e552e 3343 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
756bbf54
RF
3344 my @cos;
3345
3346 $maxcount ||= 1;
3347 $skip ||= 0;
3348
756bbf54
RF
3349 local $/ = "\0";
3350
3351 open my $fd, "-|", git_cmd(), "rev-list",
3352 "--header",
311e552e 3353 @args,
756bbf54 3354 ("--max-count=" . $maxcount),
f47efbb7 3355 ("--skip=" . $skip),
868bc068 3356 @extra_options,
756bbf54
RF
3357 $commit_id,
3358 "--",
3359 ($filename ? ($filename) : ())
074afaa0 3360 or die_error(500, "Open git-rev-list failed");
756bbf54
RF
3361 while (my $line = <$fd>) {
3362 my %co = parse_commit_text($line);
3363 push @cos, \%co;
3364 }
3365 close $fd;
3366
3367 return wantarray ? @cos : \@cos;
3368}
3369
e8e41a93 3370# parse line of git-diff-tree "raw" output
740e67f9
JN
3371sub parse_difftree_raw_line {
3372 my $line = shift;
3373 my %res;
3374
3375 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
3376 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
3377 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3378 $res{'from_mode'} = $1;
3379 $res{'to_mode'} = $2;
3380 $res{'from_id'} = $3;
3381 $res{'to_id'} = $4;
4ed4a347 3382 $res{'status'} = $5;
740e67f9
JN
3383 $res{'similarity'} = $6;
3384 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
e8e41a93 3385 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
740e67f9 3386 } else {
9d301456 3387 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
740e67f9
JN
3388 }
3389 }
78bc403a
JN
3390 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3391 # combined diff (for merge commit)
3392 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3393 $res{'nparents'} = length($1);
3394 $res{'from_mode'} = [ split(' ', $2) ];
3395 $res{'to_mode'} = pop @{$res{'from_mode'}};
3396 $res{'from_id'} = [ split(' ', $3) ];
3397 $res{'to_id'} = pop @{$res{'from_id'}};
3398 $res{'status'} = [ split('', $4) ];
3399 $res{'to_file'} = unquote($5);
3400 }
740e67f9 3401 # 'c512b523472485aef4fff9e57b229d9d243c967f'
0edcb37d
JN
3402 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3403 $res{'commit'} = $1;
3404 }
740e67f9
JN
3405
3406 return wantarray ? %res : \%res;
3407}
3408
0cec6db5
JN
3409# wrapper: return parsed line of git-diff-tree "raw" output
3410# (the argument might be raw line, or parsed info)
3411sub parsed_difftree_line {
3412 my $line_or_ref = shift;
3413
3414 if (ref($line_or_ref) eq "HASH") {
3415 # pre-parsed (or generated by hand)
3416 return $line_or_ref;
3417 } else {
3418 return parse_difftree_raw_line($line_or_ref);
3419 }
3420}
3421
cb849b46 3422# parse line of git-ls-tree output
74fd8728 3423sub parse_ls_tree_line {
cb849b46
JN
3424 my $line = shift;
3425 my %opts = @_;
3426 my %res;
3427
e4b48eaa
JN
3428 if ($opts{'-l'}) {
3429 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3430 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
cb849b46 3431
e4b48eaa
JN
3432 $res{'mode'} = $1;
3433 $res{'type'} = $2;
3434 $res{'hash'} = $3;
3435 $res{'size'} = $4;
3436 if ($opts{'-z'}) {
3437 $res{'name'} = $5;
3438 } else {
3439 $res{'name'} = unquote($5);
3440 }
cb849b46 3441 } else {
e4b48eaa
JN
3442 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3443 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3444
3445 $res{'mode'} = $1;
3446 $res{'type'} = $2;
3447 $res{'hash'} = $3;
3448 if ($opts{'-z'}) {
3449 $res{'name'} = $4;
3450 } else {
3451 $res{'name'} = unquote($4);
3452 }
cb849b46
JN
3453 }
3454
3455 return wantarray ? %res : \%res;
3456}
3457
90921740
JN
3458# generates _two_ hashes, references to which are passed as 2 and 3 argument
3459sub parse_from_to_diffinfo {
3460 my ($diffinfo, $from, $to, @parents) = @_;
3461
3462 if ($diffinfo->{'nparents'}) {
3463 # combined diff
3464 $from->{'file'} = [];
3465 $from->{'href'} = [];
3466 fill_from_file_info($diffinfo, @parents)
3467 unless exists $diffinfo->{'from_file'};
3468 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
9d301456
JN
3469 $from->{'file'}[$i] =
3470 defined $diffinfo->{'from_file'}[$i] ?
3471 $diffinfo->{'from_file'}[$i] :
3472 $diffinfo->{'to_file'};
90921740
JN
3473 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3474 $from->{'href'}[$i] = href(action=>"blob",
3475 hash_base=>$parents[$i],
3476 hash=>$diffinfo->{'from_id'}[$i],
3477 file_name=>$from->{'file'}[$i]);
3478 } else {
3479 $from->{'href'}[$i] = undef;
3480 }
3481 }
3482 } else {
0cec6db5 3483 # ordinary (not combined) diff
9d301456 3484 $from->{'file'} = $diffinfo->{'from_file'};
90921740
JN
3485 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3486 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3487 hash=>$diffinfo->{'from_id'},
3488 file_name=>$from->{'file'});
3489 } else {
3490 delete $from->{'href'};
3491 }
3492 }
3493
9d301456 3494 $to->{'file'} = $diffinfo->{'to_file'};
90921740
JN
3495 if (!is_deleted($diffinfo)) { # file exists in result
3496 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3497 hash=>$diffinfo->{'to_id'},
3498 file_name=>$to->{'file'});
3499 } else {
3500 delete $to->{'href'};
3501 }
3502}
3503
717b8311
JN
3504## ......................................................................
3505## parse to array of hashes functions
4c02e3c5 3506
cd146408 3507sub git_get_heads_list {
9b3f3de1 3508 my ($limit, @classes) = @_;
00fa6fef 3509 @classes = ('heads') unless @classes;
9b3f3de1 3510 my @patterns = map { "refs/$_" } @classes;
cd146408
JN
3511 my @headslist;
3512
3513 open my $fd, '-|', git_cmd(), 'for-each-ref',
3514 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3515 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
9b3f3de1 3516 @patterns
c83a77e4
JN
3517 or return;
3518 while (my $line = <$fd>) {
cd146408 3519 my %ref_item;
120ddde2 3520
cd146408
JN
3521 chomp $line;
3522 my ($refinfo, $committerinfo) = split(/\0/, $line);
3523 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3524 my ($committer, $epoch, $tz) =
3525 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 3526 $ref_item{'fullname'} = $name;
60efa245 3527 $name =~ s!^refs/(?:head|remote)s/!!;
cd146408
JN
3528
3529 $ref_item{'name'} = $name;
3530 $ref_item{'id'} = $hash;
3531 $ref_item{'title'} = $title || '(no commit message)';
3532 $ref_item{'epoch'} = $epoch;
3533 if ($epoch) {
3534 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3535 } else {
3536 $ref_item{'age'} = "unknown";
717b8311 3537 }
cd146408
JN
3538
3539 push @headslist, \%ref_item;
c83a77e4
JN
3540 }
3541 close $fd;
3542
cd146408
JN
3543 return wantarray ? @headslist : \@headslist;
3544}
3545
3546sub git_get_tags_list {
3547 my $limit = shift;
3548 my @tagslist;
3549
3550 open my $fd, '-|', git_cmd(), 'for-each-ref',
3551 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3552 '--format=%(objectname) %(objecttype) %(refname) '.
3553 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3554 'refs/tags'
3555 or return;
3556 while (my $line = <$fd>) {
3557 my %ref_item;
7a13b999 3558
cd146408
JN
3559 chomp $line;
3560 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3561 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3562 my ($creator, $epoch, $tz) =
3563 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 3564 $ref_item{'fullname'} = $name;
cd146408
JN
3565 $name =~ s!^refs/tags/!!;
3566
3567 $ref_item{'type'} = $type;
3568 $ref_item{'id'} = $id;
3569 $ref_item{'name'} = $name;
3570 if ($type eq "tag") {
3571 $ref_item{'subject'} = $title;
3572 $ref_item{'reftype'} = $reftype;
3573 $ref_item{'refid'} = $refid;
3574 } else {
3575 $ref_item{'reftype'} = $type;
3576 $ref_item{'refid'} = $id;
3577 }
3578
3579 if ($type eq "tag" || $type eq "commit") {
3580 $ref_item{'epoch'} = $epoch;
3581 if ($epoch) {
3582 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3583 } else {
3584 $ref_item{'age'} = "unknown";
3585 }
3586 }
991910a9 3587
cd146408 3588 push @tagslist, \%ref_item;
717b8311 3589 }
cd146408
JN
3590 close $fd;
3591
3592 return wantarray ? @tagslist : \@tagslist;
86eed32d
KS
3593}
3594
717b8311
JN
3595## ----------------------------------------------------------------------
3596## filesystem-related functions
022be3d0 3597
c07ad4b9
KS
3598sub get_file_owner {
3599 my $path = shift;
3600
3601 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3602 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3603 if (!defined $gcos) {
3604 return undef;
3605 }
3606 my $owner = $gcos;
3607 $owner =~ s/[,;].*$//;
00f429af 3608 return to_utf8($owner);
c07ad4b9
KS
3609}
3610
2dcb5e1a
JN
3611# assume that file exists
3612sub insert_file {
3613 my $filename = shift;
3614
3615 open my $fd, '<', $filename;
4586864a 3616 print map { to_utf8($_) } <$fd>;
2dcb5e1a
JN
3617 close $fd;
3618}
3619
717b8311
JN
3620## ......................................................................
3621## mimetype related functions
09bd7898 3622
717b8311
JN
3623sub mimetype_guess_file {
3624 my $filename = shift;
3625 my $mimemap = shift;
3626 -r $mimemap or return undef;
3627
3628 my %mimemap;
dff2b6d4 3629 open(my $mh, '<', $mimemap) or return undef;
ad87e4f6 3630 while (<$mh>) {
618918e5 3631 next if m/^#/; # skip comments
93a6ad16
LN
3632 my ($mimetype, @exts) = split(/\s+/);
3633 foreach my $ext (@exts) {
3634 $mimemap{$ext} = $mimetype;
09bd7898 3635 }
09bd7898 3636 }
ad87e4f6 3637 close($mh);
09bd7898 3638
8059319a 3639 $filename =~ /\.([^.]*)$/;
717b8311
JN
3640 return $mimemap{$1};
3641}
5996ca08 3642
717b8311
JN
3643sub mimetype_guess {
3644 my $filename = shift;
3645 my $mime;
3646 $filename =~ /\./ or return undef;
5996ca08 3647
717b8311
JN
3648 if ($mimetypes_file) {
3649 my $file = $mimetypes_file;
d5aa50de
JN
3650 if ($file !~ m!^/!) { # if it is relative path
3651 # it is relative to project
3652 $file = "$projectroot/$project/$file";
3653 }
717b8311
JN
3654 $mime = mimetype_guess_file($filename, $file);
3655 }
3656 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3657 return $mime;
5996ca08
FF
3658}
3659
847e01fb 3660sub blob_mimetype {
717b8311
JN
3661 my $fd = shift;
3662 my $filename = shift;
5996ca08 3663
717b8311
JN
3664 if ($filename) {
3665 my $mime = mimetype_guess($filename);
3666 $mime and return $mime;
d8d17b5d 3667 }
717b8311
JN
3668
3669 # just in case
3670 return $default_blob_plain_mimetype unless $fd;
3671
3672 if (-T $fd) {
7f718e8b 3673 return 'text/plain';
717b8311
JN
3674 } elsif (! $filename) {
3675 return 'application/octet-stream';
3676 } elsif ($filename =~ m/\.png$/i) {
3677 return 'image/png';
3678 } elsif ($filename =~ m/\.gif$/i) {
3679 return 'image/gif';
3680 } elsif ($filename =~ m/\.jpe?g$/i) {
3681 return 'image/jpeg';
d8d17b5d 3682 } else {
717b8311 3683 return 'application/octet-stream';
f7ab660c 3684 }
717b8311
JN
3685}
3686
7f718e8b
JN
3687sub blob_contenttype {
3688 my ($fd, $file_name, $type) = @_;
3689
3690 $type ||= blob_mimetype($fd, $file_name);
3691 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3692 $type .= "; charset=$default_text_plain_charset";
3693 }
3694
3695 return $type;
3696}
3697
592ea417
JN
3698# guess file syntax for syntax highlighting; return undef if no highlighting
3699# the name of syntax can (in the future) depend on syntax highlighter used
3700sub guess_file_syntax {
3701 my ($highlight, $mimetype, $file_name) = @_;
3702 return undef unless ($highlight && defined $file_name);
592ea417
JN
3703 my $basename = basename($file_name, '.in');
3704 return $highlight_basename{$basename}
3705 if exists $highlight_basename{$basename};
3706
3707 $basename =~ /\.([^.]*)$/;
3708 my $ext = $1 or return undef;
3709 return $highlight_ext{$ext}
3710 if exists $highlight_ext{$ext};
3711
3712 return undef;
3713}
3714
3715# run highlighter and return FD of its output,
3716# or return original FD if no highlighting
3717sub run_highlighter {
3718 my ($fd, $highlight, $syntax) = @_;
3719 return $fd unless ($highlight && defined $syntax);
3720
3ca7353c 3721 close $fd;
592ea417 3722 open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
7ce896b3 3723 quote_command($highlight_bin).
6affdbe6 3724 " --replace-tabs=8 --fragment --syntax $syntax |"
592ea417
JN
3725 or die_error(500, "Couldn't open file or run syntax highlighter");
3726 return $fd;
3727}
3728
717b8311
JN
3729## ======================================================================
3730## functions printing HTML: header, footer, error page
3731
efb2d0c5
JN
3732sub get_page_title {
3733 my $title = to_utf8($site_name);
3734
3735 return $title unless (defined $project);
3736 $title .= " - " . to_utf8($project);
3737
3738 return $title unless (defined $action);
3739 $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3740
3741 return $title unless (defined $file_name);
3742 $title .= " - " . esc_path($file_name);
3743 if ($action eq "tree" && $file_name !~ m|/$|) {
3744 $title .= "/";
3745 }
3746
3747 return $title;
3748}
3749
6ee9033d
JN
3750sub get_content_type_html {
3751 # require explicit support from the UA if we are to send the page as
3752 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3753 # we have to do this because MSIE sometimes globs '*/*', pretending to
3754 # support xhtml+xml but choking when it gets what it asked for.
3755 if (defined $cgi->http('HTTP_ACCEPT') &&
3756 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3757 $cgi->Accept('application/xhtml+xml') != 0) {
3758 return 'application/xhtml+xml';
3759 } else {
3760 return 'text/html';
3761 }
3762}
3763
05bb5a25
JN
3764sub print_feed_meta {
3765 if (defined $project) {
3766 my %href_params = get_feed_info();
3767 if (!exists $href_params{'-title'}) {
3768 $href_params{'-title'} = 'log';
3769 }
3770
0f54b7d0 3771 foreach my $format (qw(RSS Atom)) {
05bb5a25
JN
3772 my $type = lc($format);
3773 my %link_attr = (
3774 '-rel' => 'alternate',
3775 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3776 '-type' => "application/$type+xml"
3777 );
3778
3779 $href_params{'action'} = $type;
3780 $link_attr{'-href'} = href(%href_params);
3781 print "<link ".
3782 "rel=\"$link_attr{'-rel'}\" ".
3783 "title=\"$link_attr{'-title'}\" ".
3784 "href=\"$link_attr{'-href'}\" ".
3785 "type=\"$link_attr{'-type'}\" ".
3786 "/>\n";
3787
3788 $href_params{'extra_options'} = '--no-merges';
3789 $link_attr{'-href'} = href(%href_params);
3790 $link_attr{'-title'} .= ' (no merges)';
3791 print "<link ".
3792 "rel=\"$link_attr{'-rel'}\" ".
3793 "title=\"$link_attr{'-title'}\" ".
3794 "href=\"$link_attr{'-href'}\" ".
3795 "type=\"$link_attr{'-type'}\" ".
3796 "/>\n";
3797 }
3798
3799 } else {
3800 printf('<link rel="alternate" title="%s projects list" '.
3801 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3802 esc_attr($site_name), href(project=>undef, action=>"project_index"));
3803 printf('<link rel="alternate" title="%s projects feeds" '.
3804 'href="%s" type="text/x-opml" />'."\n",
3805 esc_attr($site_name), href(project=>undef, action=>"opml"));
3806 }
3807}
3808
6ee9033d
JN
3809sub print_header_links {
3810 my $status = shift;
3811
3812 # print out each stylesheet that exist, providing backwards capability
3813 # for those people who defined $stylesheet in a config file
3814 if (defined $stylesheet) {
3815 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3816 } else {
3817 foreach my $stylesheet (@stylesheets) {
3818 next unless $stylesheet;
3819 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3820 }
3821 }
3822 print_feed_meta()
3823 if ($status eq '200 OK');
3824 if (defined $favicon) {
3825 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3826 }
3827}
3828
3829sub print_nav_breadcrumbs {
3830 my %opts = @_;
3831
3832 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3833 if (defined $project) {
3834 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3835 if (defined $action) {
3836 my $action_print = $action ;
3837 if (defined $opts{-action_extra}) {
3838 $action_print = $cgi->a({-href => href(action=>$action)},
3839 $action);
3840 }
3841 print " / $action_print";
3842 }
3843 if (defined $opts{-action_extra}) {
3844 print " / $opts{-action_extra}";
3845 }
3846 print "\n";
3847 }
3848}
3849
3850sub print_search_form {
3851 if (!defined $searchtext) {
3852 $searchtext = "";
3853 }
3854 my $search_hash;
3855 if (defined $hash_base) {
3856 $search_hash = $hash_base;
3857 } elsif (defined $hash) {
3858 $search_hash = $hash;
3859 } else {
3860 $search_hash = "HEAD";
3861 }
3862 my $action = $my_uri;
3863 my $use_pathinfo = gitweb_check_feature('pathinfo');
3864 if ($use_pathinfo) {
3865 $action .= "/".esc_url($project);
3866 }
3867 print $cgi->startform(-method => "get", -action => $action) .
3868 "<div class=\"search\">\n" .
3869 (!$use_pathinfo &&
3870 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3871 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3872 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3873 $cgi->popup_menu(-name => 'st', -default => 'commit',
3874 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3875 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3876 " search:\n",
3877 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3878 "<span title=\"Extended regular expression\">" .
3879 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3880 -checked => $search_use_regexp) .
3881 "</span>" .
3882 "</div>" .
3883 $cgi->end_form() . "\n";
3884}
3885
717b8311
JN
3886sub git_header_html {
3887 my $status = shift || "200 OK";
3888 my $expires = shift;
7a597457 3889 my %opts = @_;
717b8311 3890
efb2d0c5 3891 my $title = get_page_title();
6ee9033d 3892 my $content_type = get_content_type_html();
952c65fc 3893 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
7a597457 3894 -status=> $status, -expires => $expires)
ad709ea9 3895 unless ($opts{'-no_http_header'});
45c9a758 3896 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
717b8311
JN
3897 print <<EOF;
3898<?xml version="1.0" encoding="utf-8"?>
3899<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3900<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
d4baf9ea 3901<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
717b8311
JN
3902<!-- git core binaries version $git_version -->
3903<head>
3904<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
45c9a758 3905<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
717b8311
JN
3906<meta name="robots" content="index, nofollow"/>
3907<title>$title</title>
717b8311 3908EOF
41a4d16e
GB
3909 # the stylesheet, favicon etc urls won't work correctly with path_info
3910 # unless we set the appropriate base URL
c3254aee 3911 if ($ENV{'PATH_INFO'}) {
81d3fe9f 3912 print "<base href=\"".esc_url($base_url)."\" />\n";
c3254aee 3913 }
6ee9033d 3914 print_header_links($status);
c1355b7f
LH
3915
3916 if (defined $site_html_head_string) {
3917 print to_utf8($site_html_head_string);
3918 }
3919
dd04c428 3920 print "</head>\n" .
b2d3476e
AC
3921 "<body>\n";
3922
24d4afcd 3923 if (defined $site_header && -f $site_header) {
2dcb5e1a 3924 insert_file($site_header);
b2d3476e
AC
3925 }
3926
68220524
JN
3927 print "<div class=\"page_header\">\n";
3928 if (defined $logo) {
3929 print $cgi->a({-href => esc_url($logo_url),
3930 -title => $logo_label},
3931 $cgi->img({-src => esc_url($logo),
3932 -width => 72, -height => 27,
3933 -alt => "git",
3934 -class => "logo"}));
3935 }
6ee9033d 3936 print_nav_breadcrumbs(%opts);
d77b5673
PB
3937 print "</div>\n";
3938
25b2790f 3939 my $have_search = gitweb_check_feature('search');
f70dda25 3940 if (defined $project && $have_search) {
6ee9033d 3941 print_search_form();
b87d78d6 3942 }
717b8311
JN
3943}
3944
3945sub git_footer_html {
3562198b
JN
3946 my $feed_class = 'rss_logo';
3947
717b8311
JN
3948 print "<div class=\"page_footer\">\n";
3949 if (defined $project) {
847e01fb 3950 my $descr = git_get_project_description($project);
717b8311
JN
3951 if (defined $descr) {
3952 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3953 }
3562198b
JN
3954
3955 my %href_params = get_feed_info();
3956 if (!%href_params) {
3957 $feed_class .= ' generic';
3958 }
3959 $href_params{'-title'} ||= 'log';
3960
0f54b7d0 3961 foreach my $format (qw(RSS Atom)) {
3562198b
JN
3962 $href_params{'action'} = lc($format);
3963 print $cgi->a({-href => href(%href_params),
3964 -title => "$href_params{'-title'} $format feed",
3965 -class => $feed_class}, $format)."\n";
3966 }
3967
717b8311 3968 } else {
a1565c44 3969 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3562198b 3970 -class => $feed_class}, "OPML") . " ";
9d0734ae 3971 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3562198b 3972 -class => $feed_class}, "TXT") . "\n";
717b8311 3973 }
3562198b 3974 print "</div>\n"; # class="page_footer"
b2d3476e 3975
aa7dd05e
JN
3976 if (defined $t0 && gitweb_check_feature('timed')) {
3977 print "<div id=\"generating_info\">\n";
3978 print 'This page took '.
3979 '<span id="generating_time" class="time_span">'.
3962f1d7 3980 tv_interval($t0, [ gettimeofday() ]).
aa7dd05e
JN
3981 ' seconds </span>'.
3982 ' and '.
3983 '<span id="generating_cmd">'.
3984 $number_of_git_cmds.
3985 '</span> git commands '.
3986 " to generate.\n";
3987 print "</div>\n"; # class="page_footer"
3988 }
3989
24d4afcd 3990 if (defined $site_footer && -f $site_footer) {
2dcb5e1a 3991 insert_file($site_footer);
b2d3476e
AC
3992 }
3993
abf411e2 3994 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
b62a1a98
JWH
3995 if (defined $action &&
3996 $action eq 'blame_incremental') {
c4ccf61f
JN
3997 print qq!<script type="text/javascript">\n!.
3998 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3999 qq! "!. href() .qq!");\n!.
4000 qq!</script>\n!;
291e52bd 4001 } else {
2e987f92
JN
4002 my ($jstimezone, $tz_cookie, $datetime_class) =
4003 gitweb_get_feature('javascript-timezone');
4004
c4ccf61f 4005 print qq!<script type="text/javascript">\n!.
2e987f92
JN
4006 qq!window.onload = function () {\n!;
4007 if (gitweb_check_feature('javascript-actions')) {
4008 print qq! fixLinks();\n!;
4009 }
4010 if ($jstimezone && $tz_cookie && $datetime_class) {
4011 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
4012 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
4013 }
4014 print qq!};\n!.
c4ccf61f
JN
4015 qq!</script>\n!;
4016 }
4017
b2d3476e 4018 print "</body>\n" .
717b8311
JN
4019 "</html>";
4020}
4021
453541fc 4022# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
074afaa0
LW
4023# Example: die_error(404, 'Hash not found')
4024# By convention, use the following status codes (as defined in RFC 2616):
4025# 400: Invalid or missing CGI parameters, or
4026# requested object exists but has wrong type.
4027# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4028# this server or project.
4029# 404: Requested object/revision/project doesn't exist.
4030# 500: The server isn't configured properly, or
4031# an internal error occurred (e.g. failed assertions caused by bugs), or
4032# an unknown error occurred (e.g. the git binary died unexpectedly).
b62a1a98
JWH
4033# 503: The server is currently unavailable (because it is overloaded,
4034# or down for maintenance). Generally, this is a temporary state.
717b8311 4035sub die_error {
074afaa0 4036 my $status = shift || 500;
1df48766 4037 my $error = esc_html(shift) || "Internal Server Error";
aa14013a 4038 my $extra = shift;
7a597457 4039 my %opts = @_;
074afaa0 4040
b62a1a98
JWH
4041 my %http_responses = (
4042 400 => '400 Bad Request',
4043 403 => '403 Forbidden',
4044 404 => '404 Not Found',
4045 500 => '500 Internal Server Error',
4046 503 => '503 Service Unavailable',
4047 );
7a597457 4048 git_header_html($http_responses{$status}, undef, %opts);
59b9f61a
JN
4049 print <<EOF;
4050<div class="page_body">
4051<br /><br />
4052$status - $error
4053<br />
59b9f61a 4054EOF
aa14013a
JWH
4055 if (defined $extra) {
4056 print "<hr />\n" .
4057 "$extra\n";
4058 }
4059 print "</div>\n";
4060
b87d78d6 4061 git_footer_html();
7a597457
JN
4062 goto DONE_GITWEB
4063 unless ($opts{'-error_handler'});
161332a5
KS
4064}
4065
717b8311
JN
4066## ----------------------------------------------------------------------
4067## functions printing or outputting HTML: navigation
4068
847e01fb 4069sub git_print_page_nav {
717b8311
JN
4070 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4071 $extra = '' if !defined $extra; # pager or formats
4072
4073 my @navs = qw(summary shortlog log commit commitdiff tree);
4074 if ($suppress) {
4075 @navs = grep { $_ ne $suppress } @navs;
4076 }
4077
1c2a4f5a 4078 my %arg = map { $_ => {action=>$_} } @navs;
717b8311
JN
4079 if (defined $head) {
4080 for (qw(commit commitdiff)) {
3be8e720 4081 $arg{$_}{'hash'} = $head;
717b8311
JN
4082 }
4083 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4084 for (qw(shortlog log)) {
3be8e720 4085 $arg{$_}{'hash'} = $head;
045e531a 4086 }
6a928415
KS
4087 }
4088 }
d627f68f 4089
3be8e720
JN
4090 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
4091 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
717b8311 4092
a7c5a283 4093 my @actions = gitweb_get_feature('actions');
2b11e059
JN
4094 my %repl = (
4095 '%' => '%',
4096 'n' => $project, # project name
4097 'f' => $git_dir, # project path within filesystem
4098 'h' => $treehead || '', # current hash ('h' parameter)
4099 'b' => $treebase || '', # hash base ('hb' parameter)
4100 );
d627f68f 4101 while (@actions) {
2b11e059
JN
4102 my ($label, $link, $pos) = splice(@actions,0,3);
4103 # insert
d627f68f
PB
4104 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
4105 # munch munch
2b11e059 4106 $link =~ s/%([%nfhb])/$repl{$1}/g;
d627f68f
PB
4107 $arg{$label}{'_href'} = $link;
4108 }
4109
717b8311
JN
4110 print "<div class=\"page_nav\">\n" .
4111 (join " | ",
1c2a4f5a 4112 map { $_ eq $current ?
d627f68f 4113 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
1c2a4f5a 4114 } @navs);
717b8311
JN
4115 print "<br/>\n$extra<br/>\n" .
4116 "</div>\n";
6a928415
KS
4117}
4118
11e7bece
GB
4119# returns a submenu for the nagivation of the refs views (tags, heads,
4120# remotes) with the current view disabled and the remotes view only
4121# available if the feature is enabled
4122sub format_ref_views {
4123 my ($current) = @_;
4124 my @ref_views = qw{tags heads};
4125 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
4126 return join " | ", map {
4127 $_ eq $current ? $_ :
4128 $cgi->a({-href => href(action=>$_)}, $_)
4129 } @ref_views
4130}
4131
847e01fb 4132sub format_paging_nav {
69ca37d2 4133 my ($action, $page, $has_next_link) = @_;
717b8311 4134 my $paging_nav;
594e212b 4135
717b8311 4136
717b8311 4137 if ($page > 0) {
69ca37d2
JN
4138 $paging_nav .=
4139 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4140 " &sdot; " .
7afd77bf 4141 $cgi->a({-href => href(-replay=>1, page=>$page-1),
26298b5f 4142 -accesskey => "p", -title => "Alt-p"}, "prev");
717b8311 4143 } else {
69ca37d2 4144 $paging_nav .= "first &sdot; prev";
717b8311
JN
4145 }
4146
1f684dc0 4147 if ($has_next_link) {
717b8311 4148 $paging_nav .= " &sdot; " .
7afd77bf 4149 $cgi->a({-href => href(-replay=>1, page=>$page+1),
26298b5f 4150 -accesskey => "n", -title => "Alt-n"}, "next");
717b8311
JN
4151 } else {
4152 $paging_nav .= " &sdot; next";
594e212b 4153 }
717b8311
JN
4154
4155 return $paging_nav;
594e212b
JN
4156}
4157
717b8311
JN
4158## ......................................................................
4159## functions printing or outputting HTML: div
4160
847e01fb 4161sub git_print_header_div {
717b8311 4162 my ($action, $title, $hash, $hash_base) = @_;
1c2a4f5a 4163 my %args = ();
717b8311 4164
3be8e720
JN
4165 $args{'action'} = $action;
4166 $args{'hash'} = $hash if $hash;
4167 $args{'hash_base'} = $hash_base if $hash_base;
717b8311
JN
4168
4169 print "<div class=\"header\">\n" .
1c2a4f5a
MW
4170 $cgi->a({-href => href(%args), -class => "title"},
4171 $title ? $title : $action) .
4172 "\n</div>\n";
717b8311 4173}
ede5e100 4174
0e656999
GB
4175sub format_repo_url {
4176 my ($name, $url) = @_;
4177 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4178}
4179
b891d52a
GB
4180# Group output by placing it in a DIV element and adding a header.
4181# Options for start_div() can be provided by passing a hash reference as the
4182# first parameter to the function.
4183# Options to git_print_header_div() can be provided by passing an array
4184# reference. This must follow the options to start_div if they are present.
4185# The content can be a scalar, which is output as-is, a scalar reference, which
4186# is output after html escaping, an IO handle passed either as *handle or
4187# *handle{IO}, or a function reference. In the latter case all following
4188# parameters will be taken as argument to the content function call.
4189sub git_print_section {
4190 my ($div_args, $header_args, $content);
4191 my $arg = shift;
4192 if (ref($arg) eq 'HASH') {
4193 $div_args = $arg;
4194 $arg = shift;
4195 }
4196 if (ref($arg) eq 'ARRAY') {
4197 $header_args = $arg;
4198 $arg = shift;
4199 }
4200 $content = $arg;
4201
4202 print $cgi->start_div($div_args);
4203 git_print_header_div(@$header_args);
4204
4205 if (ref($content) eq 'CODE') {
4206 $content->(@_);
4207 } elsif (ref($content) eq 'SCALAR') {
4208 print esc_html($$content);
4209 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4210 print <$content>;
4211 } elsif (!ref($content) && defined($content)) {
4212 print $content;
4213 }
4214
4215 print $cgi->end_div;
4216}
4217
256b7b48 4218sub format_timestamp_html {
ce71b076 4219 my $date = shift;
2e987f92 4220 my $strtime = $date->{'rfc2822'};
0cf207f7 4221
2e987f92
JN
4222 my (undef, undef, $datetime_class) =
4223 gitweb_get_feature('javascript-timezone');
4224 if ($datetime_class) {
4225 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
4226 }
0cf207f7 4227
256b7b48
JN
4228 my $localtime_format = '(%02d:%02d %s)';
4229 if ($date->{'hour_local'} < 6) {
4230 $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
1c49a4e1 4231 }
256b7b48
JN
4232 $strtime .= ' ' .
4233 sprintf($localtime_format,
4234 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
0cf207f7 4235
256b7b48 4236 return $strtime;
1c49a4e1
GB
4237}
4238
4239# Outputs the author name and date in long form
6fd92a28
JN
4240sub git_print_authorship {
4241 my $co = shift;
1c49a4e1
GB
4242 my %opts = @_;
4243 my $tag = $opts{-tag} || 'div';
e133d65c 4244 my $author = $co->{'author_name'};
6fd92a28 4245
a44465cc 4246 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
1c49a4e1 4247 print "<$tag class=\"author_date\">" .
e133d65c 4248 format_search_author($author, "author", esc_html($author)) .
ce71b076 4249 " [".format_timestamp_html(\%ad)."]".
256b7b48
JN
4250 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
4251 "</$tag>\n";
1c49a4e1
GB
4252}
4253
4254# Outputs table rows containing the full author or committer information,
22e5e58a 4255# in the format expected for 'commit' view (& similar).
1c49a4e1 4256# Parameters are a commit hash reference, followed by the list of people
22e5e58a 4257# to output information for. If the list is empty it defaults to both
1c49a4e1
GB
4258# author and committer.
4259sub git_print_authorship_rows {
4260 my $co = shift;
4261 # too bad we can't use @people = @_ || ('author', 'committer')
4262 my @people = @_;
4263 @people = ('author', 'committer') unless @people;
4264 foreach my $who (@people) {
4265 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
e133d65c
SB
4266 print "<tr><td>$who</td><td>" .
4267 format_search_author($co->{"${who}_name"}, $who,
256b7b48 4268 esc_html($co->{"${who}_name"})) . " " .
e133d65c 4269 format_search_author($co->{"${who}_email"}, $who,
256b7b48 4270 esc_html("<" . $co->{"${who}_email"} . ">")) .
e133d65c 4271 "</td><td rowspan=\"2\">" .
e9fdd74e
GB
4272 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4273 "</td></tr>\n" .
1c49a4e1 4274 "<tr>" .
256b7b48 4275 "<td></td><td>" .
ce71b076 4276 format_timestamp_html(\%wd) .
256b7b48 4277 "</td>" .
1c49a4e1 4278 "</tr>\n";
a44465cc 4279 }
6fd92a28
JN
4280}
4281
717b8311
JN
4282sub git_print_page_path {
4283 my $name = shift;
4284 my $type = shift;
59fb1c94 4285 my $hb = shift;
ede5e100 4286
4df118ed
JN
4287
4288 print "<div class=\"page_path\">";
4289 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
00f429af 4290 -title => 'tree root'}, to_utf8("[$project]"));
4df118ed
JN
4291 print " / ";
4292 if (defined $name) {
762c7205
JN
4293 my @dirname = split '/', $name;
4294 my $basename = pop @dirname;
4295 my $fullname = '';
4296
762c7205 4297 foreach my $dir (@dirname) {
16fdb488 4298 $fullname .= ($fullname ? '/' : '') . $dir;
762c7205
JN
4299 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4300 hash_base=>$hb),
edc04e90 4301 -title => $fullname}, esc_path($dir));
26d0a976 4302 print " / ";
762c7205
JN
4303 }
4304 if (defined $type && $type eq 'blob') {
952c65fc 4305 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
762c7205 4306 hash_base=>$hb),
edc04e90 4307 -title => $name}, esc_path($basename));
762c7205
JN
4308 } elsif (defined $type && $type eq 'tree') {
4309 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4310 hash_base=>$hb),
edc04e90 4311 -title => $name}, esc_path($basename));
4df118ed 4312 print " / ";
59fb1c94 4313 } else {
403d0906 4314 print esc_path($basename);
59fb1c94 4315 }
ede5e100 4316 }
4df118ed 4317 print "<br/></div>\n";
ede5e100
KS
4318}
4319
74fd8728 4320sub git_print_log {
d16d093c 4321 my $log = shift;
b7f9253d 4322 my %opts = @_;
d16d093c 4323
b7f9253d
JN
4324 if ($opts{'-remove_title'}) {
4325 # remove title, i.e. first line of log
4326 shift @$log;
4327 }
d16d093c
JN
4328 # remove leading empty lines
4329 while (defined $log->[0] && $log->[0] eq "") {
4330 shift @$log;
4331 }
4332
4333 # print log
4334 my $signoff = 0;
4335 my $empty = 0;
4336 foreach my $line (@$log) {
b7f9253d
JN
4337 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
4338 $signoff = 1;
fba20b42 4339 $empty = 0;
b7f9253d
JN
4340 if (! $opts{'-remove_signoff'}) {
4341 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4342 next;
4343 } else {
4344 # remove signoff lines
4345 next;
4346 }
4347 } else {
4348 $signoff = 0;
4349 }
4350
d16d093c
JN
4351 # print only one empty line
4352 # do not print empty line after signoff
4353 if ($line eq "") {
4354 next if ($empty || $signoff);
4355 $empty = 1;
4356 } else {
4357 $empty = 0;
4358 }
b7f9253d
JN
4359
4360 print format_log_line_html($line) . "<br/>\n";
4361 }
4362
4363 if ($opts{'-final_empty_line'}) {
4364 # end with single empty line
4365 print "<br/>\n" unless $empty;
d16d093c
JN
4366 }
4367}
4368
e33fba4c
JN
4369# return link target (what link points to)
4370sub git_get_link_target {
4371 my $hash = shift;
4372 my $link_target;
4373
4374 # read link
4375 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4376 or return;
4377 {
34122b57 4378 local $/ = undef;
e33fba4c
JN
4379 $link_target = <$fd>;
4380 }
4381 close $fd
4382 or return;
4383
4384 return $link_target;
4385}
4386
3bf9d570
JN
4387# given link target, and the directory (basedir) the link is in,
4388# return target of link relative to top directory (top tree);
4389# return undef if it is not possible (including absolute links).
4390sub normalize_link_target {
15c54fe7 4391 my ($link_target, $basedir) = @_;
3bf9d570
JN
4392
4393 # absolute symlinks (beginning with '/') cannot be normalized
4394 return if (substr($link_target, 0, 1) eq '/');
4395
4396 # normalize link target to path from top (root) tree (dir)
4397 my $path;
4398 if ($basedir) {
4399 $path = $basedir . '/' . $link_target;
4400 } else {
4401 # we are in top (root) tree (dir)
4402 $path = $link_target;
4403 }
4404
4405 # remove //, /./, and /../
4406 my @path_parts;
4407 foreach my $part (split('/', $path)) {
4408 # discard '.' and ''
4409 next if (!$part || $part eq '.');
4410 # handle '..'
4411 if ($part eq '..') {
4412 if (@path_parts) {
4413 pop @path_parts;
4414 } else {
4415 # link leads outside repository (outside top dir)
4416 return;
4417 }
4418 } else {
4419 push @path_parts, $part;
4420 }
4421 }
4422 $path = join('/', @path_parts);
4423
4424 return $path;
4425}
e33fba4c 4426
fa702003
JN
4427# print tree entry (row of git_tree), but without encompassing <tr> element
4428sub git_print_tree_entry {
4429 my ($t, $basedir, $hash_base, $have_blame) = @_;
4430
4431 my %base_key = ();
e33fba4c 4432 $base_key{'hash_base'} = $hash_base if defined $hash_base;
fa702003 4433
4de741b3
LT
4434 # The format of a table row is: mode list link. Where mode is
4435 # the mode of the entry, list is the name of the entry, an href,
4436 # and link is the action links of the entry.
4437
fa702003 4438 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
e4b48eaa
JN
4439 if (exists $t->{'size'}) {
4440 print "<td class=\"size\">$t->{'size'}</td>\n";
4441 }
fa702003
JN
4442 if ($t->{'type'} eq "blob") {
4443 print "<td class=\"list\">" .
4de741b3 4444 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e7fb022a 4445 file_name=>"$basedir$t->{'name'}", %base_key),
e33fba4c
JN
4446 -class => "list"}, esc_path($t->{'name'}));
4447 if (S_ISLNK(oct $t->{'mode'})) {
4448 my $link_target = git_get_link_target($t->{'hash'});
4449 if ($link_target) {
15c54fe7 4450 my $norm_target = normalize_link_target($link_target, $basedir);
3bf9d570
JN
4451 if (defined $norm_target) {
4452 print " -> " .
4453 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4454 file_name=>$norm_target),
4455 -title => $norm_target}, esc_path($link_target));
4456 } else {
4457 print " -> " . esc_path($link_target);
4458 }
e33fba4c
JN
4459 }
4460 }
4461 print "</td>\n";
4de741b3 4462 print "<td class=\"link\">";
4777b014 4463 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e33fba4c
JN
4464 file_name=>"$basedir$t->{'name'}", %base_key)},
4465 "blob");
fa702003 4466 if ($have_blame) {
4777b014
PB
4467 print " | " .
4468 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
e33fba4c
JN
4469 file_name=>"$basedir$t->{'name'}", %base_key)},
4470 "blame");
fa702003
JN
4471 }
4472 if (defined $hash_base) {
4777b014
PB
4473 print " | " .
4474 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003
JN
4475 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4476 "history");
4477 }
4478 print " | " .
6f7ea5fb 4479 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
e7fb022a
JN
4480 file_name=>"$basedir$t->{'name'}")},
4481 "raw");
4de741b3 4482 print "</td>\n";
fa702003
JN
4483
4484 } elsif ($t->{'type'} eq "tree") {
0fa105e7
LT
4485 print "<td class=\"list\">";
4486 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
e4b48eaa
JN
4487 file_name=>"$basedir$t->{'name'}",
4488 %base_key)},
403d0906 4489 esc_path($t->{'name'}));
0fa105e7
LT
4490 print "</td>\n";
4491 print "<td class=\"link\">";
4777b014 4492 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
e4b48eaa
JN
4493 file_name=>"$basedir$t->{'name'}",
4494 %base_key)},
e33fba4c 4495 "tree");
fa702003 4496 if (defined $hash_base) {
4777b014
PB
4497 print " | " .
4498 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003 4499 file_name=>"$basedir$t->{'name'}")},
01ac1e38
JN
4500 "history");
4501 }
4502 print "</td>\n";
4503 } else {
4504 # unknown object: we can only present history for it
4505 # (this includes 'commit' object, i.e. submodule support)
4506 print "<td class=\"list\">" .
4507 esc_path($t->{'name'}) .
4508 "</td>\n";
4509 print "<td class=\"link\">";
4510 if (defined $hash_base) {
4511 print $cgi->a({-href => href(action=>"history",
4512 hash_base=>$hash_base,
4513 file_name=>"$basedir$t->{'name'}")},
fa702003
JN
4514 "history");
4515 }
4516 print "</td>\n";
4517 }
4518}
4519
717b8311
JN
4520## ......................................................................
4521## functions printing large fragments of HTML
4522
0cec6db5 4523# get pre-image filenames for merge (combined) diff
e72c0eaf
JN
4524sub fill_from_file_info {
4525 my ($diff, @parents) = @_;
4526
4527 $diff->{'from_file'} = [ ];
4528 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4529 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4530 if ($diff->{'status'}[$i] eq 'R' ||
4531 $diff->{'status'}[$i] eq 'C') {
4532 $diff->{'from_file'}[$i] =
4533 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4534 }
4535 }
4536
4537 return $diff;
4538}
4539
0cec6db5 4540# is current raw difftree line of file deletion
90921740
JN
4541sub is_deleted {
4542 my $diffinfo = shift;
4543
4ed4a347 4544 return $diffinfo->{'to_id'} eq ('0' x 40);
90921740 4545}
e72c0eaf 4546
0cec6db5
JN
4547# does patch correspond to [previous] difftree raw line
4548# $diffinfo - hashref of parsed raw diff format
4549# $patchinfo - hashref of parsed patch diff format
4550# (the same keys as in $diffinfo)
4551sub is_patch_split {
4552 my ($diffinfo, $patchinfo) = @_;
4553
4554 return defined $diffinfo && defined $patchinfo
9d301456 4555 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
0cec6db5
JN
4556}
4557
4558
4a4a1a53 4559sub git_difftree_body {
ed224dea
JN
4560 my ($difftree, $hash, @parents) = @_;
4561 my ($parent) = $parents[0];
25b2790f 4562 my $have_blame = gitweb_check_feature('blame');
4a4a1a53
JN
4563 print "<div class=\"list_head\">\n";
4564 if ($#{$difftree} > 10) {
4565 print(($#{$difftree} + 1) . " files changed:\n");
4566 }
4567 print "</div>\n";
4568
ed224dea
JN
4569 print "<table class=\"" .
4570 (@parents > 1 ? "combined " : "") .
4571 "diff_tree\">\n";
47598d7a
JN
4572
4573 # header only for combined diff in 'commitdiff' view
3ef408ae 4574 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
47598d7a
JN
4575 if ($has_header) {
4576 # table header
4577 print "<thead><tr>\n" .
4578 "<th></th><th></th>\n"; # filename, patchN link
4579 for (my $i = 0; $i < @parents; $i++) {
4580 my $par = $parents[$i];
4581 print "<th>" .
4582 $cgi->a({-href => href(action=>"commitdiff",
4583 hash=>$hash, hash_parent=>$par),
4584 -title => 'commitdiff to parent number ' .
4585 ($i+1) . ': ' . substr($par,0,7)},
4586 $i+1) .
4587 "&nbsp;</th>\n";
4588 }
4589 print "</tr></thead>\n<tbody>\n";
4590 }
4591
6dd36acd 4592 my $alternate = 1;
b4657e77 4593 my $patchno = 0;
4a4a1a53 4594 foreach my $line (@{$difftree}) {
0cec6db5 4595 my $diff = parsed_difftree_line($line);
4a4a1a53
JN
4596
4597 if ($alternate) {
4598 print "<tr class=\"dark\">\n";
4599 } else {
4600 print "<tr class=\"light\">\n";
4601 }
4602 $alternate ^= 1;
4603
493e01db 4604 if (exists $diff->{'nparents'}) { # combined diff
ed224dea 4605
493e01db
JN
4606 fill_from_file_info($diff, @parents)
4607 unless exists $diff->{'from_file'};
e72c0eaf 4608
90921740 4609 if (!is_deleted($diff)) {
ed224dea
JN
4610 # file exists in the result (child) commit
4611 print "<td>" .
493e01db
JN
4612 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4613 file_name=>$diff->{'to_file'},
ed224dea 4614 hash_base=>$hash),
493e01db 4615 -class => "list"}, esc_path($diff->{'to_file'})) .
ed224dea
JN
4616 "</td>\n";
4617 } else {
4618 print "<td>" .
493e01db 4619 esc_path($diff->{'to_file'}) .
ed224dea
JN
4620 "</td>\n";
4621 }
4622
4623 if ($action eq 'commitdiff') {
4624 # link to patch
4625 $patchno++;
4626 print "<td class=\"link\">" .
5e96a847
KC
4627 $cgi->a({-href => href(-anchor=>"patch$patchno")},
4628 "patch") .
ed224dea
JN
4629 " | " .
4630 "</td>\n";
4631 }
4632
4633 my $has_history = 0;
4634 my $not_deleted = 0;
493e01db 4635 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
ed224dea 4636 my $hash_parent = $parents[$i];
493e01db
JN
4637 my $from_hash = $diff->{'from_id'}[$i];
4638 my $from_path = $diff->{'from_file'}[$i];
4639 my $status = $diff->{'status'}[$i];
ed224dea
JN
4640
4641 $has_history ||= ($status ne 'A');
4642 $not_deleted ||= ($status ne 'D');
4643
ed224dea
JN
4644 if ($status eq 'A') {
4645 print "<td class=\"link\" align=\"right\"> | </td>\n";
4646 } elsif ($status eq 'D') {
4647 print "<td class=\"link\">" .
4648 $cgi->a({-href => href(action=>"blob",
4649 hash_base=>$hash,
4650 hash=>$from_hash,
4651 file_name=>$from_path)},
4652 "blob" . ($i+1)) .
4653 " | </td>\n";
4654 } else {
493e01db 4655 if ($diff->{'to_id'} eq $from_hash) {
ed224dea
JN
4656 print "<td class=\"link nochange\">";
4657 } else {
4658 print "<td class=\"link\">";
4659 }
4660 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 4661 hash=>$diff->{'to_id'},
ed224dea
JN
4662 hash_parent=>$from_hash,
4663 hash_base=>$hash,
4664 hash_parent_base=>$hash_parent,
493e01db 4665 file_name=>$diff->{'to_file'},
ed224dea
JN
4666 file_parent=>$from_path)},
4667 "diff" . ($i+1)) .
4668 " | </td>\n";
4669 }
4670 }
4671
4672 print "<td class=\"link\">";
4673 if ($not_deleted) {
4674 print $cgi->a({-href => href(action=>"blob",
493e01db
JN
4675 hash=>$diff->{'to_id'},
4676 file_name=>$diff->{'to_file'},
ed224dea
JN
4677 hash_base=>$hash)},
4678 "blob");
4679 print " | " if ($has_history);
4680 }
4681 if ($has_history) {
4682 print $cgi->a({-href => href(action=>"history",
493e01db 4683 file_name=>$diff->{'to_file'},
ed224dea
JN
4684 hash_base=>$hash)},
4685 "history");
4686 }
4687 print "</td>\n";
4688
4689 print "</tr>\n";
4690 next; # instead of 'else' clause, to avoid extra indent
4691 }
4692 # else ordinary diff
4693
e8e41a93
JN
4694 my ($to_mode_oct, $to_mode_str, $to_file_type);
4695 my ($from_mode_oct, $from_mode_str, $from_file_type);
493e01db
JN
4696 if ($diff->{'to_mode'} ne ('0' x 6)) {
4697 $to_mode_oct = oct $diff->{'to_mode'};
e8e41a93
JN
4698 if (S_ISREG($to_mode_oct)) { # only for regular file
4699 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4700 }
493e01db 4701 $to_file_type = file_type($diff->{'to_mode'});
e8e41a93 4702 }
493e01db
JN
4703 if ($diff->{'from_mode'} ne ('0' x 6)) {
4704 $from_mode_oct = oct $diff->{'from_mode'};
98885c29 4705 if (S_ISREG($from_mode_oct)) { # only for regular file
e8e41a93 4706 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4a4a1a53 4707 }
493e01db 4708 $from_file_type = file_type($diff->{'from_mode'});
e8e41a93
JN
4709 }
4710
493e01db 4711 if ($diff->{'status'} eq "A") { # created
e8e41a93
JN
4712 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4713 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
4714 $mode_chng .= "]</span>";
499faeda 4715 print "<td>";
493e01db
JN
4716 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4717 hash_base=>$hash, file_name=>$diff->{'file'}),
4718 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
4719 print "</td>\n";
4720 print "<td>$mode_chng</td>\n";
4721 print "<td class=\"link\">";
72dbafa1 4722 if ($action eq 'commitdiff') {
b4657e77
JN
4723 # link to patch
4724 $patchno++;
5e96a847
KC
4725 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4726 "patch") .
4727 " | ";
b4657e77 4728 }
493e01db
JN
4729 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4730 hash_base=>$hash, file_name=>$diff->{'file'})},
3faa541f 4731 "blob");
b4657e77 4732 print "</td>\n";
4a4a1a53 4733
493e01db 4734 } elsif ($diff->{'status'} eq "D") { # deleted
e8e41a93 4735 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
499faeda 4736 print "<td>";
493e01db
JN
4737 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4738 hash_base=>$parent, file_name=>$diff->{'file'}),
4739 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
4740 print "</td>\n";
4741 print "<td>$mode_chng</td>\n";
4742 print "<td class=\"link\">";
72dbafa1 4743 if ($action eq 'commitdiff') {
b4657e77
JN
4744 # link to patch
4745 $patchno++;
5e96a847
KC
4746 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4747 "patch") .
4748 " | ";
b4657e77 4749 }
493e01db
JN
4750 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4751 hash_base=>$parent, file_name=>$diff->{'file'})},
897d1d2e 4752 "blob") . " | ";
2b2a8c78 4753 if ($have_blame) {
897d1d2e 4754 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
493e01db 4755 file_name=>$diff->{'file'})},
897d1d2e 4756 "blame") . " | ";
2b2a8c78 4757 }
b4657e77 4758 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
493e01db 4759 file_name=>$diff->{'file'})},
e7fb022a 4760 "history");
499faeda 4761 print "</td>\n";
4a4a1a53 4762
493e01db 4763 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4a4a1a53 4764 my $mode_chnge = "";
493e01db 4765 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93 4766 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6e72cf43 4767 if ($from_file_type ne $to_file_type) {
e8e41a93 4768 $mode_chnge .= " from $from_file_type to $to_file_type";
4a4a1a53 4769 }
e8e41a93
JN
4770 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4771 if ($from_mode_str && $to_mode_str) {
4772 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4773 } elsif ($to_mode_str) {
4774 $mode_chnge .= " mode: $to_mode_str";
4a4a1a53
JN
4775 }
4776 }
4777 $mode_chnge .= "]</span>\n";
4778 }
4779 print "<td>";
493e01db
JN
4780 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4781 hash_base=>$hash, file_name=>$diff->{'file'}),
4782 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
4783 print "</td>\n";
4784 print "<td>$mode_chnge</td>\n";
4785 print "<td class=\"link\">";
241cc599
JN
4786 if ($action eq 'commitdiff') {
4787 # link to patch
4788 $patchno++;
5e96a847
KC
4789 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4790 "patch") .
241cc599 4791 " | ";
493e01db 4792 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
4793 # "commit" view and modified file (not onlu mode changed)
4794 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 4795 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 4796 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 4797 file_name=>$diff->{'file'})},
241cc599
JN
4798 "diff") .
4799 " | ";
4a4a1a53 4800 }
493e01db
JN
4801 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4802 hash_base=>$hash, file_name=>$diff->{'file'})},
897d1d2e 4803 "blob") . " | ";
2b2a8c78 4804 if ($have_blame) {
897d1d2e 4805 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 4806 file_name=>$diff->{'file'})},
897d1d2e 4807 "blame") . " | ";
2b2a8c78 4808 }
eb51ec9c 4809 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 4810 file_name=>$diff->{'file'})},
e7fb022a 4811 "history");
4a4a1a53
JN
4812 print "</td>\n";
4813
493e01db 4814 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
e8e41a93 4815 my %status_name = ('R' => 'moved', 'C' => 'copied');
493e01db 4816 my $nstatus = $status_name{$diff->{'status'}};
4a4a1a53 4817 my $mode_chng = "";
493e01db 4818 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93
JN
4819 # mode also for directories, so we cannot use $to_mode_str
4820 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4a4a1a53
JN
4821 }
4822 print "<td>" .
e8e41a93 4823 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
493e01db
JN
4824 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4825 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
e8e41a93
JN
4826 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4827 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
493e01db
JN
4828 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4829 -class => "list"}, esc_path($diff->{'from_file'})) .
4830 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
499faeda 4831 "<td class=\"link\">";
241cc599
JN
4832 if ($action eq 'commitdiff') {
4833 # link to patch
4834 $patchno++;
5e96a847
KC
4835 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
4836 "patch") .
241cc599 4837 " | ";
493e01db 4838 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
4839 # "commit" view and modified file (not only pure rename or copy)
4840 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 4841 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 4842 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 4843 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
241cc599
JN
4844 "diff") .
4845 " | ";
4a4a1a53 4846 }
493e01db
JN
4847 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4848 hash_base=>$parent, file_name=>$diff->{'to_file'})},
897d1d2e 4849 "blob") . " | ";
2b2a8c78 4850 if ($have_blame) {
897d1d2e 4851 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 4852 file_name=>$diff->{'to_file'})},
897d1d2e 4853 "blame") . " | ";
2b2a8c78 4854 }
897d1d2e 4855 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 4856 file_name=>$diff->{'to_file'})},
e7fb022a 4857 "history");
4a4a1a53 4858 print "</td>\n";
e8e41a93 4859
4a4a1a53
JN
4860 } # we should not encounter Unmerged (U) or Unknown (X) status
4861 print "</tr>\n";
4862 }
47598d7a 4863 print "</tbody>" if $has_header;
4a4a1a53
JN
4864 print "</table>\n";
4865}
4866
6ba1eb51
KK
4867sub print_sidebyside_diff_chunk {
4868 my @chunk = @_;
4869 my (@ctx, @rem, @add);
4870
4871 return unless @chunk;
4872
4873 # incomplete last line might be among removed or added lines,
4874 # or both, or among context lines: find which
4875 for (my $i = 1; $i < @chunk; $i++) {
4876 if ($chunk[$i][0] eq 'incomplete') {
4877 $chunk[$i][0] = $chunk[$i-1][0];
4878 }
4879 }
4880
4881 # guardian
4882 push @chunk, ["", ""];
4883
4884 foreach my $line_info (@chunk) {
4885 my ($class, $line) = @$line_info;
4886
4887 # print chunk headers
4888 if ($class && $class eq 'chunk_header') {
4889 print $line;
4890 next;
4891 }
4892
4893 ## print from accumulator when type of class of lines change
4894 # empty contents block on start rem/add block, or end of chunk
4895 if (@ctx && (!$class || $class eq 'rem' || $class eq 'add')) {
4896 print join '',
970fac5e 4897 '<div class="chunk_block ctx">',
6ba1eb51
KK
4898 '<div class="old">',
4899 @ctx,
4900 '</div>',
4901 '<div class="new">',
4902 @ctx,
4903 '</div>',
4904 '</div>';
4905 @ctx = ();
4906 }
4907 # empty add/rem block on start context block, or end of chunk
4908 if ((@rem || @add) && (!$class || $class eq 'ctx')) {
970fac5e
JN
4909 if (!@add) {
4910 # pure removal
4911 print join '',
4912 '<div class="chunk_block rem">',
4913 '<div class="old">',
4914 @rem,
4915 '</div>',
4916 '</div>';
4917 } elsif (!@rem) {
4918 # pure addition
4919 print join '',
4920 '<div class="chunk_block add">',
4921 '<div class="new">',
4922 @add,
4923 '</div>',
4924 '</div>';
4925 } else {
4926 # assume that it is change
4927 print join '',
4928 '<div class="chunk_block chg">',
4929 '<div class="old">',
4930 @rem,
4931 '</div>',
4932 '<div class="new">',
4933 @add,
4934 '</div>',
4935 '</div>';
4936 }
6ba1eb51
KK
4937 @rem = @add = ();
4938 }
4939
4940 ## adding lines to accumulator
4941 # guardian value
4942 last unless $line;
4943 # rem, add or change
4944 if ($class eq 'rem') {
4945 push @rem, $line;
4946 } elsif ($class eq 'add') {
4947 push @add, $line;
4948 }
4949 # context line
4950 if ($class eq 'ctx') {
4951 push @ctx, $line;
4952 }
4953 }
4954}
4955
eee08903 4956sub git_patchset_body {
6ba1eb51 4957 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
e72c0eaf 4958 my ($hash_parent) = $hash_parents[0];
eee08903 4959
0cec6db5 4960 my $is_combined = (@hash_parents > 1);
eee08903 4961 my $patch_idx = 0;
4280cde9 4962 my $patch_number = 0;
6d55f055 4963 my $patch_line;
fe87585e 4964 my $diffinfo;
0cec6db5 4965 my $to_name;
744d0ac3 4966 my (%from, %to);
6ba1eb51 4967 my @chunk; # for side-by-side diff
eee08903
JN
4968
4969 print "<div class=\"patchset\">\n";
4970
6d55f055
JN
4971 # skip to first patch
4972 while ($patch_line = <$fd>) {
157e43b4 4973 chomp $patch_line;
eee08903 4974
6d55f055
JN
4975 last if ($patch_line =~ m/^diff /);
4976 }
4977
4978 PATCH:
4979 while ($patch_line) {
6d55f055 4980
0cec6db5
JN
4981 # parse "git diff" header line
4982 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4983 # $1 is from_name, which we do not use
4984 $to_name = unquote($2);
4985 $to_name =~ s!^b/!!;
4986 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4987 # $1 is 'cc' or 'combined', which we do not use
4988 $to_name = unquote($2);
4989 } else {
4990 $to_name = undef;
6d55f055 4991 }
6d55f055
JN
4992
4993 # check if current patch belong to current raw line
4994 # and parse raw git-diff line if needed
0cec6db5 4995 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
2206537c 4996 # this is continuation of a split patch
6d55f055
JN
4997 print "<div class=\"patch cont\">\n";
4998 } else {
4999 # advance raw git-diff output if needed
5000 $patch_idx++ if defined $diffinfo;
eee08903 5001
0cec6db5
JN
5002 # read and prepare patch information
5003 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 5004
0cec6db5
JN
5005 # compact combined diff output can have some patches skipped
5006 # find which patch (using pathname of result) we are at now;
5007 if ($is_combined) {
5008 while ($to_name ne $diffinfo->{'to_file'}) {
cd030c3a
JN
5009 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5010 format_diff_cc_simplified($diffinfo, @hash_parents) .
5011 "</div>\n"; # class="patch"
5012
5013 $patch_idx++;
5014 $patch_number++;
0cec6db5
JN
5015
5016 last if $patch_idx > $#$difftree;
5017 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 5018 }
0cec6db5 5019 }
711fa742 5020
90921740
JN
5021 # modifies %from, %to hashes
5022 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5f855052 5023
6d55f055
JN
5024 # this is first patch for raw difftree line with $patch_idx index
5025 # we index @$difftree array from 0, but number patches from 1
5026 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
744d0ac3 5027 }
eee08903 5028
0cec6db5
JN
5029 # git diff header
5030 #assert($patch_line =~ m/^diff /) if DEBUG;
5031 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5032 $patch_number++;
6d55f055 5033 # print "git diff" header
90921740
JN
5034 print format_git_diff_header_line($patch_line, $diffinfo,
5035 \%from, \%to);
6d55f055
JN
5036
5037 # print extended diff header
0cec6db5 5038 print "<div class=\"diff extended_header\">\n";
6d55f055 5039 EXTENDED_HEADER:
0cec6db5
JN
5040 while ($patch_line = <$fd>) {
5041 chomp $patch_line;
5042
5043 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
5044
90921740
JN
5045 print format_extended_diff_header_line($patch_line, $diffinfo,
5046 \%from, \%to);
6d55f055 5047 }
0cec6db5 5048 print "</div>\n"; # class="diff extended_header"
6d55f055
JN
5049
5050 # from-file/to-file diff header
0bdb28c9
JN
5051 if (! $patch_line) {
5052 print "</div>\n"; # class="patch"
5053 last PATCH;
5054 }
66399eff 5055 next PATCH if ($patch_line =~ m/^diff /);
6d55f055 5056 #assert($patch_line =~ m/^---/) if DEBUG;
744d0ac3 5057
0cec6db5 5058 my $last_patch_line = $patch_line;
6d55f055 5059 $patch_line = <$fd>;
6d55f055 5060 chomp $patch_line;
90921740 5061 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
e4e4f825 5062
90921740 5063 print format_diff_from_to_header($last_patch_line, $patch_line,
91af4ce4
JN
5064 $diffinfo, \%from, \%to,
5065 @hash_parents);
e4e4f825 5066
6d55f055
JN
5067 # the patch itself
5068 LINE:
5069 while ($patch_line = <$fd>) {
5070 chomp $patch_line;
e4e4f825 5071
6d55f055 5072 next PATCH if ($patch_line =~ m/^diff /);
e4e4f825 5073
6ba1eb51
KK
5074 my ($class, $line) = process_diff_line($patch_line, \%from, \%to);
5075 my $diff_classes = "diff";
5076 $diff_classes .= " $class" if ($class);
5077 $line = "<div class=\"$diff_classes\">$line</div>\n";
5078
5079 if ($diff_style eq 'sidebyside' && !$is_combined) {
5080 if ($class eq 'chunk_header') {
5081 print_sidebyside_diff_chunk(@chunk);
5082 @chunk = ( [ $class, $line ] );
5083 } else {
5084 push @chunk, [ $class, $line ];
5085 }
5086 } else {
5087 # default 'inline' style and unknown styles
5088 print $line;
5089 }
eee08903 5090 }
eee08903 5091
6d55f055 5092 } continue {
6ba1eb51
KK
5093 if (@chunk) {
5094 print_sidebyside_diff_chunk(@chunk);
5095 @chunk = ();
5096 }
6d55f055 5097 print "</div>\n"; # class="patch"
eee08903 5098 }
d26c4264 5099
22e5e58a
RW
5100 # for compact combined (--cc) format, with chunk and patch simplification
5101 # the patchset might be empty, but there might be unprocessed raw lines
0cec6db5 5102 for (++$patch_idx if $patch_number > 0;
cd030c3a 5103 $patch_idx < @$difftree;
0cec6db5 5104 ++$patch_idx) {
cd030c3a 5105 # read and prepare patch information
0cec6db5 5106 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a
JN
5107
5108 # generate anchor for "patch" links in difftree / whatchanged part
5109 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5110 format_diff_cc_simplified($diffinfo, @hash_parents) .
5111 "</div>\n"; # class="patch"
5112
5113 $patch_number++;
5114 }
5115
d26c4264
JN
5116 if ($patch_number == 0) {
5117 if (@hash_parents > 1) {
5118 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
5119 } else {
5120 print "<div class=\"diff nodifferences\">No differences found</div>\n";
5121 }
5122 }
eee08903
JN
5123
5124 print "</div>\n"; # class="patchset"
5125}
5126
5127# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5128
d940c901
SC
5129# fills project list info (age, description, owner, category, forks)
5130# for each project in the list, removing invalid projects from
5131# returned list
69913415
JN
5132# NOTE: modifies $projlist, but does not remove entries from it
5133sub fill_project_list_info {
12b1443c 5134 my $projlist = shift;
e30496df 5135 my @projects;
69913415 5136
25b2790f 5137 my $show_ctags = gitweb_check_feature('ctags');
69913415 5138 PROJECT:
e30496df 5139 foreach my $pr (@$projlist) {
69913415
JN
5140 my (@activity) = git_get_last_activity($pr->{'path'});
5141 unless (@activity) {
5142 next PROJECT;
e30496df 5143 }
69913415 5144 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
e30496df
PB
5145 if (!defined $pr->{'descr'}) {
5146 my $descr = git_get_project_description($pr->{'path'}) || "";
69913415
JN
5147 $descr = to_utf8($descr);
5148 $pr->{'descr_long'} = $descr;
55feb120 5149 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
e30496df
PB
5150 }
5151 if (!defined $pr->{'owner'}) {
76e4f5d0 5152 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
e30496df 5153 }
12b1443c
JN
5154 if ($show_ctags) {
5155 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
e30496df 5156 }
d940c901
SC
5157 if ($projects_list_group_categories && !defined $pr->{'category'}) {
5158 my $cat = git_get_project_category($pr->{'path'}) ||
5159 $project_list_default_category;
5160 $pr->{'category'} = to_utf8($cat);
5161 }
5162
e30496df
PB
5163 push @projects, $pr;
5164 }
5165
69913415
JN
5166 return @projects;
5167}
5168
12b1443c
JN
5169sub sort_projects_list {
5170 my ($projlist, $order) = @_;
5171 my @projects;
5172
5173 my %order_info = (
5174 project => { key => 'path', type => 'str' },
5175 descr => { key => 'descr_long', type => 'str' },
5176 owner => { key => 'owner', type => 'str' },
5177 age => { key => 'age', type => 'num' }
5178 );
5179 my $oi = $order_info{$order};
5180 return @$projlist unless defined $oi;
5181 if ($oi->{'type'} eq 'str') {
5182 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @$projlist;
5183 } else {
5184 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @$projlist;
5185 }
5186
5187 return @projects;
5188}
5189
d940c901
SC
5190# returns a hash of categories, containing the list of project
5191# belonging to each category
5192sub build_projlist_by_category {
5193 my ($projlist, $from, $to) = @_;
5194 my %categories;
5195
5196 $from = 0 unless defined $from;
5197 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5198
5199 for (my $i = $from; $i <= $to; $i++) {
5200 my $pr = $projlist->[$i];
5201 push @{$categories{ $pr->{'category'} }}, $pr;
5202 }
5203
5204 return wantarray ? %categories : \%categories;
5205}
5206
6b28da67
PB
5207# print 'sort by' <th> element, generating 'sort by $name' replay link
5208# if that order is not selected
7da0f3a4 5209sub print_sort_th {
1ee4b4ef
JWH
5210 print format_sort_th(@_);
5211}
5212
5213sub format_sort_th {
6b28da67 5214 my ($name, $order, $header) = @_;
1ee4b4ef 5215 my $sort_th = "";
7da0f3a4
JN
5216 $header ||= ucfirst($name);
5217
5218 if ($order eq $name) {
1ee4b4ef 5219 $sort_th .= "<th>$header</th>\n";
7da0f3a4 5220 } else {
1ee4b4ef
JWH
5221 $sort_th .= "<th>" .
5222 $cgi->a({-href => href(-replay=>1, order=>$name),
5223 -class => "header"}, $header) .
5224 "</th>\n";
7da0f3a4 5225 }
1ee4b4ef
JWH
5226
5227 return $sort_th;
7da0f3a4
JN
5228}
5229
0fa920c3
SC
5230sub git_project_list_rows {
5231 my ($projlist, $from, $to, $check_forks) = @_;
5232
5233 $from = 0 unless defined $from;
5234 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5235
5236 my $alternate = 1;
5237 for (my $i = $from; $i <= $to; $i++) {
5238 my $pr = $projlist->[$i];
5239
5240 if ($alternate) {
5241 print "<tr class=\"dark\">\n";
5242 } else {
5243 print "<tr class=\"light\">\n";
5244 }
5245 $alternate ^= 1;
5246
5247 if ($check_forks) {
5248 print "<td>";
5249 if ($pr->{'forks'}) {
5250 my $nforks = scalar @{$pr->{'forks'}};
5251 if ($nforks > 0) {
5252 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5253 -title => "$nforks forks"}, "+");
5254 } else {
5255 print $cgi->span({-title => "$nforks forks"}, "+");
5256 }
5257 }
5258 print "</td>\n";
5259 }
5260 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5261 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
5262 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5263 -class => "list", -title => $pr->{'descr_long'}},
5264 esc_html($pr->{'descr'})) . "</td>\n" .
5265 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5266 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5267 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
5268 "<td class=\"link\">" .
5269 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
5270 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5271 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5272 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5273 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5274 "</td>\n" .
5275 "</tr>\n";
5276 }
5277}
5278
69913415 5279sub git_project_list_body {
42326110 5280 # actually uses global variable $project
69913415 5281 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
12b1443c 5282 my @projects = @$projlist;
69913415 5283
25b2790f 5284 my $check_forks = gitweb_check_feature('forks');
12b1443c
JN
5285 my $show_ctags = gitweb_check_feature('ctags');
5286 my $tagfilter = $show_ctags ? $cgi->param('by_tag') : undef;
5287 $check_forks = undef
5288 if ($tagfilter || $searchtext);
5289
5290 # filtering out forks before filling info allows to do less work
5291 @projects = filter_forks_from_projects_list(\@projects)
5292 if ($check_forks);
5293 @projects = fill_project_list_info(\@projects);
5294 # searching projects require filling to be run before it
5295 @projects = search_projects_list(\@projects,
5296 'searchtext' => $searchtext,
5297 'tagfilter' => $tagfilter)
5298 if ($tagfilter || $searchtext);
69913415 5299
b06dcf8c 5300 $order ||= $default_projects_order;
e30496df
PB
5301 $from = 0 unless defined $from;
5302 $to = $#projects if (!defined $to || $#projects < $to);
5303
12b1443c
JN
5304 # short circuit
5305 if ($from > $to) {
5306 print "<center>\n".
5307 "<b>No such projects found</b><br />\n".
5308 "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5309 "</center>\n<br />\n";
5310 return;
6b28da67
PB
5311 }
5312
12b1443c
JN
5313 @projects = sort_projects_list(\@projects, $order);
5314
aed93de4 5315 if ($show_ctags) {
0368c492
JN
5316 my $ctags = git_gather_all_ctags(\@projects);
5317 my $cloud = git_populate_project_tagcloud($ctags);
aed93de4
PB
5318 print git_show_project_tagcloud($cloud, 64);
5319 }
5320
e30496df
PB
5321 print "<table class=\"project_list\">\n";
5322 unless ($no_header) {
5323 print "<tr>\n";
5324 if ($check_forks) {
5325 print "<th></th>\n";
5326 }
6b28da67
PB
5327 print_sort_th('project', $order, 'Project');
5328 print_sort_th('descr', $order, 'Description');
5329 print_sort_th('owner', $order, 'Owner');
5330 print_sort_th('age', $order, 'Last Change');
7da0f3a4 5331 print "<th></th>\n" . # for links
e30496df
PB
5332 "</tr>\n";
5333 }
42326110 5334
d940c901
SC
5335 if ($projects_list_group_categories) {
5336 # only display categories with projects in the $from-$to window
5337 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
5338 my %categories = build_projlist_by_category(\@projects, $from, $to);
5339 foreach my $cat (sort keys %categories) {
5340 unless ($cat eq "") {
5341 print "<tr>\n";
5342 if ($check_forks) {
5343 print "<td></td>\n";
12b1443c 5344 }
d940c901
SC
5345 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
5346 print "</tr>\n";
e30496df 5347 }
d940c901
SC
5348
5349 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
e30496df 5350 }
d940c901
SC
5351 } else {
5352 git_project_list_rows(\@projects, $from, $to, $check_forks);
e30496df 5353 }
12b1443c 5354
e30496df
PB
5355 if (defined $extra) {
5356 print "<tr>\n";
5357 if ($check_forks) {
5358 print "<td></td>\n";
5359 }
5360 print "<td colspan=\"5\">$extra</td>\n" .
5361 "</tr>\n";
5362 }
5363 print "</table>\n";
5364}
5365
42671caa
JN
5366sub git_log_body {
5367 # uses global variable $project
5368 my ($commitlist, $from, $to, $refs, $extra) = @_;
5369
5370 $from = 0 unless defined $from;
5371 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5372
5373 for (my $i = 0; $i <= $to; $i++) {
5374 my %co = %{$commitlist->[$i]};
5375 next if !%co;
5376 my $commit = $co{'id'};
5377 my $ref = format_ref_marker($refs, $commit);
42671caa
JN
5378 git_print_header_div('commit',
5379 "<span class=\"age\">$co{'age_string'}</span>" .
5380 esc_html($co{'title'}) . $ref,
5381 $commit);
5382 print "<div class=\"title_text\">\n" .
5383 "<div class=\"log_link\">\n" .
5384 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5385 " | " .
5386 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5387 " | " .
5388 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5389 "<br/>\n" .
5390 "</div>\n";
5391 git_print_authorship(\%co, -tag => 'span');
5392 print "<br/>\n</div>\n";
5393
5394 print "<div class=\"log_body\">\n";
5395 git_print_log($co{'comment'}, -final_empty_line=> 1);
5396 print "</div>\n";
5397 }
5398 if ($extra) {
5399 print "<div class=\"page_nav\">\n";
5400 print "$extra\n";
5401 print "</div>\n";
5402 }
5403}
5404
9f5dcb81
JN
5405sub git_shortlog_body {
5406 # uses global variable $project
190d7fdc 5407 my ($commitlist, $from, $to, $refs, $extra) = @_;
ddb8d900 5408
9f5dcb81 5409 $from = 0 unless defined $from;
190d7fdc 5410 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
9f5dcb81 5411
591ebf65 5412 print "<table class=\"shortlog\">\n";
6dd36acd 5413 my $alternate = 1;
9f5dcb81 5414 for (my $i = $from; $i <= $to; $i++) {
190d7fdc
RF
5415 my %co = %{$commitlist->[$i]};
5416 my $commit = $co{'id'};
847e01fb 5417 my $ref = format_ref_marker($refs, $commit);
9f5dcb81
JN
5418 if ($alternate) {
5419 print "<tr class=\"dark\">\n";
5420 } else {
5421 print "<tr class=\"light\">\n";
5422 }
5423 $alternate ^= 1;
5424 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5425 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1c49a4e1 5426 format_author_html('td', \%co, 10) . "<td>";
952c65fc
JN
5427 print format_subject_html($co{'title'}, $co{'title_short'},
5428 href(action=>"commit", hash=>$commit), $ref);
9f5dcb81
JN
5429 print "</td>\n" .
5430 "<td class=\"link\">" .
4777b014 5431 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
35749ae5 5432 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
55ff35cb 5433 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
a3c8ab30
MM
5434 my $snapshot_links = format_snapshot_links($commit);
5435 if (defined $snapshot_links) {
5436 print " | " . $snapshot_links;
55ff35cb 5437 }
cb9c6e5b 5438 print "</td>\n" .
9f5dcb81
JN
5439 "</tr>\n";
5440 }
5441 if (defined $extra) {
5442 print "<tr>\n" .
5443 "<td colspan=\"4\">$extra</td>\n" .
5444 "</tr>\n";
5445 }
5446 print "</table>\n";
5447}
5448
581860e1
JN
5449sub git_history_body {
5450 # Warning: assumes constant type (blob or tree) during history
69ca37d2
JN
5451 my ($commitlist, $from, $to, $refs, $extra,
5452 $file_name, $file_hash, $ftype) = @_;
8be68352
JN
5453
5454 $from = 0 unless defined $from;
a8b983bf 5455 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
581860e1 5456
591ebf65 5457 print "<table class=\"history\">\n";
6dd36acd 5458 my $alternate = 1;
8be68352 5459 for (my $i = $from; $i <= $to; $i++) {
a8b983bf 5460 my %co = %{$commitlist->[$i]};
581860e1
JN
5461 if (!%co) {
5462 next;
5463 }
a8b983bf 5464 my $commit = $co{'id'};
581860e1
JN
5465
5466 my $ref = format_ref_marker($refs, $commit);
5467
5468 if ($alternate) {
5469 print "<tr class=\"dark\">\n";
5470 } else {
5471 print "<tr class=\"light\">\n";
5472 }
5473 $alternate ^= 1;
5474 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1c49a4e1
GB
5475 # shortlog: format_author_html('td', \%co, 10)
5476 format_author_html('td', \%co, 15, 3) . "<td>";
581860e1 5477 # originally git_history used chop_str($co{'title'}, 50)
952c65fc
JN
5478 print format_subject_html($co{'title'}, $co{'title_short'},
5479 href(action=>"commit", hash=>$commit), $ref);
581860e1
JN
5480 print "</td>\n" .
5481 "<td class=\"link\">" .
6d81c5a2
LT
5482 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5483 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
581860e1
JN
5484
5485 if ($ftype eq 'blob') {
69ca37d2 5486 my $blob_current = $file_hash;
581860e1
JN
5487 my $blob_parent = git_get_hash_by_path($commit, $file_name);
5488 if (defined $blob_current && defined $blob_parent &&
5489 $blob_current ne $blob_parent) {
5490 print " | " .
420e92f2
JN
5491 $cgi->a({-href => href(action=>"blobdiff",
5492 hash=>$blob_current, hash_parent=>$blob_parent,
5493 hash_base=>$hash_base, hash_parent_base=>$commit,
5494 file_name=>$file_name)},
581860e1
JN
5495 "diff to current");
5496 }
5497 }
5498 print "</td>\n" .
5499 "</tr>\n";
5500 }
5501 if (defined $extra) {
5502 print "<tr>\n" .
5503 "<td colspan=\"4\">$extra</td>\n" .
5504 "</tr>\n";
5505 }
5506 print "</table>\n";
5507}
5508
717b8311
JN
5509sub git_tags_body {
5510 # uses global variable $project
5511 my ($taglist, $from, $to, $extra) = @_;
5512 $from = 0 unless defined $from;
5513 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
5514
591ebf65 5515 print "<table class=\"tags\">\n";
6dd36acd 5516 my $alternate = 1;
717b8311
JN
5517 for (my $i = $from; $i <= $to; $i++) {
5518 my $entry = $taglist->[$i];
5519 my %tag = %$entry;
cd146408 5520 my $comment = $tag{'subject'};
717b8311
JN
5521 my $comment_short;
5522 if (defined $comment) {
5523 $comment_short = chop_str($comment, 30, 5);
5524 }
5525 if ($alternate) {
5526 print "<tr class=\"dark\">\n";
5527 } else {
5528 print "<tr class=\"light\">\n";
5529 }
5530 $alternate ^= 1;
27dd1a83
JN
5531 if (defined $tag{'age'}) {
5532 print "<td><i>$tag{'age'}</i></td>\n";
5533 } else {
5534 print "<td></td>\n";
5535 }
5536 print "<td>" .
1c2a4f5a 5537 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
63e4220b 5538 -class => "list name"}, esc_html($tag{'name'})) .
717b8311
JN
5539 "</td>\n" .
5540 "<td>";
5541 if (defined $comment) {
952c65fc
JN
5542 print format_subject_html($comment, $comment_short,
5543 href(action=>"tag", hash=>$tag{'id'}));
717b8311
JN
5544 }
5545 print "</td>\n" .
5546 "<td class=\"selflink\">";
5547 if ($tag{'type'} eq "tag") {
1c2a4f5a 5548 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
717b8311
JN
5549 } else {
5550 print "&nbsp;";
5551 }
5552 print "</td>\n" .
5553 "<td class=\"link\">" . " | " .
1c2a4f5a 5554 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
717b8311 5555 if ($tag{'reftype'} eq "commit") {
bf901f8e
JN
5556 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
5557 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
717b8311 5558 } elsif ($tag{'reftype'} eq "blob") {
1c2a4f5a 5559 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
717b8311
JN
5560 }
5561 print "</td>\n" .
5562 "</tr>";
5563 }
5564 if (defined $extra) {
5565 print "<tr>\n" .
5566 "<td colspan=\"5\">$extra</td>\n" .
5567 "</tr>\n";
5568 }
5569 print "</table>\n";
5570}
5571
5572sub git_heads_body {
5573 # uses global variable $project
120ddde2 5574 my ($headlist, $head, $from, $to, $extra) = @_;
717b8311 5575 $from = 0 unless defined $from;
120ddde2 5576 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
717b8311 5577
591ebf65 5578 print "<table class=\"heads\">\n";
6dd36acd 5579 my $alternate = 1;
717b8311 5580 for (my $i = $from; $i <= $to; $i++) {
120ddde2 5581 my $entry = $headlist->[$i];
cd146408
JN
5582 my %ref = %$entry;
5583 my $curr = $ref{'id'} eq $head;
717b8311
JN
5584 if ($alternate) {
5585 print "<tr class=\"dark\">\n";
5586 } else {
5587 print "<tr class=\"light\">\n";
5588 }
5589 $alternate ^= 1;
cd146408
JN
5590 print "<td><i>$ref{'age'}</i></td>\n" .
5591 ($curr ? "<td class=\"current_head\">" : "<td>") .
bf901f8e 5592 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
cd146408 5593 -class => "list name"},esc_html($ref{'name'})) .
717b8311
JN
5594 "</td>\n" .
5595 "<td class=\"link\">" .
bf901f8e
JN
5596 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
5597 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
9e70e158 5598 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
717b8311
JN
5599 "</td>\n" .
5600 "</tr>";
5601 }
5602 if (defined $extra) {
5603 print "<tr>\n" .
5604 "<td colspan=\"3\">$extra</td>\n" .
5605 "</tr>\n";
5606 }
5607 print "</table>\n";
5608}
5609
9d0d42f3
GB
5610# Display a single remote block
5611sub git_remote_block {
5612 my ($remote, $rdata, $limit, $head) = @_;
5613
5614 my $heads = $rdata->{'heads'};
5615 my $fetch = $rdata->{'fetch'};
5616 my $push = $rdata->{'push'};
5617
5618 my $urls_table = "<table class=\"projects_list\">\n" ;
5619
5620 if (defined $fetch) {
5621 if ($fetch eq $push) {
5622 $urls_table .= format_repo_url("URL", $fetch);
5623 } else {
5624 $urls_table .= format_repo_url("Fetch URL", $fetch);
5625 $urls_table .= format_repo_url("Push URL", $push) if defined $push;
5626 }
5627 } elsif (defined $push) {
5628 $urls_table .= format_repo_url("Push URL", $push);
5629 } else {
5630 $urls_table .= format_repo_url("", "No remote URL");
5631 }
5632
5633 $urls_table .= "</table>\n";
5634
5635 my $dots;
5636 if (defined $limit && $limit < @$heads) {
5637 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
5638 }
5639
5640 print $urls_table;
5641 git_heads_body($heads, $head, 0, $limit, $dots);
5642}
5643
5644# Display a list of remote names with the respective fetch and push URLs
5645sub git_remotes_list {
5646 my ($remotedata, $limit) = @_;
5647 print "<table class=\"heads\">\n";
5648 my $alternate = 1;
5649 my @remotes = sort keys %$remotedata;
5650
5651 my $limited = $limit && $limit < @remotes;
5652
5653 $#remotes = $limit - 1 if $limited;
5654
5655 while (my $remote = shift @remotes) {
5656 my $rdata = $remotedata->{$remote};
5657 my $fetch = $rdata->{'fetch'};
5658 my $push = $rdata->{'push'};
5659 if ($alternate) {
5660 print "<tr class=\"dark\">\n";
5661 } else {
5662 print "<tr class=\"light\">\n";
5663 }
5664 $alternate ^= 1;
5665 print "<td>" .
5666 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
5667 -class=> "list name"},esc_html($remote)) .
5668 "</td>";
5669 print "<td class=\"link\">" .
5670 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
5671 " | " .
5672 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
5673 "</td>";
5674
5675 print "</tr>\n";
5676 }
5677
5678 if ($limited) {
5679 print "<tr>\n" .
5680 "<td colspan=\"3\">" .
5681 $cgi->a({-href => href(action=>"remotes")}, "...") .
5682 "</td>\n" . "</tr>\n";
5683 }
5684
5685 print "</table>";
5686}
5687
5688# Display remote heads grouped by remote, unless there are too many
5689# remotes, in which case we only display the remote names
5690sub git_remotes_body {
5691 my ($remotedata, $limit, $head) = @_;
5692 if ($limit and $limit < keys %$remotedata) {
5693 git_remotes_list($remotedata, $limit);
5694 } else {
5695 fill_remote_heads($remotedata);
5696 while (my ($remote, $rdata) = each %$remotedata) {
5697 git_print_section({-class=>"remote", -id=>$remote},
5698 ["remotes", $remote, $remote], sub {
5699 git_remote_block($remote, $rdata, $limit, $head);
5700 });
5701 }
5702 }
5703}
5704
16f20725
JN
5705sub git_search_message {
5706 my %co = @_;
5707
5708 my $greptype;
5709 if ($searchtype eq 'commit') {
5710 $greptype = "--grep=";
5711 } elsif ($searchtype eq 'author') {
5712 $greptype = "--author=";
5713 } elsif ($searchtype eq 'committer') {
5714 $greptype = "--committer=";
5715 }
5716 $greptype .= $searchtext;
5717 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5718 $greptype, '--regexp-ignore-case',
5719 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5720
5721 my $paging_nav = '';
5722 if ($page > 0) {
5723 $paging_nav .=
882541b8
JN
5724 $cgi->a({-href => href(-replay=>1, page=>undef)},
5725 "first") .
5726 " &sdot; " .
16f20725
JN
5727 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5728 -accesskey => "p", -title => "Alt-p"}, "prev");
5729 } else {
882541b8 5730 $paging_nav .= "first &sdot; prev";
16f20725
JN
5731 }
5732 my $next_link = '';
5733 if ($#commitlist >= 100) {
5734 $next_link =
5735 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5736 -accesskey => "n", -title => "Alt-n"}, "next");
5737 $paging_nav .= " &sdot; $next_link";
5738 } else {
5739 $paging_nav .= " &sdot; next";
5740 }
5741
1ae05be4
JN
5742 git_header_html();
5743
16f20725
JN
5744 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5745 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5746 if ($page == 0 && !@commitlist) {
5747 print "<p>No match.</p>\n";
5748 } else {
5749 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5750 }
1ae05be4
JN
5751
5752 git_footer_html();
16f20725
JN
5753}
5754
5755sub git_search_changes {
5756 my %co = @_;
5757
1ae05be4
JN
5758 local $/ = "\n";
5759 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5760 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5761 ($search_use_regexp ? '--pickaxe-regex' : ())
5762 or die_error(500, "Open git-log failed");
5763
5764 git_header_html();
5765
16f20725
JN
5766 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5767 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5768
5769 print "<table class=\"pickaxe search\">\n";
5770 my $alternate = 1;
16f20725
JN
5771 undef %co;
5772 my @files;
5773 while (my $line = <$fd>) {
5774 chomp $line;
5775 next unless $line;
5776
5777 my %set = parse_difftree_raw_line($line);
5778 if (defined $set{'commit'}) {
5779 # finish previous commit
5780 if (%co) {
5781 print "</td>\n" .
5782 "<td class=\"link\">" .
882541b8
JN
5783 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
5784 "commit") .
16f20725 5785 " | " .
882541b8
JN
5786 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
5787 hash_base=>$co{'id'})},
5788 "tree") .
5789 "</td>\n" .
16f20725
JN
5790 "</tr>\n";
5791 }
5792
5793 if ($alternate) {
5794 print "<tr class=\"dark\">\n";
5795 } else {
5796 print "<tr class=\"light\">\n";
5797 }
5798 $alternate ^= 1;
5799 %co = parse_commit($set{'commit'});
5800 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5801 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5802 "<td><i>$author</i></td>\n" .
5803 "<td>" .
5804 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5805 -class => "list subject"},
5806 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5807 } elsif (defined $set{'to_id'}) {
5808 next if ($set{'to_id'} =~ m/^0{40}$/);
5809
5810 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5811 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5812 -class => "list"},
5813 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5814 "<br/>\n";
5815 }
5816 }
5817 close $fd;
5818
5819 # finish last commit (warning: repetition!)
5820 if (%co) {
5821 print "</td>\n" .
5822 "<td class=\"link\">" .
882541b8
JN
5823 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
5824 "commit") .
16f20725 5825 " | " .
882541b8
JN
5826 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
5827 hash_base=>$co{'id'})},
5828 "tree") .
5829 "</td>\n" .
16f20725
JN
5830 "</tr>\n";
5831 }
5832
5833 print "</table>\n";
1ae05be4
JN
5834
5835 git_footer_html();
16f20725
JN
5836}
5837
5838sub git_search_files {
5839 my %co = @_;
5840
1ae05be4 5841 local $/ = "\n";
8e09fd1a 5842 open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
1ae05be4
JN
5843 $search_use_regexp ? ('-E', '-i') : '-F',
5844 $searchtext, $co{'tree'}
5845 or die_error(500, "Open git-grep failed");
5846
5847 git_header_html();
5848
16f20725
JN
5849 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5850 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5851
5852 print "<table class=\"grep_search\">\n";
5853 my $alternate = 1;
5854 my $matches = 0;
16f20725
JN
5855 my $lastfile = '';
5856 while (my $line = <$fd>) {
5857 chomp $line;
ff7f2185 5858 my ($file, $file_href, $lno, $ltext, $binary);
16f20725
JN
5859 last if ($matches++ > 1000);
5860 if ($line =~ /^Binary file (.+) matches$/) {
5861 $file = $1;
5862 $binary = 1;
5863 } else {
8e09fd1a
JN
5864 ($file, $lno, $ltext) = split(/\0/, $line, 3);
5865 $file =~ s/^$co{'tree'}://;
16f20725
JN
5866 }
5867 if ($file ne $lastfile) {
5868 $lastfile and print "</td></tr>\n";
5869 if ($alternate++) {
5870 print "<tr class=\"dark\">\n";
5871 } else {
5872 print "<tr class=\"light\">\n";
5873 }
ff7f2185
JN
5874 $file_href = href(action=>"blob", hash_base=>$co{'id'},
5875 file_name=>$file);
16f20725 5876 print "<td class=\"list\">".
ff7f2185 5877 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
16f20725
JN
5878 print "</td><td>\n";
5879 $lastfile = $file;
5880 }
5881 if ($binary) {
5882 print "<div class=\"binary\">Binary file</div>\n";
5883 } else {
5884 $ltext = untabify($ltext);
5885 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
5886 $ltext = esc_html($1, -nbsp=>1);
5887 $ltext .= '<span class="match">';
5888 $ltext .= esc_html($2, -nbsp=>1);
5889 $ltext .= '</span>';
5890 $ltext .= esc_html($3, -nbsp=>1);
5891 } else {
5892 $ltext = esc_html($ltext, -nbsp=>1);
5893 }
5894 print "<div class=\"pre\">" .
ff7f2185
JN
5895 $cgi->a({-href => $file_href.'#l'.$lno,
5896 -class => "linenr"}, sprintf('%4i', $lno)) .
5897 ' ' . $ltext . "</div>\n";
16f20725
JN
5898 }
5899 }
5900 if ($lastfile) {
5901 print "</td></tr>\n";
5902 if ($matches > 1000) {
5903 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5904 }
5905 } else {
5906 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5907 }
5908 close $fd;
5909
5910 print "</table>\n";
1ae05be4
JN
5911
5912 git_footer_html();
16f20725
JN
5913}
5914
8dbc0fce 5915sub git_search_grep_body {
5ad66088 5916 my ($commitlist, $from, $to, $extra) = @_;
8dbc0fce 5917 $from = 0 unless defined $from;
5ad66088 5918 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
8dbc0fce 5919
591ebf65 5920 print "<table class=\"commit_search\">\n";
8dbc0fce
RF
5921 my $alternate = 1;
5922 for (my $i = $from; $i <= $to; $i++) {
5ad66088 5923 my %co = %{$commitlist->[$i]};
8dbc0fce
RF
5924 if (!%co) {
5925 next;
5926 }
5ad66088 5927 my $commit = $co{'id'};
8dbc0fce
RF
5928 if ($alternate) {
5929 print "<tr class=\"dark\">\n";
5930 } else {
5931 print "<tr class=\"light\">\n";
5932 }
5933 $alternate ^= 1;
5934 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
1c49a4e1 5935 format_author_html('td', \%co, 15, 5) .
8dbc0fce 5936 "<td>" .
be8b9063
JH
5937 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5938 -class => "list subject"},
5939 chop_and_escape_str($co{'title'}, 50) . "<br/>");
8dbc0fce
RF
5940 my $comment = $co{'comment'};
5941 foreach my $line (@$comment) {
6dfbb304 5942 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
be8b9063 5943 my ($lead, $match, $trail) = ($1, $2, $3);
b8d97d07
JN
5944 $match = chop_str($match, 70, 5, 'center');
5945 my $contextlen = int((80 - length($match))/2);
5946 $contextlen = 30 if ($contextlen > 30);
5947 $lead = chop_str($lead, $contextlen, 10, 'left');
5948 $trail = chop_str($trail, $contextlen, 10, 'right');
be8b9063
JH
5949
5950 $lead = esc_html($lead);
5951 $match = esc_html($match);
5952 $trail = esc_html($trail);
5953
5954 print "$lead<span class=\"match\">$match</span>$trail<br />";
8dbc0fce
RF
5955 }
5956 }
5957 print "</td>\n" .
5958 "<td class=\"link\">" .
5959 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5960 " | " .
f1fe8f5c
CR
5961 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
5962 " | " .
8dbc0fce
RF
5963 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5964 print "</td>\n" .
5965 "</tr>\n";
5966 }
5967 if (defined $extra) {
5968 print "<tr>\n" .
5969 "<td colspan=\"3\">$extra</td>\n" .
5970 "</tr>\n";
5971 }
5972 print "</table>\n";
5973}
5974
717b8311
JN
5975## ======================================================================
5976## ======================================================================
5977## actions
5978
717b8311 5979sub git_project_list {
1b2d297e 5980 my $order = $input_params{'order'};
b06dcf8c 5981 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 5982 die_error(400, "Unknown order parameter");
6326b60c
JN
5983 }
5984
847e01fb 5985 my @list = git_get_projects_list();
717b8311 5986 if (!@list) {
074afaa0 5987 die_error(404, "No projects found");
717b8311 5988 }
6326b60c 5989
717b8311 5990 git_header_html();
24d4afcd 5991 if (defined $home_text && -f $home_text) {
717b8311 5992 print "<div class=\"index_include\">\n";
2dcb5e1a 5993 insert_file($home_text);
717b8311 5994 print "</div>\n";
9f5dcb81 5995 }
0d1d154d
PB
5996 print $cgi->startform(-method => "get") .
5997 "<p class=\"projsearch\">Search:\n" .
5998 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5999 "</p>" .
6000 $cgi->end_form() . "\n";
e30496df
PB
6001 git_project_list_body(\@list, $order);
6002 git_footer_html();
6003}
6004
6005sub git_forks {
1b2d297e 6006 my $order = $input_params{'order'};
b06dcf8c 6007 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 6008 die_error(400, "Unknown order parameter");
717b8311 6009 }
e30496df 6010
4c7cd177
BL
6011 my $filter = $project;
6012 $filter =~ s/\.git$//;
6013 my @list = git_get_projects_list($filter);
e30496df 6014 if (!@list) {
074afaa0 6015 die_error(404, "No forks found");
9f5dcb81 6016 }
e30496df
PB
6017
6018 git_header_html();
6019 git_print_page_nav('','');
6020 git_print_header_div('summary', "$project forks");
6021 git_project_list_body(\@list, $order);
717b8311 6022 git_footer_html();
9f5dcb81
JN
6023}
6024
fc2b2be0 6025sub git_project_index {
12b1443c
JN
6026 my @projects = git_get_projects_list();
6027 if (!@projects) {
6028 die_error(404, "No projects found");
6029 }
fc2b2be0
JN
6030
6031 print $cgi->header(
6032 -type => 'text/plain',
6033 -charset => 'utf-8',
ab41dfbf 6034 -content_disposition => 'inline; filename="index.aux"');
fc2b2be0
JN
6035
6036 foreach my $pr (@projects) {
6037 if (!exists $pr->{'owner'}) {
76e4f5d0 6038 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
fc2b2be0
JN
6039 }
6040
6041 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6042 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6043 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6044 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6045 $path =~ s/ /\+/g;
6046 $owner =~ s/ /\+/g;
6047
6048 print "$path $owner\n";
6049 }
6050}
6051
ede5e100 6052sub git_summary {
847e01fb 6053 my $descr = git_get_project_description($project) || "none";
a979d128 6054 my %co = parse_commit("HEAD");
785cdea9 6055 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
a979d128 6056 my $head = $co{'id'};
00fa6fef 6057 my $remote_heads = gitweb_check_feature('remote_heads');
ede5e100 6058
1e0cf030 6059 my $owner = git_get_project_owner($project);
ede5e100 6060
cd146408 6061 my $refs = git_get_references();
313ce8ce
RF
6062 # These get_*_list functions return one more to allow us to see if
6063 # there are more ...
6064 my @taglist = git_get_tags_list(16);
6065 my @headlist = git_get_heads_list(16);
9d0d42f3 6066 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
e30496df 6067 my @forklist;
25b2790f 6068 my $check_forks = gitweb_check_feature('forks');
5dd5ed09
JH
6069
6070 if ($check_forks) {
12b1443c 6071 # find forks of a project
4c7cd177
BL
6072 my $filter = $project;
6073 $filter =~ s/\.git$//;
6074 @forklist = git_get_projects_list($filter);
12b1443c
JN
6075 # filter out forks of forks
6076 @forklist = filter_forks_from_projects_list(\@forklist)
6077 if (@forklist);
e30496df 6078 }
120ddde2 6079
ede5e100 6080 git_header_html();
847e01fb 6081 git_print_page_nav('summary','', $head);
9f5dcb81 6082
19806691 6083 print "<div class=\"title\">&nbsp;</div>\n";
591ebf65 6084 print "<table class=\"projects_list\">\n" .
a476142f
PB
6085 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
6086 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
785cdea9 6087 if (defined $cd{'rfc2822'}) {
256b7b48
JN
6088 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6089 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
785cdea9
JN
6090 }
6091
e79ca7cc
JN
6092 # use per project git URL list in $projectroot/$project/cloneurl
6093 # or make project git URL from git base URL and project name
19a8721e 6094 my $url_tag = "URL";
e79ca7cc
JN
6095 my @url_list = git_get_project_url_list($project);
6096 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6097 foreach my $git_url (@url_list) {
6098 next unless $git_url;
0e656999 6099 print format_repo_url($url_tag, $git_url);
19a8721e
JN
6100 $url_tag = "";
6101 }
aed93de4
PB
6102
6103 # Tag cloud
25b2790f 6104 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
6105 if ($show_ctags) {
6106 my $ctags = git_get_project_ctags($project);
0368c492
JN
6107 if (%$ctags) {
6108 # without ability to add tags, don't show if there are none
6109 my $cloud = git_populate_project_tagcloud($ctags);
6110 print "<tr id=\"metadata_ctags\">" .
6111 "<td>content tags</td>" .
6112 "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6113 "</tr>\n";
6114 }
aed93de4
PB
6115 }
6116
19a8721e 6117 print "</table>\n";
9f5dcb81 6118
7e1100e9
MM
6119 # If XSS prevention is on, we don't include README.html.
6120 # TODO: Allow a readme in some safe format.
6121 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
2dcb5e1a
JN
6122 print "<div class=\"title\">readme</div>\n" .
6123 "<div class=\"readme\">\n";
6124 insert_file("$projectroot/$project/README.html");
6125 print "\n</div>\n"; # class="readme"
447ef09a
PB
6126 }
6127
313ce8ce
RF
6128 # we need to request one more than 16 (0..15) to check if
6129 # those 16 are all
785cdea9
JN
6130 my @commitlist = $head ? parse_commits($head, 17) : ();
6131 if (@commitlist) {
6132 git_print_header_div('shortlog');
6133 git_shortlog_body(\@commitlist, 0, 15, $refs,
6134 $#commitlist <= 15 ? undef :
6135 $cgi->a({-href => href(action=>"shortlog")}, "..."));
6136 }
ede5e100 6137
120ddde2 6138 if (@taglist) {
847e01fb 6139 git_print_header_div('tags');
120ddde2 6140 git_tags_body(\@taglist, 0, 15,
313ce8ce 6141 $#taglist <= 15 ? undef :
1c2a4f5a 6142 $cgi->a({-href => href(action=>"tags")}, "..."));
ede5e100 6143 }
0db37973 6144
120ddde2 6145 if (@headlist) {
847e01fb 6146 git_print_header_div('heads');
120ddde2 6147 git_heads_body(\@headlist, $head, 0, 15,
313ce8ce 6148 $#headlist <= 15 ? undef :
1c2a4f5a 6149 $cgi->a({-href => href(action=>"heads")}, "..."));
0db37973 6150 }
9f5dcb81 6151
9d0d42f3 6152 if (%remotedata) {
00fa6fef 6153 git_print_header_div('remotes');
9d0d42f3 6154 git_remotes_body(\%remotedata, 15, $head);
00fa6fef
GB
6155 }
6156
e30496df
PB
6157 if (@forklist) {
6158 git_print_header_div('forks');
f04f27e8 6159 git_project_list_body(\@forklist, 'age', 0, 15,
aaca9675 6160 $#forklist <= 15 ? undef :
e30496df 6161 $cgi->a({-href => href(action=>"forks")}, "..."),
f04f27e8 6162 'no_header');
e30496df
PB
6163 }
6164
ede5e100
KS
6165 git_footer_html();
6166}
6167
d8a20ba9 6168sub git_tag {
847e01fb 6169 my %tag = parse_tag($hash);
198a2a8a
JN
6170
6171 if (! %tag) {
074afaa0 6172 die_error(404, "Unknown tag object");
198a2a8a
JN
6173 }
6174
d8a94803
AK
6175 my $head = git_get_head_hash($project);
6176 git_header_html();
6177 git_print_page_nav('','', $head,undef,$head);
847e01fb 6178 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
d8a20ba9 6179 print "<div class=\"title_text\">\n" .
591ebf65 6180 "<table class=\"object_header\">\n" .
e4669df9
KS
6181 "<tr>\n" .
6182 "<td>object</td>\n" .
952c65fc
JN
6183 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6184 $tag{'object'}) . "</td>\n" .
6185 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6186 $tag{'type'}) . "</td>\n" .
e4669df9 6187 "</tr>\n";
d8a20ba9 6188 if (defined($tag{'author'})) {
ba924733 6189 git_print_authorship_rows(\%tag, 'author');
d8a20ba9
KS
6190 }
6191 print "</table>\n\n" .
6192 "</div>\n";
6193 print "<div class=\"page_body\">";
6194 my $comment = $tag{'comment'};
6195 foreach my $line (@$comment) {
7002243f 6196 chomp $line;
793c400c 6197 print esc_html($line, -nbsp=>1) . "<br/>\n";
d8a20ba9
KS
6198 }
6199 print "</div>\n";
6200 git_footer_html();
6201}
6202
4af819d4
JN
6203sub git_blame_common {
6204 my $format = shift || 'porcelain';
c4ccf61f
JN
6205 if ($format eq 'porcelain' && $cgi->param('js')) {
6206 $format = 'incremental';
6207 $action = 'blame_incremental'; # for page title etc
6208 }
4af819d4 6209
d2ce10d7 6210 # permissions
25b2790f 6211 gitweb_check_feature('blame')
d2ce10d7 6212 or die_error(403, "Blame view not allowed");
074afaa0 6213
d2ce10d7 6214 # error checking
074afaa0 6215 die_error(400, "No file name given") unless $file_name;
847e01fb 6216 $hash_base ||= git_get_head_hash($project);
d2ce10d7 6217 die_error(404, "Couldn't find base commit") unless $hash_base;
847e01fb 6218 my %co = parse_commit($hash_base)
074afaa0 6219 or die_error(404, "Commit not found");
d2ce10d7 6220 my $ftype = "blob";
1f2857ea
LT
6221 if (!defined $hash) {
6222 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
074afaa0 6223 or die_error(404, "Error looking up file");
d2ce10d7
JN
6224 } else {
6225 $ftype = git_get_type($hash);
6226 if ($ftype !~ "blob") {
6227 die_error(400, "Object is not a blob");
6228 }
1f2857ea 6229 }
d2ce10d7 6230
4af819d4
JN
6231 my $fd;
6232 if ($format eq 'incremental') {
6233 # get file contents (as base)
6234 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
6235 or die_error(500, "Open git-cat-file failed");
6236 } elsif ($format eq 'data') {
6237 # run git-blame --incremental
6238 open $fd, "-|", git_cmd(), "blame", "--incremental",
6239 $hash_base, "--", $file_name
6240 or die_error(500, "Open git-blame --incremental failed");
6241 } else {
6242 # run git-blame --porcelain
6243 open $fd, "-|", git_cmd(), "blame", '-p',
6244 $hash_base, '--', $file_name
6245 or die_error(500, "Open git-blame --porcelain failed");
6246 }
6247
6248 # incremental blame data returns early
6249 if ($format eq 'data') {
6250 print $cgi->header(
6251 -type=>"text/plain", -charset => "utf-8",
6252 -status=> "200 OK");
6253 local $| = 1; # output autoflush
57cf4ad6
JK
6254 while (my $line = <$fd>) {
6255 print to_utf8($line);
6256 }
4af819d4
JN
6257 close $fd
6258 or print "ERROR $!\n";
6259
6260 print 'END';
6261 if (defined $t0 && gitweb_check_feature('timed')) {
6262 print ' '.
3962f1d7 6263 tv_interval($t0, [ gettimeofday() ]).
4af819d4
JN
6264 ' '.$number_of_git_cmds;
6265 }
6266 print "\n";
6267
6268 return;
6269 }
d2ce10d7
JN
6270
6271 # page header
1f2857ea 6272 git_header_html();
0d83ddc4 6273 my $formats_nav =
a3823e5a 6274 $cgi->a({-href => href(action=>"blob", -replay=>1)},
952c65fc 6275 "blob") .
87e573f6
JN
6276 " | ";
6277 if ($format eq 'incremental') {
6278 $formats_nav .=
6279 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6280 "blame") . " (non-incremental)";
6281 } else {
6282 $formats_nav .=
6283 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6284 "blame") . " (incremental)";
6285 }
6286 $formats_nav .=
952c65fc 6287 " | " .
a3823e5a
JN
6288 $cgi->a({-href => href(action=>"history", -replay=>1)},
6289 "history") .
cae1862a 6290 " | " .
4af819d4 6291 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
f35274da 6292 "HEAD");
847e01fb
JN
6293 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6294 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
59fb1c94 6295 git_print_page_path($file_name, $ftype, $hash_base);
d2ce10d7
JN
6296
6297 # page body
4af819d4
JN
6298 if ($format eq 'incremental') {
6299 print "<noscript>\n<div class=\"error\"><center><b>\n".
6300 "This page requires JavaScript to run.\n Use ".
c4ccf61f 6301 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
4af819d4
JN
6302 'this page').
6303 " instead.\n".
6304 "</b></center></div>\n</noscript>\n";
6305
6306 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6307 }
6308
6309 print qq!<div class="page_body">\n!;
6310 print qq!<div id="progress_info">... / ...</div>\n!
6311 if ($format eq 'incremental');
6312 print qq!<table id="blame_table" class="blame" width="100%">\n!.
6313 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6314 qq!<thead>\n!.
6315 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6316 qq!</thead>\n!.
6317 qq!<tbody>\n!;
6318
aef37684 6319 my @rev_color = qw(light dark);
cc1bf97e
LT
6320 my $num_colors = scalar(@rev_color);
6321 my $current_color = 0;
d2ce10d7 6322
4af819d4
JN
6323 if ($format eq 'incremental') {
6324 my $color_class = $rev_color[$current_color];
6325
6326 #contents of a file
6327 my $linenr = 0;
6328 LINE:
6329 while (my $line = <$fd>) {
6330 chomp $line;
6331 $linenr++;
6332
6333 print qq!<tr id="l$linenr" class="$color_class">!.
6334 qq!<td class="sha1"><a href=""> </a></td>!.
6335 qq!<td class="linenr">!.
6336 qq!<a class="linenr" href="">$linenr</a></td>!;
6337 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
6338 print qq!</tr>\n!;
eeef88cd 6339 }
4af819d4
JN
6340
6341 } else { # porcelain, i.e. ordinary blame
6342 my %metainfo = (); # saves information about commits
6343
6344 # blame data
6345 LINE:
6346 while (my $line = <$fd>) {
6347 chomp $line;
6348 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
6349 # no <lines in group> for subsequent lines in group of lines
6350 my ($full_rev, $orig_lineno, $lineno, $group_size) =
6351 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
6352 if (!exists $metainfo{$full_rev}) {
6353 $metainfo{$full_rev} = { 'nprevious' => 0 };
eeef88cd 6354 }
4af819d4
JN
6355 my $meta = $metainfo{$full_rev};
6356 my $data;
6357 while ($data = <$fd>) {
6358 chomp $data;
6359 last if ($data =~ s/^\t//); # contents of line
6360 if ($data =~ /^(\S+)(?: (.*))?$/) {
6361 $meta->{$1} = $2 unless exists $meta->{$1};
6362 }
6363 if ($data =~ /^previous /) {
6364 $meta->{'nprevious'}++;
6365 }
eeef88cd 6366 }
4af819d4
JN
6367 my $short_rev = substr($full_rev, 0, 8);
6368 my $author = $meta->{'author'};
6369 my %date =
6370 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
6371 my $date = $date{'iso-tz'};
6372 if ($group_size) {
6373 $current_color = ($current_color + 1) % $num_colors;
3665e7e7 6374 }
4af819d4
JN
6375 my $tr_class = $rev_color[$current_color];
6376 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
6377 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
6378 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
6379 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
6380 if ($group_size) {
6381 print "<td class=\"sha1\"";
6382 print " title=\"". esc_html($author) . ", $date\"";
6383 print " rowspan=\"$group_size\"" if ($group_size > 1);
6384 print ">";
6385 print $cgi->a({-href => href(action=>"commit",
6386 hash=>$full_rev,
6387 file_name=>$file_name)},
6388 esc_html($short_rev));
6389 if ($group_size >= 2) {
6390 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
6391 if (@author_initials) {
6392 print "<br />" .
6393 esc_html(join('', @author_initials));
6394 # or join('.', ...)
6395 }
a36817b6 6396 }
4af819d4 6397 print "</td>\n";
a36817b6 6398 }
4af819d4
JN
6399 # 'previous' <sha1 of parent commit> <filename at commit>
6400 if (exists $meta->{'previous'} &&
6401 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
6402 $meta->{'parent'} = $1;
6403 $meta->{'file_parent'} = unquote($2);
6404 }
6405 my $linenr_commit =
6406 exists($meta->{'parent'}) ?
6407 $meta->{'parent'} : $full_rev;
6408 my $linenr_filename =
6409 exists($meta->{'file_parent'}) ?
6410 $meta->{'file_parent'} : unquote($meta->{'filename'});
6411 my $blamed = href(action => 'blame',
6412 file_name => $linenr_filename,
6413 hash_base => $linenr_commit);
6414 print "<td class=\"linenr\">";
6415 print $cgi->a({ -href => "$blamed#l$orig_lineno",
6416 -class => "linenr" },
6417 esc_html($lineno));
6418 print "</td>";
6419 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
6420 print "</tr>\n";
6421 } # end while
6422
1f2857ea 6423 }
4af819d4
JN
6424
6425 # footer
6426 print "</tbody>\n".
6427 "</table>\n"; # class="blame"
6428 print "</div>\n"; # class="blame_body"
952c65fc
JN
6429 close $fd
6430 or print "Reading blob failed\n";
d2ce10d7 6431
1f2857ea
LT
6432 git_footer_html();
6433}
6434
4af819d4
JN
6435sub git_blame {
6436 git_blame_common();
6437}
6438
6439sub git_blame_incremental {
6440 git_blame_common('incremental');
6441}
6442
6443sub git_blame_data {
6444 git_blame_common('data');
6445}
6446
717b8311 6447sub git_tags {
847e01fb 6448 my $head = git_get_head_hash($project);
717b8311 6449 git_header_html();
11e7bece 6450 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
847e01fb 6451 git_print_header_div('summary', $project);
2d007374 6452
cd146408
JN
6453 my @tagslist = git_get_tags_list();
6454 if (@tagslist) {
6455 git_tags_body(\@tagslist);
2d007374 6456 }
717b8311 6457 git_footer_html();
2d007374
PB
6458}
6459
717b8311 6460sub git_heads {
847e01fb 6461 my $head = git_get_head_hash($project);
717b8311 6462 git_header_html();
11e7bece 6463 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
847e01fb 6464 git_print_header_div('summary', $project);
930cf7dd 6465
cd146408
JN
6466 my @headslist = git_get_heads_list();
6467 if (@headslist) {
6468 git_heads_body(\@headslist, $head);
f5aa79d9 6469 }
717b8311 6470 git_footer_html();
f5aa79d9
JN
6471}
6472
9d0d42f3 6473# used both for single remote view and for list of all the remotes
00fa6fef
GB
6474sub git_remotes {
6475 gitweb_check_feature('remote_heads')
6476 or die_error(403, "Remote heads view is disabled");
6477
6478 my $head = git_get_head_hash($project);
bb607760
GB
6479 my $remote = $input_params{'hash'};
6480
9d0d42f3
GB
6481 my $remotedata = git_get_remotes_list($remote);
6482 die_error(500, "Unable to get remote information") unless defined $remotedata;
bb607760 6483
9d0d42f3
GB
6484 unless (%$remotedata) {
6485 die_error(404, defined $remote ?
6486 "Remote $remote not found" :
6487 "No remotes found");
bb607760
GB
6488 }
6489
6490 git_header_html(undef, undef, -action_extra => $remote);
6491 git_print_page_nav('', '', $head, undef, $head,
6492 format_ref_views($remote ? '' : 'remotes'));
6493
9d0d42f3 6494 fill_remote_heads($remotedata);
bb607760
GB
6495 if (defined $remote) {
6496 git_print_header_div('remotes', "$remote remote for $project");
9d0d42f3 6497 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
bb607760
GB
6498 } else {
6499 git_print_header_div('summary', "$project remotes");
9d0d42f3 6500 git_remotes_body($remotedata, undef, $head);
00fa6fef 6501 }
bb607760 6502
00fa6fef
GB
6503 git_footer_html();
6504}
6505
19806691 6506sub git_blob_plain {
7f718e8b 6507 my $type = shift;
f2e73302 6508 my $expires;
f2e73302 6509
cff0771b 6510 if (!defined $hash) {
5be01bc8 6511 if (defined $file_name) {
847e01fb 6512 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 6513 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 6514 or die_error(404, "Cannot find file");
5be01bc8 6515 } else {
074afaa0 6516 die_error(400, "No file name defined");
5be01bc8 6517 }
800764cf
MW
6518 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6519 # blobs defined by non-textual hash id's can be cached
6520 $expires = "+1d";
5be01bc8 6521 }
800764cf 6522
25691fbe 6523 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 6524 or die_error(500, "Open git-cat-file blob '$hash' failed");
930cf7dd 6525
7f718e8b
JN
6526 # content-type (can include charset)
6527 $type = blob_contenttype($fd, $file_name, $type);
f5aa79d9 6528
7f718e8b 6529 # "save as" filename, even when no $file_name is given
f5aa79d9 6530 my $save_as = "$hash";
9312944d
KS
6531 if (defined $file_name) {
6532 $save_as = $file_name;
f5aa79d9
JN
6533 } elsif ($type =~ m/^text\//) {
6534 $save_as .= '.txt';
9312944d 6535 }
f5aa79d9 6536
7e1100e9
MM
6537 # With XSS prevention on, blobs of all types except a few known safe
6538 # ones are served with "Content-Disposition: attachment" to make sure
6539 # they don't run in our security domain. For certain image types,
6540 # blob view writes an <img> tag referring to blob_plain view, and we
6541 # want to be sure not to break that by serving the image as an
6542 # attachment (though Firefox 3 doesn't seem to care).
6543 my $sandbox = $prevent_xss &&
86afbd02
JN
6544 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
6545
6546 # serve text/* as text/plain
6547 if ($prevent_xss &&
e8c35317
JN
6548 ($type =~ m!^text/[a-z]+\b(.*)$! ||
6549 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
86afbd02
JN
6550 my $rest = $1;
6551 $rest = defined $rest ? $rest : '';
6552 $type = "text/plain$rest";
6553 }
7e1100e9 6554
f2e73302 6555 print $cgi->header(
7f718e8b
JN
6556 -type => $type,
6557 -expires => $expires,
7e1100e9
MM
6558 -content_disposition =>
6559 ($sandbox ? 'attachment' : 'inline')
6560 . '; filename="' . $save_as . '"');
34122b57 6561 local $/ = undef;
ad14e931 6562 binmode STDOUT, ':raw';
19806691 6563 print <$fd>;
ad14e931 6564 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
19806691
KS
6565 close $fd;
6566}
6567
930cf7dd 6568sub git_blob {
f2e73302 6569 my $expires;
f2e73302 6570
cff0771b 6571 if (!defined $hash) {
5be01bc8 6572 if (defined $file_name) {
847e01fb 6573 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 6574 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 6575 or die_error(404, "Cannot find file");
5be01bc8 6576 } else {
074afaa0 6577 die_error(400, "No file name defined");
5be01bc8 6578 }
800764cf
MW
6579 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6580 # blobs defined by non-textual hash id's can be cached
6581 $expires = "+1d";
5be01bc8 6582 }
800764cf 6583
25b2790f 6584 my $have_blame = gitweb_check_feature('blame');
25691fbe 6585 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 6586 or die_error(500, "Couldn't cat $file_name, $hash");
847e01fb 6587 my $mimetype = blob_mimetype($fd, $file_name);
b331fe54 6588 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
dfa7c7d2 6589 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
930cf7dd
LT
6590 close $fd;
6591 return git_blob_plain($mimetype);
6592 }
5a4cf334
JN
6593 # we can have blame only for text/* mimetype
6594 $have_blame &&= ($mimetype =~ m!^text/!);
6595
592ea417
JN
6596 my $highlight = gitweb_check_feature('highlight');
6597 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
6598 $fd = run_highlighter($fd, $highlight, $syntax)
6599 if $syntax;
b331fe54 6600
f2e73302 6601 git_header_html(undef, $expires);
0d83ddc4 6602 my $formats_nav = '';
847e01fb 6603 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
930cf7dd
LT
6604 if (defined $file_name) {
6605 if ($have_blame) {
952c65fc 6606 $formats_nav .=
a3823e5a 6607 $cgi->a({-href => href(action=>"blame", -replay=>1)},
952c65fc
JN
6608 "blame") .
6609 " | ";
930cf7dd 6610 }
0d83ddc4 6611 $formats_nav .=
a3823e5a 6612 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
6613 "history") .
6614 " | " .
a3823e5a 6615 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
35329cc1 6616 "raw") .
952c65fc
JN
6617 " | " .
6618 $cgi->a({-href => href(action=>"blob",
6619 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 6620 "HEAD");
930cf7dd 6621 } else {
952c65fc 6622 $formats_nav .=
a3823e5a
JN
6623 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
6624 "raw");
930cf7dd 6625 }
847e01fb
JN
6626 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6627 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
930cf7dd
LT
6628 } else {
6629 print "<div class=\"page_nav\">\n" .
6630 "<br/><br/></div>\n" .
3017ed62 6631 "<div class=\"title\">".esc_html($hash)."</div>\n";
930cf7dd 6632 }
59fb1c94 6633 git_print_page_path($file_name, "blob", $hash_base);
930cf7dd 6634 print "<div class=\"page_body\">\n";
dfa7c7d2 6635 if ($mimetype =~ m!^image/!) {
3017ed62 6636 print qq!<img type="!.esc_attr($mimetype).qq!"!;
5a4cf334 6637 if ($file_name) {
3017ed62 6638 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
5a4cf334
JN
6639 }
6640 print qq! src="! .
6641 href(action=>"blob_plain", hash=>$hash,
6642 hash_base=>$hash_base, file_name=>$file_name) .
6643 qq!" />\n!;
dfa7c7d2
JN
6644 } else {
6645 my $nr;
6646 while (my $line = <$fd>) {
6647 chomp $line;
6648 $nr++;
6649 $line = untabify($line);
592ea417 6650 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
0866786b
JN
6651 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
6652 $syntax ? sanitize($line) : esc_html($line, -nbsp=>1);
dfa7c7d2 6653 }
930cf7dd 6654 }
952c65fc
JN
6655 close $fd
6656 or print "Reading blob failed.\n";
930cf7dd
LT
6657 print "</div>";
6658 git_footer_html();
6659}
6660
09bd7898 6661sub git_tree {
6f7ea5fb
LT
6662 if (!defined $hash_base) {
6663 $hash_base = "HEAD";
6664 }
b87d78d6 6665 if (!defined $hash) {
09bd7898 6666 if (defined $file_name) {
6f7ea5fb
LT
6667 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
6668 } else {
6669 $hash = $hash_base;
10dba28d 6670 }
e925f38c 6671 }
2d7a3532 6672 die_error(404, "No such tree") unless defined($hash);
34122b57 6673
e4b48eaa
JN
6674 my $show_sizes = gitweb_check_feature('show-sizes');
6675 my $have_blame = gitweb_check_feature('blame');
6676
34122b57
JN
6677 my @entries = ();
6678 {
6679 local $/ = "\0";
e4b48eaa
JN
6680 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
6681 ($show_sizes ? '-l' : ()), @extra_options, $hash
34122b57
JN
6682 or die_error(500, "Open git-ls-tree failed");
6683 @entries = map { chomp; $_ } <$fd>;
6684 close $fd
6685 or die_error(404, "Reading tree failed");
6686 }
d63577da 6687
847e01fb
JN
6688 my $refs = git_get_references();
6689 my $ref = format_ref_marker($refs, $hash_base);
12a88f2f 6690 git_header_html();
300454fe 6691 my $basedir = '';
847e01fb 6692 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
cae1862a
PB
6693 my @views_nav = ();
6694 if (defined $file_name) {
6695 push @views_nav,
a3823e5a 6696 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
6697 "history"),
6698 $cgi->a({-href => href(action=>"tree",
6699 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 6700 "HEAD"),
cae1862a 6701 }
a3c8ab30
MM
6702 my $snapshot_links = format_snapshot_links($hash);
6703 if (defined $snapshot_links) {
cae1862a 6704 # FIXME: Should be available when we have no hash base as well.
a3c8ab30 6705 push @views_nav, $snapshot_links;
cae1862a 6706 }
e4b48eaa
JN
6707 git_print_page_nav('tree','', $hash_base, undef, undef,
6708 join(' | ', @views_nav));
847e01fb 6709 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
d63577da 6710 } else {
fa702003 6711 undef $hash_base;
d63577da
KS
6712 print "<div class=\"page_nav\">\n";
6713 print "<br/><br/></div>\n";
3017ed62 6714 print "<div class=\"title\">".esc_html($hash)."</div>\n";
d63577da 6715 }
09bd7898 6716 if (defined $file_name) {
300454fe
JN
6717 $basedir = $file_name;
6718 if ($basedir ne '' && substr($basedir, -1) ne '/') {
6719 $basedir .= '/';
6720 }
2d7a3532 6721 git_print_page_path($file_name, 'tree', $hash_base);
09bd7898 6722 }
fbb592a9 6723 print "<div class=\"page_body\">\n";
591ebf65 6724 print "<table class=\"tree\">\n";
6dd36acd 6725 my $alternate = 1;
b6b7fc72
JN
6726 # '..' (top directory) link if possible
6727 if (defined $hash_base &&
6728 defined $file_name && $file_name =~ m![^/]+$!) {
6729 if ($alternate) {
6730 print "<tr class=\"dark\">\n";
6731 } else {
6732 print "<tr class=\"light\">\n";
6733 }
6734 $alternate ^= 1;
6735
6736 my $up = $file_name;
6737 $up =~ s!/?[^/]+$!!;
6738 undef $up unless $up;
6739 # based on git_print_tree_entry
6740 print '<td class="mode">' . mode_str('040000') . "</td>\n";
e4b48eaa 6741 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
b6b7fc72 6742 print '<td class="list">';
e4b48eaa
JN
6743 print $cgi->a({-href => href(action=>"tree",
6744 hash_base=>$hash_base,
b6b7fc72
JN
6745 file_name=>$up)},
6746 "..");
6747 print "</td>\n";
6748 print "<td class=\"link\"></td>\n";
6749
6750 print "</tr>\n";
6751 }
161332a5 6752 foreach my $line (@entries) {
e4b48eaa 6753 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
cb849b46 6754
bddec01d 6755 if ($alternate) {
c994d620 6756 print "<tr class=\"dark\">\n";
bddec01d 6757 } else {
c994d620 6758 print "<tr class=\"light\">\n";
bddec01d
KS
6759 }
6760 $alternate ^= 1;
cb849b46 6761
300454fe 6762 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
fa702003 6763
42f7eb94 6764 print "</tr>\n";
161332a5 6765 }
42f7eb94
KS
6766 print "</table>\n" .
6767 "</div>";
12a88f2f 6768 git_footer_html();
09bd7898
KS
6769}
6770
b629275f
MR
6771sub snapshot_name {
6772 my ($project, $hash) = @_;
6773
6774 # path/to/project.git -> project
6775 # path/to/project/.git -> project
6776 my $name = to_utf8($project);
6777 $name =~ s,([^/])/*\.git$,$1,;
6778 $name = basename($name);
6779 # sanitize name
6780 $name =~ s/[[:cntrl:]]/?/g;
6781
6782 my $ver = $hash;
6783 if ($hash =~ /^[0-9a-fA-F]+$/) {
6784 # shorten SHA-1 hash
6785 my $full_hash = git_get_full_hash($project, $hash);
6786 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
6787 $ver = git_get_short_hash($project, $hash);
6788 }
6789 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
6790 # tags don't need shortened SHA-1 hash
6791 $ver = $1;
6792 } else {
6793 # branches and other need shortened SHA-1 hash
6794 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
6795 $ver = $1;
6796 }
6797 $ver .= '-' . git_get_short_hash($project, $hash);
6798 }
6799 # in case of hierarchical branch names
6800 $ver =~ s!/!.!g;
6801
6802 # name = project-version_string
6803 $name = "$name-$ver";
6804
6805 return wantarray ? ($name, $name) : $name;
6806}
6807
cb9c6e5b 6808sub git_snapshot {
1b2d297e 6809 my $format = $input_params{'snapshot_format'};
5e166843 6810 if (!@snapshot_fmts) {
074afaa0 6811 die_error(403, "Snapshots not allowed");
3473e7df
JN
6812 }
6813 # default to first supported snapshot format
5e166843 6814 $format ||= $snapshot_fmts[0];
3473e7df 6815 if ($format !~ m/^[a-z0-9]+$/) {
074afaa0 6816 die_error(400, "Invalid snapshot format parameter");
3473e7df 6817 } elsif (!exists($known_snapshot_formats{$format})) {
074afaa0 6818 die_error(400, "Unknown snapshot format");
1bfd3631
MR
6819 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
6820 die_error(403, "Snapshot format not allowed");
34b31a8d
MR
6821 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
6822 die_error(403, "Unsupported snapshot format");
ddb8d900
AK
6823 }
6824
fdb0c36e
MR
6825 my $type = git_get_type("$hash^{}");
6826 if (!$type) {
6827 die_error(404, 'Object does not exist');
6828 } elsif ($type eq 'blob') {
6829 die_error(400, 'Object is not a tree-ish');
cb9c6e5b
AK
6830 }
6831
b629275f
MR
6832 my ($name, $prefix) = snapshot_name($project, $hash);
6833 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
6834 my $cmd = quote_command(
516381d5
LW
6835 git_cmd(), 'archive',
6836 "--format=$known_snapshot_formats{$format}{'format'}",
b629275f 6837 "--prefix=$prefix/", $hash);
a3c8ab30 6838 if (exists $known_snapshot_formats{$format}{'compressor'}) {
516381d5 6839 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
072570ee 6840 }
cb9c6e5b 6841
b629275f 6842 $filename =~ s/(["\\])/\\$1/g;
ab41dfbf 6843 print $cgi->header(
a3c8ab30 6844 -type => $known_snapshot_formats{$format}{'type'},
b629275f 6845 -content_disposition => 'inline; filename="' . $filename . '"',
ab41dfbf 6846 -status => '200 OK');
cb9c6e5b 6847
072570ee 6848 open my $fd, "-|", $cmd
074afaa0 6849 or die_error(500, "Execute git-archive failed");
cb9c6e5b
AK
6850 binmode STDOUT, ':raw';
6851 print <$fd>;
6852 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
6853 close $fd;
cb9c6e5b
AK
6854}
6855
15f0b112 6856sub git_log_generic {
69ca37d2 6857 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
15f0b112 6858
847e01fb 6859 my $head = git_get_head_hash($project);
69ca37d2
JN
6860 if (!defined $base) {
6861 $base = $head;
0db37973 6862 }
ea4a6df4
KS
6863 if (!defined $page) {
6864 $page = 0;
b87d78d6 6865 }
847e01fb 6866 my $refs = git_get_references();
ea4a6df4 6867
69ca37d2
JN
6868 my $commit_hash = $base;
6869 if (defined $parent) {
6870 $commit_hash = "$parent..$base";
6871 }
6872 my @commitlist =
6873 parse_commits($commit_hash, 101, (100 * $page),
6874 defined $file_name ? ($file_name, "--full-history") : ());
6875
6876 my $ftype;
6877 if (!defined $file_hash && defined $file_name) {
6878 # some commits could have deleted file in question,
6879 # and not have it in tree, but one of them has to have it
6880 for (my $i = 0; $i < @commitlist; $i++) {
6881 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
6882 last if defined $file_hash;
6883 }
6884 }
6885 if (defined $file_hash) {
6886 $ftype = git_get_type($file_hash);
6887 }
6888 if (defined $file_name && !defined $ftype) {
6889 die_error(500, "Unknown type of object");
6890 }
6891 my %co;
6892 if (defined $file_name) {
6893 %co = parse_commit($base)
6894 or die_error(404, "Unknown commit object");
15f0b112 6895 }
ea4a6df4 6896
69ca37d2
JN
6897
6898 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
15f0b112 6899 my $next_link = '';
42671caa
JN
6900 if ($#commitlist >= 100) {
6901 $next_link =
6902 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6903 -accesskey => "n", -title => "Alt-n"}, "next");
6904 }
15f0b112 6905 my $patch_max = gitweb_get_feature('patches');
69ca37d2 6906 if ($patch_max && !defined $file_name) {
75bf2cb2
GB
6907 if ($patch_max < 0 || @commitlist <= $patch_max) {
6908 $paging_nav .= " &sdot; " .
6909 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6910 "patches");
6911 }
6912 }
6913
0d83ddc4 6914 git_header_html();
15f0b112 6915 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
69ca37d2
JN
6916 if (defined $file_name) {
6917 git_print_header_div('commit', esc_html($co{'title'}), $base);
6918 } else {
6919 git_print_header_div('summary', $project)
6920 }
6921 git_print_page_path($file_name, $ftype, $hash_base)
6922 if (defined $file_name);
d16d093c 6923
69ca37d2
JN
6924 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
6925 $file_name, $file_hash, $ftype);
42671caa 6926
034df39e 6927 git_footer_html();
09bd7898
KS
6928}
6929
15f0b112 6930sub git_log {
69ca37d2
JN
6931 git_log_generic('log', \&git_log_body,
6932 $hash, $hash_parent);
15f0b112
JN
6933}
6934
09bd7898 6935sub git_commit {
9954f772 6936 $hash ||= $hash_base || "HEAD";
074afaa0
LW
6937 my %co = parse_commit($hash)
6938 or die_error(404, "Unknown commit object");
161332a5 6939
c9d193df
JN
6940 my $parent = $co{'parent'};
6941 my $parents = $co{'parents'}; # listref
6942
6943 # we need to prepare $formats_nav before any parameter munging
6944 my $formats_nav;
6945 if (!defined $parent) {
6946 # --root commitdiff
6947 $formats_nav .= '(initial)';
6948 } elsif (@$parents == 1) {
6949 # single parent commit
6950 $formats_nav .=
6951 '(parent: ' .
6952 $cgi->a({-href => href(action=>"commit",
6953 hash=>$parent)},
6954 esc_html(substr($parent, 0, 7))) .
6955 ')';
6956 } else {
6957 # merge commit
6958 $formats_nav .=
6959 '(merge: ' .
6960 join(' ', map {
f9308a18 6961 $cgi->a({-href => href(action=>"commit",
c9d193df
JN
6962 hash=>$_)},
6963 esc_html(substr($_, 0, 7)));
6964 } @$parents ) .
6965 ')';
6966 }
1655c987 6967 if (gitweb_check_feature('patches') && @$parents <= 1) {
75bf2cb2
GB
6968 $formats_nav .= " | " .
6969 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6970 "patch");
6971 }
c9d193df 6972
d8a20ba9 6973 if (!defined $parent) {
b9182987 6974 $parent = "--root";
6191f8e1 6975 }
549ab4a3 6976 my @difftree;
208ecb2e
JN
6977 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
6978 @diff_opts,
6979 (@$parents <= 1 ? $parent : '-c'),
6980 $hash, "--"
074afaa0 6981 or die_error(500, "Open git-diff-tree failed");
208ecb2e 6982 @difftree = map { chomp; $_ } <$fd>;
074afaa0 6983 close $fd or die_error(404, "Reading git-diff-tree failed");
11044297
KS
6984
6985 # non-textual hash id's can be cached
6986 my $expires;
6987 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6988 $expires = "+1d";
6989 }
847e01fb
JN
6990 my $refs = git_get_references();
6991 my $ref = format_ref_marker($refs, $co{'id'});
ddb8d900 6992
594e212b 6993 git_header_html(undef, $expires);
a144154f 6994 git_print_page_nav('commit', '',
952c65fc 6995 $hash, $co{'tree'}, $hash,
c9d193df 6996 $formats_nav);
4f7b34c9 6997
b87d78d6 6998 if (defined $co{'parent'}) {
847e01fb 6999 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
b87d78d6 7000 } else {
847e01fb 7001 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
b87d78d6 7002 }
6191f8e1 7003 print "<div class=\"title_text\">\n" .
591ebf65 7004 "<table class=\"object_header\">\n";
1c49a4e1 7005 git_print_authorship_rows(\%co);
1f1ab5f0 7006 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
bddec01d
KS
7007 print "<tr>" .
7008 "<td>tree</td>" .
1f1ab5f0 7009 "<td class=\"sha1\">" .
952c65fc
JN
7010 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7011 class => "list"}, $co{'tree'}) .
19806691 7012 "</td>" .
952c65fc
JN
7013 "<td class=\"link\">" .
7014 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7015 "tree");
a3c8ab30
MM
7016 my $snapshot_links = format_snapshot_links($hash);
7017 if (defined $snapshot_links) {
7018 print " | " . $snapshot_links;
cb9c6e5b
AK
7019 }
7020 print "</td>" .
bddec01d 7021 "</tr>\n";
549ab4a3 7022
3e029299 7023 foreach my $par (@$parents) {
bddec01d
KS
7024 print "<tr>" .
7025 "<td>parent</td>" .
952c65fc
JN
7026 "<td class=\"sha1\">" .
7027 $cgi->a({-href => href(action=>"commit", hash=>$par),
7028 class => "list"}, $par) .
7029 "</td>" .
bddec01d 7030 "<td class=\"link\">" .
1c2a4f5a 7031 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
952c65fc 7032 " | " .
f2e60947 7033 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
bddec01d
KS
7034 "</td>" .
7035 "</tr>\n";
3e029299 7036 }
7a9b4c5f 7037 print "</table>".
b87d78d6 7038 "</div>\n";
d16d093c 7039
fbb592a9 7040 print "<div class=\"page_body\">\n";
d16d093c 7041 git_print_log($co{'comment'});
927dcec4 7042 print "</div>\n";
4a4a1a53 7043
208ecb2e 7044 git_difftree_body(\@difftree, $hash, @$parents);
4a4a1a53 7045
12a88f2f 7046 git_footer_html();
09bd7898
KS
7047}
7048
ca94601c
JN
7049sub git_object {
7050 # object is defined by:
7051 # - hash or hash_base alone
7052 # - hash_base and file_name
7053 my $type;
7054
7055 # - hash or hash_base alone
7056 if ($hash || ($hash_base && !defined $file_name)) {
7057 my $object_id = $hash || $hash_base;
7058
516381d5
LW
7059 open my $fd, "-|", quote_command(
7060 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
074afaa0 7061 or die_error(404, "Object does not exist");
ca94601c
JN
7062 $type = <$fd>;
7063 chomp $type;
7064 close $fd
074afaa0 7065 or die_error(404, "Object does not exist");
ca94601c
JN
7066
7067 # - hash_base and file_name
7068 } elsif ($hash_base && defined $file_name) {
7069 $file_name =~ s,/+$,,;
7070
7071 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
074afaa0 7072 or die_error(404, "Base object does not exist");
ca94601c
JN
7073
7074 # here errors should not hapen
7075 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
074afaa0 7076 or die_error(500, "Open git-ls-tree failed");
ca94601c
JN
7077 my $line = <$fd>;
7078 close $fd;
7079
7080 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
7081 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
074afaa0 7082 die_error(404, "File or directory for given base does not exist");
ca94601c
JN
7083 }
7084 $type = $2;
7085 $hash = $3;
7086 } else {
074afaa0 7087 die_error(400, "Not enough information to find object");
ca94601c
JN
7088 }
7089
7090 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7091 hash=>$hash, hash_base=>$hash_base,
7092 file_name=>$file_name),
7093 -status => '302 Found');
7094}
7095
09bd7898 7096sub git_blobdiff {
9b71b1f6 7097 my $format = shift || 'html';
6ba1eb51 7098 my $diff_style = $input_params{'diff_style'} || 'inline';
9b71b1f6 7099
7c5e2ebb
JN
7100 my $fd;
7101 my @difftree;
7102 my %diffinfo;
9b71b1f6 7103 my $expires;
7c5e2ebb
JN
7104
7105 # preparing $fd and %diffinfo for git_patchset_body
7106 # new style URI
7107 if (defined $hash_base && defined $hash_parent_base) {
7108 if (defined $file_name) {
7109 # read raw output
45bd0c80
JN
7110 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7111 $hash_parent_base, $hash_base,
5ae917ac 7112 "--", (defined $file_parent ? $file_parent : ()), $file_name
074afaa0 7113 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
7114 @difftree = map { chomp; $_ } <$fd>;
7115 close $fd
074afaa0 7116 or die_error(404, "Reading git-diff-tree failed");
7c5e2ebb 7117 @difftree
074afaa0 7118 or die_error(404, "Blob diff not found");
7c5e2ebb 7119
0aea3376
JN
7120 } elsif (defined $hash &&
7121 $hash =~ /[0-9a-fA-F]{40}/) {
7122 # try to find filename from $hash
7c5e2ebb
JN
7123
7124 # read filtered raw output
45bd0c80
JN
7125 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7126 $hash_parent_base, $hash_base, "--"
074afaa0 7127 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
7128 @difftree =
7129 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
7130 # $hash == to_id
7131 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7132 map { chomp; $_ } <$fd>;
7133 close $fd
074afaa0 7134 or die_error(404, "Reading git-diff-tree failed");
7c5e2ebb 7135 @difftree
074afaa0 7136 or die_error(404, "Blob diff not found");
7c5e2ebb
JN
7137
7138 } else {
074afaa0 7139 die_error(400, "Missing one of the blob diff parameters");
7c5e2ebb
JN
7140 }
7141
7142 if (@difftree > 1) {
074afaa0 7143 die_error(400, "Ambiguous blob diff specification");
7c5e2ebb
JN
7144 }
7145
7146 %diffinfo = parse_difftree_raw_line($difftree[0]);
9d301456
JN
7147 $file_parent ||= $diffinfo{'from_file'} || $file_name;
7148 $file_name ||= $diffinfo{'to_file'};
7c5e2ebb
JN
7149
7150 $hash_parent ||= $diffinfo{'from_id'};
7151 $hash ||= $diffinfo{'to_id'};
7152
9b71b1f6
JN
7153 # non-textual hash id's can be cached
7154 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
7155 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
7156 $expires = '+1d';
7157 }
7158
7c5e2ebb 7159 # open patch output
25691fbe 7160 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
957d6ea7
JN
7161 '-p', ($format eq 'html' ? "--full-index" : ()),
7162 $hash_parent_base, $hash_base,
5ae917ac 7163 "--", (defined $file_parent ? $file_parent : ()), $file_name
074afaa0 7164 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
7165 }
7166
b54dc9fd
JH
7167 # old/legacy style URI -- not generated anymore since 1.4.3.
7168 if (!%diffinfo) {
7169 die_error('404 Not Found', "Missing one of the blob diff parameters")
7c5e2ebb
JN
7170 }
7171
7172 # header
9b71b1f6
JN
7173 if ($format eq 'html') {
7174 my $formats_nav =
a3823e5a 7175 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
35329cc1 7176 "raw");
6ae683c0 7177 $formats_nav .= diff_style_nav($diff_style);
9b71b1f6
JN
7178 git_header_html(undef, $expires);
7179 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7180 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7181 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7182 } else {
7183 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
3017ed62 7184 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
9b71b1f6
JN
7185 }
7186 if (defined $file_name) {
7187 git_print_page_path($file_name, "blob", $hash_base);
7188 } else {
7189 print "<div class=\"page_path\"></div>\n";
7190 }
7191
7192 } elsif ($format eq 'plain') {
7193 print $cgi->header(
7194 -type => 'text/plain',
7195 -charset => 'utf-8',
7196 -expires => $expires,
a2a3bf7b 7197 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
9b71b1f6
JN
7198
7199 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7200
7c5e2ebb 7201 } else {
074afaa0 7202 die_error(400, "Unknown blobdiff format");
7c5e2ebb
JN
7203 }
7204
7205 # patch
9b71b1f6
JN
7206 if ($format eq 'html') {
7207 print "<div class=\"page_body\">\n";
7c5e2ebb 7208
6ba1eb51
KK
7209 git_patchset_body($fd, $diff_style,
7210 [ \%diffinfo ], $hash_base, $hash_parent_base);
9b71b1f6 7211 close $fd;
7c5e2ebb 7212
9b71b1f6
JN
7213 print "</div>\n"; # class="page_body"
7214 git_footer_html();
7215
7216 } else {
7217 while (my $line = <$fd>) {
403d0906
JN
7218 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7219 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9b71b1f6
JN
7220
7221 print $line;
7222
7223 last if $line =~ m!^\+\+\+!;
7224 }
7225 local $/ = undef;
7226 print <$fd>;
7227 close $fd;
7228 }
09bd7898
KS
7229}
7230
19806691 7231sub git_blobdiff_plain {
9b71b1f6 7232 git_blobdiff('plain');
19806691
KS
7233}
7234
6ae683c0
KK
7235# assumes that it is added as later part of already existing navigation,
7236# so it returns "| foo | bar" rather than just "foo | bar"
7237sub diff_style_nav {
7238 my ($diff_style, $is_combined) = @_;
7239 $diff_style ||= 'inline';
7240
7241 return "" if ($is_combined);
7242
7243 my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7244 my %styles = @styles;
7245 @styles =
7246 @styles[ map { $_ * 2 } 0..$#styles/2 ];
7247
7248 return join '',
7249 map { " | ".$_ }
7250 map {
7251 $_ eq $diff_style ? $styles{$_} :
7252 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7253 } @styles;
7254}
7255
09bd7898 7256sub git_commitdiff {
20209854
GB
7257 my %params = @_;
7258 my $format = $params{-format} || 'html';
6ba1eb51 7259 my $diff_style = $input_params{'diff_style'} || 'inline';
9872cd6f 7260
75bf2cb2 7261 my ($patch_max) = gitweb_get_feature('patches');
9872cd6f 7262 if ($format eq 'patch') {
9872cd6f
GB
7263 die_error(403, "Patch view not allowed") unless $patch_max;
7264 }
7265
9954f772 7266 $hash ||= $hash_base || "HEAD";
074afaa0
LW
7267 my %co = parse_commit($hash)
7268 or die_error(404, "Unknown commit object");
151602df 7269
cd030c3a
JN
7270 # choose format for commitdiff for merge
7271 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
7272 $hash_parent = '--cc';
7273 }
7274 # we need to prepare $formats_nav before almost any parameter munging
151602df
JN
7275 my $formats_nav;
7276 if ($format eq 'html') {
7277 $formats_nav =
a3823e5a 7278 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
151602df 7279 "raw");
1655c987 7280 if ($patch_max && @{$co{'parents'}} <= 1) {
75bf2cb2
GB
7281 $formats_nav .= " | " .
7282 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7283 "patch");
7284 }
6ae683c0 7285 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
151602df 7286
cd030c3a
JN
7287 if (defined $hash_parent &&
7288 $hash_parent ne '-c' && $hash_parent ne '--cc') {
151602df
JN
7289 # commitdiff with two commits given
7290 my $hash_parent_short = $hash_parent;
7291 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
7292 $hash_parent_short = substr($hash_parent, 0, 7);
7293 }
7294 $formats_nav .=
ada3e1f7
JN
7295 ' (from';
7296 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
7297 if ($co{'parents'}[$i] eq $hash_parent) {
7298 $formats_nav .= ' parent ' . ($i+1);
7299 last;
7300 }
7301 }
7302 $formats_nav .= ': ' .
d0e6e29e
JN
7303 $cgi->a({-href => href(-replay=>1,
7304 hash=>$hash_parent, hash_base=>undef)},
151602df
JN
7305 esc_html($hash_parent_short)) .
7306 ')';
7307 } elsif (!$co{'parent'}) {
7308 # --root commitdiff
7309 $formats_nav .= ' (initial)';
7310 } elsif (scalar @{$co{'parents'}} == 1) {
7311 # single parent commit
7312 $formats_nav .=
7313 ' (parent: ' .
d0e6e29e
JN
7314 $cgi->a({-href => href(-replay=>1,
7315 hash=>$co{'parent'}, hash_base=>undef)},
151602df
JN
7316 esc_html(substr($co{'parent'}, 0, 7))) .
7317 ')';
7318 } else {
7319 # merge commit
cd030c3a
JN
7320 if ($hash_parent eq '--cc') {
7321 $formats_nav .= ' | ' .
d0e6e29e 7322 $cgi->a({-href => href(-replay=>1,
cd030c3a
JN
7323 hash=>$hash, hash_parent=>'-c')},
7324 'combined');
7325 } else { # $hash_parent eq '-c'
7326 $formats_nav .= ' | ' .
d0e6e29e 7327 $cgi->a({-href => href(-replay=>1,
cd030c3a
JN
7328 hash=>$hash, hash_parent=>'--cc')},
7329 'compact');
7330 }
151602df
JN
7331 $formats_nav .=
7332 ' (merge: ' .
7333 join(' ', map {
d0e6e29e
JN
7334 $cgi->a({-href => href(-replay=>1,
7335 hash=>$_, hash_base=>undef)},
151602df
JN
7336 esc_html(substr($_, 0, 7)));
7337 } @{$co{'parents'}} ) .
7338 ')';
7339 }
7340 }
7341
fb1dde4a 7342 my $hash_parent_param = $hash_parent;
cd030c3a
JN
7343 if (!defined $hash_parent_param) {
7344 # --cc for multiple parents, --root for parentless
fb1dde4a 7345 $hash_parent_param =
cd030c3a 7346 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
bddec01d 7347 }
eee08903
JN
7348
7349 # read commitdiff
7350 my $fd;
7351 my @difftree;
eee08903 7352 if ($format eq 'html') {
25691fbe 7353 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
45bd0c80 7354 "--no-commit-id", "--patch-with-raw", "--full-index",
fb1dde4a 7355 $hash_parent_param, $hash, "--"
074afaa0 7356 or die_error(500, "Open git-diff-tree failed");
eee08903 7357
04408c35
JN
7358 while (my $line = <$fd>) {
7359 chomp $line;
eee08903
JN
7360 # empty line ends raw part of diff-tree output
7361 last unless $line;
493e01db 7362 push @difftree, scalar parse_difftree_raw_line($line);
eee08903 7363 }
eee08903 7364
eee08903 7365 } elsif ($format eq 'plain') {
25691fbe 7366 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
fb1dde4a 7367 '-p', $hash_parent_param, $hash, "--"
074afaa0 7368 or die_error(500, "Open git-diff-tree failed");
9872cd6f
GB
7369 } elsif ($format eq 'patch') {
7370 # For commit ranges, we limit the output to the number of
7371 # patches specified in the 'patches' feature.
7372 # For single commits, we limit the output to a single patch,
7373 # diverging from the git-format-patch default.
7374 my @commit_spec = ();
7375 if ($hash_parent) {
7376 if ($patch_max > 0) {
7377 push @commit_spec, "-$patch_max";
7378 }
7379 push @commit_spec, '-n', "$hash_parent..$hash";
7380 } else {
a3411f8a
GB
7381 if ($params{-single}) {
7382 push @commit_spec, '-1';
7383 } else {
7384 if ($patch_max > 0) {
7385 push @commit_spec, "-$patch_max";
7386 }
7387 push @commit_spec, "-n";
7388 }
7389 push @commit_spec, '--root', $hash;
9872cd6f 7390 }
04794fdc
PKS
7391 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
7392 '--encoding=utf8', '--stdout', @commit_spec
9872cd6f 7393 or die_error(500, "Open git-format-patch failed");
eee08903 7394 } else {
074afaa0 7395 die_error(400, "Unknown commitdiff format");
eee08903 7396 }
161332a5 7397
11044297
KS
7398 # non-textual hash id's can be cached
7399 my $expires;
7400 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7401 $expires = "+1d";
7402 }
09bd7898 7403
eee08903
JN
7404 # write commit message
7405 if ($format eq 'html') {
7406 my $refs = git_get_references();
7407 my $ref = format_ref_marker($refs, $co{'id'});
1b1cd421 7408
eee08903
JN
7409 git_header_html(undef, $expires);
7410 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
7411 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
f88bafad
GB
7412 print "<div class=\"title_text\">\n" .
7413 "<table class=\"object_header\">\n";
7414 git_print_authorship_rows(\%co);
7415 print "</table>".
7416 "</div>\n";
eee08903 7417 print "<div class=\"page_body\">\n";
82560983
JN
7418 if (@{$co{'comment'}} > 1) {
7419 print "<div class=\"log\">\n";
7420 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
7421 print "</div>\n"; # class="log"
7422 }
eee08903
JN
7423
7424 } elsif ($format eq 'plain') {
7425 my $refs = git_get_references("tags");
edf735ab 7426 my $tagname = git_get_rev_name_tags($hash);
eee08903
JN
7427 my $filename = basename($project) . "-$hash.patch";
7428
7429 print $cgi->header(
7430 -type => 'text/plain',
7431 -charset => 'utf-8',
7432 -expires => $expires,
a2a3bf7b 7433 -content_disposition => 'inline; filename="' . "$filename" . '"');
eee08903 7434 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7720224c
YS
7435 print "From: " . to_utf8($co{'author'}) . "\n";
7436 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
7437 print "Subject: " . to_utf8($co{'title'}) . "\n";
7438
edf735ab 7439 print "X-Git-Tag: $tagname\n" if $tagname;
eee08903 7440 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
edf735ab 7441
eee08903 7442 foreach my $line (@{$co{'comment'}}) {
7720224c 7443 print to_utf8($line) . "\n";
eee08903
JN
7444 }
7445 print "---\n\n";
9872cd6f
GB
7446 } elsif ($format eq 'patch') {
7447 my $filename = basename($project) . "-$hash.patch";
7448
7449 print $cgi->header(
7450 -type => 'text/plain',
7451 -charset => 'utf-8',
7452 -expires => $expires,
7453 -content_disposition => 'inline; filename="' . "$filename" . '"');
1b1cd421 7454 }
1b1cd421 7455
eee08903
JN
7456 # write patch
7457 if ($format eq 'html') {
cd030c3a
JN
7458 my $use_parents = !defined $hash_parent ||
7459 $hash_parent eq '-c' || $hash_parent eq '--cc';
7460 git_difftree_body(\@difftree, $hash,
7461 $use_parents ? @{$co{'parents'}} : $hash_parent);
b4657e77 7462 print "<br/>\n";
1b1cd421 7463
6ba1eb51
KK
7464 git_patchset_body($fd, $diff_style,
7465 \@difftree, $hash,
cd030c3a 7466 $use_parents ? @{$co{'parents'}} : $hash_parent);
157e43b4 7467 close $fd;
eee08903
JN
7468 print "</div>\n"; # class="page_body"
7469 git_footer_html();
7470
7471 } elsif ($format eq 'plain') {
7472 local $/ = undef;
7473 print <$fd>;
7474 close $fd
7475 or print "Reading git-diff-tree failed\n";
9872cd6f
GB
7476 } elsif ($format eq 'patch') {
7477 local $/ = undef;
7478 print <$fd>;
7479 close $fd
7480 or print "Reading git-format-patch failed\n";
19806691
KS
7481 }
7482}
7483
eee08903 7484sub git_commitdiff_plain {
20209854 7485 git_commitdiff(-format => 'plain');
eee08903
JN
7486}
7487
9872cd6f
GB
7488# format-patch-style patches
7489sub git_patch {
1655c987 7490 git_commitdiff(-format => 'patch', -single => 1);
a3411f8a
GB
7491}
7492
7493sub git_patches {
20209854 7494 git_commitdiff(-format => 'patch');
eee08903
JN
7495}
7496
09bd7898 7497sub git_history {
69ca37d2
JN
7498 git_log_generic('history', \&git_history_body,
7499 $hash_base, $hash_parent_base,
7500 $file_name, $hash);
161332a5 7501}
19806691
KS
7502
7503sub git_search {
e0ca3645
JN
7504 $searchtype ||= 'commit';
7505
7506 # check if appropriate features are enabled
7507 gitweb_check_feature('search')
7508 or die_error(403, "Search is disabled");
7509 if ($searchtype eq 'pickaxe') {
7510 # pickaxe may take all resources of your box and run for several minutes
7511 # with every query - so decide by yourself how public you make this feature
7512 gitweb_check_feature('pickaxe')
7513 or die_error(403, "Pickaxe search is disabled");
7514 }
7515 if ($searchtype eq 'grep') {
7516 # grep search might be potentially CPU-intensive, too
7517 gitweb_check_feature('grep')
7518 or die_error(403, "Grep search is disabled");
7519 }
7520
19806691 7521 if (!defined $searchtext) {
074afaa0 7522 die_error(400, "Text field is empty");
19806691
KS
7523 }
7524 if (!defined $hash) {
847e01fb 7525 $hash = git_get_head_hash($project);
19806691 7526 }
847e01fb 7527 my %co = parse_commit($hash);
19806691 7528 if (!%co) {
074afaa0 7529 die_error(404, "Unknown commit object");
19806691 7530 }
8dbc0fce
RF
7531 if (!defined $page) {
7532 $page = 0;
7533 }
04f7a94f 7534
16f20725
JN
7535 if ($searchtype eq 'commit' ||
7536 $searchtype eq 'author' ||
7537 $searchtype eq 'committer') {
7538 git_search_message(%co);
7539 } elsif ($searchtype eq 'pickaxe') {
7540 git_search_changes(%co);
7541 } elsif ($searchtype eq 'grep') {
7542 git_search_files(%co);
1ae05be4
JN
7543 } else {
7544 die_error(400, "Unknown search type");
e7738553 7545 }
19806691
KS
7546}
7547
88ad729b
PB
7548sub git_search_help {
7549 git_header_html();
7550 git_print_page_nav('','', $hash,$hash,$hash);
7551 print <<EOT;
0e559919
PB
7552<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
7553regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
7554the pattern entered is recognized as the POSIX extended
7555<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
7556insensitive).</p>
88ad729b
PB
7557<dl>
7558<dt><b>commit</b></dt>
0e559919 7559<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
e7738553 7560EOT
25b2790f 7561 my $have_grep = gitweb_check_feature('grep');
e7738553
PB
7562 if ($have_grep) {
7563 print <<EOT;
7564<dt><b>grep</b></dt>
7565<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
0e559919
PB
7566 a different one) are searched for the given pattern. On large trees, this search can take
7567a while and put some strain on the server, so please use it with some consideration. Note that
7568due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
7569case-sensitive.</dd>
e7738553
PB
7570EOT
7571 }
7572 print <<EOT;
88ad729b 7573<dt><b>author</b></dt>
0e559919 7574<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 7575<dt><b>committer</b></dt>
0e559919 7576<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
88ad729b 7577EOT
25b2790f 7578 my $have_pickaxe = gitweb_check_feature('pickaxe');
88ad729b
PB
7579 if ($have_pickaxe) {
7580 print <<EOT;
7581<dt><b>pickaxe</b></dt>
7582<dd>All commits that caused the string to appear or disappear from any file (changes that
7583added, removed or "modified" the string) will be listed. This search can take a while and
0e559919
PB
7584takes a lot of strain on the server, so please use it wisely. Note that since you may be
7585interested even in changes just changing the case as well, this search is case sensitive.</dd>
88ad729b
PB
7586EOT
7587 }
7588 print "</dl>\n";
7589 git_footer_html();
7590}
7591
19806691 7592sub git_shortlog {
69ca37d2
JN
7593 git_log_generic('shortlog', \&git_shortlog_body,
7594 $hash, $hash_parent);
19806691 7595}
717b8311
JN
7596
7597## ......................................................................
af6feeb2 7598## feeds (RSS, Atom; OPML)
717b8311 7599
af6feeb2
JN
7600sub git_feed {
7601 my $format = shift || 'atom';
25b2790f 7602 my $have_blame = gitweb_check_feature('blame');
af6feeb2
JN
7603
7604 # Atom: http://www.atomenabled.org/developers/syndication/
7605 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
7606 if ($format ne 'rss' && $format ne 'atom') {
074afaa0 7607 die_error(400, "Unknown web feed format");
af6feeb2
JN
7608 }
7609
7610 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
7611 my $head = $hash || 'HEAD';
311e552e 7612 my @commitlist = parse_commits($head, 150, 0, $file_name);
af6feeb2
JN
7613
7614 my %latest_commit;
7615 my %latest_date;
7616 my $content_type = "application/$format+xml";
7617 if (defined $cgi->http('HTTP_ACCEPT') &&
7618 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
7619 # browser (feed reader) prefers text/xml
7620 $content_type = 'text/xml';
7621 }
b6093a5c
RF
7622 if (defined($commitlist[0])) {
7623 %latest_commit = %{$commitlist[0]};
cd956c73 7624 my $latest_epoch = $latest_commit{'committer_epoch'};
6368d9f1 7625 %latest_date = parse_date($latest_epoch, $latest_commit{'comitter_tz'});
cd956c73
GB
7626 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7627 if (defined $if_modified) {
7628 my $since;
7629 if (eval { require HTTP::Date; 1; }) {
7630 $since = HTTP::Date::str2time($if_modified);
7631 } elsif (eval { require Time::ParseDate; 1; }) {
7632 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7633 }
7634 if (defined $since && $latest_epoch <= $since) {
7635 print $cgi->header(
7636 -type => $content_type,
7637 -charset => 'utf-8',
7638 -last_modified => $latest_date{'rfc2822'},
7639 -status => '304 Not Modified');
7640 return;
7641 }
7642 }
af6feeb2
JN
7643 print $cgi->header(
7644 -type => $content_type,
7645 -charset => 'utf-8',
7646 -last_modified => $latest_date{'rfc2822'});
7647 } else {
7648 print $cgi->header(
7649 -type => $content_type,
7650 -charset => 'utf-8');
7651 }
7652
7653 # Optimization: skip generating the body if client asks only
7654 # for Last-Modified date.
7655 return if ($cgi->request_method() eq 'HEAD');
7656
7657 # header variables
7658 my $title = "$site_name - $project/$action";
7659 my $feed_type = 'log';
7660 if (defined $hash) {
7661 $title .= " - '$hash'";
7662 $feed_type = 'branch log';
7663 if (defined $file_name) {
7664 $title .= " :: $file_name";
7665 $feed_type = 'history';
7666 }
7667 } elsif (defined $file_name) {
7668 $title .= " - $file_name";
7669 $feed_type = 'history';
7670 }
7671 $title .= " $feed_type";
7672 my $descr = git_get_project_description($project);
7673 if (defined $descr) {
7674 $descr = esc_html($descr);
7675 } else {
7676 $descr = "$project " .
7677 ($format eq 'rss' ? 'RSS' : 'Atom') .
7678 " feed";
7679 }
7680 my $owner = git_get_project_owner($project);
7681 $owner = esc_html($owner);
7682
7683 #header
7684 my $alt_url;
7685 if (defined $file_name) {
7686 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
7687 } elsif (defined $hash) {
7688 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
7689 } else {
7690 $alt_url = href(-full=>1, action=>"summary");
7691 }
7692 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
7693 if ($format eq 'rss') {
7694 print <<XML;
59b9f61a
JN
7695<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
7696<channel>
59b9f61a 7697XML
af6feeb2
JN
7698 print "<title>$title</title>\n" .
7699 "<link>$alt_url</link>\n" .
7700 "<description>$descr</description>\n" .
3ac109ae
GB
7701 "<language>en</language>\n" .
7702 # project owner is responsible for 'editorial' content
7703 "<managingEditor>$owner</managingEditor>\n";
1ba68ce2
GB
7704 if (defined $logo || defined $favicon) {
7705 # prefer the logo to the favicon, since RSS
7706 # doesn't allow both
7707 my $img = esc_url($logo || $favicon);
7708 print "<image>\n" .
7709 "<url>$img</url>\n" .
7710 "<title>$title</title>\n" .
7711 "<link>$alt_url</link>\n" .
7712 "</image>\n";
7713 }
0cf31285
GB
7714 if (%latest_date) {
7715 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
7716 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
7717 }
ad59a7a3 7718 print "<generator>gitweb v.$version/$git_version</generator>\n";
af6feeb2
JN
7719 } elsif ($format eq 'atom') {
7720 print <<XML;
7721<feed xmlns="http://www.w3.org/2005/Atom">
7722XML
7723 print "<title>$title</title>\n" .
7724 "<subtitle>$descr</subtitle>\n" .
7725 '<link rel="alternate" type="text/html" href="' .
7726 $alt_url . '" />' . "\n" .
7727 '<link rel="self" type="' . $content_type . '" href="' .
7728 $cgi->self_url() . '" />' . "\n" .
7729 "<id>" . href(-full=>1) . "</id>\n" .
7730 # use project owner for feed author
7731 "<author><name>$owner</name></author>\n";
7732 if (defined $favicon) {
7733 print "<icon>" . esc_url($favicon) . "</icon>\n";
7734 }
9d9f5e72 7735 if (defined $logo) {
af6feeb2 7736 # not twice as wide as tall: 72 x 27 pixels
e1147267 7737 print "<logo>" . esc_url($logo) . "</logo>\n";
af6feeb2
JN
7738 }
7739 if (! %latest_date) {
7740 # dummy date to keep the feed valid until commits trickle in:
7741 print "<updated>1970-01-01T00:00:00Z</updated>\n";
7742 } else {
7743 print "<updated>$latest_date{'iso-8601'}</updated>\n";
7744 }
ad59a7a3 7745 print "<generator version='$version/$git_version'>gitweb</generator>\n";
af6feeb2 7746 }
717b8311 7747
af6feeb2 7748 # contents
b6093a5c
RF
7749 for (my $i = 0; $i <= $#commitlist; $i++) {
7750 my %co = %{$commitlist[$i]};
7751 my $commit = $co{'id'};
717b8311 7752 # we read 150, we always show 30 and the ones more recent than 48 hours
91fd2bf3 7753 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
717b8311
JN
7754 last;
7755 }
6368d9f1 7756 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
af6feeb2
JN
7757
7758 # get list of changed files
b6093a5c 7759 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
c906b181
JN
7760 $co{'parent'} || "--root",
7761 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6bcf4b46 7762 or next;
717b8311 7763 my @difftree = map { chomp; $_ } <$fd>;
6bcf4b46
JN
7764 close $fd
7765 or next;
af6feeb2
JN
7766
7767 # print element (entry, item)
e62a641d 7768 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
af6feeb2
JN
7769 if ($format eq 'rss') {
7770 print "<item>\n" .
7771 "<title>" . esc_html($co{'title'}) . "</title>\n" .
7772 "<author>" . esc_html($co{'author'}) . "</author>\n" .
7773 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
7774 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
7775 "<link>$co_url</link>\n" .
7776 "<description>" . esc_html($co{'title'}) . "</description>\n" .
7777 "<content:encoded>" .
7778 "<![CDATA[\n";
7779 } elsif ($format eq 'atom') {
7780 print "<entry>\n" .
7781 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
7782 "<updated>$cd{'iso-8601'}</updated>\n" .
ab23c19d
JN
7783 "<author>\n" .
7784 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
7785 if ($co{'author_email'}) {
7786 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
7787 }
7788 print "</author>\n" .
af6feeb2 7789 # use committer for contributor
ab23c19d
JN
7790 "<contributor>\n" .
7791 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
7792 if ($co{'committer_email'}) {
7793 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
7794 }
7795 print "</contributor>\n" .
af6feeb2
JN
7796 "<published>$cd{'iso-8601'}</published>\n" .
7797 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
7798 "<id>$co_url</id>\n" .
7799 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
7800 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
7801 }
717b8311 7802 my $comment = $co{'comment'};
af6feeb2 7803 print "<pre>\n";
717b8311 7804 foreach my $line (@$comment) {
af6feeb2
JN
7805 $line = esc_html($line);
7806 print "$line\n";
717b8311 7807 }
af6feeb2
JN
7808 print "</pre><ul>\n";
7809 foreach my $difftree_line (@difftree) {
7810 my %difftree = parse_difftree_raw_line($difftree_line);
7811 next if !$difftree{'from_id'};
7812
7813 my $file = $difftree{'file'} || $difftree{'to_file'};
7814
7815 print "<li>" .
7816 "[" .
7817 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
7818 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
7819 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
7820 file_name=>$file, file_parent=>$difftree{'from_file'}),
7821 -title => "diff"}, 'D');
7822 if ($have_blame) {
7823 print $cgi->a({-href => href(-full=>1, action=>"blame",
7824 file_name=>$file, hash_base=>$commit),
7825 -title => "blame"}, 'B');
717b8311 7826 }
af6feeb2
JN
7827 # if this is not a feed of a file history
7828 if (!defined $file_name || $file_name ne $file) {
7829 print $cgi->a({-href => href(-full=>1, action=>"history",
7830 file_name=>$file, hash=>$commit),
7831 -title => "history"}, 'H');
7832 }
7833 $file = esc_path($file);
7834 print "] ".
7835 "$file</li>\n";
7836 }
7837 if ($format eq 'rss') {
7838 print "</ul>]]>\n" .
7839 "</content:encoded>\n" .
7840 "</item>\n";
7841 } elsif ($format eq 'atom') {
7842 print "</ul>\n</div>\n" .
7843 "</content>\n" .
7844 "</entry>\n";
717b8311 7845 }
717b8311 7846 }
af6feeb2
JN
7847
7848 # end of feed
7849 if ($format eq 'rss') {
7850 print "</channel>\n</rss>\n";
3278fbc5 7851 } elsif ($format eq 'atom') {
af6feeb2
JN
7852 print "</feed>\n";
7853 }
7854}
7855
7856sub git_rss {
7857 git_feed('rss');
7858}
7859
7860sub git_atom {
7861 git_feed('atom');
717b8311
JN
7862}
7863
7864sub git_opml {
847e01fb 7865 my @list = git_get_projects_list();
12b1443c
JN
7866 if (!@list) {
7867 die_error(404, "No projects found");
7868 }
717b8311 7869
ae35785e
GB
7870 print $cgi->header(
7871 -type => 'text/xml',
7872 -charset => 'utf-8',
7873 -content_disposition => 'inline; filename="opml.xml"');
7874
5d791056 7875 my $title = esc_html($site_name);
59b9f61a
JN
7876 print <<XML;
7877<?xml version="1.0" encoding="utf-8"?>
7878<opml version="1.0">
7879<head>
5d791056 7880 <title>$title OPML Export</title>
59b9f61a
JN
7881</head>
7882<body>
7883<outline text="git RSS feeds">
7884XML
717b8311
JN
7885
7886 foreach my $pr (@list) {
7887 my %proj = %$pr;
847e01fb 7888 my $head = git_get_head_hash($proj{'path'});
717b8311
JN
7889 if (!defined $head) {
7890 next;
7891 }
25691fbe 7892 $git_dir = "$projectroot/$proj{'path'}";
847e01fb 7893 my %co = parse_commit($head);
717b8311
JN
7894 if (!%co) {
7895 next;
7896 }
7897
7898 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
df63fbbf
GB
7899 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7900 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
717b8311
JN
7901 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7902 }
59b9f61a
JN
7903 print <<XML;
7904</outline>
7905</body>
7906</opml>
7907XML
717b8311 7908}