gitweb.perl 87.9 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
	# action which does not check rest of parameters
M
Martin Waitz 已提交
159
	if ($action eq "opml") {
K
v220  
Kay Sievers 已提交
160 161
		git_opml();
		exit;
K
v118  
Kay Sievers 已提交
162
	}
K
v107  
Kay Sievers 已提交
163
}
K
v014  
Kay Sievers 已提交
164

165
our $project = ($cgi->param('p') || $ENV{'PATH_INFO'});
M
Matthias Lederhofer 已提交
166 167 168
if (defined $project) {
	$project =~ s|^/||;
	$project =~ s|/$||;
169
	$project = undef unless $project;
M
Matthias Lederhofer 已提交
170
}
171
if (defined $project) {
172
	if (!validate_input($project)) {
173
		die_error(undef, "Invalid project parameter");
K
v070  
Kay Sievers 已提交
174 175
	}
	if (!(-d "$projectroot/$project")) {
176
		die_error(undef, "No such directory");
K
v107  
Kay Sievers 已提交
177 178
	}
	if (!(-e "$projectroot/$project/HEAD")) {
179
		die_error(undef, "No such project");
K
v070  
Kay Sievers 已提交
180
	}
K
v227  
Kay Sievers 已提交
181
	$ENV{'GIT_DIR'} = "$projectroot/$project";
K
v118  
Kay Sievers 已提交
182
} else {
K
v142  
Kay Sievers 已提交
183
	git_project_list();
K
v118  
Kay Sievers 已提交
184
	exit;
K
v055  
Kay Sievers 已提交
185
}
K
v085  
Kay Sievers 已提交
186

187
our $file_name = $cgi->param('f');
K
v107  
Kay Sievers 已提交
188
if (defined $file_name) {
189
	if (!validate_input($file_name)) {
190
		die_error(undef, "Invalid file parameter");
K
v107  
Kay Sievers 已提交
191
	}
K
v055  
Kay Sievers 已提交
192
}
K
v085  
Kay Sievers 已提交
193

194 195 196 197 198 199 200
our $file_parent = $cgi->param('fp');
if (defined $file_parent) {
	if (!validate_input($file_parent)) {
		die_error(undef, "Invalid file parent parameter");
	}
}

201
our $hash = $cgi->param('h');
K
v227  
Kay Sievers 已提交
202
if (defined $hash) {
203
	if (!validate_input($hash)) {
204
		die_error(undef, "Invalid hash parameter");
K
v227  
Kay Sievers 已提交
205
	}
K
v055  
Kay Sievers 已提交
206
}
K
v085  
Kay Sievers 已提交
207

208
our $hash_parent = $cgi->param('hp');
209
if (defined $hash_parent) {
210
	if (!validate_input($hash_parent)) {
211
		die_error(undef, "Invalid hash parent parameter");
212
	}
K
v118  
Kay Sievers 已提交
213 214
}

215
our $hash_base = $cgi->param('hb');
216
if (defined $hash_base) {
217
	if (!validate_input($hash_base)) {
218
		die_error(undef, "Invalid hash base parameter");
219
	}
K
v055  
Kay Sievers 已提交
220
}
K
v085  
Kay Sievers 已提交
221

222
our $page = $cgi->param('pg');
K
v206  
Kay Sievers 已提交
223
if (defined $page) {
224
	if ($page =~ m/[^0-9]$/) {
225
		die_error(undef, "Invalid page parameter");
K
v107  
Kay Sievers 已提交
226
	}
K
v053  
Kay Sievers 已提交
227
}
K
v005  
Kay Sievers 已提交
228

229
our $searchtext = $cgi->param('s');
K
v203  
Kay Sievers 已提交
230 231
if (defined $searchtext) {
	if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
232
		die_error(undef, "Invalid search parameter");
K
v203  
Kay Sievers 已提交
233 234 235 236
	}
	$searchtext = quotemeta $searchtext;
}

237
# dispatch
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
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 已提交
257
	"snapshot" => \&git_snapshot,
258 259 260 261
);

$action = 'summary' if (!defined($action));
if (!defined($actions{$action})) {
262
	die_error(undef, "Unknown action");
K
v118  
Kay Sievers 已提交
263
}
264 265
$actions{$action}->();
exit;
K
v118  
Kay Sievers 已提交
266

267 268 269 270 271 272 273 274
## ======================================================================
## action links

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

	my %params = @_;
	$params{"project"} ||= $project;

	my $href = "$my_uri?";
	$href .= esc_param( join(";",
288 289 290
		map {
			"$mapping{$_}=$params{$_}" if defined $params{$_}
		} keys %params
291 292 293 294 295 296
	) );

	return $href;
}


297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
## ======================================================================
## 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;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

526 527
## ----------------------------------------------------------------------
## git utility subroutines, invoking git commands
K
v125  
Kay Sievers 已提交
528

529
# get HEAD ref of given project as hash
530
sub git_get_head_hash {
531 532 533 534
	my $project = shift;
	my $oENV = $ENV{'GIT_DIR'};
	my $retval = undef;
	$ENV{'GIT_DIR'} = "$projectroot/$project";
535
	if (open my $fd, "-|", $GIT, "rev-parse", "--verify", "HEAD") {
536 537
		my $head = <$fd>;
		close $fd;
K
Kay Sievers 已提交
538 539
		if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
			$retval = $1;
540 541
		}
	}
K
Kay Sievers 已提交
542 543 544
	if (defined $oENV) {
		$ENV{'GIT_DIR'} = $oENV;
	}
545 546 547
	return $retval;
}

548 549 550 551 552 553 554 555 556 557 558 559
# 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 {
560
	my ($key, $type) = @_;
561 562 563 564 565

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

566 567 568 569 570 571
	my @x = ($GIT, 'repo-config');
	if (defined $type) { push @x, $type; }
	push @x, "--get";
	push @x, "gitweb.$key";
	my $val = qx(@x);
	chomp $val;
572 573 574 575 576 577 578 579 580 581 582
	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
583
		or die_error(undef, "Open git-ls-tree failed");
584 585 586 587 588 589 590 591 592 593 594 595
	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
596
sub git_get_hash_by_ref {
K
v041  
Kay Sievers 已提交
597
	my $path = shift;
K
v118  
Kay Sievers 已提交
598

K
v203  
Kay Sievers 已提交
599
	open my $fd, "$projectroot/$path" or return undef;
K
v021  
Kay Sievers 已提交
600 601 602
	my $head = <$fd>;
	close $fd;
	chomp $head;
K
v107  
Kay Sievers 已提交
603 604 605 606 607
	if ($head =~ m/^[0-9a-fA-F]{40}$/) {
		return $head;
	}
}

608
sub git_get_project_description {
K
v107  
Kay Sievers 已提交
609
	my $path = shift;
K
v118  
Kay Sievers 已提交
610

K
v203  
Kay Sievers 已提交
611
	open my $fd, "$projectroot/$path/description" or return undef;
K
v107  
Kay Sievers 已提交
612 613 614 615
	my $descr = <$fd>;
	close $fd;
	chomp $descr;
	return $descr;
K
v021  
Kay Sievers 已提交
616 617
}

618 619 620 621 622 623 624 625 626 627
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;
}

628
sub git_get_projects_list {
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
	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;
}

672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
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;
}

703
sub git_get_references {
704 705
	my $type = shift || "";
	my %refs;
706
	my $fd;
707 708
	# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c	refs/tags/v2.6.11
	# c39ae07f393806ccf406ef966e9a15afc43cc36a	refs/tags/v2.6.11^{}
709 710 711 712 713 714 715 716
	if (-f "$projectroot/$project/info/refs") {
		open $fd, "$projectroot/$project/info/refs"
			or return;
	} else {
		open $fd, "-|", $GIT, "ls-remote", "."
			or return;
	}

717 718
	while (my $line = <$fd>) {
		chomp $line;
719
		if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
720
			if (defined $refs{$1}) {
721
				push @{$refs{$1}}, $2;
722
			} else {
723
				$refs{$1} = [ $2 ];
724 725 726 727 728 729 730 731 732 733
			}
		}
	}
	close $fd or return;
	return \%refs;
}

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

734
sub parse_date {
735 736 737 738 739 740 741 742 743 744 745 746
	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];
747 748 749 750
	$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;
751 752 753 754 755 756 757 758 759 760

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

761
sub parse_tag {
K
v142  
Kay Sievers 已提交
762 763
	my $tag_id = shift;
	my %tag;
K
v235  
Kay Sievers 已提交
764
	my @comment;
K
v142  
Kay Sievers 已提交
765

766
	open my $fd, "-|", $GIT, "cat-file", "tag", $tag_id or return;
K
v235  
Kay Sievers 已提交
767
	$tag{'id'} = $tag_id;
K
v142  
Kay Sievers 已提交
768 769 770 771
	while (my $line = <$fd>) {
		chomp $line;
		if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
			$tag{'object'} = $1;
K
v163  
Kay Sievers 已提交
772
		} elsif ($line =~ m/^type (.+)$/) {
K
v142  
Kay Sievers 已提交
773
			$tag{'type'} = $1;
K
v163  
Kay Sievers 已提交
774
		} elsif ($line =~ m/^tag (.+)$/) {
K
v142  
Kay Sievers 已提交
775
			$tag{'name'} = $1;
K
v235  
Kay Sievers 已提交
776 777 778 779 780 781 782 783 784
		} 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 已提交
785 786
		}
	}
K
v235  
Kay Sievers 已提交
787 788
	push @comment, <$fd>;
	$tag{'comment'} = \@comment;
K
v203  
Kay Sievers 已提交
789
	close $fd or return;
K
v142  
Kay Sievers 已提交
790 791 792 793 794 795
	if (!defined $tag{'name'}) {
		return
	};
	return %tag
}

796
sub parse_commit {
K
v203  
Kay Sievers 已提交
797 798 799 800
	my $commit_id = shift;
	my $commit_text = shift;

	my @commit_lines;
K
v021  
Kay Sievers 已提交
801 802
	my %co;

K
v203  
Kay Sievers 已提交
803 804 805
	if (defined $commit_text) {
		@commit_lines = @$commit_text;
	} else {
806
		$/ = "\0";
807 808
		open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", "--max-count=1", $commit_id
			or return;
809
		@commit_lines = split '\n', <$fd>;
K
v203  
Kay Sievers 已提交
810
		close $fd or return;
811 812
		$/ = "\n";
		pop @commit_lines;
K
v203  
Kay Sievers 已提交
813
	}
814 815 816 817 818 819 820
	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 已提交
821
	while (my $line = shift @commit_lines) {
K
v107  
Kay Sievers 已提交
822
		last if $line eq "\n";
K
v163  
Kay Sievers 已提交
823
		if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
K
v021  
Kay Sievers 已提交
824
			$co{'tree'} = $1;
K
v035  
Kay Sievers 已提交
825
		} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
K
v021  
Kay Sievers 已提交
826
			$co{'author'} = $1;
K
v049  
Kay Sievers 已提交
827 828
			$co{'author_epoch'} = $2;
			$co{'author_tz'} = $3;
K
v164  
Kay Sievers 已提交
829 830 831 832 833
			if ($co{'author'} =~ m/^([^<]+) </) {
				$co{'author_name'} = $1;
			} else {
				$co{'author_name'} = $co{'author'};
			}
K
v041  
Kay Sievers 已提交
834 835
		} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
			$co{'committer'} = $1;
K
v049  
Kay Sievers 已提交
836 837
			$co{'committer_epoch'} = $2;
			$co{'committer_tz'} = $3;
K
v042  
Kay Sievers 已提交
838 839
			$co{'committer_name'} = $co{'committer'};
			$co{'committer_name'} =~ s/ <.*//;
K
v021  
Kay Sievers 已提交
840 841
		}
	}
K
v142  
Kay Sievers 已提交
842
	if (!defined $co{'tree'}) {
843
		return;
K
v142  
Kay Sievers 已提交
844
	};
845

K
v203  
Kay Sievers 已提交
846
	foreach my $title (@commit_lines) {
847
		$title =~ s/^    //;
K
v203  
Kay Sievers 已提交
848
		if ($title ne "") {
K
v241  
Kay Sievers 已提交
849
			$co{'title'} = chop_str($title, 80, 5);
K
v203  
Kay Sievers 已提交
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
			# 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 已提交
867
			$co{'title_short'} = chop_str($title, 50, 5);
K
v203  
Kay Sievers 已提交
868 869 870
			last;
		}
	}
871 872 873 874 875
	# remove added spaces
	foreach my $line (@commit_lines) {
		$line =~ s/^    //;
	}
	$co{'comment'} = \@commit_lines;
K
v062  
Kay Sievers 已提交
876 877 878

	my $age = time - $co{'committer_epoch'};
	$co{'age'} = $age;
K
v236  
Kay Sievers 已提交
879
	$co{'age_string'} = age_string($age);
K
v225  
Kay Sievers 已提交
880 881
	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
	if ($age > 60*60*24*7*2) {
K
v232  
Kay Sievers 已提交
882
		$co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
K
v225  
Kay Sievers 已提交
883 884 885
		$co{'age_string_age'} = $co{'age_string'};
	} else {
		$co{'age_string_date'} = $co{'age_string'};
K
v232  
Kay Sievers 已提交
886
		$co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
K
v225  
Kay Sievers 已提交
887
	}
K
v021  
Kay Sievers 已提交
888 889 890
	return %co;
}

891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933
# 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;
}

934
# parse line of git-diff-tree "raw" output
935 936 937 938 939 940 941 942 943 944 945 946 947 948
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
949
			($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
950 951 952 953 954 955 956 957 958 959 960 961
		} else {
			$res{'file'} = unquote($7);
		}
	}
	# 'c512b523472485aef4fff9e57b229d9d243c967f'
	#elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
	#	$res{'commit'} = $1;
	#}

	return wantarray ? %res : \%res;
}

962 963
## ......................................................................
## parse to array of hashes functions
K
v000  
Kay Sievers 已提交
964

965
sub git_get_refs_list {
966 967
	my $ref_dir = shift;
	my @reflist;
K
v000  
Kay Sievers 已提交
968

969
	my @refs;
970 971 972 973 974
	my $pfxlen = length("$projectroot/$project/$ref_dir");
	File::Find::find(sub {
		return if (/^\./);
		if (-f $_) {
			push @refs, substr($File::Find::name, $pfxlen + 1);
975
		}
976 977
	}, "$projectroot/$project/$ref_dir");

978
	foreach my $ref_file (@refs) {
979
		my $ref_id = git_get_hash_by_ref("$project/$ref_dir/$ref_file");
980
		my $type = git_get_type($ref_id) || next;
981
		my %ref_item = parse_ref($ref_file, $ref_id, $type);
K
v042  
Kay Sievers 已提交
982

983 984
		push @reflist, \%ref_item;
	}
985
	# sort refs by age
986 987
	@reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
	return \@reflist;
K
v041  
Kay Sievers 已提交
988 989
}

990 991
## ----------------------------------------------------------------------
## filesystem-related functions
K
v035  
Kay Sievers 已提交
992

K
v133  
Kay Sievers 已提交
993 994 995 996 997 998 999 1000 1001 1002
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/[,;].*$//;
1003
	return decode("utf8", $owner, Encode::FB_DEFAULT);
K
v133  
Kay Sievers 已提交
1004 1005
}

1006 1007
## ......................................................................
## mimetype related functions
K
v118  
Kay Sievers 已提交
1008

1009 1010 1011 1012 1013 1014 1015 1016
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>) {
1017
		next if m/^#/; # skip comments
1018
		my ($mime, $exts) = split(/\t+/);
1019 1020 1021 1022 1023
		if (defined $exts) {
			my @exts = split(/\s+/, $exts);
			foreach my $ext (@exts) {
				$mimemap{$ext} = $mime;
			}
K
v118  
Kay Sievers 已提交
1024 1025
		}
	}
1026
	close(MIME);
K
v118  
Kay Sievers 已提交
1027

1028 1029 1030
	$filename =~ /\.(.*?)$/;
	return $mimemap{$1};
}
1031

1032 1033 1034 1035
sub mimetype_guess {
	my $filename = shift;
	my $mime;
	$filename =~ /\./ or return undef;
1036

1037 1038
	if ($mimetypes_file) {
		my $file = $mimetypes_file;
1039 1040 1041 1042
		if ($file !~ m!^/!) { # if it is relative path
			# it is relative to project
			$file = "$projectroot/$project/$file";
		}
1043 1044 1045 1046
		$mime = mimetype_guess_file($filename, $file);
	}
	$mime ||= mimetype_guess_file($filename, '/etc/mime.types');
	return $mime;
1047 1048
}

1049
sub blob_mimetype {
1050 1051
	my $fd = shift;
	my $filename = shift;
1052

1053 1054 1055
	if ($filename) {
		my $mime = mimetype_guess($filename);
		$mime and return $mime;
1056
	}
1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071

	# 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';
1072
	} else {
1073
		return 'application/octet-stream';
1074
	}
1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
}

## ======================================================================
## 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 .= "/";
				}
			}
		}
1096
	}
1097 1098 1099 1100 1101
	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.
1102 1103 1104
	if (defined $cgi->http('HTTP_ACCEPT') &&
	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
	    $cgi->Accept('application/xhtml+xml') != 0) {
1105
		$content_type = 'application/xhtml+xml';
1106
	} else {
1107
		$content_type = 'text/html';
1108
	}
1109 1110
	print $cgi->header(-type=>$content_type, -charset => 'utf-8',
	                   -status=> $status, -expires => $expires);
1111 1112 1113 1114
	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">
1115
<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
1116 1117 1118
<!-- git core binaries version $git_version -->
<head>
<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
1119
<meta name="generator" content="gitweb/$version git/$git_version"/>
1120 1121 1122 1123
<meta name="robots" content="index, nofollow"/>
<title>$title</title>
<link rel="stylesheet" type="text/css" href="$stylesheet"/>
EOF
M
Matthias Lederhofer 已提交
1124 1125 1126
	if (defined $project) {
		printf('<link rel="alternate" title="%s log" '.
		       'href="%s" type="application/rss+xml"/>'."\n",
1127
		       esc_param($project), href(action=>"rss"));
M
Matthias Lederhofer 已提交
1128
	}
J
Jakub Narebski 已提交
1129

M
Matthias Lederhofer 已提交
1130 1131
	print "</head>\n" .
	      "<body>\n" .
J
Jakub Narebski 已提交
1132
	      "<div class=\"page_header\">\n" .
1133
	      "<a href=\"http://www.kernel.org/pub/software/scm/git/docs/\" title=\"git documentation\">" .
M
Martin Waitz 已提交
1134
	      "<img src=\"$logo\" width=\"72\" height=\"27\" alt=\"git\" style=\"float:right; border-width:0px;\"/>" .
1135
	      "</a>\n";
1136
	print $cgi->a({-href => esc_param($home_link)}, $home_link_str) . " / ";
1137
	if (defined $project) {
1138
		print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150
		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 已提交
1151
		} else {
1152
			$search_hash = "HEAD";
K
v160  
Kay Sievers 已提交
1153
		}
1154 1155 1156 1157 1158 1159 1160 1161 1162 1163
		$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 已提交
1164
	}
1165 1166 1167 1168 1169 1170
	print "</div>\n";
}

sub git_footer_html {
	print "<div class=\"page_footer\">\n";
	if (defined $project) {
1171
		my $descr = git_get_project_description($project);
1172 1173 1174
		if (defined $descr) {
			print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
		}
1175
		print $cgi->a({-href => href(action=>"rss"), -class => "rss_logo"}, "RSS") . "\n";
1176
	} else {
1177
		print $cgi->a({-href => href(action=>"opml"), -class => "rss_logo"}, "OPML") . "\n";
1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193
	}
	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);
	print "<div class=\"page_body\">\n" .
	      "<br/><br/>\n" .
	      "$status - $error\n" .
	      "<br/>\n" .
	      "</div>\n";
K
v107  
Kay Sievers 已提交
1194
	git_footer_html();
1195
	exit;
K
Kay Sievers 已提交
1196 1197
}

1198 1199 1200
## ----------------------------------------------------------------------
## functions printing or outputting HTML: navigation

1201
sub git_print_page_nav {
1202 1203 1204 1205 1206 1207 1208 1209
	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;
	}

1210
	my %arg = map { $_ => {action=>$_} } @navs;
1211 1212
	if (defined $head) {
		for (qw(commit commitdiff)) {
1213
			$arg{$_}{hash} = $head;
1214 1215 1216
		}
		if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
			for (qw(shortlog log)) {
1217
				$arg{$_}{hash} = $head;
K
Kay Sievers 已提交
1218
			}
K
Kay Sievers 已提交
1219 1220
		}
	}
1221 1222
	$arg{tree}{hash} = $treehead if defined $treehead;
	$arg{tree}{hash_base} = $treebase if defined $treebase;
1223 1224 1225

	print "<div class=\"page_nav\">\n" .
		(join " | ",
1226 1227 1228
		 map { $_ eq $current ?
		       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
		 } @navs);
1229 1230
	print "<br/>\n$extra<br/>\n" .
	      "</div>\n";
K
Kay Sievers 已提交
1231 1232
}

1233
sub format_paging_nav {
1234 1235
	my ($action, $hash, $head, $page, $nrevs) = @_;
	my $paging_nav;
1236

1237 1238

	if ($hash ne $head || $page) {
1239
		$paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
1240
	} else {
1241 1242 1243 1244 1245
		$paging_nav .= "HEAD";
	}

	if ($page > 0) {
		$paging_nav .= " &sdot; " .
1246
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
1247
			         -accesskey => "p", -title => "Alt-p"}, "prev");
1248 1249 1250 1251 1252 1253
	} else {
		$paging_nav .= " &sdot; prev";
	}

	if ($nrevs >= (100 * ($page+1)-1)) {
		$paging_nav .= " &sdot; " .
1254
			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
1255
			         -accesskey => "n", -title => "Alt-n"}, "next");
1256 1257
	} else {
		$paging_nav .= " &sdot; next";
1258
	}
1259 1260

	return $paging_nav;
1261 1262
}

1263 1264 1265
## ......................................................................
## functions printing or outputting HTML: div

1266
sub git_print_header_div {
1267
	my ($action, $title, $hash, $hash_base) = @_;
1268
	my %args = ();
1269

1270 1271 1272
	$args{action} = $action;
	$args{hash} = $hash if $hash;
	$args{hash_base} = $hash_base if $hash_base;
1273 1274

	print "<div class=\"header\">\n" .
1275 1276 1277
	      $cgi->a({-href => href(%args), -class => "title"},
	      $title ? $title : $action) .
	      "\n</div>\n";
1278
}
K
v142  
Kay Sievers 已提交
1279

1280 1281 1282
sub git_print_page_path {
	my $name = shift;
	my $type = shift;
1283
	my $hb = shift;
K
v142  
Kay Sievers 已提交
1284

1285
	if (!defined $name) {
1286
		print "<div class=\"page_path\">/</div>\n";
1287
	} elsif (defined $type && $type eq 'blob') {
1288
		print "<div class=\"page_path\">";
1289
		if (defined $hb) {
1290 1291 1292
			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
			                             hash_base=>$hb)},
			              esc_html($name));
1293
		} else {
1294 1295
			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name)},
			              esc_html($name));
1296
		}
1297
		print "<br/></div>\n";
1298
	} else {
1299
		print "<div class=\"page_path\">" . esc_html($name) . "<br/></div>\n";
K
v142  
Kay Sievers 已提交
1300 1301 1302
	}
}

1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 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
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;
}

1363 1364 1365
## ......................................................................
## functions printing large fragments of HTML

1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377
sub git_difftree_body {
	my ($difftree, $parent) = @_;

	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}) {
1378
		my %diff = parse_difftree_raw_line($line);
1379 1380 1381 1382 1383 1384 1385 1386

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

1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399
		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
1400
			}
1401 1402 1403 1404 1405 1406 1407
			$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>";
1408
			print "<td>" .
1409 1410 1411
			      $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
			                             hash_base=>$hash, file_name=>$diff{'file'}),
			              -class => "list"}, esc_html($diff{'file'})) .
1412
			      "</td>\n" .
1413
			      "<td>$mode_chng</td>\n" .
1414
			      "<td class=\"link\">" .
1415 1416 1417
			      $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
			                             hash_base=>$hash, file_name=>$diff{'file'})},
			              "blob") .
1418 1419
			      "</td>\n";

1420 1421
		} elsif ($diff{'status'} eq "D") { # deleted
			my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
1422
			print "<td>" .
1423 1424 1425 1426 1427
			      $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" .
1428
			      "<td class=\"link\">" .
1429 1430 1431 1432 1433 1434 1435 1436
			      $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";
1437

1438
		} elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
1439
			my $mode_chnge = "";
1440 1441 1442 1443
			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";
1444
				}
1445 1446 1447 1448 1449
				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";
1450 1451 1452 1453 1454
					}
				}
				$mode_chnge .= "]</span>\n";
			}
			print "<td>";
1455 1456 1457 1458 1459 1460 1461 1462
			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'}));
1463 1464 1465 1466
			}
			print "</td>\n" .
			      "<td>$mode_chnge</td>\n" .
			      "<td class=\"link\">" .
1467 1468 1469 1470
				$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
1471
				print " | " .
1472 1473 1474
					$cgi->a({-href => href(action=>"blobdiff", hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
					                       hash_base=>$hash, file_name=>$diff{'file'})},
					        "diff");
1475
			}
1476 1477 1478 1479
			print " | " .
				$cgi->a({-href => href(action=>"history",
				                       hash_base=>$hash, file_name=>$diff{'file'})},
				        "history");
1480 1481
			print "</td>\n";

1482 1483 1484
		} 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'}};
1485
			my $mode_chng = "";
1486 1487 1488
			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);
1489 1490
			}
			print "<td>" .
1491 1492 1493 1494 1495 1496 1497 1498
			      $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" .
1499
			      "<td class=\"link\">" .
1500 1501 1502 1503
			      $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'}) {
1504
				print " | " .
1505 1506 1507 1508
					$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");
1509 1510
			}
			print "</td>\n";
1511

1512 1513 1514 1515 1516 1517
		} # we should not encounter Unmerged (U) or Unknown (X) status
		print "</tr>\n";
	}
	print "</table>\n";
}

1518 1519 1520
sub git_shortlog_body {
	# uses global variable $project
	my ($revlist, $from, $to, $refs, $extra) = @_;
1521 1522 1523 1524

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

1525 1526 1527 1528 1529 1530 1531
	$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];
1532 1533 1534
		#my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
1535 1536 1537 1538 1539 1540 1541 1542 1543 1544
		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>";
1545 1546
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
1547 1548
		print "</td>\n" .
		      "<td class=\"link\">" .
1549
		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
A
Aneesh Kumar K.V 已提交
1550 1551 1552 1553 1554
		      $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" .
1555 1556 1557 1558 1559 1560 1561 1562 1563 1564
		      "</tr>\n";
	}
	if (defined $extra) {
		print "<tr>\n" .
		      "<td colspan=\"4\">$extra</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
}

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
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)
1595 1596
		print format_subject_html($co{'title'}, $co{'title_short'},
		                          href(action=>"commit", hash=>$commit), $ref);
1597 1598
		print "</td>\n" .
		      "<td class=\"link\">" .
1599 1600 1601
		      $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);
1602 1603 1604 1605 1606 1607 1608

		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 " | " .
1609 1610
					$cgi->a({-href => href(action=>"blobdiff", hash=>$blob_current, hash_parent=>$blob_parent,
					                       hash_base=>$commit, file_name=>$file_name)},
1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624
					        "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";
}

1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649
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>" .
1650
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
1651
		               -class => "list name"}, esc_html($tag{'name'})) .
1652 1653 1654
		      "</td>\n" .
		      "<td>";
		if (defined $comment) {
1655 1656
			print format_subject_html($comment, $comment_short,
			                          href(action=>"tag", hash=>$tag{'id'}));
1657 1658 1659 1660
		}
		print "</td>\n" .
		      "<td class=\"selflink\">";
		if ($tag{'type'} eq "tag") {
1661
			print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
1662 1663 1664 1665 1666
		} else {
			print "&nbsp;";
		}
		print "</td>\n" .
		      "<td class=\"link\">" . " | " .
1667
		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
1668
		if ($tag{'reftype'} eq "commit") {
1669 1670
			print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
			      " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
1671
		} elsif ($tag{'reftype'} eq "blob") {
1672
			print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704
		}
		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>") .
1705
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
1706
		               -class => "list name"},esc_html($tag{'name'})) .
1707 1708
		      "</td>\n" .
		      "<td class=\"link\">" .
1709 1710
		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
		      $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") .
1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 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 1769 1770 1771 1772 1773 1774 1775 1776 1777
		      "</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;
			}
1778
			$line = untabify($line);
1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797
			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 {
1798 1799
	my $order = $cgi->param('o');
	if (defined $order && $order !~ m/project|descr|owner|age/) {
1800
		die_error(undef, "Unknown order parameter");
1801 1802
	}

1803
	my @list = git_get_projects_list();
1804 1805
	my @projects;
	if (!@list) {
1806
		die_error(undef, "No projects found");
1807 1808
	}
	foreach my $pr (@list) {
1809
		my $head = git_get_head_hash($pr->{'path'});
1810 1811
		if (!defined $head) {
			next;
1812
		}
1813
		$ENV{'GIT_DIR'} = "$projectroot/$pr->{'path'}";
1814
		my %co = parse_commit($head);
1815 1816
		if (!%co) {
			next;
1817
		}
1818 1819
		$pr->{'commit'} = \%co;
		if (!defined $pr->{'descr'}) {
1820
			my $descr = git_get_project_description($pr->{'path'}) || "";
1821
			$pr->{'descr'} = chop_str($descr, 25, 5);
1822
		}
1823 1824
		if (!defined $pr->{'owner'}) {
			$pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
1825
		}
1826
		push @projects, $pr;
1827
	}
1828

1829 1830 1831 1832 1833 1834 1835
	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";
1836
	}
1837 1838
	print "<table class=\"project_list\">\n" .
	      "<tr>\n";
1839 1840
	$order ||= "project";
	if ($order eq "project") {
1841 1842 1843
		@projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
		print "<th>Project</th>\n";
	} else {
1844 1845 1846 1847
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=project"),
		               -class => "header"}, "Project") .
		      "</th>\n";
1848
	}
1849
	if ($order eq "descr") {
1850 1851 1852
		@projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
		print "<th>Description</th>\n";
	} else {
1853 1854 1855 1856
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=descr"),
		               -class => "header"}, "Description") .
		      "</th>\n";
1857
	}
1858
	if ($order eq "owner") {
1859 1860 1861
		@projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
		print "<th>Owner</th>\n";
	} else {
1862 1863 1864 1865
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=owner"),
		               -class => "header"}, "Owner") .
		      "</th>\n";
1866
	}
1867
	if ($order eq "age") {
1868 1869 1870
		@projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
		print "<th>Last Change</th>\n";
	} else {
1871 1872 1873 1874
		print "<th>" .
		      $cgi->a({-href => "$my_uri?" . esc_param("o=age"),
		               -class => "header"}, "Last Change") .
		      "</th>\n";
1875 1876 1877
	}
	print "<th></th>\n" .
	      "</tr>\n";
1878
	my $alternate = 0;
1879
	foreach my $pr (@projects) {
1880 1881 1882 1883 1884 1885
		if ($alternate) {
			print "<tr class=\"dark\">\n";
		} else {
			print "<tr class=\"light\">\n";
		}
		$alternate ^= 1;
1886
		print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
1887
		                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
1888 1889
		      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
		      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
1890 1891
		print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" .
		      $pr->{'commit'}{'age_string'} . "</td>\n" .
1892
		      "<td class=\"link\">" .
1893 1894 1895
		      $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") .
1896 1897 1898 1899
		      "</td>\n" .
		      "</tr>\n";
	}
	print "</table>\n";
1900
	git_footer_html();
1901 1902
}

K
v142  
Kay Sievers 已提交
1903
sub git_summary {
1904 1905 1906 1907
	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 已提交
1908

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

1911
	my $refs = git_get_references();
K
v142  
Kay Sievers 已提交
1912
	git_header_html();
1913
	git_print_page_nav('summary','', $head);
1914

K
v203  
Kay Sievers 已提交
1915
	print "<div class=\"title\">&nbsp;</div>\n";
K
v160  
Kay Sievers 已提交
1916
	print "<table cellspacing=\"0\">\n" .
1917
	      "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
K
v142  
Kay Sievers 已提交
1918
	      "<tr><td>owner</td><td>$owner</td></tr>\n" .
1919
	      "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
1920 1921
	# use per project git URL list in $projectroot/$project/cloneurl
	# or make project git URL from git base URL and project name
1922
	my $url_tag = "URL";
1923 1924 1925 1926 1927
	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";
1928 1929 1930
		$url_tag = "";
	}
	print "</table>\n";
1931

1932
	open my $fd, "-|", $GIT, "rev-list", "--max-count=17", git_get_head_hash($project)
1933
		or die_error(undef, "Open git-rev-list failed");
1934
	my @revlist = map { chomp; $_ } <$fd>;
K
v142  
Kay Sievers 已提交
1935
	close $fd;
1936
	git_print_header_div('shortlog');
1937
	git_shortlog_body(\@revlist, 0, 15, $refs,
1938
	                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
K
v142  
Kay Sievers 已提交
1939

1940
	my $taglist = git_get_refs_list("refs/tags");
K
v142  
Kay Sievers 已提交
1941
	if (defined @$taglist) {
1942
		git_print_header_div('tags');
1943
		git_tags_body($taglist, 0, 15,
1944
		              $cgi->a({-href => href(action=>"tags")}, "..."));
K
v142  
Kay Sievers 已提交
1945
	}
K
v150  
Kay Sievers 已提交
1946

1947
	my $headlist = git_get_refs_list("refs/heads");
K
Kay Sievers 已提交
1948
	if (defined @$headlist) {
1949
		git_print_header_div('heads');
1950
		git_heads_body($headlist, $head, 0, 15,
1951
		               $cgi->a({-href => href(action=>"heads")}, "..."));
K
v150  
Kay Sievers 已提交
1952
	}
1953

K
v142  
Kay Sievers 已提交
1954 1955 1956
	git_footer_html();
}

K
v235  
Kay Sievers 已提交
1957
sub git_tag {
1958
	my $head = git_get_head_hash($project);
K
v235  
Kay Sievers 已提交
1959
	git_header_html();
1960 1961 1962
	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 已提交
1963 1964
	print "<div class=\"title_text\">\n" .
	      "<table cellspacing=\"0\">\n" .
1965 1966
	      "<tr>\n" .
	      "<td>object</td>\n" .
1967 1968 1969 1970
	      "<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" .
1971
	      "</tr>\n";
K
v235  
Kay Sievers 已提交
1972
	if (defined($tag{'author'})) {
1973
		my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
1974
		print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
1975 1976 1977
		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 已提交
1978 1979 1980 1981 1982 1983
	}
	print "</table>\n\n" .
	      "</div>\n";
	print "<div class=\"page_body\">";
	my $comment = $tag{'comment'};
	foreach my $line (@$comment) {
1984
		print esc_html($line) . "<br/>\n";
K
v235  
Kay Sievers 已提交
1985 1986 1987 1988 1989
	}
	print "</div>\n";
	git_footer_html();
}

1990 1991 1992
sub git_blame2 {
	my $fd;
	my $ftype;
1993 1994 1995 1996

	if (!gitweb_check_feature('blame')) {
		die_error('403 Permission denied', "Permission denied");
	}
1997
	die_error('404 Not Found', "File name not defined") if (!$file_name);
1998
	$hash_base ||= git_get_head_hash($project);
1999
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2000
	my %co = parse_commit($hash_base)
2001 2002 2003 2004 2005 2006 2007
		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") {
2008
		die_error("400 Bad Request", "Object is not a blob");
2009 2010
	}
	open ($fd, "-|", $GIT, "blame", '-l', $file_name, $hash_base)
2011
		or die_error(undef, "Open git-blame failed");
2012
	git_header_html();
2013
	my $formats_nav =
2014 2015 2016 2017 2018
		$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");
2019 2020
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2021
	git_print_page_path($file_name, $ftype, $hash_base);
2022
	my @rev_color = (qw(light2 dark2));
2023 2024 2025
	my $num_colors = scalar(@rev_color);
	my $current_color = 0;
	my $last_rev;
2026 2027 2028
	print "<div class=\"page_body\">\n";
	print "<table class=\"blame\">\n";
	print "<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n";
2029 2030 2031
	while (<$fd>) {
		/^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
		my $full_rev = $1;
2032
		my $rev = substr($full_rev, 0, 8);
2033 2034
		my $lineno = $2;
		my $data = $3;
2035

2036 2037 2038 2039 2040 2041 2042
		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";
2043
		print "<td class=\"sha1\">" .
2044 2045 2046 2047
			$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";
2048 2049 2050 2051 2052
		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
		print "</tr>\n";
	}
	print "</table>\n";
	print "</div>";
2053 2054
	close $fd
		or print "Reading blob failed\n";
2055 2056 2057
	git_footer_html();
}

2058 2059
sub git_blame {
	my $fd;
2060 2061 2062 2063

	if (!gitweb_check_feature('blame')) {
		die_error('403 Permission denied', "Permission denied");
	}
2064
	die_error('404 Not Found', "File name not defined") if (!$file_name);
2065
	$hash_base ||= git_get_head_hash($project);
2066
	die_error(undef, "Couldn't find base commit") unless ($hash_base);
2067
	my %co = parse_commit($hash_base)
2068
		or die_error(undef, "Reading commit failed");
2069 2070
	if (!defined $hash) {
		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
2071
			or die_error(undef, "Error lookup file");
2072
	}
2073
	open ($fd, "-|", $GIT, "annotate", '-l', '-t', '-r', $file_name, $hash_base)
2074
		or die_error(undef, "Open git-annotate failed");
2075
	git_header_html();
2076
	my $formats_nav =
2077 2078 2079 2080 2081
		$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");
2082 2083
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2084
	git_print_page_path($file_name, 'blob', $hash_base);
2085 2086
	print "<div class=\"page_body\">\n";
	print <<HTML;
2087
<table class="blame">
2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107
  <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;
2108
		my $age_class;
2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119

		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 {
2120
			print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
2121 2122 2123 2124 2125 2126
			next;
		}
		$short_rev  = substr ($long_rev, 0, 8);
		$age        = time () - $time;
		$age_str    = age_string ($age);
		$age_str    =~ s/ /&nbsp;/g;
2127
		$age_class  = age_class($age);
2128 2129
		$author     = esc_html ($author);
		$author     =~ s/ /&nbsp;/g;
2130 2131

		$data = untabify($data);
2132
		$data = esc_html ($data);
2133

2134 2135
		print <<HTML;
  <tr class="$line_class[$line_class_num]">
2136
    <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
2137 2138 2139 2140 2141 2142 2143 2144
    <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";
2145 2146
	close $fd
		or print "Reading blob failed.\n";
2147 2148
	print "</div>";
	git_footer_html();
2149 2150
}

2151
sub git_tags {
2152
	my $head = git_get_head_hash($project);
2153
	git_header_html();
2154 2155
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2156

2157
	my $taglist = git_get_refs_list("refs/tags");
2158 2159
	if (defined @$taglist) {
		git_tags_body($taglist);
2160
	}
2161
	git_footer_html();
2162 2163
}

2164
sub git_heads {
2165
	my $head = git_get_head_hash($project);
2166
	git_header_html();
2167 2168
	git_print_page_nav('','', $head,undef,$head);
	git_print_header_div('summary', $project);
2169

2170
	my $taglist = git_get_refs_list("refs/heads");
2171 2172
	if (defined @$taglist) {
		git_heads_body($taglist, $head);
2173
	}
2174
	git_footer_html();
2175 2176
}

K
v203  
Kay Sievers 已提交
2177
sub git_blob_plain {
2178
	if (!defined $hash) {
J
Jakub Narebski 已提交
2179
		if (defined $file_name) {
2180
			my $base = $hash_base || git_get_head_hash($project);
J
Jakub Narebski 已提交
2181
			$hash = git_get_hash_by_path($base, $file_name, "blob")
2182
				or die_error(undef, "Error lookup file");
J
Jakub Narebski 已提交
2183
		} else {
2184
			die_error(undef, "No file name defined");
J
Jakub Narebski 已提交
2185 2186
		}
	}
2187
	my $type = shift;
2188
	open my $fd, "-|", $GIT, "cat-file", "blob", $hash
2189
		or die_error(undef, "Couldn't cat $file_name, $hash");
2190

2191
	$type ||= blob_mimetype($fd, $file_name);
2192 2193 2194

	# save as filename, even when no $file_name is given
	my $save_as = "$hash";
2195 2196
	if (defined $file_name) {
		$save_as = $file_name;
2197 2198
	} elsif ($type =~ m/^text\//) {
		$save_as .= '.txt';
2199
	}
2200 2201

	print $cgi->header(-type => "$type", '-content-disposition' => "inline; filename=\"$save_as\"");
K
v203  
Kay Sievers 已提交
2202
	undef $/;
2203
	binmode STDOUT, ':raw';
K
v203  
Kay Sievers 已提交
2204
	print <$fd>;
2205
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
K
v203  
Kay Sievers 已提交
2206 2207 2208 2209
	$/ = "\n";
	close $fd;
}

2210
sub git_blob {
2211
	if (!defined $hash) {
J
Jakub Narebski 已提交
2212
		if (defined $file_name) {
2213
			my $base = $hash_base || git_get_head_hash($project);
J
Jakub Narebski 已提交
2214
			$hash = git_get_hash_by_path($base, $file_name, "blob")
2215
				or die_error(undef, "Error lookup file");
J
Jakub Narebski 已提交
2216
		} else {
2217
			die_error(undef, "No file name defined");
J
Jakub Narebski 已提交
2218 2219
		}
	}
2220
	my $have_blame = gitweb_check_feature('blame');
2221
	open my $fd, "-|", $GIT, "cat-file", "blob", $hash
2222
		or die_error(undef, "Couldn't cat $file_name, $hash");
2223
	my $mimetype = blob_mimetype($fd, $file_name);
2224 2225 2226 2227 2228
	if ($mimetype !~ m/^text\//) {
		close $fd;
		return git_blob_plain($mimetype);
	}
	git_header_html();
2229
	my $formats_nav = '';
2230
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2231 2232
		if (defined $file_name) {
			if ($have_blame) {
2233 2234 2235 2236 2237
				$formats_nav .=
					$cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
					                       hash=>$hash, file_name=>$file_name)},
					        "blame") .
					" | ";
2238
			}
2239
			$formats_nav .=
2240 2241 2242 2243 2244 2245 2246
				$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");
2247
		} else {
2248 2249
			$formats_nav .=
				$cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "plain");
2250
		}
2251 2252
		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2253 2254 2255 2256 2257
	} else {
		print "<div class=\"page_nav\">\n" .
		      "<br/><br/></div>\n" .
		      "<div class=\"title\">$hash</div>\n";
	}
2258
	git_print_page_path($file_name, "blob", $hash_base);
2259 2260 2261 2262 2263
	print "<div class=\"page_body\">\n";
	my $nr;
	while (my $line = <$fd>) {
		chomp $line;
		$nr++;
2264
		$line = untabify($line);
2265 2266
		printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
		       $nr, $nr, $nr, esc_html($line);
2267
	}
2268 2269
	close $fd
		or print "Reading blob failed.\n";
2270 2271 2272 2273
	print "</div>";
	git_footer_html();
}

K
v118  
Kay Sievers 已提交
2274
sub git_tree {
K
v107  
Kay Sievers 已提交
2275
	if (!defined $hash) {
2276
		$hash = git_get_head_hash($project);
K
v118  
Kay Sievers 已提交
2277
		if (defined $file_name) {
2278
			my $base = $hash_base || $hash;
K
v118  
Kay Sievers 已提交
2279 2280
			$hash = git_get_hash_by_path($base, $file_name, "tree");
		}
K
v157  
Kay Sievers 已提交
2281
		if (!defined $hash_base) {
2282
			$hash_base = $hash;
K
v157  
Kay Sievers 已提交
2283
		}
K
v145  
Kay Sievers 已提交
2284
	}
2285
	$/ = "\0";
2286
	open my $fd, "-|", $GIT, "ls-tree", '-z', $hash
2287
		or die_error(undef, "Open git-ls-tree failed");
2288
	my @entries = map { chomp; $_ } <$fd>;
2289
	close $fd or die_error(undef, "Reading tree failed");
2290
	$/ = "\n";
K
v077  
Kay Sievers 已提交
2291

2292 2293
	my $refs = git_get_references();
	my $ref = format_ref_marker($refs, $hash_base);
K
v021  
Kay Sievers 已提交
2294
	git_header_html();
2295
	my %base_key = ();
K
v118  
Kay Sievers 已提交
2296
	my $base = "";
2297
	my $have_blame = gitweb_check_feature('blame');
2298
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2299
		$base_key{hash_base} = $hash_base;
2300 2301
		git_print_page_nav('tree','', $hash_base);
		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
K
v077  
Kay Sievers 已提交
2302 2303 2304 2305 2306
	} else {
		print "<div class=\"page_nav\">\n";
		print "<br/><br/></div>\n";
		print "<div class=\"title\">$hash</div>\n";
	}
K
v118  
Kay Sievers 已提交
2307
	if (defined $file_name) {
2308
		$base = esc_html("$file_name/");
K
v118  
Kay Sievers 已提交
2309
	}
2310
	git_print_page_path($file_name, 'tree', $hash_base);
K
v043  
Kay Sievers 已提交
2311
	print "<div class=\"page_body\">\n";
K
v125  
Kay Sievers 已提交
2312
	print "<table cellspacing=\"0\">\n";
K
v160  
Kay Sievers 已提交
2313
	my $alternate = 0;
K
Kay Sievers 已提交
2314
	foreach my $line (@entries) {
K
v003  
Kay Sievers 已提交
2315
		#'100644	blob	0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
K
v203  
Kay Sievers 已提交
2316
		$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
K
v031  
Kay Sievers 已提交
2317
		my $t_mode = $1;
K
Kay Sievers 已提交
2318 2319
		my $t_type = $2;
		my $t_hash = $3;
2320
		my $t_name = validate_input($4);
K
v160  
Kay Sievers 已提交
2321
		if ($alternate) {
K
v220  
Kay Sievers 已提交
2322
			print "<tr class=\"dark\">\n";
K
v160  
Kay Sievers 已提交
2323
		} else {
K
v220  
Kay Sievers 已提交
2324
			print "<tr class=\"light\">\n";
K
v160  
Kay Sievers 已提交
2325 2326
		}
		$alternate ^= 1;
2327
		print "<td class=\"mode\">" . mode_str($t_mode) . "</td>\n";
K
Kay Sievers 已提交
2328
		if ($t_type eq "blob") {
K
v157  
Kay Sievers 已提交
2329
			print "<td class=\"list\">" .
2330 2331
			      $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 已提交
2332 2333
			      "</td>\n" .
			      "<td class=\"link\">" .
2334 2335
			      $cgi->a({-href => href(action=>"blob", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
			              "blob");
2336
			if ($have_blame) {
2337 2338 2339
				print " | " .
					$cgi->a({-href => href(action=>"blame", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
					        "blame");
2340
			}
2341 2342 2343 2344 2345 2346 2347 2348
			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 已提交
2349
			      "</td>\n";
K
Kay Sievers 已提交
2350
		} elsif ($t_type eq "tree") {
K
v157  
Kay Sievers 已提交
2351
			print "<td class=\"list\">" .
2352 2353
			      $cgi->a({-href => href(action=>"tree", hash=>$t_hash, file_name=>"$base$t_name", %base_key)},
			              esc_html($t_name)) .
K
v203  
Kay Sievers 已提交
2354
			      "</td>\n" .
K
v220  
Kay Sievers 已提交
2355
			      "<td class=\"link\">" .
2356 2357 2358 2359 2360
			      $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 已提交
2361
			      "</td>\n";
K
Kay Sievers 已提交
2362
		}
K
v125  
Kay Sievers 已提交
2363
		print "</tr>\n";
K
Kay Sievers 已提交
2364
	}
K
v125  
Kay Sievers 已提交
2365 2366
	print "</table>\n" .
	      "</div>";
K
v021  
Kay Sievers 已提交
2367
	git_footer_html();
K
v118  
Kay Sievers 已提交
2368 2369
}

A
Aneesh Kumar K.V 已提交
2370 2371
sub git_snapshot {

2372 2373 2374 2375 2376 2377
	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 已提交
2378 2379 2380 2381
	if (!defined $hash) {
		$hash = git_get_head_hash($project);
	}

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

	print $cgi->header(-type => 'application/x-tar',
2385 2386 2387
	                   -content-encoding => $ctype,
	                  '-content-disposition' => "inline; filename=\"$filename\"",
	                   -status => '200 OK');
A
Aneesh Kumar K.V 已提交
2388

2389 2390
	open my $fd, "-|", "$GIT tar-tree $hash \'$project\' | $command" or
		die_error(undef, "Execute git-tar-tree failed.");
A
Aneesh Kumar K.V 已提交
2391 2392 2393 2394 2395 2396 2397
	binmode STDOUT, ':raw';
	print <$fd>;
	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
	close $fd;

}

K
v118  
Kay Sievers 已提交
2398
sub git_log {
2399
	my $head = git_get_head_hash($project);
K
v150  
Kay Sievers 已提交
2400
	if (!defined $hash) {
K
v203  
Kay Sievers 已提交
2401
		$hash = $head;
K
v150  
Kay Sievers 已提交
2402
	}
K
v206  
Kay Sievers 已提交
2403 2404
	if (!defined $page) {
		$page = 0;
K
v107  
Kay Sievers 已提交
2405
	}
2406
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
2407 2408

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

2414
	my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
2415 2416

	git_header_html();
2417
	git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
2418

K
v107  
Kay Sievers 已提交
2419
	if (!@revlist) {
2420
		my %co = parse_commit($hash);
2421

2422
		git_print_header_div('summary', $project);
K
v145  
Kay Sievers 已提交
2423
		print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
K
Kay Sievers 已提交
2424
	}
K
v220  
Kay Sievers 已提交
2425 2426
	for (my $i = ($page * 100); $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
2427 2428
		my $ref = format_ref_marker($refs, $commit);
		my %co = parse_commit($commit);
K
v107  
Kay Sievers 已提交
2429
		next if !%co;
2430 2431
		my %ad = parse_date($co{'author_epoch'});
		git_print_header_div('commit',
2432 2433 2434
		               "<span class=\"age\">$co{'age_string'}</span>" .
		               esc_html($co{'title'}) . $ref,
		               $commit);
K
v088  
Kay Sievers 已提交
2435 2436
		print "<div class=\"title_text\">\n" .
		      "<div class=\"log_link\">\n" .
2437
		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
2438 2439
		      " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
K
v121  
Kay Sievers 已提交
2440
		      "<br/>\n" .
K
v088  
Kay Sievers 已提交
2441
		      "</div>\n" .
2442
		      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
2443 2444 2445 2446
		      "</div>\n";

		print "<div class=\"log_body\">\n";
		git_print_simplified_log($co{'comment'});
K
v118  
Kay Sievers 已提交
2447
		print "</div>\n";
K
v021  
Kay Sievers 已提交
2448
	}
K
v088  
Kay Sievers 已提交
2449
	git_footer_html();
K
v118  
Kay Sievers 已提交
2450 2451 2452
}

sub git_commit {
2453
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
2454
	if (!%co) {
2455
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
2456
	}
2457 2458
	my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
K
Kay Sievers 已提交
2459

K
v235  
Kay Sievers 已提交
2460 2461
	my $parent = $co{'parent'};
	if (!defined $parent) {
2462
		$parent = "--root";
K
v085  
Kay Sievers 已提交
2463
	}
2464
	open my $fd, "-|", $GIT, "diff-tree", '-r', '-M', $parent, $hash
2465
		or die_error(undef, "Open git-diff-tree failed");
2466
	my @difftree = map { chomp; $_ } <$fd>;
2467
	close $fd or die_error(undef, "Reading git-diff-tree failed");
2468 2469 2470 2471 2472 2473

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

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

2480
	my $formats_nav = '';
2481 2482
	if (defined $file_name && defined $co{'parent'}) {
		my $parent = $co{'parent'};
2483 2484 2485
		$formats_nav .=
			$cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
			        "blame");
2486
	}
2487
	git_header_html(undef, $expires);
2488
	git_print_page_nav('commit', defined $co{'parent'} ? '' : 'commitdiff',
2489 2490
	                   $hash, $co{'tree'}, $hash,
	                   $formats_nav);
2491

K
v107  
Kay Sievers 已提交
2492
	if (defined $co{'parent'}) {
2493
		git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
K
v107  
Kay Sievers 已提交
2494
	} else {
2495
		git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
K
v107  
Kay Sievers 已提交
2496
	}
K
v085  
Kay Sievers 已提交
2497
	print "<div class=\"title_text\">\n" .
K
v107  
Kay Sievers 已提交
2498
	      "<table cellspacing=\"0\">\n";
2499
	print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
K
v160  
Kay Sievers 已提交
2500 2501
	      "<tr>" .
	      "<td></td><td> $ad{'rfc2822'}";
K
v080  
Kay Sievers 已提交
2502
	if ($ad{'hour_local'} < 6) {
2503 2504
		printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
K
v107  
Kay Sievers 已提交
2505
	} else {
2506 2507
		printf(" (%02d:%02d %s)",
		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
K
v107  
Kay Sievers 已提交
2508
	}
K
v160  
Kay Sievers 已提交
2509 2510
	print "</td>" .
	      "</tr>\n";
2511
	print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
2512 2513 2514
	print "<tr><td></td><td> $cd{'rfc2822'}" .
	      sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
	      "</td></tr>\n";
2515
	print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
K
v160  
Kay Sievers 已提交
2516 2517
	print "<tr>" .
	      "<td>tree</td>" .
2518
	      "<td class=\"sha1\">" .
2519 2520
	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
	               class => "list"}, $co{'tree'}) .
K
v203  
Kay Sievers 已提交
2521
	      "</td>" .
2522 2523 2524
	      "<td class=\"link\">" .
	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
	              "tree");
A
Aneesh Kumar K.V 已提交
2525
	if ($have_snapshot) {
2526 2527
		print " | " .
		      $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
A
Aneesh Kumar K.V 已提交
2528 2529
	}
	print "</td>" .
K
v160  
Kay Sievers 已提交
2530
	      "</tr>\n";
2531
	my $parents = $co{'parents'};
K
v025  
Kay Sievers 已提交
2532
	foreach my $par (@$parents) {
K
v160  
Kay Sievers 已提交
2533 2534
		print "<tr>" .
		      "<td>parent</td>" .
2535 2536 2537 2538
		      "<td class=\"sha1\">" .
		      $cgi->a({-href => href(action=>"commit", hash=>$par),
		               class => "list"}, $par) .
		      "</td>" .
K
v160  
Kay Sievers 已提交
2539
		      "<td class=\"link\">" .
2540
		      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
2541 2542
		      " | " .
		      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "commitdiff") .
K
v160  
Kay Sievers 已提交
2543 2544
		      "</td>" .
		      "</tr>\n";
K
v025  
Kay Sievers 已提交
2545
	}
J
Jakub Narebski 已提交
2546
	print "</table>".
K
v107  
Kay Sievers 已提交
2547
	      "</div>\n";
2548

K
v043  
Kay Sievers 已提交
2549
	print "<div class=\"page_body\">\n";
2550
	git_print_log($co{'comment'});
K
v080  
Kay Sievers 已提交
2551
	print "</div>\n";
2552 2553 2554

	git_difftree_body(\@difftree, $parent);

K
v021  
Kay Sievers 已提交
2555
	git_footer_html();
K
v118  
Kay Sievers 已提交
2556 2557 2558
}

sub git_blobdiff {
K
v203  
Kay Sievers 已提交
2559
	mkdir($git_temp, 0700);
K
v021  
Kay Sievers 已提交
2560
	git_header_html();
2561
	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
2562
		my $formats_nav =
2563 2564 2565
			$cgi->a({-href => href(action=>"blobdiff_plain",
			                       hash=>$hash, hash_parent=>$hash_parent)},
			        "plain");
2566 2567
		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 已提交
2568 2569 2570 2571 2572
	} else {
		print "<div class=\"page_nav\">\n" .
		      "<br/><br/></div>\n" .
		      "<div class=\"title\">$hash vs $hash_parent</div>\n";
	}
2573
	git_print_page_path($file_name, "blob", $hash_base);
K
v070  
Kay Sievers 已提交
2574
	print "<div class=\"page_body\">\n" .
K
v133  
Kay Sievers 已提交
2575
	      "<div class=\"diff_info\">blob:" .
2576 2577 2578
	      $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 已提交
2579
	      " -> blob:" .
2580 2581 2582
	      $cgi->a({-href => href(action=>"blob", hash=>$hash,
	                             hash_base=>$hash_base, file_name=>$file_name)},
	              $hash) .
K
v133  
Kay Sievers 已提交
2583
	      "</div>\n";
K
v203  
Kay Sievers 已提交
2584
	git_diff_print($hash_parent, $file_name || $hash_parent, $hash, $file_name || $hash);
2585
	print "</div>"; # page_body
K
v021  
Kay Sievers 已提交
2586
	git_footer_html();
K
v118  
Kay Sievers 已提交
2587 2588
}

K
v203  
Kay Sievers 已提交
2589 2590 2591 2592 2593 2594
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 已提交
2595
sub git_commitdiff {
K
v203  
Kay Sievers 已提交
2596
	mkdir($git_temp, 0700);
2597
	my %co = parse_commit($hash);
K
v088  
Kay Sievers 已提交
2598
	if (!%co) {
2599
		die_error(undef, "Unknown commit object");
K
v077  
Kay Sievers 已提交
2600
	}
K
v160  
Kay Sievers 已提交
2601
	if (!defined $hash_parent) {
2602
		$hash_parent = $co{'parent'} || '--root';
K
v160  
Kay Sievers 已提交
2603
	}
2604
	open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash
2605
		or die_error(undef, "Open git-diff-tree failed");
2606
	my @difftree = map { chomp; $_ } <$fd>;
2607
	close $fd or die_error(undef, "Reading git-diff-tree failed");
K
Kay Sievers 已提交
2608

2609 2610 2611 2612 2613
	# non-textual hash id's can be cached
	my $expires;
	if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
		$expires = "+1d";
	}
2614 2615
	my $refs = git_get_references();
	my $ref = format_ref_marker($refs, $co{'id'});
2616
	my $formats_nav =
2617 2618
		$cgi->a({-href => href(action=>"commitdiff_plain", hash=>$hash, hash_parent=>$hash_parent)},
		        "plain");
2619
	git_header_html(undef, $expires);
2620 2621
	git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
	git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
K
v133  
Kay Sievers 已提交
2622
	print "<div class=\"page_body\">\n";
2623
	git_print_simplified_log($co{'comment'}, 1); # skip title
K
v150  
Kay Sievers 已提交
2624
	print "<br/>\n";
K
v000  
Kay Sievers 已提交
2625
	foreach my $line (@difftree) {
K
v203  
Kay Sievers 已提交
2626 2627
		# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M      ls-files.c'
		# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M      rev-tree.c'
2628 2629 2630
		if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
			next;
		}
K
v203  
Kay Sievers 已提交
2631 2632 2633 2634 2635
		my $from_mode = $1;
		my $to_mode = $2;
		my $from_id = $3;
		my $to_id = $4;
		my $status = $5;
2636
		my $file = validate_input(unquote($6));
K
v233  
Kay Sievers 已提交
2637
		if ($status eq "A") {
J
Jakub Narebski 已提交
2638
			print "<div class=\"diff_info\">" . file_type($to_mode) . ":" .
2639 2640 2641
			      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
			                             hash=>$to_id, file_name=>$file)},
			              $to_id) . "(new)" .
K
v203  
Kay Sievers 已提交
2642 2643 2644 2645
			      "</div>\n";
			git_diff_print(undef, "/dev/null", $to_id, "b/$file");
		} elsif ($status eq "D") {
			print "<div class=\"diff_info\">" . file_type($from_mode) . ":" .
2646 2647 2648
			      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
			                             hash=>$from_id, file_name=>$file)},
			              $from_id) . "(deleted)" .
K
v203  
Kay Sievers 已提交
2649 2650 2651 2652 2653
			      "</div>\n";
			git_diff_print($from_id, "a/$file", undef, "/dev/null");
		} elsif ($status eq "M") {
			if ($from_id ne $to_id) {
				print "<div class=\"diff_info\">" .
2654
				      file_type($from_mode) . ":" .
2655 2656 2657
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
				                             hash=>$from_id, file_name=>$file)},
				              $from_id) .
K
v203  
Kay Sievers 已提交
2658
				      " -> " .
2659
				      file_type($to_mode) . ":" .
2660 2661 2662
				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
				                             hash=>$to_id, file_name=>$file)},
				              $to_id);
K
v203  
Kay Sievers 已提交
2663 2664
				print "</div>\n";
				git_diff_print($from_id, "a/$file",  $to_id, "b/$file");
K
v000  
Kay Sievers 已提交
2665 2666
			}
		}
K
Kay Sievers 已提交
2667
	}
K
v133  
Kay Sievers 已提交
2668 2669
	print "<br/>\n" .
	      "</div>";
K
v021  
Kay Sievers 已提交
2670
	git_footer_html();
K
v118  
Kay Sievers 已提交
2671 2672
}

K
v203  
Kay Sievers 已提交
2673 2674
sub git_commitdiff_plain {
	mkdir($git_temp, 0700);
2675
	my %co = parse_commit($hash);
2676 2677 2678 2679 2680 2681
	if (!%co) {
		die_error(undef, "Unknown commit object");
	}
	if (!defined $hash_parent) {
		$hash_parent = $co{'parent'} || '--root';
	}
2682
	open my $fd, "-|", $GIT, "diff-tree", '-r', $hash_parent, $hash
2683
		or die_error(undef, "Open git-diff-tree failed");
2684
	my @difftree = map { chomp; $_ } <$fd>;
2685
	close $fd or die_error(undef, "Reading diff-tree failed");
K
v203  
Kay Sievers 已提交
2686

K
v232  
Kay Sievers 已提交
2687 2688
	# try to figure out the next tag after this commit
	my $tagname;
2689
	my $refs = git_get_references("tags");
2690
	open $fd, "-|", $GIT, "rev-list", "HEAD";
2691
	my @commits = map { chomp; $_ } <$fd>;
K
Kay Sievers 已提交
2692 2693 2694 2695
	close $fd;
	foreach my $commit (@commits) {
		if (defined $refs->{$commit}) {
			$tagname = $refs->{$commit}
K
v232  
Kay Sievers 已提交
2696 2697 2698 2699 2700 2701
		}
		if ($commit eq $hash) {
			last;
		}
	}

2702 2703 2704
	print $cgi->header(-type => "text/plain",
	                   -charset => 'utf-8',
	                  '-content-disposition' => "inline; filename=\"git-$hash.patch\"");
2705
	my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
K
v220  
Kay Sievers 已提交
2706
	my $comment = $co{'comment'};
K
v232  
Kay Sievers 已提交
2707
	print "From: $co{'author'}\n" .
K
v220  
Kay Sievers 已提交
2708
	      "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n".
K
v232  
Kay Sievers 已提交
2709 2710
	      "Subject: $co{'title'}\n";
	if (defined $tagname) {
J
Jakub Narebski 已提交
2711
		print "X-Git-Tag: $tagname\n";
K
v232  
Kay Sievers 已提交
2712 2713
	}
	print "X-Git-Url: $my_url?p=$project;a=commitdiff;h=$hash\n" .
K
v220  
Kay Sievers 已提交
2714
	      "\n";
K
v232  
Kay Sievers 已提交
2715

K
v220  
Kay Sievers 已提交
2716
	foreach my $line (@$comment) {;
2717
		print "$line\n";
K
v220  
Kay Sievers 已提交
2718
	}
K
v232  
Kay Sievers 已提交
2719 2720
	print "---\n\n";

K
v203  
Kay Sievers 已提交
2721
	foreach my $line (@difftree) {
2722 2723 2724
		if ($line !~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
			next;
		}
K
v203  
Kay Sievers 已提交
2725 2726 2727 2728
		my $from_id = $3;
		my $to_id = $4;
		my $status = $5;
		my $file = $6;
K
v233  
Kay Sievers 已提交
2729
		if ($status eq "A") {
K
v203  
Kay Sievers 已提交
2730 2731 2732 2733 2734 2735 2736 2737 2738
			git_diff_print(undef, "/dev/null", $to_id, "b/$file", "plain");
		} elsif ($status eq "D") {
			git_diff_print($from_id, "a/$file", undef, "/dev/null", "plain");
		} elsif ($status eq "M") {
			git_diff_print($from_id, "a/$file",  $to_id, "b/$file", "plain");
		}
	}
}

K
v118  
Kay Sievers 已提交
2739
sub git_history {
2740
	if (!defined $hash_base) {
2741
		$hash_base = git_get_head_hash($project);
K
v118  
Kay Sievers 已提交
2742
	}
2743
	my $ftype;
2744
	my %co = parse_commit($hash_base);
K
v118  
Kay Sievers 已提交
2745
	if (!%co) {
2746
		die_error(undef, "Unknown commit object");
K
v062  
Kay Sievers 已提交
2747
	}
2748
	my $refs = git_get_references();
K
v057  
Kay Sievers 已提交
2749
	git_header_html();
2750 2751
	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base);
	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
2752 2753 2754
	if (!defined $hash && defined $file_name) {
		$hash = git_get_hash_by_path($hash_base, $file_name);
	}
2755
	if (defined $hash) {
2756
		$ftype = git_get_type($hash);
2757
	}
2758
	git_print_page_path($file_name, $ftype, $hash_base);
K
v157  
Kay Sievers 已提交
2759

2760
	open my $fd, "-|",
2761
		$GIT, "rev-list", "--full-history", $hash_base, "--", $file_name;
2762 2763
	git_history_body($fd, $refs, $hash_base, $ftype);

K
v107  
Kay Sievers 已提交
2764
	close $fd;
K
v057  
Kay Sievers 已提交
2765
	git_footer_html();
K
Kay Sievers 已提交
2766
}
K
v203  
Kay Sievers 已提交
2767 2768 2769

sub git_search {
	if (!defined $searchtext) {
2770
		die_error(undef, "Text field empty");
K
v203  
Kay Sievers 已提交
2771 2772
	}
	if (!defined $hash) {
2773
		$hash = git_get_head_hash($project);
K
v203  
Kay Sievers 已提交
2774
	}
2775
	my %co = parse_commit($hash);
K
v203  
Kay Sievers 已提交
2776
	if (!%co) {
2777
		die_error(undef, "Unknown commit object");
K
v203  
Kay Sievers 已提交
2778
	}
K
v220  
Kay Sievers 已提交
2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792
	# 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 已提交
2793
	git_header_html();
2794 2795
	git_print_page_nav('','', $hash,$co{'tree'},$hash);
	git_print_header_div('commit', esc_html($co{'title'}), $hash);
K
v203  
Kay Sievers 已提交
2796 2797 2798

	print "<table cellspacing=\"0\">\n";
	my $alternate = 0;
K
v220  
Kay Sievers 已提交
2799 2800
	if ($commit_search) {
		$/ = "\0";
2801
		open my $fd, "-|", $GIT, "rev-list", "--header", "--parents", $hash or next;
K
v220  
Kay Sievers 已提交
2802 2803 2804
		while (my $commit_text = <$fd>) {
			if (!grep m/$searchtext/i, $commit_text) {
				next;
K
v203  
Kay Sievers 已提交
2805
			}
K
v220  
Kay Sievers 已提交
2806 2807
			if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
				next;
K
v203  
Kay Sievers 已提交
2808
			}
K
v220  
Kay Sievers 已提交
2809
			if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
K
v203  
Kay Sievers 已提交
2810 2811
				next;
			}
K
v220  
Kay Sievers 已提交
2812
			my @commit_lines = split "\n", $commit_text;
2813
			my %co = parse_commit(undef, \@commit_lines);
K
v220  
Kay Sievers 已提交
2814 2815 2816 2817 2818 2819 2820 2821 2822
			if (!%co) {
				next;
			}
			if ($alternate) {
				print "<tr class=\"dark\">\n";
			} else {
				print "<tr class=\"light\">\n";
			}
			$alternate ^= 1;
K
v225  
Kay Sievers 已提交
2823
			print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2824
			      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
K
v220  
Kay Sievers 已提交
2825
			      "<td>" .
2826 2827
			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
			               esc_html(chop_str($co{'title'}, 50)) . "<br/>");
K
v220  
Kay Sievers 已提交
2828 2829 2830
			my $comment = $co{'comment'};
			foreach my $line (@$comment) {
				if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
2831
					my $lead = esc_html($1) || "";
K
v220  
Kay Sievers 已提交
2832
					$lead = chop_str($lead, 30, 10);
2833 2834
					my $match = esc_html($2) || "";
					my $trail = esc_html($3) || "";
K
v220  
Kay Sievers 已提交
2835
					$trail = chop_str($trail, 30, 10);
2836
					my $text = "$lead<span class=\"match\">$match</span>$trail";
K
v220  
Kay Sievers 已提交
2837
					print chop_str($text, 80, 5) . "<br/>\n";
K
v203  
Kay Sievers 已提交
2838
				}
K
v220  
Kay Sievers 已提交
2839 2840 2841
			}
			print "</td>\n" .
			      "<td class=\"link\">" .
2842
			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
2843 2844
			      " | " .
			      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
K
v220  
Kay Sievers 已提交
2845 2846 2847 2848 2849 2850 2851 2852
			print "</td>\n" .
			      "</tr>\n";
		}
		close $fd;
	}

	if ($pickaxe_search) {
		$/ = "\n";
2853
		open my $fd, "-|", "$GIT rev-list $hash | $GIT diff-tree -r --stdin -S\'$searchtext\'";
K
v220  
Kay Sievers 已提交
2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864
		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 已提交
2865
				}
K
v220  
Kay Sievers 已提交
2866 2867 2868 2869
				if ($set{'id'} =~ m/0{40}/) {
					next;
				}
				push @files, \%set;
2870
			} elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
K
v220  
Kay Sievers 已提交
2871 2872 2873 2874 2875 2876 2877
				if (%co) {
					if ($alternate) {
						print "<tr class=\"dark\">\n";
					} else {
						print "<tr class=\"light\">\n";
					}
					$alternate ^= 1;
K
v225  
Kay Sievers 已提交
2878
					print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
2879
					      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
K
v220  
Kay Sievers 已提交
2880
					      "<td>" .
2881 2882
					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
					              -class => "list subject"},
2883
					              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
K
v220  
Kay Sievers 已提交
2884 2885
					while (my $setref = shift @files) {
						my %set = %$setref;
2886 2887 2888 2889
						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 已提交
2890 2891 2892 2893
						      "<br/>\n";
					}
					print "</td>\n" .
					      "<td class=\"link\">" .
2894
					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
2895 2896
					      " | " .
					      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
K
v220  
Kay Sievers 已提交
2897 2898 2899
					print "</td>\n" .
					      "</tr>\n";
				}
2900
				%co = parse_commit($1);
K
v203  
Kay Sievers 已提交
2901 2902
			}
		}
K
v220  
Kay Sievers 已提交
2903
		close $fd;
K
v203  
Kay Sievers 已提交
2904 2905 2906 2907 2908 2909
	}
	print "</table>\n";
	git_footer_html();
}

sub git_shortlog {
2910
	my $head = git_get_head_hash($project);
K
v203  
Kay Sievers 已提交
2911 2912 2913
	if (!defined $hash) {
		$hash = $head;
	}
K
v206  
Kay Sievers 已提交
2914 2915 2916
	if (!defined $page) {
		$page = 0;
	}
2917
	my $refs = git_get_references();
K
v206  
Kay Sievers 已提交
2918 2919

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

2925
	my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
2926 2927 2928
	my $next_link = '';
	if ($#revlist >= (100 * ($page+1)-1)) {
		$next_link =
2929
			$cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
2930 2931 2932
			         -title => "Alt-n"}, "next");
	}

2933 2934

	git_header_html();
2935 2936
	git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
	git_print_header_div('summary', $project);
2937

2938 2939
	git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);

K
v203  
Kay Sievers 已提交
2940 2941
	git_footer_html();
}
2942 2943 2944 2945 2946 2947

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

sub git_rss {
	# http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
2948
	open my $fd, "-|", $GIT, "rev-list", "--max-count=150", git_get_head_hash($project)
2949
		or die_error(undef, "Open git-rev-list failed");
2950
	my @revlist = map { chomp; $_ } <$fd>;
2951
	close $fd or die_error(undef, "Reading git-rev-list failed");
2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962
	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
	print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
	      "<rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n";
	print "<channel>\n";
	print "<title>$project</title>\n".
	      "<link>" . esc_html("$my_url?p=$project;a=summary") . "</link>\n".
	      "<description>$project log</description>\n".
	      "<language>en</language>\n";

	for (my $i = 0; $i <= $#revlist; $i++) {
		my $commit = $revlist[$i];
2963
		my %co = parse_commit($commit);
2964 2965 2966 2967
		# 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;
		}
2968
		my %cd = parse_date($co{'committer_epoch'});
2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004
		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 {
3005
	my @list = git_get_projects_list();
3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017

	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
	print "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
	      "<opml version=\"1.0\">\n".
	      "<head>".
	      "  <title>$site_name Git OPML Export</title>\n".
	      "</head>\n".
	      "<body>\n".
	      "<outline text=\"git RSS feeds\">\n";

	foreach my $pr (@list) {
		my %proj = %$pr;
3018
		my $head = git_get_head_hash($proj{'path'});
3019 3020 3021 3022
		if (!defined $head) {
			next;
		}
		$ENV{'GIT_DIR'} = "$projectroot/$proj{'path'}";
3023
		my %co = parse_commit($head);
3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036
		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";
	}
	print "</outline>\n".
	      "</body>\n".
	      "</opml>\n";
}