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