summaryrefslogtreecommitdiff
path: root/gitweb/gitweb.cgi
blob: ba94c36b53466347b89609c6378efd6f8ca0fd19 (plain)
    1 #!/usr/bin/perl
    2 
    3 # gitweb - simple web interface to track changes in git repositories
    4 #
    5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
    6 # (C) 2005, Christian Gierke
    7 #
    8 # This program is licensed under the GPLv2
    9 
   10 use strict;
   11 use warnings;
   12 use CGI qw(:standard :escapeHTML -nosticky);
   13 use CGI::Util qw(unescape);
   14 use CGI::Carp qw(fatalsToBrowser);
   15 use Encode;
   16 use Fcntl ':mode';
   17 use File::Find qw();
   18 use File::Basename qw(basename);
   19 binmode STDOUT, ':utf8';
   20 
   21 our $cgi = new CGI;
   22 our $version = "1.4.3.2";
   23 our $my_url = $cgi->url();
   24 our $my_uri = $cgi->url(-absolute => 1);
   25 
   26 # core git executable to use
   27 # this can just be "git" if your webserver has a sensible PATH
   28 our $GIT = "/usr/bin/git";
   29 
   30 # absolute fs-path which will be prepended to the project path
   31 #our $projectroot = "/pub/scm";
   32 our $projectroot = "/home/crux/scm";
   33 
   34 # target of the home link on top of all pages
   35 our $home_link = $my_uri || "/";
   36 
   37 # string of the home link on top of all pages
   38 our $home_link_str = "projects";
   39 
   40 # name of your site or organization to appear in page titles
   41 # replace this with something more descriptive for clearer bookmarks
   42 our $site_name = "" || $ENV{'SERVER_NAME'} || "Untitled";
   43 
   44 # html text to include at home page
   45 our $home_text = "indextext.html";
   46 
   47 # URI of default stylesheet
   48 our $stylesheet = "gitweb.css";
   49 # URI of GIT logo (72x27 size)
   50 our $logo = "git-logo.png";
   51 # URI of GIT favicon, assumed to be image/png type
   52 our $favicon = "git-favicon.png";
   53 
   54 # URI and label (title) of GIT logo link
   55 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
   56 #our $logo_label = "git documentation";
   57 our $logo_url = "http://git.or.cz/";
   58 our $logo_label = "git homepage";
   59 
   60 # source of projects list
   61 our $projects_list = "index.aux";
   62 
   63 # show repository only if this file exists
   64 # (only effective if this variable evaluates to true)
   65 our $export_ok = "";
   66 
   67 # only allow viewing of repositories also shown on the overview page
   68 our $strict_export = "";
   69 
   70 # list of git base URLs used for URL to where fetch project from,
   71 # i.e. full URL is "$git_base_url/$project"
   72 our @git_base_url_list = ("");
   73 
   74 # default blob_plain mimetype and default charset for text/plain blob
   75 our $default_blob_plain_mimetype = 'text/plain';
   76 our $default_text_plain_charset  = undef;
   77 
   78 # file to use for guessing MIME types before trying /etc/mime.types
   79 # (relative to the current git repository)
   80 our $mimetypes_file = undef;
   81 
   82 # You define site-wide feature defaults here; override them with
   83 # $GITWEB_CONFIG as necessary.
   84 our %feature = (
   85 	# feature => {
   86 	# 	'sub' => feature-sub (subroutine),
   87 	# 	'override' => allow-override (boolean),
   88 	# 	'default' => [ default options...] (array reference)}
   89 	#
   90 	# if feature is overridable (it means that allow-override has true value,
   91 	# then feature-sub will be called with default options as parameters;
   92 	# return value of feature-sub indicates if to enable specified feature
   93 	#
   94 	# use gitweb_check_feature(<feature>) to check if <feature> is enabled
   95 
   96 	'blame' => {
   97 		'sub' => \&feature_blame,
   98 		'override' => 0,
   99 		'default' => [0]},
  100 
  101 	'snapshot' => {
  102 		'sub' => \&feature_snapshot,
  103 		'override' => 0,
  104 		#         => [content-encoding, suffix, program]
  105 		'default' => ['x-gzip', 'gz', 'gzip']},
  106 
  107 	'pickaxe' => {
  108 		'sub' => \&feature_pickaxe,
  109 		'override' => 0,
  110 		'default' => [1]},
  111 );
  112 
  113 # CRUX: allow custom default HEAD for each project
  114 our %default_heads = (
  115     "ports/core.git" => "2.2",
  116     "ports/opt.git" => "2.2",
  117     "ports/contrib.git" => "2.2",
  118     "ports/xorg.git" => "2.2",
  119     "ports/sip.git" => "2.2",
  120 );
  121 
  122 sub gitweb_get_default_head {
  123     my $project = shift;
  124     exists $default_heads{$project} && return $default_heads{$project};
  125     return "HEAD";
  126 }
  127 
  128 sub gitweb_check_feature {
  129 	my ($name) = @_;
  130 	return unless exists $feature{$name};
  131 	my ($sub, $override, @defaults) = (
  132 		$feature{$name}{'sub'},
  133 		$feature{$name}{'override'},
  134 		@{$feature{$name}{'default'}});
  135 	if (!$override) { return @defaults; }
  136 	return $sub->(@defaults);
  137 }
  138 
  139 # To enable system wide have in $GITWEB_CONFIG
  140 # $feature{'blame'}{'default'} = [1];
  141 # To have project specific config enable override in $GITWEB_CONFIG
  142 # $feature{'blame'}{'override'} = 1;
  143 # and in project config gitweb.blame = 0|1;
  144 
  145 sub feature_blame {
  146 	my ($val) = git_get_project_config('blame', '--bool');
  147 
  148 	if ($val eq 'true') {
  149 		return 1;
  150 	} elsif ($val eq 'false') {
  151 		return 0;
  152 	}
  153 
  154 	return $_[0];
  155 }
  156 
  157 # To disable system wide have in $GITWEB_CONFIG
  158 # $feature{'snapshot'}{'default'} = [undef];
  159 # To have project specific config enable override in $GITWEB_CONFIG
  160 # $feature{'blame'}{'override'} = 1;
  161 # and in project config  gitweb.snapshot = none|gzip|bzip2
  162 
  163 sub feature_snapshot {
  164 	my ($ctype, $suffix, $command) = @_;
  165 
  166 	my ($val) = git_get_project_config('snapshot');
  167 
  168 	if ($val eq 'gzip') {
  169 		return ('x-gzip', 'gz', 'gzip');
  170 	} elsif ($val eq 'bzip2') {
  171 		return ('x-bzip2', 'bz2', 'bzip2');
  172 	} elsif ($val eq 'none') {
  173 		return ();
  174 	}
  175 
  176 	return ($ctype, $suffix, $command);
  177 }
  178 
  179 sub gitweb_have_snapshot {
  180 	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
  181 	my $have_snapshot = (defined $ctype && defined $suffix);
  182 
  183 	return $have_snapshot;
  184 }
  185 
  186 # To enable system wide have in $GITWEB_CONFIG
  187 # $feature{'pickaxe'}{'default'} = [1];
  188 # To have project specific config enable override in $GITWEB_CONFIG
  189 # $feature{'pickaxe'}{'override'} = 1;
  190 # and in project config gitweb.pickaxe = 0|1;
  191 
  192 sub feature_pickaxe {
  193 	my ($val) = git_get_project_config('pickaxe', '--bool');
  194 
  195 	if ($val eq 'true') {
  196 		return (1);
  197 	} elsif ($val eq 'false') {
  198 		return (0);
  199 	}
  200 
  201 	return ($_[0]);
  202 }
  203 
  204 # rename detection options for git-diff and git-diff-tree
  205 # - default is '-M', with the cost proportional to
  206 #   (number of removed files) * (number of new files).
  207 # - more costly is '-C' (or '-C', '-M'), with the cost proportional to
  208 #   (number of changed files + number of removed files) * (number of new files)
  209 # - even more costly is '-C', '--find-copies-harder' with cost
  210 #   (number of files in the original tree) * (number of new files)
  211 # - one might want to include '-B' option, e.g. '-B', '-M'
  212 our @diff_opts = ('-M'); # taken from git_commit
  213 
  214 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "gitweb_config.perl";
  215 do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
  216 
  217 # version of the core git binary
  218 our $git_version = qx($GIT --version) =~ m/git version (.*)$/ ? $1 : "unknown";
  219 
  220 $projects_list ||= $projectroot;
  221 
  222 # ======================================================================
  223 # input validation and dispatch
  224 our $action = $cgi->param('a');
  225 if (defined $action) {
  226 	if ($action =~ m/[^0-9a-zA-Z\.\-_]/) {
  227 		die_error(undef, "Invalid action parameter");
  228 	}
  229 }
  230 
  231 # parameters which are pathnames
  232 our $project = $cgi->param('p');
  233 if (defined $project) {
  234 	if (!validate_pathname($project) ||
  235 	    !(-d "$projectroot/$project") ||
  236 	    !(-e "$projectroot/$project/HEAD") ||
  237 	    ($export_ok && !(-e "$projectroot/$project/$export_ok")) ||
  238 	    ($strict_export && !project_in_list($project))) {
  239 		undef $project;
  240 		die_error(undef, "No such project");
  241 	}
  242 }
  243 
  244 our $file_name = $cgi->param('f');
  245 if (defined $file_name) {
  246 	if (!validate_pathname($file_name)) {
  247 		die_error(undef, "Invalid file parameter");
  248 	}
  249 }
  250 
  251 our $file_parent = $cgi->param('fp');
  252 if (defined $file_parent) {
  253 	if (!validate_pathname($file_parent)) {
  254 		die_error(undef, "Invalid file parent parameter");
  255 	}
  256 }
  257 
  258 # parameters which are refnames
  259 our $hash = $cgi->param('h');
  260 if (defined $hash) {
  261 	if (!validate_refname($hash)) {
  262 		die_error(undef, "Invalid hash parameter");
  263 	}
  264 }
  265 our $hash_parent = $cgi->param('hp');
  266 if (defined $hash_parent) {
  267 	if (!validate_refname($hash_parent)) {
  268 		die_error(undef, "Invalid hash parent parameter");
  269 	}
  270 }
  271 
  272 our $hash_base = $cgi->param('hb');
  273 if (defined $hash_base) {
  274 	if (!validate_refname($hash_base)) {
  275 		die_error(undef, "Invalid hash base parameter");
  276 	}
  277 }
  278 
  279 our $hash_parent_base = $cgi->param('hpb');
  280 if (defined $hash_parent_base) {
  281 	if (!validate_refname($hash_parent_base)) {
  282 		die_error(undef, "Invalid hash parent base parameter");
  283 	}
  284 }
  285 
  286 # other parameters
  287 our $page = $cgi->param('pg');
  288 if (defined $page) {
  289 	if ($page =~ m/[^0-9]/) {
  290 		die_error(undef, "Invalid page parameter");
  291 	}
  292 }
  293 
  294 our $searchtext = $cgi->param('s');
  295 if (defined $searchtext) {
  296 	if ($searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
  297 		die_error(undef, "Invalid search parameter");
  298 	}
  299 	$searchtext = quotemeta $searchtext;
  300 }
  301 
  302 # now read PATH_INFO and use it as alternative to parameters
  303 sub evaluate_path_info {
  304 	return if defined $project;
  305 	my $path_info = $ENV{"PATH_INFO"};
  306 	return if !$path_info;
  307 	$path_info =~ s,^/+,,;
  308 	return if !$path_info;
  309 	# find which part of PATH_INFO is project
  310 	$project = $path_info;
  311 	$project =~ s,/+$,,;
  312 	while ($project && !-e "$projectroot/$project/HEAD") {
  313 		$project =~ s,/*[^/]*$,,;
  314 	}
  315 	# validate project
  316 	$project = validate_pathname($project);
  317 	if (!$project ||
  318 	    ($export_ok && !-e "$projectroot/$project/$export_ok") ||
  319 	    ($strict_export && !project_in_list($project))) {
  320 		undef $project;
  321 		return;
  322 	}
  323 	# do not change any parameters if an action is given using the query string
  324 	return if $action;
  325 	$path_info =~ s,^$project/*,,;
  326 	my ($refname, $pathname) = split(/:/, $path_info, 2);
  327 	if (defined $pathname) {
  328 		# we got "project.git/branch:filename" or "project.git/branch:dir/"
  329 		# we could use git_get_type(branch:pathname), but it needs $git_dir
  330 		$pathname =~ s,^/+,,;
  331 		if (!$pathname || substr($pathname, -1) eq "/") {
  332 			$action  ||= "tree";
  333 			$pathname =~ s,/$,,;
  334 		} else {
  335 			$action  ||= "blob_plain";
  336 		}
  337 		$hash_base ||= validate_refname($refname);
  338 		$file_name ||= validate_pathname($pathname);
  339 	} elsif (defined $refname) {
  340 		# we got "project.git/branch"
  341 		$action ||= "shortlog";
  342 		$hash   ||= validate_refname($refname);
  343 	}
  344 }
  345 evaluate_path_info();
  346 
  347 # path to the current git repository
  348 our $git_dir;
  349 $git_dir = "$projectroot/$project" if $project;
  350 
  351 # dispatch
  352 my %actions = (
  353 	"blame" => \&git_blame2,
  354 	"blobdiff" => \&git_blobdiff,
  355 	"blobdiff_plain" => \&git_blobdiff_plain,
  356 	"blob" => \&git_blob,
  357 	"blob_plain" => \&git_blob_plain,
  358 	"commitdiff" => \&git_commitdiff,
  359 	"commitdiff_plain" => \&git_commitdiff_plain,
  360 	"commit" => \&git_commit,
  361 	"heads" => \&git_heads,
  362 	"history" => \&git_history,
  363 	"log" => \&git_log,
  364 	"rss" => \&git_rss,
  365 	"search" => \&git_search,
  366 	"shortlog" => \&git_shortlog,
  367 	"summary" => \&git_summary,
  368 	"tag" => \&git_tag,
  369 	"tags" => \&git_tags,
  370 	"tree" => \&git_tree,
  371 	"snapshot" => \&git_snapshot,
  372 	# those below don't need $project
  373 	"opml" => \&git_opml,
  374 	"project_list" => \&git_project_list,
  375 	"project_index" => \&git_project_index,
  376 );
  377 
  378 if (defined $project) {
  379 	$action ||= 'summary';
  380 } else {
  381 	$action ||= 'project_list';
  382 }
  383 if (!defined($actions{$action})) {
  384 	die_error(undef, "Unknown action");
  385 }
  386 if ($action !~ m/^(opml|project_list|project_index)$/ &&
  387     !$project) {
  388 	die_error(undef, "Project needed");
  389 }
  390 $actions{$action}->();
  391 exit;
  392 
  393 ## ======================================================================
  394 ## action links
  395 
  396 sub href(%) {
  397 	my %params = @_;
  398 
  399 	my @mapping = (
  400 		project => "p",
  401 		action => "a",
  402 		file_name => "f",
  403 		file_parent => "fp",
  404 		hash => "h",
  405 		hash_parent => "hp",
  406 		hash_base => "hb",
  407 		hash_parent_base => "hpb",
  408 		page => "pg",
  409 		order => "o",
  410 		searchtext => "s",
  411 	);
  412 	my %mapping = @mapping;
  413 
  414 	$params{'project'} = $project unless exists $params{'project'};
  415 
  416 	my @result = ();
  417 	for (my $i = 0; $i < @mapping; $i += 2) {
  418 		my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
  419 		if (defined $params{$name}) {
  420 			push @result, $symbol . "=" . esc_param($params{$name});
  421 		}
  422 	}
  423 	return "$my_uri?" . join(';', @result);
  424 }
  425 
  426 
  427 ## ======================================================================
  428 ## validation, quoting/unquoting and escaping
  429 
  430 sub validate_pathname {
  431 	my $input = shift || return undef;
  432 
  433 	# no '.' or '..' as elements of path, i.e. no '.' nor '..'
  434 	# at the beginning, at the end, and between slashes.
  435 	# also this catches doubled slashes
  436 	if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
  437 		return undef;
  438 	}
  439 	# no null characters
  440 	if ($input =~ m!\0!) {
  441 		return undef;
  442 	}
  443 	return $input;
  444 }
  445 
  446 sub validate_refname {
  447 	my $input = shift || return undef;
  448 
  449 	# textual hashes are O.K.
  450 	if ($input =~ m/^[0-9a-fA-F]{40}$/) {
  451 		return $input;
  452 	}
  453 	# it must be correct pathname
  454 	$input = validate_pathname($input)
  455 		or return undef;
  456 	# restrictions on ref name according to git-check-ref-format
  457 	if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
  458 		return undef;
  459 	}
  460 	return $input;
  461 }
  462 
  463 # very thin wrapper for decode("utf8", $str, Encode::FB_DEFAULT);
  464 sub to_utf8 {
  465 	my $str = shift;
  466 	return decode("utf8", $str, Encode::FB_DEFAULT);
  467 }
  468 
  469 # quote unsafe chars, but keep the slash, even when it's not
  470 # correct, but quoted slashes look too horrible in bookmarks
  471 sub esc_param {
  472 	my $str = shift;
  473 	$str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
  474 	$str =~ s/\+/%2B/g;
  475 	$str =~ s/ /\+/g;
  476 	return $str;
  477 }
  478 
  479 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
  480 sub esc_url {
  481 	my $str = shift;
  482 	$str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
  483 	$str =~ s/\+/%2B/g;
  484 	$str =~ s/ /\+/g;
  485 	return $str;
  486 }
  487 
  488 # replace invalid utf8 character with SUBSTITUTION sequence
  489 sub esc_html {
  490 	my $str = shift;
  491 	$str = to_utf8($str);
  492 	$str = escapeHTML($str);
  493 	$str =~ s/\014/^L/g; # escape FORM FEED (FF) character (e.g. in COPYING file)
  494 	$str =~ s/\033/^[/g; # "escape" ESCAPE (\e) character (e.g. commit 20a3847d8a5032ce41f90dcc68abfb36e6fee9b1)
  495 	return $str;
  496 }
  497 
  498 # git may return quoted and escaped filenames
  499 sub unquote {
  500 	my $str = shift;
  501 	if ($str =~ m/^"(.*)"$/) {
  502 		$str = $1;
  503 		$str =~ s/\\([0-7]{1,3})/chr(oct($1))/eg;
  504 	}
  505 	return $str;
  506 }
  507 
  508 # escape tabs (convert tabs to spaces)
  509 sub untabify {
  510 	my $line = shift;
  511 
  512 	while ((my $pos = index($line, "\t")) != -1) {
  513 		if (my $count = (8 - ($pos % 8))) {
  514 			my $spaces = ' ' x $count;
  515 			$line =~ s/\t/$spaces/;
  516 		}
  517 	}
  518 
  519 	return $line;
  520 }
  521 
  522 sub project_in_list {
  523 	my $project = shift;
  524 	my @list = git_get_projects_list();
  525 	return @list && scalar(grep { $_->{'path'} eq $project } @list);
  526 }
  527 
  528 ## ----------------------------------------------------------------------
  529 ## HTML aware string manipulation
  530 
  531 sub chop_str {
  532 	my $str = shift;
  533 	my $len = shift;
  534 	my $add_len = shift || 10;
  535 
  536 	# allow only $len chars, but don't cut a word if it would fit in $add_len
  537 	# if it doesn't fit, cut it if it's still longer than the dots we would add
  538 	$str =~ m/^(.{0,$len}[^ \/\-_:\.@]{0,$add_len})(.*)/;
  539 	my $body = $1;
  540 	my $tail = $2;
  541 	if (length($tail) > 4) {
  542 		$tail = " ...";
  543 		$body =~ s/&[^;]*$//; # remove chopped character entities
  544 	}
  545 	return "$body$tail";
  546 }
  547 
  548 ## ----------------------------------------------------------------------
  549 ## functions returning short strings
  550 
  551 # CSS class for given age value (in seconds)
  552 sub age_class {
  553 	my $age = shift;
  554 
  555 	if ($age < 60*60*2) {
  556 		return "age0";
  557 	} elsif ($age < 60*60*24*2) {
  558 		return "age1";
  559 	} else {
  560 		return "age2";
  561 	}
  562 }
  563 
  564 # convert age in seconds to "nn units ago" string
  565 sub age_string {
  566 	my $age = shift;
  567 	my $age_str;
  568 
  569 	if ($age > 60*60*24*365*2) {
  570 		$age_str = (int $age/60/60/24/365);
  571 		$age_str .= " years ago";
  572 	} elsif ($age > 60*60*24*(365/12)*2) {
  573 		$age_str = int $age/60/60/24/(365/12);
  574 		$age_str .= " months ago";
  575 	} elsif ($age > 60*60*24*7*2) {
  576 		$age_str = int $age/60/60/24/7;
  577 		$age_str .= " weeks ago";
  578 	} elsif ($age > 60*60*24*2) {
  579 		$age_str = int $age/60/60/24;
  580 		$age_str .= " days ago";
  581 	} elsif ($age > 60*60*2) {
  582 		$age_str = int $age/60/60;
  583 		$age_str .= " hours ago";
  584 	} elsif ($age > 60*2) {
  585 		$age_str = int $age/60;
  586 		$age_str .= " min ago";
  587 	} elsif ($age > 2) {
  588 		$age_str = int $age;
  589 		$age_str .= " sec ago";
  590 	} else {
  591 		$age_str .= " right now";
  592 	}
  593 	return $age_str;
  594 }
  595 
  596 # convert file mode in octal to symbolic file mode string
  597 sub mode_str {
  598 	my $mode = oct shift;
  599 
  600 	if (S_ISDIR($mode & S_IFMT)) {
  601 		return 'drwxr-xr-x';
  602 	} elsif (S_ISLNK($mode)) {
  603 		return 'lrwxrwxrwx';
  604 	} elsif (S_ISREG($mode)) {
  605 		# git cares only about the executable bit
  606 		if ($mode & S_IXUSR) {
  607 			return '-rwxr-xr-x';
  608 		} else {
  609 			return '-rw-r--r--';
  610 		};
  611 	} else {
  612 		return '----------';
  613 	}
  614 }
  615 
  616 # convert file mode in octal to file type string
  617 sub file_type {
  618 	my $mode = shift;
  619 
  620 	if ($mode !~ m/^[0-7]+$/) {
  621 		return $mode;
  622 	} else {
  623 		$mode = oct $mode;
  624 	}
  625 
  626 	if (S_ISDIR($mode & S_IFMT)) {
  627 		return "directory";
  628 	} elsif (S_ISLNK($mode)) {
  629 		return "symlink";
  630 	} elsif (S_ISREG($mode)) {
  631 		return "file";
  632 	} else {
  633 		return "unknown";
  634 	}
  635 }
  636 
  637 ## ----------------------------------------------------------------------
  638 ## functions returning short HTML fragments, or transforming HTML fragments
  639 ## which don't beling to other sections
  640 
  641 # format line of commit message or tag comment
  642 sub format_log_line_html {
  643 	my $line = shift;
  644 
  645 	$line = esc_html($line);
  646 	$line =~ s/ /&nbsp;/g;
  647 	if ($line =~ m/([0-9a-fA-F]{40})/) {
  648 		my $hash_text = $1;
  649 		if (git_get_type($hash_text) eq "commit") {
  650 			my $link =
  651 				$cgi->a({-href => href(action=>"commit", hash=>$hash_text),
  652 				        -class => "text"}, $hash_text);
  653 			$line =~ s/$hash_text/$link/;
  654 		}
  655 	}
  656 	return $line;
  657 }
  658 
  659 # format marker of refs pointing to given object
  660 sub format_ref_marker {
  661 	my ($refs, $id) = @_;
  662 	my $markers = '';
  663 
  664 	if (defined $refs->{$id}) {
  665 		foreach my $ref (@{$refs->{$id}}) {
  666 			my ($type, $name) = qw();
  667 			# e.g. tags/v2.6.11 or heads/next
  668 			if ($ref =~ m!^(.*?)s?/(.*)$!) {
  669 				$type = $1;
  670 				$name = $2;
  671 			} else {
  672 				$type = "ref";
  673 				$name = $ref;
  674 			}
  675 
  676 			$markers .= " <span class=\"$type\">" . esc_html($name) . "</span>";
  677 		}
  678 	}
  679 
  680 	if ($markers) {
  681 		return ' <span class="refs">'. $markers . '</span>';
  682 	} else {
  683 		return "";
  684 	}
  685 }
  686 
  687 # format, perhaps shortened and with markers, title line
  688 sub format_subject_html {
  689 	my ($long, $short, $href, $extra) = @_;
  690 	$extra = '' unless defined($extra);
  691 
  692 	if (length($short) < length($long)) {
  693 		return $cgi->a({-href => $href, -class => "list subject",
  694 		                -title => to_utf8($long)},
  695 		       esc_html($short) . $extra);
  696 	} else {
  697 		return $cgi->a({-href => $href, -class => "list subject"},
  698 		       esc_html($long)  . $extra);
  699 	}
  700 }
  701 
  702 sub format_diff_line {
  703 	my $line = shift;
  704 	my $char = substr($line, 0, 1);
  705 	my $diff_class = "";
  706 
  707 	chomp $line;
  708 
  709 	if ($char eq '+') {
  710 		$diff_class = " add";
  711 	} elsif ($char eq "-") {
  712 		$diff_class = " rem";
  713 	} elsif ($char eq "@") {
  714 		$diff_class = " chunk_header";
  715 	} elsif ($char eq "\\") {
  716 		$diff_class = " incomplete";
  717 	}
  718 	$line = untabify($line);
  719 	return "<div class=\"diff$diff_class\">" . esc_html($line) . "</div>\n";
  720 }
  721 
  722 ## ----------------------------------------------------------------------
  723 ## git utility subroutines, invoking git commands
  724 
  725 # returns path to the core git executable and the --git-dir parameter as list
  726 sub git_cmd {
  727 	return $GIT, '--git-dir='.$git_dir;
  728 }
  729 
  730 # returns path to the core git executable and the --git-dir parameter as string
  731 sub git_cmd_str {
  732 	return join(' ', git_cmd());
  733 }
  734 
  735 # get HEAD ref of given project as hash
  736 sub git_get_head_hash {
  737 	my $project = shift;
  738 	my $head = gitweb_get_default_head($project);
  739 	my $o_git_dir = $git_dir;
  740 	my $retval = undef;
  741 	$git_dir = "$projectroot/$project";
  742 	if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", $head) {
  743 		my $head = <$fd>;
  744 		close $fd;
  745 		if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
  746 			$retval = $1;
  747 		}
  748 	}
  749 	if (defined $o_git_dir) {
  750 		$git_dir = $o_git_dir;
  751 	}
  752 	return $retval;
  753 }
  754 
  755 # get type of given object
  756 sub git_get_type {
  757 	my $hash = shift;
  758 
  759 	open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
  760 	my $type = <$fd>;
  761 	close $fd or return;
  762 	chomp $type;
  763 	return $type;
  764 }
  765 
  766 sub git_get_project_config {
  767 	my ($key, $type) = @_;
  768 
  769 	return unless ($key);
  770 	$key =~ s/^gitweb\.//;
  771 	return if ($key =~ m/\W/);
  772 
  773 	my @x = (git_cmd(), 'repo-config');
  774 	if (defined $type) { push @x, $type; }
  775 	push @x, "--get";
  776 	push @x, "gitweb.$key";
  777 	my $val = qx(@x);
  778 	chomp $val;
  779 	return ($val);
  780 }
  781 
  782 # get hash of given path at given ref
  783 sub git_get_hash_by_path {
  784 	my $base = shift;
  785 	my $path = shift || return undef;
  786 	my $type = shift;
  787 
  788 	$path =~ s,/+$,,;
  789 
  790 	open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
  791 		or die_error(undef, "Open git-ls-tree failed");
  792 	my $line = <$fd>;
  793 	close $fd or return undef;
  794 
  795 	#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
  796 	$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
  797 	if (defined $type && $type ne $2) {
  798 		# type doesn't match
  799 		return undef;
  800 	}
  801 	return $3;
  802 }
  803 
  804 ## ......................................................................
  805 ## git utility functions, directly accessing git repository
  806 
  807 sub git_get_project_description {
  808 	my $path = shift;
  809 
  810 	open my $fd, "$projectroot/$path/description" or return undef;
  811 	my $descr = <$fd>;
  812 	close $fd;
  813 	chomp $descr;
  814 	return $descr;
  815 }
  816 
  817 sub git_get_project_url_list {
  818 	my $path = shift;
  819 
  820 	open my $fd, "$projectroot/$path/cloneurl" or return;
  821 	my @git_project_url_list = map { chomp; $_ } <$fd>;
  822 	close $fd;
  823 
  824 	return wantarray ? @git_project_url_list : \@git_project_url_list;
  825 }
  826 
  827 sub git_get_projects_list {
  828 	my @list;
  829 
  830 	if (-d $projects_list) {
  831 		# search in directory
  832 		my $dir = $projects_list;
  833 		my $pfxlen = length("$dir");
  834 
  835 		File::Find::find({
  836 			follow_fast => 1, # follow symbolic links
  837 			dangling_symlinks => 0, # ignore dangling symlinks, silently
  838 			wanted => sub {
  839 				# skip project-list toplevel, if we get it.
  840 				return if (m!^[/.]$!);
  841 				# only directories can be git repositories
  842 				return unless (-d $_);
  843 
  844 				my $subdir = substr($File::Find::name, $pfxlen + 1);
  845 				# we check related file in $projectroot
  846 				if (-e "$projectroot/$subdir/HEAD" && (!$export_ok ||
  847 				    -e "$projectroot/$subdir/$export_ok")) {
  848 					push @list, { path => $subdir };
  849 					$File::Find::prune = 1;
  850 				}
  851 			},
  852 		}, "$dir");
  853 
  854 	} elsif (-f $projects_list) {
  855 		# read from file(url-encoded):
  856 		# 'git%2Fgit.git Linus+Torvalds'
  857 		# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
  858 		# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
  859 		open my ($fd), $projects_list or return;
  860 		while (my $line = <$fd>) {
  861 			chomp $line;
  862 			my ($path, $owner) = split ' ', $line;
  863 			$path = unescape($path);
  864 			$owner = unescape($owner);
  865 			if (!defined $path) {
  866 				next;
  867 			}
  868 			if (-e "$projectroot/$path/HEAD" && (!$export_ok ||
  869 			    -e "$projectroot/$path/$export_ok")) {
  870 				my $pr = {
  871 					path => $path,
  872 					owner => to_utf8($owner),
  873 				};
  874 				push @list, $pr
  875 			}
  876 		}
  877 		close $fd;
  878 	}
  879 	@list = sort {$a->{'path'} cmp $b->{'path'}} @list;
  880 	return @list;
  881 }
  882 
  883 sub git_get_project_owner {
  884 	my $project = shift;
  885 	my $owner;
  886 
  887 	return undef unless $project;
  888 
  889 	# read from file (url-encoded):
  890 	# 'git%2Fgit.git Linus+Torvalds'
  891 	# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
  892 	# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
  893 	if (-f $projects_list) {
  894 		open (my $fd , $projects_list);
  895 		while (my $line = <$fd>) {
  896 			chomp $line;
  897 			my ($pr, $ow) = split ' ', $line;
  898 			$pr = unescape($pr);
  899 			$ow = unescape($ow);
  900 			if ($pr eq $project) {
  901 				$owner = to_utf8($ow);
  902 				last;
  903 			}
  904 		}
  905 		close $fd;
  906 	}
  907 	if (!defined $owner) {
  908 		$owner = get_file_owner("$projectroot/$project");
  909 	}
  910 
  911 	return $owner;
  912 }
  913 
  914 sub git_get_references {
  915 	my $type = shift || "";
  916 	my %refs;
  917 	# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c	refs/tags/v2.6.11
  918 	# c39ae07f393806ccf406ef966e9a15afc43cc36a	refs/tags/v2.6.11^{}
  919 	open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
  920 		or return;
  921 
  922 	while (my $line = <$fd>) {
  923 		chomp $line;
  924 		if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?[^\^]+)/) {
  925 			if (defined $refs{$1}) {
  926 				push @{$refs{$1}}, $2;
  927 			} else {
  928 				$refs{$1} = [ $2 ];
  929 			}
  930 		}
  931 	}
  932 	close $fd or return;
  933 	return \%refs;
  934 }
  935 
  936 sub git_get_rev_name_tags {
  937 	my $hash = shift || return undef;
  938 
  939 	open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
  940 		or return;
  941 	my $name_rev = <$fd>;
  942 	close $fd;
  943 
  944 	if ($name_rev =~ m|^$hash tags/(.*)$|) {
  945 		return $1;
  946 	} else {
  947 		# catches also '$hash undefined' output
  948 		return undef;
  949 	}
  950 }
  951 
  952 ## ----------------------------------------------------------------------
  953 ## parse to hash functions
  954 
  955 sub parse_date {
  956 	my $epoch = shift;
  957 	my $tz = shift || "-0000";
  958 
  959 	my %date;
  960 	my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
  961 	my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
  962 	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
  963 	$date{'hour'} = $hour;
  964 	$date{'minute'} = $min;
  965 	$date{'mday'} = $mday;
  966 	$date{'day'} = $days[$wday];
  967 	$date{'month'} = $months[$mon];
  968 	$date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
  969 	                   $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
  970 	$date{'mday-time'} = sprintf "%d %s %02d:%02d",
  971 	                     $mday, $months[$mon], $hour ,$min;
  972 
  973 	$tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
  974 	my $local = $epoch + ((int $1 + ($2/60)) * 3600);
  975 	($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
  976 	$date{'hour_local'} = $hour;
  977 	$date{'minute_local'} = $min;
  978 	$date{'tz_local'} = $tz;
  979 	return %date;
  980 }
  981 
  982 sub parse_tag {
  983 	my $tag_id = shift;
  984 	my %tag;
  985 	my @comment;
  986 
  987 	open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
  988 	$tag{'id'} = $tag_id;
  989 	while (my $line = <$fd>) {
  990 		chomp $line;
  991 		if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
  992 			$tag{'object'} = $1;
  993 		} elsif ($line =~ m/^type (.+)$/) {
  994 			$tag{'type'} = $1;
  995 		} elsif ($line =~ m/^tag (.+)$/) {
  996 			$tag{'name'} = $1;
  997 		} elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
  998 			$tag{'author'} = $1;
  999 			$tag{'epoch'} = $2;
 1000 			$tag{'tz'} = $3;
 1001 		} elsif ($line =~ m/--BEGIN/) {
 1002 			push @comment, $line;
 1003 			last;
 1004 		} elsif ($line eq "") {
 1005 			last;
 1006 		}
 1007 	}
 1008 	push @comment, <$fd>;
 1009 	$tag{'comment'} = \@comment;
 1010 	close $fd or return;
 1011 	if (!defined $tag{'name'}) {
 1012 		return
 1013 	};
 1014 	return %tag
 1015 }
 1016 
 1017 sub parse_commit {
 1018 	my $commit_id = shift;
 1019 	my $commit_text = shift;
 1020 
 1021 	my @commit_lines;
 1022 	my %co;
 1023 
 1024 	if (defined $commit_text) {
 1025 		@commit_lines = @$commit_text;
 1026 	} else {
 1027 		local $/ = "\0";
 1028 		open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", "--max-count=1", $commit_id
 1029 			or return;
 1030 		@commit_lines = split '\n', <$fd>;
 1031 		close $fd or return;
 1032 		pop @commit_lines;
 1033 	}
 1034 	my $header = shift @commit_lines;
 1035 	if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
 1036 		return;
 1037 	}
 1038 	($co{'id'}, my @parents) = split ' ', $header;
 1039 	$co{'parents'} = \@parents;
 1040 	$co{'parent'} = $parents[0];
 1041 	while (my $line = shift @commit_lines) {
 1042 		last if $line eq "\n";
 1043 		if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
 1044 			$co{'tree'} = $1;
 1045 		} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
 1046 			$co{'author'} = $1;
 1047 			$co{'author_epoch'} = $2;
 1048 			$co{'author_tz'} = $3;
 1049 			if ($co{'author'} =~ m/^([^<]+) </) {
 1050 				$co{'author_name'} = $1;
 1051 			} else {
 1052 				$co{'author_name'} = $co{'author'};
 1053 			}
 1054 		} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
 1055 			$co{'committer'} = $1;
 1056 			$co{'committer_epoch'} = $2;
 1057 			$co{'committer_tz'} = $3;
 1058 			$co{'committer_name'} = $co{'committer'};
 1059 			$co{'committer_name'} =~ s/ <.*//;
 1060 		}
 1061 	}
 1062 	if (!defined $co{'tree'}) {
 1063 		return;
 1064 	};
 1065 
 1066 	foreach my $title (@commit_lines) {
 1067 		$title =~ s/^    //;
 1068 		if ($title ne "") {
 1069 			$co{'title'} = chop_str($title, 80, 5);
 1070 			# remove leading stuff of merges to make the interesting part visible
 1071 			if (length($title) > 50) {
 1072 				$title =~ s/^Automatic //;
 1073 				$title =~ s/^merge (of|with) /Merge ... /i;
 1074 				if (length($title) > 50) {
 1075 					$title =~ s/(http|rsync):\/\///;
 1076 				}
 1077 				if (length($title) > 50) {
 1078 					$title =~ s/(master|www|rsync)\.//;
 1079 				}
 1080 				if (length($title) > 50) {
 1081 					$title =~ s/kernel.org:?//;
 1082 				}
 1083 				if (length($title) > 50) {
 1084 					$title =~ s/\/pub\/scm//;
 1085 				}
 1086 			}
 1087 			$co{'title_short'} = chop_str($title, 50, 5);
 1088 			last;
 1089 		}
 1090 	}
 1091 	if ($co{'title'} eq "") {
 1092 		$co{'title'} = $co{'title_short'} = '(no commit message)';
 1093 	}
 1094 	# remove added spaces
 1095 	foreach my $line (@commit_lines) {
 1096 		$line =~ s/^    //;
 1097 	}
 1098 	$co{'comment'} = \@commit_lines;
 1099 
 1100 	my $age = time - $co{'committer_epoch'};
 1101 	$co{'age'} = $age;
 1102 	$co{'age_string'} = age_string($age);
 1103 	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
 1104 	if ($age > 60*60*24*7*2) {
 1105 		$co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
 1106 		$co{'age_string_age'} = $co{'age_string'};
 1107 	} else {
 1108 		$co{'age_string_date'} = $co{'age_string'};
 1109 		$co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
 1110 	}
 1111 	return %co;
 1112 }
 1113 
 1114 # parse ref from ref_file, given by ref_id, with given type
 1115 sub parse_ref {
 1116 	my $ref_file = shift;
 1117 	my $ref_id = shift;
 1118 	my $type = shift || git_get_type($ref_id);
 1119 	my %ref_item;
 1120 
 1121 	$ref_item{'type'} = $type;
 1122 	$ref_item{'id'} = $ref_id;
 1123 	$ref_item{'epoch'} = 0;
 1124 	$ref_item{'age'} = "unknown";
 1125 	if ($type eq "tag") {
 1126 		my %tag = parse_tag($ref_id);
 1127 		$ref_item{'comment'} = $tag{'comment'};
 1128 		if ($tag{'type'} eq "commit") {
 1129 			my %co = parse_commit($tag{'object'});
 1130 			$ref_item{'epoch'} = $co{'committer_epoch'};
 1131 			$ref_item{'age'} = $co{'age_string'};
 1132 		} elsif (defined($tag{'epoch'})) {
 1133 			my $age = time - $tag{'epoch'};
 1134 			$ref_item{'epoch'} = $tag{'epoch'};
 1135 			$ref_item{'age'} = age_string($age);
 1136 		}
 1137 		$ref_item{'reftype'} = $tag{'type'};
 1138 		$ref_item{'name'} = $tag{'name'};
 1139 		$ref_item{'refid'} = $tag{'object'};
 1140 	} elsif ($type eq "commit"){
 1141 		my %co = parse_commit($ref_id);
 1142 		$ref_item{'reftype'} = "commit";
 1143 		$ref_item{'name'} = $ref_file;
 1144 		$ref_item{'title'} = $co{'title'};
 1145 		$ref_item{'refid'} = $ref_id;
 1146 		$ref_item{'epoch'} = $co{'committer_epoch'};
 1147 		$ref_item{'age'} = $co{'age_string'};
 1148 	} else {
 1149 		$ref_item{'reftype'} = $type;
 1150 		$ref_item{'name'} = $ref_file;
 1151 		$ref_item{'refid'} = $ref_id;
 1152 	}
 1153 
 1154 	return %ref_item;
 1155 }
 1156 
 1157 # parse line of git-diff-tree "raw" output
 1158 sub parse_difftree_raw_line {
 1159 	my $line = shift;
 1160 	my %res;
 1161 
 1162 	# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M	ls-files.c'
 1163 	# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M	rev-tree.c'
 1164 	if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
 1165 		$res{'from_mode'} = $1;
 1166 		$res{'to_mode'} = $2;
 1167 		$res{'from_id'} = $3;
 1168 		$res{'to_id'} = $4;
 1169 		$res{'status'} = $5;
 1170 		$res{'similarity'} = $6;
 1171 		if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
 1172 			($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
 1173 		} else {
 1174 			$res{'file'} = unquote($7);
 1175 		}
 1176 	}
 1177 	# 'c512b523472485aef4fff9e57b229d9d243c967f'
 1178 	elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
 1179 		$res{'commit'} = $1;
 1180 	}
 1181 
 1182 	return wantarray ? %res : \%res;
 1183 }
 1184 
 1185 # parse line of git-ls-tree output
 1186 sub parse_ls_tree_line ($;%) {
 1187 	my $line = shift;
 1188 	my %opts = @_;
 1189 	my %res;
 1190 
 1191 	#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
 1192 	$line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/;
 1193 
 1194 	$res{'mode'} = $1;
 1195 	$res{'type'} = $2;
 1196 	$res{'hash'} = $3;
 1197 	if ($opts{'-z'}) {
 1198 		$res{'name'} = $4;
 1199 	} else {
 1200 		$res{'name'} = unquote($4);
 1201 	}
 1202 
 1203 	return wantarray ? %res : \%res;
 1204 }
 1205 
 1206 ## ......................................................................
 1207 ## parse to array of hashes functions
 1208 
 1209 sub git_get_refs_list {
 1210 	my $type = shift || "";
 1211 	my %refs;
 1212 	my @reflist;
 1213 
 1214 	my @refs;
 1215 	open my $fd, "-|", $GIT, "peek-remote", "$projectroot/$project/"
 1216 		or return;
 1217 	while (my $line = <$fd>) {
 1218 		chomp $line;
 1219 		if ($line =~ m/^([0-9a-fA-F]{40})\trefs\/($type\/?([^\^]+))(\^\{\})?$/) {
 1220 			if (defined $refs{$1}) {
 1221 				push @{$refs{$1}}, $2;
 1222 			} else {
 1223 				$refs{$1} = [ $2 ];
 1224 			}
 1225 
 1226 			if (! $4) { # unpeeled, direct reference
 1227 				push @refs, { hash => $1, name => $3 }; # without type
 1228 			} elsif ($3 eq $refs[-1]{'name'}) {
 1229 				# most likely a tag is followed by its peeled
 1230 				# (deref) one, and when that happens we know the
 1231 				# previous one was of type 'tag'.
 1232 				$refs[-1]{'type'} = "tag";
 1233 			}
 1234 		}
 1235 	}
 1236 	close $fd;
 1237 
 1238 	foreach my $ref (@refs) {
 1239 		my $ref_file = $ref->{'name'};
 1240 		my $ref_id   = $ref->{'hash'};
 1241 
 1242 		my $type = $ref->{'type'} || git_get_type($ref_id) || next;
 1243 		my %ref_item = parse_ref($ref_file, $ref_id, $type);
 1244 
 1245 		push @reflist, \%ref_item;
 1246 	}
 1247 	# sort refs by age
 1248 	@reflist = sort {$b->{'epoch'} <=> $a->{'epoch'}} @reflist;
 1249 	return (\@reflist, \%refs);
 1250 }
 1251 
 1252 ## ----------------------------------------------------------------------
 1253 ## filesystem-related functions
 1254 
 1255 sub get_file_owner {
 1256 	my $path = shift;
 1257 
 1258 	my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
 1259 	my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
 1260 	if (!defined $gcos) {
 1261 		return undef;
 1262 	}
 1263 	my $owner = $gcos;
 1264 	$owner =~ s/[,;].*$//;
 1265 	return to_utf8($owner);
 1266 }
 1267 
 1268 ## ......................................................................
 1269 ## mimetype related functions
 1270 
 1271 sub mimetype_guess_file {
 1272 	my $filename = shift;
 1273 	my $mimemap = shift;
 1274 	-r $mimemap or return undef;
 1275 
 1276 	my %mimemap;
 1277 	open(MIME, $mimemap) or return undef;
 1278 	while (<MIME>) {
 1279 		next if m/^#/; # skip comments
 1280 		my ($mime, $exts) = split(/\t+/);
 1281 		if (defined $exts) {
 1282 			my @exts = split(/\s+/, $exts);
 1283 			foreach my $ext (@exts) {
 1284 				$mimemap{$ext} = $mime;
 1285 			}
 1286 		}
 1287 	}
 1288 	close(MIME);
 1289 
 1290 	$filename =~ /\.([^.]*)$/;
 1291 	return $mimemap{$1};
 1292 }
 1293 
 1294 sub mimetype_guess {
 1295 	my $filename = shift;
 1296 	my $mime;
 1297 	$filename =~ /\./ or return undef;
 1298 
 1299 	if ($mimetypes_file) {
 1300 		my $file = $mimetypes_file;
 1301 		if ($file !~ m!^/!) { # if it is relative path
 1302 			# it is relative to project
 1303 			$file = "$projectroot/$project/$file";
 1304 		}
 1305 		$mime = mimetype_guess_file($filename, $file);
 1306 	}
 1307 	$mime ||= mimetype_guess_file($filename, '/etc/mime.types');
 1308 	return $mime;
 1309 }
 1310 
 1311 sub blob_mimetype {
 1312 	my $fd = shift;
 1313 	my $filename = shift;
 1314 
 1315 	if ($filename) {
 1316 		my $mime = mimetype_guess($filename);
 1317 		$mime and return $mime;
 1318 	}
 1319 
 1320 	# just in case
 1321 	return $default_blob_plain_mimetype unless $fd;
 1322 
 1323 	if (-T $fd) {
 1324 		return 'text/plain' .
 1325 		       ($default_text_plain_charset ? '; charset='.$default_text_plain_charset : '');
 1326 	} elsif (! $filename) {
 1327 		return 'application/octet-stream';
 1328 	} elsif ($filename =~ m/\.png$/i) {
 1329 		return 'image/png';
 1330 	} elsif ($filename =~ m/\.gif$/i) {
 1331 		return 'image/gif';
 1332 	} elsif ($filename =~ m/\.jpe?g$/i) {
 1333 		return 'image/jpeg';
 1334 	} else {
 1335 		return 'application/octet-stream';
 1336 	}
 1337 }
 1338 
 1339 ## ======================================================================
 1340 ## functions printing HTML: header, footer, error page
 1341 
 1342 sub git_header_html {
 1343 	my $status = shift || "200 OK";
 1344 	my $expires = shift;
 1345 
 1346 	my $title = "$site_name git";
 1347 	if (defined $project) {
 1348 		$title .= " - $project";
 1349 		if (defined $action) {
 1350 			$title .= "/$action";
 1351 			if (defined $file_name) {
 1352 				$title .= " - " . esc_html($file_name);
 1353 				if ($action eq "tree" && $file_name !~ m|/$|) {
 1354 					$title .= "/";
 1355 				}
 1356 			}
 1357 		}
 1358 	}
 1359 	my $content_type;
 1360 	# require explicit support from the UA if we are to send the page as
 1361 	# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
 1362 	# we have to do this because MSIE sometimes globs '*/*', pretending to
 1363 	# support xhtml+xml but choking when it gets what it asked for.
 1364 	if (defined $cgi->http('HTTP_ACCEPT') &&
 1365 	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
 1366 	    $cgi->Accept('application/xhtml+xml') != 0) {
 1367 		$content_type = 'application/xhtml+xml';
 1368 	} else {
 1369 		$content_type = 'text/html';
 1370 	}
 1371 	$content_type = 'text/html';
 1372 	print $cgi->header(-type=>$content_type, -charset => 'utf-8',
 1373 	                   -status=> $status, -expires => $expires);
 1374 	print <<EOF;
 1375 <?xml version="1.0" encoding="utf-8"?>
 1376 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 1377 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
 1378 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
 1379 <!-- git core binaries version $git_version -->
 1380 <head>
 1381 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
 1382 <meta name="generator" content="gitweb/$version git/$git_version"/>
 1383 <meta name="robots" content="index, nofollow"/>
 1384 <title>$title</title>
 1385 <link rel="stylesheet" type="text/css" href="$stylesheet"/>
 1386 EOF
 1387 	if (defined $project) {
 1388 		printf('<link rel="alternate" title="%s log" '.
 1389 		       'href="%s" type="application/rss+xml"/>'."\n",
 1390 		       esc_param($project), href(action=>"rss"));
 1391 	} else {
 1392 		printf('<link rel="alternate" title="%s projects list" '.
 1393 		       'href="%s" type="text/plain; charset=utf-8"/>'."\n",
 1394 		       $site_name, href(project=>undef, action=>"project_index"));
 1395 		printf('<link rel="alternate" title="%s projects logs" '.
 1396 		       'href="%s" type="text/x-opml"/>'."\n",
 1397 		       $site_name, href(project=>undef, action=>"opml"));
 1398 	}
 1399 	if (defined $favicon) {
 1400 		print qq(<link rel="shortcut icon" href="$favicon" type="image/png"/>\n);
 1401 	}
 1402 
 1403 	print "</head>\n" .
 1404 	      "<body>\n" .
 1405 		  '<div class="cruxheader">
 1406 			<a href="/" title="">Home</a> :: 
 1407 			 <a href="/Main/Documentation">Documentation</a> :: 
 1408 			  <a href="/Main/Download">Download</a> :: 
 1409 			  <a href="/Main/Development">Development</a> :: 
 1410 			  <a href="/Main/Community">Community</a> :: 
 1411 			  <a href="/Public/HomePage">Public</a> :: 
 1412 			  <a href="/portdb">Ports</a> :: 
 1413 			  <a href="/Main/Bugs" title="">Bugs</a> :: 
 1414 			  <a href="/Main/Links" title="">Links</a> :: 
 1415 			  <a href="/Main/About" title="">About</a>
 1416 			</div>' .
 1417 	      "<div class=\"page_header\">\n" .
 1418 	      $cgi->a({-href => esc_url($logo_url),
 1419 	               -title => $logo_label},
 1420 	              qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
 1421 	print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
 1422 	if (defined $project) {
 1423 		print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
 1424 		if (defined $action) {
 1425 			print " / $action";
 1426 		}
 1427 		print "\n";
 1428 		if (!defined $searchtext) {
 1429 			$searchtext = "";
 1430 		}
 1431 		my $search_hash;
 1432 		if (defined $hash_base) {
 1433 			$search_hash = $hash_base;
 1434 		} elsif (defined $hash) {
 1435 			$search_hash = $hash;
 1436 		} else {
 1437 			$search_hash = "HEAD";
 1438 		}
 1439 		$cgi->param("a", "search");
 1440 		$cgi->param("h", $search_hash);
 1441 		print $cgi->startform(-method => "get", -action => $my_uri) .
 1442 		      "<div class=\"search\">\n" .
 1443 		      $cgi->hidden(-name => "p") . "\n" .
 1444 		      $cgi->hidden(-name => "a") . "\n" .
 1445 		      $cgi->hidden(-name => "h") . "\n" .
 1446 		      $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
 1447 		      "</div>" .
 1448 		      $cgi->end_form() . "\n";
 1449 	}
 1450 	print "</div><div class=\"content\">\n";
 1451 }
 1452 
 1453 sub git_footer_html {
 1454 	print "</div><div class=\"page_footer\">\n";
 1455 	if (defined $project) {
 1456 		my $descr = git_get_project_description($project);
 1457 		if (defined $descr) {
 1458 			print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
 1459 		}
 1460 		print $cgi->a({-href => href(action=>"rss"),
 1461 		              -class => "rss_logo"}, "RSS") . "\n";
 1462 	} else {
 1463 		print $cgi->a({-href => href(project=>undef, action=>"opml"),
 1464 		              -class => "rss_logo"}, "OPML") . " ";
 1465 		print $cgi->a({-href => href(project=>undef, action=>"project_index"),
 1466 		              -class => "rss_logo"}, "TXT") . "\n";
 1467 	}
 1468 	print "</div>\n" .
 1469 	      "</body>\n" .
 1470 	      "</html>";
 1471 }
 1472 
 1473 sub die_error {
 1474 	my $status = shift || "403 Forbidden";
 1475 	my $error = shift || "Malformed query, file missing or permission denied";
 1476 
 1477 	git_header_html($status);
 1478 	print <<EOF;
 1479 <div class="page_body">
 1480 <br /><br />
 1481 $status - $error
 1482 <br />
 1483 </div>
 1484 EOF
 1485 	git_footer_html();
 1486 	exit;
 1487 }
 1488 
 1489 ## ----------------------------------------------------------------------
 1490 ## functions printing or outputting HTML: navigation
 1491 
 1492 sub git_print_page_nav {
 1493 	my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
 1494 	$extra = '' if !defined $extra; # pager or formats
 1495 
 1496 	my @navs = qw(summary shortlog log commit commitdiff tree);
 1497 	if ($suppress) {
 1498 		@navs = grep { $_ ne $suppress } @navs;
 1499 	}
 1500 
 1501 	my %arg = map { $_ => {action=>$_} } @navs;
 1502 	if (defined $head) {
 1503 		for (qw(commit commitdiff)) {
 1504 			$arg{$_}{hash} = $head;
 1505 		}
 1506 		if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
 1507 			for (qw(shortlog log)) {
 1508 				$arg{$_}{hash} = $head;
 1509 			}
 1510 		}
 1511 	}
 1512 	$arg{tree}{hash} = $treehead if defined $treehead;
 1513 	$arg{tree}{hash_base} = $treebase if defined $treebase;
 1514 
 1515 	print "<div class=\"page_nav\">\n" .
 1516 		(join " | ",
 1517 		 map { $_ eq $current ?
 1518 		       $_ : $cgi->a({-href => href(%{$arg{$_}})}, "$_")
 1519 		 } @navs);
 1520 	print "<br/>\n$extra<br/>\n" .
 1521 	      "</div>\n";
 1522 }
 1523 
 1524 sub format_paging_nav {
 1525 	my ($action, $hash, $head, $page, $nrevs) = @_;
 1526 	my $paging_nav;
 1527 
 1528 
 1529 	if ($hash ne $head || $page) {
 1530 		$paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
 1531 	} else {
 1532 		$paging_nav .= "HEAD";
 1533 	}
 1534 
 1535 	if ($page > 0) {
 1536 		$paging_nav .= " &sdot; " .
 1537 			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page-1),
 1538 			         -accesskey => "p", -title => "Alt-p"}, "prev");
 1539 	} else {
 1540 		$paging_nav .= " &sdot; prev";
 1541 	}
 1542 
 1543 	if ($nrevs >= (100 * ($page+1)-1)) {
 1544 		$paging_nav .= " &sdot; " .
 1545 			$cgi->a({-href => href(action=>$action, hash=>$hash, page=>$page+1),
 1546 			         -accesskey => "n", -title => "Alt-n"}, "next");
 1547 	} else {
 1548 		$paging_nav .= " &sdot; next";
 1549 	}
 1550 
 1551 	return $paging_nav;
 1552 }
 1553 
 1554 ## ......................................................................
 1555 ## functions printing or outputting HTML: div
 1556 
 1557 sub git_print_header_div {
 1558 	my ($action, $title, $hash, $hash_base) = @_;
 1559 	my %args = ();
 1560 
 1561 	$args{action} = $action;
 1562 	$args{hash} = $hash if $hash;
 1563 	$args{hash_base} = $hash_base if $hash_base;
 1564 
 1565 	print "<div class=\"header\">\n" .
 1566 	      $cgi->a({-href => href(%args), -class => "title"},
 1567 	      $title ? $title : $action) .
 1568 	      "\n</div>\n";
 1569 }
 1570 
 1571 #sub git_print_authorship (\%) {
 1572 sub git_print_authorship {
 1573 	my $co = shift;
 1574 
 1575 	my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
 1576 	print "<div class=\"author_date\">" .
 1577 	      esc_html($co->{'author_name'}) .
 1578 	      " [$ad{'rfc2822'}";
 1579 	if ($ad{'hour_local'} < 6) {
 1580 		printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
 1581 		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 1582 	} else {
 1583 		printf(" (%02d:%02d %s)",
 1584 		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 1585 	}
 1586 	print "]</div>\n";
 1587 }
 1588 
 1589 sub git_print_page_path {
 1590 	my $name = shift;
 1591 	my $type = shift;
 1592 	my $hb = shift;
 1593 
 1594 	if (!defined $name) {
 1595 		print "<div class=\"page_path\">/</div>\n";
 1596 	} else {
 1597 		my @dirname = split '/', $name;
 1598 		my $basename = pop @dirname;
 1599 		my $fullname = '';
 1600 
 1601 		print "<div class=\"page_path\">";
 1602 		print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
 1603 			      -title => 'tree root'}, "[$project]");
 1604 		print " / ";
 1605 		foreach my $dir (@dirname) {
 1606 			$fullname .= ($fullname ? '/' : '') . $dir;
 1607 			print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
 1608 			                             hash_base=>$hb),
 1609 			              -title => $fullname}, esc_html($dir));
 1610 			print " / ";
 1611 		}
 1612 		if (defined $type && $type eq 'blob') {
 1613 			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
 1614 			                             hash_base=>$hb),
 1615 			              -title => $name}, esc_html($basename));
 1616 		} elsif (defined $type && $type eq 'tree') {
 1617 			print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
 1618 			                             hash_base=>$hb),
 1619 			              -title => $name}, esc_html($basename));
 1620 		} else {
 1621 			print esc_html($basename);
 1622 		}
 1623 		print "<br/></div>\n";
 1624 	}
 1625 }
 1626 
 1627 # sub git_print_log (\@;%) {
 1628 sub git_print_log ($;%) {
 1629 	my $log = shift;
 1630 	my %opts = @_;
 1631 
 1632 	if ($opts{'-remove_title'}) {
 1633 		# remove title, i.e. first line of log
 1634 		shift @$log;
 1635 	}
 1636 	# remove leading empty lines
 1637 	while (defined $log->[0] && $log->[0] eq "") {
 1638 		shift @$log;
 1639 	}
 1640 
 1641 	# print log
 1642 	my $signoff = 0;
 1643 	my $empty = 0;
 1644 	foreach my $line (@$log) {
 1645 		if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
 1646 			$signoff = 1;
 1647 			$empty = 0;
 1648 			if (! $opts{'-remove_signoff'}) {
 1649 				print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
 1650 				next;
 1651 			} else {
 1652 				# remove signoff lines
 1653 				next;
 1654 			}
 1655 		} else {
 1656 			$signoff = 0;
 1657 		}
 1658 
 1659 		# print only one empty line
 1660 		# do not print empty line after signoff
 1661 		if ($line eq "") {
 1662 			next if ($empty || $signoff);
 1663 			$empty = 1;
 1664 		} else {
 1665 			$empty = 0;
 1666 		}
 1667 
 1668 		print format_log_line_html($line) . "<br/>\n";
 1669 	}
 1670 
 1671 	if ($opts{'-final_empty_line'}) {
 1672 		# end with single empty line
 1673 		print "<br/>\n" unless $empty;
 1674 	}
 1675 }
 1676 
 1677 sub git_print_simplified_log {
 1678 	my $log = shift;
 1679 	my $remove_title = shift;
 1680 
 1681 	git_print_log($log,
 1682 		-final_empty_line=> 1,
 1683 		-remove_title => $remove_title);
 1684 }
 1685 
 1686 # print tree entry (row of git_tree), but without encompassing <tr> element
 1687 sub git_print_tree_entry {
 1688 	my ($t, $basedir, $hash_base, $have_blame) = @_;
 1689 
 1690 	my %base_key = ();
 1691 	$base_key{hash_base} = $hash_base if defined $hash_base;
 1692 
 1693 	# The format of a table row is: mode list link.  Where mode is
 1694 	# the mode of the entry, list is the name of the entry, an href,
 1695 	# and link is the action links of the entry.
 1696 
 1697 	print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
 1698 	if ($t->{'type'} eq "blob") {
 1699 		print "<td class=\"list\">" .
 1700 			$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
 1701 			                       file_name=>"$basedir$t->{'name'}", %base_key),
 1702 			        -class => "list"}, esc_html($t->{'name'})) . "</td>\n";
 1703 		print "<td class=\"link\">";
 1704 		if ($have_blame) {
 1705 			print $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
 1706 				                           file_name=>"$basedir$t->{'name'}", %base_key)},
 1707 				            "blame");
 1708 		}
 1709 		if (defined $hash_base) {
 1710 			if ($have_blame) {
 1711 				print " | ";
 1712 			}
 1713 			print $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
 1714 			                             hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
 1715 			              "history");
 1716 		}
 1717 		print " | " .
 1718 			$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
 1719 			                       file_name=>"$basedir$t->{'name'}")},
 1720 			        "raw");
 1721 		print "</td>\n";
 1722 
 1723 	} elsif ($t->{'type'} eq "tree") {
 1724 		print "<td class=\"list\">";
 1725 		print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
 1726 		                             file_name=>"$basedir$t->{'name'}", %base_key)},
 1727 		              esc_html($t->{'name'}));
 1728 		print "</td>\n";
 1729 		print "<td class=\"link\">";
 1730 		if (defined $hash_base) {
 1731 			print $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
 1732 			                             file_name=>"$basedir$t->{'name'}")},
 1733 			              "history");
 1734 		}
 1735 		print "</td>\n";
 1736 	}
 1737 }
 1738 
 1739 ## ......................................................................
 1740 ## functions printing large fragments of HTML
 1741 
 1742 sub git_difftree_body {
 1743 	my ($difftree, $hash, $parent) = @_;
 1744 
 1745 	print "<div class=\"list_head\">\n";
 1746 	if ($#{$difftree} > 10) {
 1747 		print(($#{$difftree} + 1) . " files changed:\n");
 1748 	}
 1749 	print "</div>\n";
 1750 
 1751 	print "<table class=\"diff_tree\">\n";
 1752 	my $alternate = 1;
 1753 	my $patchno = 0;
 1754 	foreach my $line (@{$difftree}) {
 1755 		my %diff = parse_difftree_raw_line($line);
 1756 
 1757 		if ($alternate) {
 1758 			print "<tr class=\"dark\">\n";
 1759 		} else {
 1760 			print "<tr class=\"light\">\n";
 1761 		}
 1762 		$alternate ^= 1;
 1763 
 1764 		my ($to_mode_oct, $to_mode_str, $to_file_type);
 1765 		my ($from_mode_oct, $from_mode_str, $from_file_type);
 1766 		if ($diff{'to_mode'} ne ('0' x 6)) {
 1767 			$to_mode_oct = oct $diff{'to_mode'};
 1768 			if (S_ISREG($to_mode_oct)) { # only for regular file
 1769 				$to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
 1770 			}
 1771 			$to_file_type = file_type($diff{'to_mode'});
 1772 		}
 1773 		if ($diff{'from_mode'} ne ('0' x 6)) {
 1774 			$from_mode_oct = oct $diff{'from_mode'};
 1775 			if (S_ISREG($to_mode_oct)) { # only for regular file
 1776 				$from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
 1777 			}
 1778 			$from_file_type = file_type($diff{'from_mode'});
 1779 		}
 1780 
 1781 		if ($diff{'status'} eq "A") { # created
 1782 			my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
 1783 			$mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
 1784 			$mode_chng   .= "]</span>";
 1785 			print "<td>";
 1786 			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 1787 			                             hash_base=>$hash, file_name=>$diff{'file'}),
 1788 			              -class => "list"}, esc_html($diff{'file'}));
 1789 			print "</td>\n";
 1790 			print "<td>$mode_chng</td>\n";
 1791 			print "<td class=\"link\">";
 1792 			if ($action eq 'commitdiff') {
 1793 				# link to patch
 1794 				$patchno++;
 1795 				print $cgi->a({-href => "#patch$patchno"}, "patch");
 1796 			}
 1797 			print "</td>\n";
 1798 
 1799 		} elsif ($diff{'status'} eq "D") { # deleted
 1800 			my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
 1801 			print "<td>";
 1802 			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'from_id'},
 1803 			                             hash_base=>$parent, file_name=>$diff{'file'}),
 1804 			               -class => "list"}, esc_html($diff{'file'}));
 1805 			print "</td>\n";
 1806 			print "<td>$mode_chng</td>\n";
 1807 			print "<td class=\"link\">";
 1808 			if ($action eq 'commitdiff') {
 1809 				# link to patch
 1810 				$patchno++;
 1811 				print $cgi->a({-href => "#patch$patchno"}, "patch");
 1812 				print " | ";
 1813 			}
 1814 			print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
 1815 			                             file_name=>$diff{'file'})},
 1816 			              "blame") . " | ";
 1817 			print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
 1818 			                             file_name=>$diff{'file'})},
 1819 			              "history");
 1820 			print "</td>\n";
 1821 
 1822 		} elsif ($diff{'status'} eq "M" || $diff{'status'} eq "T") { # modified, or type changed
 1823 			my $mode_chnge = "";
 1824 			if ($diff{'from_mode'} != $diff{'to_mode'}) {
 1825 				$mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
 1826 				if ($from_file_type != $to_file_type) {
 1827 					$mode_chnge .= " from $from_file_type to $to_file_type";
 1828 				}
 1829 				if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
 1830 					if ($from_mode_str && $to_mode_str) {
 1831 						$mode_chnge .= " mode: $from_mode_str->$to_mode_str";
 1832 					} elsif ($to_mode_str) {
 1833 						$mode_chnge .= " mode: $to_mode_str";
 1834 					}
 1835 				}
 1836 				$mode_chnge .= "]</span>\n";
 1837 			}
 1838 			print "<td>";
 1839 			print $cgi->a({-href => href(action=>"blob", hash=>$diff{'to_id'},
 1840 			                             hash_base=>$hash, file_name=>$diff{'file'}),
 1841 			              -class => "list"}, esc_html($diff{'file'}));
 1842 			print "</td>\n";
 1843 			print "<td>$mode_chnge</td>\n";
 1844 			print "<td class=\"link\">";
 1845 			if ($diff{'to_id'} ne $diff{'from_id'}) { # modified
 1846 				if ($action eq 'commitdiff') {
 1847 					# link to patch
 1848 					$patchno++;
 1849 					print $cgi->a({-href => "#patch$patchno"}, "patch");
 1850 				} else {
 1851 					print $cgi->a({-href => href(action=>"blobdiff",
 1852 					                             hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 1853 					                             hash_base=>$hash, hash_parent_base=>$parent,
 1854 					                             file_name=>$diff{'file'})},
 1855 					              "diff");
 1856 				}
 1857 				print " | ";
 1858 			}
 1859 			print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
 1860 			                             file_name=>$diff{'file'})},
 1861 			              "blame") . " | ";
 1862 			print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
 1863 			                             file_name=>$diff{'file'})},
 1864 			              "history");
 1865 			print "</td>\n";
 1866 
 1867 		} elsif ($diff{'status'} eq "R" || $diff{'status'} eq "C") { # renamed or copied
 1868 			my %status_name = ('R' => 'moved', 'C' => 'copied');
 1869 			my $nstatus = $status_name{$diff{'status'}};
 1870 			my $mode_chng = "";
 1871 			if ($diff{'from_mode'} != $diff{'to_mode'}) {
 1872 				# mode also for directories, so we cannot use $to_mode_str
 1873 				$mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
 1874 			}
 1875 			print "<td>" .
 1876 			      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 1877 			                             hash=>$diff{'to_id'}, file_name=>$diff{'to_file'}),
 1878 			              -class => "list"}, esc_html($diff{'to_file'})) . "</td>\n" .
 1879 			      "<td><span class=\"file_status $nstatus\">[$nstatus from " .
 1880 			      $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
 1881 			                             hash=>$diff{'from_id'}, file_name=>$diff{'from_file'}),
 1882 			              -class => "list"}, esc_html($diff{'from_file'})) .
 1883 			      " with " . (int $diff{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
 1884 			      "<td class=\"link\">";
 1885 			if ($diff{'to_id'} ne $diff{'from_id'}) {
 1886 				if ($action eq 'commitdiff') {
 1887 					# link to patch
 1888 					$patchno++;
 1889 					print $cgi->a({-href => "#patch$patchno"}, "patch");
 1890 				} else {
 1891 					print $cgi->a({-href => href(action=>"blobdiff",
 1892 					                             hash=>$diff{'to_id'}, hash_parent=>$diff{'from_id'},
 1893 					                             hash_base=>$hash, hash_parent_base=>$parent,
 1894 					                             file_name=>$diff{'to_file'}, file_parent=>$diff{'from_file'})},
 1895 					              "diff");
 1896 				}
 1897 				print " | ";
 1898 			}
 1899 			print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
 1900 			                             file_name=>$diff{'from_file'})},
 1901 			              "blame") . " | ";
 1902 			print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
 1903 			                            file_name=>$diff{'from_file'})},
 1904 			              "history");
 1905 			print "</td>\n";
 1906 
 1907 		} # we should not encounter Unmerged (U) or Unknown (X) status
 1908 		print "</tr>\n";
 1909 	}
 1910 	print "</table>\n";
 1911 }
 1912 
 1913 sub git_patchset_body {
 1914 	my ($fd, $difftree, $hash, $hash_parent) = @_;
 1915 
 1916 	my $patch_idx = 0;
 1917 	my $in_header = 0;
 1918 	my $patch_found = 0;
 1919 	my $diffinfo;
 1920 
 1921 	print "<div class=\"patchset\">\n";
 1922 
 1923 	LINE:
 1924 	while (my $patch_line = <$fd>) {
 1925 		chomp $patch_line;
 1926 
 1927 		if ($patch_line =~ m/^diff /) { # "git diff" header
 1928 			# beginning of patch (in patchset)
 1929 			if ($patch_found) {
 1930 				# close previous patch
 1931 				print "</div>\n"; # class="patch"
 1932 			} else {
 1933 				# first patch in patchset
 1934 				$patch_found = 1;
 1935 			}
 1936 			print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
 1937 
 1938 			if (ref($difftree->[$patch_idx]) eq "HASH") {
 1939 				$diffinfo = $difftree->[$patch_idx];
 1940 			} else {
 1941 				$diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
 1942 			}
 1943 			$patch_idx++;
 1944 
 1945 			# for now, no extended header, hence we skip empty patches
 1946 			# companion to	next LINE if $in_header;
 1947 			if ($diffinfo->{'from_id'} eq $diffinfo->{'to_id'}) { # no change
 1948 				$in_header = 1;
 1949 				next LINE;
 1950 			}
 1951 
 1952 			if ($diffinfo->{'status'} eq "A") { # added
 1953 				print "<div class=\"diff_info\">" . file_type($diffinfo->{'to_mode'}) . ":" .
 1954 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 1955 				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
 1956 				              $diffinfo->{'to_id'}) . " (new)" .
 1957 				      "</div>\n"; # class="diff_info"
 1958 
 1959 			} elsif ($diffinfo->{'status'} eq "D") { # deleted
 1960 				print "<div class=\"diff_info\">" . file_type($diffinfo->{'from_mode'}) . ":" .
 1961 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 1962 				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
 1963 				              $diffinfo->{'from_id'}) . " (deleted)" .
 1964 				      "</div>\n"; # class="diff_info"
 1965 
 1966 			} elsif ($diffinfo->{'status'} eq "R" || # renamed
 1967 			         $diffinfo->{'status'} eq "C" || # copied
 1968 			         $diffinfo->{'status'} eq "2") { # with two filenames (from git_blobdiff)
 1969 				print "<div class=\"diff_info\">" .
 1970 				      file_type($diffinfo->{'from_mode'}) . ":" .
 1971 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 1972 				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'from_file'})},
 1973 				              $diffinfo->{'from_id'}) .
 1974 				      " -> " .
 1975 				      file_type($diffinfo->{'to_mode'}) . ":" .
 1976 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 1977 				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'to_file'})},
 1978 				              $diffinfo->{'to_id'});
 1979 				print "</div>\n"; # class="diff_info"
 1980 
 1981 			} else { # modified, mode changed, ...
 1982 				print "<div class=\"diff_info\">" .
 1983 				      file_type($diffinfo->{'from_mode'}) . ":" .
 1984 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 1985 				                             hash=>$diffinfo->{'from_id'}, file_name=>$diffinfo->{'file'})},
 1986 				              $diffinfo->{'from_id'}) .
 1987 				      " -> " .
 1988 				      file_type($diffinfo->{'to_mode'}) . ":" .
 1989 				      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 1990 				                             hash=>$diffinfo->{'to_id'}, file_name=>$diffinfo->{'file'})},
 1991 				              $diffinfo->{'to_id'});
 1992 				print "</div>\n"; # class="diff_info"
 1993 			}
 1994 
 1995 			#print "<div class=\"diff extended_header\">\n";
 1996 			$in_header = 1;
 1997 			next LINE;
 1998 		} # start of patch in patchset
 1999 
 2000 
 2001 		if ($in_header && $patch_line =~ m/^---/) {
 2002 			#print "</div>\n"; # class="diff extended_header"
 2003 			$in_header = 0;
 2004 
 2005 			my $file = $diffinfo->{'from_file'};
 2006 			$file  ||= $diffinfo->{'file'};
 2007 			$file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash_parent,
 2008 			                               hash=>$diffinfo->{'from_id'}, file_name=>$file),
 2009 			                -class => "list"}, esc_html($file));
 2010 			$patch_line =~ s|a/.*$|a/$file|g;
 2011 			print "<div class=\"diff from_file\">$patch_line</div>\n";
 2012 
 2013 			$patch_line = <$fd>;
 2014 			chomp $patch_line;
 2015 
 2016 			#$patch_line =~ m/^+++/;
 2017 			$file    = $diffinfo->{'to_file'};
 2018 			$file  ||= $diffinfo->{'file'};
 2019 			$file = $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
 2020 			                               hash=>$diffinfo->{'to_id'}, file_name=>$file),
 2021 			                -class => "list"}, esc_html($file));
 2022 			$patch_line =~ s|b/.*|b/$file|g;
 2023 			print "<div class=\"diff to_file\">$patch_line</div>\n";
 2024 
 2025 			next LINE;
 2026 		}
 2027 		next LINE if $in_header;
 2028 
 2029 		print format_diff_line($patch_line);
 2030 	}
 2031 	print "</div>\n" if $patch_found; # class="patch"
 2032 
 2033 	print "</div>\n"; # class="patchset"
 2034 }
 2035 
 2036 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
 2037 
 2038 sub git_shortlog_body {
 2039 	# uses global variable $project
 2040 	my ($revlist, $from, $to, $refs, $extra) = @_;
 2041 
 2042 	$from = 0 unless defined $from;
 2043 	$to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
 2044 
 2045 	print "<table class=\"shortlog\" cellspacing=\"0\">\n";
 2046 	my $alternate = 1;
 2047 	for (my $i = $from; $i <= $to; $i++) {
 2048 		my $commit = $revlist->[$i];
 2049 		#my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
 2050 		my $ref = format_ref_marker($refs, $commit);
 2051 		my %co = parse_commit($commit);
 2052 		if ($alternate) {
 2053 			print "<tr class=\"dark\">\n";
 2054 		} else {
 2055 			print "<tr class=\"light\">\n";
 2056 		}
 2057 		$alternate ^= 1;
 2058 		# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
 2059 		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 2060 		      "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
 2061 		      "<td>";
 2062 		print format_subject_html($co{'title'}, $co{'title_short'},
 2063 		                          href(action=>"commit", hash=>$commit), $ref);
 2064 		print "</td>\n" .
 2065 		      "<td class=\"link\">" .
 2066 		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
 2067 		      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
 2068 		if (gitweb_have_snapshot()) {
 2069 			print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
 2070 		}
 2071 		print "</td>\n" .
 2072 		      "</tr>\n";
 2073 	}
 2074 	if (defined $extra) {
 2075 		print "<tr>\n" .
 2076 		      "<td colspan=\"4\">$extra</td>\n" .
 2077 		      "</tr>\n";
 2078 	}
 2079 	print "</table>\n";
 2080 }
 2081 
 2082 sub git_history_body {
 2083 	# Warning: assumes constant type (blob or tree) during history
 2084 	my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
 2085 
 2086 	$from = 0 unless defined $from;
 2087 	$to = $#{$revlist} unless (defined $to && $to <= $#{$revlist});
 2088 
 2089 	print "<table class=\"history\" cellspacing=\"0\">\n";
 2090 	my $alternate = 1;
 2091 	for (my $i = $from; $i <= $to; $i++) {
 2092 		if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
 2093 			next;
 2094 		}
 2095 
 2096 		my $commit = $1;
 2097 		my %co = parse_commit($commit);
 2098 		if (!%co) {
 2099 			next;
 2100 		}
 2101 
 2102 		my $ref = format_ref_marker($refs, $commit);
 2103 
 2104 		if ($alternate) {
 2105 			print "<tr class=\"dark\">\n";
 2106 		} else {
 2107 			print "<tr class=\"light\">\n";
 2108 		}
 2109 		$alternate ^= 1;
 2110 		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 2111 		      # shortlog uses      chop_str($co{'author_name'}, 10)
 2112 		      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
 2113 		      "<td>";
 2114 		# originally git_history used chop_str($co{'title'}, 50)
 2115 		print format_subject_html($co{'title'}, $co{'title_short'},
 2116 		                          href(action=>"commit", hash=>$commit), $ref);
 2117 		print "</td>\n" .
 2118 		      "<td class=\"link\">" .
 2119 		      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
 2120 		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
 2121 
 2122 		if ($ftype eq 'blob') {
 2123 			my $blob_current = git_get_hash_by_path($hash_base, $file_name);
 2124 			my $blob_parent  = git_get_hash_by_path($commit, $file_name);
 2125 			if (defined $blob_current && defined $blob_parent &&
 2126 					$blob_current ne $blob_parent) {
 2127 				print " | " .
 2128 					$cgi->a({-href => href(action=>"blobdiff",
 2129 					                       hash=>$blob_current, hash_parent=>$blob_parent,
 2130 					                       hash_base=>$hash_base, hash_parent_base=>$commit,
 2131 					                       file_name=>$file_name)},
 2132 					        "diff to current");
 2133 			}
 2134 		}
 2135 		print "</td>\n" .
 2136 		      "</tr>\n";
 2137 	}
 2138 	if (defined $extra) {
 2139 		print "<tr>\n" .
 2140 		      "<td colspan=\"4\">$extra</td>\n" .
 2141 		      "</tr>\n";
 2142 	}
 2143 	print "</table>\n";
 2144 }
 2145 
 2146 sub git_tags_body {
 2147 	# uses global variable $project
 2148 	my ($taglist, $from, $to, $extra) = @_;
 2149 	$from = 0 unless defined $from;
 2150 	$to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
 2151 
 2152 	print "<table class=\"tags\" cellspacing=\"0\">\n";
 2153 	my $alternate = 1;
 2154 	for (my $i = $from; $i <= $to; $i++) {
 2155 		my $entry = $taglist->[$i];
 2156 		my %tag = %$entry;
 2157 		my $comment_lines = $tag{'comment'};
 2158 		my $comment = shift @$comment_lines;
 2159 		my $comment_short;
 2160 		if (defined $comment) {
 2161 			$comment_short = chop_str($comment, 30, 5);
 2162 		}
 2163 		if ($alternate) {
 2164 			print "<tr class=\"dark\">\n";
 2165 		} else {
 2166 			print "<tr class=\"light\">\n";
 2167 		}
 2168 		$alternate ^= 1;
 2169 		print "<td><i>$tag{'age'}</i></td>\n" .
 2170 		      "<td>" .
 2171 		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
 2172 		               -class => "list name"}, esc_html($tag{'name'})) .
 2173 		      "</td>\n" .
 2174 		      "<td>";
 2175 		if (defined $comment) {
 2176 			print format_subject_html($comment, $comment_short,
 2177 			                          href(action=>"tag", hash=>$tag{'id'}));
 2178 		}
 2179 		print "</td>\n" .
 2180 		      "<td class=\"selflink\">";
 2181 		if ($tag{'type'} eq "tag") {
 2182 			print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
 2183 		} else {
 2184 			print "&nbsp;";
 2185 		}
 2186 		print "</td>\n" .
 2187 		      "<td class=\"link\">" . " | " .
 2188 		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
 2189 		if ($tag{'reftype'} eq "commit") {
 2190 			print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") .
 2191 			      " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'refid'})}, "log");
 2192 		} elsif ($tag{'reftype'} eq "blob") {
 2193 			print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
 2194 		}
 2195 		print "</td>\n" .
 2196 		      "</tr>";
 2197 	}
 2198 	if (defined $extra) {
 2199 		print "<tr>\n" .
 2200 		      "<td colspan=\"5\">$extra</td>\n" .
 2201 		      "</tr>\n";
 2202 	}
 2203 	print "</table>\n";
 2204 }
 2205 
 2206 sub git_heads_body {
 2207 	# uses global variable $project
 2208 	my ($headlist, $head, $from, $to, $extra) = @_;
 2209 	$from = 0 unless defined $from;
 2210 	$to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
 2211 
 2212 	print "<table class=\"heads\" cellspacing=\"0\">\n";
 2213 	my $alternate = 1;
 2214 	for (my $i = $from; $i <= $to; $i++) {
 2215 		my $entry = $headlist->[$i];
 2216 		my %tag = %$entry;
 2217 		my $curr = $tag{'id'} eq $head;
 2218 		if ($alternate) {
 2219 			print "<tr class=\"dark\">\n";
 2220 		} else {
 2221 			print "<tr class=\"light\">\n";
 2222 		}
 2223 		$alternate ^= 1;
 2224 		print "<td><i>$tag{'age'}</i></td>\n" .
 2225 		      ($tag{'id'} eq $head ? "<td class=\"current_head\">" : "<td>") .
 2226 		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'}),
 2227 		               -class => "list name"},esc_html($tag{'name'})) .
 2228 		      "</td>\n" .
 2229 		      "<td class=\"link\">" .
 2230 		      $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'name'})}, "shortlog") . " | " .
 2231 		      $cgi->a({-href => href(action=>"log", hash=>$tag{'name'})}, "log") . " | " .
 2232 		      $cgi->a({-href => href(action=>"tree", hash=>$tag{'name'}, hash_base=>$tag{'name'})}, "tree") .
 2233 		      "</td>\n" .
 2234 		      "</tr>";
 2235 	}
 2236 	if (defined $extra) {
 2237 		print "<tr>\n" .
 2238 		      "<td colspan=\"3\">$extra</td>\n" .
 2239 		      "</tr>\n";
 2240 	}
 2241 	print "</table>\n";
 2242 }
 2243 
 2244 ## ======================================================================
 2245 ## ======================================================================
 2246 ## actions
 2247 
 2248 sub git_project_list {
 2249 	my $order = $cgi->param('o');
 2250 	if (defined $order && $order !~ m/project|descr|owner|age/) {
 2251 		die_error(undef, "Unknown order parameter");
 2252 	}
 2253 
 2254 	my @list = git_get_projects_list();
 2255 	my @projects;
 2256 	if (!@list) {
 2257 		die_error(undef, "No projects found");
 2258 	}
 2259 	foreach my $pr (@list) {
 2260         my $defhead = gitweb_get_default_head($pr->{'path'});
 2261 		my $head = git_get_head_hash($pr->{'path'}, $defhead);
 2262 		if (!defined $head) {
 2263 			next;
 2264 		}
 2265 		$git_dir = "$projectroot/$pr->{'path'}";
 2266 		my %co = parse_commit($head);
 2267 		if (!%co) {
 2268 			next;
 2269 		}
 2270 		$pr->{'commit'} = \%co;
 2271 		if (!defined $pr->{'descr'}) {
 2272 			my $descr = git_get_project_description($pr->{'path'}) || "";
 2273 			$pr->{'descr'} = chop_str($descr, 25, 5);
 2274 		}
 2275 		if (!defined $pr->{'owner'}) {
 2276 			$pr->{'owner'} = get_file_owner("$projectroot/$pr->{'path'}") || "";
 2277 		}
 2278 		push @projects, $pr;
 2279 	}
 2280 
 2281 	git_header_html();
 2282 	if (-f $home_text) {
 2283 		print "<div class=\"index_include\">\n";
 2284 		open (my $fd, $home_text);
 2285 		print <$fd>;
 2286 		close $fd;
 2287 		print "</div>\n";
 2288 	}
 2289 	print "<table class=\"project_list\">\n" .
 2290 	      "<tr>\n";
 2291 	$order ||= "project";
 2292 	if ($order eq "project") {
 2293 		@projects = sort {$a->{'path'} cmp $b->{'path'}} @projects;
 2294 		print "<th>Project</th>\n";
 2295 	} else {
 2296 		print "<th>" .
 2297 		      $cgi->a({-href => href(project=>undef, order=>'project'),
 2298 		               -class => "header"}, "Project") .
 2299 		      "</th>\n";
 2300 	}
 2301 	if ($order eq "descr") {
 2302 		@projects = sort {$a->{'descr'} cmp $b->{'descr'}} @projects;
 2303 		print "<th>Description</th>\n";
 2304 	} else {
 2305 		print "<th>" .
 2306 		      $cgi->a({-href => href(project=>undef, order=>'descr'),
 2307 		               -class => "header"}, "Description") .
 2308 		      "</th>\n";
 2309 	}
 2310 	if ($order eq "owner") {
 2311 		@projects = sort {$a->{'owner'} cmp $b->{'owner'}} @projects;
 2312 		print "<th>Owner</th>\n";
 2313 	} else {
 2314 		print "<th>" .
 2315 		      $cgi->a({-href => href(project=>undef, order=>'owner'),
 2316 		               -class => "header"}, "Owner") .
 2317 		      "</th>\n";
 2318 	}
 2319 	if ($order eq "age") {
 2320 		@projects = sort {$a->{'commit'}{'age'} <=> $b->{'commit'}{'age'}} @projects;
 2321 		print "<th>Last Change</th>\n";
 2322 	} else {
 2323 		print "<th>" .
 2324 		      $cgi->a({-href => href(project=>undef, order=>'age'),
 2325 		               -class => "header"}, "Last Change") .
 2326 		      "</th>\n";
 2327 	}
 2328 	print "<th></th>\n" .
 2329 	      "</tr>\n";
 2330 	my $alternate = 1;
 2331 	foreach my $pr (@projects) {
 2332 		if ($alternate) {
 2333 			print "<tr class=\"dark\">\n";
 2334 		} else {
 2335 			print "<tr class=\"light\">\n";
 2336 		}
 2337 		$alternate ^= 1;
 2338 		print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
 2339 		                        -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
 2340 		      "<td>" . esc_html($pr->{'descr'}) . "</td>\n" .
 2341 		      "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
 2342 		print "<td class=\"". age_class($pr->{'commit'}{'age'}) . "\">" .
 2343 		      $pr->{'commit'}{'age_string'} . "</td>\n" .
 2344 		      "<td class=\"link\">" .
 2345 		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
 2346 		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
 2347 		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
 2348 		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
 2349 		      "</td>\n" .
 2350 		      "</tr>\n";
 2351 	}
 2352 	print "</table>\n";
 2353 	git_footer_html();
 2354 }
 2355 
 2356 sub git_project_index {
 2357 	my @projects = git_get_projects_list();
 2358 
 2359 	print $cgi->header(
 2360 		-type => 'text/plain',
 2361 		-charset => 'utf-8',
 2362 		-content_disposition => 'inline; filename="index.aux"');
 2363 
 2364 	foreach my $pr (@projects) {
 2365 		if (!exists $pr->{'owner'}) {
 2366 			$pr->{'owner'} = get_file_owner("$projectroot/$project");
 2367 		}
 2368 
 2369 		my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
 2370 		# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
 2371 		$path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
 2372 		$owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
 2373 		$path  =~ s/ /\+/g;
 2374 		$owner =~ s/ /\+/g;
 2375 
 2376 		print "$path $owner\n";
 2377 	}
 2378 }
 2379 
 2380 sub git_summary {
 2381 	my $descr = git_get_project_description($project) || "none";
 2382 	my $head = git_get_head_hash($project);
 2383 	my %co = parse_commit($head);
 2384 	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
 2385 
 2386 	my $owner = git_get_project_owner($project);
 2387 
 2388 	my ($reflist, $refs) = git_get_refs_list();
 2389 
 2390 	my @taglist;
 2391 	my @headlist;
 2392 	foreach my $ref (@$reflist) {
 2393 		if ($ref->{'name'} =~ s!^heads/!!) {
 2394 			push @headlist, $ref;
 2395 		} else {
 2396 			$ref->{'name'} =~ s!^tags/!!;
 2397 			push @taglist, $ref;
 2398 		}
 2399 	}
 2400 
 2401 	git_header_html();
 2402 	git_print_page_nav('summary','', $head);
 2403 
 2404 	print "<div class=\"title\">&nbsp;</div>\n";
 2405 	print "<table cellspacing=\"0\">\n" .
 2406 	      "<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
 2407 	      "<tr><td>owner</td><td>$owner</td></tr>\n" .
 2408 	      "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
 2409 	# use per project git URL list in $projectroot/$project/cloneurl
 2410 	# or make project git URL from git base URL and project name
 2411 	my $url_tag = "URL";
 2412 	my @url_list = git_get_project_url_list($project);
 2413 	@url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
 2414 	foreach my $git_url (@url_list) {
 2415 		next unless $git_url;
 2416 		print "<tr><td>$url_tag</td><td>$git_url</td></tr>\n";
 2417 		$url_tag = "";
 2418 	}
 2419 	print "</table>\n";
 2420 
 2421 	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
 2422 		git_get_head_hash($project)
 2423 		or die_error(undef, "Open git-rev-list failed");
 2424 	my @revlist = map { chomp; $_ } <$fd>;
 2425 	close $fd;
 2426 	git_print_header_div('shortlog');
 2427 	git_shortlog_body(\@revlist, 0, 15, $refs,
 2428 	                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
 2429 
 2430 	if (@taglist) {
 2431 		git_print_header_div('tags');
 2432 		git_tags_body(\@taglist, 0, 15,
 2433 		              $cgi->a({-href => href(action=>"tags")}, "..."));
 2434 	}
 2435 
 2436 	if (@headlist) {
 2437 		git_print_header_div('heads');
 2438 		git_heads_body(\@headlist, $head, 0, 15,
 2439 		               $cgi->a({-href => href(action=>"heads")}, "..."));
 2440 	}
 2441 
 2442 	git_footer_html();
 2443 }
 2444 
 2445 sub git_tag {
 2446 	my $head = git_get_head_hash($project);
 2447 	git_header_html();
 2448 	git_print_page_nav('','', $head,undef,$head);
 2449 	my %tag = parse_tag($hash);
 2450 	git_print_header_div('commit', esc_html($tag{'name'}), $hash);
 2451 	print "<div class=\"title_text\">\n" .
 2452 	      "<table cellspacing=\"0\">\n" .
 2453 	      "<tr>\n" .
 2454 	      "<td>object</td>\n" .
 2455 	      "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
 2456 	                       $tag{'object'}) . "</td>\n" .
 2457 	      "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
 2458 	                                      $tag{'type'}) . "</td>\n" .
 2459 	      "</tr>\n";
 2460 	if (defined($tag{'author'})) {
 2461 		my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
 2462 		print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
 2463 		print "<tr><td></td><td>" . $ad{'rfc2822'} .
 2464 			sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
 2465 			"</td></tr>\n";
 2466 	}
 2467 	print "</table>\n\n" .
 2468 	      "</div>\n";
 2469 	print "<div class=\"page_body\">";
 2470 	my $comment = $tag{'comment'};
 2471 	foreach my $line (@$comment) {
 2472 		print esc_html($line) . "<br/>\n";
 2473 	}
 2474 	print "</div>\n";
 2475 	git_footer_html();
 2476 }
 2477 
 2478 sub git_blame2 {
 2479 	my $fd;
 2480 	my $ftype;
 2481 
 2482 	my ($have_blame) = gitweb_check_feature('blame');
 2483 	if (!$have_blame) {
 2484 		die_error('403 Permission denied', "Permission denied");
 2485 	}
 2486 	die_error('404 Not Found', "File name not defined") if (!$file_name);
 2487 	$hash_base ||= git_get_head_hash($project);
 2488 	die_error(undef, "Couldn't find base commit") unless ($hash_base);
 2489 	my %co = parse_commit($hash_base)
 2490 		or die_error(undef, "Reading commit failed");
 2491 	if (!defined $hash) {
 2492 		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
 2493 			or die_error(undef, "Error looking up file");
 2494 	}
 2495 	$ftype = git_get_type($hash);
 2496 	if ($ftype !~ "blob") {
 2497 		die_error("400 Bad Request", "Object is not a blob");
 2498 	}
 2499 	open ($fd, "-|", git_cmd(), "blame", '-l', '--', $file_name, $hash_base)
 2500 		or die_error(undef, "Open git-blame failed");
 2501 	git_header_html();
 2502 	my $formats_nav =
 2503 		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
 2504 		        "blob") .
 2505 		" | " .
 2506 		$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
 2507 			"history") .
 2508 		" | " .
 2509 		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
 2510 		        "HEAD");
 2511 	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 2512 	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 2513 	git_print_page_path($file_name, $ftype, $hash_base);
 2514 	my @rev_color = (qw(light2 dark2));
 2515 	my $num_colors = scalar(@rev_color);
 2516 	my $current_color = 0;
 2517 	my $last_rev;
 2518 	print <<HTML;
 2519 <div class="page_body">
 2520 <table class="blame">
 2521 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
 2522 HTML
 2523 	while (<$fd>) {
 2524 		/^([0-9a-fA-F]{40}).*?(\d+)\)\s{1}(\s*.*)/;
 2525 		my $full_rev = $1;
 2526 		my $rev = substr($full_rev, 0, 8);
 2527 		my $lineno = $2;
 2528 		my $data = $3;
 2529 
 2530 		if (!defined $last_rev) {
 2531 			$last_rev = $full_rev;
 2532 		} elsif ($last_rev ne $full_rev) {
 2533 			$last_rev = $full_rev;
 2534 			$current_color = ++$current_color % $num_colors;
 2535 		}
 2536 		print "<tr class=\"$rev_color[$current_color]\">\n";
 2537 		print "<td class=\"sha1\">" .
 2538 			$cgi->a({-href => href(action=>"commit", hash=>$full_rev, file_name=>$file_name)},
 2539 			        esc_html($rev)) . "</td>\n";
 2540 		print "<td class=\"linenr\"><a id=\"l$lineno\" href=\"#l$lineno\" class=\"linenr\">" .
 2541 		      esc_html($lineno) . "</a></td>\n";
 2542 		print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
 2543 		print "</tr>\n";
 2544 	}
 2545 	print "</table>\n";
 2546 	print "</div>";
 2547 	close $fd
 2548 		or print "Reading blob failed\n";
 2549 	git_footer_html();
 2550 }
 2551 
 2552 sub git_blame {
 2553 	my $fd;
 2554 
 2555 	my ($have_blame) = gitweb_check_feature('blame');
 2556 	if (!$have_blame) {
 2557 		die_error('403 Permission denied', "Permission denied");
 2558 	}
 2559 	die_error('404 Not Found', "File name not defined") if (!$file_name);
 2560 	$hash_base ||= git_get_head_hash($project);
 2561 	die_error(undef, "Couldn't find base commit") unless ($hash_base);
 2562 	my %co = parse_commit($hash_base)
 2563 		or die_error(undef, "Reading commit failed");
 2564 	if (!defined $hash) {
 2565 		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
 2566 			or die_error(undef, "Error lookup file");
 2567 	}
 2568 	open ($fd, "-|", git_cmd(), "annotate", '-l', '-t', '-r', $file_name, $hash_base)
 2569 		or die_error(undef, "Open git-annotate failed");
 2570 	git_header_html();
 2571 	my $formats_nav =
 2572 		$cgi->a({-href => href(action=>"blob", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
 2573 		        "blob") .
 2574 		" | " .
 2575 		$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base, file_name=>$file_name)},
 2576 			"history") .
 2577 		" | " .
 2578 		$cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
 2579 		        "HEAD");
 2580 	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 2581 	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 2582 	git_print_page_path($file_name, 'blob', $hash_base);
 2583 	print "<div class=\"page_body\">\n";
 2584 	print <<HTML;
 2585 <table class="blame">
 2586   <tr>
 2587     <th>Commit</th>
 2588     <th>Age</th>
 2589     <th>Author</th>
 2590     <th>Line</th>
 2591     <th>Data</th>
 2592   </tr>
 2593 HTML
 2594 	my @line_class = (qw(light dark));
 2595 	my $line_class_len = scalar (@line_class);
 2596 	my $line_class_num = $#line_class;
 2597 	while (my $line = <$fd>) {
 2598 		my $long_rev;
 2599 		my $short_rev;
 2600 		my $author;
 2601 		my $time;
 2602 		my $lineno;
 2603 		my $data;
 2604 		my $age;
 2605 		my $age_str;
 2606 		my $age_class;
 2607 
 2608 		chomp $line;
 2609 		$line_class_num = ($line_class_num + 1) % $line_class_len;
 2610 
 2611 		if ($line =~ m/^([0-9a-fA-F]{40})\t\(\s*([^\t]+)\t(\d+) [+-]\d\d\d\d\t(\d+)\)(.*)$/) {
 2612 			$long_rev = $1;
 2613 			$author   = $2;
 2614 			$time     = $3;
 2615 			$lineno   = $4;
 2616 			$data     = $5;
 2617 		} else {
 2618 			print qq(  <tr><td colspan="5" class="error">Unable to parse: $line</td></tr>\n);
 2619 			next;
 2620 		}
 2621 		$short_rev  = substr ($long_rev, 0, 8);
 2622 		$age        = time () - $time;
 2623 		$age_str    = age_string ($age);
 2624 		$age_str    =~ s/ /&nbsp;/g;
 2625 		$age_class  = age_class($age);
 2626 		$author     = esc_html ($author);
 2627 		$author     =~ s/ /&nbsp;/g;
 2628 
 2629 		$data = untabify($data);
 2630 		$data = esc_html ($data);
 2631 
 2632 		print <<HTML;
 2633   <tr class="$line_class[$line_class_num]">
 2634     <td class="sha1"><a href="${\href (action=>"commit", hash=>$long_rev)}" class="text">$short_rev..</a></td>
 2635     <td class="$age_class">$age_str</td>
 2636     <td>$author</td>
 2637     <td class="linenr"><a id="$lineno" href="#$lineno" class="linenr">$lineno</a></td>
 2638     <td class="pre">$data</td>
 2639   </tr>
 2640 HTML
 2641 	} # while (my $line = <$fd>)
 2642 	print "</table>\n\n";
 2643 	close $fd
 2644 		or print "Reading blob failed.\n";
 2645 	print "</div>";
 2646 	git_footer_html();
 2647 }
 2648 
 2649 sub git_tags {
 2650 	my $head = git_get_head_hash($project);
 2651 	git_header_html();
 2652 	git_print_page_nav('','', $head,undef,$head);
 2653 	git_print_header_div('summary', $project);
 2654 
 2655 	my ($taglist) = git_get_refs_list("tags");
 2656 	if (@$taglist) {
 2657 		git_tags_body($taglist);
 2658 	}
 2659 	git_footer_html();
 2660 }
 2661 
 2662 sub git_heads {
 2663 	my $head = git_get_head_hash($project);
 2664 	git_header_html();
 2665 	git_print_page_nav('','', $head,undef,$head);
 2666 	git_print_header_div('summary', $project);
 2667 
 2668 	my ($headlist) = git_get_refs_list("heads");
 2669 	if (@$headlist) {
 2670 		git_heads_body($headlist, $head);
 2671 	}
 2672 	git_footer_html();
 2673 }
 2674 
 2675 sub git_blob_plain {
 2676 	my $expires;
 2677 
 2678 	if (!defined $hash) {
 2679 		if (defined $file_name) {
 2680 			my $base = $hash_base || git_get_head_hash($project);
 2681 			$hash = git_get_hash_by_path($base, $file_name, "blob")
 2682 				or die_error(undef, "Error lookup file");
 2683 		} else {
 2684 			die_error(undef, "No file name defined");
 2685 		}
 2686 	} elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
 2687 		# blobs defined by non-textual hash id's can be cached
 2688 		$expires = "+1d";
 2689 	}
 2690 
 2691 	my $type = shift;
 2692 	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
 2693 		or die_error(undef, "Couldn't cat $file_name, $hash");
 2694 
 2695 	$type ||= blob_mimetype($fd, $file_name);
 2696 
 2697 	# save as filename, even when no $file_name is given
 2698 	my $save_as = "$hash";
 2699 	if (defined $file_name) {
 2700 		$save_as = $file_name;
 2701 	} elsif ($type =~ m/^text\//) {
 2702 		$save_as .= '.txt';
 2703 	}
 2704 
 2705 	print $cgi->header(
 2706 		-type => "$type",
 2707 		-expires=>$expires,
 2708 		-content_disposition => 'inline; filename="' . "$save_as" . '"');
 2709 	undef $/;
 2710 	binmode STDOUT, ':raw';
 2711 	print <$fd>;
 2712 	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
 2713 	$/ = "\n";
 2714 	close $fd;
 2715 }
 2716 
 2717 sub git_blob {
 2718 	my $expires;
 2719 
 2720 	if (!defined $hash) {
 2721 		if (defined $file_name) {
 2722 			my $base = $hash_base || git_get_head_hash($project);
 2723 			$hash = git_get_hash_by_path($base, $file_name, "blob")
 2724 				or die_error(undef, "Error lookup file");
 2725 		} else {
 2726 			die_error(undef, "No file name defined");
 2727 		}
 2728 	} elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
 2729 		# blobs defined by non-textual hash id's can be cached
 2730 		$expires = "+1d";
 2731 	}
 2732 
 2733 	my ($have_blame) = gitweb_check_feature('blame');
 2734 	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
 2735 		or die_error(undef, "Couldn't cat $file_name, $hash");
 2736 	my $mimetype = blob_mimetype($fd, $file_name);
 2737 	if ($mimetype !~ m/^text\//) {
 2738 		close $fd;
 2739 		return git_blob_plain($mimetype);
 2740 	}
 2741 	git_header_html(undef, $expires);
 2742 	my $formats_nav = '';
 2743 	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 2744 		if (defined $file_name) {
 2745 			if ($have_blame) {
 2746 				$formats_nav .=
 2747 					$cgi->a({-href => href(action=>"blame", hash_base=>$hash_base,
 2748 					                       hash=>$hash, file_name=>$file_name)},
 2749 					        "blame") .
 2750 					" | ";
 2751 			}
 2752 			$formats_nav .=
 2753 				$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
 2754 				                       hash=>$hash, file_name=>$file_name)},
 2755 				        "history") .
 2756 				" | " .
 2757 				$cgi->a({-href => href(action=>"blob_plain",
 2758 				                       hash=>$hash, file_name=>$file_name)},
 2759 				        "raw") .
 2760 				" | " .
 2761 				$cgi->a({-href => href(action=>"blob",
 2762 				                       hash_base=>"HEAD", file_name=>$file_name)},
 2763 				        "HEAD");
 2764 		} else {
 2765 			$formats_nav .=
 2766 				$cgi->a({-href => href(action=>"blob_plain", hash=>$hash)}, "raw");
 2767 		}
 2768 		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 2769 		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 2770 	} else {
 2771 		print "<div class=\"page_nav\">\n" .
 2772 		      "<br/><br/></div>\n" .
 2773 		      "<div class=\"title\">$hash</div>\n";
 2774 	}
 2775 	git_print_page_path($file_name, "blob", $hash_base);
 2776 	print "<div class=\"page_body\">\n";
 2777 	my $nr;
 2778 	while (my $line = <$fd>) {
 2779 		chomp $line;
 2780 		$nr++;
 2781 		$line = untabify($line);
 2782 		printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
 2783 		       $nr, $nr, $nr, esc_html($line);
 2784 	}
 2785 	close $fd
 2786 		or print "Reading blob failed.\n";
 2787 	print "</div>";
 2788 	git_footer_html();
 2789 }
 2790 
 2791 sub git_tree {
 2792 	my $have_snapshot = gitweb_have_snapshot();
 2793 
 2794 	if (!defined $hash_base) {
 2795 		$hash_base = gitweb_get_default_head($project);
 2796 	}
 2797 	if (!defined $hash) {
 2798 		if (defined $file_name) {
 2799 			$hash = git_get_hash_by_path($hash_base, $file_name, "tree");
 2800 		} else {
 2801 			$hash = $hash_base;
 2802 		}
 2803 	}
 2804 	$/ = "\0";
 2805 	open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
 2806 		or die_error(undef, "Open git-ls-tree failed");
 2807 	my @entries = map { chomp; $_ } <$fd>;
 2808 	close $fd or die_error(undef, "Reading tree failed");
 2809 	$/ = "\n";
 2810 
 2811 	my $refs = git_get_references();
 2812 	my $ref = format_ref_marker($refs, $hash_base);
 2813 	git_header_html();
 2814 	my $basedir = '';
 2815 	my ($have_blame) = gitweb_check_feature('blame');
 2816 	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 2817 		my @views_nav = ();
 2818 		if (defined $file_name) {
 2819 			push @views_nav,
 2820 				$cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
 2821 				                       hash=>$hash, file_name=>$file_name)},
 2822 				        "history"),
 2823 				$cgi->a({-href => href(action=>"tree",
 2824 				                       hash_base=>"HEAD", file_name=>$file_name)},
 2825 				        "HEAD"),
 2826 		}
 2827 		if ($have_snapshot) {
 2828 			# FIXME: Should be available when we have no hash base as well.
 2829 			push @views_nav,
 2830 				$cgi->a({-href => href(action=>"snapshot", hash=>$hash)},
 2831 				        "snapshot");
 2832 		}
 2833 		git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
 2834 		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
 2835 	} else {
 2836 		undef $hash_base;
 2837 		print "<div class=\"page_nav\">\n";
 2838 		print "<br/><br/></div>\n";
 2839 		print "<div class=\"title\">$hash</div>\n";
 2840 	}
 2841 	if (defined $file_name) {
 2842 		$basedir = $file_name;
 2843 		if ($basedir ne '' && substr($basedir, -1) ne '/') {
 2844 			$basedir .= '/';
 2845 		}
 2846 	}
 2847 	git_print_page_path($file_name, 'tree', $hash_base);
 2848 	print "<div class=\"page_body\">\n";
 2849 	print "<table cellspacing=\"0\">\n";
 2850 	my $alternate = 1;
 2851 	foreach my $line (@entries) {
 2852 		my %t = parse_ls_tree_line($line, -z => 1);
 2853 
 2854 		if ($alternate) {
 2855 			print "<tr class=\"dark\">\n";
 2856 		} else {
 2857 			print "<tr class=\"light\">\n";
 2858 		}
 2859 		$alternate ^= 1;
 2860 
 2861 		git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
 2862 
 2863 		print "</tr>\n";
 2864 	}
 2865 	print "</table>\n" .
 2866 	      "</div>";
 2867 	git_footer_html();
 2868 }
 2869 
 2870 sub git_snapshot {
 2871 	my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
 2872 	my $have_snapshot = (defined $ctype && defined $suffix);
 2873 	if (!$have_snapshot) {
 2874 		die_error('403 Permission denied', "Permission denied");
 2875 	}
 2876 
 2877 	if (!defined $hash) {
 2878 		$hash = git_get_head_hash($project);
 2879 	}
 2880 
 2881 	my $filename = basename($project) . "-$hash.tar.$suffix";
 2882 
 2883 	print $cgi->header(
 2884 		-type => 'application/x-tar',
 2885 		-content_encoding => $ctype,
 2886 		-content_disposition => 'inline; filename="' . "$filename" . '"',
 2887 		-status => '200 OK');
 2888 
 2889 	my $git = git_cmd_str();
 2890 	my $name = $project;
 2891 	$name =~ s/\047/\047\\\047\047/g;
 2892 	open my $fd, "-|",
 2893 	"$git archive --format=tar --prefix=\'$name\'/ $hash | $command"
 2894 		or die_error(undef, "Execute git-tar-tree failed.");
 2895 	binmode STDOUT, ':raw';
 2896 	print <$fd>;
 2897 	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
 2898 	close $fd;
 2899 
 2900 }
 2901 
 2902 sub git_log {
 2903 	my $head = git_get_head_hash($project);
 2904 	if (!defined $hash) {
 2905 		$hash = $head;
 2906 	}
 2907 	if (!defined $page) {
 2908 		$page = 0;
 2909 	}
 2910 	my $refs = git_get_references();
 2911 
 2912 	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 2913 	open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
 2914 		or die_error(undef, "Open git-rev-list failed");
 2915 	my @revlist = map { chomp; $_ } <$fd>;
 2916 	close $fd;
 2917 
 2918 	my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
 2919 
 2920 	git_header_html();
 2921 	git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 2922 
 2923 	if (!@revlist) {
 2924 		my %co = parse_commit($hash);
 2925 
 2926 		git_print_header_div('summary', $project);
 2927 		print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
 2928 	}
 2929 	for (my $i = ($page * 100); $i <= $#revlist; $i++) {
 2930 		my $commit = $revlist[$i];
 2931 		my $ref = format_ref_marker($refs, $commit);
 2932 		my %co = parse_commit($commit);
 2933 		next if !%co;
 2934 		my %ad = parse_date($co{'author_epoch'});
 2935 		git_print_header_div('commit',
 2936 		               "<span class=\"age\">$co{'age_string'}</span>" .
 2937 		               esc_html($co{'title'}) . $ref,
 2938 		               $commit);
 2939 		print "<div class=\"title_text\">\n" .
 2940 		      "<div class=\"log_link\">\n" .
 2941 		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
 2942 		      " | " .
 2943 		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
 2944 		      " | " .
 2945 		      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
 2946 		      "<br/>\n" .
 2947 		      "</div>\n" .
 2948 		      "<i>" . esc_html($co{'author_name'}) .  " [$ad{'rfc2822'}]</i><br/>\n" .
 2949 		      "</div>\n";
 2950 
 2951 		print "<div class=\"log_body\">\n";
 2952 		git_print_simplified_log($co{'comment'});
 2953 		print "</div>\n";
 2954 	}
 2955 	git_footer_html();
 2956 }
 2957 
 2958 sub git_commit {
 2959 	my %co = parse_commit($hash);
 2960 	if (!%co) {
 2961 		die_error(undef, "Unknown commit object");
 2962 	}
 2963 	my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
 2964 	my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
 2965 
 2966 	my $parent = $co{'parent'};
 2967 	if (!defined $parent) {
 2968 		$parent = "--root";
 2969 	}
 2970 	open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $parent, $hash
 2971 		or die_error(undef, "Open git-diff-tree failed");
 2972 	my @difftree = map { chomp; $_ } <$fd>;
 2973 	close $fd or die_error(undef, "Reading git-diff-tree failed");
 2974 
 2975 	# non-textual hash id's can be cached
 2976 	my $expires;
 2977 	if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
 2978 		$expires = "+1d";
 2979 	}
 2980 	my $refs = git_get_references();
 2981 	my $ref = format_ref_marker($refs, $co{'id'});
 2982 
 2983 	my $have_snapshot = gitweb_have_snapshot();
 2984 
 2985 	my @views_nav = ();
 2986 	if (defined $file_name && defined $co{'parent'}) {
 2987 		push @views_nav,
 2988 			$cgi->a({-href => href(action=>"blame", hash_parent=>$parent, file_name=>$file_name)},
 2989 			        "blame");
 2990 	}
 2991 	git_header_html(undef, $expires);
 2992 	git_print_page_nav('commit', '',
 2993 	                   $hash, $co{'tree'}, $hash,
 2994 	                   join (' | ', @views_nav));
 2995 
 2996 	if (defined $co{'parent'}) {
 2997 		git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
 2998 	} else {
 2999 		git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
 3000 	}
 3001 	print "<div class=\"title_text\">\n" .
 3002 	      "<table cellspacing=\"0\">\n";
 3003 	print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
 3004 	      "<tr>" .
 3005 	      "<td></td><td> $ad{'rfc2822'}";
 3006 	if ($ad{'hour_local'} < 6) {
 3007 		printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
 3008 		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 3009 	} else {
 3010 		printf(" (%02d:%02d %s)",
 3011 		       $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
 3012 	}
 3013 	print "</td>" .
 3014 	      "</tr>\n";
 3015 	print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
 3016 	print "<tr><td></td><td> $cd{'rfc2822'}" .
 3017 	      sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
 3018 	      "</td></tr>\n";
 3019 	print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
 3020 	print "<tr>" .
 3021 	      "<td>tree</td>" .
 3022 	      "<td class=\"sha1\">" .
 3023 	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
 3024 	               class => "list"}, $co{'tree'}) .
 3025 	      "</td>" .
 3026 	      "<td class=\"link\">" .
 3027 	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
 3028 	              "tree");
 3029 	if ($have_snapshot) {
 3030 		print " | " .
 3031 		      $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
 3032 	}
 3033 	print "</td>" .
 3034 	      "</tr>\n";
 3035 	my $parents = $co{'parents'};
 3036 	foreach my $par (@$parents) {
 3037 		print "<tr>" .
 3038 		      "<td>parent</td>" .
 3039 		      "<td class=\"sha1\">" .
 3040 		      $cgi->a({-href => href(action=>"commit", hash=>$par),
 3041 		               class => "list"}, $par) .
 3042 		      "</td>" .
 3043 		      "<td class=\"link\">" .
 3044 		      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
 3045 		      " | " .
 3046 		      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
 3047 		      "</td>" .
 3048 		      "</tr>\n";
 3049 	}
 3050 	print "</table>".
 3051 	      "</div>\n";
 3052 
 3053 	print "<div class=\"page_body\">\n";
 3054 	git_print_log($co{'comment'});
 3055 	print "</div>\n";
 3056 
 3057 	git_difftree_body(\@difftree, $hash, $parent);
 3058 
 3059 	git_footer_html();
 3060 }
 3061 
 3062 sub git_blobdiff {
 3063 	my $format = shift || 'html';
 3064 
 3065 	my $fd;
 3066 	my @difftree;
 3067 	my %diffinfo;
 3068 	my $expires;
 3069 
 3070 	# preparing $fd and %diffinfo for git_patchset_body
 3071 	# new style URI
 3072 	if (defined $hash_base && defined $hash_parent_base) {
 3073 		if (defined $file_name) {
 3074 			# read raw output
 3075 			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base,
 3076 				"--", $file_name
 3077 				or die_error(undef, "Open git-diff-tree failed");
 3078 			@difftree = map { chomp; $_ } <$fd>;
 3079 			close $fd
 3080 				or die_error(undef, "Reading git-diff-tree failed");
 3081 			@difftree
 3082 				or die_error('404 Not Found', "Blob diff not found");
 3083 
 3084 		} elsif (defined $hash &&
 3085 		         $hash =~ /[0-9a-fA-F]{40}/) {
 3086 			# try to find filename from $hash
 3087 
 3088 			# read filtered raw output
 3089 			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts, $hash_parent_base, $hash_base
 3090 				or die_error(undef, "Open git-diff-tree failed");
 3091 			@difftree =
 3092 				# ':100644 100644 03b21826... 3b93d5e7... M	ls-files.c'
 3093 				# $hash == to_id
 3094 				grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
 3095 				map { chomp; $_ } <$fd>;
 3096 			close $fd
 3097 				or die_error(undef, "Reading git-diff-tree failed");
 3098 			@difftree
 3099 				or die_error('404 Not Found', "Blob diff not found");
 3100 
 3101 		} else {
 3102 			die_error('404 Not Found', "Missing one of the blob diff parameters");
 3103 		}
 3104 
 3105 		if (@difftree > 1) {
 3106 			die_error('404 Not Found', "Ambiguous blob diff specification");
 3107 		}
 3108 
 3109 		%diffinfo = parse_difftree_raw_line($difftree[0]);
 3110 		$file_parent ||= $diffinfo{'from_file'} || $file_name || $diffinfo{'file'};
 3111 		$file_name   ||= $diffinfo{'to_file'}   || $diffinfo{'file'};
 3112 
 3113 		$hash_parent ||= $diffinfo{'from_id'};
 3114 		$hash        ||= $diffinfo{'to_id'};
 3115 
 3116 		# non-textual hash id's can be cached
 3117 		if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
 3118 		    $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
 3119 			$expires = '+1d';
 3120 		}
 3121 
 3122 		# open patch output
 3123 		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 3124 			'-p', $hash_parent_base, $hash_base,
 3125 			"--", $file_name
 3126 			or die_error(undef, "Open git-diff-tree failed");
 3127 	}
 3128 
 3129 	# old/legacy style URI
 3130 	if (!%diffinfo && # if new style URI failed
 3131 	    defined $hash && defined $hash_parent) {
 3132 		# fake git-diff-tree raw output
 3133 		$diffinfo{'from_mode'} = $diffinfo{'to_mode'} = "blob";
 3134 		$diffinfo{'from_id'} = $hash_parent;
 3135 		$diffinfo{'to_id'}   = $hash;
 3136 		if (defined $file_name) {
 3137 			if (defined $file_parent) {
 3138 				$diffinfo{'status'} = '2';
 3139 				$diffinfo{'from_file'} = $file_parent;
 3140 				$diffinfo{'to_file'}   = $file_name;
 3141 			} else { # assume not renamed
 3142 				$diffinfo{'status'} = '1';
 3143 				$diffinfo{'from_file'} = $file_name;
 3144 				$diffinfo{'to_file'}   = $file_name;
 3145 			}
 3146 		} else { # no filename given
 3147 			$diffinfo{'status'} = '2';
 3148 			$diffinfo{'from_file'} = $hash_parent;
 3149 			$diffinfo{'to_file'}   = $hash;
 3150 		}
 3151 
 3152 		# non-textual hash id's can be cached
 3153 		if ($hash =~ m/^[0-9a-fA-F]{40}$/ &&
 3154 		    $hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
 3155 			$expires = '+1d';
 3156 		}
 3157 
 3158 		# open patch output
 3159 		open $fd, "-|", git_cmd(), "diff", '-p', @diff_opts, $hash_parent, $hash
 3160 			or die_error(undef, "Open git-diff failed");
 3161 	} else  {
 3162 		die_error('404 Not Found', "Missing one of the blob diff parameters")
 3163 			unless %diffinfo;
 3164 	}
 3165 
 3166 	# header
 3167 	if ($format eq 'html') {
 3168 		my $formats_nav =
 3169 			$cgi->a({-href => href(action=>"blobdiff_plain",
 3170 			                       hash=>$hash, hash_parent=>$hash_parent,
 3171 			                       hash_base=>$hash_base, hash_parent_base=>$hash_parent_base,
 3172 			                       file_name=>$file_name, file_parent=>$file_parent)},
 3173 			        "raw");
 3174 		git_header_html(undef, $expires);
 3175 		if (defined $hash_base && (my %co = parse_commit($hash_base))) {
 3176 			git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
 3177 			git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 3178 		} else {
 3179 			print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
 3180 			print "<div class=\"title\">$hash vs $hash_parent</div>\n";
 3181 		}
 3182 		if (defined $file_name) {
 3183 			git_print_page_path($file_name, "blob", $hash_base);
 3184 		} else {
 3185 			print "<div class=\"page_path\"></div>\n";
 3186 		}
 3187 
 3188 	} elsif ($format eq 'plain') {
 3189 		print $cgi->header(
 3190 			-type => 'text/plain',
 3191 			-charset => 'utf-8',
 3192 			-expires => $expires,
 3193 			-content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
 3194 
 3195 		print "X-Git-Url: " . $cgi->self_url() . "\n\n";
 3196 
 3197 	} else {
 3198 		die_error(undef, "Unknown blobdiff format");
 3199 	}
 3200 
 3201 	# patch
 3202 	if ($format eq 'html') {
 3203 		print "<div class=\"page_body\">\n";
 3204 
 3205 		git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
 3206 		close $fd;
 3207 
 3208 		print "</div>\n"; # class="page_body"
 3209 		git_footer_html();
 3210 
 3211 	} else {
 3212 		while (my $line = <$fd>) {
 3213 			$line =~ s!a/($hash|$hash_parent)!'a/'.esc_html($diffinfo{'from_file'})!eg;
 3214 			$line =~ s!b/($hash|$hash_parent)!'b/'.esc_html($diffinfo{'to_file'})!eg;
 3215 
 3216 			print $line;
 3217 
 3218 			last if $line =~ m!^\+\+\+!;
 3219 		}
 3220 		local $/ = undef;
 3221 		print <$fd>;
 3222 		close $fd;
 3223 	}
 3224 }
 3225 
 3226 sub git_blobdiff_plain {
 3227 	git_blobdiff('plain');
 3228 }
 3229 
 3230 sub git_commitdiff {
 3231 	my $format = shift || 'html';
 3232 	my %co = parse_commit($hash);
 3233 	if (!%co) {
 3234 		die_error(undef, "Unknown commit object");
 3235 	}
 3236 	if (!defined $hash_parent) {
 3237 		$hash_parent = $co{'parent'} || '--root';
 3238 	}
 3239 
 3240 	# read commitdiff
 3241 	my $fd;
 3242 	my @difftree;
 3243 	if ($format eq 'html') {
 3244 		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 3245 			"--patch-with-raw", "--full-index", $hash_parent, $hash
 3246 			or die_error(undef, "Open git-diff-tree failed");
 3247 
 3248 		while (chomp(my $line = <$fd>)) {
 3249 			# empty line ends raw part of diff-tree output
 3250 			last unless $line;
 3251 			push @difftree, $line;
 3252 		}
 3253 
 3254 	} elsif ($format eq 'plain') {
 3255 		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 3256 			'-p', $hash_parent, $hash
 3257 			or die_error(undef, "Open git-diff-tree failed");
 3258 
 3259 	} else {
 3260 		die_error(undef, "Unknown commitdiff format");
 3261 	}
 3262 
 3263 	# non-textual hash id's can be cached
 3264 	my $expires;
 3265 	if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
 3266 		$expires = "+1d";
 3267 	}
 3268 
 3269 	# write commit message
 3270 	if ($format eq 'html') {
 3271 		my $refs = git_get_references();
 3272 		my $ref = format_ref_marker($refs, $co{'id'});
 3273 		my $formats_nav =
 3274 			$cgi->a({-href => href(action=>"commitdiff_plain",
 3275 			                       hash=>$hash, hash_parent=>$hash_parent)},
 3276 			        "raw");
 3277 
 3278 		git_header_html(undef, $expires);
 3279 		git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
 3280 		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
 3281 		git_print_authorship(\%co);
 3282 		print "<div class=\"page_body\">\n";
 3283 		print "<div class=\"log\">\n";
 3284 		git_print_simplified_log($co{'comment'}, 1); # skip title
 3285 		print "</div>\n"; # class="log"
 3286 
 3287 	} elsif ($format eq 'plain') {
 3288 		my $refs = git_get_references("tags");
 3289 		my $tagname = git_get_rev_name_tags($hash);
 3290 		my $filename = basename($project) . "-$hash.patch";
 3291 
 3292 		print $cgi->header(
 3293 			-type => 'text/plain',
 3294 			-charset => 'utf-8',
 3295 			-expires => $expires,
 3296 			-content_disposition => 'inline; filename="' . "$filename" . '"');
 3297 		my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
 3298 		print <<TEXT;
 3299 From: $co{'author'}
 3300 Date: $ad{'rfc2822'} ($ad{'tz_local'})
 3301 Subject: $co{'title'}
 3302 TEXT
 3303 		print "X-Git-Tag: $tagname\n" if $tagname;
 3304 		print "X-Git-Url: " . $cgi->self_url() . "\n\n";
 3305 
 3306 		foreach my $line (@{$co{'comment'}}) {
 3307 			print "$line\n";
 3308 		}
 3309 		print "---\n\n";
 3310 	}
 3311 
 3312 	# write patch
 3313 	if ($format eq 'html') {
 3314 		git_difftree_body(\@difftree, $hash, $hash_parent);
 3315 		print "<br/>\n";
 3316 
 3317 		git_patchset_body($fd, \@difftree, $hash, $hash_parent);
 3318 		close $fd;
 3319 		print "</div>\n"; # class="page_body"
 3320 		git_footer_html();
 3321 
 3322 	} elsif ($format eq 'plain') {
 3323 		local $/ = undef;
 3324 		print <$fd>;
 3325 		close $fd
 3326 			or print "Reading git-diff-tree failed\n";
 3327 	}
 3328 }
 3329 
 3330 sub git_commitdiff_plain {
 3331 	git_commitdiff('plain');
 3332 }
 3333 
 3334 sub git_history {
 3335 	if (!defined $hash_base) {
 3336 		$hash_base = git_get_head_hash($project);
 3337 	}
 3338 	if (!defined $page) {
 3339 		$page = 0;
 3340 	}
 3341 	my $ftype;
 3342 	my %co = parse_commit($hash_base);
 3343 	if (!%co) {
 3344 		die_error(undef, "Unknown commit object");
 3345 	}
 3346 
 3347 	my $refs = git_get_references();
 3348 	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 3349 
 3350 	if (!defined $hash && defined $file_name) {
 3351 		$hash = git_get_hash_by_path($hash_base, $file_name);
 3352 	}
 3353 	if (defined $hash) {
 3354 		$ftype = git_get_type($hash);
 3355 	}
 3356 
 3357 	open my $fd, "-|",
 3358 		git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name
 3359 			or die_error(undef, "Open git-rev-list-failed");
 3360 	my @revlist = map { chomp; $_ } <$fd>;
 3361 	close $fd
 3362 		or die_error(undef, "Reading git-rev-list failed");
 3363 
 3364 	my $paging_nav = '';
 3365 	if ($page > 0) {
 3366 		$paging_nav .=
 3367 			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 3368 			                       file_name=>$file_name)},
 3369 			        "first");
 3370 		$paging_nav .= " &sdot; " .
 3371 			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 3372 			                       file_name=>$file_name, page=>$page-1),
 3373 			         -accesskey => "p", -title => "Alt-p"}, "prev");
 3374 	} else {
 3375 		$paging_nav .= "first";
 3376 		$paging_nav .= " &sdot; prev";
 3377 	}
 3378 	if ($#revlist >= (100 * ($page+1)-1)) {
 3379 		$paging_nav .= " &sdot; " .
 3380 			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 3381 			                       file_name=>$file_name, page=>$page+1),
 3382 			         -accesskey => "n", -title => "Alt-n"}, "next");
 3383 	} else {
 3384 		$paging_nav .= " &sdot; next";
 3385 	}
 3386 	my $next_link = '';
 3387 	if ($#revlist >= (100 * ($page+1)-1)) {
 3388 		$next_link =
 3389 			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 3390 			                       file_name=>$file_name, page=>$page+1),
 3391 			         -title => "Alt-n"}, "next");
 3392 	}
 3393 
 3394 	git_header_html();
 3395 	git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
 3396 	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 3397 	git_print_page_path($file_name, $ftype, $hash_base);
 3398 
 3399 	git_history_body(\@revlist, ($page * 100), $#revlist,
 3400 	                 $refs, $hash_base, $ftype, $next_link);
 3401 
 3402 	git_footer_html();
 3403 }
 3404 
 3405 sub git_search {
 3406 	if (!defined $searchtext) {
 3407 		die_error(undef, "Text field empty");
 3408 	}
 3409 	if (!defined $hash) {
 3410 		$hash = git_get_head_hash($project);
 3411 	}
 3412 	my %co = parse_commit($hash);
 3413 	if (!%co) {
 3414 		die_error(undef, "Unknown commit object");
 3415 	}
 3416 
 3417 	my $commit_search = 1;
 3418 	my $author_search = 0;
 3419 	my $committer_search = 0;
 3420 	my $pickaxe_search = 0;
 3421 	if ($searchtext =~ s/^author\\://i) {
 3422 		$author_search = 1;
 3423 	} elsif ($searchtext =~ s/^committer\\://i) {
 3424 		$committer_search = 1;
 3425 	} elsif ($searchtext =~ s/^pickaxe\\://i) {
 3426 		$commit_search = 0;
 3427 		$pickaxe_search = 1;
 3428 
 3429 		# pickaxe may take all resources of your box and run for several minutes
 3430 		# with every query - so decide by yourself how public you make this feature
 3431 		my ($have_pickaxe) = gitweb_check_feature('pickaxe');
 3432 		if (!$have_pickaxe) {
 3433 			die_error('403 Permission denied', "Permission denied");
 3434 		}
 3435 	}
 3436 	git_header_html();
 3437 	git_print_page_nav('','', $hash,$co{'tree'},$hash);
 3438 	git_print_header_div('commit', esc_html($co{'title'}), $hash);
 3439 
 3440 	print "<table cellspacing=\"0\">\n";
 3441 	my $alternate = 1;
 3442 	if ($commit_search) {
 3443 		$/ = "\0";
 3444 		open my $fd, "-|", git_cmd(), "rev-list", "--header", "--parents", $hash or next;
 3445 		while (my $commit_text = <$fd>) {
 3446 			if (!grep m/$searchtext/i, $commit_text) {
 3447 				next;
 3448 			}
 3449 			if ($author_search && !grep m/\nauthor .*$searchtext/i, $commit_text) {
 3450 				next;
 3451 			}
 3452 			if ($committer_search && !grep m/\ncommitter .*$searchtext/i, $commit_text) {
 3453 				next;
 3454 			}
 3455 			my @commit_lines = split "\n", $commit_text;
 3456 			my %co = parse_commit(undef, \@commit_lines);
 3457 			if (!%co) {
 3458 				next;
 3459 			}
 3460 			if ($alternate) {
 3461 				print "<tr class=\"dark\">\n";
 3462 			} else {
 3463 				print "<tr class=\"light\">\n";
 3464 			}
 3465 			$alternate ^= 1;
 3466 			print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 3467 			      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
 3468 			      "<td>" .
 3469 			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
 3470 			               esc_html(chop_str($co{'title'}, 50)) . "<br/>");
 3471 			my $comment = $co{'comment'};
 3472 			foreach my $line (@$comment) {
 3473 				if ($line =~ m/^(.*)($searchtext)(.*)$/i) {
 3474 					my $lead = esc_html($1) || "";
 3475 					$lead = chop_str($lead, 30, 10);
 3476 					my $match = esc_html($2) || "";
 3477 					my $trail = esc_html($3) || "";
 3478 					$trail = chop_str($trail, 30, 10);
 3479 					my $text = "$lead<span class=\"match\">$match</span>$trail";
 3480 					print chop_str($text, 80, 5) . "<br/>\n";
 3481 				}
 3482 			}
 3483 			print "</td>\n" .
 3484 			      "<td class=\"link\">" .
 3485 			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
 3486 			      " | " .
 3487 			      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
 3488 			print "</td>\n" .
 3489 			      "</tr>\n";
 3490 		}
 3491 		close $fd;
 3492 	}
 3493 
 3494 	if ($pickaxe_search) {
 3495 		$/ = "\n";
 3496 		my $git_command = git_cmd_str();
 3497 		open my $fd, "-|", "$git_command rev-list $hash | " .
 3498 			"$git_command diff-tree -r --stdin -S\'$searchtext\'";
 3499 		undef %co;
 3500 		my @files;
 3501 		while (my $line = <$fd>) {
 3502 			if (%co && $line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)\t(.*)$/) {
 3503 				my %set;
 3504 				$set{'file'} = $6;
 3505 				$set{'from_id'} = $3;
 3506 				$set{'to_id'} = $4;
 3507 				$set{'id'} = $set{'to_id'};
 3508 				if ($set{'id'} =~ m/0{40}/) {
 3509 					$set{'id'} = $set{'from_id'};
 3510 				}
 3511 				if ($set{'id'} =~ m/0{40}/) {
 3512 					next;
 3513 				}
 3514 				push @files, \%set;
 3515 			} elsif ($line =~ m/^([0-9a-fA-F]{40})$/){
 3516 				if (%co) {
 3517 					if ($alternate) {
 3518 						print "<tr class=\"dark\">\n";
 3519 					} else {
 3520 						print "<tr class=\"light\">\n";
 3521 					}
 3522 					$alternate ^= 1;
 3523 					print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
 3524 					      "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
 3525 					      "<td>" .
 3526 					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
 3527 					              -class => "list subject"},
 3528 					              esc_html(chop_str($co{'title'}, 50)) . "<br/>");
 3529 					while (my $setref = shift @files) {
 3530 						my %set = %$setref;
 3531 						print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
 3532 						                             hash=>$set{'id'}, file_name=>$set{'file'}),
 3533 						              -class => "list"},
 3534 						              "<span class=\"match\">" . esc_html($set{'file'}) . "</span>") .
 3535 						      "<br/>\n";
 3536 					}
 3537 					print "</td>\n" .
 3538 					      "<td class=\"link\">" .
 3539 					      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
 3540 					      " | " .
 3541 					      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
 3542 					print "</td>\n" .
 3543 					      "</tr>\n";
 3544 				}
 3545 				%co = parse_commit($1);
 3546 			}
 3547 		}
 3548 		close $fd;
 3549 	}
 3550 	print "</table>\n";
 3551 	git_footer_html();
 3552 }
 3553 
 3554 sub git_shortlog {
 3555 	my $head = git_get_head_hash($project);
 3556 	if (!defined $hash) {
 3557 		$hash = $head;
 3558 	}
 3559 	if (!defined $page) {
 3560 		$page = 0;
 3561 	}
 3562 	my $refs = git_get_references();
 3563 
 3564 	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
 3565 	open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash
 3566 		or die_error(undef, "Open git-rev-list failed");
 3567 	my @revlist = map { chomp; $_ } <$fd>;
 3568 	close $fd;
 3569 
 3570 	my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
 3571 	my $next_link = '';
 3572 	if ($#revlist >= (100 * ($page+1)-1)) {
 3573 		$next_link =
 3574 			$cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
 3575 			         -title => "Alt-n"}, "next");
 3576 	}
 3577 
 3578 
 3579 	git_header_html();
 3580 	git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
 3581 	git_print_header_div('summary', $project);
 3582 
 3583 	git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);
 3584 
 3585 	git_footer_html();
 3586 }
 3587 
 3588 ## ......................................................................
 3589 ## feeds (RSS, OPML)
 3590 
 3591 sub git_rss {
 3592 	# http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
 3593 	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150", git_get_head_hash($project)
 3594 		or die_error(undef, "Open git-rev-list failed");
 3595 	my @revlist = map { chomp; $_ } <$fd>;
 3596 	close $fd or die_error(undef, "Reading git-rev-list failed");
 3597 	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
 3598 	print <<XML;
 3599 <?xml version="1.0" encoding="utf-8"?>
 3600 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
 3601 <channel>
 3602 <title>$project $my_uri $my_url</title>
 3603 <link>${\esc_html("$my_url?p=$project;a=summary")}</link>
 3604 <description>$project log</description>
 3605 <language>en</language>
 3606 XML
 3607 
 3608 	for (my $i = 0; $i <= $#revlist; $i++) {
 3609 		my $commit = $revlist[$i];
 3610 		my %co = parse_commit($commit);
 3611 		# we read 150, we always show 30 and the ones more recent than 48 hours
 3612 		if (($i >= 20) && ((time - $co{'committer_epoch'}) > 48*60*60)) {
 3613 			last;
 3614 		}
 3615 		my %cd = parse_date($co{'committer_epoch'});
 3616 		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 3617 			$co{'parent'}, $co{'id'}
 3618 			or next;
 3619 		my @difftree = map { chomp; $_ } <$fd>;
 3620 		close $fd
 3621 			or next;
 3622 		print "<item>\n" .
 3623 		      "<title>" .
 3624 		      sprintf("%d %s %02d:%02d", $cd{'mday'}, $cd{'month'}, $cd{'hour'}, $cd{'minute'}) . " - " . esc_html($co{'title'}) .
 3625 		      "</title>\n" .
 3626 		      "<author>" . esc_html($co{'author'}) . "</author>\n" .
 3627 		      "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
 3628 		      "<guid isPermaLink=\"true\">" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</guid>\n" .
 3629 		      "<link>" . esc_html("$my_url?p=$project;a=commit;h=$commit") . "</link>\n" .
 3630 		      "<description>" . esc_html($co{'title'}) . "</description>\n" .
 3631 		      "<content:encoded>" .
 3632 		      "<![CDATA[\n";
 3633 		my $comment = $co{'comment'};
 3634 		foreach my $line (@$comment) {
 3635 			$line = to_utf8($line);
 3636 			print "$line<br/>\n";
 3637 		}
 3638 		print "<br/>\n";
 3639 		foreach my $line (@difftree) {
 3640 			if (!($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/)) {
 3641 				next;
 3642 			}
 3643 			my $file = esc_html(unquote($7));
 3644 			$file = to_utf8($file);
 3645 			print "$file<br/>\n";
 3646 		}
 3647 		print "]]>\n" .
 3648 		      "</content:encoded>\n" .
 3649 		      "</item>\n";
 3650 	}
 3651 	print "</channel></rss>";
 3652 }
 3653 
 3654 sub git_opml {
 3655 	my @list = git_get_projects_list();
 3656 
 3657 	print $cgi->header(-type => 'text/xml', -charset => 'utf-8');
 3658 	print <<XML;
 3659 <?xml version="1.0" encoding="utf-8"?>
 3660 <opml version="1.0">
 3661 <head>
 3662   <title>$site_name Git OPML Export</title>
 3663 </head>
 3664 <body>
 3665 <outline text="git RSS feeds">
 3666 XML
 3667 
 3668 	foreach my $pr (@list) {
 3669 		my %proj = %$pr;
 3670 		my $head = git_get_head_hash($proj{'path'});
 3671 		if (!defined $head) {
 3672 			next;
 3673 		}
 3674 		$git_dir = "$projectroot/$proj{'path'}";
 3675 		my %co = parse_commit($head);
 3676 		if (!%co) {
 3677 			next;
 3678 		}
 3679 
 3680 		my $path = esc_html(chop_str($proj{'path'}, 25, 5));
 3681 		my $rss  = "$my_url?p=$proj{'path'};a=rss";
 3682 		my $html = "$my_url?p=$proj{'path'};a=summary";
 3683 		print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
 3684 	}
 3685 	print <<XML;
 3686 </outline>
 3687 </body>
 3688 </opml>
 3689 XML
 3690 }

Generated by cgit