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

K
v118  
Kay Sievers 已提交
54
# source of projects list
55
our $projects_list = "++GITWEB_LIST++";
K
v107  
Kay Sievers 已提交
56

M
Matthias Lederhofer 已提交
57 58 59 60 61 62 63
# 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++";

64 65 66 67
# 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++");

68
# default blob_plain mimetype and default charset for text/plain blob
69 70
our $default_blob_plain_mimetype = 'text/plain';
our $default_text_plain_charset  = undef;
71

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

76 77
# You define site-wide feature defaults here; override them with
# $GITWEB_CONFIG as necessary.
78
our %feature = (
79 80 81 82 83 84 85 86 87 88
	# 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
89 90 91 92 93 94 95 96 97 98 99

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

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

	'pickaxe' => {
		'sub' => \&feature_pickaxe,
		'override' => 0,
		'default' => [1]},
105 106 107 108
);

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

# To enable system wide have in $GITWEB_CONFIG
119 120 121
# $feature{'blame'}{'default'} = [1];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'blame'}{'override'} = 1;
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
# 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
137 138 139
# $feature{'snapshot'}{'default'} = [undef];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'blame'}{'override'} = 1;
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
# 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);
}

158 159 160 161 162 163 164
sub gitweb_have_snapshot {
	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
	my $have_snapshot = (defined $ctype && defined $suffix);

	return $have_snapshot;
}

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
# 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]);
}

183 184 185 186 187 188 189 190 191 192
# 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

193
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
194
do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
195 196 197 198 199 200

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

$projects_list ||= $projectroot;

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

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

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

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

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

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

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

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

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

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

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

327 328 329
# path to the current git repository
our $git_dir;
$git_dir = "$projectroot/$project" if $project;
330

331
# dispatch
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
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 已提交
351
	"snapshot" => \&git_snapshot,
352 353 354
	# those below don't need $project
	"opml" => \&git_opml,
	"project_list" => \&git_project_list,
355
	"project_index" => \&git_project_index,
356 357
);

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

373 374 375 376
## ======================================================================
## action links

sub href(%) {
377 378 379
	my %params = @_;

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

394
	$params{'project'} = $project unless exists $params{'project'};
395

396 397 398 399 400 401 402 403
	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);
404 405 406
}


407 408 409
## ======================================================================
## validation, quoting/unquoting and escaping

410 411
sub validate_pathname {
	my $input = shift || return undef;
412

413 414 415 416 417
	# 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;
418
	}
419 420
	# no null characters
	if ($input =~ m!\0!) {
421 422
		return undef;
	}
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
	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 ~^:?*\[]|/$)!) {
438 439 440 441 442
		return undef;
	}
	return $input;
}

443 444 445
# 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 已提交
446
	my $str = shift;
447
	$str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
448
	$str =~ s/\+/%2B/g;
K
Kay Sievers 已提交
449
	$str =~ s/ /\+/g;
K
Kay Sievers 已提交
450 451 452
	return $str;
}

453 454 455 456 457 458 459 460 461
# 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;
}

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

471 472 473 474 475 476 477 478 479 480
# 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;
}

481 482 483 484 485 486 487 488 489 490 491 492 493 494
# 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 已提交
495 496 497 498 499 500
sub project_in_list {
	my $project = shift;
	my @list = git_get_projects_list();
	return @list && scalar(grep { $_->{'path'} eq $project } @list);
}

501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
## ----------------------------------------------------------------------
## 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

524 525 526 527 528 529 530 531 532 533 534 535 536
# 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";
	}
}

537 538 539 540
# convert age in seconds to "nn units ago" string
sub age_string {
	my $age = shift;
	my $age_str;
K
v055  
Kay Sievers 已提交
541

542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
	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";
563
	} else {
564
		$age_str .= " right now";
K
v000  
Kay Sievers 已提交
565
	}
566
	return $age_str;
K
Kay Sievers 已提交
567 568
}

569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
# 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 已提交
584
	} else {
585
		return '----------';
K
v048  
Kay Sievers 已提交
586
	}
K
Kay Sievers 已提交
587 588
}

589 590
# convert file mode in octal to file type string
sub file_type {
591 592 593 594 595 596 597
	my $mode = shift;

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

599 600 601 602 603 604 605 606 607
	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 已提交
608 609
}

610 611 612
## ----------------------------------------------------------------------
## functions returning short HTML fragments, or transforming HTML fragments
## which don't beling to other sections
613

614 615 616
# format line of commit message or tag comment
sub format_log_line_html {
	my $line = shift;
617

618 619 620 621 622
	$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") {
623 624 625
			my $link =
				$cgi->a({-href => href(action=>"commit", hash=>$hash_text),
				        -class => "text"}, $hash_text);
626
			$line =~ s/$hash_text/$link/;
627 628
		}
	}
629
	return $line;
630 631
}

632
# format marker of refs pointing to given object
633
sub format_ref_marker {
634
	my ($refs, $id) = @_;
635
	my $markers = '';
636

637
	if (defined $refs->{$id}) {
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654
		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>';
655 656 657
	} else {
		return "";
	}
658 659
}

660 661
# format, perhaps shortened and with markers, title line
sub format_subject_html {
662
	my ($long, $short, $href, $extra) = @_;
663 664 665
	$extra = '' unless defined($extra);

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

675 676 677 678 679 680 681 682 683 684 685 686 687 688
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 "\\") {
689
		$diff_class = " incomplete";
690 691 692 693 694
	}
	$line = untabify($line);
	return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
}

695 696
## ----------------------------------------------------------------------
## git utility subroutines, invoking git commands
K
v125  
Kay Sievers 已提交
697

698 699 700 701 702 703 704 705 706 707
# 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());
}

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

727 728 729 730
# get type of given object
sub git_get_type {
	my $hash = shift;

731
	open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
732 733 734 735 736 737 738
	my $type = <$fd>;
	close $fd or return;
	chomp $type;
	return $type;
}

sub git_get_project_config {
739
	my ($key, $type) = @_;
740 741 742 743 744

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

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

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

760
	$path =~ s,/+$,,;
761

762
	open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
763
		or die_error(undef, "Open git-ls-tree failed");
764 765 766 767 768
	my $line = <$fd>;
	close $fd or return undef;

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

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

779
sub git_get_project_description {
K
v107  
Kay Sievers 已提交
780
	my $path = shift;
K
v118  
Kay Sievers 已提交
781

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

789 790 791
sub git_get_project_url_list {
	my $path = shift;

792
	open my $fd, "$projectroot/$path/cloneurl" or return;
793 794 795 796 797 798
	my @git_project_url_list = map { chomp; $_ } <$fd>;
	close $fd;

	return wantarray ? @git_project_url_list : \@git_project_url_list;
}

799
sub git_get_projects_list {
800 801 802 803 804
	my @list;

	if (-d $projects_list) {
		# search in directory
		my $dir = $projects_list;
805 806 807 808 809 810 811 812 813 814 815 816 817
		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 已提交
818 819
				if (-e "$projectroot/$subdir/HEAD" && (!$export_ok ||
				    -e "$projectroot/$subdir/$export_ok")) {
820 821 822 823 824 825
					push @list, { path => $subdir };
					$File::Find::prune = 1;
				}
			},
		}, "$dir");

826 827 828 829 830
	} 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'
831
		open my ($fd), $projects_list or return;
832 833 834 835 836 837 838 839
		while (my $line = <$fd>) {
			chomp $line;
			my ($path, $owner) = split ' ', $line;
			$path = unescape($path);
			$owner = unescape($owner);
			if (!defined $path) {
				next;
			}
M
Matthias Lederhofer 已提交
840 841
			if (-e "$projectroot/$path/HEAD" && (!$export_ok ||
			    -e "$projectroot/$path/$export_ok")) {
842 843 844 845 846 847 848 849 850 851 852 853 854
				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;
}

855 856 857 858 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
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;
}

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

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

908 909 910
sub git_get_rev_name_tags {
	my $hash = shift || return undef;

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

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

924 925 926
## ----------------------------------------------------------------------
## parse to hash functions

927
sub parse_date {
928 929 930 931 932 933 934 935 936 937 938 939
	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];
940 941 942 943
	$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;
944 945 946 947 948 949 950 951 952 953

	$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;
}

954
sub parse_tag {
K
v142  
Kay Sievers 已提交
955 956
	my $tag_id = shift;
	my %tag;
K
v235  
Kay Sievers 已提交
957
	my @comment;
K
v142  
Kay Sievers 已提交
958

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

989
sub parse_commit {
K
v203  
Kay Sievers 已提交
990 991 992 993
	my $commit_id = shift;
	my $commit_text = shift;

	my @commit_lines;
K
v021  
Kay Sievers 已提交
994 995
	my %co;

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

K
v203  
Kay Sievers 已提交
1039
	foreach my $title (@commit_lines) {
1040
		$title =~ s/^    //;
K
v203  
Kay Sievers 已提交
1041
		if ($title ne "") {
K
v241  
Kay Sievers 已提交
1042
			$co{'title'} = chop_str($title, 80, 5);
K
v203  
Kay Sievers 已提交
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059
			# 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 已提交
1060
			$co{'title_short'} = chop_str($title, 50, 5);
K
v203  
Kay Sievers 已提交
1061 1062 1063
			last;
		}
	}
1064 1065 1066 1067 1068
	# remove added spaces
	foreach my $line (@commit_lines) {
		$line =~ s/^    //;
	}
	$co{'comment'} = \@commit_lines;
K
v062  
Kay Sievers 已提交
1069 1070 1071

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

1084 1085 1086 1087 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
# 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;
}

1127
# parse line of git-diff-tree "raw" output
1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141
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
1142
			($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
1143 1144 1145 1146 1147
		} else {
			$res{'file'} = unquote($7);
		}
	}
	# 'c512b523472485aef4fff9e57b229d9d243c967f'
1148 1149 1150
	elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
		$res{'commit'} = $1;
	}
1151 1152 1153 1154

	return wantarray ? %res : \%res;
}

1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175
# 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;
}

1176 1177
## ......................................................................
## parse to array of hashes functions
K
v000  
Kay Sievers 已提交
1178

1179
sub git_get_refs_list {
1180 1181
	my $type = shift || "";
	my %refs;
1182
	my @reflist;
K
v000  
Kay Sievers 已提交
1183

1184
	my @refs;
1185 1186 1187 1188
	open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
		or return;
	while (my $line = <$fd>) {
		chomp $line;
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203
		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";
			}
1204
		}
1205 1206 1207 1208 1209 1210
	}
	close $fd;

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

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

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

1222 1223
## ----------------------------------------------------------------------
## filesystem-related functions
K
v035  
Kay Sievers 已提交
1224

K
v133  
Kay Sievers 已提交
1225 1226 1227 1228 1229 1230 1231 1232 1233 1234
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/[,;].*$//;
1235
	return decode("utf8", $owner, Encode::FB_DEFAULT);
K
v133  
Kay Sievers 已提交
1236 1237
}

1238 1239
## ......................................................................
## mimetype related functions
K
v118  
Kay Sievers 已提交
1240

1241 1242 1243 1244 1245 1246 1247 1248
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>) {
1249
		next if m/^#/; # skip comments
1250
		my ($mime, $exts) = split(/\t+/);
1251 1252 1253 1254 1255
		if (defined $exts) {
			my @exts = split(/\s+/, $exts);
			foreach my $ext (@exts) {
				$mimemap{$ext} = $mime;
			}
K
v118  
Kay Sievers 已提交
1256 1257
		}
	}
1258
	close(MIME);
K
v118  
Kay Sievers 已提交
1259

1260
	$filename =~ /\.([^.]*)$/;
1261 1262
	return $mimemap{$1};
}
1263

1264 1265 1266 1267
sub mimetype_guess {
	my $filename = shift;
	my $mime;
	$filename =~ /\./ or return undef;
1268

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

1281
sub blob_mimetype {
1282 1283
	my $fd = shift;
	my $filename = shift;
1284

1285 1286 1287
	if ($filename) {
		my $mime = mimetype_guess($filename);
		$mime and return $mime;
1288
	}
1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303

	# 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';
1304
	} else {
1305
		return 'application/octet-stream';
1306
	}
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321
}

## ======================================================================
## 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) {
1322
				$title .= " - " . esc_html($file_name);
1323 1324 1325 1326 1327
				if ($action eq "tree" && $file_name !~ m|/$|) {
					$title .= "/";
				}
			}
		}
1328
	}
1329 1330 1331 1332 1333
	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.
1334 1335 1336
	if (defined $cgi->http('HTTP_ACCEPT') &&
	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
	    $cgi->Accept('application/xhtml+xml') != 0) {
1337
		$content_type = 'application/xhtml+xml';
1338
	} else {
1339
		$content_type = 'text/html';
1340
	}
1341 1342
	print $cgi->header(-type=>$content_type, -charset => 'utf-8',
	                   -status=> $status, -expires => $expires);
1343 1344 1345 1346
	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">
1347
<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
1348 1349 1350
<!-- git core binaries version $git_version -->
<head>
<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
1351
<meta name="generator" content="gitweb/$version git/$git_version"/>
1352 1353 1354 1355
<meta name="robots" content="index, nofollow"/>
<title>$title</title>
<link rel="stylesheet" type="text/css" href="$stylesheet"/>
EOF
M
Matthias Lederhofer 已提交
1356 1357 1358
	if (defined $project) {
		printf('<link rel="alternate" title="%s log" '.
		       'href="%s" type="application/rss+xml"/>'."\n",
1359
		       esc_param($project), href(action=>"rss"));
1360 1361 1362 1363 1364 1365 1366
	} 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 已提交
1367
	}
1368 1369 1370
	if (defined $favicon) {
		print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
	}
J
Jakub Narebski 已提交
1371

M
Matthias Lederhofer 已提交
1372 1373
	print "</head>\n" .
	      "<body>\n" .
J
Jakub Narebski 已提交
1374
	      "<div class=\"page_header\">\n" .
1375
	      "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
M
Martin Waitz 已提交
1376
	      "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
1377
	      "</a>\n";
1378
	print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
1379
	if (defined $project) {
1380
		print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392
		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 已提交
1393
		} else {
1394
			$search_hash = "HEAD";
K
v160  
Kay Sievers 已提交
1395
		}
1396 1397 1398 1399 1400 1401 1402 1403 1404 1405
		$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 已提交
1406
	}
1407 1408 1409 1410 1411 1412
	print "</div>\n";
}

sub git_footer_html {
	print "<div class=\"page_footer\">\n";
	if (defined $project) {
1413
		my $descr = git_get_project_description($project);
1414 1415 1416
		if (defined $descr) {
			print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
		}
1417 1418
		print $cgi->a({-href => href(action=>"rss"),
		              -class => "rss_logo"}, "RSS") . "\n";
1419
	} else {
1420
		print $cgi->a({-href => href(project=>undef, action=>"opml"),
1421 1422 1423
		              -class => "rss_logo"}, "OPML") . " ";
		print $cgi->a({-href => href(project=>undef, action=>"project_index"),
		              -class => "rss_logo"}, "TXT") . "\n";
1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434
	}
	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 已提交
1435 1436 1437 1438 1439 1440 1441
	print <<EOF;
<div class="page_body">
<br /><br />
$status - $error
<br />
</div>
EOF
K
v107  
Kay Sievers 已提交
1442
	git_footer_html();
1443
	exit;
K
Kay Sievers 已提交
1444 1445
}

1446 1447 1448
## ----------------------------------------------------------------------
## functions printing or outputting HTML: navigation

1449
sub git_print_page_nav {
1450 1451 1452 1453 1454 1455 1456 1457
	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;
	}

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

	print "<div class=\"page_nav\">\n" .
		(join " | ",
1474 1475 1476
		 map { $_ eq $current ?
		       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
		 } @navs);
1477 1478
	print "<br/>\n$extra<br/>\n" .
	      "</div>\n";
K
Kay Sievers 已提交
1479 1480
}

1481
sub format_paging_nav {
1482 1483
	my ($action, $hash, $head, $page, $nrevs) = @_;
	my $paging_nav;
1484

1485 1486

	if ($hash ne $head || $page) {
1487
		$paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
1488
	} else {
1489 1490 1491 1492 1493
		$paging_nav .= "HEAD";
	}

	if ($page > 0) {
		$paging_nav .= " &sdot; " .
1494
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
1495
			         -accesskey => "p", -title => "Alt-p"}, "prev");
1496 1497 1498 1499 1500 1501
	} else {
		$paging_nav .= " &sdot; prev";
	}

	if ($nrevs >= (100 * ($page+1)-1)) {
		$paging_nav .= " &sdot; " .
1502
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
1503
			         -accesskey => "n", -title => "Alt-n"}, "next");
1504 1505
	} else {
		$paging_nav .= " &sdot; next";
1506
	}
1507 1508

	return $paging_nav;
1509 1510
}

1511 1512 1513
## ......................................................................
## functions printing or outputting HTML: div

1514
sub git_print_header_div {
1515
	my ($action, $title, $hash, $hash_base) = @_;
1516
	my %args = ();
1517

1518 1519 1520
	$args{action} = $action;
	$args{hash} = $hash if $hash;
	$args{hash_base} = $hash_base if $hash_base;
1521 1522

	print "<div class=\"header\">\n" .
1523 1524 1525
	      $cgi->a({-href => href(%args), -class => "title"},
	      $title ? $title : $action) .
	      "\n</div>\n";
1526
}
K
v142  
Kay Sievers 已提交
1527

1528 1529 1530 1531
#sub git_print_authorship (\%) {
sub git_print_authorship {
	my $co = shift;

1532
	my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
1533 1534
	print "<div class=\"author_date\">" .
	      esc_html($co->{'author_name'}) .
1535 1536 1537 1538 1539 1540 1541 1542 1543
	      " [$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";
1544 1545
}

1546 1547 1548
sub git_print_page_path {
	my $name = shift;
	my $type = shift;
1549
	my $hb = shift;
K
v142  
Kay Sievers 已提交
1550

1551
	if (!defined $name) {
1552
		print "<div class=\"page_path\">/</div>\n";
1553 1554 1555 1556 1557
	} else {
		my @dirname = split '/', $name;
		my $basename = pop @dirname;
		my $fullname = '';

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

1584 1585
# sub git_print_log (\@;%) {
sub git_print_log ($;%) {
1586
	my $log = shift;
1587
	my %opts = @_;
1588

1589 1590 1591 1592
	if ($opts{'-remove_title'}) {
		# remove title, i.e. first line of log
		shift @$log;
	}
1593 1594 1595 1596 1597 1598 1599 1600 1601
	# 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) {
1602 1603
		if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
			$signoff = 1;
1604
			$empty = 0;
1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615
			if (! $opts{'-remove_signoff'}) {
				print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
				next;
			} else {
				# remove signoff lines
				next;
			}
		} else {
			$signoff = 0;
		}

1616 1617 1618 1619 1620 1621 1622 1623
		# print only one empty line
		# do not print empty line after signoff
		if ($line eq "") {
			next if ($empty || $signoff);
			$empty = 1;
		} else {
			$empty = 0;
		}
1624 1625 1626 1627 1628 1629 1630

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

	if ($opts{'-final_empty_line'}) {
		# end with single empty line
		print "<br/>\n" unless $empty;
1631 1632 1633 1634 1635 1636 1637
	}
}

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

1638 1639 1640
	git_print_log($log,
		-final_empty_line=> 1,
		-remove_title => $remove_title);
1641 1642
}

1643 1644 1645 1646 1647 1648 1649
# 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;

1650 1651 1652 1653
	# 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.

1654 1655 1656
	print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
	if ($t->{'type'} eq "blob") {
		print "<td class=\"list\">" .
1657 1658 1659 1660
			$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\">";
1661
		if ($have_blame) {
1662 1663 1664
			print $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
						     file_name=>"$basedir$t->{'name'}", %base_key)},
				      "blame");
1665 1666
		}
		if (defined $hash_base) {
1667 1668 1669 1670
			if ($have_blame) {
				print " | ";
			}
			print $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
1671 1672 1673 1674
			                             hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
			              "history");
		}
		print " | " .
1675 1676 1677
			$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
					       file_name=>"$basedir$t->{'name'}")},
				"raw");
1678
		print "</td>\n";
1679 1680

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

1696 1697 1698
## ......................................................................
## functions printing large fragments of HTML

1699
sub git_difftree_body {
1700
	my ($difftree, $hash, $parent) = @_;
1701 1702 1703 1704 1705 1706 1707 1708

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

	print "<table class=\"diff_tree\">\n";
1709
	my $alternate = 1;
1710
	my $patchno = 0;
1711
	foreach my $line (@{$difftree}) {
1712
		my %diff = parse_difftree_raw_line($line);
1713 1714 1715 1716 1717 1718 1719 1720

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

1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733
		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
1734
			}
1735 1736 1737 1738 1739 1740 1741
			$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>";
1742 1743
			print "<td>";
			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
1744
			                             hash_base=>$hash, file_name=>$diff{'file'}),
1745 1746 1747 1748
				       -class => "list"}, esc_html($diff{'file'}));
			print "</td>\n";
			print "<td>$mode_chng</td>\n";
			print "<td class=\"link\">";
1749
			if ($action eq 'commitdiff') {
1750 1751
				# link to patch
				$patchno++;
1752
				print $cgi->a({-href => "#patch$patchno"}, "patch");
1753 1754
			}
			print "</td>\n";
1755

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

1779
		} elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
1780
			my $mode_chnge = "";
1781 1782 1783 1784
			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";
1785
				}
1786 1787 1788 1789 1790
				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";
1791 1792 1793 1794 1795
					}
				}
				$mode_chnge .= "]</span>\n";
			}
			print "<td>";
1796 1797 1798 1799 1800 1801
			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\">";
1802
			if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
1803
				if ($action eq 'commitdiff') {
1804 1805
					# link to patch
					$patchno++;
1806
					print $cgi->a({-href => "#patch$patchno"}, "patch");
1807
				} else {
1808 1809 1810 1811 1812
					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");
1813
				}
1814
				print " | ";
1815
			}
1816 1817 1818 1819 1820
			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'})},
1821
				      "history");
1822 1823
			print "</td>\n";

1824 1825 1826
		} 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'}};
1827
			my $mode_chng = "";
1828 1829 1830
			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);
1831 1832
			}
			print "<td>" .
1833 1834 1835 1836 1837 1838 1839 1840
			      $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" .
1841
			      "<td class=\"link\">";
1842
			if ($diff{'to_id'} ne $diff{'from_id'}) {
1843
				if ($action eq 'commitdiff') {
1844 1845
					# link to patch
					$patchno++;
1846
					print $cgi->a({-href => "#patch$patchno"}, "patch");
1847
				} else {
1848 1849 1850 1851 1852
					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");
1853
				}
1854
				print " | ";
1855
			}
1856 1857 1858 1859 1860 1861
			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");
1862
			print "</td>\n";
1863

1864 1865 1866 1867 1868 1869
		} # we should not encounter Unmerged (U) or Unknown (X) status
		print "</tr>\n";
	}
	print "</table>\n";
}

1870
sub git_patchset_body {
1871
	my ($fd, $difftree, $hash, $hash_parent) = @_;
1872 1873 1874 1875

	my $patch_idx = 0;
	my $in_header = 0;
	my $patch_found = 0;
1876
	my $diffinfo;
1877 1878 1879

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

1880
	LINE:
1881
	while (my $patch_line = <$fd>) {
1882
		chomp $patch_line;
1883 1884 1885 1886 1887 1888 1889 1890 1891 1892

		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;
			}
1893
			print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
1894

1895 1896 1897 1898 1899 1900
			if (ref($difftree->[$patch_idx]) eq "HASH") {
				$diffinfo = $difftree->[$patch_idx];
			} else {
				$diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
			}
			$patch_idx++;
1901 1902 1903

			# for now, no extended header, hence we skip empty patches
			# companion to	next LINE if $in_header;
1904
			if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change
1905 1906 1907 1908
				$in_header = 1;
				next LINE;
			}

1909 1910
			if ($diffinfo->{'status'} eq "A") { # added
				print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
1911
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1912 1913
				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'to_id'}) . "(new)" .
1914 1915
				      "</div>\n"; # class="diff_info"

1916 1917
			} elsif ($diffinfo->{'status'} eq "D") { # deleted
				print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
1918
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1919 1920
				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'from_id'}) . "(deleted)" .
1921 1922
				      "</div>\n"; # class="diff_info"

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

			} else { # modified, mode changed, ...
				print "<div class=\"diff_info\">" .
1940
				      file_type($diffinfo->{'from_mode'}) . ":" .
1941
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
1942 1943
				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'from_id'}) .
1944
				      " -> " .
1945
				      file_type($diffinfo->{'to_mode'}) . ":" .
1946
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
1947 1948
				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
				              $diffinfo->{'to_id'});
1949 1950 1951 1952 1953 1954 1955 1956 1957 1958
				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/^---/) {
1959
			#print "</div>\n"; # class="diff extended_header"
1960
			$in_header = 0;
1961 1962 1963

			my $file = $diffinfo->{'from_file'};
			$file  ||= $diffinfo->{'file'};
1964 1965 1966 1967 1968
			$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";
1969 1970 1971 1972 1973 1974 1975

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

			#$patch_line =~ m/^+++/;
			$file    = $diffinfo->{'to_file'};
			$file  ||= $diffinfo->{'file'};
1976 1977 1978 1979 1980
			$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";
1981 1982

			next LINE;
1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994
		}
		next LINE if $in_header;

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

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

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

1995 1996 1997
sub git_shortlog_body {
	# uses global variable $project
	my ($revlist, $from, $to, $refs, $extra) = @_;
1998

1999 2000 2001 2002
	$from = 0 unless defined $from;
	$to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);

	print "<table class=\"shortlog\" cellspacing=\"0\">\n";
2003
	my $alternate = 1;
2004 2005
	for (my $i = $from; $i <= $to; $i++) {
		my $commit = $revlist->[$i];
2006 2007 2008
		#my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
2009 2010 2011 2012 2013 2014 2015 2016 2017 2018
		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>";
2019 2020
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
2021 2022
		print "</td>\n" .
		      "<td class=\"link\">" .
2023
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
L
Luben Tuikov 已提交
2024 2025
		      $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 已提交
2026
		print "</td>\n" .
2027 2028 2029 2030 2031 2032 2033 2034 2035 2036
		      "</tr>\n";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"4\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

2037 2038
sub git_history_body {
	# Warning: assumes constant type (blob or tree) during history
J
Jakub Narebski 已提交
2039 2040 2041 2042
	my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;

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

	print "<table class=\"history\" cellspacing=\"0\">\n";
2045
	my $alternate = 1;
J
Jakub Narebski 已提交
2046 2047
	for (my $i = $from; $i <= $to; $i++) {
		if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069
			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)
2070 2071
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
2072 2073
		print "</td>\n" .
		      "<td class=\"link\">" .
2074 2075
		      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
2076 2077 2078 2079 2080 2081 2082

		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 " | " .
2083 2084 2085 2086
					$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)},
2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100
					        "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";
}

2101 2102 2103 2104 2105 2106 2107
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";
2108
	my $alternate = 1;
2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125
	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>" .
2126
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
2127
		               -class => "list name"}, esc_html($tag{'name'})) .
2128 2129 2130
		      "</td>\n" .
		      "<td>";
		if (defined $comment) {
2131 2132
			print format_subject_html($comment, $comment_short,
			                          href(action=>"tag", hash=>$tag{'id'}));
2133 2134 2135 2136
		}
		print "</td>\n" .
		      "<td class=\"selflink\">";
		if ($tag{'type'} eq "tag") {
2137
			print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
2138 2139 2140 2141 2142
		} else {
			print "&nbsp;";
		}
		print "</td>\n" .
		      "<td class=\"link\">" . " | " .
2143
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
2144
		if ($tag{'reftype'} eq "commit") {
2145 2146
			print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
			      " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
2147
		} elsif ($tag{'reftype'} eq "blob") {
2148
			print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162
		}
		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
2163
	my ($headlist, $head, $from, $to, $extra) = @_;
2164
	$from = 0 unless defined $from;
2165
	$to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
2166 2167

	print "<table class=\"heads\" cellspacing=\"0\">\n";
2168
	my $alternate = 1;
2169
	for (my $i = $from; $i <= $to; $i++) {
2170
		my $entry = $headlist->[$i];
2171 2172 2173 2174 2175 2176 2177 2178 2179 2180
		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>") .
2181
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
2182
		               -class => "list name"},esc_html($tag{'name'})) .
2183 2184
		      "</td>\n" .
		      "<td class=\"link\">" .
2185
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
2186 2187
		      $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . " | " .
		      $cgi->a({-href => href(action=>"tree", hash=>$tag{'name'}, hash_base=>$tag{'name'})}, "tree") .
2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203
		      "</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 {
2204 2205
	my $order = $cgi->param('o');
	if (defined $order && $order !~ m/project|descr|owner|age/) {
2206
		die_error(undef, "Unknown order parameter");
2207 2208
	}

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

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

2310 2311 2312 2313 2314 2315
sub git_project_index {
	my @projects = git_get_projects_list();

	print $cgi->header(
		-type => 'text/plain',
		-charset => 'utf-8',
2316
		-content_disposition => 'inline; filename="index.aux"');
2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333

	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 已提交
2334
sub git_summary {
2335 2336 2337 2338
	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 已提交
2339

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

2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354
	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 已提交
2355
	git_header_html();
2356
	git_print_page_nav('summary','', $head);
2357

K
v203  
Kay Sievers 已提交
2358
	print "<div class=\"title\">&nbsp;</div>\n";
K
v160  
Kay Sievers 已提交
2359
	print "<table cellspacing=\"0\">\n" .
2360
	      "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
K
v142  
Kay Sievers 已提交
2361
	      "<tr><td>owner</td><td>$owner</td></tr>\n" .
2362
	      "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
2363 2364
	# use per project git URL list in $projectroot/$project/cloneurl
	# or make project git URL from git base URL and project name
2365
	my $url_tag = "URL";
2366 2367 2368 2369 2370
	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";
2371 2372 2373
		$url_tag = "";
	}
	print "</table>\n";
2374

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

2384
	if (@taglist) {
2385
		git_print_header_div('tags');
2386
		git_tags_body(\@taglist, 0, 15,
2387
		              $cgi->a({-href => href(action=>"tags")}, "..."));
K
v142  
Kay Sievers 已提交
2388
	}
K
v150  
Kay Sievers 已提交
2389

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

K
v142  
Kay Sievers 已提交
2396 2397 2398
	git_footer_html();
}

K
v235  
Kay Sievers 已提交
2399
sub git_tag {
2400
	my $head = git_get_head_hash($project);
K
v235  
Kay Sievers 已提交
2401
	git_header_html();
2402 2403 2404
	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 已提交
2405 2406
	print "<div class=\"title_text\">\n" .
	      "<table cellspacing=\"0\">\n" .
2407 2408
	      "<tr>\n" .
	      "<td>object</td>\n" .
2409 2410 2411 2412
	      "<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" .
2413
	      "</tr>\n";
K
v235  
Kay Sievers 已提交
2414
	if (defined($tag{'author'})) {
2415
		my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
2416
		print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
2417 2418 2419
		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 已提交
2420 2421 2422 2423 2424 2425
	}
	print "</table>\n\n" .
	      "</div>\n";
	print "<div class=\"page_body\">";
	my $comment = $tag{'comment'};
	foreach my $line (@$comment) {
2426
		print esc_html($line) . "<br/>\n";
K
v235  
Kay Sievers 已提交
2427 2428 2429 2430 2431
	}
	print "</div>\n";
	git_footer_html();
}

2432 2433 2434
sub git_blame2 {
	my $fd;
	my $ftype;
2435

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

2484 2485 2486 2487 2488 2489 2490
		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";
2491
		print "<td class=\"sha1\">" .
2492 2493 2494 2495
			$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";
2496 2497 2498 2499 2500
		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
		print "</tr>\n";
	}
	print "</table>\n";
	print "</div>";
2501 2502
	close $fd
		or print "Reading blob failed\n";
2503 2504 2505
	git_footer_html();
}

2506 2507
sub git_blame {
	my $fd;
2508

A
Aneesh Kumar K.V 已提交
2509 2510
	my ($have_blame) = gitweb_check_feature('blame');
	if (!$have_blame) {
2511 2512
		die_error('403 Permission denied', "Permission denied");
	}
2513
	die_error('404 Not Found', "File name not defined") if (!$file_name);
2514
	$hash_base ||= git_get_head_hash($project);
2515
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2516
	my %co = parse_commit($hash_base)
2517
		or die_error(undef, "Reading commit failed");
2518 2519
	if (!defined $hash) {
		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
2520
			or die_error(undef, "Error lookup file");
2521
	}
2522
	open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
2523
		or die_error(undef, "Open git-annotate failed");
2524
	git_header_html();
2525
	my $formats_nav =
2526 2527 2528
		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
		        "blob") .
		" | " .
2529 2530 2531
		$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
			"history") .
		" | " .
2532
		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
P
Petr Baudis 已提交
2533
		        "HEAD");
2534 2535
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2536
	git_print_page_path($file_name, 'blob', $hash_base);
2537 2538
	print "<div class=\"page_body\">\n";
	print <<HTML;
2539
<table class="blame">
2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559
  <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;
2560
		my $age_class;
2561 2562 2563 2564

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

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

		$data = untabify($data);
2584
		$data = esc_html ($data);
2585

2586 2587
		print <<HTML;
  <tr class="$line_class[$line_class_num]">
2588
    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
2589 2590 2591 2592 2593 2594 2595 2596
    <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";
2597 2598
	close $fd
		or print "Reading blob failed.\n";
2599 2600
	print "</div>";
	git_footer_html();
2601 2602
}

2603
sub git_tags {
2604
	my $head = git_get_head_hash($project);
2605
	git_header_html();
2606 2607
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2608

2609
	my ($taglist) = git_get_refs_list("tags");
2610
	if (@$taglist) {
2611
		git_tags_body($taglist);
2612
	}
2613
	git_footer_html();
2614 2615
}

2616
sub git_heads {
2617
	my $head = git_get_head_hash($project);
2618
	git_header_html();
2619 2620
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2621

2622
	my ($headlist) = git_get_refs_list("heads");
2623
	if (@$headlist) {
2624
		git_heads_body($headlist, $head);
2625
	}
2626
	git_footer_html();
2627 2628
}

K
v203  
Kay Sievers 已提交
2629
sub git_blob_plain {
2630 2631
	my $expires;

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

2645
	my $type = shift;
2646
	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
2647
		or die_error(undef, "Couldn't cat $file_name, $hash");
2648

2649
	$type ||= blob_mimetype($fd, $file_name);
2650 2651 2652

	# save as filename, even when no $file_name is given
	my $save_as = "$hash";
2653 2654
	if (defined $file_name) {
		$save_as = $file_name;
2655 2656
	} elsif ($type =~ m/^text\//) {
		$save_as .= '.txt';
2657
	}
2658

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

2671
sub git_blob {
2672 2673
	my $expires;

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

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

K
v118  
Kay Sievers 已提交
2745
sub git_tree {
2746
	my $have_snapshot = gitweb_have_snapshot();
2747

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

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

K
v160  
Kay Sievers 已提交
2805
		if ($alternate) {
K
v220  
Kay Sievers 已提交
2806
			print "<tr class=\"dark\">\n";
K
v160  
Kay Sievers 已提交
2807
		} else {
K
v220  
Kay Sievers 已提交
2808
			print "<tr class=\"light\">\n";
K
v160  
Kay Sievers 已提交
2809 2810
		}
		$alternate ^= 1;
2811

2812 2813
		git_print_tree_entry(\%t, $base, $hash_base, $have_blame);

K
v125  
Kay Sievers 已提交
2814
		print "</tr>\n";
K
Kay Sievers 已提交
2815
	}
K
v125  
Kay Sievers 已提交
2816 2817
	print "</table>\n" .
	      "</div>";
K
v021  
Kay Sievers 已提交
2818
	git_footer_html();
K
v118  
Kay Sievers 已提交
2819 2820
}

A
Aneesh Kumar K.V 已提交
2821
sub git_snapshot {
2822 2823 2824 2825 2826 2827
	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 已提交
2828 2829 2830 2831
	if (!defined $hash) {
		$hash = git_get_head_hash($project);
	}

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

2834 2835 2836
	print $cgi->header(
		-type => 'application/x-tar',
		-content_encoding => $ctype,
2837
		-content_disposition => 'inline; filename="' . "$filename" . '"',
2838
		-status => '200 OK');
A
Aneesh Kumar K.V 已提交
2839

2840 2841
	my $git_command = git_cmd_str();
	open my $fd, "-|", "$git_command tar-tree $hash \'$project\' | $command" or
2842
		die_error(undef, "Execute git-tar-tree failed.");
A
Aneesh Kumar K.V 已提交
2843 2844 2845 2846 2847 2848 2849
	binmode STDOUT, ':raw';
	print <$fd>;
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
	close $fd;

}

K
v118  
Kay Sievers 已提交
2850
sub git_log {
2851
	my $head = git_get_head_hash($project);
K
v150  
Kay Sievers 已提交
2852
	if (!defined $hash) {
K
v203  
Kay Sievers 已提交
2853
		$hash = $head;
K
v150  
Kay Sievers 已提交
2854
	}
K
v206  
Kay Sievers 已提交
2855 2856
	if (!defined $page) {
		$page = 0;
K
v107  
Kay Sievers 已提交
2857
	}
2858
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
2859 2860

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

2866
	my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
2867 2868

	git_header_html();
2869
	git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
2870

K
v107  
Kay Sievers 已提交
2871
	if (!@revlist) {
2872
		my %co = parse_commit($hash);
2873

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

		print "<div class=\"log_body\">\n";
		git_print_simplified_log($co{'comment'});
K
v118  
Kay Sievers 已提交
2901
		print "</div>\n";
K
v021  
Kay Sievers 已提交
2902
	}
K
v088  
Kay Sievers 已提交
2903
	git_footer_html();
K
v118  
Kay Sievers 已提交
2904 2905 2906
}

sub git_commit {
2907
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
2908
	if (!%co) {
2909
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
2910
	}
2911 2912
	my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
K
Kay Sievers 已提交
2913

K
v235  
Kay Sievers 已提交
2914 2915
	my $parent = $co{'parent'};
	if (!defined $parent) {
2916
		$parent = "--root";
K
v085  
Kay Sievers 已提交
2917
	}
2918
	open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash
2919
		or die_error(undef, "Open git-diff-tree failed");
2920
	my @difftree = map { chomp; $_ } <$fd>;
2921
	close $fd or die_error(undef, "Reading git-diff-tree failed");
2922 2923 2924 2925 2926 2927

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

2931
	my $have_snapshot = gitweb_have_snapshot();
2932

2933
	my @views_nav = ();
2934
	if (defined $file_name && defined $co{'parent'}) {
2935
		push @views_nav,
2936 2937
			$cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
			        "blame");
2938
	}
2939 2940 2941 2942 2943
	if (defined $co{'parent'}) {
		push @views_nav,
			$cgi->a({-href => href(action=>"shortlog", hash=>$hash)}, "shortlog"),
			$cgi->a({-href => href(action=>"log", hash=>$hash)}, "log");
	}
2944
	git_header_html(undef, $expires);
2945
	git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
2946
	                   $hash, $co{'tree'}, $hash,
2947
	                   join (' | ', @views_nav));
2948

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

K
v043  
Kay Sievers 已提交
3006
	print "<div class=\"page_body\">\n";
3007
	git_print_log($co{'comment'});
K
v080  
Kay Sievers 已提交
3008
	print "</div>\n";
3009

3010
	git_difftree_body(\@difftree, $hash, $parent);
3011

K
v021  
Kay Sievers 已提交
3012
	git_footer_html();
K
v118  
Kay Sievers 已提交
3013 3014 3015
}

sub git_blobdiff {
3016 3017
	my $format = shift || 'html';

3018 3019 3020
	my $fd;
	my @difftree;
	my %diffinfo;
3021
	my $expires;
3022 3023 3024 3025 3026 3027

	# 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
3028
			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base,
3029 3030 3031 3032 3033 3034 3035 3036
				"--", $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");

3037 3038 3039
		} elsif (defined $hash &&
		         $hash =~ /[0-9a-fA-F]{40}/) {
			# try to find filename from $hash
3040 3041

			# read filtered raw output
3042
			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base
3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068
				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'};

3069 3070 3071 3072 3073 3074
		# 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';
		}

3075
		# open patch output
3076
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3077
			'-p', $hash_parent_base, $hash_base,
3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091
			"--", $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';
3092 3093
				$diffinfo{'from_file'} = $file_parent;
				$diffinfo{'to_file'}   = $file_name;
3094 3095
			} else { # assume not renamed
				$diffinfo{'status'} = '1';
3096 3097
				$diffinfo{'from_file'} = $file_name;
				$diffinfo{'to_file'}   = $file_name;
3098 3099 3100 3101 3102 3103 3104
			}
		} else { # no filename given
			$diffinfo{'status'} = '2';
			$diffinfo{'from_file'} = $hash_parent;
			$diffinfo{'to_file'}   = $hash;
		}

3105 3106 3107 3108 3109 3110 3111
		# 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
3112
		open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, $hash_parent, $hash
3113 3114 3115 3116 3117 3118 3119
			or die_error(undef, "Open git-diff failed");
	} else  {
		die_error('404 Not Found', "Missing one of the blob diff parameters")
			unless %diffinfo;
	}

	# header
3120 3121 3122 3123 3124 3125
	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)},
3126
			        "raw");
3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145
		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,
3146
			-content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
3147 3148 3149

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

3150
	} else {
3151
		die_error(undef, "Unknown blobdiff format");
3152 3153 3154
	}

	# patch
3155 3156
	if ($format eq 'html') {
		print "<div class=\"page_body\">\n";
3157

3158 3159
		git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
		close $fd;
3160

3161 3162 3163 3164 3165
		print "</div>\n"; # class="page_body"
		git_footer_html();

	} else {
		while (my $line = <$fd>) {
3166 3167
			$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;
3168 3169 3170 3171 3172 3173 3174 3175 3176

			print $line;

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

K
v203  
Kay Sievers 已提交
3179
sub git_blobdiff_plain {
3180
	git_blobdiff('plain');
K
v203  
Kay Sievers 已提交
3181 3182
}

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

	# read commitdiff
	my $fd;
	my @difftree;
	if ($format eq 'html') {
3197
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3198 3199 3200 3201 3202 3203 3204 3205 3206 3207
			"--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') {
3208
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3209
			'-p', $hash_parent, $hash
3210
			or die_error(undef, "Open git-diff-tree failed");
3211

3212 3213 3214
	} else {
		die_error(undef, "Unknown commitdiff format");
	}
K
Kay Sievers 已提交
3215

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

3222 3223 3224 3225 3226 3227 3228
	# 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)},
3229
			        "raw");
K
v232  
Kay Sievers 已提交
3230

3231 3232 3233
		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);
3234
		git_print_authorship(\%co);
3235 3236 3237 3238 3239 3240 3241
		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");
3242
		my $tagname = git_get_rev_name_tags($hash);
3243 3244 3245 3246 3247 3248
		my $filename = basename($project) . "-$hash.patch";

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

3259 3260 3261 3262
		foreach my $line (@{$co{'comment'}}) {
			print "$line\n";
		}
		print "---\n\n";
K
v232  
Kay Sievers 已提交
3263 3264
	}

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

3270 3271
		git_patchset_body($fd, \@difftree, $hash, $hash_parent);
		close $fd;
3272 3273 3274 3275 3276 3277 3278 3279
		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 已提交
3280 3281 3282
	}
}

3283 3284 3285 3286
sub git_commitdiff_plain {
	git_commitdiff('plain');
}

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

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

3303 3304 3305
	if (!defined $hash && defined $file_name) {
		$hash = git_get_hash_by_path($hash_base, $file_name);
	}
3306
	if (defined $hash) {
3307
		$ftype = git_get_type($hash);
3308
	}
K
v157  
Kay Sievers 已提交
3309

3310
	open my $fd, "-|",
J
Jakub Narebski 已提交
3311 3312 3313 3314 3315
		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");
3316

J
Jakub Narebski 已提交
3317 3318 3319 3320 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
	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);
3354

K
v057  
Kay Sievers 已提交
3355
	git_footer_html();
K
Kay Sievers 已提交
3356
}
K
v203  
Kay Sievers 已提交
3357 3358 3359

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

K
v220  
Kay Sievers 已提交
3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380
	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;
3381 3382 3383 3384 3385 3386 3387

		# 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 已提交
3388
	}
K
v203  
Kay Sievers 已提交
3389
	git_header_html();
3390 3391
	git_print_page_nav('','', $hash,$co{'tree'},$hash);
	git_print_header_div('commit', esc_html($co{'title'}), $hash);
K
v203  
Kay Sievers 已提交
3392 3393

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

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

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

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

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

3531 3532

	git_header_html();
3533 3534
	git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
	git_print_header_div('summary', $project);
3535

3536 3537
	git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);

K
v203  
Kay Sievers 已提交
3538 3539
	git_footer_html();
}
3540 3541 3542 3543 3544 3545

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

sub git_rss {
	# http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3546
	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150", git_get_head_hash($project)
3547
		or die_error(undef, "Open git-rev-list failed");
3548
	my @revlist = map { chomp; $_ } <$fd>;
3549
	close $fd or die_error(undef, "Reading git-rev-list failed");
3550
	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
J
Jakub Narebski 已提交
3551 3552 3553 3554 3555 3556 3557 3558 3559
	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
3560 3561 3562

	for (my $i = 0; $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
3563
		my %co = parse_commit($commit);
3564 3565 3566 3567
		# 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;
		}
3568
		my %cd = parse_date($co{'committer_epoch'});
3569
		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
3570 3571
			$co{'parent'}, $co{'id'}
			or next;
3572
		my @difftree = map { chomp; $_ } <$fd>;
3573 3574
		close $fd
			or next;
3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595
		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;
			}
3596
			my $file = esc_html(unquote($7));
3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607
			$file = decode("utf8", $file, Encode::FB_DEFAULT);
			print "$file<br/>\n";
		}
		print "]]>\n" .
		      "</content:encoded>\n" .
		      "</item>\n";
	}
	print "</channel></rss>";
}

sub git_opml {
3608
	my @list = git_get_projects_list();
3609 3610

	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
J
Jakub Narebski 已提交
3611 3612 3613 3614 3615 3616 3617 3618 3619
	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
3620 3621 3622

	foreach my $pr (@list) {
		my %proj = %$pr;
3623
		my $head = git_get_head_hash($proj{'path'});
3624 3625 3626
		if (!defined $head) {
			next;
		}
3627
		$git_dir = "$projectroot/$proj{'path'}";
3628
		my %co = parse_commit($head);
3629 3630 3631 3632 3633 3634 3635 3636 3637
		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 已提交
3638 3639 3640 3641 3642
	print <<XML;
</outline>
</body>
</opml>
XML
3643
}