gitweb.perl 89.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

# location for temporary files needed for diffs
35
our $git_temp = "/tmp/gitweb";
K
v088  
Kay Sievers 已提交
36

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

40 41 42
# string of the home link on top of all pages
our $home_link_str = "++GITWEB_HOME_LINK_STR++";

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

K
v136  
Kay Sievers 已提交
47
# html text to include at home page
48
our $home_text = "++GITWEB_HOMETEXT++";
K
v136  
Kay Sievers 已提交
49

J
Jakub Narebski 已提交
50
# URI of default stylesheet
51
our $stylesheet = "++GITWEB_CSS++";
M
Martin Waitz 已提交
52
# URI of GIT logo
53
our $logo = "++GITWEB_LOGO++";
J
Jakub Narebski 已提交
54

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

58 59 60 61
# 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++");

62
# default blob_plain mimetype and default charset for text/plain blob
63 64
our $default_blob_plain_mimetype = 'text/plain';
our $default_text_plain_charset  = undef;
65

66 67
# file to use for guessing MIME types before trying /etc/mime.types
# (relative to the current git repository)
68
our $mimetypes_file = undef;
69

70 71
# You define site-wide feature defaults here; override them with
# $GITWEB_CONFIG as necessary.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
our %feature = (
	# feature => {'sub' => feature-sub, 'override' => allow-override, 'default' => [ default options...]
	# if feature is overridable, feature-sub will be called with default options;
	# return value indicates if to enable specified feature

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

	'snapshot' => {
		'sub' => \&feature_snapshot,
		'override' => 0,
		#         => [content-encoding, suffix, program]
		'default' => ['x-gzip', 'gz', 'gzip']},
87 88 89 90 91
);

sub gitweb_check_feature {
	my ($name) = @_;
	return undef unless exists $feature{$name};
92 93 94 95
	my ($sub, $override, @defaults) = (
		$feature{$name}{'sub'},
		$feature{$name}{'override'},
		@{$feature{$name}{'default'}});
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
	if (!$override) { return @defaults; }
	return $sub->(@defaults);
}

# To enable system wide have in $GITWEB_CONFIG
# $feature{'blame'}{'default'} =  [1];
# To have project specific config enable override in  $GITWEB_CONFIG
# $feature{'blame'}{'override'} =  1;
# 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
# $feature{'snapshot'}{'default'} =  [undef];
# To have project specific config enable override in  $GITWEB_CONFIG
# $feature{'blame'}{'override'} =  1;
# 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);
}

140
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
141 142 143 144 145 146 147
require $GITWEB_CONFIG if -e $GITWEB_CONFIG;

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

$projects_list ||= $projectroot;
if (! -d $git_temp) {
148
	mkdir($git_temp, 0700) || die_error(undef, "Couldn't mkdir $git_temp");
149 150
}

151
# ======================================================================
K
v118  
Kay Sievers 已提交
152
# input validation and dispatch
153
our $action = $cgi->param('a');
K
v118  
Kay Sievers 已提交
154
if (defined $action) {
155
	if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
156
		die_error(undef, "Invalid action parameter");
K
v107  
Kay Sievers 已提交
157 158
	}
}
K
v014  
Kay Sievers 已提交
159

160
our $project = ($cgi->param('p') || $ENV{'PATH_INFO'});
M
Matthias Lederhofer 已提交
161 162 163
if (defined $project) {
	$project =~ s|^/||;
	$project =~ s|/$||;
164
	$project = undef unless $project;
M
Matthias Lederhofer 已提交
165
}
166
if (defined $project) {
167
	if (!validate_input($project)) {
168
		die_error(undef, "Invalid project parameter");
K
v070  
Kay Sievers 已提交
169 170
	}
	if (!(-d "$projectroot/$project")) {
171
		die_error(undef, "No such directory");
K
v107  
Kay Sievers 已提交
172 173
	}
	if (!(-e "$projectroot/$project/HEAD")) {
174
		die_error(undef, "No such project");
K
v070  
Kay Sievers 已提交
175
	}
K
v227  
Kay Sievers 已提交
176
	$ENV{'GIT_DIR'} = "$projectroot/$project";
K
v055  
Kay Sievers 已提交
177
}
K
v085  
Kay Sievers 已提交
178

179
our $file_name = $cgi->param('f');
K
v107  
Kay Sievers 已提交
180
if (defined $file_name) {
181
	if (!validate_input($file_name)) {
182
		die_error(undef, "Invalid file parameter");
K
v107  
Kay Sievers 已提交
183
	}
K
v055  
Kay Sievers 已提交
184
}
K
v085  
Kay Sievers 已提交
185

186 187 188 189 190 191 192
our $file_parent = $cgi->param('fp');
if (defined $file_parent) {
	if (!validate_input($file_parent)) {
		die_error(undef, "Invalid file parent parameter");
	}
}

193
our $hash = $cgi->param('h');
K
v227  
Kay Sievers 已提交
194
if (defined $hash) {
195
	if (!validate_input($hash)) {
196
		die_error(undef, "Invalid hash parameter");
K
v227  
Kay Sievers 已提交
197
	}
K
v055  
Kay Sievers 已提交
198
}
K
v085  
Kay Sievers 已提交
199

200
our $hash_parent = $cgi->param('hp');
201
if (defined $hash_parent) {
202
	if (!validate_input($hash_parent)) {
203
		die_error(undef, "Invalid hash parent parameter");
204
	}
K
v118  
Kay Sievers 已提交
205 206
}

207
our $hash_base = $cgi->param('hb');
208
if (defined $hash_base) {
209
	if (!validate_input($hash_base)) {
210
		die_error(undef, "Invalid hash base parameter");
211
	}
K
v055  
Kay Sievers 已提交
212
}
K
v085  
Kay Sievers 已提交
213

214
our $page = $cgi->param('pg');
K
v206  
Kay Sievers 已提交
215
if (defined $page) {
216
	if ($page =~ m/[^0-9]$/) {
217
		die_error(undef, "Invalid page parameter");
K
v107  
Kay Sievers 已提交
218
	}
K
v053  
Kay Sievers 已提交
219
}
K
v005  
Kay Sievers 已提交
220

221
our $searchtext = $cgi->param('s');
K
v203  
Kay Sievers 已提交
222 223
if (defined $searchtext) {
	if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
224
		die_error(undef, "Invalid search parameter");
K
v203  
Kay Sievers 已提交
225 226 227 228
	}
	$searchtext = quotemeta $searchtext;
}

229
# dispatch
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
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 已提交
249
	"snapshot" => \&git_snapshot,
250 251 252
	# those below don't need $project
	"opml" => \&git_opml,
	"project_list" => \&git_project_list,
253 254
);

255 256 257 258 259
if (defined $project) {
	$action ||= 'summary';
} else {
	$action ||= 'project_list';
}
260
if (!defined($actions{$action})) {
261
	die_error(undef, "Unknown action");
K
v118  
Kay Sievers 已提交
262
}
263 264
$actions{$action}->();
exit;
K
v118  
Kay Sievers 已提交
265

266 267 268 269
## ======================================================================
## action links

sub href(%) {
270 271 272
	my %params = @_;

	my @mapping = (
273 274 275
		action => "a",
		project => "p",
		file_name => "f",
276
		file_parent => "fp",
277 278 279 280 281 282
		hash => "h",
		hash_parent => "hp",
		hash_base => "hb",
		page => "pg",
		searchtext => "s",
	);
283
	my %mapping = @mapping;
284 285 286

	$params{"project"} ||= $project;

287 288 289 290 291 292 293 294
	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);
295 296 297
}


298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
## ======================================================================
## validation, quoting/unquoting and escaping

sub validate_input {
	my $input = shift;

	if ($input =~ m/^[0-9a-fA-F]{40}$/) {
		return $input;
	}
	if ($input =~ m/(^|\/)(|\.|\.\.)($|\/)/) {
		return undef;
	}
	if ($input =~ m/[^a-zA-Z0-9_\x80-\xff\ \t\.\/\-\+\#\~\%]/) {
		return undef;
	}
	return $input;
}

316 317 318
# 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 已提交
319
	my $str = shift;
320
	$str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
321
	$str =~ s/\+/%2B/g;
K
Kay Sievers 已提交
322
	$str =~ s/ /\+/g;
K
Kay Sievers 已提交
323 324 325
	return $str;
}

326
# replace invalid utf8 character with SUBSTITUTION sequence
327 328 329
sub esc_html {
	my $str = shift;
	$str = decode("utf8", $str, Encode::FB_DEFAULT);
K
Kay Sievers 已提交
330
	$str = escapeHTML($str);
331
	$str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
332 333 334
	return $str;
}

335 336 337 338 339 340 341 342 343 344
# 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;
}

345 346 347 348 349 350 351 352 353 354 355 356 357 358
# 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;
}

359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381
## ----------------------------------------------------------------------
## 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

382 383 384 385 386 387 388 389 390 391 392 393 394
# 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";
	}
}

395 396 397 398
# convert age in seconds to "nn units ago" string
sub age_string {
	my $age = shift;
	my $age_str;
K
v055  
Kay Sievers 已提交
399

400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420
	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";
421
	} else {
422
		$age_str .= " right now";
K
v000  
Kay Sievers 已提交
423
	}
424
	return $age_str;
K
Kay Sievers 已提交
425 426
}

427 428 429 430 431 432 433 434 435 436 437 438 439 440 441
# 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 已提交
442
	} else {
443
		return '----------';
K
v048  
Kay Sievers 已提交
444
	}
K
Kay Sievers 已提交
445 446
}

447 448 449
# convert file mode in octal to file type string
sub file_type {
	my $mode = oct shift;
K
v064  
Kay Sievers 已提交
450

451 452 453 454 455 456 457 458 459
	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 已提交
460 461
}

462 463 464
## ----------------------------------------------------------------------
## functions returning short HTML fragments, or transforming HTML fragments
## which don't beling to other sections
465

466 467 468
# format line of commit message or tag comment
sub format_log_line_html {
	my $line = shift;
469

470 471 472 473 474
	$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") {
475 476 477
			my $link =
				$cgi->a({-href => href(action=>"commit", hash=>$hash_text),
				        -class => "text"}, $hash_text);
478
			$line =~ s/$hash_text/$link/;
479 480
		}
	}
481
	return $line;
482 483
}

484
# format marker of refs pointing to given object
485
sub format_ref_marker {
486
	my ($refs, $id) = @_;
487
	my $markers = '';
488

489
	if (defined $refs->{$id}) {
490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
		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>';
507 508 509
	} else {
		return "";
	}
510 511
}

512 513
# format, perhaps shortened and with markers, title line
sub format_subject_html {
514
	my ($long, $short, $href, $extra) = @_;
515 516 517
	$extra = '' unless defined($extra);

	if (length($short) < length($long)) {
518
		return $cgi->a({-href => $href, -class => "list subject",
519
		                -title => $long},
520 521
		       esc_html($short) . $extra);
	} else {
522
		return $cgi->a({-href => $href, -class => "list subject"},
523 524 525 526
		       esc_html($long)  . $extra);
	}
}

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547
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 "\\") {
		# skip errors (incomplete lines)
		return "";
	}
	$line = untabify($line);
	return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
}

548 549
## ----------------------------------------------------------------------
## git utility subroutines, invoking git commands
K
v125  
Kay Sievers 已提交
550

551
# get HEAD ref of given project as hash
552
sub git_get_head_hash {
553 554 555 556
	my $project = shift;
	my $oENV = $ENV{'GIT_DIR'};
	my $retval = undef;
	$ENV{'GIT_DIR'} = "$projectroot/$project";
557
	if (open my $fd, "-|", $GIT, "rev-parse", "--verify", "HEAD") {
558 559
		my $head = <$fd>;
		close $fd;
K
Kay Sievers 已提交
560 561
		if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
			$retval = $1;
562 563
		}
	}
K
Kay Sievers 已提交
564 565 566
	if (defined $oENV) {
		$ENV{'GIT_DIR'} = $oENV;
	}
567 568 569
	return $retval;
}

570 571 572 573 574 575 576 577 578 579 580 581
# get type of given object
sub git_get_type {
	my $hash = shift;

	open my $fd, "-|", $GIT, "cat-file", '-t', $hash or return;
	my $type = <$fd>;
	close $fd or return;
	chomp $type;
	return $type;
}

sub git_get_project_config {
582
	my ($key, $type) = @_;
583 584 585 586 587

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

588 589 590 591 592 593
	my @x = ($GIT, 'repo-config');
	if (defined $type) { push @x, $type; }
	push @x, "--get";
	push @x, "gitweb.$key";
	my $val = qx(@x);
	chomp $val;
594 595 596 597 598 599 600 601 602 603 604
	return ($val);
}

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

	my $tree = $base;

	open my $fd, "-|", $GIT, "ls-tree", $base, "--", $path
605
		or die_error(undef, "Open git-ls-tree failed");
606 607 608 609 610 611 612 613 614 615 616 617
	my $line = <$fd>;
	close $fd or return undef;

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

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

# assumes that PATH is not symref
618
sub git_get_hash_by_ref {
K
v041  
Kay Sievers 已提交
619
	my $path = shift;
K
v118  
Kay Sievers 已提交
620

K
v203  
Kay Sievers 已提交
621
	open my $fd, "$projectroot/$path" or return undef;
K
v021  
Kay Sievers 已提交
622 623 624
	my $head = <$fd>;
	close $fd;
	chomp $head;
K
v107  
Kay Sievers 已提交
625 626 627 628 629
	if ($head =~ m/^[0-9a-fA-F]{40}$/) {
		return $head;
	}
}

630
sub git_get_project_description {
K
v107  
Kay Sievers 已提交
631
	my $path = shift;
K
v118  
Kay Sievers 已提交
632

K
v203  
Kay Sievers 已提交
633
	open my $fd, "$projectroot/$path/description" or return undef;
K
v107  
Kay Sievers 已提交
634 635 636 637
	my $descr = <$fd>;
	close $fd;
	chomp $descr;
	return $descr;
K
v021  
Kay Sievers 已提交
638 639
}

640 641 642 643 644 645 646 647 648 649
sub git_get_project_url_list {
	my $path = shift;

	open my $fd, "$projectroot/$path/cloneurl" or return undef;
	my @git_project_url_list = map { chomp; $_ } <$fd>;
	close $fd;

	return wantarray ? @git_project_url_list : \@git_project_url_list;
}

650
sub git_get_projects_list {
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
	my @list;

	if (-d $projects_list) {
		# search in directory
		my $dir = $projects_list;
		opendir my ($dh), $dir or return undef;
		while (my $dir = readdir($dh)) {
			if (-e "$projectroot/$dir/HEAD") {
				my $pr = {
					path => $dir,
				};
				push @list, $pr
			}
		}
		closedir($dh);
	} 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'
		open my ($fd), $projects_list or return undef;
		while (my $line = <$fd>) {
			chomp $line;
			my ($path, $owner) = split ' ', $line;
			$path = unescape($path);
			$owner = unescape($owner);
			if (!defined $path) {
				next;
			}
			if (-e "$projectroot/$path/HEAD") {
				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;
}

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
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;
}

725
sub git_get_references {
726 727
	my $type = shift || "";
	my %refs;
728
	my $fd;
729 730
	# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c	refs/tags/v2.6.11
	# c39ae07f393806ccf406ef966e9a15afc43cc36a	refs/tags/v2.6.11^{}
731 732 733 734 735 736 737 738
	if (-f "$projectroot/$project/info/refs") {
		open $fd, "$projectroot/$project/info/refs"
			or return;
	} else {
		open $fd, "-|", $GIT, "ls-remote", "."
			or return;
	}

739 740
	while (my $line = <$fd>) {
		chomp $line;
741
		if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
742
			if (defined $refs{$1}) {
743
				push @{$refs{$1}}, $2;
744
			} else {
745
				$refs{$1} = [ $2 ];
746 747 748 749 750 751 752 753 754 755
			}
		}
	}
	close $fd or return;
	return \%refs;
}

## ----------------------------------------------------------------------
## parse to hash functions

756
sub parse_date {
757 758 759 760 761 762 763 764 765 766 767 768
	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];
769 770 771 772
	$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;
773 774 775 776 777 778 779 780 781 782

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

783
sub parse_tag {
K
v142  
Kay Sievers 已提交
784 785
	my $tag_id = shift;
	my %tag;
K
v235  
Kay Sievers 已提交
786
	my @comment;
K
v142  
Kay Sievers 已提交
787

788
	open my $fd, "-|", $GIT, "cat-file", "tag", $tag_id or return;
K
v235  
Kay Sievers 已提交
789
	$tag{'id'} = $tag_id;
K
v142  
Kay Sievers 已提交
790 791 792 793
	while (my $line = <$fd>) {
		chomp $line;
		if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
			$tag{'object'} = $1;
K
v163  
Kay Sievers 已提交
794
		} elsif ($line =~ m/^type (.+)$/) {
K
v142  
Kay Sievers 已提交
795
			$tag{'type'} = $1;
K
v163  
Kay Sievers 已提交
796
		} elsif ($line =~ m/^tag (.+)$/) {
K
v142  
Kay Sievers 已提交
797
			$tag{'name'} = $1;
K
v235  
Kay Sievers 已提交
798 799 800 801 802 803 804 805 806
		} 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 已提交
807 808
		}
	}
K
v235  
Kay Sievers 已提交
809 810
	push @comment, <$fd>;
	$tag{'comment'} = \@comment;
K
v203  
Kay Sievers 已提交
811
	close $fd or return;
K
v142  
Kay Sievers 已提交
812 813 814 815 816 817
	if (!defined $tag{'name'}) {
		return
	};
	return %tag
}

818
sub parse_commit {
K
v203  
Kay Sievers 已提交
819 820 821 822
	my $commit_id = shift;
	my $commit_text = shift;

	my @commit_lines;
K
v021  
Kay Sievers 已提交
823 824
	my %co;

K
v203  
Kay Sievers 已提交
825 826 827
	if (defined $commit_text) {
		@commit_lines = @$commit_text;
	} else {
828
		$/ = "\0";
829 830
		open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id
			or return;
831
		@commit_lines = split '\n', <$fd>;
K
v203  
Kay Sievers 已提交
832
		close $fd or return;
833 834
		$/ = "\n";
		pop @commit_lines;
K
v203  
Kay Sievers 已提交
835
	}
836 837 838 839 840 841 842
	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 已提交
843
	while (my $line = shift @commit_lines) {
K
v107  
Kay Sievers 已提交
844
		last if $line eq "\n";
K
v163  
Kay Sievers 已提交
845
		if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
K
v021  
Kay Sievers 已提交
846
			$co{'tree'} = $1;
K
v035  
Kay Sievers 已提交
847
		} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
K
v021  
Kay Sievers 已提交
848
			$co{'author'} = $1;
K
v049  
Kay Sievers 已提交
849 850
			$co{'author_epoch'} = $2;
			$co{'author_tz'} = $3;
K
v164  
Kay Sievers 已提交
851 852 853 854 855
			if ($co{'author'} =~ m/^([^<]+) </) {
				$co{'author_name'} = $1;
			} else {
				$co{'author_name'} = $co{'author'};
			}
K
v041  
Kay Sievers 已提交
856 857
		} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
			$co{'committer'} = $1;
K
v049  
Kay Sievers 已提交
858 859
			$co{'committer_epoch'} = $2;
			$co{'committer_tz'} = $3;
K
v042  
Kay Sievers 已提交
860 861
			$co{'committer_name'} = $co{'committer'};
			$co{'committer_name'} =~ s/ <.*//;
K
v021  
Kay Sievers 已提交
862 863
		}
	}
K
v142  
Kay Sievers 已提交
864
	if (!defined $co{'tree'}) {
865
		return;
K
v142  
Kay Sievers 已提交
866
	};
867

K
v203  
Kay Sievers 已提交
868
	foreach my $title (@commit_lines) {
869
		$title =~ s/^    //;
K
v203  
Kay Sievers 已提交
870
		if ($title ne "") {
K
v241  
Kay Sievers 已提交
871
			$co{'title'} = chop_str($title, 80, 5);
K
v203  
Kay Sievers 已提交
872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888
			# 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 已提交
889
			$co{'title_short'} = chop_str($title, 50, 5);
K
v203  
Kay Sievers 已提交
890 891 892
			last;
		}
	}
893 894 895 896 897
	# remove added spaces
	foreach my $line (@commit_lines) {
		$line =~ s/^    //;
	}
	$co{'comment'} = \@commit_lines;
K
v062  
Kay Sievers 已提交
898 899 900

	my $age = time - $co{'committer_epoch'};
	$co{'age'} = $age;
K
v236  
Kay Sievers 已提交
901
	$co{'age_string'} = age_string($age);
K
v225  
Kay Sievers 已提交
902 903
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
	if ($age > 60*60*24*7*2) {
K
v232  
Kay Sievers 已提交
904
		$co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
K
v225  
Kay Sievers 已提交
905 906 907
		$co{'age_string_age'} = $co{'age_string'};
	} else {
		$co{'age_string_date'} = $co{'age_string'};
K
v232  
Kay Sievers 已提交
908
		$co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
K
v225  
Kay Sievers 已提交
909
	}
K
v021  
Kay Sievers 已提交
910 911 912
	return %co;
}

913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
# 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;
}

956
# parse line of git-diff-tree "raw" output
957 958 959 960 961 962 963 964 965 966 967 968 969 970
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
971
			($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
972 973 974 975 976 977 978 979 980 981 982 983
		} else {
			$res{'file'} = unquote($7);
		}
	}
	# 'c512b523472485aef4fff9e57b229d9d243c967f'
	#elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
	#	$res{'commit'} = $1;
	#}

	return wantarray ? %res : \%res;
}

984 985
## ......................................................................
## parse to array of hashes functions
K
v000  
Kay Sievers 已提交
986

987
sub git_get_refs_list {
988 989
	my $ref_dir = shift;
	my @reflist;
K
v000  
Kay Sievers 已提交
990

991
	my @refs;
992 993 994 995 996
	my $pfxlen = length("$projectroot/$project/$ref_dir");
	File::Find::find(sub {
		return if (/^\./);
		if (-f $_) {
			push @refs, substr($File::Find::name, $pfxlen + 1);
997
		}
998 999
	}, "$projectroot/$project/$ref_dir");

1000
	foreach my $ref_file (@refs) {
1001
		my $ref_id = git_get_hash_by_ref("$project/$ref_dir/$ref_file");
1002
		my $type = git_get_type($ref_id) || next;
1003
		my %ref_item = parse_ref($ref_file, $ref_id, $type);
K
v042  
Kay Sievers 已提交
1004

1005 1006
		push @reflist, \%ref_item;
	}
1007
	# sort refs by age
1008 1009
	@reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
	return \@reflist;
K
v041  
Kay Sievers 已提交
1010 1011
}

1012 1013
## ----------------------------------------------------------------------
## filesystem-related functions
K
v035  
Kay Sievers 已提交
1014

K
v133  
Kay Sievers 已提交
1015 1016 1017 1018 1019 1020 1021 1022 1023 1024
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/[,;].*$//;
1025
	return decode("utf8", $owner, Encode::FB_DEFAULT);
K
v133  
Kay Sievers 已提交
1026 1027
}

1028 1029
## ......................................................................
## mimetype related functions
K
v118  
Kay Sievers 已提交
1030

1031 1032 1033 1034 1035 1036 1037 1038
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>) {
1039
		next if m/^#/; # skip comments
1040
		my ($mime, $exts) = split(/\t+/);
1041 1042 1043 1044 1045
		if (defined $exts) {
			my @exts = split(/\s+/, $exts);
			foreach my $ext (@exts) {
				$mimemap{$ext} = $mime;
			}
K
v118  
Kay Sievers 已提交
1046 1047
		}
	}
1048
	close(MIME);
K
v118  
Kay Sievers 已提交
1049

1050 1051 1052
	$filename =~ /\.(.*?)$/;
	return $mimemap{$1};
}
1053

1054 1055 1056 1057
sub mimetype_guess {
	my $filename = shift;
	my $mime;
	$filename =~ /\./ or return undef;
1058

1059 1060
	if ($mimetypes_file) {
		my $file = $mimetypes_file;
1061 1062 1063 1064
		if ($file !~ m!^/!) { # if it is relative path
			# it is relative to project
			$file = "$projectroot/$project/$file";
		}
1065 1066 1067 1068
		$mime = mimetype_guess_file($filename, $file);
	}
	$mime ||= mimetype_guess_file($filename, '/etc/mime.types');
	return $mime;
1069 1070
}

1071
sub blob_mimetype {
1072 1073
	my $fd = shift;
	my $filename = shift;
1074

1075 1076 1077
	if ($filename) {
		my $mime = mimetype_guess($filename);
		$mime and return $mime;
1078
	}
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093

	# 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';
1094
	} else {
1095
		return 'application/octet-stream';
1096
	}
1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117
}

## ======================================================================
## 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) {
				$title .= " - $file_name";
				if ($action eq "tree" && $file_name !~ m|/$|) {
					$title .= "/";
				}
			}
		}
1118
	}
1119 1120 1121 1122 1123
	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.
1124 1125 1126
	if (defined $cgi->http('HTTP_ACCEPT') &&
	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
	    $cgi->Accept('application/xhtml+xml') != 0) {
1127
		$content_type = 'application/xhtml+xml';
1128
	} else {
1129
		$content_type = 'text/html';
1130
	}
1131 1132
	print $cgi->header(-type=>$content_type, -charset => 'utf-8',
	                   -status=> $status, -expires => $expires);
1133 1134 1135 1136
	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">
1137
<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
1138 1139 1140
<!-- git core binaries version $git_version -->
<head>
<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
1141
<meta name="generator" content="gitweb/$version git/$git_version"/>
1142 1143 1144 1145
<meta name="robots" content="index, nofollow"/>
<title>$title</title>
<link rel="stylesheet" type="text/css" href="$stylesheet"/>
EOF
M
Matthias Lederhofer 已提交
1146 1147 1148
	if (defined $project) {
		printf('<link rel="alternate" title="%s log" '.
		       'href="%s" type="application/rss+xml"/>'."\n",
1149
		       esc_param($project), href(action=>"rss"));
M
Matthias Lederhofer 已提交
1150
	}
J
Jakub Narebski 已提交
1151

M
Matthias Lederhofer 已提交
1152 1153
	print "</head>\n" .
	      "<body>\n" .
J
Jakub Narebski 已提交
1154
	      "<div class=\"page_header\">\n" .
1155
	      "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
M
Martin Waitz 已提交
1156
	      "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
1157
	      "</a>\n";
1158
	print $cgi->a({-href => esc_param($home_link)}, $home_link_str) . " / ";
1159
	if (defined $project) {
1160
		print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172
		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 已提交
1173
		} else {
1174
			$search_hash = "HEAD";
K
v160  
Kay Sievers 已提交
1175
		}
1176 1177 1178 1179 1180 1181 1182 1183 1184 1185
		$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 已提交
1186
	}
1187 1188 1189 1190 1191 1192
	print "</div>\n";
}

sub git_footer_html {
	print "<div class=\"page_footer\">\n";
	if (defined $project) {
1193
		my $descr = git_get_project_description($project);
1194 1195 1196
		if (defined $descr) {
			print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
		}
1197
		print $cgi->a({-href => href(action=>"rss"), -class => "rss_logo"}, "RSS") . "\n";
1198
	} else {
1199
		print $cgi->a({-href => href(action=>"opml"), -class => "rss_logo"}, "OPML") . "\n";
1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210
	}
	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 已提交
1211 1212 1213 1214 1215 1216 1217
	print <<EOF;
<div class="page_body">
<br /><br />
$status - $error
<br />
</div>
EOF
K
v107  
Kay Sievers 已提交
1218
	git_footer_html();
1219
	exit;
K
Kay Sievers 已提交
1220 1221
}

1222 1223 1224
## ----------------------------------------------------------------------
## functions printing or outputting HTML: navigation

1225
sub git_print_page_nav {
1226 1227 1228 1229 1230 1231 1232 1233
	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;
	}

1234
	my %arg = map { $_ => {action=>$_} } @navs;
1235 1236
	if (defined $head) {
		for (qw(commit commitdiff)) {
1237
			$arg{$_}{hash} = $head;
1238 1239 1240
		}
		if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
			for (qw(shortlog log)) {
1241
				$arg{$_}{hash} = $head;
K
Kay Sievers 已提交
1242
			}
K
Kay Sievers 已提交
1243 1244
		}
	}
1245 1246
	$arg{tree}{hash} = $treehead if defined $treehead;
	$arg{tree}{hash_base} = $treebase if defined $treebase;
1247 1248 1249

	print "<div class=\"page_nav\">\n" .
		(join " | ",
1250 1251 1252
		 map { $_ eq $current ?
		       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
		 } @navs);
1253 1254
	print "<br/>\n$extra<br/>\n" .
	      "</div>\n";
K
Kay Sievers 已提交
1255 1256
}

1257
sub format_paging_nav {
1258 1259
	my ($action, $hash, $head, $page, $nrevs) = @_;
	my $paging_nav;
1260

1261 1262

	if ($hash ne $head || $page) {
1263
		$paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
1264
	} else {
1265 1266 1267 1268 1269
		$paging_nav .= "HEAD";
	}

	if ($page > 0) {
		$paging_nav .= " &sdot; " .
1270
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
1271
			         -accesskey => "p", -title => "Alt-p"}, "prev");
1272 1273 1274 1275 1276 1277
	} else {
		$paging_nav .= " &sdot; prev";
	}

	if ($nrevs >= (100 * ($page+1)-1)) {
		$paging_nav .= " &sdot; " .
1278
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
1279
			         -accesskey => "n", -title => "Alt-n"}, "next");
1280 1281
	} else {
		$paging_nav .= " &sdot; next";
1282
	}
1283 1284

	return $paging_nav;
1285 1286
}

1287 1288 1289
## ......................................................................
## functions printing or outputting HTML: div

1290
sub git_print_header_div {
1291
	my ($action, $title, $hash, $hash_base) = @_;
1292
	my %args = ();
1293

1294 1295 1296
	$args{action} = $action;
	$args{hash} = $hash if $hash;
	$args{hash_base} = $hash_base if $hash_base;
1297 1298

	print "<div class=\"header\">\n" .
1299 1300 1301
	      $cgi->a({-href => href(%args), -class => "title"},
	      $title ? $title : $action) .
	      "\n</div>\n";
1302
}
K
v142  
Kay Sievers 已提交
1303

1304 1305 1306
sub git_print_page_path {
	my $name = shift;
	my $type = shift;
1307
	my $hb = shift;
K
v142  
Kay Sievers 已提交
1308

1309
	if (!defined $name) {
1310
		print "<div class=\"page_path\">/</div>\n";
1311
	} elsif (defined $type && $type eq 'blob') {
1312
		print "<div class=\"page_path\">";
1313
		if (defined $hb) {
1314 1315 1316
			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
			                             hash_base=>$hb)},
			              esc_html($name));
1317
		} else {
1318 1319
			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)},
			              esc_html($name));
1320
		}
1321
		print "<br/></div>\n";
1322
	} else {
1323
		print "<div class=\"page_path\">" . esc_html($name) . "<br/></div>\n";
K
v142  
Kay Sievers 已提交
1324 1325 1326
	}
}

1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386
sub git_print_log {
	my $log = shift;

	# 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) {
		# print only one empty line
		# do not print empty line after signoff
		if ($line eq "") {
			next if ($empty || $signoff);
			$empty = 1;
		} else {
			$empty = 0;
		}
		if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
			$signoff = 1;
			print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
		} else {
			$signoff = 0;
			print format_log_line_html($line) . "<br/>\n";
		}
	}
}

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

	shift @$log if $remove_title;
	# remove leading empty lines
	while (defined $log->[0] && $log->[0] eq "") {
		shift @$log;
	}

	# simplify and print log
	my $empty = 0;
	foreach my $line (@$log) {
		# remove signoff lines
		if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
			next;
		}
		# print only one empty line
		if ($line eq "") {
			next if $empty;
			$empty = 1;
		} else {
			$empty = 0;
		}
		print format_log_line_html($line) . "<br/>\n";
	}
	# end with single empty line
	print "<br/>\n" unless $empty;
}

1387 1388 1389
## ......................................................................
## functions printing large fragments of HTML

1390
sub git_difftree_body {
1391
	my ($difftree, $hash, $parent) = @_;
1392 1393 1394 1395 1396 1397 1398 1399 1400 1401

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

	print "<table class=\"diff_tree\">\n";
	my $alternate = 0;
	foreach my $line (@{$difftree}) {
1402
		my %diff = parse_difftree_raw_line($line);
1403 1404 1405 1406 1407 1408 1409 1410

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

1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423
		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
1424
			}
1425 1426 1427 1428 1429 1430 1431
			$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>";
1432
			print "<td>" .
1433 1434 1435
			      $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
			                             hash_base=>$hash, file_name=>$diff{'file'}),
			              -class => "list"}, esc_html($diff{'file'})) .
1436
			      "</td>\n" .
1437
			      "<td>$mode_chng</td>\n" .
1438
			      "<td class=\"link\">" .
1439 1440 1441
			      $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
			                             hash_base=>$hash, file_name=>$diff{'file'})},
			              "blob") .
1442 1443
			      "</td>\n";

1444 1445
		} elsif ($diff{'status'} eq "D") { # deleted
			my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
1446
			print "<td>" .
1447 1448 1449 1450 1451
			      $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
			                             hash_base=>$parent, file_name=>$diff{'file'}),
			               -class => "list"}, esc_html($diff{'file'})) .
			      "</td>\n" .
			      "<td>$mode_chng</td>\n" .
1452
			      "<td class=\"link\">" .
1453 1454 1455 1456 1457 1458 1459 1460
			      $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
			                             hash_base=>$parent, file_name=>$diff{'file'})},
			              "blob") .
			      " | " .
			      $cgi->a({-href => href(action=>"history", hash_base=>$parent,
			                             file_name=>$diff{'file'})},\
			              "history") .
			      "</td>\n";
1461

1462
		} elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
1463
			my $mode_chnge = "";
1464 1465 1466 1467
			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";
1468
				}
1469 1470 1471 1472 1473
				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";
1474 1475 1476 1477 1478
					}
				}
				$mode_chnge .= "]</span>\n";
			}
			print "<td>";
1479 1480 1481 1482 1483 1484 1485 1486
			if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
				print $cgi->a({-href => href(action=>"blobdiff", hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
				                             hash_base=>$hash, file_name=>$diff{'file'}),
				              -class => "list"}, esc_html($diff{'file'}));
			} else { # only mode changed
				print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
				                             hash_base=>$hash, file_name=>$diff{'file'}),
				              -class => "list"}, esc_html($diff{'file'}));
1487 1488 1489 1490
			}
			print "</td>\n" .
			      "<td>$mode_chnge</td>\n" .
			      "<td class=\"link\">" .
1491 1492 1493 1494
				$cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
				                       hash_base=>$hash, file_name=>$diff{'file'})},
				        "blob");
			if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
1495
				print " | " .
1496 1497 1498
					$cgi->a({-href => href(action=>"blobdiff", hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
					                       hash_base=>$hash, file_name=>$diff{'file'})},
					        "diff");
1499
			}
1500 1501 1502 1503
			print " | " .
				$cgi->a({-href => href(action=>"history",
				                       hash_base=>$hash, file_name=>$diff{'file'})},
				        "history");
1504 1505
			print "</td>\n";

1506 1507 1508
		} 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'}};
1509
			my $mode_chng = "";
1510 1511 1512
			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);
1513 1514
			}
			print "<td>" .
1515 1516 1517 1518 1519 1520 1521 1522
			      $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" .
1523
			      "<td class=\"link\">" .
1524 1525 1526 1527
			      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
			                             hash=>$diff{'to_id'}, file_name=>$diff{'to_file'})},
			              "blob");
			if ($diff{'to_id'} ne $diff{'from_id'}) {
1528
				print " | " .
1529 1530 1531 1532
					$cgi->a({-href => href(action=>"blobdiff", hash_base=>$hash,
					                       hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
					                       file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
					        "diff");
1533 1534
			}
			print "</td>\n";
1535

1536 1537 1538 1539 1540 1541
		} # we should not encounter Unmerged (U) or Unknown (X) status
		print "</tr>\n";
	}
	print "</table>\n";
}

1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636
sub git_patchset_body {
	my ($patchset, $difftree, $hash, $hash_parent) = @_;

	my $patch_idx = 0;
	my $in_header = 0;
	my $patch_found = 0;
	my %diffinfo;

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

	LINE: foreach my $patch_line (@$patchset) {

		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;
			}
			print "<div class=\"patch\">\n";

			%diffinfo = parse_difftree_raw_line($difftree->[$patch_idx++]);

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

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

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

			} elsif ($diffinfo{'status'} eq "R" || # renamed
			         $diffinfo{'status'} eq "C") { # copied
				print "<div class=\"diff_info\">" .
				      file_type($diffinfo{'from_mode'}) . ":" .
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
				                             hash=>$diffinfo{'from_id'}, file_name=>$diffinfo{'from_file'})},
				              $diffinfo{'from_id'}) .
				      " -> " .
				      file_type($diffinfo{'to_mode'}) . ":" .
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
				                             hash=>$diffinfo{'to_id'}, file_name=>$diffinfo{'to_file'})},
				              $diffinfo{'to_id'});
				print "</div>\n"; # class="diff_info"

			} else { # modified, mode changed, ...
				print "<div class=\"diff_info\">" .
				      file_type($diffinfo{'from_mode'}) . ":" .
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
				                             hash=>$diffinfo{'from_id'}, file_name=>$diffinfo{'file'})},
				              $diffinfo{'from_id'}) .
				      " -> " .
				      file_type($diffinfo{'to_mode'}) . ":" .
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
				                             hash=>$diffinfo{'to_id'}, file_name=>$diffinfo{'file'})},
				              $diffinfo{'to_id'});
				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/^---/) {
			#print "</div>\n"
			$in_header = 0;
		}
		next LINE if $in_header;

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

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

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

1637 1638 1639
sub git_shortlog_body {
	# uses global variable $project
	my ($revlist, $from, $to, $refs, $extra) = @_;
1640 1641 1642 1643

	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
	my $have_snapshot = (defined $ctype && defined $suffix);

1644 1645 1646 1647 1648 1649 1650
	$from = 0 unless defined $from;
	$to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);

	print "<table class=\"shortlog\" cellspacing=\"0\">\n";
	my $alternate = 0;
	for (my $i = $from; $i <= $to; $i++) {
		my $commit = $revlist->[$i];
1651 1652 1653
		#my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
1654 1655 1656 1657 1658 1659 1660 1661 1662 1663
		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>";
1664 1665
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
1666 1667
		print "</td>\n" .
		      "<td class=\"link\">" .
1668
		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
A
Aneesh Kumar K.V 已提交
1669 1670 1671 1672 1673
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
		if ($have_snapshot) {
			print " | " .  $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
		}
		print "</td>\n" .
1674 1675 1676 1677 1678 1679 1680 1681 1682 1683
		      "</tr>\n";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"4\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713
sub git_history_body {
	# Warning: assumes constant type (blob or tree) during history
	my ($fd, $refs, $hash_base, $ftype, $extra) = @_;

	print "<table class=\"history\" cellspacing=\"0\">\n";
	my $alternate = 0;
	while (my $line = <$fd>) {
		if ($line !~ m/^([0-9a-fA-F]{40})/) {
			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)
1714 1715
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
1716 1717
		print "</td>\n" .
		      "<td class=\"link\">" .
1718 1719 1720
		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
		      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype);
1721 1722 1723 1724 1725 1726 1727

		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 " | " .
1728 1729
					$cgi->a({-href => href(action=>"blobdiff", hash=>$blob_current, hash_parent=>$blob_parent,
					                       hash_base=>$commit, file_name=>$file_name)},
1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743
					        "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";
}

1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768
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";
	my $alternate = 0;
	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>" .
1769
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
1770
		               -class => "list name"}, esc_html($tag{'name'})) .
1771 1772 1773
		      "</td>\n" .
		      "<td>";
		if (defined $comment) {
1774 1775
			print format_subject_html($comment, $comment_short,
			                          href(action=>"tag", hash=>$tag{'id'}));
1776 1777 1778 1779
		}
		print "</td>\n" .
		      "<td class=\"selflink\">";
		if ($tag{'type'} eq "tag") {
1780
			print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
1781 1782 1783 1784 1785
		} else {
			print "&nbsp;";
		}
		print "</td>\n" .
		      "<td class=\"link\">" . " | " .
1786
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
1787
		if ($tag{'reftype'} eq "commit") {
1788 1789
			print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
			      " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
1790
		} elsif ($tag{'reftype'} eq "blob") {
1791
			print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823
		}
		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
	my ($taglist, $head, $from, $to, $extra) = @_;
	$from = 0 unless defined $from;
	$to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);

	print "<table class=\"heads\" cellspacing=\"0\">\n";
	my $alternate = 0;
	for (my $i = $from; $i <= $to; $i++) {
		my $entry = $taglist->[$i];
		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>") .
1824
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
1825
		               -class => "list name"},esc_html($tag{'name'})) .
1826 1827
		      "</td>\n" .
		      "<td class=\"link\">" .
1828 1829
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
		      $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") .
1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896
		      "</td>\n" .
		      "</tr>";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"3\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

## ----------------------------------------------------------------------
## functions printing large fragments, format as one of arguments

sub git_diff_print {
	my $from = shift;
	my $from_name = shift;
	my $to = shift;
	my $to_name = shift;
	my $format = shift || "html";

	my $from_tmp = "/dev/null";
	my $to_tmp = "/dev/null";
	my $pid = $$;

	# create tmp from-file
	if (defined $from) {
		$from_tmp = "$git_temp/gitweb_" . $$ . "_from";
		open my $fd2, "> $from_tmp";
		open my $fd, "-|", $GIT, "cat-file", "blob", $from;
		my @file = <$fd>;
		print $fd2 @file;
		close $fd2;
		close $fd;
	}

	# create tmp to-file
	if (defined $to) {
		$to_tmp = "$git_temp/gitweb_" . $$ . "_to";
		open my $fd2, "> $to_tmp";
		open my $fd, "-|", $GIT, "cat-file", "blob", $to;
		my @file = <$fd>;
		print $fd2 @file;
		close $fd2;
		close $fd;
	}

	open my $fd, "-|", "/usr/bin/diff -u -p -L \'$from_name\' -L \'$to_name\' $from_tmp $to_tmp";
	if ($format eq "plain") {
		undef $/;
		print <$fd>;
		$/ = "\n";
	} else {
		while (my $line = <$fd>) {
			chomp $line;
			my $char = substr($line, 0, 1);
			my $diff_class = "";
			if ($char eq '+') {
				$diff_class = " add";
			} elsif ($char eq "-") {
				$diff_class = " rem";
			} elsif ($char eq "@") {
				$diff_class = " chunk_header";
			} elsif ($char eq "\\") {
				# skip errors
				next;
			}
1897
			$line = untabify($line);
1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916
			print "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
		}
	}
	close $fd;

	if (defined $from) {
		unlink($from_tmp);
	}
	if (defined $to) {
		unlink($to_tmp);
	}
}


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

sub git_project_list {
1917 1918
	my $order = $cgi->param('o');
	if (defined $order && $order !~ m/project|descr|owner|age/) {
1919
		die_error(undef, "Unknown order parameter");
1920 1921
	}

1922
	my @list = git_get_projects_list();
1923 1924
	my @projects;
	if (!@list) {
1925
		die_error(undef, "No projects found");
1926 1927
	}
	foreach my $pr (@list) {
1928
		my $head = git_get_head_hash($pr->{'path'});
1929 1930
		if (!defined $head) {
			next;
1931
		}
1932
		$ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}";
1933
		my %co = parse_commit($head);
1934 1935
		if (!%co) {
			next;
1936
		}
1937 1938
		$pr->{'commit'} = \%co;
		if (!defined $pr->{'descr'}) {
1939
			my $descr = git_get_project_description($pr->{'path'}) || "";
1940
			$pr->{'descr'} = chop_str($descr, 25, 5);
1941
		}
1942 1943
		if (!defined $pr->{'owner'}) {
			$pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
1944
		}
1945
		push @projects, $pr;
1946
	}
1947

1948 1949 1950 1951 1952 1953 1954
	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";
1955
	}
1956 1957
	print "<table class=\"project_list\">\n" .
	      "<tr>\n";
1958 1959
	$order ||= "project";
	if ($order eq "project") {
1960 1961 1962
		@projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
		print "<th>Project</th>\n";
	} else {
1963 1964 1965 1966
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=project"),
		               -class => "header"}, "Project") .
		      "</th>\n";
1967
	}
1968
	if ($order eq "descr") {
1969 1970 1971
		@projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
		print "<th>Description</th>\n";
	} else {
1972 1973 1974 1975
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=descr"),
		               -class => "header"}, "Description") .
		      "</th>\n";
1976
	}
1977
	if ($order eq "owner") {
1978 1979 1980
		@projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
		print "<th>Owner</th>\n";
	} else {
1981 1982 1983 1984
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=owner"),
		               -class => "header"}, "Owner") .
		      "</th>\n";
1985
	}
1986
	if ($order eq "age") {
1987 1988 1989
		@projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
		print "<th>Last Change</th>\n";
	} else {
1990 1991 1992 1993
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=age"),
		               -class => "header"}, "Last Change") .
		      "</th>\n";
1994 1995 1996
	}
	print "<th></th>\n" .
	      "</tr>\n";
1997
	my $alternate = 0;
1998
	foreach my $pr (@projects) {
1999 2000 2001 2002 2003 2004
		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
2005
		print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
2006
		                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
2007 2008
		      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
		      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
2009 2010
		print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" .
		      $pr->{'commit'}{'age_string'} . "</td>\n" .
2011
		      "<td class=\"link\">" .
2012 2013 2014
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") .
2015 2016 2017 2018
		      "</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
2019
	git_footer_html();
2020 2021
}

K
v142  
Kay Sievers 已提交
2022
sub git_summary {
2023 2024 2025 2026
	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 已提交
2027

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

2030
	my $refs = git_get_references();
K
v142  
Kay Sievers 已提交
2031
	git_header_html();
2032
	git_print_page_nav('summary','', $head);
2033

K
v203  
Kay Sievers 已提交
2034
	print "<div class=\"title\">&nbsp;</div>\n";
K
v160  
Kay Sievers 已提交
2035
	print "<table cellspacing=\"0\">\n" .
2036
	      "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
K
v142  
Kay Sievers 已提交
2037
	      "<tr><td>owner</td><td>$owner</td></tr>\n" .
2038
	      "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
2039 2040
	# use per project git URL list in $projectroot/$project/cloneurl
	# or make project git URL from git base URL and project name
2041
	my $url_tag = "URL";
2042 2043 2044 2045 2046
	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";
2047 2048 2049
		$url_tag = "";
	}
	print "</table>\n";
2050

2051
	open my $fd, "-|", $GIT, "rev-list", "--max-count=17", git_get_head_hash($project)
2052
		or die_error(undef, "Open git-rev-list failed");
2053
	my @revlist = map { chomp; $_ } <$fd>;
K
v142  
Kay Sievers 已提交
2054
	close $fd;
2055
	git_print_header_div('shortlog');
2056
	git_shortlog_body(\@revlist, 0, 15, $refs,
2057
	                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
K
v142  
Kay Sievers 已提交
2058

2059
	my $taglist = git_get_refs_list("refs/tags");
K
v142  
Kay Sievers 已提交
2060
	if (defined @$taglist) {
2061
		git_print_header_div('tags');
2062
		git_tags_body($taglist, 0, 15,
2063
		              $cgi->a({-href => href(action=>"tags")}, "..."));
K
v142  
Kay Sievers 已提交
2064
	}
K
v150  
Kay Sievers 已提交
2065

2066
	my $headlist = git_get_refs_list("refs/heads");
K
Kay Sievers 已提交
2067
	if (defined @$headlist) {
2068
		git_print_header_div('heads');
2069
		git_heads_body($headlist, $head, 0, 15,
2070
		               $cgi->a({-href => href(action=>"heads")}, "..."));
K
v150  
Kay Sievers 已提交
2071
	}
2072

K
v142  
Kay Sievers 已提交
2073 2074 2075
	git_footer_html();
}

K
v235  
Kay Sievers 已提交
2076
sub git_tag {
2077
	my $head = git_get_head_hash($project);
K
v235  
Kay Sievers 已提交
2078
	git_header_html();
2079 2080 2081
	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 已提交
2082 2083
	print "<div class=\"title_text\">\n" .
	      "<table cellspacing=\"0\">\n" .
2084 2085
	      "<tr>\n" .
	      "<td>object</td>\n" .
2086 2087 2088 2089
	      "<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" .
2090
	      "</tr>\n";
K
v235  
Kay Sievers 已提交
2091
	if (defined($tag{'author'})) {
2092
		my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
2093
		print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
2094 2095 2096
		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 已提交
2097 2098 2099 2100 2101 2102
	}
	print "</table>\n\n" .
	      "</div>\n";
	print "<div class=\"page_body\">";
	my $comment = $tag{'comment'};
	foreach my $line (@$comment) {
2103
		print esc_html($line) . "<br/>\n";
K
v235  
Kay Sievers 已提交
2104 2105 2106 2107 2108
	}
	print "</div>\n";
	git_footer_html();
}

2109 2110 2111
sub git_blame2 {
	my $fd;
	my $ftype;
2112 2113 2114 2115

	if (!gitweb_check_feature('blame')) {
		die_error('403 Permission denied', "Permission denied");
	}
2116
	die_error('404 Not Found', "File name not defined") if (!$file_name);
2117
	$hash_base ||= git_get_head_hash($project);
2118
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2119
	my %co = parse_commit($hash_base)
2120 2121 2122 2123 2124 2125 2126
		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") {
2127
		die_error("400 Bad Request", "Object is not a blob");
2128 2129
	}
	open ($fd, "-|", $GIT, "blame", '-l', $file_name, $hash_base)
2130
		or die_error(undef, "Open git-blame failed");
2131
	git_header_html();
2132
	my $formats_nav =
2133 2134 2135 2136 2137
		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
		        "blob") .
		" | " .
		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
		        "head");
2138 2139
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2140
	git_print_page_path($file_name, $ftype, $hash_base);
2141
	my @rev_color = (qw(light2 dark2));
2142 2143 2144
	my $num_colors = scalar(@rev_color);
	my $current_color = 0;
	my $last_rev;
J
Jakub Narebski 已提交
2145 2146 2147 2148 2149
	print <<HTML;
<div class="page_body">
<table class="blame">
<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
HTML
2150 2151 2152
	while (<$fd>) {
		/^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
		my $full_rev = $1;
2153
		my $rev = substr($full_rev, 0, 8);
2154 2155
		my $lineno = $2;
		my $data = $3;
2156

2157 2158 2159 2160 2161 2162 2163
		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";
2164
		print "<td class=\"sha1\">" .
2165 2166 2167 2168
			$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";
2169 2170 2171 2172 2173
		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
		print "</tr>\n";
	}
	print "</table>\n";
	print "</div>";
2174 2175
	close $fd
		or print "Reading blob failed\n";
2176 2177 2178
	git_footer_html();
}

2179 2180
sub git_blame {
	my $fd;
2181 2182 2183 2184

	if (!gitweb_check_feature('blame')) {
		die_error('403 Permission denied', "Permission denied");
	}
2185
	die_error('404 Not Found', "File name not defined") if (!$file_name);
2186
	$hash_base ||= git_get_head_hash($project);
2187
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2188
	my %co = parse_commit($hash_base)
2189
		or die_error(undef, "Reading commit failed");
2190 2191
	if (!defined $hash) {
		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
2192
			or die_error(undef, "Error lookup file");
2193
	}
2194
	open ($fd, "-|", $GIT, "annotate", '-l', '-t', '-r', $file_name, $hash_base)
2195
		or die_error(undef, "Open git-annotate failed");
2196
	git_header_html();
2197
	my $formats_nav =
2198 2199 2200 2201 2202
		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
		        "blob") .
		" | " .
		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
		        "head");
2203 2204
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2205
	git_print_page_path($file_name, 'blob', $hash_base);
2206 2207
	print "<div class=\"page_body\">\n";
	print <<HTML;
2208
<table class="blame">
2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228
  <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;
2229
		my $age_class;
2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240

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

		if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) \+\d\d\d\d\t(\d+)\)(.*)$/) {
			$long_rev = $1;
			$author   = $2;
			$time     = $3;
			$lineno   = $4;
			$data     = $5;
		} else {
2241
			print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
2242 2243 2244 2245 2246 2247
			next;
		}
		$short_rev  = substr ($long_rev, 0, 8);
		$age        = time () - $time;
		$age_str    = age_string ($age);
		$age_str    =~ s/ /&nbsp;/g;
2248
		$age_class  = age_class($age);
2249 2250
		$author     = esc_html ($author);
		$author     =~ s/ /&nbsp;/g;
2251 2252

		$data = untabify($data);
2253
		$data = esc_html ($data);
2254

2255 2256
		print <<HTML;
  <tr class="$line_class[$line_class_num]">
2257
    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
2258 2259 2260 2261 2262 2263 2264 2265
    <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";
2266 2267
	close $fd
		or print "Reading blob failed.\n";
2268 2269
	print "</div>";
	git_footer_html();
2270 2271
}

2272
sub git_tags {
2273
	my $head = git_get_head_hash($project);
2274
	git_header_html();
2275 2276
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2277

2278
	my $taglist = git_get_refs_list("refs/tags");
2279 2280
	if (defined @$taglist) {
		git_tags_body($taglist);
2281
	}
2282
	git_footer_html();
2283 2284
}

2285
sub git_heads {
2286
	my $head = git_get_head_hash($project);
2287
	git_header_html();
2288 2289
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2290

2291
	my $taglist = git_get_refs_list("refs/heads");
2292 2293
	if (defined @$taglist) {
		git_heads_body($taglist, $head);
2294
	}
2295
	git_footer_html();
2296 2297
}

K
v203  
Kay Sievers 已提交
2298
sub git_blob_plain {
2299
	if (!defined $hash) {
J
Jakub Narebski 已提交
2300
		if (defined $file_name) {
2301
			my $base = $hash_base || git_get_head_hash($project);
J
Jakub Narebski 已提交
2302
			$hash = git_get_hash_by_path($base, $file_name, "blob")
2303
				or die_error(undef, "Error lookup file");
J
Jakub Narebski 已提交
2304
		} else {
2305
			die_error(undef, "No file name defined");
J
Jakub Narebski 已提交
2306 2307
		}
	}
2308
	my $type = shift;
2309
	open my $fd, "-|", $GIT, "cat-file", "blob", $hash
2310
		or die_error(undef, "Couldn't cat $file_name, $hash");
2311

2312
	$type ||= blob_mimetype($fd, $file_name);
2313 2314 2315

	# save as filename, even when no $file_name is given
	my $save_as = "$hash";
2316 2317
	if (defined $file_name) {
		$save_as = $file_name;
2318 2319
	} elsif ($type =~ m/^text\//) {
		$save_as .= '.txt';
2320
	}
2321

2322 2323
	print $cgi->header(-type => "$type",
	                   -content_disposition => "inline; filename=\"$save_as\"");
K
v203  
Kay Sievers 已提交
2324
	undef $/;
2325
	binmode STDOUT, ':raw';
K
v203  
Kay Sievers 已提交
2326
	print <$fd>;
2327
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
K
v203  
Kay Sievers 已提交
2328 2329 2330 2331
	$/ = "\n";
	close $fd;
}

2332
sub git_blob {
2333
	if (!defined $hash) {
J
Jakub Narebski 已提交
2334
		if (defined $file_name) {
2335
			my $base = $hash_base || git_get_head_hash($project);
J
Jakub Narebski 已提交
2336
			$hash = git_get_hash_by_path($base, $file_name, "blob")
2337
				or die_error(undef, "Error lookup file");
J
Jakub Narebski 已提交
2338
		} else {
2339
			die_error(undef, "No file name defined");
J
Jakub Narebski 已提交
2340 2341
		}
	}
2342
	my $have_blame = gitweb_check_feature('blame');
2343
	open my $fd, "-|", $GIT, "cat-file", "blob", $hash
2344
		or die_error(undef, "Couldn't cat $file_name, $hash");
2345
	my $mimetype = blob_mimetype($fd, $file_name);
2346 2347 2348 2349 2350
	if ($mimetype !~ m/^text\//) {
		close $fd;
		return git_blob_plain($mimetype);
	}
	git_header_html();
2351
	my $formats_nav = '';
2352
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2353 2354
		if (defined $file_name) {
			if ($have_blame) {
2355 2356 2357 2358 2359
				$formats_nav .=
					$cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
					                       hash=>$hash, file_name=>$file_name)},
					        "blame") .
					" | ";
2360
			}
2361
			$formats_nav .=
2362 2363 2364 2365 2366 2367 2368
				$cgi->a({-href => href(action=>"blob_plain",
				                       hash=>$hash, file_name=>$file_name)},
				        "plain") .
				" | " .
				$cgi->a({-href => href(action=>"blob",
				                       hash_base=>"HEAD", file_name=>$file_name)},
				        "head");
2369
		} else {
2370 2371
			$formats_nav .=
				$cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain");
2372
		}
2373 2374
		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2375 2376 2377 2378 2379
	} else {
		print "<div class=\"page_nav\">\n" .
		      "<br/><br/></div>\n" .
		      "<div class=\"title\">$hash</div>\n";
	}
2380
	git_print_page_path($file_name, "blob", $hash_base);
2381 2382 2383 2384 2385
	print "<div class=\"page_body\">\n";
	my $nr;
	while (my $line = <$fd>) {
		chomp $line;
		$nr++;
2386
		$line = untabify($line);
2387 2388
		printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
		       $nr, $nr, $nr, esc_html($line);
2389
	}
2390 2391
	close $fd
		or print "Reading blob failed.\n";
2392 2393 2394 2395
	print "</div>";
	git_footer_html();
}

K
v118  
Kay Sievers 已提交
2396
sub git_tree {
K
v107  
Kay Sievers 已提交
2397
	if (!defined $hash) {
2398
		$hash = git_get_head_hash($project);
K
v118  
Kay Sievers 已提交
2399
		if (defined $file_name) {
2400
			my $base = $hash_base || $hash;
K
v118  
Kay Sievers 已提交
2401 2402
			$hash = git_get_hash_by_path($base, $file_name, "tree");
		}
K
v157  
Kay Sievers 已提交
2403
		if (!defined $hash_base) {
2404
			$hash_base = $hash;
K
v157  
Kay Sievers 已提交
2405
		}
K
v145  
Kay Sievers 已提交
2406
	}
2407
	$/ = "\0";
2408
	open my $fd, "-|", $GIT, "ls-tree", '-z', $hash
2409
		or die_error(undef, "Open git-ls-tree failed");
2410
	my @entries = map { chomp; $_ } <$fd>;
2411
	close $fd or die_error(undef, "Reading tree failed");
2412
	$/ = "\n";
K
v077  
Kay Sievers 已提交
2413

2414 2415
	my $refs = git_get_references();
	my $ref = format_ref_marker($refs, $hash_base);
K
v021  
Kay Sievers 已提交
2416
	git_header_html();
2417
	my %base_key = ();
K
v118  
Kay Sievers 已提交
2418
	my $base = "";
2419
	my $have_blame = gitweb_check_feature('blame');
2420
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2421
		$base_key{hash_base} = $hash_base;
2422 2423
		git_print_page_nav('tree','', $hash_base);
		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
K
v077  
Kay Sievers 已提交
2424 2425 2426 2427 2428
	} else {
		print "<div class=\"page_nav\">\n";
		print "<br/><br/></div>\n";
		print "<div class=\"title\">$hash</div>\n";
	}
K
v118  
Kay Sievers 已提交
2429
	if (defined $file_name) {
2430
		$base = esc_html("$file_name/");
K
v118  
Kay Sievers 已提交
2431
	}
2432
	git_print_page_path($file_name, 'tree', $hash_base);
K
v043  
Kay Sievers 已提交
2433
	print "<div class=\"page_body\">\n";
K
v125  
Kay Sievers 已提交
2434
	print "<table cellspacing=\"0\">\n";
K
v160  
Kay Sievers 已提交
2435
	my $alternate = 0;
K
Kay Sievers 已提交
2436
	foreach my $line (@entries) {
K
v003  
Kay Sievers 已提交
2437
		#'100644	blob	0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
K
v203  
Kay Sievers 已提交
2438
		$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
K
v031  
Kay Sievers 已提交
2439
		my $t_mode = $1;
K
Kay Sievers 已提交
2440 2441
		my $t_type = $2;
		my $t_hash = $3;
2442
		my $t_name = validate_input($4);
K
v160  
Kay Sievers 已提交
2443
		if ($alternate) {
K
v220  
Kay Sievers 已提交
2444
			print "<tr class=\"dark\">\n";
K
v160  
Kay Sievers 已提交
2445
		} else {
K
v220  
Kay Sievers 已提交
2446
			print "<tr class=\"light\">\n";
K
v160  
Kay Sievers 已提交
2447 2448
		}
		$alternate ^= 1;
2449
		print "<td class=\"mode\">" . mode_str($t_mode) . "</td>\n";
K
Kay Sievers 已提交
2450
		if ($t_type eq "blob") {
K
v157  
Kay Sievers 已提交
2451
			print "<td class=\"list\">" .
2452 2453
			      $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key),
			              -class => "list"}, esc_html($t_name)) .
K
v220  
Kay Sievers 已提交
2454 2455
			      "</td>\n" .
			      "<td class=\"link\">" .
2456 2457
			      $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
			              "blob");
2458
			if ($have_blame) {
2459 2460 2461
				print " | " .
					$cgi->a({-href => href(action=>"blame", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
					        "blame");
2462
			}
2463 2464 2465 2466 2467 2468 2469 2470
			print " | " .
			      $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
			                             hash=>$t_hash, file_name=>"$base$t_name")},
			              "history") .
			      " | " .
			      $cgi->a({-href => href(action=>"blob_plain",
			                             hash=>$t_hash, file_name=>"$base$t_name")},
			              "raw") .
K
v125  
Kay Sievers 已提交
2471
			      "</td>\n";
K
Kay Sievers 已提交
2472
		} elsif ($t_type eq "tree") {
K
v157  
Kay Sievers 已提交
2473
			print "<td class=\"list\">" .
2474 2475
			      $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
			              esc_html($t_name)) .
K
v203  
Kay Sievers 已提交
2476
			      "</td>\n" .
K
v220  
Kay Sievers 已提交
2477
			      "<td class=\"link\">" .
2478 2479 2480 2481 2482
			      $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
			              "tree") .
			      " | " .
			      $cgi->a({-href => href(action=>"history", hash_base=>$hash_base, file_name=>"$base$t_name")},
			              "history") .
K
v220  
Kay Sievers 已提交
2483
			      "</td>\n";
K
Kay Sievers 已提交
2484
		}
K
v125  
Kay Sievers 已提交
2485
		print "</tr>\n";
K
Kay Sievers 已提交
2486
	}
K
v125  
Kay Sievers 已提交
2487 2488
	print "</table>\n" .
	      "</div>";
K
v021  
Kay Sievers 已提交
2489
	git_footer_html();
K
v118  
Kay Sievers 已提交
2490 2491
}

A
Aneesh Kumar K.V 已提交
2492 2493
sub git_snapshot {

2494 2495 2496 2497 2498 2499
	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 已提交
2500 2501 2502 2503
	if (!defined $hash) {
		$hash = git_get_head_hash($project);
	}

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

	print $cgi->header(-type => 'application/x-tar',
2507 2508
	                   -content_encoding => $ctype,
	                   -content_disposition => "inline; filename=\"$filename\"",
2509
	                   -status => '200 OK');
A
Aneesh Kumar K.V 已提交
2510

2511 2512
	open my $fd, "-|", "$GIT tar-tree $hash \'$project\' | $command" or
		die_error(undef, "Execute git-tar-tree failed.");
A
Aneesh Kumar K.V 已提交
2513 2514 2515 2516 2517 2518 2519
	binmode STDOUT, ':raw';
	print <$fd>;
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
	close $fd;

}

K
v118  
Kay Sievers 已提交
2520
sub git_log {
2521
	my $head = git_get_head_hash($project);
K
v150  
Kay Sievers 已提交
2522
	if (!defined $hash) {
K
v203  
Kay Sievers 已提交
2523
		$hash = $head;
K
v150  
Kay Sievers 已提交
2524
	}
K
v206  
Kay Sievers 已提交
2525 2526
	if (!defined $page) {
		$page = 0;
K
v107  
Kay Sievers 已提交
2527
	}
2528
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
2529 2530

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

2536
	my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
2537 2538

	git_header_html();
2539
	git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
2540

K
v107  
Kay Sievers 已提交
2541
	if (!@revlist) {
2542
		my %co = parse_commit($hash);
2543

2544
		git_print_header_div('summary', $project);
K
v145  
Kay Sievers 已提交
2545
		print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
K
Kay Sievers 已提交
2546
	}
K
v220  
Kay Sievers 已提交
2547 2548
	for (my $i = ($page * 100); $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
2549 2550
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
K
v107  
Kay Sievers 已提交
2551
		next if !%co;
2552 2553
		my %ad = parse_date($co{'author_epoch'});
		git_print_header_div('commit',
2554 2555 2556
		               "<span class=\"age\">$co{'age_string'}</span>" .
		               esc_html($co{'title'}) . $ref,
		               $commit);
K
v088  
Kay Sievers 已提交
2557 2558
		print "<div class=\"title_text\">\n" .
		      "<div class=\"log_link\">\n" .
2559
		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
2560 2561
		      " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
K
v121  
Kay Sievers 已提交
2562
		      "<br/>\n" .
K
v088  
Kay Sievers 已提交
2563
		      "</div>\n" .
2564
		      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
2565 2566 2567 2568
		      "</div>\n";

		print "<div class=\"log_body\">\n";
		git_print_simplified_log($co{'comment'});
K
v118  
Kay Sievers 已提交
2569
		print "</div>\n";
K
v021  
Kay Sievers 已提交
2570
	}
K
v088  
Kay Sievers 已提交
2571
	git_footer_html();
K
v118  
Kay Sievers 已提交
2572 2573 2574
}

sub git_commit {
2575
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
2576
	if (!%co) {
2577
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
2578
	}
2579 2580
	my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
K
Kay Sievers 已提交
2581

K
v235  
Kay Sievers 已提交
2582 2583
	my $parent = $co{'parent'};
	if (!defined $parent) {
2584
		$parent = "--root";
K
v085  
Kay Sievers 已提交
2585
	}
2586
	open my $fd, "-|", $GIT, "diff-tree", '-r', '-M', $parent, $hash
2587
		or die_error(undef, "Open git-diff-tree failed");
2588
	my @difftree = map { chomp; $_ } <$fd>;
2589
	close $fd or die_error(undef, "Reading git-diff-tree failed");
2590 2591 2592 2593 2594 2595

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

	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
	my $have_snapshot = (defined $ctype && defined $suffix);

2602
	my $formats_nav = '';
2603 2604
	if (defined $file_name && defined $co{'parent'}) {
		my $parent = $co{'parent'};
2605 2606 2607
		$formats_nav .=
			$cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
			        "blame");
2608
	}
2609
	git_header_html(undef, $expires);
2610
	git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
2611 2612
	                   $hash, $co{'tree'}, $hash,
	                   $formats_nav);
2613

K
v107  
Kay Sievers 已提交
2614
	if (defined $co{'parent'}) {
2615
		git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
K
v107  
Kay Sievers 已提交
2616
	} else {
2617
		git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
K
v107  
Kay Sievers 已提交
2618
	}
K
v085  
Kay Sievers 已提交
2619
	print "<div class=\"title_text\">\n" .
K
v107  
Kay Sievers 已提交
2620
	      "<table cellspacing=\"0\">\n";
2621
	print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
K
v160  
Kay Sievers 已提交
2622 2623
	      "<tr>" .
	      "<td></td><td> $ad{'rfc2822'}";
K
v080  
Kay Sievers 已提交
2624
	if ($ad{'hour_local'} < 6) {
2625 2626
		printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
K
v107  
Kay Sievers 已提交
2627
	} else {
2628 2629
		printf(" (%02d:%02d %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
K
v107  
Kay Sievers 已提交
2630
	}
K
v160  
Kay Sievers 已提交
2631 2632
	print "</td>" .
	      "</tr>\n";
2633
	print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
2634 2635 2636
	print "<tr><td></td><td> $cd{'rfc2822'}" .
	      sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
	      "</td></tr>\n";
2637
	print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
K
v160  
Kay Sievers 已提交
2638 2639
	print "<tr>" .
	      "<td>tree</td>" .
2640
	      "<td class=\"sha1\">" .
2641 2642
	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
	               class => "list"}, $co{'tree'}) .
K
v203  
Kay Sievers 已提交
2643
	      "</td>" .
2644 2645 2646
	      "<td class=\"link\">" .
	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
	              "tree");
A
Aneesh Kumar K.V 已提交
2647
	if ($have_snapshot) {
2648 2649
		print " | " .
		      $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
A
Aneesh Kumar K.V 已提交
2650 2651
	}
	print "</td>" .
K
v160  
Kay Sievers 已提交
2652
	      "</tr>\n";
2653
	my $parents = $co{'parents'};
K
v025  
Kay Sievers 已提交
2654
	foreach my $par (@$parents) {
K
v160  
Kay Sievers 已提交
2655 2656
		print "<tr>" .
		      "<td>parent</td>" .
2657 2658 2659 2660
		      "<td class=\"sha1\">" .
		      $cgi->a({-href => href(action=>"commit", hash=>$par),
		               class => "list"}, $par) .
		      "</td>" .
K
v160  
Kay Sievers 已提交
2661
		      "<td class=\"link\">" .
2662
		      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
2663 2664
		      " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") .
K
v160  
Kay Sievers 已提交
2665 2666
		      "</td>" .
		      "</tr>\n";
K
v025  
Kay Sievers 已提交
2667
	}
J
Jakub Narebski 已提交
2668
	print "</table>".
K
v107  
Kay Sievers 已提交
2669
	      "</div>\n";
2670

K
v043  
Kay Sievers 已提交
2671
	print "<div class=\"page_body\">\n";
2672
	git_print_log($co{'comment'});
K
v080  
Kay Sievers 已提交
2673
	print "</div>\n";
2674

2675
	git_difftree_body(\@difftree, $hash, $parent);
2676

K
v021  
Kay Sievers 已提交
2677
	git_footer_html();
K
v118  
Kay Sievers 已提交
2678 2679 2680
}

sub git_blobdiff {
K
v203  
Kay Sievers 已提交
2681
	mkdir($git_temp, 0700);
K
v021  
Kay Sievers 已提交
2682
	git_header_html();
2683
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2684
		my $formats_nav =
2685 2686 2687
			$cgi->a({-href => href(action=>"blobdiff_plain",
			                       hash=>$hash, hash_parent=>$hash_parent)},
			        "plain");
2688 2689
		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
K
v118  
Kay Sievers 已提交
2690
	} else {
J
Jakub Narebski 已提交
2691 2692 2693 2694
		print <<HTML;
<div class="page_nav"><br/><br/></div>
<div class="title">$hash vs $hash_parent</div>
HTML
K
v118  
Kay Sievers 已提交
2695
	}
2696
	git_print_page_path($file_name, "blob", $hash_base);
K
v070  
Kay Sievers 已提交
2697
	print "<div class=\"page_body\">\n" .
K
v133  
Kay Sievers 已提交
2698
	      "<div class=\"diff_info\">blob:" .
2699 2700 2701
	      $cgi->a({-href => href(action=>"blob", hash=>$hash_parent,
	                             hash_base=>$hash_base, file_name=>($file_parent || $file_name))},
	              $hash_parent) .
K
v089  
Kay Sievers 已提交
2702
	      " -> blob:" .
2703 2704 2705
	      $cgi->a({-href => href(action=>"blob", hash=>$hash,
	                             hash_base=>$hash_base, file_name=>$file_name)},
	              $hash) .
K
v133  
Kay Sievers 已提交
2706
	      "</div>\n";
K
v203  
Kay Sievers 已提交
2707
	git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
2708
	print "</div>"; # page_body
K
v021  
Kay Sievers 已提交
2709
	git_footer_html();
K
v118  
Kay Sievers 已提交
2710 2711
}

K
v203  
Kay Sievers 已提交
2712 2713 2714 2715 2716 2717
sub git_blobdiff_plain {
	mkdir($git_temp, 0700);
	print $cgi->header(-type => "text/plain", -charset => 'utf-8');
	git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash, "plain");
}

K
v118  
Kay Sievers 已提交
2718
sub git_commitdiff {
2719
	my $format = shift || 'html';
2720
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
2721
	if (!%co) {
2722
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
2723
	}
K
v160  
Kay Sievers 已提交
2724
	if (!defined $hash_parent) {
2725
		$hash_parent = $co{'parent'} || '--root';
K
v160  
Kay Sievers 已提交
2726
	}
2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751

	# read commitdiff
	my $fd;
	my @difftree;
	my @patchset;
	if ($format eq 'html') {
		open $fd, "-|", $GIT, "diff-tree", '-r', '-M', '-C',
			"--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;
		}
		@patchset = map { chomp; $_ } <$fd>;

		close $fd
			or die_error(undef, "Reading git-diff-tree failed");
	} elsif ($format eq 'plain') {
		open $fd, "-|", $GIT, "diff-tree", '-r', '-p', '-B', $hash_parent, $hash
			or die_error(undef, "Open git-diff-tree failed");
	} else {
		die_error(undef, "Unknown commitdiff format");
	}
K
Kay Sievers 已提交
2752

2753 2754 2755 2756 2757
	# non-textual hash id's can be cached
	my $expires;
	if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
		$expires = "+1d";
	}
K
v118  
Kay Sievers 已提交
2758

2759 2760 2761 2762 2763 2764 2765 2766
	# 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)},
			        "plain");
K
v232  
Kay Sievers 已提交
2767

2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790
		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);
		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");
		my @tagnames;
		if (exists $refs->{$hash}) {
			@tagnames = map { s|^tags/|| } $refs->{$hash};
		}
		my $filename = basename($project) . "-$hash.patch";

		print $cgi->header(
			-type => 'text/plain',
			-charset => 'utf-8',
			-expires => $expires,
			-content_disposition => qq(inline; filename="$filename"));
		my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
		print <<TEXT;
J
Jakub Narebski 已提交
2791 2792 2793 2794
From: $co{'author'}
Date: $ad{'rfc2822'} ($ad{'tz_local'})
Subject: $co{'title'}
TEXT
2795 2796 2797 2798 2799 2800 2801 2802
		foreach my $tag (@tagnames) {
			print "X-Git-Tag: $tag\n";
		}
		print "X-Git-Url: " . $cgi->self_url() . "\n\n";
		foreach my $line (@{$co{'comment'}}) {
			print "$line\n";
		}
		print "---\n\n";
K
v232  
Kay Sievers 已提交
2803 2804
	}

2805 2806 2807 2808
	# write patch
	if ($format eq 'html') {
		#git_difftree_body(\@difftree, $hash, $hash_parent);
		#print "<br/>\n";
K
v232  
Kay Sievers 已提交
2809

2810 2811 2812 2813 2814 2815 2816 2817 2818 2819
		git_patchset_body(\@patchset, \@difftree, $hash, $hash_parent);

		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 已提交
2820 2821 2822
	}
}

2823 2824 2825 2826
sub git_commitdiff_plain {
	git_commitdiff('plain');
}

K
v118  
Kay Sievers 已提交
2827
sub git_history {
2828
	if (!defined $hash_base) {
2829
		$hash_base = git_get_head_hash($project);
K
v118  
Kay Sievers 已提交
2830
	}
2831
	my $ftype;
2832
	my %co = parse_commit($hash_base);
K
v118  
Kay Sievers 已提交
2833
	if (!%co) {
2834
		die_error(undef, "Unknown commit object");
K
v062  
Kay Sievers 已提交
2835
	}
2836
	my $refs = git_get_references();
K
v057  
Kay Sievers 已提交
2837
	git_header_html();
2838 2839
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2840 2841 2842
	if (!defined $hash && defined $file_name) {
		$hash = git_get_hash_by_path($hash_base, $file_name);
	}
2843
	if (defined $hash) {
2844
		$ftype = git_get_type($hash);
2845
	}
2846
	git_print_page_path($file_name, $ftype, $hash_base);
K
v157  
Kay Sievers 已提交
2847

2848
	open my $fd, "-|",
2849
		$GIT, "rev-list", "--full-history", $hash_base, "--", $file_name;
2850 2851
	git_history_body($fd, $refs, $hash_base, $ftype);

K
v107  
Kay Sievers 已提交
2852
	close $fd;
K
v057  
Kay Sievers 已提交
2853
	git_footer_html();
K
Kay Sievers 已提交
2854
}
K
v203  
Kay Sievers 已提交
2855 2856 2857

sub git_search {
	if (!defined $searchtext) {
2858
		die_error(undef, "Text field empty");
K
v203  
Kay Sievers 已提交
2859 2860
	}
	if (!defined $hash) {
2861
		$hash = git_get_head_hash($project);
K
v203  
Kay Sievers 已提交
2862
	}
2863
	my %co = parse_commit($hash);
K
v203  
Kay Sievers 已提交
2864
	if (!%co) {
2865
		die_error(undef, "Unknown commit object");
K
v203  
Kay Sievers 已提交
2866
	}
K
v220  
Kay Sievers 已提交
2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880
	# 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 $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;
	}
K
v203  
Kay Sievers 已提交
2881
	git_header_html();
2882 2883
	git_print_page_nav('','', $hash,$co{'tree'},$hash);
	git_print_header_div('commit', esc_html($co{'title'}), $hash);
K
v203  
Kay Sievers 已提交
2884 2885 2886

	print "<table cellspacing=\"0\">\n";
	my $alternate = 0;
K
v220  
Kay Sievers 已提交
2887 2888
	if ($commit_search) {
		$/ = "\0";
2889
		open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", $hash or next;
K
v220  
Kay Sievers 已提交
2890 2891 2892
		while (my $commit_text = <$fd>) {
			if (!grep m/$searchtext/i, $commit_text) {
				next;
K
v203  
Kay Sievers 已提交
2893
			}
K
v220  
Kay Sievers 已提交
2894 2895
			if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
				next;
K
v203  
Kay Sievers 已提交
2896
			}
K
v220  
Kay Sievers 已提交
2897
			if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
K
v203  
Kay Sievers 已提交
2898 2899
				next;
			}
K
v220  
Kay Sievers 已提交
2900
			my @commit_lines = split "\n", $commit_text;
2901
			my %co = parse_commit(undef, \@commit_lines);
K
v220  
Kay Sievers 已提交
2902 2903 2904 2905 2906 2907 2908 2909 2910
			if (!%co) {
				next;
			}
			if ($alternate) {
				print "<tr class=\"dark\">\n";
			} else {
				print "<tr class=\"light\">\n";
			}
			$alternate ^= 1;
K
v225  
Kay Sievers 已提交
2911
			print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2912
			      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
K
v220  
Kay Sievers 已提交
2913
			      "<td>" .
2914 2915
			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
			               esc_html(chop_str($co{'title'}, 50)) . "<br/>");
K
v220  
Kay Sievers 已提交
2916 2917 2918
			my $comment = $co{'comment'};
			foreach my $line (@$comment) {
				if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
2919
					my $lead = esc_html($1) || "";
K
v220  
Kay Sievers 已提交
2920
					$lead = chop_str($lead, 30, 10);
2921 2922
					my $match = esc_html($2) || "";
					my $trail = esc_html($3) || "";
K
v220  
Kay Sievers 已提交
2923
					$trail = chop_str($trail, 30, 10);
2924
					my $text = "$lead<span class=\"match\">$match</span>$trail";
K
v220  
Kay Sievers 已提交
2925
					print chop_str($text, 80, 5) . "<br/>\n";
K
v203  
Kay Sievers 已提交
2926
				}
K
v220  
Kay Sievers 已提交
2927 2928 2929
			}
			print "</td>\n" .
			      "<td class=\"link\">" .
2930
			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
2931 2932
			      " | " .
			      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
K
v220  
Kay Sievers 已提交
2933 2934 2935 2936 2937 2938 2939 2940
			print "</td>\n" .
			      "</tr>\n";
		}
		close $fd;
	}

	if ($pickaxe_search) {
		$/ = "\n";
2941
		open my $fd, "-|", "$GIT rev-list $hash | $GIT diff-tree -r --stdin -S\'$searchtext\'";
K
v220  
Kay Sievers 已提交
2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952
		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 已提交
2953
				}
K
v220  
Kay Sievers 已提交
2954 2955 2956 2957
				if ($set{'id'} =~ m/0{40}/) {
					next;
				}
				push @files, \%set;
2958
			} elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
K
v220  
Kay Sievers 已提交
2959 2960 2961 2962 2963 2964 2965
				if (%co) {
					if ($alternate) {
						print "<tr class=\"dark\">\n";
					} else {
						print "<tr class=\"light\">\n";
					}
					$alternate ^= 1;
K
v225  
Kay Sievers 已提交
2966
					print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2967
					      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
K
v220  
Kay Sievers 已提交
2968
					      "<td>" .
2969 2970
					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
					              -class => "list subject"},
2971
					              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
K
v220  
Kay Sievers 已提交
2972 2973
					while (my $setref = shift @files) {
						my %set = %$setref;
2974 2975 2976 2977
						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 已提交
2978 2979 2980 2981
						      "<br/>\n";
					}
					print "</td>\n" .
					      "<td class=\"link\">" .
2982
					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
2983 2984
					      " | " .
					      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
K
v220  
Kay Sievers 已提交
2985 2986 2987
					print "</td>\n" .
					      "</tr>\n";
				}
2988
				%co = parse_commit($1);
K
v203  
Kay Sievers 已提交
2989 2990
			}
		}
K
v220  
Kay Sievers 已提交
2991
		close $fd;
K
v203  
Kay Sievers 已提交
2992 2993 2994 2995 2996 2997
	}
	print "</table>\n";
	git_footer_html();
}

sub git_shortlog {
2998
	my $head = git_get_head_hash($project);
K
v203  
Kay Sievers 已提交
2999 3000 3001
	if (!defined $hash) {
		$hash = $head;
	}
K
v206  
Kay Sievers 已提交
3002 3003 3004
	if (!defined $page) {
		$page = 0;
	}
3005
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
3006 3007

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

3013
	my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
3014 3015 3016
	my $next_link = '';
	if ($#revlist >= (100 * ($page+1)-1)) {
		$next_link =
3017
			$cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
3018 3019 3020
			         -title => "Alt-n"}, "next");
	}

3021 3022

	git_header_html();
3023 3024
	git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
	git_print_header_div('summary', $project);
3025

3026 3027
	git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);

K
v203  
Kay Sievers 已提交
3028 3029
	git_footer_html();
}
3030 3031 3032 3033 3034 3035

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

sub git_rss {
	# http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
3036
	open my $fd, "-|", $GIT, "rev-list", "--max-count=150", git_get_head_hash($project)
3037
		or die_error(undef, "Open git-rev-list failed");
3038
	my @revlist = map { chomp; $_ } <$fd>;
3039
	close $fd or die_error(undef, "Reading git-rev-list failed");
3040
	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
J
Jakub Narebski 已提交
3041 3042 3043 3044 3045 3046 3047 3048 3049
	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
3050 3051 3052

	for (my $i = 0; $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
3053
		my %co = parse_commit($commit);
3054 3055 3056 3057
		# 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;
		}
3058
		my %cd = parse_date($co{'committer_epoch'});
3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094
		open $fd, "-|", $GIT, "diff-tree", '-r', $co{'parent'}, $co{'id'} or next;
		my @difftree = map { chomp; $_ } <$fd>;
		close $fd or next;
		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;
			}
			my $file = validate_input(unquote($7));
			$file = decode("utf8", $file, Encode::FB_DEFAULT);
			print "$file<br/>\n";
		}
		print "]]>\n" .
		      "</content:encoded>\n" .
		      "</item>\n";
	}
	print "</channel></rss>";
}

sub git_opml {
3095
	my @list = git_get_projects_list();
3096 3097

	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
J
Jakub Narebski 已提交
3098 3099 3100 3101 3102 3103 3104 3105 3106
	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
3107 3108 3109

	foreach my $pr (@list) {
		my %proj = %$pr;
3110
		my $head = git_get_head_hash($proj{'path'});
3111 3112 3113 3114
		if (!defined $head) {
			next;
		}
		$ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}";
3115
		my %co = parse_commit($head);
3116 3117 3118 3119 3120 3121 3122 3123 3124
		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 已提交
3125 3126 3127 3128 3129
	print <<XML;
</outline>
</body>
</opml>
XML
3130
}