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