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

Generated by cgit