gitweb.perl 104.7 KB
Newer Older
K
Kay Sievers 已提交
1 2
#!/usr/bin/perl

K
v220  
Kay Sievers 已提交
3
# gitweb - simple web interface to track changes in git repositories
K
v004  
Kay Sievers 已提交
4
#
K
Kay Sievers 已提交
5 6
# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
# (C) 2005, Christian Gierke
K
v005  
Kay Sievers 已提交
7
#
K
Kay Sievers 已提交
8
# This program is licensed under the GPLv2
K
Kay Sievers 已提交
9 10 11

use strict;
use warnings;
K
v203  
Kay Sievers 已提交
12
use CGI qw(:standard :escapeHTML -nosticky);
K
v148  
Kay Sievers 已提交
13
use CGI::Util qw(unescape);
K
Kay Sievers 已提交
14
use CGI::Carp qw(fatalsToBrowser);
15
use Encode;
K
v107  
Kay Sievers 已提交
16
use Fcntl ':mode';
17
use File::Find qw();
A
Aneesh Kumar K.V 已提交
18
use File::Basename qw(basename);
K
Kay Sievers 已提交
19
binmode STDOUT, ':utf8';
K
Kay Sievers 已提交
20

21
our $cgi = new CGI;
22
our $version = "++GIT_VERSION++";
23 24
our $my_url = $cgi->url();
our $my_uri = $cgi->url(-absolute => 1);
K
v025  
Kay Sievers 已提交
25

26 27
# core git executable to use
# this can just be "git" if your webserver has a sensible PATH
28
our $GIT = "++GIT_BINDIR++/git";
29

K
v107  
Kay Sievers 已提交
30
# absolute fs-path which will be prepended to the project path
31
#our $projectroot = "/pub/scm";
32
our $projectroot = "++GITWEB_PROJECTROOT++";
K
v107  
Kay Sievers 已提交
33 34

# target of the home link on top of all pages
35
our $home_link = $my_uri || "/";
K
v107  
Kay Sievers 已提交
36

37 38 39
# string of the home link on top of all pages
our $home_link_str = "++GITWEB_HOME_LINK_STR++";

40 41
# name of your site or organization to appear in page titles
# replace this with something more descriptive for clearer bookmarks
42
our $site_name = "++GITWEB_SITENAME++" || $ENV{'SERVER_NAME'} || "Untitled";
43

K
v136  
Kay Sievers 已提交
44
# html text to include at home page
45
our $home_text = "++GITWEB_HOMETEXT++";
K
v136  
Kay Sievers 已提交
46

J
Jakub Narebski 已提交
47
# URI of default stylesheet
48
our $stylesheet = "++GITWEB_CSS++";
M
Martin Waitz 已提交
49
# URI of GIT logo
50
our $logo = "++GITWEB_LOGO++";
51 52
# URI of GIT favicon, assumed to be image/png type
our $favicon = "++GITWEB_FAVICON++";
J
Jakub Narebski 已提交
53

54 55 56
our $githelp_url = "http://git.or.cz/";
our $githelp_label = "git homepage";

K
v118  
Kay Sievers 已提交
57
# source of projects list
58
our $projects_list = "++GITWEB_LIST++";
K
v107  
Kay Sievers 已提交
59

M
Matthias Lederhofer 已提交
60 61 62 63 64 65 66
# show repository only if this file exists
# (only effective if this variable evaluates to true)
our $export_ok = "++GITWEB_EXPORT_OK++";

# only allow viewing of repositories also shown on the overview page
our $strict_export = "++GITWEB_STRICT_EXPORT++";

67 68 69 70
# list of git base URLs used for URL to where fetch project from,
# i.e. full URL is "$git_base_url/$project"
our @git_base_url_list = ("++GITWEB_BASE_URL++");

71
# default blob_plain mimetype and default charset for text/plain blob
72 73
our $default_blob_plain_mimetype = 'text/plain';
our $default_text_plain_charset  = undef;
74

75 76
# file to use for guessing MIME types before trying /etc/mime.types
# (relative to the current git repository)
77
our $mimetypes_file = undef;
78

79 80
# You define site-wide feature defaults here; override them with
# $GITWEB_CONFIG as necessary.
81
our %feature = (
82 83 84 85 86 87 88 89 90 91
	# feature => {
	# 	'sub' => feature-sub (subroutine),
	# 	'override' => allow-override (boolean),
	# 	'default' => [ default options...] (array reference)}
	#
	# if feature is overridable (it means that allow-override has true value,
	# then feature-sub will be called with default options as parameters;
	# return value of feature-sub indicates if to enable specified feature
	#
	# use gitweb_check_feature(<feature>) to check if <feature> is enabled
92 93 94 95 96 97 98 99 100 101 102

	'blame' => {
		'sub' => \&feature_blame,
		'override' => 0,
		'default' => [0]},

	'snapshot' => {
		'sub' => \&feature_snapshot,
		'override' => 0,
		#         => [content-encoding, suffix, program]
		'default' => ['x-gzip', 'gz', 'gzip']},
103 104 105 106 107

	'pickaxe' => {
		'sub' => \&feature_pickaxe,
		'override' => 0,
		'default' => [1]},
108 109 110 111
);

sub gitweb_check_feature {
	my ($name) = @_;
112
	return unless exists $feature{$name};
113 114 115 116
	my ($sub, $override, @defaults) = (
		$feature{$name}{'sub'},
		$feature{$name}{'override'},
		@{$feature{$name}{'default'}});
117 118 119 120 121
	if (!$override) { return @defaults; }
	return $sub->(@defaults);
}

# To enable system wide have in $GITWEB_CONFIG
122 123 124
# $feature{'blame'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'blame'}{'override'} = 1;
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
# and in project config gitweb.blame = 0|1;

sub feature_blame {
	my ($val) = git_get_project_config('blame', '--bool');

	if ($val eq 'true') {
		return 1;
	} elsif ($val eq 'false') {
		return 0;
	}

	return $_[0];
}

# To disable system wide have in $GITWEB_CONFIG
140 141 142
# $feature{'snapshot'}{'default'} = [undef];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'blame'}{'override'} = 1;
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
# and in project config  gitweb.snapshot = none|gzip|bzip2

sub feature_snapshot {
	my ($ctype, $suffix, $command) = @_;

	my ($val) = git_get_project_config('snapshot');

	if ($val eq 'gzip') {
		return ('x-gzip', 'gz', 'gzip');
	} elsif ($val eq 'bzip2') {
		return ('x-bzip2', 'bz2', 'bzip2');
	} elsif ($val eq 'none') {
		return ();
	}

	return ($ctype, $suffix, $command);
}

161 162 163 164 165 166 167
sub gitweb_have_snapshot {
	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
	my $have_snapshot = (defined $ctype && defined $suffix);

	return $have_snapshot;
}

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
# To enable system wide have in $GITWEB_CONFIG
# $feature{'pickaxe'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'pickaxe'}{'override'} = 1;
# and in project config gitweb.pickaxe = 0|1;

sub feature_pickaxe {
	my ($val) = git_get_project_config('pickaxe', '--bool');

	if ($val eq 'true') {
		return (1);
	} elsif ($val eq 'false') {
		return (0);
	}

	return ($_[0]);
}

186 187 188 189 190 191 192 193 194 195
# rename detection options for git-diff and git-diff-tree
# - default is '-M', with the cost proportional to
#   (number of removed files) * (number of new files).
# - more costly is '-C' (or '-C', '-M'), with the cost proportional to
#   (number of changed files + number of removed files) * (number of new files)
# - even more costly is '-C', '--find-copies-harder' with cost
#   (number of files in the original tree) * (number of new files)
# - one might want to include '-B' option, e.g. '-B', '-M'
our @diff_opts = ('-M'); # taken from git_commit

196
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
197
do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
198 199 200 201 202 203

# version of the core git binary
our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";

$projects_list ||= $projectroot;

204
# ======================================================================
K
v118  
Kay Sievers 已提交
205
# input validation and dispatch
206
our $action = $cgi->param('a');
K
v118  
Kay Sievers 已提交
207
if (defined $action) {
208
	if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
209
		die_error(undef, "Invalid action parameter");
K
v107  
Kay Sievers 已提交
210 211
	}
}
K
v014  
Kay Sievers 已提交
212

213
# parameters which are pathnames
214
our $project = $cgi->param('p');
215
if (defined $project) {
216
	if (!validate_pathname($project) ||
217
	    !(-d "$projectroot/$project") ||
M
Matthias Lederhofer 已提交
218 219 220
	    !(-e "$projectroot/$project/HEAD") ||
	    ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
	    ($strict_export && !project_in_list($project))) {
221
		undef $project;
222
		die_error(undef, "No such project");
K
v070  
Kay Sievers 已提交
223
	}
K
v055  
Kay Sievers 已提交
224
}
K
v085  
Kay Sievers 已提交
225

226
our $file_name = $cgi->param('f');
227 228 229 230 231 232
if (defined $file_name) {
	if (!validate_pathname($file_name)) {
		die_error(undef, "Invalid file parameter");
	}
}

233
our $file_parent = $cgi->param('fp');
234 235 236 237 238
if (defined $file_parent) {
	if (!validate_pathname($file_parent)) {
		die_error(undef, "Invalid file parent parameter");
	}
}
239

240
# parameters which are refnames
241
our $hash = $cgi->param('h');
K
v227  
Kay Sievers 已提交
242
if (defined $hash) {
243
	if (!validate_refname($hash)) {
244
		die_error(undef, "Invalid hash parameter");
K
v227  
Kay Sievers 已提交
245
	}
K
v055  
Kay Sievers 已提交
246
}
K
v085  
Kay Sievers 已提交
247

248
our $hash_parent = $cgi->param('hp');
249
if (defined $hash_parent) {
250
	if (!validate_refname($hash_parent)) {
251
		die_error(undef, "Invalid hash parent parameter");
252
	}
K
v118  
Kay Sievers 已提交
253 254
}

255
our $hash_base = $cgi->param('hb');
256
if (defined $hash_base) {
257
	if (!validate_refname($hash_base)) {
258
		die_error(undef, "Invalid hash base parameter");
259
	}
K
v055  
Kay Sievers 已提交
260
}
K
v085  
Kay Sievers 已提交
261

262 263
our $hash_parent_base = $cgi->param('hpb');
if (defined $hash_parent_base) {
264
	if (!validate_refname($hash_parent_base)) {
265 266 267 268
		die_error(undef, "Invalid hash parent base parameter");
	}
}

269
# other parameters
270
our $page = $cgi->param('pg');
K
v206  
Kay Sievers 已提交
271
if (defined $page) {
272
	if ($page =~ m/[^0-9]/) {
273
		die_error(undef, "Invalid page parameter");
K
v107  
Kay Sievers 已提交
274
	}
K
v053  
Kay Sievers 已提交
275
}
K
v005  
Kay Sievers 已提交
276

277
our $searchtext = $cgi->param('s');
K
v203  
Kay Sievers 已提交
278 279
if (defined $searchtext) {
	if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
280
		die_error(undef, "Invalid search parameter");
K
v203  
Kay Sievers 已提交
281 282 283 284
	}
	$searchtext = quotemeta $searchtext;
}

285
# now read PATH_INFO and use it as alternative to parameters
286 287 288 289
sub evaluate_path_info {
	return if defined $project;
	my $path_info = $ENV{"PATH_INFO"};
	return if !$path_info;
290
	$path_info =~ s,^/+,,;
291
	return if !$path_info;
292
	# find which part of PATH_INFO is project
293
	$project = $path_info;
294
	$project =~ s,/+$,,;
295 296 297
	while ($project && !-e "$projectroot/$project/HEAD") {
		$project =~ s,/*[^/]*$,,;
	}
298
	# validate project
299
	$project = validate_pathname($project);
300 301 302 303 304
	if (!$project ||
	    ($export_ok && !-e "$projectroot/$project/$export_ok") ||
	    ($strict_export && !project_in_list($project))) {
		undef $project;
		return;
305
	}
306 307
	# do not change any parameters if an action is given using the query string
	return if $action;
308 309 310 311 312 313 314 315
	$path_info =~ s,^$project/*,,;
	my ($refname, $pathname) = split(/:/, $path_info, 2);
	if (defined $pathname) {
		# we got "project.git/branch:filename" or "project.git/branch:dir/"
		# we could use git_get_type(branch:pathname), but it needs $git_dir
		$pathname =~ s,^/+,,;
		if (!$pathname || substr($pathname, -1) eq "/") {
			$action  ||= "tree";
316
			$pathname =~ s,/$,,;
317 318 319
		} else {
			$action  ||= "blob_plain";
		}
320 321
		$hash_base ||= validate_refname($refname);
		$file_name ||= validate_pathname($pathname);
322
	} elsif (defined $refname) {
323 324
		# we got "project.git/branch"
		$action ||= "shortlog";
325
		$hash   ||= validate_refname($refname);
326 327
	}
}
328
evaluate_path_info();
329

330 331 332
# path to the current git repository
our $git_dir;
$git_dir = "$projectroot/$project" if $project;
333

334
# dispatch
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
my %actions = (
	"blame" => \&git_blame2,
	"blobdiff" => \&git_blobdiff,
	"blobdiff_plain" => \&git_blobdiff_plain,
	"blob" => \&git_blob,
	"blob_plain" => \&git_blob_plain,
	"commitdiff" => \&git_commitdiff,
	"commitdiff_plain" => \&git_commitdiff_plain,
	"commit" => \&git_commit,
	"heads" => \&git_heads,
	"history" => \&git_history,
	"log" => \&git_log,
	"rss" => \&git_rss,
	"search" => \&git_search,
	"shortlog" => \&git_shortlog,
	"summary" => \&git_summary,
	"tag" => \&git_tag,
	"tags" => \&git_tags,
	"tree" => \&git_tree,
A
Aneesh Kumar K.V 已提交
354
	"snapshot" => \&git_snapshot,
355 356 357
	# those below don't need $project
	"opml" => \&git_opml,
	"project_list" => \&git_project_list,
358
	"project_index" => \&git_project_index,
359 360
);

361 362 363 364 365
if (defined $project) {
	$action ||= 'summary';
} else {
	$action ||= 'project_list';
}
366
if (!defined($actions{$action})) {
367
	die_error(undef, "Unknown action");
K
v118  
Kay Sievers 已提交
368
}
369 370 371 372
if ($action !~ m/^(opml|project_list|project_index)$/ &&
    !$project) {
	die_error(undef, "Project needed");
}
373 374
$actions{$action}->();
exit;
K
v118  
Kay Sievers 已提交
375

376 377 378 379
## ======================================================================
## action links

sub href(%) {
380 381 382
	my %params = @_;

	my @mapping = (
383
		project => "p",
384
		action => "a",
385
		file_name => "f",
386
		file_parent => "fp",
387 388 389
		hash => "h",
		hash_parent => "hp",
		hash_base => "hb",
390
		hash_parent_base => "hpb",
391
		page => "pg",
392
		order => "o",
393 394
		searchtext => "s",
	);
395
	my %mapping = @mapping;
396

397
	$params{'project'} = $project unless exists $params{'project'};
398

399 400 401 402 403 404 405 406
	my @result = ();
	for (my $i = 0; $i < @mapping; $i += 2) {
		my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
		if (defined $params{$name}) {
			push @result, $symbol . "=" . esc_param($params{$name});
		}
	}
	return "$my_uri?" . join(';', @result);
407 408 409
}


410 411 412
## ======================================================================
## validation, quoting/unquoting and escaping

413 414
sub validate_pathname {
	my $input = shift || return undef;
415

416 417 418 419 420
	# no '.' or '..' as elements of path, i.e. no '.' nor '..'
	# at the beginning, at the end, and between slashes.
	# also this catches doubled slashes
	if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
		return undef;
421
	}
422 423
	# no null characters
	if ($input =~ m!\0!) {
424 425
		return undef;
	}
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
	return $input;
}

sub validate_refname {
	my $input = shift || return undef;

	# textual hashes are O.K.
	if ($input =~ m/^[0-9a-fA-F]{40}$/) {
		return $input;
	}
	# it must be correct pathname
	$input = validate_pathname($input)
		or return undef;
	# restrictions on ref name according to git-check-ref-format
	if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
441 442 443 444 445
		return undef;
	}
	return $input;
}

446 447 448
# quote unsafe chars, but keep the slash, even when it's not
# correct, but quoted slashes look too horrible in bookmarks
sub esc_param {
K
Kay Sievers 已提交
449
	my $str = shift;
450
	$str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
451
	$str =~ s/\+/%2B/g;
K
Kay Sievers 已提交
452
	$str =~ s/ /\+/g;
K
Kay Sievers 已提交
453 454 455
	return $str;
}

456 457 458 459 460 461 462 463 464
# quote unsafe chars in whole URL, so some charactrs cannot be quoted
sub esc_url {
	my $str = shift;
	$str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
	$str =~ s/\+/%2B/g;
	$str =~ s/ /\+/g;
	return $str;
}

465
# replace invalid utf8 character with SUBSTITUTION sequence
466 467 468
sub esc_html {
	my $str = shift;
	$str = decode("utf8", $str, Encode::FB_DEFAULT);
K
Kay Sievers 已提交
469
	$str = escapeHTML($str);
470
	$str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
471
	$str =~ s/\033/^[/g; # "escape" ESCAPE (\e) character (e.g. commit 20a3847d8a5032ce41f90dcc68abfb36e6fee9b1)
472 473 474
	return $str;
}

475 476 477 478 479 480 481 482 483 484
# git may return quoted and escaped filenames
sub unquote {
	my $str = shift;
	if ($str =~ m/^"(.*)"$/) {
		$str = $1;
		$str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
	}
	return $str;
}

485 486 487 488 489 490 491 492 493 494 495 496 497 498
# escape tabs (convert tabs to spaces)
sub untabify {
	my $line = shift;

	while ((my $pos = index($line, "\t")) != -1) {
		if (my $count = (8 - ($pos % 8))) {
			my $spaces = ' ' x $count;
			$line =~ s/\t/$spaces/;
		}
	}

	return $line;
}

M
Matthias Lederhofer 已提交
499 500 501 502 503 504
sub project_in_list {
	my $project = shift;
	my @list = git_get_projects_list();
	return @list && scalar(grep { $_->{'path'} eq $project } @list);
}

505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
## ----------------------------------------------------------------------
## HTML aware string manipulation

sub chop_str {
	my $str = shift;
	my $len = shift;
	my $add_len = shift || 10;

	# allow only $len chars, but don't cut a word if it would fit in $add_len
	# if it doesn't fit, cut it if it's still longer than the dots we would add
	$str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
	my $body = $1;
	my $tail = $2;
	if (length($tail) > 4) {
		$tail = " ...";
		$body =~ s/&[^;]*$//; # remove chopped character entities
	}
	return "$body$tail";
}

## ----------------------------------------------------------------------
## functions returning short strings

528 529 530 531 532 533 534 535 536 537 538 539 540
# CSS class for given age value (in seconds)
sub age_class {
	my $age = shift;

	if ($age < 60*60*2) {
		return "age0";
	} elsif ($age < 60*60*24*2) {
		return "age1";
	} else {
		return "age2";
	}
}

541 542 543 544
# convert age in seconds to "nn units ago" string
sub age_string {
	my $age = shift;
	my $age_str;
K
v055  
Kay Sievers 已提交
545

546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566
	if ($age > 60*60*24*365*2) {
		$age_str = (int $age/60/60/24/365);
		$age_str .= " years ago";
	} elsif ($age > 60*60*24*(365/12)*2) {
		$age_str = int $age/60/60/24/(365/12);
		$age_str .= " months ago";
	} elsif ($age > 60*60*24*7*2) {
		$age_str = int $age/60/60/24/7;
		$age_str .= " weeks ago";
	} elsif ($age > 60*60*24*2) {
		$age_str = int $age/60/60/24;
		$age_str .= " days ago";
	} elsif ($age > 60*60*2) {
		$age_str = int $age/60/60;
		$age_str .= " hours ago";
	} elsif ($age > 60*2) {
		$age_str = int $age/60;
		$age_str .= " min ago";
	} elsif ($age > 2) {
		$age_str = int $age;
		$age_str .= " sec ago";
567
	} else {
568
		$age_str .= " right now";
K
v000  
Kay Sievers 已提交
569
	}
570
	return $age_str;
K
Kay Sievers 已提交
571 572
}

573 574 575 576 577 578 579 580 581 582 583 584 585 586 587
# convert file mode in octal to symbolic file mode string
sub mode_str {
	my $mode = oct shift;

	if (S_ISDIR($mode & S_IFMT)) {
		return 'drwxr-xr-x';
	} elsif (S_ISLNK($mode)) {
		return 'lrwxrwxrwx';
	} elsif (S_ISREG($mode)) {
		# git cares only about the executable bit
		if ($mode & S_IXUSR) {
			return '-rwxr-xr-x';
		} else {
			return '-rw-r--r--';
		};
K
v220  
Kay Sievers 已提交
588
	} else {
589
		return '----------';
K
v048  
Kay Sievers 已提交
590
	}
K
Kay Sievers 已提交
591 592
}

593 594
# convert file mode in octal to file type string
sub file_type {
595 596 597 598 599 600 601
	my $mode = shift;

	if ($mode !~ m/^[0-7]+$/) {
		return $mode;
	} else {
		$mode = oct $mode;
	}
K
v064  
Kay Sievers 已提交
602

603 604 605 606 607 608 609 610 611
	if (S_ISDIR($mode & S_IFMT)) {
		return "directory";
	} elsif (S_ISLNK($mode)) {
		return "symlink";
	} elsif (S_ISREG($mode)) {
		return "file";
	} else {
		return "unknown";
	}
K
v055  
Kay Sievers 已提交
612 613
}

614 615 616
## ----------------------------------------------------------------------
## functions returning short HTML fragments, or transforming HTML fragments
## which don't beling to other sections
617

618 619 620
# format line of commit message or tag comment
sub format_log_line_html {
	my $line = shift;
621

622 623 624 625 626
	$line = esc_html($line);
	$line =~ s/ /&nbsp;/g;
	if ($line =~ m/([0-9a-fA-F]{40})/) {
		my $hash_text = $1;
		if (git_get_type($hash_text) eq "commit") {
627 628 629
			my $link =
				$cgi->a({-href => href(action=>"commit", hash=>$hash_text),
				        -class => "text"}, $hash_text);
630
			$line =~ s/$hash_text/$link/;
631 632
		}
	}
633
	return $line;
634 635
}

636
# format marker of refs pointing to given object
637
sub format_ref_marker {
638
	my ($refs, $id) = @_;
639
	my $markers = '';
640

641
	if (defined $refs->{$id}) {
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
		foreach my $ref (@{$refs->{$id}}) {
			my ($type, $name) = qw();
			# e.g. tags/v2.6.11 or heads/next
			if ($ref =~ m!^(.*?)s?/(.*)$!) {
				$type = $1;
				$name = $2;
			} else {
				$type = "ref";
				$name = $ref;
			}

			$markers .= " <span class=\"$type\">" . esc_html($name) . "</span>";
		}
	}

	if ($markers) {
		return ' <span class="refs">'. $markers . '</span>';
659 660 661
	} else {
		return "";
	}
662 663
}

664 665
# format, perhaps shortened and with markers, title line
sub format_subject_html {
666
	my ($long, $short, $href, $extra) = @_;
667 668 669
	$extra = '' unless defined($extra);

	if (length($short) < length($long)) {
670
		return $cgi->a({-href => $href, -class => "list subject",
671
		                -title => decode("utf8", $long, Encode::FB_DEFAULT)},
672 673
		       esc_html($short) . $extra);
	} else {
674
		return $cgi->a({-href => $href, -class => "list subject"},
675 676 677 678
		       esc_html($long)  . $extra);
	}
}

679 680 681 682 683 684 685 686 687 688 689 690 691 692
sub format_diff_line {
	my $line = shift;
	my $char = substr($line, 0, 1);
	my $diff_class = "";

	chomp $line;

	if ($char eq '+') {
		$diff_class = " add";
	} elsif ($char eq "-") {
		$diff_class = " rem";
	} elsif ($char eq "@") {
		$diff_class = " chunk_header";
	} elsif ($char eq "\\") {
693
		$diff_class = " incomplete";
694 695 696 697 698
	}
	$line = untabify($line);
	return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
}

699 700
## ----------------------------------------------------------------------
## git utility subroutines, invoking git commands
K
v125  
Kay Sievers 已提交
701

702 703 704 705 706 707 708 709 710 711
# returns path to the core git executable and the --git-dir parameter as list
sub git_cmd {
	return $GIT, '--git-dir='.$git_dir;
}

# returns path to the core git executable and the --git-dir parameter as string
sub git_cmd_str {
	return join(' ', git_cmd());
}

712
# get HEAD ref of given project as hash
713
sub git_get_head_hash {
714
	my $project = shift;
715
	my $o_git_dir = $git_dir;
716
	my $retval = undef;
717 718
	$git_dir = "$projectroot/$project";
	if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
719 720
		my $head = <$fd>;
		close $fd;
K
Kay Sievers 已提交
721 722
		if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
			$retval = $1;
723 724
		}
	}
725 726
	if (defined $o_git_dir) {
		$git_dir = $o_git_dir;
K
Kay Sievers 已提交
727
	}
728 729 730
	return $retval;
}

731 732 733 734
# get type of given object
sub git_get_type {
	my $hash = shift;

735
	open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
736 737 738 739 740 741 742
	my $type = <$fd>;
	close $fd or return;
	chomp $type;
	return $type;
}

sub git_get_project_config {
743
	my ($key, $type) = @_;
744 745 746 747 748

	return unless ($key);
	$key =~ s/^gitweb\.//;
	return if ($key =~ m/\W/);

749
	my @x = (git_cmd(), 'repo-config');
750 751 752 753 754
	if (defined $type) { push @x, $type; }
	push @x, "--get";
	push @x, "gitweb.$key";
	my $val = qx(@x);
	chomp $val;
755 756 757 758 759 760 761
	return ($val);
}

# get hash of given path at given ref
sub git_get_hash_by_path {
	my $base = shift;
	my $path = shift || return undef;
762
	my $type = shift;
763

764
	$path =~ s,/+$,,;
765

766
	open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
767
		or die_error(undef, "Open git-ls-tree failed");
768 769 770 771 772
	my $line = <$fd>;
	close $fd or return undef;

	#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
	$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
773 774 775 776
	if (defined $type && $type ne $2) {
		# type doesn't match
		return undef;
	}
777 778 779 780 781 782
	return $3;
}

## ......................................................................
## git utility functions, directly accessing git repository

783
sub git_get_project_description {
K
v107  
Kay Sievers 已提交
784
	my $path = shift;
K
v118  
Kay Sievers 已提交
785

K
v203  
Kay Sievers 已提交
786
	open my $fd, "$projectroot/$path/description" or return undef;
K
v107  
Kay Sievers 已提交
787 788 789 790
	my $descr = <$fd>;
	close $fd;
	chomp $descr;
	return $descr;
K
v021  
Kay Sievers 已提交
791 792
}

793 794 795
sub git_get_project_url_list {
	my $path = shift;

796
	open my $fd, "$projectroot/$path/cloneurl" or return;
797 798 799 800 801 802
	my @git_project_url_list = map { chomp; $_ } <$fd>;
	close $fd;

	return wantarray ? @git_project_url_list : \@git_project_url_list;
}

803
sub git_get_projects_list {
804 805 806 807 808
	my @list;

	if (-d $projects_list) {
		# search in directory
		my $dir = $projects_list;
809 810 811 812 813 814 815 816 817 818 819 820 821
		my $pfxlen = length("$dir");

		File::Find::find({
			follow_fast => 1, # follow symbolic links
			dangling_symlinks => 0, # ignore dangling symlinks, silently
			wanted => sub {
				# skip project-list toplevel, if we get it.
				return if (m!^[/.]$!);
				# only directories can be git repositories
				return unless (-d $_);

				my $subdir = substr($File::Find::name, $pfxlen + 1);
				# we check related file in $projectroot
M
Matthias Lederhofer 已提交
822 823
				if (-e "$projectroot/$subdir/HEAD" && (!$export_ok ||
				    -e "$projectroot/$subdir/$export_ok")) {
824 825 826 827 828 829
					push @list, { path => $subdir };
					$File::Find::prune = 1;
				}
			},
		}, "$dir");

830 831 832 833 834
	} elsif (-f $projects_list) {
		# read from file(url-encoded):
		# 'git%2Fgit.git Linus+Torvalds'
		# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
		# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
835
		open my ($fd), $projects_list or return;
836 837 838 839 840 841 842 843
		while (my $line = <$fd>) {
			chomp $line;
			my ($path, $owner) = split ' ', $line;
			$path = unescape($path);
			$owner = unescape($owner);
			if (!defined $path) {
				next;
			}
M
Matthias Lederhofer 已提交
844 845
			if (-e "$projectroot/$path/HEAD" && (!$export_ok ||
			    -e "$projectroot/$path/$export_ok")) {
846 847 848 849 850 851 852 853 854 855 856 857 858
				my $pr = {
					path => $path,
					owner => decode("utf8", $owner, Encode::FB_DEFAULT),
				};
				push @list, $pr
			}
		}
		close $fd;
	}
	@list = sort {$a->{'path'} cmp $b->{'path'}} @list;
	return @list;
}

859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889
sub git_get_project_owner {
	my $project = shift;
	my $owner;

	return undef unless $project;

	# read from file (url-encoded):
	# 'git%2Fgit.git Linus+Torvalds'
	# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
	# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
	if (-f $projects_list) {
		open (my $fd , $projects_list);
		while (my $line = <$fd>) {
			chomp $line;
			my ($pr, $ow) = split ' ', $line;
			$pr = unescape($pr);
			$ow = unescape($ow);
			if ($pr eq $project) {
				$owner = decode("utf8", $ow, Encode::FB_DEFAULT);
				last;
			}
		}
		close $fd;
	}
	if (!defined $owner) {
		$owner = get_file_owner("$projectroot/$project");
	}

	return $owner;
}

890
sub git_get_references {
891 892 893 894
	my $type = shift || "";
	my %refs;
	# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c	refs/tags/v2.6.11
	# c39ae07f393806ccf406ef966e9a15afc43cc36a	refs/tags/v2.6.11^{}
895 896
	open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
		or return;
897

898 899
	while (my $line = <$fd>) {
		chomp $line;
900
		if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
901
			if (defined $refs{$1}) {
902
				push @{$refs{$1}}, $2;
903
			} else {
904
				$refs{$1} = [ $2 ];
905 906 907 908 909 910 911
			}
		}
	}
	close $fd or return;
	return \%refs;
}

912 913 914
sub git_get_rev_name_tags {
	my $hash = shift || return undef;

915
	open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
916 917 918 919 920 921 922 923 924 925 926 927
		or return;
	my $name_rev = <$fd>;
	close $fd;

	if ($name_rev =~ m|^$hash tags/(.*)$|) {
		return $1;
	} else {
		# catches also '$hash undefined' output
		return undef;
	}
}

928 929 930
## ----------------------------------------------------------------------
## parse to hash functions

931
sub parse_date {
932 933 934 935 936 937 938 939 940 941 942 943
	my $epoch = shift;
	my $tz = shift || "-0000";

	my %date;
	my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
	my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
	$date{'hour'} = $hour;
	$date{'minute'} = $min;
	$date{'mday'} = $mday;
	$date{'day'} = $days[$wday];
	$date{'month'} = $months[$mon];
944 945 946 947
	$date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
	                   $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
	$date{'mday-time'} = sprintf "%d %s %02d:%02d",
	                     $mday, $months[$mon], $hour ,$min;
948 949 950 951 952 953 954 955 956 957

	$tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
	my $local = $epoch + ((int $1 + ($2/60)) * 3600);
	($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
	$date{'hour_local'} = $hour;
	$date{'minute_local'} = $min;
	$date{'tz_local'} = $tz;
	return %date;
}

958
sub parse_tag {
K
v142  
Kay Sievers 已提交
959 960
	my $tag_id = shift;
	my %tag;
K
v235  
Kay Sievers 已提交
961
	my @comment;
K
v142  
Kay Sievers 已提交
962

963
	open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
K
v235  
Kay Sievers 已提交
964
	$tag{'id'} = $tag_id;
K
v142  
Kay Sievers 已提交
965 966 967 968
	while (my $line = <$fd>) {
		chomp $line;
		if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
			$tag{'object'} = $1;
K
v163  
Kay Sievers 已提交
969
		} elsif ($line =~ m/^type (.+)$/) {
K
v142  
Kay Sievers 已提交
970
			$tag{'type'} = $1;
K
v163  
Kay Sievers 已提交
971
		} elsif ($line =~ m/^tag (.+)$/) {
K
v142  
Kay Sievers 已提交
972
			$tag{'name'} = $1;
K
v235  
Kay Sievers 已提交
973 974 975 976 977 978 979 980 981
		} elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
			$tag{'author'} = $1;
			$tag{'epoch'} = $2;
			$tag{'tz'} = $3;
		} elsif ($line =~ m/--BEGIN/) {
			push @comment, $line;
			last;
		} elsif ($line eq "") {
			last;
K
v142  
Kay Sievers 已提交
982 983
		}
	}
K
v235  
Kay Sievers 已提交
984 985
	push @comment, <$fd>;
	$tag{'comment'} = \@comment;
K
v203  
Kay Sievers 已提交
986
	close $fd or return;
K
v142  
Kay Sievers 已提交
987 988 989 990 991 992
	if (!defined $tag{'name'}) {
		return
	};
	return %tag
}

993
sub parse_commit {
K
v203  
Kay Sievers 已提交
994 995 996 997
	my $commit_id = shift;
	my $commit_text = shift;

	my @commit_lines;
K
v021  
Kay Sievers 已提交
998 999
	my %co;

K
v203  
Kay Sievers 已提交
1000 1001 1002
	if (defined $commit_text) {
		@commit_lines = @$commit_text;
	} else {
1003
		$/ = "\0";
1004
		open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", "--max-count=1", $commit_id
1005
			or return;
1006
		@commit_lines = split '\n', <$fd>;
K
v203  
Kay Sievers 已提交
1007
		close $fd or return;
1008 1009
		$/ = "\n";
		pop @commit_lines;
K
v203  
Kay Sievers 已提交
1010
	}
1011 1012 1013 1014 1015 1016 1017
	my $header = shift @commit_lines;
	if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
		return;
	}
	($co{'id'}, my @parents) = split ' ', $header;
	$co{'parents'} = \@parents;
	$co{'parent'} = $parents[0];
K
v203  
Kay Sievers 已提交
1018
	while (my $line = shift @commit_lines) {
K
v107  
Kay Sievers 已提交
1019
		last if $line eq "\n";
K
v163  
Kay Sievers 已提交
1020
		if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
K
v021  
Kay Sievers 已提交
1021
			$co{'tree'} = $1;
K
v035  
Kay Sievers 已提交
1022
		} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
K
v021  
Kay Sievers 已提交
1023
			$co{'author'} = $1;
K
v049  
Kay Sievers 已提交
1024 1025
			$co{'author_epoch'} = $2;
			$co{'author_tz'} = $3;
K
v164  
Kay Sievers 已提交
1026 1027 1028 1029 1030
			if ($co{'author'} =~ m/^([^<]+) </) {
				$co{'author_name'} = $1;
			} else {
				$co{'author_name'} = $co{'author'};
			}
K
v041  
Kay Sievers 已提交
1031 1032
		} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
			$co{'committer'} = $1;
K
v049  
Kay Sievers 已提交
1033 1034
			$co{'committer_epoch'} = $2;
			$co{'committer_tz'} = $3;
K
v042  
Kay Sievers 已提交
1035 1036
			$co{'committer_name'} = $co{'committer'};
			$co{'committer_name'} =~ s/ <.*//;
K
v021  
Kay Sievers 已提交
1037 1038
		}
	}
K
v142  
Kay Sievers 已提交
1039
	if (!defined $co{'tree'}) {
1040
		return;
K
v142  
Kay Sievers 已提交
1041
	};
1042

K
v203  
Kay Sievers 已提交
1043
	foreach my $title (@commit_lines) {
1044
		$title =~ s/^    //;
K
v203  
Kay Sievers 已提交
1045
		if ($title ne "") {
K
v241  
Kay Sievers 已提交
1046
			$co{'title'} = chop_str($title, 80, 5);
K
v203  
Kay Sievers 已提交
1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063
			# remove leading stuff of merges to make the interesting part visible
			if (length($title) > 50) {
				$title =~ s/^Automatic //;
				$title =~ s/^merge (of|with) /Merge ... /i;
				if (length($title) > 50) {
					$title =~ s/(http|rsync):\/\///;
				}
				if (length($title) > 50) {
					$title =~ s/(master|www|rsync)\.//;
				}
				if (length($title) > 50) {
					$title =~ s/kernel.org:?//;
				}
				if (length($title) > 50) {
					$title =~ s/\/pub\/scm//;
				}
			}
K
v241  
Kay Sievers 已提交
1064
			$co{'title_short'} = chop_str($title, 50, 5);
K
v203  
Kay Sievers 已提交
1065 1066 1067
			last;
		}
	}
1068 1069 1070 1071 1072
	# remove added spaces
	foreach my $line (@commit_lines) {
		$line =~ s/^    //;
	}
	$co{'comment'} = \@commit_lines;
K
v062  
Kay Sievers 已提交
1073 1074 1075

	my $age = time - $co{'committer_epoch'};
	$co{'age'} = $age;
K
v236  
Kay Sievers 已提交
1076
	$co{'age_string'} = age_string($age);
K
v225  
Kay Sievers 已提交
1077 1078
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
	if ($age > 60*60*24*7*2) {
K
v232  
Kay Sievers 已提交
1079
		$co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
K
v225  
Kay Sievers 已提交
1080 1081 1082
		$co{'age_string_age'} = $co{'age_string'};
	} else {
		$co{'age_string_date'} = $co{'age_string'};
K
v232  
Kay Sievers 已提交
1083
		$co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
K
v225  
Kay Sievers 已提交
1084
	}
K
v021  
Kay Sievers 已提交
1085 1086 1087
	return %co;
}

1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130
# parse ref from ref_file, given by ref_id, with given type
sub parse_ref {
	my $ref_file = shift;
	my $ref_id = shift;
	my $type = shift || git_get_type($ref_id);
	my %ref_item;

	$ref_item{'type'} = $type;
	$ref_item{'id'} = $ref_id;
	$ref_item{'epoch'} = 0;
	$ref_item{'age'} = "unknown";
	if ($type eq "tag") {
		my %tag = parse_tag($ref_id);
		$ref_item{'comment'} = $tag{'comment'};
		if ($tag{'type'} eq "commit") {
			my %co = parse_commit($tag{'object'});
			$ref_item{'epoch'} = $co{'committer_epoch'};
			$ref_item{'age'} = $co{'age_string'};
		} elsif (defined($tag{'epoch'})) {
			my $age = time - $tag{'epoch'};
			$ref_item{'epoch'} = $tag{'epoch'};
			$ref_item{'age'} = age_string($age);
		}
		$ref_item{'reftype'} = $tag{'type'};
		$ref_item{'name'} = $tag{'name'};
		$ref_item{'refid'} = $tag{'object'};
	} elsif ($type eq "commit"){
		my %co = parse_commit($ref_id);
		$ref_item{'reftype'} = "commit";
		$ref_item{'name'} = $ref_file;
		$ref_item{'title'} = $co{'title'};
		$ref_item{'refid'} = $ref_id;
		$ref_item{'epoch'} = $co{'committer_epoch'};
		$ref_item{'age'} = $co{'age_string'};
	} else {
		$ref_item{'reftype'} = $type;
		$ref_item{'name'} = $ref_file;
		$ref_item{'refid'} = $ref_id;
	}

	return %ref_item;
}

1131
# parse line of git-diff-tree "raw" output
1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
sub parse_difftree_raw_line {
	my $line = shift;
	my %res;

	# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M	ls-files.c'
	# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M	rev-tree.c'
	if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
		$res{'from_mode'} = $1;
		$res{'to_mode'} = $2;
		$res{'from_id'} = $3;
		$res{'to_id'} = $4;
		$res{'status'} = $5;
		$res{'similarity'} = $6;
		if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
1146
			($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
1147 1148 1149 1150 1151
		} else {
			$res{'file'} = unquote($7);
		}
	}
	# 'c512b523472485aef4fff9e57b229d9d243c967f'
1152 1153 1154
	elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
		$res{'commit'} = $1;
	}
1155 1156 1157 1158

	return wantarray ? %res : \%res;
}

1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179
# parse line of git-ls-tree output
sub parse_ls_tree_line ($;%) {
	my $line = shift;
	my %opts = @_;
	my %res;

	#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
	$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;

	$res{'mode'} = $1;
	$res{'type'} = $2;
	$res{'hash'} = $3;
	if ($opts{'-z'}) {
		$res{'name'} = $4;
	} else {
		$res{'name'} = unquote($4);
	}

	return wantarray ? %res : \%res;
}

1180 1181
## ......................................................................
## parse to array of hashes functions
K
v000  
Kay Sievers 已提交
1182

1183
sub git_get_refs_list {
1184 1185
	my $type = shift || "";
	my %refs;
1186
	my @reflist;
K
v000  
Kay Sievers 已提交
1187

1188
	my @refs;
1189 1190 1191 1192
	open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
		or return;
	while (my $line = <$fd>) {
		chomp $line;
1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
		if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?([^\^]+))(\^\{\})?$/) {
			if (defined $refs{$1}) {
				push @{$refs{$1}}, $2;
			} else {
				$refs{$1} = [ $2 ];
			}

			if (! $4) { # unpeeled, direct reference
				push @refs, { hash => $1, name => $3 }; # without type
			} elsif ($3 eq $refs[-1]{'name'}) {
				# most likely a tag is followed by its peeled
				# (deref) one, and when that happens we know the
				# previous one was of type 'tag'.
				$refs[-1]{'type'} = "tag";
			}
1208
		}
1209 1210 1211 1212 1213 1214
	}
	close $fd;

	foreach my $ref (@refs) {
		my $ref_file = $ref->{'name'};
		my $ref_id   = $ref->{'hash'};
1215

1216
		my $type = $ref->{'type'} || git_get_type($ref_id) || next;
1217
		my %ref_item = parse_ref($ref_file, $ref_id, $type);
K
v042  
Kay Sievers 已提交
1218

1219 1220
		push @reflist, \%ref_item;
	}
1221
	# sort refs by age
1222
	@reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
1223
	return (\@reflist, \%refs);
K
v041  
Kay Sievers 已提交
1224 1225
}

1226 1227
## ----------------------------------------------------------------------
## filesystem-related functions
K
v035  
Kay Sievers 已提交
1228

K
v133  
Kay Sievers 已提交
1229 1230 1231 1232 1233 1234 1235 1236 1237 1238
sub get_file_owner {
	my $path = shift;

	my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
	my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
	if (!defined $gcos) {
		return undef;
	}
	my $owner = $gcos;
	$owner =~ s/[,;].*$//;
1239
	return decode("utf8", $owner, Encode::FB_DEFAULT);
K
v133  
Kay Sievers 已提交
1240 1241
}

1242 1243
## ......................................................................
## mimetype related functions
K
v118  
Kay Sievers 已提交
1244

1245 1246 1247 1248 1249 1250 1251 1252
sub mimetype_guess_file {
	my $filename = shift;
	my $mimemap = shift;
	-r $mimemap or return undef;

	my %mimemap;
	open(MIME, $mimemap) or return undef;
	while (<MIME>) {
1253
		next if m/^#/; # skip comments
1254
		my ($mime, $exts) = split(/\t+/);
1255 1256 1257 1258 1259
		if (defined $exts) {
			my @exts = split(/\s+/, $exts);
			foreach my $ext (@exts) {
				$mimemap{$ext} = $mime;
			}
K
v118  
Kay Sievers 已提交
1260 1261
		}
	}
1262
	close(MIME);
K
v118  
Kay Sievers 已提交
1263

1264
	$filename =~ /\.([^.]*)$/;
1265 1266
	return $mimemap{$1};
}
1267

1268 1269 1270 1271
sub mimetype_guess {
	my $filename = shift;
	my $mime;
	$filename =~ /\./ or return undef;
1272

1273 1274
	if ($mimetypes_file) {
		my $file = $mimetypes_file;
1275 1276 1277 1278
		if ($file !~ m!^/!) { # if it is relative path
			# it is relative to project
			$file = "$projectroot/$project/$file";
		}
1279 1280 1281 1282
		$mime = mimetype_guess_file($filename, $file);
	}
	$mime ||= mimetype_guess_file($filename, '/etc/mime.types');
	return $mime;
1283 1284
}

1285
sub blob_mimetype {
1286 1287
	my $fd = shift;
	my $filename = shift;
1288

1289 1290 1291
	if ($filename) {
		my $mime = mimetype_guess($filename);
		$mime and return $mime;
1292
	}
1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307

	# just in case
	return $default_blob_plain_mimetype unless $fd;

	if (-T $fd) {
		return 'text/plain' .
		       ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : '');
	} elsif (! $filename) {
		return 'application/octet-stream';
	} elsif ($filename =~ m/\.png$/i) {
		return 'image/png';
	} elsif ($filename =~ m/\.gif$/i) {
		return 'image/gif';
	} elsif ($filename =~ m/\.jpe?g$/i) {
		return 'image/jpeg';
1308
	} else {
1309
		return 'application/octet-stream';
1310
	}
1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325
}

## ======================================================================
## functions printing HTML: header, footer, error page

sub git_header_html {
	my $status = shift || "200 OK";
	my $expires = shift;

	my $title = "$site_name git";
	if (defined $project) {
		$title .= " - $project";
		if (defined $action) {
			$title .= "/$action";
			if (defined $file_name) {
1326
				$title .= " - " . esc_html($file_name);
1327 1328 1329 1330 1331
				if ($action eq "tree" && $file_name !~ m|/$|) {
					$title .= "/";
				}
			}
		}
1332
	}
1333 1334 1335 1336 1337
	my $content_type;
	# require explicit support from the UA if we are to send the page as
	# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
	# we have to do this because MSIE sometimes globs '*/*', pretending to
	# support xhtml+xml but choking when it gets what it asked for.
1338 1339 1340
	if (defined $cgi->http('HTTP_ACCEPT') &&
	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
	    $cgi->Accept('application/xhtml+xml') != 0) {
1341
		$content_type = 'application/xhtml+xml';
1342
	} else {
1343
		$content_type = 'text/html';
1344
	}
1345 1346
	print $cgi->header(-type=>$content_type, -charset => 'utf-8',
	                   -status=> $status, -expires => $expires);
1347 1348 1349 1350
	print <<EOF;
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
1351
<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
1352 1353 1354
<!-- git core binaries version $git_version -->
<head>
<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
1355
<meta name="generator" content="gitweb/$version git/$git_version"/>
1356 1357 1358 1359
<meta name="robots" content="index, nofollow"/>
<title>$title</title>
<link rel="stylesheet" type="text/css" href="$stylesheet"/>
EOF
M
Matthias Lederhofer 已提交
1360 1361 1362
	if (defined $project) {
		printf('<link rel="alternate" title="%s log" '.
		       'href="%s" type="application/rss+xml"/>'."\n",
1363
		       esc_param($project), href(action=>"rss"));
1364 1365 1366 1367 1368 1369 1370
	} else {
		printf('<link rel="alternate" title="%s projects list" '.
		       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
		       $site_name, href(project=>undef, action=>"project_index"));
		printf('<link rel="alternate" title="%s projects logs" '.
		       'href="%s" type="text/x-opml"/>'."\n",
		       $site_name, href(project=>undef, action=>"opml"));
M
Matthias Lederhofer 已提交
1371
	}
1372 1373 1374
	if (defined $favicon) {
		print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
	}
J
Jakub Narebski 已提交
1375

M
Matthias Lederhofer 已提交
1376 1377
	print "</head>\n" .
	      "<body>\n" .
J
Jakub Narebski 已提交
1378
	      "<div class=\"page_header\">\n" .
1379 1380 1381
	      "<a href=\"" . esc_html($githelp_url) .
	      "\" title=\"" . esc_html($githelp_label) .
	      "\">" .
M
Martin Waitz 已提交
1382
	      "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
1383
	      "</a>\n";
1384
	print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
1385
	if (defined $project) {
1386
		print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398
		if (defined $action) {
			print " / $action";
		}
		print "\n";
		if (!defined $searchtext) {
			$searchtext = "";
		}
		my $search_hash;
		if (defined $hash_base) {
			$search_hash = $hash_base;
		} elsif (defined $hash) {
			$search_hash = $hash;
K
v160  
Kay Sievers 已提交
1399
		} else {
1400
			$search_hash = "HEAD";
K
v160  
Kay Sievers 已提交
1401
		}
1402 1403 1404 1405 1406 1407 1408 1409 1410 1411
		$cgi->param("a", "search");
		$cgi->param("h", $search_hash);
		print $cgi->startform(-method => "get", -action => $my_uri) .
		      "<div class=\"search\">\n" .
		      $cgi->hidden(-name => "p") . "\n" .
		      $cgi->hidden(-name => "a") . "\n" .
		      $cgi->hidden(-name => "h") . "\n" .
		      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
		      "</div>" .
		      $cgi->end_form() . "\n";
K
v107  
Kay Sievers 已提交
1412
	}
1413 1414 1415 1416 1417 1418
	print "</div>\n";
}

sub git_footer_html {
	print "<div class=\"page_footer\">\n";
	if (defined $project) {
1419
		my $descr = git_get_project_description($project);
1420 1421 1422
		if (defined $descr) {
			print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
		}
1423 1424
		print $cgi->a({-href => href(action=>"rss"),
		              -class => "rss_logo"}, "RSS") . "\n";
1425
	} else {
1426
		print $cgi->a({-href => href(project=>undef, action=>"opml"),
1427 1428 1429
		              -class => "rss_logo"}, "OPML") . " ";
		print $cgi->a({-href => href(project=>undef, action=>"project_index"),
		              -class => "rss_logo"}, "TXT") . "\n";
1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440
	}
	print "</div>\n" .
	      "</body>\n" .
	      "</html>";
}

sub die_error {
	my $status = shift || "403 Forbidden";
	my $error = shift || "Malformed query, file missing or permission denied";

	git_header_html($status);
J
Jakub Narebski 已提交
1441 1442 1443 1444 1445 1446 1447
	print <<EOF;
<div class="page_body">
<br /><br />
$status - $error
<br />
</div>
EOF
K
v107  
Kay Sievers 已提交
1448
	git_footer_html();
1449
	exit;
K
Kay Sievers 已提交
1450 1451
}

1452 1453 1454
## ----------------------------------------------------------------------
## functions printing or outputting HTML: navigation

1455
sub git_print_page_nav {
1456 1457 1458 1459 1460 1461 1462 1463
	my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
	$extra = '' if !defined $extra; # pager or formats

	my @navs = qw(summary shortlog log commit commitdiff tree);
	if ($suppress) {
		@navs = grep { $_ ne $suppress } @navs;
	}

1464
	my %arg = map { $_ => {action=>$_} } @navs;
1465 1466
	if (defined $head) {
		for (qw(commit commitdiff)) {
1467
			$arg{$_}{hash} = $head;
1468 1469 1470
		}
		if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
			for (qw(shortlog log)) {
1471
				$arg{$_}{hash} = $head;
K
Kay Sievers 已提交
1472
			}
K
Kay Sievers 已提交
1473 1474
		}
	}
1475 1476
	$arg{tree}{hash} = $treehead if defined $treehead;
	$arg{tree}{hash_base} = $treebase if defined $treebase;
1477 1478 1479

	print "<div class=\"page_nav\">\n" .
		(join " | ",
1480 1481 1482
		 map { $_ eq $current ?
		       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
		 } @navs);
1483 1484
	print "<br/>\n$extra<br/>\n" .
	      "</div>\n";
K
Kay Sievers 已提交
1485 1486
}

1487
sub format_paging_nav {
1488 1489
	my ($action, $hash, $head, $page, $nrevs) = @_;
	my $paging_nav;
1490

1491 1492

	if ($hash ne $head || $page) {
1493
		$paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
1494
	} else {
1495 1496 1497 1498 1499
		$paging_nav .= "HEAD";
	}

	if ($page > 0) {
		$paging_nav .= " &sdot; " .
1500
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
1501
			         -accesskey => "p", -title => "Alt-p"}, "prev");
1502 1503 1504 1505 1506 1507
	} else {
		$paging_nav .= " &sdot; prev";
	}

	if ($nrevs >= (100 * ($page+1)-1)) {
		$paging_nav .= " &sdot; " .
1508
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
1509
			         -accesskey => "n", -title => "Alt-n"}, "next");
1510 1511
	} else {
		$paging_nav .= " &sdot; next";
1512
	}
1513 1514

	return $paging_nav;
1515 1516
}

1517 1518 1519
## ......................................................................
## functions printing or outputting HTML: div

1520
sub git_print_header_div {
1521
	my ($action, $title, $hash, $hash_base) = @_;
1522
	my %args = ();
1523

1524 1525 1526
	$args{action} = $action;
	$args{hash} = $hash if $hash;
	$args{hash_base} = $hash_base if $hash_base;
1527 1528

	print "<div class=\"header\">\n" .
1529 1530 1531
	      $cgi->a({-href => href(%args), -class => "title"},
	      $title ? $title : $action) .
	      "\n</div>\n";
1532
}
K
v142  
Kay Sievers 已提交
1533

1534 1535 1536 1537
#sub git_print_authorship (\%) {
sub git_print_authorship {
	my $co = shift;

1538
	my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
1539 1540
	print "<div class=\"author_date\">" .
	      esc_html($co->{'author_name'}) .
1541 1542 1543 1544 1545 1546 1547 1548 1549
	      " [$ad{'rfc2822'}";
	if ($ad{'hour_local'} < 6) {
		printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
	} else {
		printf(" (%02d:%02d %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
	}
	print "]</div>\n";
1550 1551
}

1552 1553 1554
sub git_print_page_path {
	my $name = shift;
	my $type = shift;
1555
	my $hb = shift;
K
v142  
Kay Sievers 已提交
1556

1557
	if (!defined $name) {
1558
		print "<div class=\"page_path\">/</div>\n";
1559 1560 1561 1562 1563
	} else {
		my @dirname = split '/', $name;
		my $basename = pop @dirname;
		my $fullname = '';

1564
		print "<div class=\"page_path\">";
P
Petr Baudis 已提交
1565
		print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
P
Petr Baudis 已提交
1566 1567
			      -title => 'tree root'}, "[$project]");
		print " / ";
1568
		foreach my $dir (@dirname) {
P
Petr Baudis 已提交
1569
			$fullname .= ($fullname ? '/' : '') . $dir;
1570 1571
			print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
			                             hash_base=>$hb),
P
Petr Baudis 已提交
1572 1573
			              -title => $fullname}, esc_html($dir));
			print " / ";
1574 1575
		}
		if (defined $type && $type eq 'blob') {
1576
			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
1577 1578 1579 1580 1581
			                             hash_base=>$hb),
			              -title => $name}, esc_html($basename));
		} elsif (defined $type && $type eq 'tree') {
			print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
			                             hash_base=>$hb),
P
Petr Baudis 已提交
1582
			              -title => $name}, esc_html($basename));
1583
		} else {
1584
			print esc_html($basename);
1585
		}
1586
		print "<br/></div>\n";
K
v142  
Kay Sievers 已提交
1587 1588 1589
	}
}

1590 1591
# sub git_print_log (\@;%) {
sub git_print_log ($;%) {
1592
	my $log = shift;
1593
	my %opts = @_;
1594

1595 1596 1597 1598
	if ($opts{'-remove_title'}) {
		# remove title, i.e. first line of log
		shift @$log;
	}
1599 1600 1601 1602 1603 1604 1605 1606 1607
	# remove leading empty lines
	while (defined $log->[0] && $log->[0] eq "") {
		shift @$log;
	}

	# print log
	my $signoff = 0;
	my $empty = 0;
	foreach my $line (@$log) {
1608 1609
		if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
			$signoff = 1;
1610
			$empty = 0;
1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621
			if (! $opts{'-remove_signoff'}) {
				print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
				next;
			} else {
				# remove signoff lines
				next;
			}
		} else {
			$signoff = 0;
		}

1622 1623 1624 1625 1626 1627 1628 1629
		# print only one empty line
		# do not print empty line after signoff
		if ($line eq "") {
			next if ($empty || $signoff);
			$empty = 1;
		} else {
			$empty = 0;
		}
1630 1631 1632 1633 1634 1635 1636

		print format_log_line_html($line) . "<br/>\n";
	}

	if ($opts{'-final_empty_line'}) {
		# end with single empty line
		print "<br/>\n" unless $empty;
1637 1638 1639 1640 1641 1642 1643
	}
}

sub git_print_simplified_log {
	my $log = shift;
	my $remove_title = shift;

1644 1645 1646
	git_print_log($log,
		-final_empty_line=> 1,
		-remove_title => $remove_title);
1647 1648
}

1649 1650 1651 1652 1653 1654 1655
# print tree entry (row of git_tree), but without encompassing <tr> element
sub git_print_tree_entry {
	my ($t, $basedir, $hash_base, $have_blame) = @_;

	my %base_key = ();
	$base_key{hash_base} = $hash_base if defined $hash_base;

1656 1657 1658 1659
	# The format of a table row is: mode list link.  Where mode is
	# the mode of the entry, list is the name of the entry, an href,
	# and link is the action links of the entry.

1660 1661 1662
	print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
	if ($t->{'type'} eq "blob") {
		print "<td class=\"list\">" .
1663 1664 1665 1666
			$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
					       file_name=>"$basedir$t->{'name'}", %base_key),
				 -class => "list"}, esc_html($t->{'name'})) . "</td>\n";
		print "<td class=\"link\">";
1667
		if ($have_blame) {
1668 1669 1670
			print $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
						     file_name=>"$basedir$t->{'name'}", %base_key)},
				      "blame");
1671 1672
		}
		if (defined $hash_base) {
1673 1674 1675 1676
			if ($have_blame) {
				print " | ";
			}
			print $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
1677 1678 1679 1680
			                             hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
			              "history");
		}
		print " | " .
1681 1682 1683
			$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
					       file_name=>"$basedir$t->{'name'}")},
				"raw");
1684
		print "</td>\n";
1685 1686

	} elsif ($t->{'type'} eq "tree") {
1687 1688
		print "<td class=\"list\">";
		print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
1689
		                             file_name=>"$basedir$t->{'name'}", %base_key)},
1690 1691 1692
		              esc_html($t->{'name'}));
		print "</td>\n";
		print "<td class=\"link\">";
1693
		if (defined $hash_base) {
1694
			print $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
1695 1696 1697 1698 1699 1700 1701
			                             file_name=>"$basedir$t->{'name'}")},
			              "history");
		}
		print "</td>\n";
	}
}

1702 1703 1704
## ......................................................................
## functions printing large fragments of HTML

1705
sub git_difftree_body {
1706
	my ($difftree, $hash, $parent) = @_;
1707 1708 1709 1710 1711 1712 1713 1714

	print "<div class=\"list_head\">\n";
	if ($#{$difftree} > 10) {
		print(($#{$difftree} + 1) . " files changed:\n");
	}
	print "</div>\n";

	print "<table class=\"diff_tree\">\n";
1715
	my $alternate = 1;
1716
	my $patchno = 0;
1717
	foreach my $line (@{$difftree}) {
1718
		my %diff = parse_difftree_raw_line($line);
1719 1720 1721 1722 1723 1724 1725 1726

		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;

1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739
		my ($to_mode_oct, $to_mode_str, $to_file_type);
		my ($from_mode_oct, $from_mode_str, $from_file_type);
		if ($diff{'to_mode'} ne ('0' x 6)) {
			$to_mode_oct = oct $diff{'to_mode'};
			if (S_ISREG($to_mode_oct)) { # only for regular file
				$to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
			}
			$to_file_type = file_type($diff{'to_mode'});
		}
		if ($diff{'from_mode'} ne ('0' x 6)) {
			$from_mode_oct = oct $diff{'from_mode'};
			if (S_ISREG($to_mode_oct)) { # only for regular file
				$from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
1740
			}
1741 1742 1743 1744 1745 1746 1747
			$from_file_type = file_type($diff{'from_mode'});
		}

		if ($diff{'status'} eq "A") { # created
			my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
			$mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
			$mode_chng   .= "]</span>";
1748 1749
			print "<td>";
			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
1750
			                             hash_base=>$hash, file_name=>$diff{'file'}),
1751 1752 1753 1754
				       -class => "list"}, esc_html($diff{'file'}));
			print "</td>\n";
			print "<td>$mode_chng</td>\n";
			print "<td class=\"link\">";
1755
			if ($action eq 'commitdiff') {
1756 1757
				# link to patch
				$patchno++;
1758
				print $cgi->a({-href => "#patch$patchno"}, "patch");
1759 1760
			}
			print "</td>\n";
1761

1762 1763
		} elsif ($diff{'status'} eq "D") { # deleted
			my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
1764 1765
			print "<td>";
			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
1766
			                             hash_base=>$parent, file_name=>$diff{'file'}),
1767 1768 1769 1770
			               -class => "list"}, esc_html($diff{'file'}));
			print "</td>\n";
			print "<td>$mode_chng</td>\n";
			print "<td class=\"link\">";
1771
			if ($action eq 'commitdiff') {
1772 1773
				# link to patch
				$patchno++;
1774 1775
				print $cgi->a({-href => "#patch$patchno"}, "patch");
				print " | ";
1776
			}
1777 1778 1779
			print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
						     file_name=>$diff{'file'})},
				      "blame") . " | ";
1780
			print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
1781 1782
						     file_name=>$diff{'file'})},
				      "history");
1783
			print "</td>\n";
1784

1785
		} elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
1786
			my $mode_chnge = "";
1787 1788 1789 1790
			if ($diff{'from_mode'} != $diff{'to_mode'}) {
				$mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
				if ($from_file_type != $to_file_type) {
					$mode_chnge .= " from $from_file_type to $to_file_type";
1791
				}
1792 1793 1794 1795 1796
				if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
					if ($from_mode_str && $to_mode_str) {
						$mode_chnge .= " mode: $from_mode_str->$to_mode_str";
					} elsif ($to_mode_str) {
						$mode_chnge .= " mode: $to_mode_str";
1797 1798 1799 1800 1801
					}
				}
				$mode_chnge .= "]</span>\n";
			}
			print "<td>";
1802 1803 1804 1805 1806 1807
			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
						     hash_base=>$hash, file_name=>$diff{'file'}),
				       -class => "list"}, esc_html($diff{'file'}));
			print "</td>\n";
			print "<td>$mode_chnge</td>\n";
			print "<td class=\"link\">";
1808
			if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
1809
				if ($action eq 'commitdiff') {
1810 1811
					# link to patch
					$patchno++;
1812
					print $cgi->a({-href => "#patch$patchno"}, "patch");
1813
				} else {
1814 1815 1816 1817 1818
					print $cgi->a({-href => href(action=>"blobdiff",
								     hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
								     hash_base=>$hash, hash_parent_base=>$parent,
								     file_name=>$diff{'file'})},
						      "diff");
1819
				}
1820
				print " | ";
1821
			}
1822 1823 1824 1825 1826
			print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
						     file_name=>$diff{'file'})},
				      "blame") . " | ";
			print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
						     file_name=>$diff{'file'})},
1827
				      "history");
1828 1829
			print "</td>\n";

1830 1831 1832
		} elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
			my %status_name = ('R' => 'moved', 'C' => 'copied');
			my $nstatus = $status_name{$diff{'status'}};
1833
			my $mode_chng = "";
1834 1835 1836
			if ($diff{'from_mode'} != $diff{'to_mode'}) {
				# mode also for directories, so we cannot use $to_mode_str
				$mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
1837 1838
			}
			print "<td>" .
1839 1840 1841 1842 1843 1844 1845 1846
			      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
			                             hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
			              -class => "list"}, esc_html($diff{'to_file'})) . "</td>\n" .
			      "<td><span class=\"file_status $nstatus\">[$nstatus from " .
			      $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
			                             hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}),
			              -class => "list"}, esc_html($diff{'from_file'})) .
			      " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
1847
			      "<td class=\"link\">";
1848
			if ($diff{'to_id'} ne $diff{'from_id'}) {
1849
				if ($action eq 'commitdiff') {
1850 1851
					# link to patch
					$patchno++;
1852
					print $cgi->a({-href => "#patch$patchno"}, "patch");
1853
				} else {
1854 1855 1856 1857 1858
					print $cgi->a({-href => href(action=>"blobdiff",
								     hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
								     hash_base=>$hash, hash_parent_base=>$parent,
								     file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
						      "diff");
1859
				}
1860
				print " | ";
1861
			}
1862 1863 1864 1865 1866 1867
			print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
						     file_name=>$diff{'from_file'})},
				      "blame") . " | ";
			print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
						     file_name=>$diff{'from_file'})},
				      "history");
1868
			print "</td>\n";
1869

1870 1871 1872 1873 1874 1875
		} # we should not encounter Unmerged (U) or Unknown (X) status
		print "</tr>\n";
	}
	print "</table>\n";
}

1876
sub git_patchset_body {
1877
	my ($fd, $difftree, $hash, $hash_parent) = @_;
1878 1879 1880 1881

	my $patch_idx = 0;
	my $in_header = 0;
	my $patch_found = 0;
1882
	my $diffinfo;
1883 1884 1885

	print "<div class=\"patchset\">\n";

1886
	LINE:
1887
	while (my $patch_line = <$fd>) {
1888
		chomp $patch_line;
1889 1890 1891 1892 1893 1894 1895 1896 1897 1898

		if ($patch_line =~ m/^diff /) { # "git diff" header
			# beginning of patch (in patchset)
			if ($patch_found) {
				# close previous patch
				print "</div>\n"; # class="patch"
			} else {
				# first patch in patchset
				$patch_found = 1;
			}
1899
			print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1900

1901 1902 1903 1904 1905 1906
			if (ref($difftree->[$patch_idx]) eq "HASH") {
				$diffinfo = $difftree->[$patch_idx];
			} else {
				$diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
			}
			$patch_idx++;
1907 1908 1909

			# for now, no extended header, hence we skip empty patches
			# companion to	next LINE if $in_header;
1910
			if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change
1911 1912 1913 1914
				$in_header = 1;
				next LINE;
			}

1915 1916
			if ($diffinfo->{'status'} eq "A") { # added
				print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
1917
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1918 1919
				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'to_id'}) . "(new)" .
1920 1921
				      "</div>\n"; # class="diff_info"

1922 1923
			} elsif ($diffinfo->{'status'} eq "D") { # deleted
				print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
1924
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1925 1926
				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'from_id'}) . "(deleted)" .
1927 1928
				      "</div>\n"; # class="diff_info"

1929
			} elsif ($diffinfo->{'status'} eq "R" || # renamed
1930 1931
			         $diffinfo->{'status'} eq "C" || # copied
			         $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff)
1932
				print "<div class=\"diff_info\">" .
1933
				      file_type($diffinfo->{'from_mode'}) . ":" .
1934
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1935 1936
				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'from_file'})},
				              $diffinfo->{'from_id'}) .
1937
				      " -> " .
1938
				      file_type($diffinfo->{'to_mode'}) . ":" .
1939
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1940 1941
				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'to_file'})},
				              $diffinfo->{'to_id'});
1942 1943 1944 1945
				print "</div>\n"; # class="diff_info"

			} else { # modified, mode changed, ...
				print "<div class=\"diff_info\">" .
1946
				      file_type($diffinfo->{'from_mode'}) . ":" .
1947
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1948 1949
				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'from_id'}) .
1950
				      " -> " .
1951
				      file_type($diffinfo->{'to_mode'}) . ":" .
1952
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1953 1954
				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'to_id'});
1955 1956 1957 1958 1959 1960 1961 1962 1963 1964
				print "</div>\n"; # class="diff_info"
			}

			#print "<div class=\"diff extended_header\">\n";
			$in_header = 1;
			next LINE;
		} # start of patch in patchset


		if ($in_header && $patch_line =~ m/^---/) {
1965
			#print "</div>\n"; # class="diff extended_header"
1966
			$in_header = 0;
1967 1968 1969

			my $file = $diffinfo->{'from_file'};
			$file  ||= $diffinfo->{'file'};
1970 1971 1972 1973 1974
			$file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
			                               hash=>$diffinfo->{'from_id'}, file_name=>$file),
			                -class => "list"}, esc_html($file));
			$patch_line =~ s|a/.*$|a/$file|g;
			print "<div class=\"diff from_file\">$patch_line</div>\n";
1975 1976 1977 1978 1979 1980 1981

			$patch_line = <$fd>;
			chomp $patch_line;

			#$patch_line =~ m/^+++/;
			$file    = $diffinfo->{'to_file'};
			$file  ||= $diffinfo->{'file'};
1982 1983 1984 1985 1986
			$file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
			                               hash=>$diffinfo->{'to_id'}, file_name=>$file),
			                -class => "list"}, esc_html($file));
			$patch_line =~ s|b/.*|b/$file|g;
			print "<div class=\"diff to_file\">$patch_line</div>\n";
1987 1988

			next LINE;
1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000
		}
		next LINE if $in_header;

		print format_diff_line($patch_line);
	}
	print "</div>\n" if $patch_found; # class="patch"

	print "</div>\n"; # class="patchset"
}

# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

2001 2002 2003
sub git_shortlog_body {
	# uses global variable $project
	my ($revlist, $from, $to, $refs, $extra) = @_;
2004

2005 2006 2007 2008
	$from = 0 unless defined $from;
	$to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);

	print "<table class=\"shortlog\" cellspacing=\"0\">\n";
2009
	my $alternate = 1;
2010 2011
	for (my $i = $from; $i <= $to; $i++) {
		my $commit = $revlist->[$i];
2012 2013 2014
		#my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
2015 2016 2017 2018 2019 2020 2021 2022 2023 2024
		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
		# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
		      "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
		      "<td>";
2025 2026
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
2027 2028
		print "</td>\n" .
		      "<td class=\"link\">" .
2029
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
L
Luben Tuikov 已提交
2030 2031
		      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") . " | " .
		      $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
A
Aneesh Kumar K.V 已提交
2032
		print "</td>\n" .
2033 2034 2035 2036 2037 2038 2039 2040 2041 2042
		      "</tr>\n";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"4\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

2043 2044
sub git_history_body {
	# Warning: assumes constant type (blob or tree) during history
J
Jakub Narebski 已提交
2045 2046 2047 2048
	my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;

	$from = 0 unless defined $from;
	$to = $#{$revlist} unless (defined $to && $to <= $#{$revlist});
2049 2050

	print "<table class=\"history\" cellspacing=\"0\">\n";
2051
	my $alternate = 1;
J
Jakub Narebski 已提交
2052 2053
	for (my $i = $from; $i <= $to; $i++) {
		if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075
			next;
		}

		my $commit = $1;
		my %co = parse_commit($commit);
		if (!%co) {
			next;
		}

		my $ref = format_ref_marker($refs, $commit);

		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
		      # shortlog uses      chop_str($co{'author_name'}, 10)
		      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
		      "<td>";
		# originally git_history used chop_str($co{'title'}, 50)
2076 2077
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
2078 2079
		print "</td>\n" .
		      "<td class=\"link\">" .
2080 2081
		      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
2082 2083 2084 2085 2086 2087 2088

		if ($ftype eq 'blob') {
			my $blob_current = git_get_hash_by_path($hash_base, $file_name);
			my $blob_parent  = git_get_hash_by_path($commit, $file_name);
			if (defined $blob_current && defined $blob_parent &&
					$blob_current ne $blob_parent) {
				print " | " .
2089 2090 2091 2092
					$cgi->a({-href => href(action=>"blobdiff",
					                       hash=>$blob_current, hash_parent=>$blob_parent,
					                       hash_base=>$hash_base, hash_parent_base=>$commit,
					                       file_name=>$file_name)},
2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106
					        "diff to current");
			}
		}
		print "</td>\n" .
		      "</tr>\n";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"4\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

2107 2108 2109 2110 2111 2112 2113
sub git_tags_body {
	# uses global variable $project
	my ($taglist, $from, $to, $extra) = @_;
	$from = 0 unless defined $from;
	$to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);

	print "<table class=\"tags\" cellspacing=\"0\">\n";
2114
	my $alternate = 1;
2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131
	for (my $i = $from; $i <= $to; $i++) {
		my $entry = $taglist->[$i];
		my %tag = %$entry;
		my $comment_lines = $tag{'comment'};
		my $comment = shift @$comment_lines;
		my $comment_short;
		if (defined $comment) {
			$comment_short = chop_str($comment, 30, 5);
		}
		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
		print "<td><i>$tag{'age'}</i></td>\n" .
		      "<td>" .
2132
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
2133
		               -class => "list name"}, esc_html($tag{'name'})) .
2134 2135 2136
		      "</td>\n" .
		      "<td>";
		if (defined $comment) {
2137 2138
			print format_subject_html($comment, $comment_short,
			                          href(action=>"tag", hash=>$tag{'id'}));
2139 2140 2141 2142
		}
		print "</td>\n" .
		      "<td class=\"selflink\">";
		if ($tag{'type'} eq "tag") {
2143
			print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
2144 2145 2146 2147 2148
		} else {
			print "&nbsp;";
		}
		print "</td>\n" .
		      "<td class=\"link\">" . " | " .
2149
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
2150
		if ($tag{'reftype'} eq "commit") {
2151 2152
			print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
			      " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
2153
		} elsif ($tag{'reftype'} eq "blob") {
2154
			print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168
		}
		print "</td>\n" .
		      "</tr>";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"5\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

sub git_heads_body {
	# uses global variable $project
2169
	my ($headlist, $head, $from, $to, $extra) = @_;
2170
	$from = 0 unless defined $from;
2171
	$to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
2172 2173

	print "<table class=\"heads\" cellspacing=\"0\">\n";
2174
	my $alternate = 1;
2175
	for (my $i = $from; $i <= $to; $i++) {
2176
		my $entry = $headlist->[$i];
2177 2178 2179 2180 2181 2182 2183 2184 2185 2186
		my %tag = %$entry;
		my $curr = $tag{'id'} eq $head;
		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
		print "<td><i>$tag{'age'}</i></td>\n" .
		      ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") .
2187
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
2188
		               -class => "list name"},esc_html($tag{'name'})) .
2189 2190
		      "</td>\n" .
		      "<td class=\"link\">" .
2191
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
2192 2193
		      $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . " | " .
		      $cgi->a({-href => href(action=>"tree", hash=>$tag{'name'}, hash_base=>$tag{'name'})}, "tree") .
2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209
		      "</td>\n" .
		      "</tr>";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"3\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

## ======================================================================
## ======================================================================
## actions

sub git_project_list {
2210 2211
	my $order = $cgi->param('o');
	if (defined $order && $order !~ m/project|descr|owner|age/) {
2212
		die_error(undef, "Unknown order parameter");
2213 2214
	}

2215
	my @list = git_get_projects_list();
2216 2217
	my @projects;
	if (!@list) {
2218
		die_error(undef, "No projects found");
2219 2220
	}
	foreach my $pr (@list) {
2221
		my $head = git_get_head_hash($pr->{'path'});
2222 2223
		if (!defined $head) {
			next;
2224
		}
2225
		$git_dir = "$projectroot/$pr->{'path'}";
2226
		my %co = parse_commit($head);
2227 2228
		if (!%co) {
			next;
2229
		}
2230 2231
		$pr->{'commit'} = \%co;
		if (!defined $pr->{'descr'}) {
2232
			my $descr = git_get_project_description($pr->{'path'}) || "";
2233
			$pr->{'descr'} = chop_str($descr, 25, 5);
2234
		}
2235 2236
		if (!defined $pr->{'owner'}) {
			$pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
2237
		}
2238
		push @projects, $pr;
2239
	}
2240

2241 2242 2243 2244 2245 2246 2247
	git_header_html();
	if (-f $home_text) {
		print "<div class=\"index_include\">\n";
		open (my $fd, $home_text);
		print <$fd>;
		close $fd;
		print "</div>\n";
2248
	}
2249 2250
	print "<table class=\"project_list\">\n" .
	      "<tr>\n";
2251 2252
	$order ||= "project";
	if ($order eq "project") {
2253 2254 2255
		@projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
		print "<th>Project</th>\n";
	} else {
2256
		print "<th>" .
2257
		      $cgi->a({-href => href(project=>undef, order=>'project'),
2258 2259
		               -class => "header"}, "Project") .
		      "</th>\n";
2260
	}
2261
	if ($order eq "descr") {
2262 2263 2264
		@projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
		print "<th>Description</th>\n";
	} else {
2265
		print "<th>" .
2266
		      $cgi->a({-href => href(project=>undef, order=>'descr'),
2267 2268
		               -class => "header"}, "Description") .
		      "</th>\n";
2269
	}
2270
	if ($order eq "owner") {
2271 2272 2273
		@projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
		print "<th>Owner</th>\n";
	} else {
2274
		print "<th>" .
2275
		      $cgi->a({-href => href(project=>undef, order=>'owner'),
2276 2277
		               -class => "header"}, "Owner") .
		      "</th>\n";
2278
	}
2279
	if ($order eq "age") {
2280 2281 2282
		@projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
		print "<th>Last Change</th>\n";
	} else {
2283
		print "<th>" .
2284
		      $cgi->a({-href => href(project=>undef, order=>'age'),
2285 2286
		               -class => "header"}, "Last Change") .
		      "</th>\n";
2287 2288 2289
	}
	print "<th></th>\n" .
	      "</tr>\n";
2290
	my $alternate = 1;
2291
	foreach my $pr (@projects) {
2292 2293 2294 2295 2296 2297
		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
2298
		print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
2299
		                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
2300 2301
		      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
		      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
2302 2303
		print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" .
		      $pr->{'commit'}{'age_string'} . "</td>\n" .
2304
		      "<td class=\"link\">" .
2305 2306
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
2307 2308
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
2309 2310 2311 2312
		      "</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
2313
	git_footer_html();
2314 2315
}

2316 2317 2318 2319 2320 2321
sub git_project_index {
	my @projects = git_get_projects_list();

	print $cgi->header(
		-type => 'text/plain',
		-charset => 'utf-8',
2322
		-content_disposition => 'inline; filename="index.aux"');
2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339

	foreach my $pr (@projects) {
		if (!exists $pr->{'owner'}) {
			$pr->{'owner'} = get_file_owner("$projectroot/$project");
		}

		my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
		# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
		$path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
		$owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
		$path  =~ s/ /\+/g;
		$owner =~ s/ /\+/g;

		print "$path $owner\n";
	}
}

K
v142  
Kay Sievers 已提交
2340
sub git_summary {
2341 2342 2343 2344
	my $descr = git_get_project_description($project) || "none";
	my $head = git_get_head_hash($project);
	my %co = parse_commit($head);
	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
K
v142  
Kay Sievers 已提交
2345

2346
	my $owner = git_get_project_owner($project);
K
v142  
Kay Sievers 已提交
2347

2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360
	my ($reflist, $refs) = git_get_refs_list();

	my @taglist;
	my @headlist;
	foreach my $ref (@$reflist) {
		if ($ref->{'name'} =~ s!^heads/!!) {
			push @headlist, $ref;
		} else {
			$ref->{'name'} =~ s!^tags/!!;
			push @taglist, $ref;
		}
	}

K
v142  
Kay Sievers 已提交
2361
	git_header_html();
2362
	git_print_page_nav('summary','', $head);
2363

K
v203  
Kay Sievers 已提交
2364
	print "<div class=\"title\">&nbsp;</div>\n";
K
v160  
Kay Sievers 已提交
2365
	print "<table cellspacing=\"0\">\n" .
2366
	      "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
K
v142  
Kay Sievers 已提交
2367
	      "<tr><td>owner</td><td>$owner</td></tr>\n" .
2368
	      "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
2369 2370
	# use per project git URL list in $projectroot/$project/cloneurl
	# or make project git URL from git base URL and project name
2371
	my $url_tag = "URL";
2372 2373 2374 2375 2376
	my @url_list = git_get_project_url_list($project);
	@url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
	foreach my $git_url (@url_list) {
		next unless $git_url;
		print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
2377 2378 2379
		$url_tag = "";
	}
	print "</table>\n";
2380

2381 2382
	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
		git_get_head_hash($project)
2383
		or die_error(undef, "Open git-rev-list failed");
2384
	my @revlist = map { chomp; $_ } <$fd>;
K
v142  
Kay Sievers 已提交
2385
	close $fd;
2386
	git_print_header_div('shortlog');
2387
	git_shortlog_body(\@revlist, 0, 15, $refs,
2388
	                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
K
v142  
Kay Sievers 已提交
2389

2390
	if (@taglist) {
2391
		git_print_header_div('tags');
2392
		git_tags_body(\@taglist, 0, 15,
2393
		              $cgi->a({-href => href(action=>"tags")}, "..."));
K
v142  
Kay Sievers 已提交
2394
	}
K
v150  
Kay Sievers 已提交
2395

2396
	if (@headlist) {
2397
		git_print_header_div('heads');
2398
		git_heads_body(\@headlist, $head, 0, 15,
2399
		               $cgi->a({-href => href(action=>"heads")}, "..."));
K
v150  
Kay Sievers 已提交
2400
	}
2401

K
v142  
Kay Sievers 已提交
2402 2403 2404
	git_footer_html();
}

K
v235  
Kay Sievers 已提交
2405
sub git_tag {
2406
	my $head = git_get_head_hash($project);
K
v235  
Kay Sievers 已提交
2407
	git_header_html();
2408 2409 2410
	git_print_page_nav('','', $head,undef,$head);
	my %tag = parse_tag($hash);
	git_print_header_div('commit', esc_html($tag{'name'}), $hash);
K
v235  
Kay Sievers 已提交
2411 2412
	print "<div class=\"title_text\">\n" .
	      "<table cellspacing=\"0\">\n" .
2413 2414
	      "<tr>\n" .
	      "<td>object</td>\n" .
2415 2416 2417 2418
	      "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
	                       $tag{'object'}) . "</td>\n" .
	      "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
	                                      $tag{'type'}) . "</td>\n" .
2419
	      "</tr>\n";
K
v235  
Kay Sievers 已提交
2420
	if (defined($tag{'author'})) {
2421
		my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
2422
		print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
2423 2424 2425
		print "<tr><td></td><td>" . $ad{'rfc2822'} .
			sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
			"</td></tr>\n";
K
v235  
Kay Sievers 已提交
2426 2427 2428 2429 2430 2431
	}
	print "</table>\n\n" .
	      "</div>\n";
	print "<div class=\"page_body\">";
	my $comment = $tag{'comment'};
	foreach my $line (@$comment) {
2432
		print esc_html($line) . "<br/>\n";
K
v235  
Kay Sievers 已提交
2433 2434 2435 2436 2437
	}
	print "</div>\n";
	git_footer_html();
}

2438 2439 2440
sub git_blame2 {
	my $fd;
	my $ftype;
2441

A
Aneesh Kumar K.V 已提交
2442 2443
	my ($have_blame) = gitweb_check_feature('blame');
	if (!$have_blame) {
2444 2445
		die_error('403 Permission denied', "Permission denied");
	}
2446
	die_error('404 Not Found', "File name not defined") if (!$file_name);
2447
	$hash_base ||= git_get_head_hash($project);
2448
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2449
	my %co = parse_commit($hash_base)
2450 2451 2452 2453 2454 2455 2456
		or die_error(undef, "Reading commit failed");
	if (!defined $hash) {
		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
			or die_error(undef, "Error looking up file");
	}
	$ftype = git_get_type($hash);
	if ($ftype !~ "blob") {
2457
		die_error("400 Bad Request", "Object is not a blob");
2458
	}
2459
	open ($fd, "-|", git_cmd(), "blame", '-l', '--', $file_name, $hash_base)
2460
		or die_error(undef, "Open git-blame failed");
2461
	git_header_html();
2462
	my $formats_nav =
2463 2464 2465
		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
		        "blob") .
		" | " .
2466 2467 2468
		$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
			"history") .
		" | " .
2469
		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
P
Petr Baudis 已提交
2470
		        "HEAD");
2471 2472
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2473
	git_print_page_path($file_name, $ftype, $hash_base);
2474
	my @rev_color = (qw(light2 dark2));
2475 2476 2477
	my $num_colors = scalar(@rev_color);
	my $current_color = 0;
	my $last_rev;
J
Jakub Narebski 已提交
2478 2479 2480 2481 2482
	print <<HTML;
<div class="page_body">
<table class="blame">
<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
HTML
2483 2484 2485
	while (<$fd>) {
		/^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
		my $full_rev = $1;
2486
		my $rev = substr($full_rev, 0, 8);
2487 2488
		my $lineno = $2;
		my $data = $3;
2489

2490 2491 2492 2493 2494 2495 2496
		if (!defined $last_rev) {
			$last_rev = $full_rev;
		} elsif ($last_rev ne $full_rev) {
			$last_rev = $full_rev;
			$current_color = ++$current_color % $num_colors;
		}
		print "<tr class=\"$rev_color[$current_color]\">\n";
2497
		print "<td class=\"sha1\">" .
2498 2499 2500 2501
			$cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)},
			        esc_html($rev)) . "</td>\n";
		print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" .
		      esc_html($lineno) . "</a></td>\n";
2502 2503 2504 2505 2506
		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
		print "</tr>\n";
	}
	print "</table>\n";
	print "</div>";
2507 2508
	close $fd
		or print "Reading blob failed\n";
2509 2510 2511
	git_footer_html();
}

2512 2513
sub git_blame {
	my $fd;
2514

A
Aneesh Kumar K.V 已提交
2515 2516
	my ($have_blame) = gitweb_check_feature('blame');
	if (!$have_blame) {
2517 2518
		die_error('403 Permission denied', "Permission denied");
	}
2519
	die_error('404 Not Found', "File name not defined") if (!$file_name);
2520
	$hash_base ||= git_get_head_hash($project);
2521
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2522
	my %co = parse_commit($hash_base)
2523
		or die_error(undef, "Reading commit failed");
2524 2525
	if (!defined $hash) {
		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
2526
			or die_error(undef, "Error lookup file");
2527
	}
2528
	open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
2529
		or die_error(undef, "Open git-annotate failed");
2530
	git_header_html();
2531
	my $formats_nav =
2532 2533 2534
		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
		        "blob") .
		" | " .
2535 2536 2537
		$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
			"history") .
		" | " .
2538
		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
P
Petr Baudis 已提交
2539
		        "HEAD");
2540 2541
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2542
	git_print_page_path($file_name, 'blob', $hash_base);
2543 2544
	print "<div class=\"page_body\">\n";
	print <<HTML;
2545
<table class="blame">
2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565
  <tr>
    <th>Commit</th>
    <th>Age</th>
    <th>Author</th>
    <th>Line</th>
    <th>Data</th>
  </tr>
HTML
	my @line_class = (qw(light dark));
	my $line_class_len = scalar (@line_class);
	my $line_class_num = $#line_class;
	while (my $line = <$fd>) {
		my $long_rev;
		my $short_rev;
		my $author;
		my $time;
		my $lineno;
		my $data;
		my $age;
		my $age_str;
2566
		my $age_class;
2567 2568 2569 2570

		chomp $line;
		$line_class_num = ($line_class_num + 1) % $line_class_len;

2571
		if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
2572 2573 2574 2575 2576 2577
			$long_rev = $1;
			$author   = $2;
			$time     = $3;
			$lineno   = $4;
			$data     = $5;
		} else {
2578
			print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
2579 2580 2581 2582 2583 2584
			next;
		}
		$short_rev  = substr ($long_rev, 0, 8);
		$age        = time () - $time;
		$age_str    = age_string ($age);
		$age_str    =~ s/ /&nbsp;/g;
2585
		$age_class  = age_class($age);
2586 2587
		$author     = esc_html ($author);
		$author     =~ s/ /&nbsp;/g;
2588 2589

		$data = untabify($data);
2590
		$data = esc_html ($data);
2591

2592 2593
		print <<HTML;
  <tr class="$line_class[$line_class_num]">
2594
    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
2595 2596 2597 2598 2599 2600 2601 2602
    <td class="$age_class">$age_str</td>
    <td>$author</td>
    <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
    <td class="pre">$data</td>
  </tr>
HTML
	} # while (my $line = <$fd>)
	print "</table>\n\n";
2603 2604
	close $fd
		or print "Reading blob failed.\n";
2605 2606
	print "</div>";
	git_footer_html();
2607 2608
}

2609
sub git_tags {
2610
	my $head = git_get_head_hash($project);
2611
	git_header_html();
2612 2613
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2614

2615
	my ($taglist) = git_get_refs_list("tags");
2616
	if (@$taglist) {
2617
		git_tags_body($taglist);
2618
	}
2619
	git_footer_html();
2620 2621
}

2622
sub git_heads {
2623
	my $head = git_get_head_hash($project);
2624
	git_header_html();
2625 2626
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2627

2628
	my ($headlist) = git_get_refs_list("heads");
2629
	if (@$headlist) {
2630
		git_heads_body($headlist, $head);
2631
	}
2632
	git_footer_html();
2633 2634
}

K
v203  
Kay Sievers 已提交
2635
sub git_blob_plain {
2636 2637
	my $expires;

2638
	if (!defined $hash) {
J
Jakub Narebski 已提交
2639
		if (defined $file_name) {
2640
			my $base = $hash_base || git_get_head_hash($project);
J
Jakub Narebski 已提交
2641
			$hash = git_get_hash_by_path($base, $file_name, "blob")
2642
				or die_error(undef, "Error lookup file");
J
Jakub Narebski 已提交
2643
		} else {
2644
			die_error(undef, "No file name defined");
J
Jakub Narebski 已提交
2645
		}
2646 2647 2648
	} elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
		# blobs defined by non-textual hash id's can be cached
		$expires = "+1d";
J
Jakub Narebski 已提交
2649
	}
2650

2651
	my $type = shift;
2652
	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2653
		or die_error(undef, "Couldn't cat $file_name, $hash");
2654

2655
	$type ||= blob_mimetype($fd, $file_name);
2656 2657 2658

	# save as filename, even when no $file_name is given
	my $save_as = "$hash";
2659 2660
	if (defined $file_name) {
		$save_as = $file_name;
2661 2662
	} elsif ($type =~ m/^text\//) {
		$save_as .= '.txt';
2663
	}
2664

2665 2666 2667
	print $cgi->header(
		-type => "$type",
		-expires=>$expires,
2668
		-content_disposition => 'inline; filename="' . "$save_as" . '"');
K
v203  
Kay Sievers 已提交
2669
	undef $/;
2670
	binmode STDOUT, ':raw';
K
v203  
Kay Sievers 已提交
2671
	print <$fd>;
2672
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
K
v203  
Kay Sievers 已提交
2673 2674 2675 2676
	$/ = "\n";
	close $fd;
}

2677
sub git_blob {
2678 2679
	my $expires;

2680
	if (!defined $hash) {
J
Jakub Narebski 已提交
2681
		if (defined $file_name) {
2682
			my $base = $hash_base || git_get_head_hash($project);
J
Jakub Narebski 已提交
2683
			$hash = git_get_hash_by_path($base, $file_name, "blob")
2684
				or die_error(undef, "Error lookup file");
J
Jakub Narebski 已提交
2685
		} else {
2686
			die_error(undef, "No file name defined");
J
Jakub Narebski 已提交
2687
		}
2688 2689 2690
	} elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
		# blobs defined by non-textual hash id's can be cached
		$expires = "+1d";
J
Jakub Narebski 已提交
2691
	}
2692

A
Aneesh Kumar K.V 已提交
2693
	my ($have_blame) = gitweb_check_feature('blame');
2694
	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2695
		or die_error(undef, "Couldn't cat $file_name, $hash");
2696
	my $mimetype = blob_mimetype($fd, $file_name);
2697 2698 2699 2700
	if ($mimetype !~ m/^text\//) {
		close $fd;
		return git_blob_plain($mimetype);
	}
2701
	git_header_html(undef, $expires);
2702
	my $formats_nav = '';
2703
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2704 2705
		if (defined $file_name) {
			if ($have_blame) {
2706 2707 2708 2709 2710
				$formats_nav .=
					$cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
					                       hash=>$hash, file_name=>$file_name)},
					        "blame") .
					" | ";
2711
			}
2712
			$formats_nav .=
2713 2714 2715 2716
				$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
				                       hash=>$hash, file_name=>$file_name)},
				        "history") .
				" | " .
2717 2718
				$cgi->a({-href => href(action=>"blob_plain",
				                       hash=>$hash, file_name=>$file_name)},
2719
				        "raw") .
2720 2721 2722
				" | " .
				$cgi->a({-href => href(action=>"blob",
				                       hash_base=>"HEAD", file_name=>$file_name)},
P
Petr Baudis 已提交
2723
				        "HEAD");
2724
		} else {
2725
			$formats_nav .=
2726
				$cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw");
2727
		}
2728 2729
		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2730 2731 2732 2733 2734
	} else {
		print "<div class=\"page_nav\">\n" .
		      "<br/><br/></div>\n" .
		      "<div class=\"title\">$hash</div>\n";
	}
2735
	git_print_page_path($file_name, "blob", $hash_base);
2736 2737 2738 2739 2740
	print "<div class=\"page_body\">\n";
	my $nr;
	while (my $line = <$fd>) {
		chomp $line;
		$nr++;
2741
		$line = untabify($line);
2742 2743
		printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
		       $nr, $nr, $nr, esc_html($line);
2744
	}
2745 2746
	close $fd
		or print "Reading blob failed.\n";
2747 2748 2749 2750
	print "</div>";
	git_footer_html();
}

K
v118  
Kay Sievers 已提交
2751
sub git_tree {
2752
	my $have_snapshot = gitweb_have_snapshot();
2753

2754 2755 2756
	if (!defined $hash_base) {
		$hash_base = "HEAD";
	}
K
v107  
Kay Sievers 已提交
2757
	if (!defined $hash) {
K
v118  
Kay Sievers 已提交
2758
		if (defined $file_name) {
2759 2760 2761
			$hash = git_get_hash_by_path($hash_base, $file_name, "tree");
		} else {
			$hash = $hash_base;
K
v157  
Kay Sievers 已提交
2762
		}
K
v145  
Kay Sievers 已提交
2763
	}
2764
	$/ = "\0";
2765
	open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
2766
		or die_error(undef, "Open git-ls-tree failed");
2767
	my @entries = map { chomp; $_ } <$fd>;
2768
	close $fd or die_error(undef, "Reading tree failed");
2769
	$/ = "\n";
K
v077  
Kay Sievers 已提交
2770

2771 2772
	my $refs = git_get_references();
	my $ref = format_ref_marker($refs, $hash_base);
K
v021  
Kay Sievers 已提交
2773
	git_header_html();
K
v118  
Kay Sievers 已提交
2774
	my $base = "";
A
Aneesh Kumar K.V 已提交
2775
	my ($have_blame) = gitweb_check_feature('blame');
2776
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2777 2778 2779 2780 2781 2782 2783 2784
		my @views_nav = ();
		if (defined $file_name) {
			push @views_nav,
				$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
				                       hash=>$hash, file_name=>$file_name)},
				        "history"),
				$cgi->a({-href => href(action=>"tree",
				                       hash_base=>"HEAD", file_name=>$file_name)},
P
Petr Baudis 已提交
2785
				        "HEAD"),
2786 2787 2788 2789
		}
		if ($have_snapshot) {
			# FIXME: Should be available when we have no hash base as well.
			push @views_nav,
P
Petr Baudis 已提交
2790
				$cgi->a({-href => href(action=>"snapshot", hash=>$hash)},
2791 2792 2793
					"snapshot");
		}
		git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
2794
		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
K
v077  
Kay Sievers 已提交
2795
	} else {
2796
		undef $hash_base;
K
v077  
Kay Sievers 已提交
2797 2798 2799 2800
		print "<div class=\"page_nav\">\n";
		print "<br/><br/></div>\n";
		print "<div class=\"title\">$hash</div>\n";
	}
K
v118  
Kay Sievers 已提交
2801
	if (defined $file_name) {
2802
		$base = esc_html("$file_name/");
K
v118  
Kay Sievers 已提交
2803
	}
2804
	git_print_page_path($file_name, 'tree', $hash_base);
K
v043  
Kay Sievers 已提交
2805
	print "<div class=\"page_body\">\n";
K
v125  
Kay Sievers 已提交
2806
	print "<table cellspacing=\"0\">\n";
2807
	my $alternate = 1;
K
Kay Sievers 已提交
2808
	foreach my $line (@entries) {
2809 2810
		my %t = parse_ls_tree_line($line, -z => 1);

K
v160  
Kay Sievers 已提交
2811
		if ($alternate) {
K
v220  
Kay Sievers 已提交
2812
			print "<tr class=\"dark\">\n";
K
v160  
Kay Sievers 已提交
2813
		} else {
K
v220  
Kay Sievers 已提交
2814
			print "<tr class=\"light\">\n";
K
v160  
Kay Sievers 已提交
2815 2816
		}
		$alternate ^= 1;
2817

2818 2819
		git_print_tree_entry(\%t, $base, $hash_base, $have_blame);

K
v125  
Kay Sievers 已提交
2820
		print "</tr>\n";
K
Kay Sievers 已提交
2821
	}
K
v125  
Kay Sievers 已提交
2822 2823
	print "</table>\n" .
	      "</div>";
K
v021  
Kay Sievers 已提交
2824
	git_footer_html();
K
v118  
Kay Sievers 已提交
2825 2826
}

A
Aneesh Kumar K.V 已提交
2827
sub git_snapshot {
2828 2829 2830 2831 2832 2833
	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
	my $have_snapshot = (defined $ctype && defined $suffix);
	if (!$have_snapshot) {
		die_error('403 Permission denied', "Permission denied");
	}

A
Aneesh Kumar K.V 已提交
2834 2835 2836 2837
	if (!defined $hash) {
		$hash = git_get_head_hash($project);
	}

2838
	my $filename = basename($project) . "-$hash.tar.$suffix";
A
Aneesh Kumar K.V 已提交
2839

2840 2841 2842
	print $cgi->header(
		-type => 'application/x-tar',
		-content_encoding => $ctype,
2843
		-content_disposition => 'inline; filename="' . "$filename" . '"',
2844
		-status => '200 OK');
A
Aneesh Kumar K.V 已提交
2845

2846 2847 2848 2849 2850 2851
	my $git = git_cmd_str();
	my $name = $project;
	$name =~ s/\047/\047\\\047\047/g;
	open my $fd, "-|",
	"$git archive --format=tar --prefix=\'$name\'/ $hash | $command"
		or die_error(undef, "Execute git-tar-tree failed.");
A
Aneesh Kumar K.V 已提交
2852 2853 2854 2855 2856 2857 2858
	binmode STDOUT, ':raw';
	print <$fd>;
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
	close $fd;

}

K
v118  
Kay Sievers 已提交
2859
sub git_log {
2860
	my $head = git_get_head_hash($project);
K
v150  
Kay Sievers 已提交
2861
	if (!defined $hash) {
K
v203  
Kay Sievers 已提交
2862
		$hash = $head;
K
v150  
Kay Sievers 已提交
2863
	}
K
v206  
Kay Sievers 已提交
2864 2865
	if (!defined $page) {
		$page = 0;
K
v107  
Kay Sievers 已提交
2866
	}
2867
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
2868 2869

	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
2870
	open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
2871
		or die_error(undef, "Open git-rev-list failed");
2872
	my @revlist = map { chomp; $_ } <$fd>;
K
v206  
Kay Sievers 已提交
2873 2874
	close $fd;

2875
	my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
2876 2877

	git_header_html();
2878
	git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
2879

K
v107  
Kay Sievers 已提交
2880
	if (!@revlist) {
2881
		my %co = parse_commit($hash);
2882

2883
		git_print_header_div('summary', $project);
K
v145  
Kay Sievers 已提交
2884
		print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
K
Kay Sievers 已提交
2885
	}
K
v220  
Kay Sievers 已提交
2886 2887
	for (my $i = ($page * 100); $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
2888 2889
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
K
v107  
Kay Sievers 已提交
2890
		next if !%co;
2891 2892
		my %ad = parse_date($co{'author_epoch'});
		git_print_header_div('commit',
2893 2894 2895
		               "<span class=\"age\">$co{'age_string'}</span>" .
		               esc_html($co{'title'}) . $ref,
		               $commit);
K
v088  
Kay Sievers 已提交
2896 2897
		print "<div class=\"title_text\">\n" .
		      "<div class=\"log_link\">\n" .
2898
		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
2899 2900
		      " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
2901
		      " | " .
2902
		      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
K
v121  
Kay Sievers 已提交
2903
		      "<br/>\n" .
K
v088  
Kay Sievers 已提交
2904
		      "</div>\n" .
2905
		      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
2906 2907 2908 2909
		      "</div>\n";

		print "<div class=\"log_body\">\n";
		git_print_simplified_log($co{'comment'});
K
v118  
Kay Sievers 已提交
2910
		print "</div>\n";
K
v021  
Kay Sievers 已提交
2911
	}
K
v088  
Kay Sievers 已提交
2912
	git_footer_html();
K
v118  
Kay Sievers 已提交
2913 2914 2915
}

sub git_commit {
2916
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
2917
	if (!%co) {
2918
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
2919
	}
2920 2921
	my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
K
Kay Sievers 已提交
2922

K
v235  
Kay Sievers 已提交
2923 2924
	my $parent = $co{'parent'};
	if (!defined $parent) {
2925
		$parent = "--root";
K
v085  
Kay Sievers 已提交
2926
	}
2927
	open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash
2928
		or die_error(undef, "Open git-diff-tree failed");
2929
	my @difftree = map { chomp; $_ } <$fd>;
2930
	close $fd or die_error(undef, "Reading git-diff-tree failed");
2931 2932 2933 2934 2935 2936

	# non-textual hash id's can be cached
	my $expires;
	if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
		$expires = "+1d";
	}
2937 2938
	my $refs = git_get_references();
	my $ref = format_ref_marker($refs, $co{'id'});
2939

2940
	my $have_snapshot = gitweb_have_snapshot();
2941

2942
	my @views_nav = ();
2943
	if (defined $file_name && defined $co{'parent'}) {
2944
		push @views_nav,
2945 2946
			$cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
			        "blame");
2947
	}
2948
	git_header_html(undef, $expires);
2949
	git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
2950
	                   $hash, $co{'tree'}, $hash,
2951
	                   join (' | ', @views_nav));
2952

K
v107  
Kay Sievers 已提交
2953
	if (defined $co{'parent'}) {
2954
		git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
K
v107  
Kay Sievers 已提交
2955
	} else {
2956
		git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
K
v107  
Kay Sievers 已提交
2957
	}
K
v085  
Kay Sievers 已提交
2958
	print "<div class=\"title_text\">\n" .
K
v107  
Kay Sievers 已提交
2959
	      "<table cellspacing=\"0\">\n";
2960
	print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
K
v160  
Kay Sievers 已提交
2961 2962
	      "<tr>" .
	      "<td></td><td> $ad{'rfc2822'}";
K
v080  
Kay Sievers 已提交
2963
	if ($ad{'hour_local'} < 6) {
2964 2965
		printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
K
v107  
Kay Sievers 已提交
2966
	} else {
2967 2968
		printf(" (%02d:%02d %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
K
v107  
Kay Sievers 已提交
2969
	}
K
v160  
Kay Sievers 已提交
2970 2971
	print "</td>" .
	      "</tr>\n";
2972
	print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
2973 2974 2975
	print "<tr><td></td><td> $cd{'rfc2822'}" .
	      sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
	      "</td></tr>\n";
2976
	print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
K
v160  
Kay Sievers 已提交
2977 2978
	print "<tr>" .
	      "<td>tree</td>" .
2979
	      "<td class=\"sha1\">" .
2980 2981
	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
	               class => "list"}, $co{'tree'}) .
K
v203  
Kay Sievers 已提交
2982
	      "</td>" .
2983 2984 2985
	      "<td class=\"link\">" .
	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
	              "tree");
A
Aneesh Kumar K.V 已提交
2986
	if ($have_snapshot) {
2987 2988
		print " | " .
		      $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
A
Aneesh Kumar K.V 已提交
2989 2990
	}
	print "</td>" .
K
v160  
Kay Sievers 已提交
2991
	      "</tr>\n";
2992
	my $parents = $co{'parents'};
K
v025  
Kay Sievers 已提交
2993
	foreach my $par (@$parents) {
K
v160  
Kay Sievers 已提交
2994 2995
		print "<tr>" .
		      "<td>parent</td>" .
2996 2997 2998 2999
		      "<td class=\"sha1\">" .
		      $cgi->a({-href => href(action=>"commit", hash=>$par),
		               class => "list"}, $par) .
		      "</td>" .
K
v160  
Kay Sievers 已提交
3000
		      "<td class=\"link\">" .
3001
		      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
3002
		      " | " .
3003
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
K
v160  
Kay Sievers 已提交
3004 3005
		      "</td>" .
		      "</tr>\n";
K
v025  
Kay Sievers 已提交
3006
	}
J
Jakub Narebski 已提交
3007
	print "</table>".
K
v107  
Kay Sievers 已提交
3008
	      "</div>\n";
3009

K
v043  
Kay Sievers 已提交
3010
	print "<div class=\"page_body\">\n";
3011
	git_print_log($co{'comment'});
K
v080  
Kay Sievers 已提交
3012
	print "</div>\n";
3013

3014
	git_difftree_body(\@difftree, $hash, $parent);
3015

K
v021  
Kay Sievers 已提交
3016
	git_footer_html();
K
v118  
Kay Sievers 已提交
3017 3018 3019
}

sub git_blobdiff {
3020 3021
	my $format = shift || 'html';

3022 3023 3024
	my $fd;
	my @difftree;
	my %diffinfo;
3025
	my $expires;
3026 3027 3028 3029 3030 3031

	# preparing $fd and %diffinfo for git_patchset_body
	# new style URI
	if (defined $hash_base && defined $hash_parent_base) {
		if (defined $file_name) {
			# read raw output
3032
			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base,
3033 3034 3035 3036 3037 3038 3039 3040
				"--", $file_name
				or die_error(undef, "Open git-diff-tree failed");
			@difftree = map { chomp; $_ } <$fd>;
			close $fd
				or die_error(undef, "Reading git-diff-tree failed");
			@difftree
				or die_error('404 Not Found', "Blob diff not found");

3041 3042 3043
		} elsif (defined $hash &&
		         $hash =~ /[0-9a-fA-F]{40}/) {
			# try to find filename from $hash
3044 3045

			# read filtered raw output
3046
			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base
3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072
				or die_error(undef, "Open git-diff-tree failed");
			@difftree =
				# ':100644 100644 03b21826... 3b93d5e7... M	ls-files.c'
				# $hash == to_id
				grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
				map { chomp; $_ } <$fd>;
			close $fd
				or die_error(undef, "Reading git-diff-tree failed");
			@difftree
				or die_error('404 Not Found', "Blob diff not found");

		} else {
			die_error('404 Not Found', "Missing one of the blob diff parameters");
		}

		if (@difftree > 1) {
			die_error('404 Not Found', "Ambiguous blob diff specification");
		}

		%diffinfo = parse_difftree_raw_line($difftree[0]);
		$file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'};
		$file_name   ||= $diffinfo{'to_file'}   || $diffinfo{'file'};

		$hash_parent ||= $diffinfo{'from_id'};
		$hash        ||= $diffinfo{'to_id'};

3073 3074 3075 3076 3077 3078
		# non-textual hash id's can be cached
		if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
		    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
			$expires = '+1d';
		}

3079
		# open patch output
3080
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3081
			'-p', $hash_parent_base, $hash_base,
3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095
			"--", $file_name
			or die_error(undef, "Open git-diff-tree failed");
	}

	# old/legacy style URI
	if (!%diffinfo && # if new style URI failed
	    defined $hash && defined $hash_parent) {
		# fake git-diff-tree raw output
		$diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
		$diffinfo{'from_id'} = $hash_parent;
		$diffinfo{'to_id'}   = $hash;
		if (defined $file_name) {
			if (defined $file_parent) {
				$diffinfo{'status'} = '2';
3096 3097
				$diffinfo{'from_file'} = $file_parent;
				$diffinfo{'to_file'}   = $file_name;
3098 3099
			} else { # assume not renamed
				$diffinfo{'status'} = '1';
3100 3101
				$diffinfo{'from_file'} = $file_name;
				$diffinfo{'to_file'}   = $file_name;
3102 3103 3104 3105 3106 3107 3108
			}
		} else { # no filename given
			$diffinfo{'status'} = '2';
			$diffinfo{'from_file'} = $hash_parent;
			$diffinfo{'to_file'}   = $hash;
		}

3109 3110 3111 3112 3113 3114 3115
		# non-textual hash id's can be cached
		if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
		    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
			$expires = '+1d';
		}

		# open patch output
3116
		open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, $hash_parent, $hash
3117 3118 3119 3120 3121 3122 3123
			or die_error(undef, "Open git-diff failed");
	} else  {
		die_error('404 Not Found', "Missing one of the blob diff parameters")
			unless %diffinfo;
	}

	# header
3124 3125 3126 3127 3128 3129
	if ($format eq 'html') {
		my $formats_nav =
			$cgi->a({-href => href(action=>"blobdiff_plain",
			                       hash=>$hash, hash_parent=>$hash_parent,
			                       hash_base=>$hash_base, hash_parent_base=>$hash_parent_base,
			                       file_name=>$file_name, file_parent=>$file_parent)},
3130
			        "raw");
3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149
		git_header_html(undef, $expires);
		if (defined $hash_base && (my %co = parse_commit($hash_base))) {
			git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
			git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
		} else {
			print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
			print "<div class=\"title\">$hash vs $hash_parent</div>\n";
		}
		if (defined $file_name) {
			git_print_page_path($file_name, "blob", $hash_base);
		} else {
			print "<div class=\"page_path\"></div>\n";
		}

	} elsif ($format eq 'plain') {
		print $cgi->header(
			-type => 'text/plain',
			-charset => 'utf-8',
			-expires => $expires,
3150
			-content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
3151 3152 3153

		print "X-Git-Url: " . $cgi->self_url() . "\n\n";

3154
	} else {
3155
		die_error(undef, "Unknown blobdiff format");
3156 3157 3158
	}

	# patch
3159 3160
	if ($format eq 'html') {
		print "<div class=\"page_body\">\n";
3161

3162 3163
		git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
		close $fd;
3164

3165 3166 3167 3168 3169
		print "</div>\n"; # class="page_body"
		git_footer_html();

	} else {
		while (my $line = <$fd>) {
3170 3171
			$line =~ s!a/($hash|$hash_parent)!'a/'.esc_html($diffinfo{'from_file'})!eg;
			$line =~ s!b/($hash|$hash_parent)!'b/'.esc_html($diffinfo{'to_file'})!eg;
3172 3173 3174 3175 3176 3177 3178 3179 3180

			print $line;

			last if $line =~ m!^\+\+\+!;
		}
		local $/ = undef;
		print <$fd>;
		close $fd;
	}
K
v118  
Kay Sievers 已提交
3181 3182
}

K
v203  
Kay Sievers 已提交
3183
sub git_blobdiff_plain {
3184
	git_blobdiff('plain');
K
v203  
Kay Sievers 已提交
3185 3186
}

K
v118  
Kay Sievers 已提交
3187
sub git_commitdiff {
3188
	my $format = shift || 'html';
3189
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
3190
	if (!%co) {
3191
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
3192
	}
K
v160  
Kay Sievers 已提交
3193
	if (!defined $hash_parent) {
3194
		$hash_parent = $co{'parent'} || '--root';
K
v160  
Kay Sievers 已提交
3195
	}
3196 3197 3198 3199 3200

	# read commitdiff
	my $fd;
	my @difftree;
	if ($format eq 'html') {
3201
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3202 3203 3204 3205 3206 3207 3208 3209 3210 3211
			"--patch-with-raw", "--full-index", $hash_parent, $hash
			or die_error(undef, "Open git-diff-tree failed");

		while (chomp(my $line = <$fd>)) {
			# empty line ends raw part of diff-tree output
			last unless $line;
			push @difftree, $line;
		}

	} elsif ($format eq 'plain') {
3212
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3213
			'-p', $hash_parent, $hash
3214
			or die_error(undef, "Open git-diff-tree failed");
3215

3216 3217 3218
	} else {
		die_error(undef, "Unknown commitdiff format");
	}
K
Kay Sievers 已提交
3219

3220 3221 3222 3223 3224
	# non-textual hash id's can be cached
	my $expires;
	if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
		$expires = "+1d";
	}
K
v118  
Kay Sievers 已提交
3225

3226 3227 3228 3229 3230 3231 3232
	# write commit message
	if ($format eq 'html') {
		my $refs = git_get_references();
		my $ref = format_ref_marker($refs, $co{'id'});
		my $formats_nav =
			$cgi->a({-href => href(action=>"commitdiff_plain",
			                       hash=>$hash, hash_parent=>$hash_parent)},
3233
			        "raw");
K
v232  
Kay Sievers 已提交
3234

3235 3236 3237
		git_header_html(undef, $expires);
		git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
3238
		git_print_authorship(\%co);
3239 3240 3241 3242 3243 3244 3245
		print "<div class=\"page_body\">\n";
		print "<div class=\"log\">\n";
		git_print_simplified_log($co{'comment'}, 1); # skip title
		print "</div>\n"; # class="log"

	} elsif ($format eq 'plain') {
		my $refs = git_get_references("tags");
3246
		my $tagname = git_get_rev_name_tags($hash);
3247 3248 3249 3250 3251 3252
		my $filename = basename($project) . "-$hash.patch";

		print $cgi->header(
			-type => 'text/plain',
			-charset => 'utf-8',
			-expires => $expires,
3253
			-content_disposition => 'inline; filename="' . "$filename" . '"');
3254 3255
		my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
		print <<TEXT;
J
Jakub Narebski 已提交
3256 3257 3258 3259
From: $co{'author'}
Date: $ad{'rfc2822'} ($ad{'tz_local'})
Subject: $co{'title'}
TEXT
3260
		print "X-Git-Tag: $tagname\n" if $tagname;
3261
		print "X-Git-Url: " . $cgi->self_url() . "\n\n";
3262

3263 3264 3265 3266
		foreach my $line (@{$co{'comment'}}) {
			print "$line\n";
		}
		print "---\n\n";
K
v232  
Kay Sievers 已提交
3267 3268
	}

3269 3270
	# write patch
	if ($format eq 'html') {
3271 3272
		git_difftree_body(\@difftree, $hash, $hash_parent);
		print "<br/>\n";
K
v232  
Kay Sievers 已提交
3273

3274 3275
		git_patchset_body($fd, \@difftree, $hash, $hash_parent);
		close $fd;
3276 3277 3278 3279 3280 3281 3282 3283
		print "</div>\n"; # class="page_body"
		git_footer_html();

	} elsif ($format eq 'plain') {
		local $/ = undef;
		print <$fd>;
		close $fd
			or print "Reading git-diff-tree failed\n";
K
v203  
Kay Sievers 已提交
3284 3285 3286
	}
}

3287 3288 3289 3290
sub git_commitdiff_plain {
	git_commitdiff('plain');
}

K
v118  
Kay Sievers 已提交
3291
sub git_history {
3292
	if (!defined $hash_base) {
3293
		$hash_base = git_get_head_hash($project);
K
v118  
Kay Sievers 已提交
3294
	}
J
Jakub Narebski 已提交
3295 3296 3297
	if (!defined $page) {
		$page = 0;
	}
3298
	my $ftype;
3299
	my %co = parse_commit($hash_base);
K
v118  
Kay Sievers 已提交
3300
	if (!%co) {
3301
		die_error(undef, "Unknown commit object");
K
v062  
Kay Sievers 已提交
3302
	}
J
Jakub Narebski 已提交
3303

3304
	my $refs = git_get_references();
J
Jakub Narebski 已提交
3305 3306
	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));

3307 3308 3309
	if (!defined $hash && defined $file_name) {
		$hash = git_get_hash_by_path($hash_base, $file_name);
	}
3310
	if (defined $hash) {
3311
		$ftype = git_get_type($hash);
3312
	}
K
v157  
Kay Sievers 已提交
3313

3314
	open my $fd, "-|",
J
Jakub Narebski 已提交
3315 3316 3317 3318 3319
		git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name
			or die_error(undef, "Open git-rev-list-failed");
	my @revlist = map { chomp; $_ } <$fd>;
	close $fd
		or die_error(undef, "Reading git-rev-list failed");
3320

J
Jakub Narebski 已提交
3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357
	my $paging_nav = '';
	if ($page > 0) {
		$paging_nav .=
			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
			                       file_name=>$file_name)},
			        "first");
		$paging_nav .= " &sdot; " .
			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
			                       file_name=>$file_name, page=>$page-1),
			         -accesskey => "p", -title => "Alt-p"}, "prev");
	} else {
		$paging_nav .= "first";
		$paging_nav .= " &sdot; prev";
	}
	if ($#revlist >= (100 * ($page+1)-1)) {
		$paging_nav .= " &sdot; " .
			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
			                       file_name=>$file_name, page=>$page+1),
			         -accesskey => "n", -title => "Alt-n"}, "next");
	} else {
		$paging_nav .= " &sdot; next";
	}
	my $next_link = '';
	if ($#revlist >= (100 * ($page+1)-1)) {
		$next_link =
			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
			                       file_name=>$file_name, page=>$page+1),
			         -title => "Alt-n"}, "next");
	}

	git_header_html();
	git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
	git_print_page_path($file_name, $ftype, $hash_base);

	git_history_body(\@revlist, ($page * 100), $#revlist,
	                 $refs, $hash_base, $ftype, $next_link);
3358

K
v057  
Kay Sievers 已提交
3359
	git_footer_html();
K
Kay Sievers 已提交
3360
}
K
v203  
Kay Sievers 已提交
3361 3362 3363

sub git_search {
	if (!defined $searchtext) {
3364
		die_error(undef, "Text field empty");
K
v203  
Kay Sievers 已提交
3365 3366
	}
	if (!defined $hash) {
3367
		$hash = git_get_head_hash($project);
K
v203  
Kay Sievers 已提交
3368
	}
3369
	my %co = parse_commit($hash);
K
v203  
Kay Sievers 已提交
3370
	if (!%co) {
3371
		die_error(undef, "Unknown commit object");
K
v203  
Kay Sievers 已提交
3372
	}
3373

K
v220  
Kay Sievers 已提交
3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384
	my $commit_search = 1;
	my $author_search = 0;
	my $committer_search = 0;
	my $pickaxe_search = 0;
	if ($searchtext =~ s/^author\\://i) {
		$author_search = 1;
	} elsif ($searchtext =~ s/^committer\\://i) {
		$committer_search = 1;
	} elsif ($searchtext =~ s/^pickaxe\\://i) {
		$commit_search = 0;
		$pickaxe_search = 1;
3385 3386 3387 3388 3389 3390 3391

		# pickaxe may take all resources of your box and run for several minutes
		# with every query - so decide by yourself how public you make this feature
		my ($have_pickaxe) = gitweb_check_feature('pickaxe');
		if (!$have_pickaxe) {
			die_error('403 Permission denied', "Permission denied");
		}
K
v220  
Kay Sievers 已提交
3392
	}
K
v203  
Kay Sievers 已提交
3393
	git_header_html();
3394 3395
	git_print_page_nav('','', $hash,$co{'tree'},$hash);
	git_print_header_div('commit', esc_html($co{'title'}), $hash);
K
v203  
Kay Sievers 已提交
3396 3397

	print "<table cellspacing=\"0\">\n";
3398
	my $alternate = 1;
K
v220  
Kay Sievers 已提交
3399 3400
	if ($commit_search) {
		$/ = "\0";
3401
		open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", $hash or next;
K
v220  
Kay Sievers 已提交
3402 3403 3404
		while (my $commit_text = <$fd>) {
			if (!grep m/$searchtext/i, $commit_text) {
				next;
K
v203  
Kay Sievers 已提交
3405
			}
K
v220  
Kay Sievers 已提交
3406 3407
			if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
				next;
K
v203  
Kay Sievers 已提交
3408
			}
K
v220  
Kay Sievers 已提交
3409
			if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
K
v203  
Kay Sievers 已提交
3410 3411
				next;
			}
K
v220  
Kay Sievers 已提交
3412
			my @commit_lines = split "\n", $commit_text;
3413
			my %co = parse_commit(undef, \@commit_lines);
K
v220  
Kay Sievers 已提交
3414 3415 3416 3417 3418 3419 3420 3421 3422
			if (!%co) {
				next;
			}
			if ($alternate) {
				print "<tr class=\"dark\">\n";
			} else {
				print "<tr class=\"light\">\n";
			}
			$alternate ^= 1;
K
v225  
Kay Sievers 已提交
3423
			print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3424
			      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
K
v220  
Kay Sievers 已提交
3425
			      "<td>" .
3426 3427
			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
			               esc_html(chop_str($co{'title'}, 50)) . "<br/>");
K
v220  
Kay Sievers 已提交
3428 3429 3430
			my $comment = $co{'comment'};
			foreach my $line (@$comment) {
				if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
3431
					my $lead = esc_html($1) || "";
K
v220  
Kay Sievers 已提交
3432
					$lead = chop_str($lead, 30, 10);
3433 3434
					my $match = esc_html($2) || "";
					my $trail = esc_html($3) || "";
K
v220  
Kay Sievers 已提交
3435
					$trail = chop_str($trail, 30, 10);
3436
					my $text = "$lead<span class=\"match\">$match</span>$trail";
K
v220  
Kay Sievers 已提交
3437
					print chop_str($text, 80, 5) . "<br/>\n";
K
v203  
Kay Sievers 已提交
3438
				}
K
v220  
Kay Sievers 已提交
3439 3440 3441
			}
			print "</td>\n" .
			      "<td class=\"link\">" .
3442
			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3443 3444
			      " | " .
			      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
K
v220  
Kay Sievers 已提交
3445 3446 3447 3448 3449 3450 3451 3452
			print "</td>\n" .
			      "</tr>\n";
		}
		close $fd;
	}

	if ($pickaxe_search) {
		$/ = "\n";
3453 3454 3455
		my $git_command = git_cmd_str();
		open my $fd, "-|", "$git_command rev-list $hash | " .
			"$git_command diff-tree -r --stdin -S\'$searchtext\'";
K
v220  
Kay Sievers 已提交
3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466
		undef %co;
		my @files;
		while (my $line = <$fd>) {
			if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
				my %set;
				$set{'file'} = $6;
				$set{'from_id'} = $3;
				$set{'to_id'} = $4;
				$set{'id'} = $set{'to_id'};
				if ($set{'id'} =~ m/0{40}/) {
					$set{'id'} = $set{'from_id'};
K
v203  
Kay Sievers 已提交
3467
				}
K
v220  
Kay Sievers 已提交
3468 3469 3470 3471
				if ($set{'id'} =~ m/0{40}/) {
					next;
				}
				push @files, \%set;
3472
			} elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
K
v220  
Kay Sievers 已提交
3473 3474 3475 3476 3477 3478 3479
				if (%co) {
					if ($alternate) {
						print "<tr class=\"dark\">\n";
					} else {
						print "<tr class=\"light\">\n";
					}
					$alternate ^= 1;
K
v225  
Kay Sievers 已提交
3480
					print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
3481
					      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
K
v220  
Kay Sievers 已提交
3482
					      "<td>" .
3483 3484
					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
					              -class => "list subject"},
3485
					              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
K
v220  
Kay Sievers 已提交
3486 3487
					while (my $setref = shift @files) {
						my %set = %$setref;
3488 3489 3490 3491
						print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
						                             hash=>$set{'id'}, file_name=>$set{'file'}),
						              -class => "list"},
						              "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
K
v220  
Kay Sievers 已提交
3492 3493 3494 3495
						      "<br/>\n";
					}
					print "</td>\n" .
					      "<td class=\"link\">" .
3496
					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
3497 3498
					      " | " .
					      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
K
v220  
Kay Sievers 已提交
3499 3500 3501
					print "</td>\n" .
					      "</tr>\n";
				}
3502
				%co = parse_commit($1);
K
v203  
Kay Sievers 已提交
3503 3504
			}
		}
K
v220  
Kay Sievers 已提交
3505
		close $fd;
K
v203  
Kay Sievers 已提交
3506 3507 3508 3509 3510 3511
	}
	print "</table>\n";
	git_footer_html();
}

sub git_shortlog {
3512
	my $head = git_get_head_hash($project);
K
v203  
Kay Sievers 已提交
3513 3514 3515
	if (!defined $hash) {
		$hash = $head;
	}
K
v206  
Kay Sievers 已提交
3516 3517 3518
	if (!defined $page) {
		$page = 0;
	}
3519
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
3520 3521

	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
3522
	open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
3523
		or die_error(undef, "Open git-rev-list failed");
3524
	my @revlist = map { chomp; $_ } <$fd>;
K
v203  
Kay Sievers 已提交
3525
	close $fd;
K
v206  
Kay Sievers 已提交
3526

3527
	my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
3528 3529 3530
	my $next_link = '';
	if ($#revlist >= (100 * ($page+1)-1)) {
		$next_link =
3531
			$cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
3532 3533 3534
			         -title => "Alt-n"}, "next");
	}

3535 3536

	git_header_html();
3537 3538
	git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
	git_print_header_div('summary', $project);
3539

3540 3541
	git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);

K
v203  
Kay Sievers 已提交
3542 3543
	git_footer_html();
}
3544 3545 3546 3547 3548 3549

## ......................................................................
## feeds (RSS, OPML)

sub git_rss {
	# http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3550
	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150", git_get_head_hash($project)
3551
		or die_error(undef, "Open git-rev-list failed");
3552
	my @revlist = map { chomp; $_ } <$fd>;
3553
	close $fd or die_error(undef, "Reading git-rev-list failed");
3554
	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
J
Jakub Narebski 已提交
3555 3556 3557 3558 3559 3560 3561 3562 3563
	print <<XML;
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>$project $my_uri $my_url</title>
<link>${\esc_html("$my_url?p=$project;a=summary")}</link>
<description>$project log</description>
<language>en</language>
XML
3564 3565 3566

	for (my $i = 0; $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
3567
		my %co = parse_commit($commit);
3568 3569 3570 3571
		# we read 150, we always show 30 and the ones more recent than 48 hours
		if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
			last;
		}
3572
		my %cd = parse_date($co{'committer_epoch'});
3573
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3574 3575
			$co{'parent'}, $co{'id'}
			or next;
3576
		my @difftree = map { chomp; $_ } <$fd>;
3577 3578
		close $fd
			or next;
3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599
		print "<item>\n" .
		      "<title>" .
		      sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
		      "</title>\n" .
		      "<author>" . esc_html($co{'author'}) . "</author>\n" .
		      "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
		      "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
		      "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
		      "<description>" . esc_html($co{'title'}) . "</description>\n" .
		      "<content:encoded>" .
		      "<![CDATA[\n";
		my $comment = $co{'comment'};
		foreach my $line (@$comment) {
			$line = decode("utf8", $line, Encode::FB_DEFAULT);
			print "$line<br/>\n";
		}
		print "<br/>\n";
		foreach my $line (@difftree) {
			if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
				next;
			}
3600
			my $file = esc_html(unquote($7));
3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611
			$file = decode("utf8", $file, Encode::FB_DEFAULT);
			print "$file<br/>\n";
		}
		print "]]>\n" .
		      "</content:encoded>\n" .
		      "</item>\n";
	}
	print "</channel></rss>";
}

sub git_opml {
3612
	my @list = git_get_projects_list();
3613 3614

	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
J
Jakub Narebski 已提交
3615 3616 3617 3618 3619 3620 3621 3622 3623
	print <<XML;
<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
<head>
  <title>$site_name Git OPML Export</title>
</head>
<body>
<outline text="git RSS feeds">
XML
3624 3625 3626

	foreach my $pr (@list) {
		my %proj = %$pr;
3627
		my $head = git_get_head_hash($proj{'path'});
3628 3629 3630
		if (!defined $head) {
			next;
		}
3631
		$git_dir = "$projectroot/$proj{'path'}";
3632
		my %co = parse_commit($head);
3633 3634 3635 3636 3637 3638 3639 3640 3641
		if (!%co) {
			next;
		}

		my $path = esc_html(chop_str($proj{'path'}, 25, 5));
		my $rss  = "$my_url?p=$proj{'path'};a=rss";
		my $html = "$my_url?p=$proj{'path'};a=summary";
		print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
	}
J
Jakub Narebski 已提交
3642 3643 3644 3645 3646
	print <<XML;
</outline>
</body>
</opml>
XML
3647
}