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

Generated by cgit