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