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