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
10use 5.008;
11use strict;
12use warnings;
13# handle ACL in file access tests
14use filetest 'access';
15use CGI qw(:standard :escapeHTML -nosticky);
16use CGI::Util qw(unescape);
17use CGI::Carp qw(fatalsToBrowser set_message);
18use Encode;
19use Fcntl ':mode';
20use File::Find qw();
21use File::Basename qw(basename);
22use Time::HiRes qw(gettimeofday tv_interval);
23use Digest::MD5 qw(md5_hex);
24
25binmode STDOUT, ':utf8';
26
27if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) {
28	eval 'sub CGI::multi_param { CGI::param(@_) }'
29}
30
31our $t0 = [ gettimeofday() ];
32our $number_of_git_cmds = 0;
33
34BEGIN {
35	CGI->compile() if $ENV{'MOD_PERL'};
36}
37
38our $version = "++GIT_VERSION++";
39
40our ($my_url, $my_uri, $base_url, $path_info, $home_link);
41sub evaluate_uri {
42	our $cgi;
43
44	our $my_url = $cgi->url();
45	our $my_uri = $cgi->url(-absolute => 1);
46
47	# Base URL for relative URLs in gitweb ($logo, $favicon, ...),
48	# needed and used only for URLs with nonempty PATH_INFO
49	our $base_url = $my_url;
50
51	# When the script is used as DirectoryIndex, the URL does not contain the name
52	# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
53	# have to do it ourselves. We make $path_info global because it's also used
54	# later on.
55	#
56	# Another issue with the script being the DirectoryIndex is that the resulting
57	# $my_url data is not the full script URL: this is good, because we want
58	# generated links to keep implying the script name if it wasn't explicitly
59	# indicated in the URL we're handling, but it means that $my_url cannot be used
60	# as base URL.
61	# Therefore, if we needed to strip PATH_INFO, then we know that we have
62	# to build the base URL ourselves:
63	our $path_info = decode_utf8($ENV{"PATH_INFO"});
64	if ($path_info) {
65		# $path_info has already been URL-decoded by the web server, but
66		# $my_url and $my_uri have not. URL-decode them so we can properly
67		# strip $path_info.
68		$my_url = unescape($my_url);
69		$my_uri = unescape($my_uri);
70		if ($my_url =~ s,\Q$path_info\E$,, &&
71		    $my_uri =~ s,\Q$path_info\E$,, &&
72		    defined $ENV{'SCRIPT_NAME'}) {
73			$base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
74		}
75	}
76
77	# target of the home link on top of all pages
78	our $home_link = $my_uri || "/";
79}
80
81# core git executable to use
82# this can just be "git" if your webserver has a sensible PATH
83our $GIT = "++GIT_BINDIR++/git";
84
85# absolute fs-path which will be prepended to the project path
86#our $projectroot = "/pub/scm";
87our $projectroot = "++GITWEB_PROJECTROOT++";
88
89# fs traversing limit for getting project list
90# the number is relative to the projectroot
91our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
92
93# string of the home link on top of all pages
94our $home_link_str = "++GITWEB_HOME_LINK_STR++";
95
96# extra breadcrumbs preceding the home link
97our @extra_breadcrumbs = ();
98
99# name of your site or organization to appear in page titles
100# replace this with something more descriptive for clearer bookmarks
101our $site_name = "++GITWEB_SITENAME++"
102                 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
103
104# html snippet to include in the <head> section of each page
105our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
106# filename of html text to include at top of each page
107our $site_header = "++GITWEB_SITE_HEADER++";
108# html text to include at home page
109our $home_text = "++GITWEB_HOMETEXT++";
110# filename of html text to include at bottom of each page
111our $site_footer = "++GITWEB_SITE_FOOTER++";
112
113# URI of stylesheets
114our @stylesheets = ("++GITWEB_CSS++");
115# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
116our $stylesheet = undef;
117# URI of GIT logo (72x27 size)
118our $logo = "++GITWEB_LOGO++";
119# URI of GIT favicon, assumed to be image/png type
120our $favicon = "++GITWEB_FAVICON++";
121# URI of gitweb.js (JavaScript code for gitweb)
122our $javascript = "++GITWEB_JS++";
123
124# URI and label (title) of GIT logo link
125#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
126#our $logo_label = "git documentation";
127our $logo_url = "http://git-scm.com/";
128our $logo_label = "git homepage";
129
130# source of projects list
131our $projects_list = "++GITWEB_LIST++";
132
133# the width (in characters) of the projects list "Description" column
134our $projects_list_description_width = 25;
135
136# group projects by category on the projects list
137# (enabled if this variable evaluates to true)
138our $projects_list_group_categories = 0;
139
140# default category if none specified
141# (leave the empty string for no category)
142our $project_list_default_category = "";
143
144# default order of projects list
145# valid values are none, project, descr, owner, and age
146our $default_projects_order = "project";
147
148# show repository only if this file exists
149# (only effective if this variable evaluates to true)
150our $export_ok = "++GITWEB_EXPORT_OK++";
151
152# don't generate age column on the projects list page
153our $omit_age_column = 0;
154
155# don't generate information about owners of repositories
156our $omit_owner=0;
157
158# show repository only if this subroutine returns true
159# when given the path to the project, for example:
160#    sub { return -e "$_[0]/git-daemon-export-ok"; }
161our $export_auth_hook = undef;
162
163# only allow viewing of repositories also shown on the overview page
164our $strict_export = "++GITWEB_STRICT_EXPORT++";
165
166# list of git base URLs used for URL to where fetch project from,
167# i.e. full URL is "$git_base_url/$project"
168our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
169
170# default blob_plain mimetype and default charset for text/plain blob
171our $default_blob_plain_mimetype = 'text/plain';
172our $default_text_plain_charset  = undef;
173
174# file to use for guessing MIME types before trying /etc/mime.types
175# (relative to the current git repository)
176our $mimetypes_file = undef;
177
178# assume this charset if line contains non-UTF-8 characters;
179# it should be valid encoding (see Encoding::Supported(3pm) for list),
180# for which encoding all byte sequences are valid, for example
181# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
182# could be even 'utf-8' for the old behavior)
183our $fallback_encoding = 'latin1';
184
185# rename detection options for git-diff and git-diff-tree
186# - default is '-M', with the cost proportional to
187#   (number of removed files) * (number of new files).
188# - more costly is '-C' (which implies '-M'), with the cost proportional to
189#   (number of changed files + number of removed files) * (number of new files)
190# - even more costly is '-C', '--find-copies-harder' with cost
191#   (number of files in the original tree) * (number of new files)
192# - one might want to include '-B' option, e.g. '-B', '-M'
193our @diff_opts = ('-M'); # taken from git_commit
194
195# Disables features that would allow repository owners to inject script into
196# the gitweb domain.
197our $prevent_xss = 0;
198
199# Path to the highlight executable to use (must be the one from
200# http://www.andre-simon.de due to assumptions about parameters and output).
201# Useful if highlight is not installed on your webserver's PATH.
202# [Default: highlight]
203our $highlight_bin = "++HIGHLIGHT_BIN++";
204
205# information about snapshot formats that gitweb is capable of serving
206our %known_snapshot_formats = (
207	# name => {
208	# 	'display' => display name,
209	# 	'type' => mime type,
210	# 	'suffix' => filename suffix,
211	# 	'format' => --format for git-archive,
212	# 	'compressor' => [compressor command and arguments]
213	# 	                (array reference, optional)
214	# 	'disabled' => boolean (optional)}
215	#
216	'tgz' => {
217		'display' => 'tar.gz',
218		'type' => 'application/x-gzip',
219		'suffix' => '.tar.gz',
220		'format' => 'tar',
221		'compressor' => ['gzip', '-n']},
222
223	'tbz2' => {
224		'display' => 'tar.bz2',
225		'type' => 'application/x-bzip2',
226		'suffix' => '.tar.bz2',
227		'format' => 'tar',
228		'compressor' => ['bzip2']},
229
230	'txz' => {
231		'display' => 'tar.xz',
232		'type' => 'application/x-xz',
233		'suffix' => '.tar.xz',
234		'format' => 'tar',
235		'compressor' => ['xz'],
236		'disabled' => 1},
237
238	'zip' => {
239		'display' => 'zip',
240		'type' => 'application/x-zip',
241		'suffix' => '.zip',
242		'format' => 'zip'},
243);
244
245# Aliases so we understand old gitweb.snapshot values in repository
246# configuration.
247our %known_snapshot_format_aliases = (
248	'gzip'  => 'tgz',
249	'bzip2' => 'tbz2',
250	'xz'    => 'txz',
251
252	# backward compatibility: legacy gitweb config support
253	'x-gzip' => undef, 'gz' => undef,
254	'x-bzip2' => undef, 'bz2' => undef,
255	'x-zip' => undef, '' => undef,
256);
257
258# Pixel sizes for icons and avatars. If the default font sizes or lineheights
259# are changed, it may be appropriate to change these values too via
260# $GITWEB_CONFIG.
261our %avatar_size = (
262	'default' => 16,
263	'double'  => 32
264);
265
266# Used to set the maximum load that we will still respond to gitweb queries.
267# If server load exceed this value then return "503 server busy" error.
268# If gitweb cannot determined server load, it is taken to be 0.
269# Leave it undefined (or set to 'undef') to turn off load checking.
270our $maxload = 300;
271
272# configuration for 'highlight' (http://www.andre-simon.de/)
273# match by basename
274our %highlight_basename = (
275	#'Program' => 'py',
276	#'Library' => 'py',
277	'SConstruct' => 'py', # SCons equivalent of Makefile
278	'Makefile' => 'make',
279);
280# match by extension
281our %highlight_ext = (
282	# main extensions, defining name of syntax;
283	# see files in /usr/share/highlight/langDefs/ directory
284	(map { $_ => $_ } qw(py rb java css js tex bib xml awk bat ini spec tcl sql)),
285	# alternate extensions, see /etc/highlight/filetypes.conf
286	(map { $_ => 'c'   } qw(c h)),
287	(map { $_ => 'sh'  } qw(sh bash zsh ksh)),
288	(map { $_ => 'cpp' } qw(cpp cxx c++ cc)),
289	(map { $_ => 'php' } qw(php php3 php4 php5 phps)),
290	(map { $_ => 'pl'  } qw(pl perl pm)), # perhaps also 'cgi'
291	(map { $_ => 'make'} qw(make mak mk)),
292	(map { $_ => 'xml' } qw(xml xhtml html htm)),
293);
294
295# You define site-wide feature defaults here; override them with
296# $GITWEB_CONFIG as necessary.
297our %feature = (
298	# feature => {
299	# 	'sub' => feature-sub (subroutine),
300	# 	'override' => allow-override (boolean),
301	# 	'default' => [ default options...] (array reference)}
302	#
303	# if feature is overridable (it means that allow-override has true value),
304	# then feature-sub will be called with default options as parameters;
305	# return value of feature-sub indicates if to enable specified feature
306	#
307	# if there is no 'sub' key (no feature-sub), then feature cannot be
308	# overridden
309	#
310	# use gitweb_get_feature(<feature>) to retrieve the <feature> value
311	# (an array) or gitweb_check_feature(<feature>) to check if <feature>
312	# is enabled
313
314	# Enable the 'blame' blob view, showing the last commit that modified
315	# each line in the file. This can be very CPU-intensive.
316
317	# To enable system wide have in $GITWEB_CONFIG
318	# $feature{'blame'}{'default'} = [1];
319	# To have project specific config enable override in $GITWEB_CONFIG
320	# $feature{'blame'}{'override'} = 1;
321	# and in project config gitweb.blame = 0|1;
322	'blame' => {
323		'sub' => sub { feature_bool('blame', @_) },
324		'override' => 0,
325		'default' => [0]},
326
327	# Enable the 'snapshot' link, providing a compressed archive of any
328	# tree. This can potentially generate high traffic if you have large
329	# project.
330
331	# Value is a list of formats defined in %known_snapshot_formats that
332	# you wish to offer.
333	# To disable system wide have in $GITWEB_CONFIG
334	# $feature{'snapshot'}{'default'} = [];
335	# To have project specific config enable override in $GITWEB_CONFIG
336	# $feature{'snapshot'}{'override'} = 1;
337	# and in project config, a comma-separated list of formats or "none"
338	# to disable.  Example: gitweb.snapshot = tbz2,zip;
339	'snapshot' => {
340		'sub' => \&feature_snapshot,
341		'override' => 0,
342		'default' => ['tgz']},
343
344	# Enable text search, which will list the commits which match author,
345	# committer or commit text to a given string.  Enabled by default.
346	# Project specific override is not supported.
347	#
348	# Note that this controls all search features, which means that if
349	# it is disabled, then 'grep' and 'pickaxe' search would also be
350	# disabled.
351	'search' => {
352		'override' => 0,
353		'default' => [1]},
354
355	# Enable grep search, which will list the files in currently selected
356	# tree containing the given string. Enabled by default. This can be
357	# potentially CPU-intensive, of course.
358	# Note that you need to have 'search' feature enabled too.
359
360	# To enable system wide have in $GITWEB_CONFIG
361	# $feature{'grep'}{'default'} = [1];
362	# To have project specific config enable override in $GITWEB_CONFIG
363	# $feature{'grep'}{'override'} = 1;
364	# and in project config gitweb.grep = 0|1;
365	'grep' => {
366		'sub' => sub { feature_bool('grep', @_) },
367		'override' => 0,
368		'default' => [1]},
369
370	# Enable the pickaxe search, which will list the commits that modified
371	# a given string in a file. This can be practical and quite faster
372	# alternative to 'blame', but still potentially CPU-intensive.
373	# Note that you need to have 'search' feature enabled too.
374
375	# To enable system wide have in $GITWEB_CONFIG
376	# $feature{'pickaxe'}{'default'} = [1];
377	# To have project specific config enable override in $GITWEB_CONFIG
378	# $feature{'pickaxe'}{'override'} = 1;
379	# and in project config gitweb.pickaxe = 0|1;
380	'pickaxe' => {
381		'sub' => sub { feature_bool('pickaxe', @_) },
382		'override' => 0,
383		'default' => [1]},
384
385	# Enable showing size of blobs in a 'tree' view, in a separate
386	# column, similar to what 'ls -l' does.  This cost a bit of IO.
387
388	# To disable system wide have in $GITWEB_CONFIG
389	# $feature{'show-sizes'}{'default'} = [0];
390	# To have project specific config enable override in $GITWEB_CONFIG
391	# $feature{'show-sizes'}{'override'} = 1;
392	# and in project config gitweb.showsizes = 0|1;
393	'show-sizes' => {
394		'sub' => sub { feature_bool('showsizes', @_) },
395		'override' => 0,
396		'default' => [1]},
397
398	# Make gitweb use an alternative format of the URLs which can be
399	# more readable and natural-looking: project name is embedded
400	# directly in the path and the query string contains other
401	# auxiliary information. All gitweb installations recognize
402	# URL in either format; this configures in which formats gitweb
403	# generates links.
404
405	# To enable system wide have in $GITWEB_CONFIG
406	# $feature{'pathinfo'}{'default'} = [1];
407	# Project specific override is not supported.
408
409	# Note that you will need to change the default location of CSS,
410	# favicon, logo and possibly other files to an absolute URL. Also,
411	# if gitweb.cgi serves as your indexfile, you will need to force
412	# $my_uri to contain the script name in your $GITWEB_CONFIG.
413	'pathinfo' => {
414		'override' => 0,
415		'default' => [0]},
416
417	# Make gitweb consider projects in project root subdirectories
418	# to be forks of existing projects. Given project $projname.git,
419	# projects matching $projname/*.git will not be shown in the main
420	# projects list, instead a '+' mark will be added to $projname
421	# there and a 'forks' view will be enabled for the project, listing
422	# all the forks. If project list is taken from a file, forks have
423	# to be listed after the main project.
424
425	# To enable system wide have in $GITWEB_CONFIG
426	# $feature{'forks'}{'default'} = [1];
427	# Project specific override is not supported.
428	'forks' => {
429		'override' => 0,
430		'default' => [0]},
431
432	# Insert custom links to the action bar of all project pages.
433	# This enables you mainly to link to third-party scripts integrating
434	# into gitweb; e.g. git-browser for graphical history representation
435	# or custom web-based repository administration interface.
436
437	# The 'default' value consists of a list of triplets in the form
438	# (label, link, position) where position is the label after which
439	# to insert the link and link is a format string where %n expands
440	# to the project name, %f to the project path within the filesystem,
441	# %h to the current hash (h gitweb parameter) and %b to the current
442	# hash base (hb gitweb parameter); %% expands to %.
443
444	# To enable system wide have in $GITWEB_CONFIG e.g.
445	# $feature{'actions'}{'default'} = [('graphiclog',
446	# 	'/git-browser/by-commit.html?r=%n', 'summary')];
447	# Project specific override is not supported.
448	'actions' => {
449		'override' => 0,
450		'default' => []},
451
452	# Allow gitweb scan project content tags of project repository,
453	# and display the popular Web 2.0-ish "tag cloud" near the projects
454	# list.  Note that this is something COMPLETELY different from the
455	# normal Git tags.
456
457	# gitweb by itself can show existing tags, but it does not handle
458	# tagging itself; you need to do it externally, outside gitweb.
459	# The format is described in git_get_project_ctags() subroutine.
460	# You may want to install the HTML::TagCloud Perl module to get
461	# a pretty tag cloud instead of just a list of tags.
462
463	# To enable system wide have in $GITWEB_CONFIG
464	# $feature{'ctags'}{'default'} = [1];
465	# Project specific override is not supported.
466
467	# In the future whether ctags editing is enabled might depend
468	# on the value, but using 1 should always mean no editing of ctags.
469	'ctags' => {
470		'override' => 0,
471		'default' => [0]},
472
473	# The maximum number of patches in a patchset generated in patch
474	# view. Set this to 0 or undef to disable patch view, or to a
475	# negative number to remove any limit.
476
477	# To disable system wide have in $GITWEB_CONFIG
478	# $feature{'patches'}{'default'} = [0];
479	# To have project specific config enable override in $GITWEB_CONFIG
480	# $feature{'patches'}{'override'} = 1;
481	# and in project config gitweb.patches = 0|n;
482	# where n is the maximum number of patches allowed in a patchset.
483	'patches' => {
484		'sub' => \&feature_patches,
485		'override' => 0,
486		'default' => [16]},
487
488	# Avatar support. When this feature is enabled, views such as
489	# shortlog or commit will display an avatar associated with
490	# the email of the committer(s) and/or author(s).
491
492	# Currently available providers are gravatar and picon.
493	# If an unknown provider is specified, the feature is disabled.
494
495	# Picon currently relies on the indiana.edu database.
496
497	# To enable system wide have in $GITWEB_CONFIG
498	# $feature{'avatar'}{'default'} = ['<provider>'];
499	# where <provider> is either gravatar or picon.
500	# To have project specific config enable override in $GITWEB_CONFIG
501	# $feature{'avatar'}{'override'} = 1;
502	# and in project config gitweb.avatar = <provider>;
503	'avatar' => {
504		'sub' => \&feature_avatar,
505		'override' => 0,
506		'default' => ['']},
507
508	# Enable displaying how much time and how many git commands
509	# it took to generate and display page.  Disabled by default.
510	# Project specific override is not supported.
511	'timed' => {
512		'override' => 0,
513		'default' => [0]},
514
515	# Enable turning some links into links to actions which require
516	# JavaScript to run (like 'blame_incremental').  Not enabled by
517	# default.  Project specific override is currently not supported.
518	'javascript-actions' => {
519		'override' => 0,
520		'default' => [0]},
521
522	# Enable and configure ability to change common timezone for dates
523	# in gitweb output via JavaScript.  Enabled by default.
524	# Project specific override is not supported.
525	'javascript-timezone' => {
526		'override' => 0,
527		'default' => [
528			'local',     # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
529			             # or undef to turn off this feature
530			'gitweb_tz', # name of cookie where to store selected timezone
531			'datetime',  # CSS class used to mark up dates for manipulation
532		]},
533
534	# Syntax highlighting support. This is based on Daniel Svensson's
535	# and Sham Chukoury's work in gitweb-xmms2.git.
536	# It requires the 'highlight' program present in $PATH,
537	# and therefore is disabled by default.
538
539	# To enable system wide have in $GITWEB_CONFIG
540	# $feature{'highlight'}{'default'} = [1];
541
542	'highlight' => {
543		'sub' => sub { feature_bool('highlight', @_) },
544		'override' => 0,
545		'default' => [0]},
546
547	# Enable displaying of remote heads in the heads list
548
549	# To enable system wide have in $GITWEB_CONFIG
550	# $feature{'remote_heads'}{'default'} = [1];
551	# To have project specific config enable override in $GITWEB_CONFIG
552	# $feature{'remote_heads'}{'override'} = 1;
553	# and in project config gitweb.remoteheads = 0|1;
554	'remote_heads' => {
555		'sub' => sub { feature_bool('remote_heads', @_) },
556		'override' => 0,
557		'default' => [0]},
558
559	# Enable showing branches under other refs in addition to heads
560
561	# To set system wide extra branch refs have in $GITWEB_CONFIG
562	# $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
563	# To have project specific config enable override in $GITWEB_CONFIG
564	# $feature{'extra-branch-refs'}{'override'} = 1;
565	# and in project config gitweb.extrabranchrefs = dirs of choice
566	# Every directory is separated with whitespace.
567
568	'extra-branch-refs' => {
569		'sub' => \&feature_extra_branch_refs,
570		'override' => 0,
571		'default' => []},
572);
573
574sub gitweb_get_feature {
575	my ($name) = @_;
576	return unless exists $feature{$name};
577	my ($sub, $override, @defaults) = (
578		$feature{$name}{'sub'},
579		$feature{$name}{'override'},
580		@{$feature{$name}{'default'}});
581	# project specific override is possible only if we have project
582	our $git_dir; # global variable, declared later
583	if (!$override || !defined $git_dir) {
584		return @defaults;
585	}
586	if (!defined $sub) {
587		warn "feature $name is not overridable";
588		return @defaults;
589	}
590	return $sub->(@defaults);
591}
592
593# A wrapper to check if a given feature is enabled.
594# With this, you can say
595#
596#   my $bool_feat = gitweb_check_feature('bool_feat');
597#   gitweb_check_feature('bool_feat') or somecode;
598#
599# instead of
600#
601#   my ($bool_feat) = gitweb_get_feature('bool_feat');
602#   (gitweb_get_feature('bool_feat'))[0] or somecode;
603#
604sub gitweb_check_feature {
605	return (gitweb_get_feature(@_))[0];
606}
607
608
609sub feature_bool {
610	my $key = shift;
611	my ($val) = git_get_project_config($key, '--bool');
612
613	if (!defined $val) {
614		return ($_[0]);
615	} elsif ($val eq 'true') {
616		return (1);
617	} elsif ($val eq 'false') {
618		return (0);
619	}
620}
621
622sub feature_snapshot {
623	my (@fmts) = @_;
624
625	my ($val) = git_get_project_config('snapshot');
626
627	if ($val) {
628		@fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
629	}
630
631	return @fmts;
632}
633
634sub feature_patches {
635	my @val = (git_get_project_config('patches', '--int'));
636
637	if (@val) {
638		return @val;
639	}
640
641	return ($_[0]);
642}
643
644sub feature_avatar {
645	my @val = (git_get_project_config('avatar'));
646
647	return @val ? @val : @_;
648}
649
650sub feature_extra_branch_refs {
651	my (@branch_refs) = @_;
652	my $values = git_get_project_config('extrabranchrefs');
653
654	if ($values) {
655		$values = config_to_multi ($values);
656		@branch_refs = ();
657		foreach my $value (@{$values}) {
658			push @branch_refs, split /\s+/, $value;
659		}
660	}
661
662	return @branch_refs;
663}
664
665# checking HEAD file with -e is fragile if the repository was
666# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
667# and then pruned.
668sub check_head_link {
669	my ($dir) = @_;
670	my $headfile = "$dir/HEAD";
671	return ((-e $headfile) ||
672		(-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
673}
674
675sub check_export_ok {
676	my ($dir) = @_;
677	return (check_head_link($dir) &&
678		(!$export_ok || -e "$dir/$export_ok") &&
679		(!$export_auth_hook || $export_auth_hook->($dir)));
680}
681
682# process alternate names for backward compatibility
683# filter out unsupported (unknown) snapshot formats
684sub filter_snapshot_fmts {
685	my @fmts = @_;
686
687	@fmts = map {
688		exists $known_snapshot_format_aliases{$_} ?
689		       $known_snapshot_format_aliases{$_} : $_} @fmts;
690	@fmts = grep {
691		exists $known_snapshot_formats{$_} &&
692		!$known_snapshot_formats{$_}{'disabled'}} @fmts;
693}
694
695sub filter_and_validate_refs {
696	my @refs = @_;
697	my %unique_refs = ();
698
699	foreach my $ref (@refs) {
700		die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
701		# 'heads' are added implicitly in get_branch_refs().
702		$unique_refs{$ref} = 1 if ($ref ne 'heads');
703	}
704	return sort keys %unique_refs;
705}
706
707# If it is set to code reference, it is code that it is to be run once per
708# request, allowing updating configurations that change with each request,
709# while running other code in config file only once.
710#
711# Otherwise, if it is false then gitweb would process config file only once;
712# if it is true then gitweb config would be run for each request.
713our $per_request_config = 1;
714
715# read and parse gitweb config file given by its parameter.
716# returns true on success, false on recoverable error, allowing
717# to chain this subroutine, using first file that exists.
718# dies on errors during parsing config file, as it is unrecoverable.
719sub read_config_file {
720	my $filename = shift;
721	return unless defined $filename;
722	# die if there are errors parsing config file
723	if (-e $filename) {
724		do $filename;
725		die $@ if $@;
726		return 1;
727	}
728	return;
729}
730
731our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
732sub evaluate_gitweb_config {
733	our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
734	our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
735	our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
736
737	# Protect against duplications of file names, to not read config twice.
738	# Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
739	# there possibility of duplication of filename there doesn't matter.
740	$GITWEB_CONFIG = ""        if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
741	$GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
742
743	# Common system-wide settings for convenience.
744	# Those settings can be overridden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
745	read_config_file($GITWEB_CONFIG_COMMON);
746
747	# Use first config file that exists.  This means use the per-instance
748	# GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
749	read_config_file($GITWEB_CONFIG) and return;
750	read_config_file($GITWEB_CONFIG_SYSTEM);
751}
752
753# Get loadavg of system, to compare against $maxload.
754# Currently it requires '/proc/loadavg' present to get loadavg;
755# if it is not present it returns 0, which means no load checking.
756sub get_loadavg {
757	if( -e '/proc/loadavg' ){
758		open my $fd, '<', '/proc/loadavg'
759			or return 0;
760		my @load = split(/\s+/, scalar <$fd>);
761		close $fd;
762
763		# The first three columns measure CPU and IO utilization of the last one,
764		# five, and 10 minute periods.  The fourth column shows the number of
765		# currently running processes and the total number of processes in the m/n
766		# format.  The last column displays the last process ID used.
767		return $load[0] || 0;
768	}
769	# additional checks for load average should go here for things that don't export
770	# /proc/loadavg
771
772	return 0;
773}
774
775# version of the core git binary
776our $git_version;
777sub evaluate_git_version {
778	our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
779	$number_of_git_cmds++;
780}
781
782sub check_loadavg {
783	if (defined $maxload && get_loadavg() > $maxload) {
784		die_error(503, "The load average on the server is too high");
785	}
786}
787
788# ======================================================================
789# input validation and dispatch
790
791# Various hash size-related values.
792my $sha1_len = 40;
793my $sha256_extra_len = 24;
794my $sha256_len = $sha1_len + $sha256_extra_len;
795
796# A regex matching $len hex characters. $len may be a range (e.g. 7,64).
797sub oid_nlen_regex {
798	my $len = shift;
799	my $hchr = qr/[0-9a-fA-F]/;
800	return qr/(?:(?:$hchr){$len})/;
801}
802
803# A regex matching two sets of $nlen hex characters, prefixed by the literal
804# string $prefix and with the literal string $infix between them.
805sub oid_nlen_prefix_infix_regex {
806	my $nlen = shift;
807	my $prefix = shift;
808	my $infix = shift;
809
810	my $rx = oid_nlen_regex($nlen);
811
812	return qr/^\Q$prefix\E$rx\Q$infix\E$rx$/;
813}
814
815# A regex matching a valid object ID.
816our $oid_regex;
817{
818	my $x = oid_nlen_regex($sha1_len);
819	my $y = oid_nlen_regex($sha256_extra_len);
820	$oid_regex = qr/(?:$x(?:$y)?)/;
821}
822
823# input parameters can be collected from a variety of sources (presently, CGI
824# and PATH_INFO), so we define an %input_params hash that collects them all
825# together during validation: this allows subsequent uses (e.g. href()) to be
826# agnostic of the parameter origin
827
828our %input_params = ();
829
830# input parameters are stored with the long parameter name as key. This will
831# also be used in the href subroutine to convert parameters to their CGI
832# equivalent, and since the href() usage is the most frequent one, we store
833# the name -> CGI key mapping here, instead of the reverse.
834#
835# XXX: Warning: If you touch this, check the search form for updating,
836# too.
837
838our @cgi_param_mapping = (
839	project => "p",
840	action => "a",
841	file_name => "f",
842	file_parent => "fp",
843	hash => "h",
844	hash_parent => "hp",
845	hash_base => "hb",
846	hash_parent_base => "hpb",
847	page => "pg",
848	order => "o",
849	searchtext => "s",
850	searchtype => "st",
851	snapshot_format => "sf",
852	extra_options => "opt",
853	search_use_regexp => "sr",
854	ctag => "by_tag",
855	diff_style => "ds",
856	project_filter => "pf",
857	# this must be last entry (for manipulation from JavaScript)
858	javascript => "js"
859);
860our %cgi_param_mapping = @cgi_param_mapping;
861
862# we will also need to know the possible actions, for validation
863our %actions = (
864	"blame" => \&git_blame,
865	"blame_incremental" => \&git_blame_incremental,
866	"blame_data" => \&git_blame_data,
867	"blobdiff" => \&git_blobdiff,
868	"blobdiff_plain" => \&git_blobdiff_plain,
869	"blob" => \&git_blob,
870	"blob_plain" => \&git_blob_plain,
871	"commitdiff" => \&git_commitdiff,
872	"commitdiff_plain" => \&git_commitdiff_plain,
873	"commit" => \&git_commit,
874	"forks" => \&git_forks,
875	"heads" => \&git_heads,
876	"history" => \&git_history,
877	"log" => \&git_log,
878	"patch" => \&git_patch,
879	"patches" => \&git_patches,
880	"remotes" => \&git_remotes,
881	"rss" => \&git_rss,
882	"atom" => \&git_atom,
883	"search" => \&git_search,
884	"search_help" => \&git_search_help,
885	"shortlog" => \&git_shortlog,
886	"summary" => \&git_summary,
887	"tag" => \&git_tag,
888	"tags" => \&git_tags,
889	"tree" => \&git_tree,
890	"snapshot" => \&git_snapshot,
891	"object" => \&git_object,
892	# those below don't need $project
893	"opml" => \&git_opml,
894	"project_list" => \&git_project_list,
895	"project_index" => \&git_project_index,
896);
897
898# finally, we have the hash of allowed extra_options for the commands that
899# allow them
900our %allowed_options = (
901	"--no-merges" => [ qw(rss atom log shortlog history) ],
902);
903
904# fill %input_params with the CGI parameters. All values except for 'opt'
905# should be single values, but opt can be an array. We should probably
906# build an array of parameters that can be multi-valued, but since for the time
907# being it's only this one, we just single it out
908sub evaluate_query_params {
909	our $cgi;
910
911	while (my ($name, $symbol) = each %cgi_param_mapping) {
912		if ($symbol eq 'opt') {
913			$input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
914		} else {
915			$input_params{$name} = decode_utf8($cgi->param($symbol));
916		}
917	}
918}
919
920# now read PATH_INFO and update the parameter list for missing parameters
921sub evaluate_path_info {
922	return if defined $input_params{'project'};
923	return if !$path_info;
924	$path_info =~ s,^/+,,;
925	return if !$path_info;
926
927	# find which part of PATH_INFO is project
928	my $project = $path_info;
929	$project =~ s,/+$,,;
930	while ($project && !check_head_link("$projectroot/$project")) {
931		$project =~ s,/*[^/]*$,,;
932	}
933	return unless $project;
934	$input_params{'project'} = $project;
935
936	# do not change any parameters if an action is given using the query string
937	return if $input_params{'action'};
938	$path_info =~ s,^\Q$project\E/*,,;
939
940	# next, check if we have an action
941	my $action = $path_info;
942	$action =~ s,/.*$,,;
943	if (exists $actions{$action}) {
944		$path_info =~ s,^$action/*,,;
945		$input_params{'action'} = $action;
946	}
947
948	# list of actions that want hash_base instead of hash, but can have no
949	# pathname (f) parameter
950	my @wants_base = (
951		'tree',
952		'history',
953	);
954
955	# we want to catch, among others
956	# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
957	my ($parentrefname, $parentpathname, $refname, $pathname) =
958		($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
959
960	# first, analyze the 'current' part
961	if (defined $pathname) {
962		# we got "branch:filename" or "branch:dir/"
963		# we could use git_get_type(branch:pathname), but:
964		# - it needs $git_dir
965		# - it does a git() call
966		# - the convention of terminating directories with a slash
967		#   makes it superfluous
968		# - embedding the action in the PATH_INFO would make it even
969		#   more superfluous
970		$pathname =~ s,^/+,,;
971		if (!$pathname || substr($pathname, -1) eq "/") {
972			$input_params{'action'} ||= "tree";
973			$pathname =~ s,/$,,;
974		} else {
975			# the default action depends on whether we had parent info
976			# or not
977			if ($parentrefname) {
978				$input_params{'action'} ||= "blobdiff_plain";
979			} else {
980				$input_params{'action'} ||= "blob_plain";
981			}
982		}
983		$input_params{'hash_base'} ||= $refname;
984		$input_params{'file_name'} ||= $pathname;
985	} elsif (defined $refname) {
986		# we got "branch". In this case we have to choose if we have to
987		# set hash or hash_base.
988		#
989		# Most of the actions without a pathname only want hash to be
990		# set, except for the ones specified in @wants_base that want
991		# hash_base instead. It should also be noted that hand-crafted
992		# links having 'history' as an action and no pathname or hash
993		# set will fail, but that happens regardless of PATH_INFO.
994		if (defined $parentrefname) {
995			# if there is parent let the default be 'shortlog' action
996			# (for http://git.example.com/repo.git/A..B links); if there
997			# is no parent, dispatch will detect type of object and set
998			# action appropriately if required (if action is not set)
999			$input_params{'action'} ||= "shortlog";
1000		}
1001		if ($input_params{'action'} &&
1002		    grep { $_ eq $input_params{'action'} } @wants_base) {
1003			$input_params{'hash_base'} ||= $refname;
1004		} else {
1005			$input_params{'hash'} ||= $refname;
1006		}
1007	}
1008
1009	# next, handle the 'parent' part, if present
1010	if (defined $parentrefname) {
1011		# a missing pathspec defaults to the 'current' filename, allowing e.g.
1012		# someproject/blobdiff/oldrev..newrev:/filename
1013		if ($parentpathname) {
1014			$parentpathname =~ s,^/+,,;
1015			$parentpathname =~ s,/$,,;
1016			$input_params{'file_parent'} ||= $parentpathname;
1017		} else {
1018			$input_params{'file_parent'} ||= $input_params{'file_name'};
1019		}
1020		# we assume that hash_parent_base is wanted if a path was specified,
1021		# or if the action wants hash_base instead of hash
1022		if (defined $input_params{'file_parent'} ||
1023			grep { $_ eq $input_params{'action'} } @wants_base) {
1024			$input_params{'hash_parent_base'} ||= $parentrefname;
1025		} else {
1026			$input_params{'hash_parent'} ||= $parentrefname;
1027		}
1028	}
1029
1030	# for the snapshot action, we allow URLs in the form
1031	# $project/snapshot/$hash.ext
1032	# where .ext determines the snapshot and gets removed from the
1033	# passed $refname to provide the $hash.
1034	#
1035	# To be able to tell that $refname includes the format extension, we
1036	# require the following two conditions to be satisfied:
1037	# - the hash input parameter MUST have been set from the $refname part
1038	#   of the URL (i.e. they must be equal)
1039	# - the snapshot format MUST NOT have been defined already (e.g. from
1040	#   CGI parameter sf)
1041	# It's also useless to try any matching unless $refname has a dot,
1042	# so we check for that too
1043	if (defined $input_params{'action'} &&
1044		$input_params{'action'} eq 'snapshot' &&
1045		defined $refname && index($refname, '.') != -1 &&
1046		$refname eq $input_params{'hash'} &&
1047		!defined $input_params{'snapshot_format'}) {
1048		# We loop over the known snapshot formats, checking for
1049		# extensions. Allowed extensions are both the defined suffix
1050		# (which includes the initial dot already) and the snapshot
1051		# format key itself, with a prepended dot
1052		while (my ($fmt, $opt) = each %known_snapshot_formats) {
1053			my $hash = $refname;
1054			unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1055				next;
1056			}
1057			my $sfx = $1;
1058			# a valid suffix was found, so set the snapshot format
1059			# and reset the hash parameter
1060			$input_params{'snapshot_format'} = $fmt;
1061			$input_params{'hash'} = $hash;
1062			# we also set the format suffix to the one requested
1063			# in the URL: this way a request for e.g. .tgz returns
1064			# a .tgz instead of a .tar.gz
1065			$known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1066			last;
1067		}
1068	}
1069}
1070
1071our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1072     $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1073     $searchtext, $search_regexp, $project_filter);
1074sub evaluate_and_validate_params {
1075	our $action = $input_params{'action'};
1076	if (defined $action) {
1077		if (!is_valid_action($action)) {
1078			die_error(400, "Invalid action parameter");
1079		}
1080	}
1081
1082	# parameters which are pathnames
1083	our $project = $input_params{'project'};
1084	if (defined $project) {
1085		if (!is_valid_project($project)) {
1086			undef $project;
1087			die_error(404, "No such project");
1088		}
1089	}
1090
1091	our $project_filter = $input_params{'project_filter'};
1092	if (defined $project_filter) {
1093		if (!is_valid_pathname($project_filter)) {
1094			die_error(404, "Invalid project_filter parameter");
1095		}
1096	}
1097
1098	our $file_name = $input_params{'file_name'};
1099	if (defined $file_name) {
1100		if (!is_valid_pathname($file_name)) {
1101			die_error(400, "Invalid file parameter");
1102		}
1103	}
1104
1105	our $file_parent = $input_params{'file_parent'};
1106	if (defined $file_parent) {
1107		if (!is_valid_pathname($file_parent)) {
1108			die_error(400, "Invalid file parent parameter");
1109		}
1110	}
1111
1112	# parameters which are refnames
1113	our $hash = $input_params{'hash'};
1114	if (defined $hash) {
1115		if (!is_valid_refname($hash)) {
1116			die_error(400, "Invalid hash parameter");
1117		}
1118	}
1119
1120	our $hash_parent = $input_params{'hash_parent'};
1121	if (defined $hash_parent) {
1122		if (!is_valid_refname($hash_parent)) {
1123			die_error(400, "Invalid hash parent parameter");
1124		}
1125	}
1126
1127	our $hash_base = $input_params{'hash_base'};
1128	if (defined $hash_base) {
1129		if (!is_valid_refname($hash_base)) {
1130			die_error(400, "Invalid hash base parameter");
1131		}
1132	}
1133
1134	our @extra_options = @{$input_params{'extra_options'}};
1135	# @extra_options is always defined, since it can only be (currently) set from
1136	# CGI, and $cgi->param() returns the empty array in array context if the param
1137	# is not set
1138	foreach my $opt (@extra_options) {
1139		if (not exists $allowed_options{$opt}) {
1140			die_error(400, "Invalid option parameter");
1141		}
1142		if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1143			die_error(400, "Invalid option parameter for this action");
1144		}
1145	}
1146
1147	our $hash_parent_base = $input_params{'hash_parent_base'};
1148	if (defined $hash_parent_base) {
1149		if (!is_valid_refname($hash_parent_base)) {
1150			die_error(400, "Invalid hash parent base parameter");
1151		}
1152	}
1153
1154	# other parameters
1155	our $page = $input_params{'page'};
1156	if (defined $page) {
1157		if ($page =~ m/[^0-9]/) {
1158			die_error(400, "Invalid page parameter");
1159		}
1160	}
1161
1162	our $searchtype = $input_params{'searchtype'};
1163	if (defined $searchtype) {
1164		if ($searchtype =~ m/[^a-z]/) {
1165			die_error(400, "Invalid searchtype parameter");
1166		}
1167	}
1168
1169	our $search_use_regexp = $input_params{'search_use_regexp'};
1170
1171	our $searchtext = $input_params{'searchtext'};
1172	our $search_regexp = undef;
1173	if (defined $searchtext) {
1174		if (length($searchtext) < 2) {
1175			die_error(403, "At least two characters are required for search parameter");
1176		}
1177		if ($search_use_regexp) {
1178			$search_regexp = $searchtext;
1179			if (!eval { qr/$search_regexp/; 1; }) {
1180				(my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1181				die_error(400, "Invalid search regexp '$search_regexp'",
1182				          esc_html($error));
1183			}
1184		} else {
1185			$search_regexp = quotemeta $searchtext;
1186		}
1187	}
1188}
1189
1190# path to the current git repository
1191our $git_dir;
1192sub evaluate_git_dir {
1193	our $git_dir = "$projectroot/$project" if $project;
1194}
1195
1196our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1197sub configure_gitweb_features {
1198	# list of supported snapshot formats
1199	our @snapshot_fmts = gitweb_get_feature('snapshot');
1200	@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1201
1202	our ($git_avatar) = gitweb_get_feature('avatar');
1203	$git_avatar = '' unless $git_avatar =~ /^(?:gravatar|picon)$/s;
1204
1205	our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
1206	@extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
1207}
1208
1209sub get_branch_refs {
1210	return ('heads', @extra_branch_refs);
1211}
1212
1213# custom error handler: 'die <message>' is Internal Server Error
1214sub handle_errors_html {
1215	my $msg = shift; # it is already HTML escaped
1216
1217	# to avoid infinite loop where error occurs in die_error,
1218	# change handler to default handler, disabling handle_errors_html
1219	set_message("Error occurred when inside die_error:\n$msg");
1220
1221	# you cannot jump out of die_error when called as error handler;
1222	# the subroutine set via CGI::Carp::set_message is called _after_
1223	# HTTP headers are already written, so it cannot write them itself
1224	die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1225}
1226set_message(\&handle_errors_html);
1227
1228# dispatch
1229sub dispatch {
1230	if (!defined $action) {
1231		if (defined $hash) {
1232			$action = git_get_type($hash);
1233			$action or die_error(404, "Object does not exist");
1234		} elsif (defined $hash_base && defined $file_name) {
1235			$action = git_get_type("$hash_base:$file_name");
1236			$action or die_error(404, "File or directory does not exist");
1237		} elsif (defined $project) {
1238			$action = 'summary';
1239		} else {
1240			$action = 'project_list';
1241		}
1242	}
1243	if (!defined($actions{$action})) {
1244		die_error(400, "Unknown action");
1245	}
1246	if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1247	    !$project) {
1248		die_error(400, "Project needed");
1249	}
1250	$actions{$action}->();
1251}
1252
1253sub reset_timer {
1254	our $t0 = [ gettimeofday() ]
1255		if defined $t0;
1256	our $number_of_git_cmds = 0;
1257}
1258
1259our $first_request = 1;
1260sub run_request {
1261	reset_timer();
1262
1263	evaluate_uri();
1264	if ($first_request) {
1265		evaluate_gitweb_config();
1266		evaluate_git_version();
1267	}
1268	if ($per_request_config) {
1269		if (ref($per_request_config) eq 'CODE') {
1270			$per_request_config->();
1271		} elsif (!$first_request) {
1272			evaluate_gitweb_config();
1273		}
1274	}
1275	check_loadavg();
1276
1277	# $projectroot and $projects_list might be set in gitweb config file
1278	$projects_list ||= $projectroot;
1279
1280	evaluate_query_params();
1281	evaluate_path_info();
1282	evaluate_and_validate_params();
1283	evaluate_git_dir();
1284
1285	configure_gitweb_features();
1286
1287	dispatch();
1288}
1289
1290our $is_last_request = sub { 1 };
1291our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1292our $CGI = 'CGI';
1293our $cgi;
1294sub configure_as_fcgi {
1295	require CGI::Fast;
1296	our $CGI = 'CGI::Fast';
1297
1298	my $request_number = 0;
1299	# let each child service 100 requests
1300	our $is_last_request = sub { ++$request_number > 100 };
1301}
1302sub evaluate_argv {
1303	my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1304	configure_as_fcgi()
1305		if $script_name =~ /\.fcgi$/;
1306
1307	return unless (@ARGV);
1308
1309	require Getopt::Long;
1310	Getopt::Long::GetOptions(
1311		'fastcgi|fcgi|f' => \&configure_as_fcgi,
1312		'nproc|n=i' => sub {
1313			my ($arg, $val) = @_;
1314			return unless eval { require FCGI::ProcManager; 1; };
1315			my $proc_manager = FCGI::ProcManager->new({
1316				n_processes => $val,
1317			});
1318			our $pre_listen_hook    = sub { $proc_manager->pm_manage()        };
1319			our $pre_dispatch_hook  = sub { $proc_manager->pm_pre_dispatch()  };
1320			our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1321		},
1322	);
1323}
1324
1325sub run {
1326	evaluate_argv();
1327
1328	$first_request = 1;
1329	$pre_listen_hook->()
1330		if $pre_listen_hook;
1331
1332 REQUEST:
1333	while ($cgi = $CGI->new()) {
1334		$pre_dispatch_hook->()
1335			if $pre_dispatch_hook;
1336
1337		run_request();
1338
1339		$post_dispatch_hook->()
1340			if $post_dispatch_hook;
1341		$first_request = 0;
1342
1343		last REQUEST if ($is_last_request->());
1344	}
1345
1346 DONE_GITWEB:
1347	1;
1348}
1349
1350run();
1351
1352if (defined caller) {
1353	# wrapped in a subroutine processing requests,
1354	# e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1355	return;
1356} else {
1357	# pure CGI script, serving single request
1358	exit;
1359}
1360
1361## ======================================================================
1362## action links
1363
1364# possible values of extra options
1365# -full => 0|1      - use absolute/full URL ($my_uri/$my_url as base)
1366# -replay => 1      - start from a current view (replay with modifications)
1367# -path_info => 0|1 - don't use/use path_info URL (if possible)
1368# -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1369sub href {
1370	my %params = @_;
1371	# default is to use -absolute url() i.e. $my_uri
1372	my $href = $params{-full} ? $my_url : $my_uri;
1373
1374	# implicit -replay, must be first of implicit params
1375	$params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1376
1377	$params{'project'} = $project unless exists $params{'project'};
1378
1379	if ($params{-replay}) {
1380		while (my ($name, $symbol) = each %cgi_param_mapping) {
1381			if (!exists $params{$name}) {
1382				$params{$name} = $input_params{$name};
1383			}
1384		}
1385	}
1386
1387	my $use_pathinfo = gitweb_check_feature('pathinfo');
1388	if (defined $params{'project'} &&
1389	    (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1390		# try to put as many parameters as possible in PATH_INFO:
1391		#   - project name
1392		#   - action
1393		#   - hash_parent or hash_parent_base:/file_parent
1394		#   - hash or hash_base:/filename
1395		#   - the snapshot_format as an appropriate suffix
1396
1397		# When the script is the root DirectoryIndex for the domain,
1398		# $href here would be something like http://gitweb.example.com/
1399		# Thus, we strip any trailing / from $href, to spare us double
1400		# slashes in the final URL
1401		$href =~ s,/$,,;
1402
1403		# Then add the project name, if present
1404		$href .= "/".esc_path_info($params{'project'});
1405		delete $params{'project'};
1406
1407		# since we destructively absorb parameters, we keep this
1408		# boolean that remembers if we're handling a snapshot
1409		my $is_snapshot = $params{'action'} eq 'snapshot';
1410
1411		# Summary just uses the project path URL, any other action is
1412		# added to the URL
1413		if (defined $params{'action'}) {
1414			$href .= "/".esc_path_info($params{'action'})
1415				unless $params{'action'} eq 'summary';
1416			delete $params{'action'};
1417		}
1418
1419		# Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1420		# stripping nonexistent or useless pieces
1421		$href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1422			|| $params{'hash_parent'} || $params{'hash'});
1423		if (defined $params{'hash_base'}) {
1424			if (defined $params{'hash_parent_base'}) {
1425				$href .= esc_path_info($params{'hash_parent_base'});
1426				# skip the file_parent if it's the same as the file_name
1427				if (defined $params{'file_parent'}) {
1428					if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1429						delete $params{'file_parent'};
1430					} elsif ($params{'file_parent'} !~ /\.\./) {
1431						$href .= ":/".esc_path_info($params{'file_parent'});
1432						delete $params{'file_parent'};
1433					}
1434				}
1435				$href .= "..";
1436				delete $params{'hash_parent'};
1437				delete $params{'hash_parent_base'};
1438			} elsif (defined $params{'hash_parent'}) {
1439				$href .= esc_path_info($params{'hash_parent'}). "..";
1440				delete $params{'hash_parent'};
1441			}
1442
1443			$href .= esc_path_info($params{'hash_base'});
1444			if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1445				$href .= ":/".esc_path_info($params{'file_name'});
1446				delete $params{'file_name'};
1447			}
1448			delete $params{'hash'};
1449			delete $params{'hash_base'};
1450		} elsif (defined $params{'hash'}) {
1451			$href .= esc_path_info($params{'hash'});
1452			delete $params{'hash'};
1453		}
1454
1455		# If the action was a snapshot, we can absorb the
1456		# snapshot_format parameter too
1457		if ($is_snapshot) {
1458			my $fmt = $params{'snapshot_format'};
1459			# snapshot_format should always be defined when href()
1460			# is called, but just in case some code forgets, we
1461			# fall back to the default
1462			$fmt ||= $snapshot_fmts[0];
1463			$href .= $known_snapshot_formats{$fmt}{'suffix'};
1464			delete $params{'snapshot_format'};
1465		}
1466	}
1467
1468	# now encode the parameters explicitly
1469	my @result = ();
1470	for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1471		my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1472		if (defined $params{$name}) {
1473			if (ref($params{$name}) eq "ARRAY") {
1474				foreach my $par (@{$params{$name}}) {
1475					push @result, $symbol . "=" . esc_param($par);
1476				}
1477			} else {
1478				push @result, $symbol . "=" . esc_param($params{$name});
1479			}
1480		}
1481	}
1482	$href .= "?" . join(';', @result) if scalar @result;
1483
1484	# final transformation: trailing spaces must be escaped (URI-encoded)
1485	$href =~ s/(\s+)$/CGI::escape($1)/e;
1486
1487	if ($params{-anchor}) {
1488		$href .= "#".esc_param($params{-anchor});
1489	}
1490
1491	return $href;
1492}
1493
1494
1495## ======================================================================
1496## validation, quoting/unquoting and escaping
1497
1498sub is_valid_action {
1499	my $input = shift;
1500	return undef unless exists $actions{$input};
1501	return 1;
1502}
1503
1504sub is_valid_project {
1505	my $input = shift;
1506
1507	return unless defined $input;
1508	if (!is_valid_pathname($input) ||
1509		!(-d "$projectroot/$input") ||
1510		!check_export_ok("$projectroot/$input") ||
1511		($strict_export && !project_in_list($input))) {
1512		return undef;
1513	} else {
1514		return 1;
1515	}
1516}
1517
1518sub is_valid_pathname {
1519	my $input = shift;
1520
1521	return undef unless defined $input;
1522	# no '.' or '..' as elements of path, i.e. no '.' or '..'
1523	# at the beginning, at the end, and between slashes.
1524	# also this catches doubled slashes
1525	if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1526		return undef;
1527	}
1528	# no null characters
1529	if ($input =~ m!\0!) {
1530		return undef;
1531	}
1532	return 1;
1533}
1534
1535sub is_valid_ref_format {
1536	my $input = shift;
1537
1538	return undef unless defined $input;
1539	# restrictions on ref name according to git-check-ref-format
1540	if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1541		return undef;
1542	}
1543	return 1;
1544}
1545
1546sub is_valid_refname {
1547	my $input = shift;
1548
1549	return undef unless defined $input;
1550	# textual hashes are O.K.
1551	if ($input =~ m/^$oid_regex$/) {
1552		return 1;
1553	}
1554	# it must be correct pathname
1555	is_valid_pathname($input) or return undef;
1556	# check git-check-ref-format restrictions
1557	is_valid_ref_format($input) or return undef;
1558	return 1;
1559}
1560
1561# decode sequences of octets in utf8 into Perl's internal form,
1562# which is utf-8 with utf8 flag set if needed.  gitweb writes out
1563# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1564sub to_utf8 {
1565	my $str = shift;
1566	return undef unless defined $str;
1567
1568	if (utf8::is_utf8($str) || utf8::decode($str)) {
1569		return $str;
1570	} else {
1571		return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1572	}
1573}
1574
1575# quote unsafe chars, but keep the slash, even when it's not
1576# correct, but quoted slashes look too horrible in bookmarks
1577sub esc_param {
1578	my $str = shift;
1579	return undef unless defined $str;
1580	$str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1581	$str =~ s/ /\+/g;
1582	return $str;
1583}
1584
1585# the quoting rules for path_info fragment are slightly different
1586sub esc_path_info {
1587	my $str = shift;
1588	return undef unless defined $str;
1589
1590	# path_info doesn't treat '+' as space (specially), but '?' must be escaped
1591	$str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1592
1593	return $str;
1594}
1595
1596# quote unsafe chars in whole URL, so some characters cannot be quoted
1597sub esc_url {
1598	my $str = shift;
1599	return undef unless defined $str;
1600	$str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1601	$str =~ s/ /\+/g;
1602	return $str;
1603}
1604
1605# quote unsafe characters in HTML attributes
1606sub esc_attr {
1607
1608	# for XHTML conformance escaping '"' to '&quot;' is not enough
1609	return esc_html(@_);
1610}
1611
1612# replace invalid utf8 character with SUBSTITUTION sequence
1613sub esc_html {
1614	my $str = shift;
1615	my %opts = @_;
1616
1617	return undef unless defined $str;
1618
1619	$str = to_utf8($str);
1620	$str = $cgi->escapeHTML($str);
1621	if ($opts{'-nbsp'}) {
1622		$str =~ s/ /&nbsp;/g;
1623	}
1624	$str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1625	return $str;
1626}
1627
1628# quote control characters and escape filename to HTML
1629sub esc_path {
1630	my $str = shift;
1631	my %opts = @_;
1632
1633	return undef unless defined $str;
1634
1635	$str = to_utf8($str);
1636	$str = $cgi->escapeHTML($str);
1637	if ($opts{'-nbsp'}) {
1638		$str =~ s/ /&nbsp;/g;
1639	}
1640	$str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1641	return $str;
1642}
1643
1644# Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0)
1645sub sanitize {
1646	my $str = shift;
1647
1648	return undef unless defined $str;
1649
1650	$str = to_utf8($str);
1651	$str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
1652	return $str;
1653}
1654
1655# Make control characters "printable", using character escape codes (CEC)
1656sub quot_cec {
1657	my $cntrl = shift;
1658	my %opts = @_;
1659	my %es = ( # character escape codes, aka escape sequences
1660		"\t" => '\t',   # tab             (HT)
1661		"\n" => '\n',   # line feed       (LF)
1662		"\r" => '\r',   # carriage return (CR)
1663		"\f" => '\f',   # form feed       (FF)
1664		"\b" => '\b',   # backspace       (BS)
1665		"\a" => '\a',   # alarm (bell)    (BEL)
1666		"\e" => '\e',   # escape          (ESC)
1667		"\013" => '\v', # vertical tab    (VT)
1668		"\000" => '\0', # nul character   (NUL)
1669	);
1670	my $chr = ( (exists $es{$cntrl})
1671		    ? $es{$cntrl}
1672		    : sprintf('\%2x', ord($cntrl)) );
1673	if ($opts{-nohtml}) {
1674		return $chr;
1675	} else {
1676		return "<span class=\"cntrl\">$chr</span>";
1677	}
1678}
1679
1680# Alternatively use unicode control pictures codepoints,
1681# Unicode "printable representation" (PR)
1682sub quot_upr {
1683	my $cntrl = shift;
1684	my %opts = @_;
1685
1686	my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1687	if ($opts{-nohtml}) {
1688		return $chr;
1689	} else {
1690		return "<span class=\"cntrl\">$chr</span>";
1691	}
1692}
1693
1694# git may return quoted and escaped filenames
1695sub unquote {
1696	my $str = shift;
1697
1698	sub unq {
1699		my $seq = shift;
1700		my %es = ( # character escape codes, aka escape sequences
1701			't' => "\t",   # tab            (HT, TAB)
1702			'n' => "\n",   # newline        (NL)
1703			'r' => "\r",   # return         (CR)
1704			'f' => "\f",   # form feed      (FF)
1705			'b' => "\b",   # backspace      (BS)
1706			'a' => "\a",   # alarm (bell)   (BEL)
1707			'e' => "\e",   # escape         (ESC)
1708			'v' => "\013", # vertical tab   (VT)
1709		);
1710
1711		if ($seq =~ m/^[0-7]{1,3}$/) {
1712			# octal char sequence
1713			return chr(oct($seq));
1714		} elsif (exists $es{$seq}) {
1715			# C escape sequence, aka character escape code
1716			return $es{$seq};
1717		}
1718		# quoted ordinary character
1719		return $seq;
1720	}
1721
1722	if ($str =~ m/^"(.*)"$/) {
1723		# needs unquoting
1724		$str = $1;
1725		$str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1726	}
1727	return $str;
1728}
1729
1730# escape tabs (convert tabs to spaces)
1731sub untabify {
1732	my $line = shift;
1733
1734	while ((my $pos = index($line, "\t")) != -1) {
1735		if (my $count = (8 - ($pos % 8))) {
1736			my $spaces = ' ' x $count;
1737			$line =~ s/\t/$spaces/;
1738		}
1739	}
1740
1741	return $line;
1742}
1743
1744sub project_in_list {
1745	my $project = shift;
1746	my @list = git_get_projects_list();
1747	return @list && scalar(grep { $_->{'path'} eq $project } @list);
1748}
1749
1750## ----------------------------------------------------------------------
1751## HTML aware string manipulation
1752
1753# Try to chop given string on a word boundary between position
1754# $len and $len+$add_len. If there is no word boundary there,
1755# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1756# (marking chopped part) would be longer than given string.
1757sub chop_str {
1758	my $str = shift;
1759	my $len = shift;
1760	my $add_len = shift || 10;
1761	my $where = shift || 'right'; # 'left' | 'center' | 'right'
1762
1763	# Make sure perl knows it is utf8 encoded so we don't
1764	# cut in the middle of a utf8 multibyte char.
1765	$str = to_utf8($str);
1766
1767	# allow only $len chars, but don't cut a word if it would fit in $add_len
1768	# if it doesn't fit, cut it if it's still longer than the dots we would add
1769	# remove chopped character entities entirely
1770
1771	# when chopping in the middle, distribute $len into left and right part
1772	# return early if chopping wouldn't make string shorter
1773	if ($where eq 'center') {
1774		return $str if ($len + 5 >= length($str)); # filler is length 5
1775		$len = int($len/2);
1776	} else {
1777		return $str if ($len + 4 >= length($str)); # filler is length 4
1778	}
1779
1780	# regexps: ending and beginning with word part up to $add_len
1781	my $endre = qr/.{$len}\w{0,$add_len}/;
1782	my $begre = qr/\w{0,$add_len}.{$len}/;
1783
1784	if ($where eq 'left') {
1785		$str =~ m/^(.*?)($begre)$/;
1786		my ($lead, $body) = ($1, $2);
1787		if (length($lead) > 4) {
1788			$lead = " ...";
1789		}
1790		return "$lead$body";
1791
1792	} elsif ($where eq 'center') {
1793		$str =~ m/^($endre)(.*)$/;
1794		my ($left, $str)  = ($1, $2);
1795		$str =~ m/^(.*?)($begre)$/;
1796		my ($mid, $right) = ($1, $2);
1797		if (length($mid) > 5) {
1798			$mid = " ... ";
1799		}
1800		return "$left$mid$right";
1801
1802	} else {
1803		$str =~ m/^($endre)(.*)$/;
1804		my $body = $1;
1805		my $tail = $2;
1806		if (length($tail) > 4) {
1807			$tail = "... ";
1808		}
1809		return "$body$tail";
1810	}
1811}
1812
1813# takes the same arguments as chop_str, but also wraps a <span> around the
1814# result with a title attribute if it does get chopped. Additionally, the
1815# string is HTML-escaped.
1816sub chop_and_escape_str {
1817	my ($str) = @_;
1818
1819	my $chopped = chop_str(@_);
1820	$str = to_utf8($str);
1821	if ($chopped eq $str) {
1822		return esc_html($chopped);
1823	} else {
1824		$str =~ s/[[:cntrl:]]/?/g;
1825		return $cgi->span({-title=>$str}, esc_html($chopped));
1826	}
1827}
1828
1829# Highlight selected fragments of string, using given CSS class,
1830# and escape HTML.  It is assumed that fragments do not overlap.
1831# Regions are passed as list of pairs (array references).
1832#
1833# Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
1834# '<span class="mark">foo</span>bar'
1835sub esc_html_hl_regions {
1836	my ($str, $css_class, @sel) = @_;
1837	my %opts = grep { ref($_) ne 'ARRAY' } @sel;
1838	@sel     = grep { ref($_) eq 'ARRAY' } @sel;
1839	return esc_html($str, %opts) unless @sel;
1840
1841	my $out = '';
1842	my $pos = 0;
1843
1844	for my $s (@sel) {
1845		my ($begin, $end) = @$s;
1846
1847		# Don't create empty <span> elements.
1848		next if $end <= $begin;
1849
1850		my $escaped = esc_html(substr($str, $begin, $end - $begin),
1851		                       %opts);
1852
1853		$out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
1854			if ($begin - $pos > 0);
1855		$out .= $cgi->span({-class => $css_class}, $escaped);
1856
1857		$pos = $end;
1858	}
1859	$out .= esc_html(substr($str, $pos), %opts)
1860		if ($pos < length($str));
1861
1862	return $out;
1863}
1864
1865# return positions of beginning and end of each match
1866sub matchpos_list {
1867	my ($str, $regexp) = @_;
1868	return unless (defined $str && defined $regexp);
1869
1870	my @matches;
1871	while ($str =~ /$regexp/g) {
1872		push @matches, [$-[0], $+[0]];
1873	}
1874	return @matches;
1875}
1876
1877# highlight match (if any), and escape HTML
1878sub esc_html_match_hl {
1879	my ($str, $regexp) = @_;
1880	return esc_html($str) unless defined $regexp;
1881
1882	my @matches = matchpos_list($str, $regexp);
1883	return esc_html($str) unless @matches;
1884
1885	return esc_html_hl_regions($str, 'match', @matches);
1886}
1887
1888
1889# highlight match (if any) of shortened string, and escape HTML
1890sub esc_html_match_hl_chopped {
1891	my ($str, $chopped, $regexp) = @_;
1892	return esc_html_match_hl($str, $regexp) unless defined $chopped;
1893
1894	my @matches = matchpos_list($str, $regexp);
1895	return esc_html($chopped) unless @matches;
1896
1897	# filter matches so that we mark chopped string
1898	my $tail = "... "; # see chop_str
1899	unless ($chopped =~ s/\Q$tail\E$//) {
1900		$tail = '';
1901	}
1902	my $chop_len = length($chopped);
1903	my $tail_len = length($tail);
1904	my @filtered;
1905
1906	for my $m (@matches) {
1907		if ($m->[0] > $chop_len) {
1908			push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
1909			last;
1910		} elsif ($m->[1] > $chop_len) {
1911			push @filtered, [ $m->[0], $chop_len + $tail_len ];
1912			last;
1913		}
1914		push @filtered, $m;
1915	}
1916
1917	return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
1918}
1919
1920## ----------------------------------------------------------------------
1921## functions returning short strings
1922
1923# CSS class for given age value (in seconds)
1924sub age_class {
1925	my $age = shift;
1926
1927	if (!defined $age) {
1928		return "noage";
1929	} elsif ($age < 60*60*2) {
1930		return "age0";
1931	} elsif ($age < 60*60*24*2) {
1932		return "age1";
1933	} else {
1934		return "age2";
1935	}
1936}
1937
1938# convert age in seconds to "nn units ago" string
1939sub age_string {
1940	my $age = shift;
1941	my $age_str;
1942
1943	if ($age > 60*60*24*365*2) {
1944		$age_str = (int $age/60/60/24/365);
1945		$age_str .= " years ago";
1946	} elsif ($age > 60*60*24*(365/12)*2) {
1947		$age_str = int $age/60/60/24/(365/12);
1948		$age_str .= " months ago";
1949	} elsif ($age > 60*60*24*7*2) {
1950		$age_str = int $age/60/60/24/7;
1951		$age_str .= " weeks ago";
1952	} elsif ($age > 60*60*24*2) {
1953		$age_str = int $age/60/60/24;
1954		$age_str .= " days ago";
1955	} elsif ($age > 60*60*2) {
1956		$age_str = int $age/60/60;
1957		$age_str .= " hours ago";
1958	} elsif ($age > 60*2) {
1959		$age_str = int $age/60;
1960		$age_str .= " min ago";
1961	} elsif ($age > 2) {
1962		$age_str = int $age;
1963		$age_str .= " sec ago";
1964	} else {
1965		$age_str .= " right now";
1966	}
1967	return $age_str;
1968}
1969
1970use constant {
1971	S_IFINVALID => 0030000,
1972	S_IFGITLINK => 0160000,
1973};
1974
1975# submodule/subproject, a commit object reference
1976sub S_ISGITLINK {
1977	my $mode = shift;
1978
1979	return (($mode & S_IFMT) == S_IFGITLINK)
1980}
1981
1982# convert file mode in octal to symbolic file mode string
1983sub mode_str {
1984	my $mode = oct shift;
1985
1986	if (S_ISGITLINK($mode)) {
1987		return 'm---------';
1988	} elsif (S_ISDIR($mode & S_IFMT)) {
1989		return 'drwxr-xr-x';
1990	} elsif (S_ISLNK($mode)) {
1991		return 'lrwxrwxrwx';
1992	} elsif (S_ISREG($mode)) {
1993		# git cares only about the executable bit
1994		if ($mode & S_IXUSR) {
1995			return '-rwxr-xr-x';
1996		} else {
1997			return '-rw-r--r--';
1998		};
1999	} else {
2000		return '----------';
2001	}
2002}
2003
2004# convert file mode in octal to file type string
2005sub file_type {
2006	my $mode = shift;
2007
2008	if ($mode !~ m/^[0-7]+$/) {
2009		return $mode;
2010	} else {
2011		$mode = oct $mode;
2012	}
2013
2014	if (S_ISGITLINK($mode)) {
2015		return "submodule";
2016	} elsif (S_ISDIR($mode & S_IFMT)) {
2017		return "directory";
2018	} elsif (S_ISLNK($mode)) {
2019		return "symlink";
2020	} elsif (S_ISREG($mode)) {
2021		return "file";
2022	} else {
2023		return "unknown";
2024	}
2025}
2026
2027# convert file mode in octal to file type description string
2028sub file_type_long {
2029	my $mode = shift;
2030
2031	if ($mode !~ m/^[0-7]+$/) {
2032		return $mode;
2033	} else {
2034		$mode = oct $mode;
2035	}
2036
2037	if (S_ISGITLINK($mode)) {
2038		return "submodule";
2039	} elsif (S_ISDIR($mode & S_IFMT)) {
2040		return "directory";
2041	} elsif (S_ISLNK($mode)) {
2042		return "symlink";
2043	} elsif (S_ISREG($mode)) {
2044		if ($mode & S_IXUSR) {
2045			return "executable";
2046		} else {
2047			return "file";
2048		};
2049	} else {
2050		return "unknown";
2051	}
2052}
2053
2054
2055## ----------------------------------------------------------------------
2056## functions returning short HTML fragments, or transforming HTML fragments
2057## which don't belong to other sections
2058
2059# format line of commit message.
2060sub format_log_line_html {
2061	my $line = shift;
2062
2063	# Potentially abbreviated OID.
2064	my $regex = oid_nlen_regex("7,64");
2065
2066	$line = esc_html($line, -nbsp=>1);
2067	$line =~ s{
2068        \b
2069        (
2070            # The output of "git describe", e.g. v2.10.0-297-gf6727b0
2071            # or hadoop-20160921-113441-20-g094fb7d
2072            (?<!-) # see strbuf_check_tag_ref(). Tags can't start with -
2073            [A-Za-z0-9.-]+
2074            (?!\.) # refs can't end with ".", see check_refname_format()
2075            -g$regex
2076            |
2077            # Just a normal looking Git SHA1
2078	    $regex
2079        )
2080        \b
2081    }{
2082		$cgi->a({-href => href(action=>"object", hash=>$1),
2083					-class => "text"}, $1);
2084	}egx;
2085
2086	return $line;
2087}
2088
2089# format marker of refs pointing to given object
2090
2091# the destination action is chosen based on object type and current context:
2092# - for annotated tags, we choose the tag view unless it's the current view
2093#   already, in which case we go to shortlog view
2094# - for other refs, we keep the current view if we're in history, shortlog or
2095#   log view, and select shortlog otherwise
2096sub format_ref_marker {
2097	my ($refs, $id) = @_;
2098	my $markers = '';
2099
2100	if (defined $refs->{$id}) {
2101		foreach my $ref (@{$refs->{$id}}) {
2102			# this code exploits the fact that non-lightweight tags are the
2103			# only indirect objects, and that they are the only objects for which
2104			# we want to use tag instead of shortlog as action
2105			my ($type, $name) = qw();
2106			my $indirect = ($ref =~ s/\^\{\}$//);
2107			# e.g. tags/v2.6.11 or heads/next
2108			if ($ref =~ m!^(.*?)s?/(.*)$!) {
2109				$type = $1;
2110				$name = $2;
2111			} else {
2112				$type = "ref";
2113				$name = $ref;
2114			}
2115
2116			my $class = $type;
2117			$class .= " indirect" if $indirect;
2118
2119			my $dest_action = "shortlog";
2120
2121			if ($indirect) {
2122				$dest_action = "tag" unless $action eq "tag";
2123			} elsif ($action =~ /^(history|(short)?log)$/) {
2124				$dest_action = $action;
2125			}
2126
2127			my $dest = "";
2128			$dest .= "refs/" unless $ref =~ m!^refs/!;
2129			$dest .= $ref;
2130
2131			my $link = $cgi->a({
2132				-href => href(
2133					action=>$dest_action,
2134					hash=>$dest
2135				)}, esc_html($name));
2136
2137			$markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2138				$link . "</span>";
2139		}
2140	}
2141
2142	if ($markers) {
2143		return ' <span class="refs">'. $markers . '</span>';
2144	} else {
2145		return "";
2146	}
2147}
2148
2149# format, perhaps shortened and with markers, title line
2150sub format_subject_html {
2151	my ($long, $short, $href, $extra) = @_;
2152	$extra = '' unless defined($extra);
2153
2154	if (length($short) < length($long)) {
2155		$long =~ s/[[:cntrl:]]/?/g;
2156		return $cgi->a({-href => $href, -class => "list subject",
2157		                -title => to_utf8($long)},
2158		       esc_html($short)) . $extra;
2159	} else {
2160		return $cgi->a({-href => $href, -class => "list subject"},
2161		       esc_html($long)) . $extra;
2162	}
2163}
2164
2165# Rather than recomputing the url for an email multiple times, we cache it
2166# after the first hit. This gives a visible benefit in views where the avatar
2167# for the same email is used repeatedly (e.g. shortlog).
2168# The cache is shared by all avatar engines (currently gravatar only), which
2169# are free to use it as preferred. Since only one avatar engine is used for any
2170# given page, there's no risk for cache conflicts.
2171our %avatar_cache = ();
2172
2173# Compute the picon url for a given email, by using the picon search service over at
2174# http://www.cs.indiana.edu/picons/search.html
2175sub picon_url {
2176	my $email = lc shift;
2177	if (!$avatar_cache{$email}) {
2178		my ($user, $domain) = split('@', $email);
2179		$avatar_cache{$email} =
2180			"//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2181			"$domain/$user/" .
2182			"users+domains+unknown/up/single";
2183	}
2184	return $avatar_cache{$email};
2185}
2186
2187# Compute the gravatar url for a given email, if it's not in the cache already.
2188# Gravatar stores only the part of the URL before the size, since that's the
2189# one computationally more expensive. This also allows reuse of the cache for
2190# different sizes (for this particular engine).
2191sub gravatar_url {
2192	my $email = lc shift;
2193	my $size = shift;
2194	$avatar_cache{$email} ||=
2195		"//www.gravatar.com/avatar/" .
2196			md5_hex($email) . "?s=";
2197	return $avatar_cache{$email} . $size;
2198}
2199
2200# Insert an avatar for the given $email at the given $size if the feature
2201# is enabled.
2202sub git_get_avatar {
2203	my ($email, %opts) = @_;
2204	my $pre_white  = ($opts{-pad_before} ? "&nbsp;" : "");
2205	my $post_white = ($opts{-pad_after}  ? "&nbsp;" : "");
2206	$opts{-size} ||= 'default';
2207	my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2208	my $url = "";
2209	if ($git_avatar eq 'gravatar') {
2210		$url = gravatar_url($email, $size);
2211	} elsif ($git_avatar eq 'picon') {
2212		$url = picon_url($email);
2213	}
2214	# Other providers can be added by extending the if chain, defining $url
2215	# as needed. If no variant puts something in $url, we assume avatars
2216	# are completely disabled/unavailable.
2217	if ($url) {
2218		return $pre_white .
2219		       "<img width=\"$size\" " .
2220		            "class=\"avatar\" " .
2221		            "src=\"".esc_url($url)."\" " .
2222			    "alt=\"\" " .
2223		       "/>" . $post_white;
2224	} else {
2225		return "";
2226	}
2227}
2228
2229sub format_search_author {
2230	my ($author, $searchtype, $displaytext) = @_;
2231	my $have_search = gitweb_check_feature('search');
2232
2233	if ($have_search) {
2234		my $performed = "";
2235		if ($searchtype eq 'author') {
2236			$performed = "authored";
2237		} elsif ($searchtype eq 'committer') {
2238			$performed = "committed";
2239		}
2240
2241		return $cgi->a({-href => href(action=>"search", hash=>$hash,
2242				searchtext=>$author,
2243				searchtype=>$searchtype), class=>"list",
2244				title=>"Search for commits $performed by $author"},
2245				$displaytext);
2246
2247	} else {
2248		return $displaytext;
2249	}
2250}
2251
2252# format the author name of the given commit with the given tag
2253# the author name is chopped and escaped according to the other
2254# optional parameters (see chop_str).
2255sub format_author_html {
2256	my $tag = shift;
2257	my $co = shift;
2258	my $author = chop_and_escape_str($co->{'author_name'}, @_);
2259	return "<$tag class=\"author\">" .
2260	       format_search_author($co->{'author_name'}, "author",
2261		       git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2262		       $author) .
2263	       "</$tag>";
2264}
2265
2266# format git diff header line, i.e. "diff --(git|combined|cc) ..."
2267sub format_git_diff_header_line {
2268	my $line = shift;
2269	my $diffinfo = shift;
2270	my ($from, $to) = @_;
2271
2272	if ($diffinfo->{'nparents'}) {
2273		# combined diff
2274		$line =~ s!^(diff (.*?) )"?.*$!$1!;
2275		if ($to->{'href'}) {
2276			$line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2277			                 esc_path($to->{'file'}));
2278		} else { # file was deleted (no href)
2279			$line .= esc_path($to->{'file'});
2280		}
2281	} else {
2282		# "ordinary" diff
2283		$line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2284		if ($from->{'href'}) {
2285			$line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2286			                 'a/' . esc_path($from->{'file'}));
2287		} else { # file was added (no href)
2288			$line .= 'a/' . esc_path($from->{'file'});
2289		}
2290		$line .= ' ';
2291		if ($to->{'href'}) {
2292			$line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2293			                 'b/' . esc_path($to->{'file'}));
2294		} else { # file was deleted
2295			$line .= 'b/' . esc_path($to->{'file'});
2296		}
2297	}
2298
2299	return "<div class=\"diff header\">$line</div>\n";
2300}
2301
2302# format extended diff header line, before patch itself
2303sub format_extended_diff_header_line {
2304	my $line = shift;
2305	my $diffinfo = shift;
2306	my ($from, $to) = @_;
2307
2308	# match <path>
2309	if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2310		$line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2311		                       esc_path($from->{'file'}));
2312	}
2313	if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2314		$line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2315		                 esc_path($to->{'file'}));
2316	}
2317	# match single <mode>
2318	if ($line =~ m/\s(\d{6})$/) {
2319		$line .= '<span class="info"> (' .
2320		         file_type_long($1) .
2321		         ')</span>';
2322	}
2323	# match <hash>
2324	if ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", ",") |
2325	    $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", ",")) {
2326		# can match only for combined diff
2327		$line = 'index ';
2328		for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2329			if ($from->{'href'}[$i]) {
2330				$line .= $cgi->a({-href=>$from->{'href'}[$i],
2331				                  -class=>"hash"},
2332				                 substr($diffinfo->{'from_id'}[$i],0,7));
2333			} else {
2334				$line .= '0' x 7;
2335			}
2336			# separator
2337			$line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2338		}
2339		$line .= '..';
2340		if ($to->{'href'}) {
2341			$line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2342			                 substr($diffinfo->{'to_id'},0,7));
2343		} else {
2344			$line .= '0' x 7;
2345		}
2346
2347	} elsif ($line =~ oid_nlen_prefix_infix_regex($sha1_len, "index ", "..") |
2348		 $line =~ oid_nlen_prefix_infix_regex($sha256_len, "index ", "..")) {
2349		# can match only for ordinary diff
2350		my ($from_link, $to_link);
2351		if ($from->{'href'}) {
2352			$from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2353			                     substr($diffinfo->{'from_id'},0,7));
2354		} else {
2355			$from_link = '0' x 7;
2356		}
2357		if ($to->{'href'}) {
2358			$to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2359			                   substr($diffinfo->{'to_id'},0,7));
2360		} else {
2361			$to_link = '0' x 7;
2362		}
2363		my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2364		$line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2365	}
2366
2367	return $line . "<br/>\n";
2368}
2369
2370# format from-file/to-file diff header
2371sub format_diff_from_to_header {
2372	my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2373	my $line;
2374	my $result = '';
2375
2376	$line = $from_line;
2377	#assert($line =~ m/^---/) if DEBUG;
2378	# no extra formatting for "^--- /dev/null"
2379	if (! $diffinfo->{'nparents'}) {
2380		# ordinary (single parent) diff
2381		if ($line =~ m!^--- "?a/!) {
2382			if ($from->{'href'}) {
2383				$line = '--- a/' .
2384				        $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2385				                esc_path($from->{'file'}));
2386			} else {
2387				$line = '--- a/' .
2388				        esc_path($from->{'file'});
2389			}
2390		}
2391		$result .= qq!<div class="diff from_file">$line</div>\n!;
2392
2393	} else {
2394		# combined diff (merge commit)
2395		for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2396			if ($from->{'href'}[$i]) {
2397				$line = '--- ' .
2398				        $cgi->a({-href=>href(action=>"blobdiff",
2399				                             hash_parent=>$diffinfo->{'from_id'}[$i],
2400				                             hash_parent_base=>$parents[$i],
2401				                             file_parent=>$from->{'file'}[$i],
2402				                             hash=>$diffinfo->{'to_id'},
2403				                             hash_base=>$hash,
2404				                             file_name=>$to->{'file'}),
2405				                 -class=>"path",
2406				                 -title=>"diff" . ($i+1)},
2407				                $i+1) .
2408				        '/' .
2409				        $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2410				                esc_path($from->{'file'}[$i]));
2411			} else {
2412				$line = '--- /dev/null';
2413			}
2414			$result .= qq!<div class="diff from_file">$line</div>\n!;
2415		}
2416	}
2417
2418	$line = $to_line;
2419	#assert($line =~ m/^\+\+\+/) if DEBUG;
2420	# no extra formatting for "^+++ /dev/null"
2421	if ($line =~ m!^\+\+\+ "?b/!) {
2422		if ($to->{'href'}) {
2423			$line = '+++ b/' .
2424			        $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2425			                esc_path($to->{'file'}));
2426		} else {
2427			$line = '+++ b/' .
2428			        esc_path($to->{'file'});
2429		}
2430	}
2431	$result .= qq!<div class="diff to_file">$line</div>\n!;
2432
2433	return $result;
2434}
2435
2436# create note for patch simplified by combined diff
2437sub format_diff_cc_simplified {
2438	my ($diffinfo, @parents) = @_;
2439	my $result = '';
2440
2441	$result .= "<div class=\"diff header\">" .
2442	           "diff --cc ";
2443	if (!is_deleted($diffinfo)) {
2444		$result .= $cgi->a({-href => href(action=>"blob",
2445		                                  hash_base=>$hash,
2446		                                  hash=>$diffinfo->{'to_id'},
2447		                                  file_name=>$diffinfo->{'to_file'}),
2448		                    -class => "path"},
2449		                   esc_path($diffinfo->{'to_file'}));
2450	} else {
2451		$result .= esc_path($diffinfo->{'to_file'});
2452	}
2453	$result .= "</div>\n" . # class="diff header"
2454	           "<div class=\"diff nodifferences\">" .
2455	           "Simple merge" .
2456	           "</div>\n"; # class="diff nodifferences"
2457
2458	return $result;
2459}
2460
2461sub diff_line_class {
2462	my ($line, $from, $to) = @_;
2463
2464	# ordinary diff
2465	my $num_sign = 1;
2466	# combined diff
2467	if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2468		$num_sign = scalar @{$from->{'href'}};
2469	}
2470
2471	my @diff_line_classifier = (
2472		{ regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
2473		{ regexp => qr/^\\/,               class => "incomplete"  },
2474		{ regexp => qr/^ {$num_sign}/,     class => "ctx" },
2475		# classifier for context must come before classifier add/rem,
2476		# or we would have to use more complicated regexp, for example
2477		# qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2478		{ regexp => qr/^[+ ]{$num_sign}/,   class => "add" },
2479		{ regexp => qr/^[- ]{$num_sign}/,   class => "rem" },
2480	);
2481	for my $clsfy (@diff_line_classifier) {
2482		return $clsfy->{'class'}
2483			if ($line =~ $clsfy->{'regexp'});
2484	}
2485
2486	# fallback
2487	return "";
2488}
2489
2490# assumes that $from and $to are defined and correctly filled,
2491# and that $line holds a line of chunk header for unified diff
2492sub format_unidiff_chunk_header {
2493	my ($line, $from, $to) = @_;
2494
2495	my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2496		$line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2497
2498	$from_lines = 0 unless defined $from_lines;
2499	$to_lines   = 0 unless defined $to_lines;
2500
2501	if ($from->{'href'}) {
2502		$from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2503		                     -class=>"list"}, $from_text);
2504	}
2505	if ($to->{'href'}) {
2506		$to_text   = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2507		                     -class=>"list"}, $to_text);
2508	}
2509	$line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2510	        "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2511	return $line;
2512}
2513
2514# assumes that $from and $to are defined and correctly filled,
2515# and that $line holds a line of chunk header for combined diff
2516sub format_cc_diff_chunk_header {
2517	my ($line, $from, $to) = @_;
2518
2519	my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2520	my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2521
2522	@from_text = split(' ', $ranges);
2523	for (my $i = 0; $i < @from_text; ++$i) {
2524		($from_start[$i], $from_nlines[$i]) =
2525			(split(',', substr($from_text[$i], 1)), 0);
2526	}
2527
2528	$to_text   = pop @from_text;
2529	$to_start  = pop @from_start;
2530	$to_nlines = pop @from_nlines;
2531
2532	$line = "<span class=\"chunk_info\">$prefix ";
2533	for (my $i = 0; $i < @from_text; ++$i) {
2534		if ($from->{'href'}[$i]) {
2535			$line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2536			                  -class=>"list"}, $from_text[$i]);
2537		} else {
2538			$line .= $from_text[$i];
2539		}
2540		$line .= " ";
2541	}
2542	if ($to->{'href'}) {
2543		$line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2544		                  -class=>"list"}, $to_text);
2545	} else {
2546		$line .= $to_text;
2547	}
2548	$line .= " $prefix</span>" .
2549	         "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2550	return $line;
2551}
2552
2553# process patch (diff) line (not to be used for diff headers),
2554# returning HTML-formatted (but not wrapped) line.
2555# If the line is passed as a reference, it is treated as HTML and not
2556# esc_html()'ed.
2557sub format_diff_line {
2558	my ($line, $diff_class, $from, $to) = @_;
2559
2560	if (ref($line)) {
2561		$line = $$line;
2562	} else {
2563		chomp $line;
2564		$line = untabify($line);
2565
2566		if ($from && $to && $line =~ m/^\@{2} /) {
2567			$line = format_unidiff_chunk_header($line, $from, $to);
2568		} elsif ($from && $to && $line =~ m/^\@{3}/) {
2569			$line = format_cc_diff_chunk_header($line, $from, $to);
2570		} else {
2571			$line = esc_html($line, -nbsp=>1);
2572		}
2573	}
2574
2575	my $diff_classes = "diff";
2576	$diff_classes .= " $diff_class" if ($diff_class);
2577	$line = "<div class=\"$diff_classes\">$line</div>\n";
2578
2579	return $line;
2580}
2581
2582# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2583# linked.  Pass the hash of the tree/commit to snapshot.
2584sub format_snapshot_links {
2585	my ($hash) = @_;
2586	my $num_fmts = @snapshot_fmts;
2587	if ($num_fmts > 1) {
2588		# A parenthesized list of links bearing format names.
2589		# e.g. "snapshot (_tar.gz_ _zip_)"
2590		return "snapshot (" . join(' ', map
2591			$cgi->a({
2592				-href => href(
2593					action=>"snapshot",
2594					hash=>$hash,
2595					snapshot_format=>$_
2596				)
2597			}, $known_snapshot_formats{$_}{'display'})
2598		, @snapshot_fmts) . ")";
2599	} elsif ($num_fmts == 1) {
2600		# A single "snapshot" link whose tooltip bears the format name.
2601		# i.e. "_snapshot_"
2602		my ($fmt) = @snapshot_fmts;
2603		return
2604			$cgi->a({
2605				-href => href(
2606					action=>"snapshot",
2607					hash=>$hash,
2608					snapshot_format=>$fmt
2609				),
2610				-title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2611			}, "snapshot");
2612	} else { # $num_fmts == 0
2613		return undef;
2614	}
2615}
2616
2617## ......................................................................
2618## functions returning values to be passed, perhaps after some
2619## transformation, to other functions; e.g. returning arguments to href()
2620
2621# returns hash to be passed to href to generate gitweb URL
2622# in -title key it returns description of link
2623sub get_feed_info {
2624	my $format = shift || 'Atom';
2625	my %res = (action => lc($format));
2626	my $matched_ref = 0;
2627
2628	# feed links are possible only for project views
2629	return unless (defined $project);
2630	# some views should link to OPML, or to generic project feed,
2631	# or don't have specific feed yet (so they should use generic)
2632	return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2633
2634	my $branch = undef;
2635	# branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
2636	# (fullname) to differentiate from tag links; this also makes
2637	# possible to detect branch links
2638	for my $ref (get_branch_refs()) {
2639		if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
2640		    (defined $hash      && $hash      =~ m!^refs/\Q$ref\E/(.*)$!)) {
2641			$branch = $1;
2642			$matched_ref = $ref;
2643			last;
2644		}
2645	}
2646	# find log type for feed description (title)
2647	my $type = 'log';
2648	if (defined $file_name) {
2649		$type  = "history of $file_name";
2650		$type .= "/" if ($action eq 'tree');
2651		$type .= " on '$branch'" if (defined $branch);
2652	} else {
2653		$type = "log of $branch" if (defined $branch);
2654	}
2655
2656	$res{-title} = $type;
2657	$res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);
2658	$res{'file_name'} = $file_name;
2659
2660	return %res;
2661}
2662
2663## ----------------------------------------------------------------------
2664## git utility subroutines, invoking git commands
2665
2666# returns path to the core git executable and the --git-dir parameter as list
2667sub git_cmd {
2668	$number_of_git_cmds++;
2669	return $GIT, '--git-dir='.$git_dir;
2670}
2671
2672# quote the given arguments for passing them to the shell
2673# quote_command("command", "arg 1", "arg with ' and ! characters")
2674# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2675# Try to avoid using this function wherever possible.
2676sub quote_command {
2677	return join(' ',
2678		map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2679}
2680
2681# get HEAD ref of given project as hash
2682sub git_get_head_hash {
2683	return git_get_full_hash(shift, 'HEAD');
2684}
2685
2686sub git_get_full_hash {
2687	return git_get_hash(@_);
2688}
2689
2690sub git_get_short_hash {
2691	return git_get_hash(@_, '--short=7');
2692}
2693
2694sub git_get_hash {
2695	my ($project, $hash, @options) = @_;
2696	my $o_git_dir = $git_dir;
2697	my $retval = undef;
2698	$git_dir = "$projectroot/$project";
2699	if (open my $fd, '-|', git_cmd(), 'rev-parse',
2700	    '--verify', '-q', @options, $hash) {
2701		$retval = <$fd>;
2702		chomp $retval if defined $retval;
2703		close $fd;
2704	}
2705	if (defined $o_git_dir) {
2706		$git_dir = $o_git_dir;
2707	}
2708	return $retval;
2709}
2710
2711# get type of given object
2712sub git_get_type {
2713	my $hash = shift;
2714
2715	open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2716	my $type = <$fd>;
2717	close $fd or return;
2718	chomp $type;
2719	return $type;
2720}
2721
2722# repository configuration
2723our $config_file = '';
2724our %config;
2725
2726# store multiple values for single key as anonymous array reference
2727# single values stored directly in the hash, not as [ <value> ]
2728sub hash_set_multi {
2729	my ($hash, $key, $value) = @_;
2730
2731	if (!exists $hash->{$key}) {
2732		$hash->{$key} = $value;
2733	} elsif (!ref $hash->{$key}) {
2734		$hash->{$key} = [ $hash->{$key}, $value ];
2735	} else {
2736		push @{$hash->{$key}}, $value;
2737	}
2738}
2739
2740# return hash of git project configuration
2741# optionally limited to some section, e.g. 'gitweb'
2742sub git_parse_project_config {
2743	my $section_regexp = shift;
2744	my %config;
2745
2746	local $/ = "\0";
2747
2748	open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2749		or return;
2750
2751	while (my $keyval = <$fh>) {
2752		chomp $keyval;
2753		my ($key, $value) = split(/\n/, $keyval, 2);
2754
2755		hash_set_multi(\%config, $key, $value)
2756			if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2757	}
2758	close $fh;
2759
2760	return %config;
2761}
2762
2763# convert config value to boolean: 'true' or 'false'
2764# no value, number > 0, 'true' and 'yes' values are true
2765# rest of values are treated as false (never as error)
2766sub config_to_bool {
2767	my $val = shift;
2768
2769	return 1 if !defined $val;             # section.key
2770
2771	# strip leading and trailing whitespace
2772	$val =~ s/^\s+//;
2773	$val =~ s/\s+$//;
2774
2775	return (($val =~ /^\d+$/ && $val) ||   # section.key = 1
2776	        ($val =~ /^(?:true|yes)$/i));  # section.key = true
2777}
2778
2779# convert config value to simple decimal number
2780# an optional value suffix of 'k', 'm', or 'g' will cause the value
2781# to be multiplied by 1024, 1048576, or 1073741824
2782sub config_to_int {
2783	my $val = shift;
2784
2785	# strip leading and trailing whitespace
2786	$val =~ s/^\s+//;
2787	$val =~ s/\s+$//;
2788
2789	if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2790		$unit = lc($unit);
2791		# unknown unit is treated as 1
2792		return $num * ($unit eq 'g' ? 1073741824 :
2793		               $unit eq 'm' ?    1048576 :
2794		               $unit eq 'k' ?       1024 : 1);
2795	}
2796	return $val;
2797}
2798
2799# convert config value to array reference, if needed
2800sub config_to_multi {
2801	my $val = shift;
2802
2803	return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2804}
2805
2806sub git_get_project_config {
2807	my ($key, $type) = @_;
2808
2809	return unless defined $git_dir;
2810
2811	# key sanity check
2812	return unless ($key);
2813	# only subsection, if exists, is case sensitive,
2814	# and not lowercased by 'git config -z -l'
2815	if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2816		$lo =~ s/_//g;
2817		$key = join(".", lc($hi), $mi, lc($lo));
2818		return if ($lo =~ /\W/ || $hi =~ /\W/);
2819	} else {
2820		$key = lc($key);
2821		$key =~ s/_//g;
2822		return if ($key =~ /\W/);
2823	}
2824	$key =~ s/^gitweb\.//;
2825
2826	# type sanity check
2827	if (defined $type) {
2828		$type =~ s/^--//;
2829		$type = undef
2830			unless ($type eq 'bool' || $type eq 'int');
2831	}
2832
2833	# get config
2834	if (!defined $config_file ||
2835	    $config_file ne "$git_dir/config") {
2836		%config = git_parse_project_config('gitweb');
2837		$config_file = "$git_dir/config";
2838	}
2839
2840	# check if config variable (key) exists
2841	return unless exists $config{"gitweb.$key"};
2842
2843	# ensure given type
2844	if (!defined $type) {
2845		return $config{"gitweb.$key"};
2846	} elsif ($type eq 'bool') {
2847		# backward compatibility: 'git config --bool' returns true/false
2848		return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2849	} elsif ($type eq 'int') {
2850		return config_to_int($config{"gitweb.$key"});
2851	}
2852	return $config{"gitweb.$key"};
2853}
2854
2855# get hash of given path at given ref
2856sub git_get_hash_by_path {
2857	my $base = shift;
2858	my $path = shift || return undef;
2859	my $type = shift;
2860
2861	$path =~ s,/+$,,;
2862
2863	open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2864		or die_error(500, "Open git-ls-tree failed");
2865	my $line = <$fd>;
2866	close $fd or return undef;
2867
2868	if (!defined $line) {
2869		# there is no tree or hash given by $path at $base
2870		return undef;
2871	}
2872
2873	#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
2874	$line =~ m/^([0-9]+) (.+) ($oid_regex)\t/;
2875	if (defined $type && $type ne $2) {
2876		# type doesn't match
2877		return undef;
2878	}
2879	return $3;
2880}
2881
2882# get path of entry with given hash at given tree-ish (ref)
2883# used to get 'from' filename for combined diff (merge commit) for renames
2884sub git_get_path_by_hash {
2885	my $base = shift || return;
2886	my $hash = shift || return;
2887
2888	local $/ = "\0";
2889
2890	open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2891		or return undef;
2892	while (my $line = <$fd>) {
2893		chomp $line;
2894
2895		#'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423	gitweb'
2896		#'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f	gitweb/README'
2897		if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2898			close $fd;
2899			return $1;
2900		}
2901	}
2902	close $fd;
2903	return undef;
2904}
2905
2906## ......................................................................
2907## git utility functions, directly accessing git repository
2908
2909# get the value of config variable either from file named as the variable
2910# itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
2911# configuration variable in the repository config file.
2912sub git_get_file_or_project_config {
2913	my ($path, $name) = @_;
2914
2915	$git_dir = "$projectroot/$path";
2916	open my $fd, '<', "$git_dir/$name"
2917		or return git_get_project_config($name);
2918	my $conf = <$fd>;
2919	close $fd;
2920	if (defined $conf) {
2921		chomp $conf;
2922	}
2923	return $conf;
2924}
2925
2926sub git_get_project_description {
2927	my $path = shift;
2928	return git_get_file_or_project_config($path, 'description');
2929}
2930
2931sub git_get_project_category {
2932	my $path = shift;
2933	return git_get_file_or_project_config($path, 'category');
2934}
2935
2936
2937# supported formats:
2938# * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
2939#   - if its contents is a number, use it as tag weight,
2940#   - otherwise add a tag with weight 1
2941# * $GIT_DIR/ctags file, each line is a tag (with weight 1)
2942#   the same value multiple times increases tag weight
2943# * `gitweb.ctag' multi-valued repo config variable
2944sub git_get_project_ctags {
2945	my $project = shift;
2946	my $ctags = {};
2947
2948	$git_dir = "$projectroot/$project";
2949	if (opendir my $dh, "$git_dir/ctags") {
2950		my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
2951		foreach my $tagfile (@files) {
2952			open my $ct, '<', $tagfile
2953				or next;
2954			my $val = <$ct>;
2955			chomp $val if $val;
2956			close $ct;
2957
2958			(my $ctag = $tagfile) =~ s#.*/##;
2959			if ($val =~ /^\d+$/) {
2960				$ctags->{$ctag} = $val;
2961			} else {
2962				$ctags->{$ctag} = 1;
2963			}
2964		}
2965		closedir $dh;
2966
2967	} elsif (open my $fh, '<', "$git_dir/ctags") {
2968		while (my $line = <$fh>) {
2969			chomp $line;
2970			$ctags->{$line}++ if $line;
2971		}
2972		close $fh;
2973
2974	} else {
2975		my $taglist = config_to_multi(git_get_project_config('ctag'));
2976		foreach my $tag (@$taglist) {
2977			$ctags->{$tag}++;
2978		}
2979	}
2980
2981	return $ctags;
2982}
2983
2984# return hash, where keys are content tags ('ctags'),
2985# and values are sum of weights of given tag in every project
2986sub git_gather_all_ctags {
2987	my $projects = shift;
2988	my $ctags = {};
2989
2990	foreach my $p (@$projects) {
2991		foreach my $ct (keys %{$p->{'ctags'}}) {
2992			$ctags->{$ct} += $p->{'ctags'}->{$ct};
2993		}
2994	}
2995
2996	return $ctags;
2997}
2998
2999sub git_populate_project_tagcloud {
3000	my $ctags = shift;
3001
3002	# First, merge different-cased tags; tags vote on casing
3003	my %ctags_lc;
3004	foreach (keys %$ctags) {
3005		$ctags_lc{lc $_}->{count} += $ctags->{$_};
3006		if (not $ctags_lc{lc $_}->{topcount}
3007		    or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
3008			$ctags_lc{lc $_}->{topcount} = $ctags->{$_};
3009			$ctags_lc{lc $_}->{topname} = $_;
3010		}
3011	}
3012
3013	my $cloud;
3014	my $matched = $input_params{'ctag'};
3015	if (eval { require HTML::TagCloud; 1; }) {
3016		$cloud = HTML::TagCloud->new;
3017		foreach my $ctag (sort keys %ctags_lc) {
3018			# Pad the title with spaces so that the cloud looks
3019			# less crammed.
3020			my $title = esc_html($ctags_lc{$ctag}->{topname});
3021			$title =~ s/ /&nbsp;/g;
3022			$title =~ s/^/&nbsp;/g;
3023			$title =~ s/$/&nbsp;/g;
3024			if (defined $matched && $matched eq $ctag) {
3025				$title = qq(<span class="match">$title</span>);
3026			}
3027			$cloud->add($title, href(project=>undef, ctag=>$ctag),
3028			            $ctags_lc{$ctag}->{count});
3029		}
3030	} else {
3031		$cloud = {};
3032		foreach my $ctag (keys %ctags_lc) {
3033			my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
3034			if (defined $matched && $matched eq $ctag) {
3035				$title = qq(<span class="match">$title</span>);
3036			}
3037			$cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
3038			$cloud->{$ctag}{ctag} =
3039				$cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
3040		}
3041	}
3042	return $cloud;
3043}
3044
3045sub git_show_project_tagcloud {
3046	my ($cloud, $count) = @_;
3047	if (ref $cloud eq 'HTML::TagCloud') {
3048		return $cloud->html_and_css($count);
3049	} else {
3050		my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3051		return
3052			'<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
3053			join (', ', map {
3054				$cloud->{$_}->{'ctag'}
3055			} splice(@tags, 0, $count)) .
3056			'</div>';
3057	}
3058}
3059
3060sub git_get_project_url_list {
3061	my $path = shift;
3062
3063	$git_dir = "$projectroot/$path";
3064	open my $fd, '<', "$git_dir/cloneurl"
3065		or return wantarray ?
3066		@{ config_to_multi(git_get_project_config('url')) } :
3067		   config_to_multi(git_get_project_config('url'));
3068	my @git_project_url_list = map { chomp; $_ } <$fd>;
3069	close $fd;
3070
3071	return wantarray ? @git_project_url_list : \@git_project_url_list;
3072}
3073
3074sub git_get_projects_list {
3075	my $filter = shift || '';
3076	my $paranoid = shift;
3077	my @list;
3078
3079	if (-d $projects_list) {
3080		# search in directory
3081		my $dir = $projects_list;
3082		# remove the trailing "/"
3083		$dir =~ s!/+$!!;
3084		my $pfxlen = length("$dir");
3085		my $pfxdepth = ($dir =~ tr!/!!);
3086		# when filtering, search only given subdirectory
3087		if ($filter && !$paranoid) {
3088			$dir .= "/$filter";
3089			$dir =~ s!/+$!!;
3090		}
3091
3092		File::Find::find({
3093			follow_fast => 1, # follow symbolic links
3094			follow_skip => 2, # ignore duplicates
3095			dangling_symlinks => 0, # ignore dangling symlinks, silently
3096			wanted => sub {
3097				# global variables
3098				our $project_maxdepth;
3099				our $projectroot;
3100				# skip project-list toplevel, if we get it.
3101				return if (m!^[/.]$!);
3102				# only directories can be git repositories
3103				return unless (-d $_);
3104				# need search permission
3105				return unless (-x $_);
3106				# don't traverse too deep (Find is super slow on os x)
3107				# $project_maxdepth excludes depth of $projectroot
3108				if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3109					$File::Find::prune = 1;
3110					return;
3111				}
3112
3113				my $path = substr($File::Find::name, $pfxlen + 1);
3114				# paranoidly only filter here
3115				if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3116					next;
3117				}
3118				# we check related file in $projectroot
3119				if (check_export_ok("$projectroot/$path")) {
3120					push @list, { path => $path };
3121					$File::Find::prune = 1;
3122				}
3123			},
3124		}, "$dir");
3125
3126	} elsif (-f $projects_list) {
3127		# read from file(url-encoded):
3128		# 'git%2Fgit.git Linus+Torvalds'
3129		# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3130		# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3131		open my $fd, '<', $projects_list or return;
3132	PROJECT:
3133		while (my $line = <$fd>) {
3134			chomp $line;
3135			my ($path, $owner) = split ' ', $line;
3136			$path = unescape($path);
3137			$owner = unescape($owner);
3138			if (!defined $path) {
3139				next;
3140			}
3141			# if $filter is rpovided, check if $path begins with $filter
3142			if ($filter && $path !~ m!^\Q$filter\E/!) {
3143				next;
3144			}
3145			if (check_export_ok("$projectroot/$path")) {
3146				my $pr = {
3147					path => $path
3148				};
3149				if ($owner) {
3150					$pr->{'owner'} = to_utf8($owner);
3151				}
3152				push @list, $pr;
3153			}
3154		}
3155		close $fd;
3156	}
3157	return @list;
3158}
3159
3160# written with help of Tree::Trie module (Perl Artistic License, GPL compatible)
3161# as side effects it sets 'forks' field to list of forks for forked projects
3162sub filter_forks_from_projects_list {
3163	my $projects = shift;
3164
3165	my %trie; # prefix tree of directories (path components)
3166	# generate trie out of those directories that might contain forks
3167	foreach my $pr (@$projects) {
3168		my $path = $pr->{'path'};
3169		$path =~ s/\.git$//;      # forks of 'repo.git' are in 'repo/' directory
3170		next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3171		next unless ($path);      # skip '.git' repository: tests, git-instaweb
3172		next unless (-d "$projectroot/$path"); # containing directory exists
3173		$pr->{'forks'} = [];      # there can be 0 or more forks of project
3174
3175		# add to trie
3176		my @dirs = split('/', $path);
3177		# walk the trie, until either runs out of components or out of trie
3178		my $ref = \%trie;
3179		while (scalar @dirs &&
3180		       exists($ref->{$dirs[0]})) {
3181			$ref = $ref->{shift @dirs};
3182		}
3183		# create rest of trie structure from rest of components
3184		foreach my $dir (@dirs) {
3185			$ref = $ref->{$dir} = {};
3186		}
3187		# create end marker, store $pr as a data
3188		$ref->{''} = $pr if (!exists $ref->{''});
3189	}
3190
3191	# filter out forks, by finding shortest prefix match for paths
3192	my @filtered;
3193 PROJECT:
3194	foreach my $pr (@$projects) {
3195		# trie lookup
3196		my $ref = \%trie;
3197	DIR:
3198		foreach my $dir (split('/', $pr->{'path'})) {
3199			if (exists $ref->{''}) {
3200				# found [shortest] prefix, is a fork - skip it
3201				push @{$ref->{''}{'forks'}}, $pr;
3202				next PROJECT;
3203			}
3204			if (!exists $ref->{$dir}) {
3205				# not in trie, cannot have prefix, not a fork
3206				push @filtered, $pr;
3207				next PROJECT;
3208			}
3209			# If the dir is there, we just walk one step down the trie.
3210			$ref = $ref->{$dir};
3211		}
3212		# we ran out of trie
3213		# (shouldn't happen: it's either no match, or end marker)
3214		push @filtered, $pr;
3215	}
3216
3217	return @filtered;
3218}
3219
3220# note: fill_project_list_info must be run first,
3221# for 'descr_long' and 'ctags' to be filled
3222sub search_projects_list {
3223	my ($projlist, %opts) = @_;
3224	my $tagfilter  = $opts{'tagfilter'};
3225	my $search_re = $opts{'search_regexp'};
3226
3227	return @$projlist
3228		unless ($tagfilter || $search_re);
3229
3230	# searching projects require filling to be run before it;
3231	fill_project_list_info($projlist,
3232	                       $tagfilter  ? 'ctags' : (),
3233	                       $search_re ? ('path', 'descr') : ());
3234	my @projects;
3235 PROJECT:
3236	foreach my $pr (@$projlist) {
3237
3238		if ($tagfilter) {
3239			next unless ref($pr->{'ctags'}) eq 'HASH';
3240			next unless
3241				grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3242		}
3243
3244		if ($search_re) {
3245			next unless
3246				$pr->{'path'} =~ /$search_re/ ||
3247				$pr->{'descr_long'} =~ /$search_re/;
3248		}
3249
3250		push @projects, $pr;
3251	}
3252
3253	return @projects;
3254}
3255
3256our $gitweb_project_owner = undef;
3257sub git_get_project_list_from_file {
3258
3259	return if (defined $gitweb_project_owner);
3260
3261	$gitweb_project_owner = {};
3262	# read from file (url-encoded):
3263	# 'git%2Fgit.git Linus+Torvalds'
3264	# 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3265	# 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3266	if (-f $projects_list) {
3267		open(my $fd, '<', $projects_list);
3268		while (my $line = <$fd>) {
3269			chomp $line;
3270			my ($pr, $ow) = split ' ', $line;
3271			$pr = unescape($pr);
3272			$ow = unescape($ow);
3273			$gitweb_project_owner->{$pr} = to_utf8($ow);
3274		}
3275		close $fd;
3276	}
3277}
3278
3279sub git_get_project_owner {
3280	my $project = shift;
3281	my $owner;
3282
3283	return undef unless $project;
3284	$git_dir = "$projectroot/$project";
3285
3286	if (!defined $gitweb_project_owner) {
3287		git_get_project_list_from_file();
3288	}
3289
3290	if (exists $gitweb_project_owner->{$project}) {
3291		$owner = $gitweb_project_owner->{$project};
3292	}
3293	if (!defined $owner){
3294		$owner = git_get_project_config('owner');
3295	}
3296	if (!defined $owner) {
3297		$owner = get_file_owner("$git_dir");
3298	}
3299
3300	return $owner;
3301}
3302
3303sub git_get_last_activity {
3304	my ($path) = @_;
3305	my $fd;
3306
3307	$git_dir = "$projectroot/$path";
3308	open($fd, "-|", git_cmd(), 'for-each-ref',
3309	     '--format=%(committer)',
3310	     '--sort=-committerdate',
3311	     '--count=1',
3312	     map { "refs/$_" } get_branch_refs ()) or return;
3313	my $most_recent = <$fd>;
3314	close $fd or return;
3315	if (defined $most_recent &&
3316	    $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
3317		my $timestamp = $1;
3318		my $age = time - $timestamp;
3319		return ($age, age_string($age));
3320	}
3321	return (undef, undef);
3322}
3323
3324# Implementation note: when a single remote is wanted, we cannot use 'git
3325# remote show -n' because that command always work (assuming it's a remote URL
3326# if it's not defined), and we cannot use 'git remote show' because that would
3327# try to make a network roundtrip. So the only way to find if that particular
3328# remote is defined is to walk the list provided by 'git remote -v' and stop if
3329# and when we find what we want.
3330sub git_get_remotes_list {
3331	my $wanted = shift;
3332	my %remotes = ();
3333
3334	open my $fd, '-|' , git_cmd(), 'remote', '-v';
3335	return unless $fd;
3336	while (my $remote = <$fd>) {
3337		chomp $remote;
3338		$remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3339		next if $wanted and not $remote eq $wanted;
3340		my ($url, $key) = ($1, $2);
3341
3342		$remotes{$remote} ||= { 'heads' => () };
3343		$remotes{$remote}{$key} = $url;
3344	}
3345	close $fd or return;
3346	return wantarray ? %remotes : \%remotes;
3347}
3348
3349# Takes a hash of remotes as first parameter and fills it by adding the
3350# available remote heads for each of the indicated remotes.
3351sub fill_remote_heads {
3352	my $remotes = shift;
3353	my @heads = map { "remotes/$_" } keys %$remotes;
3354	my @remoteheads = git_get_heads_list(undef, @heads);
3355	foreach my $remote (keys %$remotes) {
3356		$remotes->{$remote}{'heads'} = [ grep {
3357			$_->{'name'} =~ s!^$remote/!!
3358			} @remoteheads ];
3359	}
3360}
3361
3362sub git_get_references {
3363	my $type = shift || "";
3364	my %refs;
3365	# 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3366	# c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3367	open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
3368		($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
3369		or return;
3370
3371	while (my $line = <$fd>) {
3372		chomp $line;
3373		if ($line =~ m!^($oid_regex)\srefs/($type.*)$!) {
3374			if (defined $refs{$1}) {
3375				push @{$refs{$1}}, $2;
3376			} else {
3377				$refs{$1} = [ $2 ];
3378			}
3379		}
3380	}
3381	close $fd or return;
3382	return \%refs;
3383}
3384
3385sub git_get_rev_name_tags {
3386	my $hash = shift || return undef;
3387
3388	open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
3389		or return;
3390	my $name_rev = <$fd>;
3391	close $fd;
3392
3393	if ($name_rev =~ m|^$hash tags/(.*)$|) {
3394		return $1;
3395	} else {
3396		# catches also '$hash undefined' output
3397		return undef;
3398	}
3399}
3400
3401## ----------------------------------------------------------------------
3402## parse to hash functions
3403
3404sub parse_date {
3405	my $epoch = shift;
3406	my $tz = shift || "-0000";
3407
3408	my %date;
3409	my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3410	my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3411	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3412	$date{'hour'} = $hour;
3413	$date{'minute'} = $min;
3414	$date{'mday'} = $mday;
3415	$date{'day'} = $days[$wday];
3416	$date{'month'} = $months[$mon];
3417	$date{'rfc2822'}   = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3418	                     $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3419	$date{'mday-time'} = sprintf "%d %s %02d:%02d",
3420	                     $mday, $months[$mon], $hour ,$min;
3421	$date{'iso-8601'}  = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3422	                     1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3423
3424	my ($tz_sign, $tz_hour, $tz_min) =
3425		($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3426	$tz_sign = ($tz_sign eq '-' ? -1 : +1);
3427	my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3428	($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3429	$date{'hour_local'} = $hour;
3430	$date{'minute_local'} = $min;
3431	$date{'tz_local'} = $tz;
3432	$date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3433	                          1900+$year, $mon+1, $mday,
3434	                          $hour, $min, $sec, $tz);
3435	return %date;
3436}
3437
3438sub parse_tag {
3439	my $tag_id = shift;
3440	my %tag;
3441	my @comment;
3442
3443	open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
3444	$tag{'id'} = $tag_id;
3445	while (my $line = <$fd>) {
3446		chomp $line;
3447		if ($line =~ m/^object ($oid_regex)$/) {
3448			$tag{'object'} = $1;
3449		} elsif ($line =~ m/^type (.+)$/) {
3450			$tag{'type'} = $1;
3451		} elsif ($line =~ m/^tag (.+)$/) {
3452			$tag{'name'} = $1;
3453		} elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3454			$tag{'author'} = $1;
3455			$tag{'author_epoch'} = $2;
3456			$tag{'author_tz'} = $3;
3457			if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3458				$tag{'author_name'}  = $1;
3459				$tag{'author_email'} = $2;
3460			} else {
3461				$tag{'author_name'} = $tag{'author'};
3462			}
3463		} elsif ($line =~ m/--BEGIN/) {
3464			push @comment, $line;
3465			last;
3466		} elsif ($line eq "") {
3467			last;
3468		}
3469	}
3470	push @comment, <$fd>;
3471	$tag{'comment'} = \@comment;
3472	close $fd or return;
3473	if (!defined $tag{'name'}) {
3474		return
3475	};
3476	return %tag
3477}
3478
3479sub parse_commit_text {
3480	my ($commit_text, $withparents) = @_;
3481	my @commit_lines = split '\n', $commit_text;
3482	my %co;
3483
3484	pop @commit_lines; # Remove '\0'
3485
3486	if (! @commit_lines) {
3487		return;
3488	}
3489
3490	my $header = shift @commit_lines;
3491	if ($header !~ m/^$oid_regex/) {
3492		return;
3493	}
3494	($co{'id'}, my @parents) = split ' ', $header;
3495	while (my $line = shift @commit_lines) {
3496		last if $line eq "\n";
3497		if ($line =~ m/^tree ($oid_regex)$/) {
3498			$co{'tree'} = $1;
3499		} elsif ((!defined $withparents) && ($line =~ m/^parent ($oid_regex)$/)) {
3500			push @parents, $1;
3501		} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3502			$co{'author'} = to_utf8($1);
3503			$co{'author_epoch'} = $2;
3504			$co{'author_tz'} = $3;
3505			if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3506				$co{'author_name'}  = $1;
3507				$co{'author_email'} = $2;
3508			} else {
3509				$co{'author_name'} = $co{'author'};
3510			}
3511		} elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3512			$co{'committer'} = to_utf8($1);
3513			$co{'committer_epoch'} = $2;
3514			$co{'committer_tz'} = $3;
3515			if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3516				$co{'committer_name'}  = $1;
3517				$co{'committer_email'} = $2;
3518			} else {
3519				$co{'committer_name'} = $co{'committer'};
3520			}
3521		}
3522	}
3523	if (!defined $co{'tree'}) {
3524		return;
3525	};
3526	$co{'parents'} = \@parents;
3527	$co{'parent'} = $parents[0];
3528
3529	foreach my $title (@commit_lines) {
3530		$title =~ s/^    //;
3531		if ($title ne "") {
3532			$co{'title'} = chop_str($title, 80, 5);
3533			# remove leading stuff of merges to make the interesting part visible
3534			if (length($title) > 50) {
3535				$title =~ s/^Automatic //;
3536				$title =~ s/^merge (of|with) /Merge ... /i;
3537				if (length($title) > 50) {
3538					$title =~ s/(http|rsync):\/\///;
3539				}
3540				if (length($title) > 50) {
3541					$title =~ s/(master|www|rsync)\.//;
3542				}
3543				if (length($title) > 50) {
3544					$title =~ s/kernel.org:?//;
3545				}
3546				if (length($title) > 50) {
3547					$title =~ s/\/pub\/scm//;
3548				}
3549			}
3550			$co{'title_short'} = chop_str($title, 50, 5);
3551			last;
3552		}
3553	}
3554	if (! defined $co{'title'} || $co{'title'} eq "") {
3555		$co{'title'} = $co{'title_short'} = '(no commit message)';
3556	}
3557	# remove added spaces
3558	foreach my $line (@commit_lines) {
3559		$line =~ s/^    //;
3560	}
3561	$co{'comment'} = \@commit_lines;
3562
3563	my $age = time - $co{'committer_epoch'};
3564	$co{'age'} = $age;
3565	$co{'age_string'} = age_string($age);
3566	my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3567	if ($age > 60*60*24*7*2) {
3568		$co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3569		$co{'age_string_age'} = $co{'age_string'};
3570	} else {
3571		$co{'age_string_date'} = $co{'age_string'};
3572		$co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3573	}
3574	return %co;
3575}
3576
3577sub parse_commit {
3578	my ($commit_id) = @_;
3579	my %co;
3580
3581	local $/ = "\0";
3582
3583	open my $fd, "-|", git_cmd(), "rev-list",
3584		"--parents",
3585		"--header",
3586		"--max-count=1",
3587		$commit_id,
3588		"--",
3589		or die_error(500, "Open git-rev-list failed");
3590	%co = parse_commit_text(<$fd>, 1);
3591	close $fd;
3592
3593	return %co;
3594}
3595
3596sub parse_commits {
3597	my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3598	my @cos;
3599
3600	$maxcount ||= 1;
3601	$skip ||= 0;
3602
3603	local $/ = "\0";
3604
3605	open my $fd, "-|", git_cmd(), "rev-list",
3606		"--header",
3607		@args,
3608		("--max-count=" . $maxcount),
3609		("--skip=" . $skip),
3610		@extra_options,
3611		$commit_id,
3612		"--",
3613		($filename ? ($filename) : ())
3614		or die_error(500, "Open git-rev-list failed");
3615	while (my $line = <$fd>) {
3616		my %co = parse_commit_text($line);
3617		push @cos, \%co;
3618	}
3619	close $fd;
3620
3621	return wantarray ? @cos : \@cos;
3622}
3623
3624# parse line of git-diff-tree "raw" output
3625sub parse_difftree_raw_line {
3626	my $line = shift;
3627	my %res;
3628
3629	# ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M	ls-files.c'
3630	# ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M	rev-tree.c'
3631	if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ($oid_regex) ($oid_regex) (.)([0-9]{0,3})\t(.*)$/) {
3632		$res{'from_mode'} = $1;
3633		$res{'to_mode'} = $2;
3634		$res{'from_id'} = $3;
3635		$res{'to_id'} = $4;
3636		$res{'status'} = $5;
3637		$res{'similarity'} = $6;
3638		if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3639			($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3640		} else {
3641			$res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3642		}
3643	}
3644	# '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR	git-gui/git-gui.sh'
3645	# combined diff (for merge commit)
3646	elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:$oid_regex )+)([a-zA-Z]+)\t(.*)$//) {
3647		$res{'nparents'}  = length($1);
3648		$res{'from_mode'} = [ split(' ', $2) ];
3649		$res{'to_mode'} = pop @{$res{'from_mode'}};
3650		$res{'from_id'} = [ split(' ', $3) ];
3651		$res{'to_id'} = pop @{$res{'from_id'}};
3652		$res{'status'} = [ split('', $4) ];
3653		$res{'to_file'} = unquote($5);
3654	}
3655	# 'c512b523472485aef4fff9e57b229d9d243c967f'
3656	elsif ($line =~ m/^($oid_regex)$/) {
3657		$res{'commit'} = $1;
3658	}
3659
3660	return wantarray ? %res : \%res;
3661}
3662
3663# wrapper: return parsed line of git-diff-tree "raw" output
3664# (the argument might be raw line, or parsed info)
3665sub parsed_difftree_line {
3666	my $line_or_ref = shift;
3667
3668	if (ref($line_or_ref) eq "HASH") {
3669		# pre-parsed (or generated by hand)
3670		return $line_or_ref;
3671	} else {
3672		return parse_difftree_raw_line($line_or_ref);
3673	}
3674}
3675
3676# parse line of git-ls-tree output
3677sub parse_ls_tree_line {
3678	my $line = shift;
3679	my %opts = @_;
3680	my %res;
3681
3682	if ($opts{'-l'}) {
3683		#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa   16717	panic.c'
3684		$line =~ m/^([0-9]+) (.+) ($oid_regex) +(-|[0-9]+)\t(.+)$/s;
3685
3686		$res{'mode'} = $1;
3687		$res{'type'} = $2;
3688		$res{'hash'} = $3;
3689		$res{'size'} = $4;
3690		if ($opts{'-z'}) {
3691			$res{'name'} = $5;
3692		} else {
3693			$res{'name'} = unquote($5);
3694		}
3695	} else {
3696		#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
3697		$line =~ m/^([0-9]+) (.+) ($oid_regex)\t(.+)$/s;
3698
3699		$res{'mode'} = $1;
3700		$res{'type'} = $2;
3701		$res{'hash'} = $3;
3702		if ($opts{'-z'}) {
3703			$res{'name'} = $4;
3704		} else {
3705			$res{'name'} = unquote($4);
3706		}
3707	}
3708
3709	return wantarray ? %res : \%res;
3710}
3711
3712# generates _two_ hashes, references to which are passed as 2 and 3 argument
3713sub parse_from_to_diffinfo {
3714	my ($diffinfo, $from, $to, @parents) = @_;
3715
3716	if ($diffinfo->{'nparents'}) {
3717		# combined diff
3718		$from->{'file'} = [];
3719		$from->{'href'} = [];
3720		fill_from_file_info($diffinfo, @parents)
3721			unless exists $diffinfo->{'from_file'};
3722		for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3723			$from->{'file'}[$i] =
3724				defined $diffinfo->{'from_file'}[$i] ?
3725				        $diffinfo->{'from_file'}[$i] :
3726				        $diffinfo->{'to_file'};
3727			if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3728				$from->{'href'}[$i] = href(action=>"blob",
3729				                           hash_base=>$parents[$i],
3730				                           hash=>$diffinfo->{'from_id'}[$i],
3731				                           file_name=>$from->{'file'}[$i]);
3732			} else {
3733				$from->{'href'}[$i] = undef;
3734			}
3735		}
3736	} else {
3737		# ordinary (not combined) diff
3738		$from->{'file'} = $diffinfo->{'from_file'};
3739		if ($diffinfo->{'status'} ne "A") { # not new (added) file
3740			$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3741			                       hash=>$diffinfo->{'from_id'},
3742			                       file_name=>$from->{'file'});
3743		} else {
3744			delete $from->{'href'};
3745		}
3746	}
3747
3748	$to->{'file'} = $diffinfo->{'to_file'};
3749	if (!is_deleted($diffinfo)) { # file exists in result
3750		$to->{'href'} = href(action=>"blob", hash_base=>$hash,
3751		                     hash=>$diffinfo->{'to_id'},
3752		                     file_name=>$to->{'file'});
3753	} else {
3754		delete $to->{'href'};
3755	}
3756}
3757
3758## ......................................................................
3759## parse to array of hashes functions
3760
3761sub git_get_heads_list {
3762	my ($limit, @classes) = @_;
3763	@classes = get_branch_refs() unless @classes;
3764	my @patterns = map { "refs/$_" } @classes;
3765	my @headslist;
3766
3767	open my $fd, '-|', git_cmd(), 'for-each-ref',
3768		($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3769		'--format=%(objectname) %(refname) %(subject)%00%(committer)',
3770		@patterns
3771		or return;
3772	while (my $line = <$fd>) {
3773		my %ref_item;
3774
3775		chomp $line;
3776		my ($refinfo, $committerinfo) = split(/\0/, $line);
3777		my ($hash, $name, $title) = split(' ', $refinfo, 3);
3778		my ($committer, $epoch, $tz) =
3779			($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3780		$ref_item{'fullname'}  = $name;
3781		my $strip_refs = join '|', map { quotemeta } get_branch_refs();
3782		$name =~ s!^refs/($strip_refs|remotes)/!!;
3783		$ref_item{'name'} = $name;
3784		# for refs neither in 'heads' nor 'remotes' we want to
3785		# show their ref dir
3786		my $ref_dir = (defined $1) ? $1 : '';
3787		if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
3788		    $ref_item{'name'} .= ' (' . $ref_dir . ')';
3789		}
3790
3791		$ref_item{'id'}    = $hash;
3792		$ref_item{'title'} = $title || '(no commit message)';
3793		$ref_item{'epoch'} = $epoch;
3794		if ($epoch) {
3795			$ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3796		} else {
3797			$ref_item{'age'} = "unknown";
3798		}
3799
3800		push @headslist, \%ref_item;
3801	}
3802	close $fd;
3803
3804	return wantarray ? @headslist : \@headslist;
3805}
3806
3807sub git_get_tags_list {
3808	my $limit = shift;
3809	my @tagslist;
3810
3811	open my $fd, '-|', git_cmd(), 'for-each-ref',
3812		($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3813		'--format=%(objectname) %(objecttype) %(refname) '.
3814		'%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3815		'refs/tags'
3816		or return;
3817	while (my $line = <$fd>) {
3818		my %ref_item;
3819
3820		chomp $line;
3821		my ($refinfo, $creatorinfo) = split(/\0/, $line);
3822		my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3823		my ($creator, $epoch, $tz) =
3824			($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3825		$ref_item{'fullname'} = $name;
3826		$name =~ s!^refs/tags/!!;
3827
3828		$ref_item{'type'} = $type;
3829		$ref_item{'id'} = $id;
3830		$ref_item{'name'} = $name;
3831		if ($type eq "tag") {
3832			$ref_item{'subject'} = $title;
3833			$ref_item{'reftype'} = $reftype;
3834			$ref_item{'refid'}   = $refid;
3835		} else {
3836			$ref_item{'reftype'} = $type;
3837			$ref_item{'refid'}   = $id;
3838		}
3839
3840		if ($type eq "tag" || $type eq "commit") {
3841			$ref_item{'epoch'} = $epoch;
3842			if ($epoch) {
3843				$ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3844			} else {
3845				$ref_item{'age'} = "unknown";
3846			}
3847		}
3848
3849		push @tagslist, \%ref_item;
3850	}
3851	close $fd;
3852
3853	return wantarray ? @tagslist : \@tagslist;
3854}
3855
3856## ----------------------------------------------------------------------
3857## filesystem-related functions
3858
3859sub get_file_owner {
3860	my $path = shift;
3861
3862	my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3863	my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3864	if (!defined $gcos) {
3865		return undef;
3866	}
3867	my $owner = $gcos;
3868	$owner =~ s/[,;].*$//;
3869	return to_utf8($owner);
3870}
3871
3872# assume that file exists
3873sub insert_file {
3874	my $filename = shift;
3875
3876	open my $fd, '<', $filename;
3877	print map { to_utf8($_) } <$fd>;
3878	close $fd;
3879}
3880
3881## ......................................................................
3882## mimetype related functions
3883
3884sub mimetype_guess_file {
3885	my $filename = shift;
3886	my $mimemap = shift;
3887	-r $mimemap or return undef;
3888
3889	my %mimemap;
3890	open(my $mh, '<', $mimemap) or return undef;
3891	while (<$mh>) {
3892		next if m/^#/; # skip comments
3893		my ($mimetype, @exts) = split(/\s+/);
3894		foreach my $ext (@exts) {
3895			$mimemap{$ext} = $mimetype;
3896		}
3897	}
3898	close($mh);
3899
3900	$filename =~ /\.([^.]*)$/;
3901	return $mimemap{$1};
3902}
3903
3904sub mimetype_guess {
3905	my $filename = shift;
3906	my $mime;
3907	$filename =~ /\./ or return undef;
3908
3909	if ($mimetypes_file) {
3910		my $file = $mimetypes_file;
3911		if ($file !~ m!^/!) { # if it is relative path
3912			# it is relative to project
3913			$file = "$projectroot/$project/$file";
3914		}
3915		$mime = mimetype_guess_file($filename, $file);
3916	}
3917	$mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3918	return $mime;
3919}
3920
3921sub blob_mimetype {
3922	my $fd = shift;
3923	my $filename = shift;
3924
3925	if ($filename) {
3926		my $mime = mimetype_guess($filename);
3927		$mime and return $mime;
3928	}
3929
3930	# just in case
3931	return $default_blob_plain_mimetype unless $fd;
3932
3933	if (-T $fd) {
3934		return 'text/plain';
3935	} elsif (! $filename) {
3936		return 'application/octet-stream';
3937	} elsif ($filename =~ m/\.png$/i) {
3938		return 'image/png';
3939	} elsif ($filename =~ m/\.gif$/i) {
3940		return 'image/gif';
3941	} elsif ($filename =~ m/\.jpe?g$/i) {
3942		return 'image/jpeg';
3943	} else {
3944		return 'application/octet-stream';
3945	}
3946}
3947
3948sub blob_contenttype {
3949	my ($fd, $file_name, $type) = @_;
3950
3951	$type ||= blob_mimetype($fd, $file_name);
3952	if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3953		$type .= "; charset=$default_text_plain_charset";
3954	}
3955
3956	return $type;
3957}
3958
3959# guess file syntax for syntax highlighting; return undef if no highlighting
3960# the name of syntax can (in the future) depend on syntax highlighter used
3961sub guess_file_syntax {
3962	my ($highlight, $file_name) = @_;
3963	return undef unless ($highlight && defined $file_name);
3964	my $basename = basename($file_name, '.in');
3965	return $highlight_basename{$basename}
3966		if exists $highlight_basename{$basename};
3967
3968	$basename =~ /\.([^.]*)$/;
3969	my $ext = $1 or return undef;
3970	return $highlight_ext{$ext}
3971		if exists $highlight_ext{$ext};
3972
3973	return undef;
3974}
3975
3976# run highlighter and return FD of its output,
3977# or return original FD if no highlighting
3978sub run_highlighter {
3979	my ($fd, $highlight, $syntax) = @_;
3980	return $fd unless ($highlight);
3981
3982	close $fd;
3983	my $syntax_arg = (defined $syntax) ? "--syntax $syntax" : "--force";
3984	open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3985	          quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
3986	            '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
3987	            '--', "-fe=$fallback_encoding")." | ".
3988	          quote_command($highlight_bin).
3989	          " --replace-tabs=8 --fragment $syntax_arg |"
3990		or die_error(500, "Couldn't open file or run syntax highlighter");
3991	return $fd;
3992}
3993
3994## ======================================================================
3995## functions printing HTML: header, footer, error page
3996
3997sub get_page_title {
3998	my $title = to_utf8($site_name);
3999
4000	unless (defined $project) {
4001		if (defined $project_filter) {
4002			$title .= " - projects in '" . esc_path($project_filter) . "'";
4003		}
4004		return $title;
4005	}
4006	$title .= " - " . to_utf8($project);
4007
4008	return $title unless (defined $action);
4009	$title .= "/$action"; # $action is US-ASCII (7bit ASCII)
4010
4011	return $title unless (defined $file_name);
4012	$title .= " - " . esc_path($file_name);
4013	if ($action eq "tree" && $file_name !~ m|/$|) {
4014		$title .= "/";
4015	}
4016
4017	return $title;
4018}
4019
4020sub get_content_type_html {
4021	# require explicit support from the UA if we are to send the page as
4022	# 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
4023	# we have to do this because MSIE sometimes globs '*/*', pretending to
4024	# support xhtml+xml but choking when it gets what it asked for.
4025	if (defined $cgi->http('HTTP_ACCEPT') &&
4026	    $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
4027	    $cgi->Accept('application/xhtml+xml') != 0) {
4028		return 'application/xhtml+xml';
4029	} else {
4030		return 'text/html';
4031	}
4032}
4033
4034sub print_feed_meta {
4035	if (defined $project) {
4036		my %href_params = get_feed_info();
4037		if (!exists $href_params{'-title'}) {
4038			$href_params{'-title'} = 'log';
4039		}
4040
4041		foreach my $format (qw(RSS Atom)) {
4042			my $type = lc($format);
4043			my %link_attr = (
4044				'-rel' => 'alternate',
4045				'-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
4046				'-type' => "application/$type+xml"
4047			);
4048
4049			$href_params{'extra_options'} = undef;
4050			$href_params{'action'} = $type;
4051			$link_attr{'-href'} = esc_attr(href(%href_params));
4052			print "<link ".
4053			      "rel=\"$link_attr{'-rel'}\" ".
4054			      "title=\"$link_attr{'-title'}\" ".
4055			      "href=\"$link_attr{'-href'}\" ".
4056			      "type=\"$link_attr{'-type'}\" ".
4057			      "/>\n";
4058
4059			$href_params{'extra_options'} = '--no-merges';
4060			$link_attr{'-href'} = esc_attr(href(%href_params));
4061			$link_attr{'-title'} .= ' (no merges)';
4062			print "<link ".
4063			      "rel=\"$link_attr{'-rel'}\" ".
4064			      "title=\"$link_attr{'-title'}\" ".
4065			      "href=\"$link_attr{'-href'}\" ".
4066			      "type=\"$link_attr{'-type'}\" ".
4067			      "/>\n";
4068		}
4069
4070	} else {
4071		printf('<link rel="alternate" title="%s projects list" '.
4072		       'href="%s" type="text/plain; charset=utf-8" />'."\n",
4073		       esc_attr($site_name),
4074		       esc_attr(href(project=>undef, action=>"project_index")));
4075		printf('<link rel="alternate" title="%s projects feeds" '.
4076		       'href="%s" type="text/x-opml" />'."\n",
4077		       esc_attr($site_name),
4078		       esc_attr(href(project=>undef, action=>"opml")));
4079	}
4080}
4081
4082sub print_header_links {
4083	my $status = shift;
4084
4085	# print out each stylesheet that exist, providing backwards capability
4086	# for those people who defined $stylesheet in a config file
4087	if (defined $stylesheet) {
4088		print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4089	} else {
4090		foreach my $stylesheet (@stylesheets) {
4091			next unless $stylesheet;
4092			print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4093		}
4094	}
4095	print_feed_meta()
4096		if ($status eq '200 OK');
4097	if (defined $favicon) {
4098		print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
4099	}
4100}
4101
4102sub print_nav_breadcrumbs_path {
4103	my $dirprefix = undef;
4104	while (my $part = shift) {
4105		$dirprefix .= "/" if defined $dirprefix;
4106		$dirprefix .= $part;
4107		print $cgi->a({-href => href(project => undef,
4108		                             project_filter => $dirprefix,
4109		                             action => "project_list")},
4110			      esc_html($part)) . " / ";
4111	}
4112}
4113
4114sub print_nav_breadcrumbs {
4115	my %opts = @_;
4116
4117	for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4118		print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / ";
4119	}
4120	if (defined $project) {
4121		my @dirname = split '/', $project;
4122		my $projectbasename = pop @dirname;
4123		print_nav_breadcrumbs_path(@dirname);
4124		print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
4125		if (defined $action) {
4126			my $action_print = $action ;
4127			if (defined $opts{-action_extra}) {
4128				$action_print = $cgi->a({-href => href(action=>$action)},
4129					$action);
4130			}
4131			print " / $action_print";
4132		}
4133		if (defined $opts{-action_extra}) {
4134			print " / $opts{-action_extra}";
4135		}
4136		print "\n";
4137	} elsif (defined $project_filter) {
4138		print_nav_breadcrumbs_path(split '/', $project_filter);
4139	}
4140}
4141
4142sub print_search_form {
4143	if (!defined $searchtext) {
4144		$searchtext = "";
4145	}
4146	my $search_hash;
4147	if (defined $hash_base) {
4148		$search_hash = $hash_base;
4149	} elsif (defined $hash) {
4150		$search_hash = $hash;
4151	} else {
4152		$search_hash = "HEAD";
4153	}
4154	my $action = $my_uri;
4155	my $use_pathinfo = gitweb_check_feature('pathinfo');
4156	if ($use_pathinfo) {
4157		$action .= "/".esc_url($project);
4158	}
4159	print $cgi->start_form(-method => "get", -action => $action) .
4160	      "<div class=\"search\">\n" .
4161	      (!$use_pathinfo &&
4162	      $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
4163	      $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
4164	      $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
4165	      $cgi->popup_menu(-name => 'st', -default => 'commit',
4166	                       -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
4167	      " " . $cgi->a({-href => href(action=>"search_help"),
4168			     -title => "search help" }, "?") . " search:\n",
4169	      $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
4170	      "<span title=\"Extended regular expression\">" .
4171	      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
4172	                     -checked => $search_use_regexp) .
4173	      "</span>" .
4174	      "</div>" .
4175	      $cgi->end_form() . "\n";
4176}
4177
4178sub git_header_html {
4179	my $status = shift || "200 OK";
4180	my $expires = shift;
4181	my %opts = @_;
4182
4183	my $title = get_page_title();
4184	my $content_type = get_content_type_html();
4185	print $cgi->header(-type=>$content_type, -charset => 'utf-8',
4186	                   -status=> $status, -expires => $expires)
4187		unless ($opts{'-no_http_header'});
4188	my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
4189	print <<EOF;
4190<?xml version="1.0" encoding="utf-8"?>
4191<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4192<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
4193<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
4194<!-- git core binaries version $git_version -->
4195<head>
4196<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
4197<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
4198<meta name="robots" content="index, nofollow"/>
4199<title>$title</title>
4200EOF
4201	# the stylesheet, favicon etc urls won't work correctly with path_info
4202	# unless we set the appropriate base URL
4203	if ($ENV{'PATH_INFO'}) {
4204		print "<base href=\"".esc_url($base_url)."\" />\n";
4205	}
4206	print_header_links($status);
4207
4208	if (defined $site_html_head_string) {
4209		print to_utf8($site_html_head_string);
4210	}
4211
4212	print "</head>\n" .
4213	      "<body>\n";
4214
4215	if (defined $site_header && -f $site_header) {
4216		insert_file($site_header);
4217	}
4218
4219	print "<div class=\"page_header\">\n";
4220	if (defined $logo) {
4221		print $cgi->a({-href => esc_url($logo_url),
4222		               -title => $logo_label},
4223		              $cgi->img({-src => esc_url($logo),
4224		                         -width => 72, -height => 27,
4225		                         -alt => "git",
4226		                         -class => "logo"}));
4227	}
4228	print_nav_breadcrumbs(%opts);
4229	print "</div>\n";
4230
4231	my $have_search = gitweb_check_feature('search');
4232	if (defined $project && $have_search) {
4233		print_search_form();
4234	}
4235}
4236
4237sub git_footer_html {
4238	my $feed_class = 'rss_logo';
4239
4240	print "<div class=\"page_footer\">\n";
4241	if (defined $project) {
4242		my $descr = git_get_project_description($project);
4243		if (defined $descr) {
4244			print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
4245		}
4246
4247		my %href_params = get_feed_info();
4248		if (!%href_params) {
4249			$feed_class .= ' generic';
4250		}
4251		$href_params{'-title'} ||= 'log';
4252
4253		foreach my $format (qw(RSS Atom)) {
4254			$href_params{'action'} = lc($format);
4255			print $cgi->a({-href => href(%href_params),
4256			              -title => "$href_params{'-title'} $format feed",
4257			              -class => $feed_class}, $format)."\n";
4258		}
4259
4260	} else {
4261		print $cgi->a({-href => href(project=>undef, action=>"opml",
4262		                             project_filter => $project_filter),
4263		              -class => $feed_class}, "OPML") . " ";
4264		print $cgi->a({-href => href(project=>undef, action=>"project_index",
4265		                             project_filter => $project_filter),
4266		              -class => $feed_class}, "TXT") . "\n";
4267	}
4268	print "</div>\n"; # class="page_footer"
4269
4270	if (defined $t0 && gitweb_check_feature('timed')) {
4271		print "<div id=\"generating_info\">\n";
4272		print 'This page took '.
4273		      '<span id="generating_time" class="time_span">'.
4274		      tv_interval($t0, [ gettimeofday() ]).
4275		      ' seconds </span>'.
4276		      ' and '.
4277		      '<span id="generating_cmd">'.
4278		      $number_of_git_cmds.
4279		      '</span> git commands '.
4280		      " to generate.\n";
4281		print "</div>\n"; # class="page_footer"
4282	}
4283
4284	if (defined $site_footer && -f $site_footer) {
4285		insert_file($site_footer);
4286	}
4287
4288	print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
4289	if (defined $action &&
4290	    $action eq 'blame_incremental') {
4291		print qq!<script type="text/javascript">\n!.
4292		      qq!startBlame("!. esc_attr(href(action=>"blame_data", -replay=>1)) .qq!",\n!.
4293		      qq!           "!. esc_attr(href()) .qq!");\n!.
4294		      qq!</script>\n!;
4295	} else {
4296		my ($jstimezone, $tz_cookie, $datetime_class) =
4297			gitweb_get_feature('javascript-timezone');
4298
4299		print qq!<script type="text/javascript">\n!.
4300		      qq!window.onload = function () {\n!;
4301		if (gitweb_check_feature('javascript-actions')) {
4302			print qq!	fixLinks();\n!;
4303		}
4304		if ($jstimezone && $tz_cookie && $datetime_class) {
4305			print qq!	var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
4306			      qq!	onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
4307		}
4308		print qq!};\n!.
4309		      qq!</script>\n!;
4310	}
4311
4312	print "</body>\n" .
4313	      "</html>";
4314}
4315
4316# die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
4317# Example: die_error(404, 'Hash not found')
4318# By convention, use the following status codes (as defined in RFC 2616):
4319# 400: Invalid or missing CGI parameters, or
4320#      requested object exists but has wrong type.
4321# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4322#      this server or project.
4323# 404: Requested object/revision/project doesn't exist.
4324# 500: The server isn't configured properly, or
4325#      an internal error occurred (e.g. failed assertions caused by bugs), or
4326#      an unknown error occurred (e.g. the git binary died unexpectedly).
4327# 503: The server is currently unavailable (because it is overloaded,
4328#      or down for maintenance).  Generally, this is a temporary state.
4329sub die_error {
4330	my $status = shift || 500;
4331	my $error = esc_html(shift) || "Internal Server Error";
4332	my $extra = shift;
4333	my %opts = @_;
4334
4335	my %http_responses = (
4336		400 => '400 Bad Request',
4337		403 => '403 Forbidden',
4338		404 => '404 Not Found',
4339		500 => '500 Internal Server Error',
4340		503 => '503 Service Unavailable',
4341	);
4342	git_header_html($http_responses{$status}, undef, %opts);
4343	print <<EOF;
4344<div class="page_body">
4345<br /><br />
4346$status - $error
4347<br />
4348EOF
4349	if (defined $extra) {
4350		print "<hr />\n" .
4351		      "$extra\n";
4352	}
4353	print "</div>\n";
4354
4355	git_footer_html();
4356	goto DONE_GITWEB
4357		unless ($opts{'-error_handler'});
4358}
4359
4360## ----------------------------------------------------------------------
4361## functions printing or outputting HTML: navigation
4362
4363sub git_print_page_nav {
4364	my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4365	$extra = '' if !defined $extra; # pager or formats
4366
4367	my @navs = qw(summary shortlog log commit commitdiff tree);
4368	if ($suppress) {
4369		@navs = grep { $_ ne $suppress } @navs;
4370	}
4371
4372	my %arg = map { $_ => {action=>$_} } @navs;
4373	if (defined $head) {
4374		for (qw(commit commitdiff)) {
4375			$arg{$_}{'hash'} = $head;
4376		}
4377		if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4378			for (qw(shortlog log)) {
4379				$arg{$_}{'hash'} = $head;
4380			}
4381		}
4382	}
4383
4384	$arg{'tree'}{'hash'} = $treehead if defined $treehead;
4385	$arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
4386
4387	my @actions = gitweb_get_feature('actions');
4388	my %repl = (
4389		'%' => '%',
4390		'n' => $project,         # project name
4391		'f' => $git_dir,         # project path within filesystem
4392		'h' => $treehead || '',  # current hash ('h' parameter)
4393		'b' => $treebase || '',  # hash base ('hb' parameter)
4394	);
4395	while (@actions) {
4396		my ($label, $link, $pos) = splice(@actions,0,3);
4397		# insert
4398		@navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
4399		# munch munch
4400		$link =~ s/%([%nfhb])/$repl{$1}/g;
4401		$arg{$label}{'_href'} = $link;
4402	}
4403
4404	print "<div class=\"page_nav\">\n" .
4405		(join " | ",
4406		 map { $_ eq $current ?
4407		       $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
4408		 } @navs);
4409	print "<br/>\n$extra<br/>\n" .
4410	      "</div>\n";
4411}
4412
4413# returns a submenu for the navigation of the refs views (tags, heads,
4414# remotes) with the current view disabled and the remotes view only
4415# available if the feature is enabled
4416sub format_ref_views {
4417	my ($current) = @_;
4418	my @ref_views = qw{tags heads};
4419	push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
4420	return join " | ", map {
4421		$_ eq $current ? $_ :
4422		$cgi->a({-href => href(action=>$_)}, $_)
4423	} @ref_views
4424}
4425
4426sub format_paging_nav {
4427	my ($action, $page, $has_next_link) = @_;
4428	my $paging_nav;
4429
4430
4431	if ($page > 0) {
4432		$paging_nav .=
4433			$cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4434			" &sdot; " .
4435			$cgi->a({-href => href(-replay=>1, page=>$page-1),
4436			         -accesskey => "p", -title => "Alt-p"}, "prev");
4437	} else {
4438		$paging_nav .= "first &sdot; prev";
4439	}
4440
4441	if ($has_next_link) {
4442		$paging_nav .= " &sdot; " .
4443			$cgi->a({-href => href(-replay=>1, page=>$page+1),
4444			         -accesskey => "n", -title => "Alt-n"}, "next");
4445	} else {
4446		$paging_nav .= " &sdot; next";
4447	}
4448
4449	return $paging_nav;
4450}
4451
4452## ......................................................................
4453## functions printing or outputting HTML: div
4454
4455sub git_print_header_div {
4456	my ($action, $title, $hash, $hash_base) = @_;
4457	my %args = ();
4458
4459	$args{'action'} = $action;
4460	$args{'hash'} = $hash if $hash;
4461	$args{'hash_base'} = $hash_base if $hash_base;
4462
4463	print "<div class=\"header\">\n" .
4464	      $cgi->a({-href => href(%args), -class => "title"},
4465	      $title ? $title : $action) .
4466	      "\n</div>\n";
4467}
4468
4469sub format_repo_url {
4470	my ($name, $url) = @_;
4471	return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4472}
4473
4474# Group output by placing it in a DIV element and adding a header.
4475# Options for start_div() can be provided by passing a hash reference as the
4476# first parameter to the function.
4477# Options to git_print_header_div() can be provided by passing an array
4478# reference. This must follow the options to start_div if they are present.
4479# The content can be a scalar, which is output as-is, a scalar reference, which
4480# is output after html escaping, an IO handle passed either as *handle or
4481# *handle{IO}, or a function reference. In the latter case all following
4482# parameters will be taken as argument to the content function call.
4483sub git_print_section {
4484	my ($div_args, $header_args, $content);
4485	my $arg = shift;
4486	if (ref($arg) eq 'HASH') {
4487		$div_args = $arg;
4488		$arg = shift;
4489	}
4490	if (ref($arg) eq 'ARRAY') {
4491		$header_args = $arg;
4492		$arg = shift;
4493	}
4494	$content = $arg;
4495
4496	print $cgi->start_div($div_args);
4497	git_print_header_div(@$header_args);
4498
4499	if (ref($content) eq 'CODE') {
4500		$content->(@_);
4501	} elsif (ref($content) eq 'SCALAR') {
4502		print esc_html($$content);
4503	} elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4504		print <$content>;
4505	} elsif (!ref($content) && defined($content)) {
4506		print $content;
4507	}
4508
4509	print $cgi->end_div;
4510}
4511
4512sub format_timestamp_html {
4513	my $date = shift;
4514	my $strtime = $date->{'rfc2822'};
4515
4516	my (undef, undef, $datetime_class) =
4517		gitweb_get_feature('javascript-timezone');
4518	if ($datetime_class) {
4519		$strtime = qq!<span class="$datetime_class">$strtime</span>!;
4520	}
4521
4522	my $localtime_format = '(%02d:%02d %s)';
4523	if ($date->{'hour_local'} < 6) {
4524		$localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
4525	}
4526	$strtime .= ' ' .
4527	            sprintf($localtime_format,
4528	                    $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
4529
4530	return $strtime;
4531}
4532
4533# Outputs the author name and date in long form
4534sub git_print_authorship {
4535	my $co = shift;
4536	my %opts = @_;
4537	my $tag = $opts{-tag} || 'div';
4538	my $author = $co->{'author_name'};
4539
4540	my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4541	print "<$tag class=\"author_date\">" .
4542	      format_search_author($author, "author", esc_html($author)) .
4543	      " [".format_timestamp_html(\%ad)."]".
4544	      git_get_avatar($co->{'author_email'}, -pad_before => 1) .
4545	      "</$tag>\n";
4546}
4547
4548# Outputs table rows containing the full author or committer information,
4549# in the format expected for 'commit' view (& similar).
4550# Parameters are a commit hash reference, followed by the list of people
4551# to output information for. If the list is empty it defaults to both
4552# author and committer.
4553sub git_print_authorship_rows {
4554	my $co = shift;
4555	# too bad we can't use @people = @_ || ('author', 'committer')
4556	my @people = @_;
4557	@people = ('author', 'committer') unless @people;
4558	foreach my $who (@people) {
4559		my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4560		print "<tr><td>$who</td><td>" .
4561		      format_search_author($co->{"${who}_name"}, $who,
4562		                           esc_html($co->{"${who}_name"})) . " " .
4563		      format_search_author($co->{"${who}_email"}, $who,
4564		                           esc_html("<" . $co->{"${who}_email"} . ">")) .
4565		      "</td><td rowspan=\"2\">" .
4566		      git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4567		      "</td></tr>\n" .
4568		      "<tr>" .
4569		      "<td></td><td>" .
4570		      format_timestamp_html(\%wd) .
4571		      "</td>" .
4572		      "</tr>\n";
4573	}
4574}
4575
4576sub git_print_page_path {
4577	my $name = shift;
4578	my $type = shift;
4579	my $hb = shift;
4580
4581
4582	print "<div class=\"page_path\">";
4583	print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4584	              -title => 'tree root'}, to_utf8("[$project]"));
4585	print " / ";
4586	if (defined $name) {
4587		my @dirname = split '/', $name;
4588		my $basename = pop @dirname;
4589		my $fullname = '';
4590
4591		foreach my $dir (@dirname) {
4592			$fullname .= ($fullname ? '/' : '') . $dir;
4593			print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4594			                             hash_base=>$hb),
4595			              -title => $fullname}, esc_path($dir));
4596			print " / ";
4597		}
4598		if (defined $type && $type eq 'blob') {
4599			print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4600			                             hash_base=>$hb),
4601			              -title => $name}, esc_path($basename));
4602		} elsif (defined $type && $type eq 'tree') {
4603			print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4604			                             hash_base=>$hb),
4605			              -title => $name}, esc_path($basename));
4606			print " / ";
4607		} else {
4608			print esc_path($basename);
4609		}
4610	}
4611	print "<br/></div>\n";
4612}
4613
4614sub git_print_log {
4615	my $log = shift;
4616	my %opts = @_;
4617
4618	if ($opts{'-remove_title'}) {
4619		# remove title, i.e. first line of log
4620		shift @$log;
4621	}
4622	# remove leading empty lines
4623	while (defined $log->[0] && $log->[0] eq "") {
4624		shift @$log;
4625	}
4626
4627	# print log
4628	my $skip_blank_line = 0;
4629	foreach my $line (@$log) {
4630		if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
4631			if (! $opts{'-remove_signoff'}) {
4632				print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4633				$skip_blank_line = 1;
4634			}
4635			next;
4636		}
4637
4638		if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
4639			if (! $opts{'-remove_signoff'}) {
4640				print "<span class=\"signoff\">" . esc_html($1) . ": " .
4641					"<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
4642					"</span><br/>\n";
4643				$skip_blank_line = 1;
4644			}
4645			next;
4646		}
4647
4648		# print only one empty line
4649		# do not print empty line after signoff
4650		if ($line eq "") {
4651			next if ($skip_blank_line);
4652			$skip_blank_line = 1;
4653		} else {
4654			$skip_blank_line = 0;
4655		}
4656
4657		print format_log_line_html($line) . "<br/>\n";
4658	}
4659
4660	if ($opts{'-final_empty_line'}) {
4661		# end with single empty line
4662		print "<br/>\n" unless $skip_blank_line;
4663	}
4664}
4665
4666# return link target (what link points to)
4667sub git_get_link_target {
4668	my $hash = shift;
4669	my $link_target;
4670
4671	# read link
4672	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4673		or return;
4674	{
4675		local $/ = undef;
4676		$link_target = <$fd>;
4677	}
4678	close $fd
4679		or return;
4680
4681	return $link_target;
4682}
4683
4684# given link target, and the directory (basedir) the link is in,
4685# return target of link relative to top directory (top tree);
4686# return undef if it is not possible (including absolute links).
4687sub normalize_link_target {
4688	my ($link_target, $basedir) = @_;
4689
4690	# absolute symlinks (beginning with '/') cannot be normalized
4691	return if (substr($link_target, 0, 1) eq '/');
4692
4693	# normalize link target to path from top (root) tree (dir)
4694	my $path;
4695	if ($basedir) {
4696		$path = $basedir . '/' . $link_target;
4697	} else {
4698		# we are in top (root) tree (dir)
4699		$path = $link_target;
4700	}
4701
4702	# remove //, /./, and /../
4703	my @path_parts;
4704	foreach my $part (split('/', $path)) {
4705		# discard '.' and ''
4706		next if (!$part || $part eq '.');
4707		# handle '..'
4708		if ($part eq '..') {
4709			if (@path_parts) {
4710				pop @path_parts;
4711			} else {
4712				# link leads outside repository (outside top dir)
4713				return;
4714			}
4715		} else {
4716			push @path_parts, $part;
4717		}
4718	}
4719	$path = join('/', @path_parts);
4720
4721	return $path;
4722}
4723
4724# print tree entry (row of git_tree), but without encompassing <tr> element
4725sub git_print_tree_entry {
4726	my ($t, $basedir, $hash_base, $have_blame) = @_;
4727
4728	my %base_key = ();
4729	$base_key{'hash_base'} = $hash_base if defined $hash_base;
4730
4731	# The format of a table row is: mode list link.  Where mode is
4732	# the mode of the entry, list is the name of the entry, an href,
4733	# and link is the action links of the entry.
4734
4735	print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4736	if (exists $t->{'size'}) {
4737		print "<td class=\"size\">$t->{'size'}</td>\n";
4738	}
4739	if ($t->{'type'} eq "blob") {
4740		print "<td class=\"list\">" .
4741			$cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4742			                       file_name=>"$basedir$t->{'name'}", %base_key),
4743			        -class => "list"}, esc_path($t->{'name'}));
4744		if (S_ISLNK(oct $t->{'mode'})) {
4745			my $link_target = git_get_link_target($t->{'hash'});
4746			if ($link_target) {
4747				my $norm_target = normalize_link_target($link_target, $basedir);
4748				if (defined $norm_target) {
4749					print " -> " .
4750					      $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4751					                             file_name=>$norm_target),
4752					               -title => $norm_target}, esc_path($link_target));
4753				} else {
4754					print " -> " . esc_path($link_target);
4755				}
4756			}
4757		}
4758		print "</td>\n";
4759		print "<td class=\"link\">";
4760		print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4761		                             file_name=>"$basedir$t->{'name'}", %base_key)},
4762		              "blob");
4763		if ($have_blame) {
4764			print " | " .
4765			      $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4766			                             file_name=>"$basedir$t->{'name'}", %base_key)},
4767			              "blame");
4768		}
4769		if (defined $hash_base) {
4770			print " | " .
4771			      $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4772			                             hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4773			              "history");
4774		}
4775		print " | " .
4776			$cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4777			                       file_name=>"$basedir$t->{'name'}")},
4778			        "raw");
4779		print "</td>\n";
4780
4781	} elsif ($t->{'type'} eq "tree") {
4782		print "<td class=\"list\">";
4783		print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4784		                             file_name=>"$basedir$t->{'name'}",
4785		                             %base_key)},
4786		              esc_path($t->{'name'}));
4787		print "</td>\n";
4788		print "<td class=\"link\">";
4789		print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4790		                             file_name=>"$basedir$t->{'name'}",
4791		                             %base_key)},
4792		              "tree");
4793		if (defined $hash_base) {
4794			print " | " .
4795			      $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4796			                             file_name=>"$basedir$t->{'name'}")},
4797			              "history");
4798		}
4799		print "</td>\n";
4800	} else {
4801		# unknown object: we can only present history for it
4802		# (this includes 'commit' object, i.e. submodule support)
4803		print "<td class=\"list\">" .
4804		      esc_path($t->{'name'}) .
4805		      "</td>\n";
4806		print "<td class=\"link\">";
4807		if (defined $hash_base) {
4808			print $cgi->a({-href => href(action=>"history",
4809			                             hash_base=>$hash_base,
4810			                             file_name=>"$basedir$t->{'name'}")},
4811			              "history");
4812		}
4813		print "</td>\n";
4814	}
4815}
4816
4817## ......................................................................
4818## functions printing large fragments of HTML
4819
4820# get pre-image filenames for merge (combined) diff
4821sub fill_from_file_info {
4822	my ($diff, @parents) = @_;
4823
4824	$diff->{'from_file'} = [ ];
4825	$diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4826	for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4827		if ($diff->{'status'}[$i] eq 'R' ||
4828		    $diff->{'status'}[$i] eq 'C') {
4829			$diff->{'from_file'}[$i] =
4830				git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4831		}
4832	}
4833
4834	return $diff;
4835}
4836
4837# is current raw difftree line of file deletion
4838sub is_deleted {
4839	my $diffinfo = shift;
4840
4841	return $diffinfo->{'to_id'} eq ('0' x 40) || $diffinfo->{'to_id'} eq ('0' x 64);
4842}
4843
4844# does patch correspond to [previous] difftree raw line
4845# $diffinfo  - hashref of parsed raw diff format
4846# $patchinfo - hashref of parsed patch diff format
4847#              (the same keys as in $diffinfo)
4848sub is_patch_split {
4849	my ($diffinfo, $patchinfo) = @_;
4850
4851	return defined $diffinfo && defined $patchinfo
4852		&& $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4853}
4854
4855
4856sub git_difftree_body {
4857	my ($difftree, $hash, @parents) = @_;
4858	my ($parent) = $parents[0];
4859	my $have_blame = gitweb_check_feature('blame');
4860	print "<div class=\"list_head\">\n";
4861	if ($#{$difftree} > 10) {
4862		print(($#{$difftree} + 1) . " files changed:\n");
4863	}
4864	print "</div>\n";
4865
4866	print "<table class=\"" .
4867	      (@parents > 1 ? "combined " : "") .
4868	      "diff_tree\">\n";
4869
4870	# header only for combined diff in 'commitdiff' view
4871	my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4872	if ($has_header) {
4873		# table header
4874		print "<thead><tr>\n" .
4875		       "<th></th><th></th>\n"; # filename, patchN link
4876		for (my $i = 0; $i < @parents; $i++) {
4877			my $par = $parents[$i];
4878			print "<th>" .
4879			      $cgi->a({-href => href(action=>"commitdiff",
4880			                             hash=>$hash, hash_parent=>$par),
4881			               -title => 'commitdiff to parent number ' .
4882			                          ($i+1) . ': ' . substr($par,0,7)},
4883			              $i+1) .
4884			      "&nbsp;</th>\n";
4885		}
4886		print "</tr></thead>\n<tbody>\n";
4887	}
4888
4889	my $alternate = 1;
4890	my $patchno = 0;
4891	foreach my $line (@{$difftree}) {
4892		my $diff = parsed_difftree_line($line);
4893
4894		if ($alternate) {
4895			print "<tr class=\"dark\">\n";
4896		} else {
4897			print "<tr class=\"light\">\n";
4898		}
4899		$alternate ^= 1;
4900
4901		if (exists $diff->{'nparents'}) { # combined diff
4902
4903			fill_from_file_info($diff, @parents)
4904				unless exists $diff->{'from_file'};
4905
4906			if (!is_deleted($diff)) {
4907				# file exists in the result (child) commit
4908				print "<td>" .
4909				      $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4910				                             file_name=>$diff->{'to_file'},
4911				                             hash_base=>$hash),
4912				              -class => "list"}, esc_path($diff->{'to_file'})) .
4913				      "</td>\n";
4914			} else {
4915				print "<td>" .
4916				      esc_path($diff->{'to_file'}) .
4917				      "</td>\n";
4918			}
4919
4920			if ($action eq 'commitdiff') {
4921				# link to patch
4922				$patchno++;
4923				print "<td class=\"link\">" .
4924				      $cgi->a({-href => href(-anchor=>"patch$patchno")},
4925				              "patch") .
4926				      " | " .
4927				      "</td>\n";
4928			}
4929
4930			my $has_history = 0;
4931			my $not_deleted = 0;
4932			for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4933				my $hash_parent = $parents[$i];
4934				my $from_hash = $diff->{'from_id'}[$i];
4935				my $from_path = $diff->{'from_file'}[$i];
4936				my $status = $diff->{'status'}[$i];
4937
4938				$has_history ||= ($status ne 'A');
4939				$not_deleted ||= ($status ne 'D');
4940
4941				if ($status eq 'A') {
4942					print "<td  class=\"link\" align=\"right\"> | </td>\n";
4943				} elsif ($status eq 'D') {
4944					print "<td class=\"link\">" .
4945					      $cgi->a({-href => href(action=>"blob",
4946					                             hash_base=>$hash,
4947					                             hash=>$from_hash,
4948					                             file_name=>$from_path)},
4949					              "blob" . ($i+1)) .
4950					      " | </td>\n";
4951				} else {
4952					if ($diff->{'to_id'} eq $from_hash) {
4953						print "<td class=\"link nochange\">";
4954					} else {
4955						print "<td class=\"link\">";
4956					}
4957					print $cgi->a({-href => href(action=>"blobdiff",
4958					                             hash=>$diff->{'to_id'},
4959					                             hash_parent=>$from_hash,
4960					                             hash_base=>$hash,
4961					                             hash_parent_base=>$hash_parent,
4962					                             file_name=>$diff->{'to_file'},
4963					                             file_parent=>$from_path)},
4964					              "diff" . ($i+1)) .
4965					      " | </td>\n";
4966				}
4967			}
4968
4969			print "<td class=\"link\">";
4970			if ($not_deleted) {
4971				print $cgi->a({-href => href(action=>"blob",
4972				                             hash=>$diff->{'to_id'},
4973				                             file_name=>$diff->{'to_file'},
4974				                             hash_base=>$hash)},
4975				              "blob");
4976				print " | " if ($has_history);
4977			}
4978			if ($has_history) {
4979				print $cgi->a({-href => href(action=>"history",
4980				                             file_name=>$diff->{'to_file'},
4981				                             hash_base=>$hash)},
4982				              "history");
4983			}
4984			print "</td>\n";
4985
4986			print "</tr>\n";
4987			next; # instead of 'else' clause, to avoid extra indent
4988		}
4989		# else ordinary diff
4990
4991		my ($to_mode_oct, $to_mode_str, $to_file_type);
4992		my ($from_mode_oct, $from_mode_str, $from_file_type);
4993		if ($diff->{'to_mode'} ne ('0' x 6)) {
4994			$to_mode_oct = oct $diff->{'to_mode'};
4995			if (S_ISREG($to_mode_oct)) { # only for regular file
4996				$to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4997			}
4998			$to_file_type = file_type($diff->{'to_mode'});
4999		}
5000		if ($diff->{'from_mode'} ne ('0' x 6)) {
5001			$from_mode_oct = oct $diff->{'from_mode'};
5002			if (S_ISREG($from_mode_oct)) { # only for regular file
5003				$from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5004			}
5005			$from_file_type = file_type($diff->{'from_mode'});
5006		}
5007
5008		if ($diff->{'status'} eq "A") { # created
5009			my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5010			$mode_chng   .= " with mode: $to_mode_str" if $to_mode_str;
5011			$mode_chng   .= "]</span>";
5012			print "<td>";
5013			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5014			                             hash_base=>$hash, file_name=>$diff->{'file'}),
5015			              -class => "list"}, esc_path($diff->{'file'}));
5016			print "</td>\n";
5017			print "<td>$mode_chng</td>\n";
5018			print "<td class=\"link\">";
5019			if ($action eq 'commitdiff') {
5020				# link to patch
5021				$patchno++;
5022				print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5023				              "patch") .
5024				      " | ";
5025			}
5026			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5027			                             hash_base=>$hash, file_name=>$diff->{'file'})},
5028			              "blob");
5029			print "</td>\n";
5030
5031		} elsif ($diff->{'status'} eq "D") { # deleted
5032			my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
5033			print "<td>";
5034			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
5035			                             hash_base=>$parent, file_name=>$diff->{'file'}),
5036			               -class => "list"}, esc_path($diff->{'file'}));
5037			print "</td>\n";
5038			print "<td>$mode_chng</td>\n";
5039			print "<td class=\"link\">";
5040			if ($action eq 'commitdiff') {
5041				# link to patch
5042				$patchno++;
5043				print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5044				              "patch") .
5045				      " | ";
5046			}
5047			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
5048			                             hash_base=>$parent, file_name=>$diff->{'file'})},
5049			              "blob") . " | ";
5050			if ($have_blame) {
5051				print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
5052				                             file_name=>$diff->{'file'})},
5053				              "blame") . " | ";
5054			}
5055			print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
5056			                             file_name=>$diff->{'file'})},
5057			              "history");
5058			print "</td>\n";
5059
5060		} elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
5061			my $mode_chnge = "";
5062			if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
5063				$mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
5064				if ($from_file_type ne $to_file_type) {
5065					$mode_chnge .= " from $from_file_type to $to_file_type";
5066				}
5067				if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
5068					if ($from_mode_str && $to_mode_str) {
5069						$mode_chnge .= " mode: $from_mode_str->$to_mode_str";
5070					} elsif ($to_mode_str) {
5071						$mode_chnge .= " mode: $to_mode_str";
5072					}
5073				}
5074				$mode_chnge .= "]</span>\n";
5075			}
5076			print "<td>";
5077			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5078			                             hash_base=>$hash, file_name=>$diff->{'file'}),
5079			              -class => "list"}, esc_path($diff->{'file'}));
5080			print "</td>\n";
5081			print "<td>$mode_chnge</td>\n";
5082			print "<td class=\"link\">";
5083			if ($action eq 'commitdiff') {
5084				# link to patch
5085				$patchno++;
5086				print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5087				              "patch") .
5088				      " | ";
5089			} elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
5090				# "commit" view and modified file (not onlu mode changed)
5091				print $cgi->a({-href => href(action=>"blobdiff",
5092				                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
5093				                             hash_base=>$hash, hash_parent_base=>$parent,
5094				                             file_name=>$diff->{'file'})},
5095				              "diff") .
5096				      " | ";
5097			}
5098			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5099			                             hash_base=>$hash, file_name=>$diff->{'file'})},
5100			               "blob") . " | ";
5101			if ($have_blame) {
5102				print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
5103				                             file_name=>$diff->{'file'})},
5104				              "blame") . " | ";
5105			}
5106			print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
5107			                             file_name=>$diff->{'file'})},
5108			              "history");
5109			print "</td>\n";
5110
5111		} elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
5112			my %status_name = ('R' => 'moved', 'C' => 'copied');
5113			my $nstatus = $status_name{$diff->{'status'}};
5114			my $mode_chng = "";
5115			if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
5116				# mode also for directories, so we cannot use $to_mode_str
5117				$mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
5118			}
5119			print "<td>" .
5120			      $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
5121			                             hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
5122			              -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
5123			      "<td><span class=\"file_status $nstatus\">[$nstatus from " .
5124			      $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
5125			                             hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
5126			              -class => "list"}, esc_path($diff->{'from_file'})) .
5127			      " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
5128			      "<td class=\"link\">";
5129			if ($action eq 'commitdiff') {
5130				# link to patch
5131				$patchno++;
5132				print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5133				              "patch") .
5134				      " | ";
5135			} elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
5136				# "commit" view and modified file (not only pure rename or copy)
5137				print $cgi->a({-href => href(action=>"blobdiff",
5138				                             hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
5139				                             hash_base=>$hash, hash_parent_base=>$parent,
5140				                             file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
5141				              "diff") .
5142				      " | ";
5143			}
5144			print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5145			                             hash_base=>$parent, file_name=>$diff->{'to_file'})},
5146			              "blob") . " | ";
5147			if ($have_blame) {
5148				print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
5149				                             file_name=>$diff->{'to_file'})},
5150				              "blame") . " | ";
5151			}
5152			print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
5153			                            file_name=>$diff->{'to_file'})},
5154			              "history");
5155			print "</td>\n";
5156
5157		} # we should not encounter Unmerged (U) or Unknown (X) status
5158		print "</tr>\n";
5159	}
5160	print "</tbody>" if $has_header;
5161	print "</table>\n";
5162}
5163
5164# Print context lines and then rem/add lines in a side-by-side manner.
5165sub print_sidebyside_diff_lines {
5166	my ($ctx, $rem, $add) = @_;
5167
5168	# print context block before add/rem block
5169	if (@$ctx) {
5170		print join '',
5171			'<div class="chunk_block ctx">',
5172				'<div class="old">',
5173				@$ctx,
5174				'</div>',
5175				'<div class="new">',
5176				@$ctx,
5177				'</div>',
5178			'</div>';
5179	}
5180
5181	if (!@$add) {
5182		# pure removal
5183		print join '',
5184			'<div class="chunk_block rem">',
5185				'<div class="old">',
5186				@$rem,
5187				'</div>',
5188			'</div>';
5189	} elsif (!@$rem) {
5190		# pure addition
5191		print join '',
5192			'<div class="chunk_block add">',
5193				'<div class="new">',
5194				@$add,
5195				'</div>',
5196			'</div>';
5197	} else {
5198		print join '',
5199			'<div class="chunk_block chg">',
5200				'<div class="old">',
5201				@$rem,
5202				'</div>',
5203				'<div class="new">',
5204				@$add,
5205				'</div>',
5206			'</div>';
5207	}
5208}
5209
5210# Print context lines and then rem/add lines in inline manner.
5211sub print_inline_diff_lines {
5212	my ($ctx, $rem, $add) = @_;
5213
5214	print @$ctx, @$rem, @$add;
5215}
5216
5217# Format removed and added line, mark changed part and HTML-format them.
5218# Implementation is based on contrib/diff-highlight
5219sub format_rem_add_lines_pair {
5220	my ($rem, $add, $num_parents) = @_;
5221
5222	# We need to untabify lines before split()'ing them;
5223	# otherwise offsets would be invalid.
5224	chomp $rem;
5225	chomp $add;
5226	$rem = untabify($rem);
5227	$add = untabify($add);
5228
5229	my @rem = split(//, $rem);
5230	my @add = split(//, $add);
5231	my ($esc_rem, $esc_add);
5232	# Ignore leading +/- characters for each parent.
5233	my ($prefix_len, $suffix_len) = ($num_parents, 0);
5234	my ($prefix_has_nonspace, $suffix_has_nonspace);
5235
5236	my $shorter = (@rem < @add) ? @rem : @add;
5237	while ($prefix_len < $shorter) {
5238		last if ($rem[$prefix_len] ne $add[$prefix_len]);
5239
5240		$prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
5241		$prefix_len++;
5242	}
5243
5244	while ($prefix_len + $suffix_len < $shorter) {
5245		last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
5246
5247		$suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
5248		$suffix_len++;
5249	}
5250
5251	# Mark lines that are different from each other, but have some common
5252	# part that isn't whitespace.  If lines are completely different, don't
5253	# mark them because that would make output unreadable, especially if
5254	# diff consists of multiple lines.
5255	if ($prefix_has_nonspace || $suffix_has_nonspace) {
5256		$esc_rem = esc_html_hl_regions($rem, 'marked',
5257		        [$prefix_len, @rem - $suffix_len], -nbsp=>1);
5258		$esc_add = esc_html_hl_regions($add, 'marked',
5259		        [$prefix_len, @add - $suffix_len], -nbsp=>1);
5260	} else {
5261		$esc_rem = esc_html($rem, -nbsp=>1);
5262		$esc_add = esc_html($add, -nbsp=>1);
5263	}
5264
5265	return format_diff_line(\$esc_rem, 'rem'),
5266	       format_diff_line(\$esc_add, 'add');
5267}
5268
5269# HTML-format diff context, removed and added lines.
5270sub format_ctx_rem_add_lines {
5271	my ($ctx, $rem, $add, $num_parents) = @_;
5272	my (@new_ctx, @new_rem, @new_add);
5273	my $can_highlight = 0;
5274	my $is_combined = ($num_parents > 1);
5275
5276	# Highlight if every removed line has a corresponding added line.
5277	if (@$add > 0 && @$add == @$rem) {
5278		$can_highlight = 1;
5279
5280		# Highlight lines in combined diff only if the chunk contains
5281		# diff between the same version, e.g.
5282		#
5283		#    - a
5284		#   -  b
5285		#    + c
5286		#   +  d
5287		#
5288		# Otherwise the highlighting would be confusing.
5289		if ($is_combined) {
5290			for (my $i = 0; $i < @$add; $i++) {
5291				my $prefix_rem = substr($rem->[$i], 0, $num_parents);
5292				my $prefix_add = substr($add->[$i], 0, $num_parents);
5293
5294				$prefix_rem =~ s/-/+/g;
5295
5296				if ($prefix_rem ne $prefix_add) {
5297					$can_highlight = 0;
5298					last;
5299				}
5300			}
5301		}
5302	}
5303
5304	if ($can_highlight) {
5305		for (my $i = 0; $i < @$add; $i++) {
5306			my ($line_rem, $line_add) = format_rem_add_lines_pair(
5307			        $rem->[$i], $add->[$i], $num_parents);
5308			push @new_rem, $line_rem;
5309			push @new_add, $line_add;
5310		}
5311	} else {
5312		@new_rem = map { format_diff_line($_, 'rem') } @$rem;
5313		@new_add = map { format_diff_line($_, 'add') } @$add;
5314	}
5315
5316	@new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
5317
5318	return (\@new_ctx, \@new_rem, \@new_add);
5319}
5320
5321# Print context lines and then rem/add lines.
5322sub print_diff_lines {
5323	my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
5324	my $is_combined = $num_parents > 1;
5325
5326	($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
5327	        $num_parents);
5328
5329	if ($diff_style eq 'sidebyside' && !$is_combined) {
5330		print_sidebyside_diff_lines($ctx, $rem, $add);
5331	} else {
5332		# default 'inline' style and unknown styles
5333		print_inline_diff_lines($ctx, $rem, $add);
5334	}
5335}
5336
5337sub print_diff_chunk {
5338	my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
5339	my (@ctx, @rem, @add);
5340
5341	# The class of the previous line.
5342	my $prev_class = '';
5343
5344	return unless @chunk;
5345
5346	# incomplete last line might be among removed or added lines,
5347	# or both, or among context lines: find which
5348	for (my $i = 1; $i < @chunk; $i++) {
5349		if ($chunk[$i][0] eq 'incomplete') {
5350			$chunk[$i][0] = $chunk[$i-1][0];
5351		}
5352	}
5353
5354	# guardian
5355	push @chunk, ["", ""];
5356
5357	foreach my $line_info (@chunk) {
5358		my ($class, $line) = @$line_info;
5359
5360		# print chunk headers
5361		if ($class && $class eq 'chunk_header') {
5362			print format_diff_line($line, $class, $from, $to);
5363			next;
5364		}
5365
5366		## print from accumulator when have some add/rem lines or end
5367		# of chunk (flush context lines), or when have add and rem
5368		# lines and new block is reached (otherwise add/rem lines could
5369		# be reordered)
5370		if (!$class || ((@rem || @add) && $class eq 'ctx') ||
5371		    (@rem && @add && $class ne $prev_class)) {
5372			print_diff_lines(\@ctx, \@rem, \@add,
5373		                         $diff_style, $num_parents);
5374			@ctx = @rem = @add = ();
5375		}
5376
5377		## adding lines to accumulator
5378		# guardian value
5379		last unless $line;
5380		# rem, add or change
5381		if ($class eq 'rem') {
5382			push @rem, $line;
5383		} elsif ($class eq 'add') {
5384			push @add, $line;
5385		}
5386		# context line
5387		if ($class eq 'ctx') {
5388			push @ctx, $line;
5389		}
5390
5391		$prev_class = $class;
5392	}
5393}
5394
5395sub git_patchset_body {
5396	my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
5397	my ($hash_parent) = $hash_parents[0];
5398
5399	my $is_combined = (@hash_parents > 1);
5400	my $patch_idx = 0;
5401	my $patch_number = 0;
5402	my $patch_line;
5403	my $diffinfo;
5404	my $to_name;
5405	my (%from, %to);
5406	my @chunk; # for side-by-side diff
5407
5408	print "<div class=\"patchset\">\n";
5409
5410	# skip to first patch
5411	while ($patch_line = <$fd>) {
5412		chomp $patch_line;
5413
5414		last if ($patch_line =~ m/^diff /);
5415	}
5416
5417 PATCH:
5418	while ($patch_line) {
5419
5420		# parse "git diff" header line
5421		if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
5422			# $1 is from_name, which we do not use
5423			$to_name = unquote($2);
5424			$to_name =~ s!^b/!!;
5425		} elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
5426			# $1 is 'cc' or 'combined', which we do not use
5427			$to_name = unquote($2);
5428		} else {
5429			$to_name = undef;
5430		}
5431
5432		# check if current patch belong to current raw line
5433		# and parse raw git-diff line if needed
5434		if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
5435			# this is continuation of a split patch
5436			print "<div class=\"patch cont\">\n";
5437		} else {
5438			# advance raw git-diff output if needed
5439			$patch_idx++ if defined $diffinfo;
5440
5441			# read and prepare patch information
5442			$diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5443
5444			# compact combined diff output can have some patches skipped
5445			# find which patch (using pathname of result) we are at now;
5446			if ($is_combined) {
5447				while ($to_name ne $diffinfo->{'to_file'}) {
5448					print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5449					      format_diff_cc_simplified($diffinfo, @hash_parents) .
5450					      "</div>\n";  # class="patch"
5451
5452					$patch_idx++;
5453					$patch_number++;
5454
5455					last if $patch_idx > $#$difftree;
5456					$diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5457				}
5458			}
5459
5460			# modifies %from, %to hashes
5461			parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5462
5463			# this is first patch for raw difftree line with $patch_idx index
5464			# we index @$difftree array from 0, but number patches from 1
5465			print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
5466		}
5467
5468		# git diff header
5469		#assert($patch_line =~ m/^diff /) if DEBUG;
5470		#assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5471		$patch_number++;
5472		# print "git diff" header
5473		print format_git_diff_header_line($patch_line, $diffinfo,
5474		                                  \%from, \%to);
5475
5476		# print extended diff header
5477		print "<div class=\"diff extended_header\">\n";
5478	EXTENDED_HEADER:
5479		while ($patch_line = <$fd>) {
5480			chomp $patch_line;
5481
5482			last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
5483
5484			print format_extended_diff_header_line($patch_line, $diffinfo,
5485			                                       \%from, \%to);
5486		}
5487		print "</div>\n"; # class="diff extended_header"
5488
5489		# from-file/to-file diff header
5490		if (! $patch_line) {
5491			print "</div>\n"; # class="patch"
5492			last PATCH;
5493		}
5494		next PATCH if ($patch_line =~ m/^diff /);
5495		#assert($patch_line =~ m/^---/) if DEBUG;
5496
5497		my $last_patch_line = $patch_line;
5498		$patch_line = <$fd>;
5499		chomp $patch_line;
5500		#assert($patch_line =~ m/^\+\+\+/) if DEBUG;
5501
5502		print format_diff_from_to_header($last_patch_line, $patch_line,
5503		                                 $diffinfo, \%from, \%to,
5504		                                 @hash_parents);
5505
5506		# the patch itself
5507	LINE:
5508		while ($patch_line = <$fd>) {
5509			chomp $patch_line;
5510
5511			next PATCH if ($patch_line =~ m/^diff /);
5512
5513			my $class = diff_line_class($patch_line, \%from, \%to);
5514
5515			if ($class eq 'chunk_header') {
5516				print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5517				@chunk = ();
5518			}
5519
5520			push @chunk, [ $class, $patch_line ];
5521		}
5522
5523	} continue {
5524		if (@chunk) {
5525			print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5526			@chunk = ();
5527		}
5528		print "</div>\n"; # class="patch"
5529	}
5530
5531	# for compact combined (--cc) format, with chunk and patch simplification
5532	# the patchset might be empty, but there might be unprocessed raw lines
5533	for (++$patch_idx if $patch_number > 0;
5534	     $patch_idx < @$difftree;
5535	     ++$patch_idx) {
5536		# read and prepare patch information
5537		$diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5538
5539		# generate anchor for "patch" links in difftree / whatchanged part
5540		print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5541		      format_diff_cc_simplified($diffinfo, @hash_parents) .
5542		      "</div>\n";  # class="patch"
5543
5544		$patch_number++;
5545	}
5546
5547	if ($patch_number == 0) {
5548		if (@hash_parents > 1) {
5549			print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
5550		} else {
5551			print "<div class=\"diff nodifferences\">No differences found</div>\n";
5552		}
5553	}
5554
5555	print "</div>\n"; # class="patchset"
5556}
5557
5558# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5559
5560sub git_project_search_form {
5561	my ($searchtext, $search_use_regexp) = @_;
5562
5563	my $limit = '';
5564	if ($project_filter) {
5565		$limit = " in '$project_filter/'";
5566	}
5567
5568	print "<div class=\"projsearch\">\n";
5569	print $cgi->start_form(-method => 'get', -action => $my_uri) .
5570	      $cgi->hidden(-name => 'a', -value => 'project_list')  . "\n";
5571	print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
5572		if (defined $project_filter);
5573	print $cgi->textfield(-name => 's', -value => $searchtext,
5574	                      -title => "Search project by name and description$limit",
5575	                      -size => 60) . "\n" .
5576	      "<span title=\"Extended regular expression\">" .
5577	      $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5578	                     -checked => $search_use_regexp) .
5579	      "</span>\n" .
5580	      $cgi->submit(-name => 'btnS', -value => 'Search') .
5581	      $cgi->end_form() . "\n" .
5582	      $cgi->a({-href => href(project => undef, searchtext => undef,
5583	                             project_filter => $project_filter)},
5584	              esc_html("List all projects$limit")) . "<br />\n";
5585	print "</div>\n";
5586}
5587
5588# entry for given @keys needs filling if at least one of keys in list
5589# is not present in %$project_info
5590sub project_info_needs_filling {
5591	my ($project_info, @keys) = @_;
5592
5593	# return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
5594	foreach my $key (@keys) {
5595		if (!exists $project_info->{$key}) {
5596			return 1;
5597		}
5598	}
5599	return;
5600}
5601
5602# fills project list info (age, description, owner, category, forks, etc.)
5603# for each project in the list, removing invalid projects from
5604# returned list, or fill only specified info.
5605#
5606# Invalid projects are removed from the returned list if and only if you
5607# ask 'age' or 'age_string' to be filled, because they are the only fields
5608# that run unconditionally git command that requires repository, and
5609# therefore do always check if project repository is invalid.
5610#
5611# USAGE:
5612# * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
5613#   ensures that 'descr_long' and 'ctags' fields are filled
5614# * @project_list = fill_project_list_info(\@project_list)
5615#   ensures that all fields are filled (and invalid projects removed)
5616#
5617# NOTE: modifies $projlist, but does not remove entries from it
5618sub fill_project_list_info {
5619	my ($projlist, @wanted_keys) = @_;
5620	my @projects;
5621	my $filter_set = sub { return @_; };
5622	if (@wanted_keys) {
5623		my %wanted_keys = map { $_ => 1 } @wanted_keys;
5624		$filter_set = sub { return grep { $wanted_keys{$_} } @_; };
5625	}
5626
5627	my $show_ctags = gitweb_check_feature('ctags');
5628 PROJECT:
5629	foreach my $pr (@$projlist) {
5630		if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
5631			my (@activity) = git_get_last_activity($pr->{'path'});
5632			unless (@activity) {
5633				next PROJECT;
5634			}
5635			($pr->{'age'}, $pr->{'age_string'}) = @activity;
5636		}
5637		if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
5638			my $descr = git_get_project_description($pr->{'path'}) || "";
5639			$descr = to_utf8($descr);
5640			$pr->{'descr_long'} = $descr;
5641			$pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
5642		}
5643		if (project_info_needs_filling($pr, $filter_set->('owner'))) {
5644			$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
5645		}
5646		if ($show_ctags &&
5647		    project_info_needs_filling($pr, $filter_set->('ctags'))) {
5648			$pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
5649		}
5650		if ($projects_list_group_categories &&
5651		    project_info_needs_filling($pr, $filter_set->('category'))) {
5652			my $cat = git_get_project_category($pr->{'path'}) ||
5653			                                   $project_list_default_category;
5654			$pr->{'category'} = to_utf8($cat);
5655		}
5656
5657		push @projects, $pr;
5658	}
5659
5660	return @projects;
5661}
5662
5663sub sort_projects_list {
5664	my ($projlist, $order) = @_;
5665
5666	sub order_str {
5667		my $key = shift;
5668		return sub { $a->{$key} cmp $b->{$key} };
5669	}
5670
5671	sub order_num_then_undef {
5672		my $key = shift;
5673		return sub {
5674			defined $a->{$key} ?
5675				(defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) :
5676				(defined $b->{$key} ? 1 : 0)
5677		};
5678	}
5679
5680	my %orderings = (
5681		project => order_str('path'),
5682		descr => order_str('descr_long'),
5683		owner => order_str('owner'),
5684		age => order_num_then_undef('age'),
5685	);
5686
5687	my $ordering = $orderings{$order};
5688	return defined $ordering ? sort $ordering @$projlist : @$projlist;
5689}
5690
5691# returns a hash of categories, containing the list of project
5692# belonging to each category
5693sub build_projlist_by_category {
5694	my ($projlist, $from, $to) = @_;
5695	my %categories;
5696
5697	$from = 0 unless defined $from;
5698	$to = $#$projlist if (!defined $to || $#$projlist < $to);
5699
5700	for (my $i = $from; $i <= $to; $i++) {
5701		my $pr = $projlist->[$i];
5702		push @{$categories{ $pr->{'category'} }}, $pr;
5703	}
5704
5705	return wantarray ? %categories : \%categories;
5706}
5707
5708# print 'sort by' <th> element, generating 'sort by $name' replay link
5709# if that order is not selected
5710sub print_sort_th {
5711	print format_sort_th(@_);
5712}
5713
5714sub format_sort_th {
5715	my ($name, $order, $header) = @_;
5716	my $sort_th = "";
5717	$header ||= ucfirst($name);
5718
5719	if ($order eq $name) {
5720		$sort_th .= "<th>$header</th>\n";
5721	} else {
5722		$sort_th .= "<th>" .
5723		            $cgi->a({-href => href(-replay=>1, order=>$name),
5724		                     -class => "header"}, $header) .
5725		            "</th>\n";
5726	}
5727
5728	return $sort_th;
5729}
5730
5731sub git_project_list_rows {
5732	my ($projlist, $from, $to, $check_forks) = @_;
5733
5734	$from = 0 unless defined $from;
5735	$to = $#$projlist if (!defined $to || $#$projlist < $to);
5736
5737	my $alternate = 1;
5738	for (my $i = $from; $i <= $to; $i++) {
5739		my $pr = $projlist->[$i];
5740
5741		if ($alternate) {
5742			print "<tr class=\"dark\">\n";
5743		} else {
5744			print "<tr class=\"light\">\n";
5745		}
5746		$alternate ^= 1;
5747
5748		if ($check_forks) {
5749			print "<td>";
5750			if ($pr->{'forks'}) {
5751				my $nforks = scalar @{$pr->{'forks'}};
5752				if ($nforks > 0) {
5753					print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5754					               -title => "$nforks forks"}, "+");
5755				} else {
5756					print $cgi->span({-title => "$nforks forks"}, "+");
5757				}
5758			}
5759			print "</td>\n";
5760		}
5761		print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5762		                        -class => "list"},
5763		                       esc_html_match_hl($pr->{'path'}, $search_regexp)) .
5764		      "</td>\n" .
5765		      "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5766		                        -class => "list",
5767		                        -title => $pr->{'descr_long'}},
5768		                        $search_regexp
5769		                        ? esc_html_match_hl_chopped($pr->{'descr_long'},
5770		                                                    $pr->{'descr'}, $search_regexp)
5771		                        : esc_html($pr->{'descr'})) .
5772		      "</td>\n";
5773		unless ($omit_owner) {
5774		        print "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5775		}
5776		unless ($omit_age_column) {
5777		        print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5778		            (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n";
5779		}
5780		print"<td class=\"link\">" .
5781		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary")   . " | " .
5782		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5783		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5784		      $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5785		      ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5786		      "</td>\n" .
5787		      "</tr>\n";
5788	}
5789}
5790
5791sub git_project_list_body {
5792	# actually uses global variable $project
5793	my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5794	my @projects = @$projlist;
5795
5796	my $check_forks = gitweb_check_feature('forks');
5797	my $show_ctags  = gitweb_check_feature('ctags');
5798	my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
5799	$check_forks = undef
5800		if ($tagfilter || $search_regexp);
5801
5802	# filtering out forks before filling info allows to do less work
5803	@projects = filter_forks_from_projects_list(\@projects)
5804		if ($check_forks);
5805	# search_projects_list pre-fills required info
5806	@projects = search_projects_list(\@projects,
5807	                                 'search_regexp' => $search_regexp,
5808	                                 'tagfilter'  => $tagfilter)
5809		if ($tagfilter || $search_regexp);
5810	# fill the rest
5811	my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
5812	push @all_fields, ('age', 'age_string') unless($omit_age_column);
5813	push @all_fields, 'owner' unless($omit_owner);
5814	@projects = fill_project_list_info(\@projects, @all_fields);
5815
5816	$order ||= $default_projects_order;
5817	$from = 0 unless defined $from;
5818	$to = $#projects if (!defined $to || $#projects < $to);
5819
5820	# short circuit
5821	if ($from > $to) {
5822		print "<center>\n".
5823		      "<b>No such projects found</b><br />\n".
5824		      "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5825		      "</center>\n<br />\n";
5826		return;
5827	}
5828
5829	@projects = sort_projects_list(\@projects, $order);
5830
5831	if ($show_ctags) {
5832		my $ctags = git_gather_all_ctags(\@projects);
5833		my $cloud = git_populate_project_tagcloud($ctags);
5834		print git_show_project_tagcloud($cloud, 64);
5835	}
5836
5837	print "<table class=\"project_list\">\n";
5838	unless ($no_header) {
5839		print "<tr>\n";
5840		if ($check_forks) {
5841			print "<th></th>\n";
5842		}
5843		print_sort_th('project', $order, 'Project');
5844		print_sort_th('descr', $order, 'Description');
5845		print_sort_th('owner', $order, 'Owner') unless $omit_owner;
5846		print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
5847		print "<th></th>\n" . # for links
5848		      "</tr>\n";
5849	}
5850
5851	if ($projects_list_group_categories) {
5852		# only display categories with projects in the $from-$to window
5853		@projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
5854		my %categories = build_projlist_by_category(\@projects, $from, $to);
5855		foreach my $cat (sort keys %categories) {
5856			unless ($cat eq "") {
5857				print "<tr>\n";
5858				if ($check_forks) {
5859					print "<td></td>\n";
5860				}
5861				print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
5862				print "</tr>\n";
5863			}
5864
5865			git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
5866		}
5867	} else {
5868		git_project_list_rows(\@projects, $from, $to, $check_forks);
5869	}
5870
5871	if (defined $extra) {
5872		print "<tr>\n";
5873		if ($check_forks) {
5874			print "<td></td>\n";
5875		}
5876		print "<td colspan=\"5\">$extra</td>\n" .
5877		      "</tr>\n";
5878	}
5879	print "</table>\n";
5880}
5881
5882sub git_log_body {
5883	# uses global variable $project
5884	my ($commitlist, $from, $to, $refs, $extra) = @_;
5885
5886	$from = 0 unless defined $from;
5887	$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5888
5889	for (my $i = 0; $i <= $to; $i++) {
5890		my %co = %{$commitlist->[$i]};
5891		next if !%co;
5892		my $commit = $co{'id'};
5893		my $ref = format_ref_marker($refs, $commit);
5894		git_print_header_div('commit',
5895		               "<span class=\"age\">$co{'age_string'}</span>" .
5896		               esc_html($co{'title'}) . $ref,
5897		               $commit);
5898		print "<div class=\"title_text\">\n" .
5899		      "<div class=\"log_link\">\n" .
5900		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5901		      " | " .
5902		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5903		      " | " .
5904		      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5905		      "<br/>\n" .
5906		      "</div>\n";
5907		      git_print_authorship(\%co, -tag => 'span');
5908		      print "<br/>\n</div>\n";
5909
5910		print "<div class=\"log_body\">\n";
5911		git_print_log($co{'comment'}, -final_empty_line=> 1);
5912		print "</div>\n";
5913	}
5914	if ($extra) {
5915		print "<div class=\"page_nav\">\n";
5916		print "$extra\n";
5917		print "</div>\n";
5918	}
5919}
5920
5921sub git_shortlog_body {
5922	# uses global variable $project
5923	my ($commitlist, $from, $to, $refs, $extra) = @_;
5924
5925	$from = 0 unless defined $from;
5926	$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
5927
5928	print "<table class=\"shortlog\">\n";
5929	my $alternate = 1;
5930	for (my $i = $from; $i <= $to; $i++) {
5931		my %co = %{$commitlist->[$i]};
5932		my $commit = $co{'id'};
5933		my $ref = format_ref_marker($refs, $commit);
5934		if ($alternate) {
5935			print "<tr class=\"dark\">\n";
5936		} else {
5937			print "<tr class=\"light\">\n";
5938		}
5939		$alternate ^= 1;
5940		# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
5941		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5942		      format_author_html('td', \%co, 10) . "<td>";
5943		print format_subject_html($co{'title'}, $co{'title_short'},
5944		                          href(action=>"commit", hash=>$commit), $ref);
5945		print "</td>\n" .
5946		      "<td class=\"link\">" .
5947		      $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
5948		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
5949		      $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
5950		my $snapshot_links = format_snapshot_links($commit);
5951		if (defined $snapshot_links) {
5952			print " | " . $snapshot_links;
5953		}
5954		print "</td>\n" .
5955		      "</tr>\n";
5956	}
5957	if (defined $extra) {
5958		print "<tr>\n" .
5959		      "<td colspan=\"4\">$extra</td>\n" .
5960		      "</tr>\n";
5961	}
5962	print "</table>\n";
5963}
5964
5965sub git_history_body {
5966	# Warning: assumes constant type (blob or tree) during history
5967	my ($commitlist, $from, $to, $refs, $extra,
5968	    $file_name, $file_hash, $ftype) = @_;
5969
5970	$from = 0 unless defined $from;
5971	$to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
5972
5973	print "<table class=\"history\">\n";
5974	my $alternate = 1;
5975	for (my $i = $from; $i <= $to; $i++) {
5976		my %co = %{$commitlist->[$i]};
5977		if (!%co) {
5978			next;
5979		}
5980		my $commit = $co{'id'};
5981
5982		my $ref = format_ref_marker($refs, $commit);
5983
5984		if ($alternate) {
5985			print "<tr class=\"dark\">\n";
5986		} else {
5987			print "<tr class=\"light\">\n";
5988		}
5989		$alternate ^= 1;
5990		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5991	# shortlog:   format_author_html('td', \%co, 10)
5992		      format_author_html('td', \%co, 15, 3) . "<td>";
5993		# originally git_history used chop_str($co{'title'}, 50)
5994		print format_subject_html($co{'title'}, $co{'title_short'},
5995		                          href(action=>"commit", hash=>$commit), $ref);
5996		print "</td>\n" .
5997		      "<td class=\"link\">" .
5998		      $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
5999		      $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
6000
6001		if ($ftype eq 'blob') {
6002			print " | " .
6003			      $cgi->a({-href => href(action=>"blob_plain", hash_base=>$commit, file_name=>$file_name)}, "raw");
6004
6005			my $blob_current = $file_hash;
6006			my $blob_parent  = git_get_hash_by_path($commit, $file_name);
6007			if (defined $blob_current && defined $blob_parent &&
6008					$blob_current ne $blob_parent) {
6009				print " | " .
6010					$cgi->a({-href => href(action=>"blobdiff",
6011					                       hash=>$blob_current, hash_parent=>$blob_parent,
6012					                       hash_base=>$hash_base, hash_parent_base=>$commit,
6013					                       file_name=>$file_name)},
6014					        "diff to current");
6015			}
6016		}
6017		print "</td>\n" .
6018		      "</tr>\n";
6019	}
6020	if (defined $extra) {
6021		print "<tr>\n" .
6022		      "<td colspan=\"4\">$extra</td>\n" .
6023		      "</tr>\n";
6024	}
6025	print "</table>\n";
6026}
6027
6028sub git_tags_body {
6029	# uses global variable $project
6030	my ($taglist, $from, $to, $extra) = @_;
6031	$from = 0 unless defined $from;
6032	$to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
6033
6034	print "<table class=\"tags\">\n";
6035	my $alternate = 1;
6036	for (my $i = $from; $i <= $to; $i++) {
6037		my $entry = $taglist->[$i];
6038		my %tag = %$entry;
6039		my $comment = $tag{'subject'};
6040		my $comment_short;
6041		if (defined $comment) {
6042			$comment_short = chop_str($comment, 30, 5);
6043		}
6044		if ($alternate) {
6045			print "<tr class=\"dark\">\n";
6046		} else {
6047			print "<tr class=\"light\">\n";
6048		}
6049		$alternate ^= 1;
6050		if (defined $tag{'age'}) {
6051			print "<td><i>$tag{'age'}</i></td>\n";
6052		} else {
6053			print "<td></td>\n";
6054		}
6055		print "<td>" .
6056		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
6057		               -class => "list name"}, esc_html($tag{'name'})) .
6058		      "</td>\n" .
6059		      "<td>";
6060		if (defined $comment) {
6061			print format_subject_html($comment, $comment_short,
6062			                          href(action=>"tag", hash=>$tag{'id'}));
6063		}
6064		print "</td>\n" .
6065		      "<td class=\"selflink\">";
6066		if ($tag{'type'} eq "tag") {
6067			print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
6068		} else {
6069			print "&nbsp;";
6070		}
6071		print "</td>\n" .
6072		      "<td class=\"link\">" . " | " .
6073		      $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
6074		if ($tag{'reftype'} eq "commit") {
6075			print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
6076			      " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
6077		} elsif ($tag{'reftype'} eq "blob") {
6078			print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
6079		}
6080		print "</td>\n" .
6081		      "</tr>";
6082	}
6083	if (defined $extra) {
6084		print "<tr>\n" .
6085		      "<td colspan=\"5\">$extra</td>\n" .
6086		      "</tr>\n";
6087	}
6088	print "</table>\n";
6089}
6090
6091sub git_heads_body {
6092	# uses global variable $project
6093	my ($headlist, $head_at, $from, $to, $extra) = @_;
6094	$from = 0 unless defined $from;
6095	$to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
6096
6097	print "<table class=\"heads\">\n";
6098	my $alternate = 1;
6099	for (my $i = $from; $i <= $to; $i++) {
6100		my $entry = $headlist->[$i];
6101		my %ref = %$entry;
6102		my $curr = defined $head_at && $ref{'id'} eq $head_at;
6103		if ($alternate) {
6104			print "<tr class=\"dark\">\n";
6105		} else {
6106			print "<tr class=\"light\">\n";
6107		}
6108		$alternate ^= 1;
6109		print "<td><i>$ref{'age'}</i></td>\n" .
6110		      ($curr ? "<td class=\"current_head\">" : "<td>") .
6111		      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
6112		               -class => "list name"},esc_html($ref{'name'})) .
6113		      "</td>\n" .
6114		      "<td class=\"link\">" .
6115		      $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
6116		      $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
6117		      $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
6118		      "</td>\n" .
6119		      "</tr>";
6120	}
6121	if (defined $extra) {
6122		print "<tr>\n" .
6123		      "<td colspan=\"3\">$extra</td>\n" .
6124		      "</tr>\n";
6125	}
6126	print "</table>\n";
6127}
6128
6129# Display a single remote block
6130sub git_remote_block {
6131	my ($remote, $rdata, $limit, $head) = @_;
6132
6133	my $heads = $rdata->{'heads'};
6134	my $fetch = $rdata->{'fetch'};
6135	my $push = $rdata->{'push'};
6136
6137	my $urls_table = "<table class=\"projects_list\">\n" ;
6138
6139	if (defined $fetch) {
6140		if ($fetch eq $push) {
6141			$urls_table .= format_repo_url("URL", $fetch);
6142		} else {
6143			$urls_table .= format_repo_url("Fetch URL", $fetch);
6144			$urls_table .= format_repo_url("Push URL", $push) if defined $push;
6145		}
6146	} elsif (defined $push) {
6147		$urls_table .= format_repo_url("Push URL", $push);
6148	} else {
6149		$urls_table .= format_repo_url("", "No remote URL");
6150	}
6151
6152	$urls_table .= "</table>\n";
6153
6154	my $dots;
6155	if (defined $limit && $limit < @$heads) {
6156		$dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
6157	}
6158
6159	print $urls_table;
6160	git_heads_body($heads, $head, 0, $limit, $dots);
6161}
6162
6163# Display a list of remote names with the respective fetch and push URLs
6164sub git_remotes_list {
6165	my ($remotedata, $limit) = @_;
6166	print "<table class=\"heads\">\n";
6167	my $alternate = 1;
6168	my @remotes = sort keys %$remotedata;
6169
6170	my $limited = $limit && $limit < @remotes;
6171
6172	$#remotes = $limit - 1 if $limited;
6173
6174	while (my $remote = shift @remotes) {
6175		my $rdata = $remotedata->{$remote};
6176		my $fetch = $rdata->{'fetch'};
6177		my $push = $rdata->{'push'};
6178		if ($alternate) {
6179			print "<tr class=\"dark\">\n";
6180		} else {
6181			print "<tr class=\"light\">\n";
6182		}
6183		$alternate ^= 1;
6184		print "<td>" .
6185		      $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
6186			       -class=> "list name"},esc_html($remote)) .
6187		      "</td>";
6188		print "<td class=\"link\">" .
6189		      (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
6190		      " | " .
6191		      (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
6192		      "</td>";
6193
6194		print "</tr>\n";
6195	}
6196
6197	if ($limited) {
6198		print "<tr>\n" .
6199		      "<td colspan=\"3\">" .
6200		      $cgi->a({-href => href(action=>"remotes")}, "...") .
6201		      "</td>\n" . "</tr>\n";
6202	}
6203
6204	print "</table>";
6205}
6206
6207# Display remote heads grouped by remote, unless there are too many
6208# remotes, in which case we only display the remote names
6209sub git_remotes_body {
6210	my ($remotedata, $limit, $head) = @_;
6211	if ($limit and $limit < keys %$remotedata) {
6212		git_remotes_list($remotedata, $limit);
6213	} else {
6214		fill_remote_heads($remotedata);
6215		while (my ($remote, $rdata) = each %$remotedata) {
6216			git_print_section({-class=>"remote", -id=>$remote},
6217				["remotes", $remote, $remote], sub {
6218					git_remote_block($remote, $rdata, $limit, $head);
6219				});
6220		}
6221	}
6222}
6223
6224sub git_search_message {
6225	my %co = @_;
6226
6227	my $greptype;
6228	if ($searchtype eq 'commit') {
6229		$greptype = "--grep=";
6230	} elsif ($searchtype eq 'author') {
6231		$greptype = "--author=";
6232	} elsif ($searchtype eq 'committer') {
6233		$greptype = "--committer=";
6234	}
6235	$greptype .= $searchtext;
6236	my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6237	                               $greptype, '--regexp-ignore-case',
6238	                               $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6239
6240	my $paging_nav = '';
6241	if ($page > 0) {
6242		$paging_nav .=
6243			$cgi->a({-href => href(-replay=>1, page=>undef)},
6244			        "first") .
6245			" &sdot; " .
6246			$cgi->a({-href => href(-replay=>1, page=>$page-1),
6247			         -accesskey => "p", -title => "Alt-p"}, "prev");
6248	} else {
6249		$paging_nav .= "first &sdot; prev";
6250	}
6251	my $next_link = '';
6252	if ($#commitlist >= 100) {
6253		$next_link =
6254			$cgi->a({-href => href(-replay=>1, page=>$page+1),
6255			         -accesskey => "n", -title => "Alt-n"}, "next");
6256		$paging_nav .= " &sdot; $next_link";
6257	} else {
6258		$paging_nav .= " &sdot; next";
6259	}
6260
6261	git_header_html();
6262
6263	git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6264	git_print_header_div('commit', esc_html($co{'title'}), $hash);
6265	if ($page == 0 && !@commitlist) {
6266		print "<p>No match.</p>\n";
6267	} else {
6268		git_search_grep_body(\@commitlist, 0, 99, $next_link);
6269	}
6270
6271	git_footer_html();
6272}
6273
6274sub git_search_changes {
6275	my %co = @_;
6276
6277	local $/ = "\n";
6278	open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6279		'--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6280		($search_use_regexp ? '--pickaxe-regex' : ())
6281			or die_error(500, "Open git-log failed");
6282
6283	git_header_html();
6284
6285	git_print_page_nav('','', $hash,$co{'tree'},$hash);
6286	git_print_header_div('commit', esc_html($co{'title'}), $hash);
6287
6288	print "<table class=\"pickaxe search\">\n";
6289	my $alternate = 1;
6290	undef %co;
6291	my @files;
6292	while (my $line = <$fd>) {
6293		chomp $line;
6294		next unless $line;
6295
6296		my %set = parse_difftree_raw_line($line);
6297		if (defined $set{'commit'}) {
6298			# finish previous commit
6299			if (%co) {
6300				print "</td>\n" .
6301				      "<td class=\"link\">" .
6302				      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6303				              "commit") .
6304				      " | " .
6305				      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6306				                             hash_base=>$co{'id'})},
6307				              "tree") .
6308				      "</td>\n" .
6309				      "</tr>\n";
6310			}
6311
6312			if ($alternate) {
6313				print "<tr class=\"dark\">\n";
6314			} else {
6315				print "<tr class=\"light\">\n";
6316			}
6317			$alternate ^= 1;
6318			%co = parse_commit($set{'commit'});
6319			my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6320			print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6321			      "<td><i>$author</i></td>\n" .
6322			      "<td>" .
6323			      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6324			              -class => "list subject"},
6325			              chop_and_escape_str($co{'title'}, 50) . "<br/>");
6326		} elsif (defined $set{'to_id'}) {
6327			next if is_deleted(\%set);
6328
6329			print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6330			                             hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6331			              -class => "list"},
6332			              "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6333			      "<br/>\n";
6334		}
6335	}
6336	close $fd;
6337
6338	# finish last commit (warning: repetition!)
6339	if (%co) {
6340		print "</td>\n" .
6341		      "<td class=\"link\">" .
6342		      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6343		              "commit") .
6344		      " | " .
6345		      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6346		                             hash_base=>$co{'id'})},
6347		              "tree") .
6348		      "</td>\n" .
6349		      "</tr>\n";
6350	}
6351
6352	print "</table>\n";
6353
6354	git_footer_html();
6355}
6356
6357sub git_search_files {
6358	my %co = @_;
6359
6360	local $/ = "\n";
6361	open my $fd, "-|", git_cmd(), 'grep', '-n', '-z',
6362		$search_use_regexp ? ('-E', '-i') : '-F',
6363		$searchtext, $co{'tree'}
6364			or die_error(500, "Open git-grep failed");
6365
6366	git_header_html();
6367
6368	git_print_page_nav('','', $hash,$co{'tree'},$hash);
6369	git_print_header_div('commit', esc_html($co{'title'}), $hash);
6370
6371	print "<table class=\"grep_search\">\n";
6372	my $alternate = 1;
6373	my $matches = 0;
6374	my $lastfile = '';
6375	my $file_href;
6376	while (my $line = <$fd>) {
6377		chomp $line;
6378		my ($file, $lno, $ltext, $binary);
6379		last if ($matches++ > 1000);
6380		if ($line =~ /^Binary file (.+) matches$/) {
6381			$file = $1;
6382			$binary = 1;
6383		} else {
6384			($file, $lno, $ltext) = split(/\0/, $line, 3);
6385			$file =~ s/^$co{'tree'}://;
6386		}
6387		if ($file ne $lastfile) {
6388			$lastfile and print "</td></tr>\n";
6389			if ($alternate++) {
6390				print "<tr class=\"dark\">\n";
6391			} else {
6392				print "<tr class=\"light\">\n";
6393			}
6394			$file_href = href(action=>"blob", hash_base=>$co{'id'},
6395			                  file_name=>$file);
6396			print "<td class=\"list\">".
6397				$cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
6398			print "</td><td>\n";
6399			$lastfile = $file;
6400		}
6401		if ($binary) {
6402			print "<div class=\"binary\">Binary file</div>\n";
6403		} else {
6404			$ltext = untabify($ltext);
6405			if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6406				$ltext = esc_html($1, -nbsp=>1);
6407				$ltext .= '<span class="match">';
6408				$ltext .= esc_html($2, -nbsp=>1);
6409				$ltext .= '</span>';
6410				$ltext .= esc_html($3, -nbsp=>1);
6411			} else {
6412				$ltext = esc_html($ltext, -nbsp=>1);
6413			}
6414			print "<div class=\"pre\">" .
6415				$cgi->a({-href => $file_href.'#l'.$lno,
6416				        -class => "linenr"}, sprintf('%4i', $lno)) .
6417				' ' .  $ltext . "</div>\n";
6418		}
6419	}
6420	if ($lastfile) {
6421		print "</td></tr>\n";
6422		if ($matches > 1000) {
6423			print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6424		}
6425	} else {
6426		print "<div class=\"diff nodifferences\">No matches found</div>\n";
6427	}
6428	close $fd;
6429
6430	print "</table>\n";
6431
6432	git_footer_html();
6433}
6434
6435sub git_search_grep_body {
6436	my ($commitlist, $from, $to, $extra) = @_;
6437	$from = 0 unless defined $from;
6438	$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6439
6440	print "<table class=\"commit_search\">\n";
6441	my $alternate = 1;
6442	for (my $i = $from; $i <= $to; $i++) {
6443		my %co = %{$commitlist->[$i]};
6444		if (!%co) {
6445			next;
6446		}
6447		my $commit = $co{'id'};
6448		if ($alternate) {
6449			print "<tr class=\"dark\">\n";
6450		} else {
6451			print "<tr class=\"light\">\n";
6452		}
6453		$alternate ^= 1;
6454		print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6455		      format_author_html('td', \%co, 15, 5) .
6456		      "<td>" .
6457		      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6458		               -class => "list subject"},
6459		              chop_and_escape_str($co{'title'}, 50) . "<br/>");
6460		my $comment = $co{'comment'};
6461		foreach my $line (@$comment) {
6462			if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
6463				my ($lead, $match, $trail) = ($1, $2, $3);
6464				$match = chop_str($match, 70, 5, 'center');
6465				my $contextlen = int((80 - length($match))/2);
6466				$contextlen = 30 if ($contextlen > 30);
6467				$lead  = chop_str($lead,  $contextlen, 10, 'left');
6468				$trail = chop_str($trail, $contextlen, 10, 'right');
6469
6470				$lead  = esc_html($lead);
6471				$match = esc_html($match);
6472				$trail = esc_html($trail);
6473
6474				print "$lead<span class=\"match\">$match</span>$trail<br />";
6475			}
6476		}
6477		print "</td>\n" .
6478		      "<td class=\"link\">" .
6479		      $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6480		      " | " .
6481		      $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
6482		      " | " .
6483		      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6484		print "</td>\n" .
6485		      "</tr>\n";
6486	}
6487	if (defined $extra) {
6488		print "<tr>\n" .
6489		      "<td colspan=\"3\">$extra</td>\n" .
6490		      "</tr>\n";
6491	}
6492	print "</table>\n";
6493}
6494
6495## ======================================================================
6496## ======================================================================
6497## actions
6498
6499sub git_project_list {
6500	my $order = $input_params{'order'};
6501	if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6502		die_error(400, "Unknown order parameter");
6503	}
6504
6505	my @list = git_get_projects_list($project_filter, $strict_export);
6506	if (!@list) {
6507		die_error(404, "No projects found");
6508	}
6509
6510	git_header_html();
6511	if (defined $home_text && -f $home_text) {
6512		print "<div class=\"index_include\">\n";
6513		insert_file($home_text);
6514		print "</div>\n";
6515	}
6516
6517	git_project_search_form($searchtext, $search_use_regexp);
6518	git_project_list_body(\@list, $order);
6519	git_footer_html();
6520}
6521
6522sub git_forks {
6523	my $order = $input_params{'order'};
6524	if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6525		die_error(400, "Unknown order parameter");
6526	}
6527
6528	my $filter = $project;
6529	$filter =~ s/\.git$//;
6530	my @list = git_get_projects_list($filter);
6531	if (!@list) {
6532		die_error(404, "No forks found");
6533	}
6534
6535	git_header_html();
6536	git_print_page_nav('','');
6537	git_print_header_div('summary', "$project forks");
6538	git_project_list_body(\@list, $order);
6539	git_footer_html();
6540}
6541
6542sub git_project_index {
6543	my @projects = git_get_projects_list($project_filter, $strict_export);
6544	if (!@projects) {
6545		die_error(404, "No projects found");
6546	}
6547
6548	print $cgi->header(
6549		-type => 'text/plain',
6550		-charset => 'utf-8',
6551		-content_disposition => 'inline; filename="index.aux"');
6552
6553	foreach my $pr (@projects) {
6554		if (!exists $pr->{'owner'}) {
6555			$pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
6556		}
6557
6558		my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6559		# quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6560		$path  =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6561		$owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6562		$path  =~ s/ /\+/g;
6563		$owner =~ s/ /\+/g;
6564
6565		print "$path $owner\n";
6566	}
6567}
6568
6569sub git_summary {
6570	my $descr = git_get_project_description($project) || "none";
6571	my %co = parse_commit("HEAD");
6572	my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
6573	my $head = $co{'id'};
6574	my $remote_heads = gitweb_check_feature('remote_heads');
6575
6576	my $owner = git_get_project_owner($project);
6577
6578	my $refs = git_get_references();
6579	# These get_*_list functions return one more to allow us to see if
6580	# there are more ...
6581	my @taglist  = git_get_tags_list(16);
6582	my @headlist = git_get_heads_list(16);
6583	my %remotedata = $remote_heads ? git_get_remotes_list() : ();
6584	my @forklist;
6585	my $check_forks = gitweb_check_feature('forks');
6586
6587	if ($check_forks) {
6588		# find forks of a project
6589		my $filter = $project;
6590		$filter =~ s/\.git$//;
6591		@forklist = git_get_projects_list($filter);
6592		# filter out forks of forks
6593		@forklist = filter_forks_from_projects_list(\@forklist)
6594			if (@forklist);
6595	}
6596
6597	git_header_html();
6598	git_print_page_nav('summary','', $head);
6599
6600	print "<div class=\"title\">&nbsp;</div>\n";
6601	print "<table class=\"projects_list\">\n" .
6602	      "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
6603        if ($owner and not $omit_owner) {
6604	        print  "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
6605        }
6606	if (defined $cd{'rfc2822'}) {
6607		print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6608		      "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
6609	}
6610
6611	# use per project git URL list in $projectroot/$project/cloneurl
6612	# or make project git URL from git base URL and project name
6613	my $url_tag = "URL";
6614	my @url_list = git_get_project_url_list($project);
6615	@url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6616	foreach my $git_url (@url_list) {
6617		next unless $git_url;
6618		print format_repo_url($url_tag, $git_url);
6619		$url_tag = "";
6620	}
6621
6622	# Tag cloud
6623	my $show_ctags = gitweb_check_feature('ctags');
6624	if ($show_ctags) {
6625		my $ctags = git_get_project_ctags($project);
6626		if (%$ctags) {
6627			# without ability to add tags, don't show if there are none
6628			my $cloud = git_populate_project_tagcloud($ctags);
6629			print "<tr id=\"metadata_ctags\">" .
6630			      "<td>content tags</td>" .
6631			      "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6632			      "</tr>\n";
6633		}
6634	}
6635
6636	print "</table>\n";
6637
6638	# If XSS prevention is on, we don't include README.html.
6639	# TODO: Allow a readme in some safe format.
6640	if (!$prevent_xss && -s "$projectroot/$project/README.html") {
6641		print "<div class=\"title\">readme</div>\n" .
6642		      "<div class=\"readme\">\n";
6643		insert_file("$projectroot/$project/README.html");
6644		print "\n</div>\n"; # class="readme"
6645	}
6646
6647	# we need to request one more than 16 (0..15) to check if
6648	# those 16 are all
6649	my @commitlist = $head ? parse_commits($head, 17) : ();
6650	if (@commitlist) {
6651		git_print_header_div('shortlog');
6652		git_shortlog_body(\@commitlist, 0, 15, $refs,
6653		                  $#commitlist <=  15 ? undef :
6654		                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
6655	}
6656
6657	if (@taglist) {
6658		git_print_header_div('tags');
6659		git_tags_body(\@taglist, 0, 15,
6660		              $#taglist <=  15 ? undef :
6661		              $cgi->a({-href => href(action=>"tags")}, "..."));
6662	}
6663
6664	if (@headlist) {
6665		git_print_header_div('heads');
6666		git_heads_body(\@headlist, $head, 0, 15,
6667		               $#headlist <= 15 ? undef :
6668		               $cgi->a({-href => href(action=>"heads")}, "..."));
6669	}
6670
6671	if (%remotedata) {
6672		git_print_header_div('remotes');
6673		git_remotes_body(\%remotedata, 15, $head);
6674	}
6675
6676	if (@forklist) {
6677		git_print_header_div('forks');
6678		git_project_list_body(\@forklist, 'age', 0, 15,
6679		                      $#forklist <= 15 ? undef :
6680		                      $cgi->a({-href => href(action=>"forks")}, "..."),
6681		                      'no_header');
6682	}
6683
6684	git_footer_html();
6685}
6686
6687sub git_tag {
6688	my %tag = parse_tag($hash);
6689
6690	if (! %tag) {
6691		die_error(404, "Unknown tag object");
6692	}
6693
6694	my $head = git_get_head_hash($project);
6695	git_header_html();
6696	git_print_page_nav('','', $head,undef,$head);
6697	git_print_header_div('commit', esc_html($tag{'name'}), $hash);
6698	print "<div class=\"title_text\">\n" .
6699	      "<table class=\"object_header\">\n" .
6700	      "<tr>\n" .
6701	      "<td>object</td>\n" .
6702	      "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6703	                       $tag{'object'}) . "</td>\n" .
6704	      "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6705	                                      $tag{'type'}) . "</td>\n" .
6706	      "</tr>\n";
6707	if (defined($tag{'author'})) {
6708		git_print_authorship_rows(\%tag, 'author');
6709	}
6710	print "</table>\n\n" .
6711	      "</div>\n";
6712	print "<div class=\"page_body\">";
6713	my $comment = $tag{'comment'};
6714	foreach my $line (@$comment) {
6715		chomp $line;
6716		print esc_html($line, -nbsp=>1) . "<br/>\n";
6717	}
6718	print "</div>\n";
6719	git_footer_html();
6720}
6721
6722sub git_blame_common {
6723	my $format = shift || 'porcelain';
6724	if ($format eq 'porcelain' && $input_params{'javascript'}) {
6725		$format = 'incremental';
6726		$action = 'blame_incremental'; # for page title etc
6727	}
6728
6729	# permissions
6730	gitweb_check_feature('blame')
6731		or die_error(403, "Blame view not allowed");
6732
6733	# error checking
6734	die_error(400, "No file name given") unless $file_name;
6735	$hash_base ||= git_get_head_hash($project);
6736	die_error(404, "Couldn't find base commit") unless $hash_base;
6737	my %co = parse_commit($hash_base)
6738		or die_error(404, "Commit not found");
6739	my $ftype = "blob";
6740	if (!defined $hash) {
6741		$hash = git_get_hash_by_path($hash_base, $file_name, "blob")
6742			or die_error(404, "Error looking up file");
6743	} else {
6744		$ftype = git_get_type($hash);
6745		if ($ftype !~ "blob") {
6746			die_error(400, "Object is not a blob");
6747		}
6748	}
6749
6750	my $fd;
6751	if ($format eq 'incremental') {
6752		# get file contents (as base)
6753		open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
6754			or die_error(500, "Open git-cat-file failed");
6755	} elsif ($format eq 'data') {
6756		# run git-blame --incremental
6757		open $fd, "-|", git_cmd(), "blame", "--incremental",
6758			$hash_base, "--", $file_name
6759			or die_error(500, "Open git-blame --incremental failed");
6760	} else {
6761		# run git-blame --porcelain
6762		open $fd, "-|", git_cmd(), "blame", '-p',
6763			$hash_base, '--', $file_name
6764			or die_error(500, "Open git-blame --porcelain failed");
6765	}
6766	binmode $fd, ':utf8';
6767
6768	# incremental blame data returns early
6769	if ($format eq 'data') {
6770		print $cgi->header(
6771			-type=>"text/plain", -charset => "utf-8",
6772			-status=> "200 OK");
6773		local $| = 1; # output autoflush
6774		while (my $line = <$fd>) {
6775			print to_utf8($line);
6776		}
6777		close $fd
6778			or print "ERROR $!\n";
6779
6780		print 'END';
6781		if (defined $t0 && gitweb_check_feature('timed')) {
6782			print ' '.
6783			      tv_interval($t0, [ gettimeofday() ]).
6784			      ' '.$number_of_git_cmds;
6785		}
6786		print "\n";
6787
6788		return;
6789	}
6790
6791	# page header
6792	git_header_html();
6793	my $formats_nav =
6794		$cgi->a({-href => href(action=>"blob", -replay=>1)},
6795		        "blob") .
6796		" | ";
6797	if ($format eq 'incremental') {
6798		$formats_nav .=
6799			$cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6800			        "blame") . " (non-incremental)";
6801	} else {
6802		$formats_nav .=
6803			$cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6804			        "blame") . " (incremental)";
6805	}
6806	$formats_nav .=
6807		" | " .
6808		$cgi->a({-href => href(action=>"history", -replay=>1)},
6809		        "history") .
6810		" | " .
6811		$cgi->a({-href => href(action=>$action, file_name=>$file_name)},
6812		        "HEAD");
6813	git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6814	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6815	git_print_page_path($file_name, $ftype, $hash_base);
6816
6817	# page body
6818	if ($format eq 'incremental') {
6819		print "<noscript>\n<div class=\"error\"><center><b>\n".
6820		      "This page requires JavaScript to run.\n Use ".
6821		      $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
6822		              'this page').
6823		      " instead.\n".
6824		      "</b></center></div>\n</noscript>\n";
6825
6826		print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6827	}
6828
6829	print qq!<div class="page_body">\n!;
6830	print qq!<div id="progress_info">... / ...</div>\n!
6831		if ($format eq 'incremental');
6832	print qq!<table id="blame_table" class="blame" width="100%">\n!.
6833	      #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6834	      qq!<thead>\n!.
6835	      qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6836	      qq!</thead>\n!.
6837	      qq!<tbody>\n!;
6838
6839	my @rev_color = qw(light dark);
6840	my $num_colors = scalar(@rev_color);
6841	my $current_color = 0;
6842
6843	if ($format eq 'incremental') {
6844		my $color_class = $rev_color[$current_color];
6845
6846		#contents of a file
6847		my $linenr = 0;
6848	LINE:
6849		while (my $line = <$fd>) {
6850			chomp $line;
6851			$linenr++;
6852
6853			print qq!<tr id="l$linenr" class="$color_class">!.
6854			      qq!<td class="sha1"><a href=""> </a></td>!.
6855			      qq!<td class="linenr">!.
6856			      qq!<a class="linenr" href="">$linenr</a></td>!;
6857			print qq!<td class="pre">! . esc_html($line) . "</td>\n";
6858			print qq!</tr>\n!;
6859		}
6860
6861	} else { # porcelain, i.e. ordinary blame
6862		my %metainfo = (); # saves information about commits
6863
6864		# blame data
6865	LINE:
6866		while (my $line = <$fd>) {
6867			chomp $line;
6868			# the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
6869			# no <lines in group> for subsequent lines in group of lines
6870			my ($full_rev, $orig_lineno, $lineno, $group_size) =
6871			   ($line =~ /^($oid_regex) (\d+) (\d+)(?: (\d+))?$/);
6872			if (!exists $metainfo{$full_rev}) {
6873				$metainfo{$full_rev} = { 'nprevious' => 0 };
6874			}
6875			my $meta = $metainfo{$full_rev};
6876			my $data;
6877			while ($data = <$fd>) {
6878				chomp $data;
6879				last if ($data =~ s/^\t//); # contents of line
6880				if ($data =~ /^(\S+)(?: (.*))?$/) {
6881					$meta->{$1} = $2 unless exists $meta->{$1};
6882				}
6883				if ($data =~ /^previous /) {
6884					$meta->{'nprevious'}++;
6885				}
6886			}
6887			my $short_rev = substr($full_rev, 0, 8);
6888			my $author = $meta->{'author'};
6889			my %date =
6890				parse_date($meta->{'author-time'}, $meta->{'author-tz'});
6891			my $date = $date{'iso-tz'};
6892			if ($group_size) {
6893				$current_color = ($current_color + 1) % $num_colors;
6894			}
6895			my $tr_class = $rev_color[$current_color];
6896			$tr_class .= ' boundary' if (exists $meta->{'boundary'});
6897			$tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
6898			$tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
6899			print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
6900			if ($group_size) {
6901				print "<td class=\"sha1\"";
6902				print " title=\"". esc_html($author) . ", $date\"";
6903				print " rowspan=\"$group_size\"" if ($group_size > 1);
6904				print ">";
6905				print $cgi->a({-href => href(action=>"commit",
6906				                             hash=>$full_rev,
6907				                             file_name=>$file_name)},
6908				              esc_html($short_rev));
6909				if ($group_size >= 2) {
6910					my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
6911					if (@author_initials) {
6912						print "<br />" .
6913						      esc_html(join('', @author_initials));
6914						#           or join('.', ...)
6915					}
6916				}
6917				print "</td>\n";
6918			}
6919			# 'previous' <sha1 of parent commit> <filename at commit>
6920			if (exists $meta->{'previous'} &&
6921			    $meta->{'previous'} =~ /^($oid_regex) (.*)$/) {
6922				$meta->{'parent'} = $1;
6923				$meta->{'file_parent'} = unquote($2);
6924			}
6925			my $linenr_commit =
6926				exists($meta->{'parent'}) ?
6927				$meta->{'parent'} : $full_rev;
6928			my $linenr_filename =
6929				exists($meta->{'file_parent'}) ?
6930				$meta->{'file_parent'} : unquote($meta->{'filename'});
6931			my $blamed = href(action => 'blame',
6932			                  file_name => $linenr_filename,
6933			                  hash_base => $linenr_commit);
6934			print "<td class=\"linenr\">";
6935			print $cgi->a({ -href => "$blamed#l$orig_lineno",
6936			                -class => "linenr" },
6937			              esc_html($lineno));
6938			print "</td>";
6939			print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
6940			print "</tr>\n";
6941		} # end while
6942
6943	}
6944
6945	# footer
6946	print "</tbody>\n".
6947	      "</table>\n"; # class="blame"
6948	print "</div>\n";   # class="blame_body"
6949	close $fd
6950		or print "Reading blob failed\n";
6951
6952	git_footer_html();
6953}
6954
6955sub git_blame {
6956	git_blame_common();
6957}
6958
6959sub git_blame_incremental {
6960	git_blame_common('incremental');
6961}
6962
6963sub git_blame_data {
6964	git_blame_common('data');
6965}
6966
6967sub git_tags {
6968	my $head = git_get_head_hash($project);
6969	git_header_html();
6970	git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
6971	git_print_header_div('summary', $project);
6972
6973	my @tagslist = git_get_tags_list();
6974	if (@tagslist) {
6975		git_tags_body(\@tagslist);
6976	}
6977	git_footer_html();
6978}
6979
6980sub git_heads {
6981	my $head = git_get_head_hash($project);
6982	git_header_html();
6983	git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
6984	git_print_header_div('summary', $project);
6985
6986	my @headslist = git_get_heads_list();
6987	if (@headslist) {
6988		git_heads_body(\@headslist, $head);
6989	}
6990	git_footer_html();
6991}
6992
6993# used both for single remote view and for list of all the remotes
6994sub git_remotes {
6995	gitweb_check_feature('remote_heads')
6996		or die_error(403, "Remote heads view is disabled");
6997
6998	my $head = git_get_head_hash($project);
6999	my $remote = $input_params{'hash'};
7000
7001	my $remotedata = git_get_remotes_list($remote);
7002	die_error(500, "Unable to get remote information") unless defined $remotedata;
7003
7004	unless (%$remotedata) {
7005		die_error(404, defined $remote ?
7006			"Remote $remote not found" :
7007			"No remotes found");
7008	}
7009
7010	git_header_html(undef, undef, -action_extra => $remote);
7011	git_print_page_nav('', '',  $head, undef, $head,
7012		format_ref_views($remote ? '' : 'remotes'));
7013
7014	fill_remote_heads($remotedata);
7015	if (defined $remote) {
7016		git_print_header_div('remotes', "$remote remote for $project");
7017		git_remote_block($remote, $remotedata->{$remote}, undef, $head);
7018	} else {
7019		git_print_header_div('summary', "$project remotes");
7020		git_remotes_body($remotedata, undef, $head);
7021	}
7022
7023	git_footer_html();
7024}
7025
7026sub git_blob_plain {
7027	my $type = shift;
7028	my $expires;
7029
7030	if (!defined $hash) {
7031		if (defined $file_name) {
7032			my $base = $hash_base || git_get_head_hash($project);
7033			$hash = git_get_hash_by_path($base, $file_name, "blob")
7034				or die_error(404, "Cannot find file");
7035		} else {
7036			die_error(400, "No file name defined");
7037		}
7038	} elsif ($hash =~ m/^$oid_regex$/) {
7039		# blobs defined by non-textual hash id's can be cached
7040		$expires = "+1d";
7041	}
7042
7043	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
7044		or die_error(500, "Open git-cat-file blob '$hash' failed");
7045
7046	# content-type (can include charset)
7047	$type = blob_contenttype($fd, $file_name, $type);
7048
7049	# "save as" filename, even when no $file_name is given
7050	my $save_as = "$hash";
7051	if (defined $file_name) {
7052		$save_as = $file_name;
7053	} elsif ($type =~ m/^text\//) {
7054		$save_as .= '.txt';
7055	}
7056
7057	# With XSS prevention on, blobs of all types except a few known safe
7058	# ones are served with "Content-Disposition: attachment" to make sure
7059	# they don't run in our security domain.  For certain image types,
7060	# blob view writes an <img> tag referring to blob_plain view, and we
7061	# want to be sure not to break that by serving the image as an
7062	# attachment (though Firefox 3 doesn't seem to care).
7063	my $sandbox = $prevent_xss &&
7064		$type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
7065
7066	# serve text/* as text/plain
7067	if ($prevent_xss &&
7068	    ($type =~ m!^text/[a-z]+\b(.*)$! ||
7069	     ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
7070		my $rest = $1;
7071		$rest = defined $rest ? $rest : '';
7072		$type = "text/plain$rest";
7073	}
7074
7075	print $cgi->header(
7076		-type => $type,
7077		-expires => $expires,
7078		-content_disposition =>
7079			($sandbox ? 'attachment' : 'inline')
7080			. '; filename="' . $save_as . '"');
7081	local $/ = undef;
7082	binmode STDOUT, ':raw';
7083	print <$fd>;
7084	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7085	close $fd;
7086}
7087
7088sub git_blob {
7089	my $expires;
7090
7091	if (!defined $hash) {
7092		if (defined $file_name) {
7093			my $base = $hash_base || git_get_head_hash($project);
7094			$hash = git_get_hash_by_path($base, $file_name, "blob")
7095				or die_error(404, "Cannot find file");
7096		} else {
7097			die_error(400, "No file name defined");
7098		}
7099	} elsif ($hash =~ m/^$oid_regex$/) {
7100		# blobs defined by non-textual hash id's can be cached
7101		$expires = "+1d";
7102	}
7103
7104	my $have_blame = gitweb_check_feature('blame');
7105	open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
7106		or die_error(500, "Couldn't cat $file_name, $hash");
7107	my $mimetype = blob_mimetype($fd, $file_name);
7108	# use 'blob_plain' (aka 'raw') view for files that cannot be displayed
7109	if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
7110		close $fd;
7111		return git_blob_plain($mimetype);
7112	}
7113	# we can have blame only for text/* mimetype
7114	$have_blame &&= ($mimetype =~ m!^text/!);
7115
7116	my $highlight = gitweb_check_feature('highlight');
7117	my $syntax = guess_file_syntax($highlight, $file_name);
7118	$fd = run_highlighter($fd, $highlight, $syntax);
7119
7120	git_header_html(undef, $expires);
7121	my $formats_nav = '';
7122	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7123		if (defined $file_name) {
7124			if ($have_blame) {
7125				$formats_nav .=
7126					$cgi->a({-href => href(action=>"blame", -replay=>1)},
7127					        "blame") .
7128					" | ";
7129			}
7130			$formats_nav .=
7131				$cgi->a({-href => href(action=>"history", -replay=>1)},
7132				        "history") .
7133				" | " .
7134				$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
7135				        "raw") .
7136				" | " .
7137				$cgi->a({-href => href(action=>"blob",
7138				                       hash_base=>"HEAD", file_name=>$file_name)},
7139				        "HEAD");
7140		} else {
7141			$formats_nav .=
7142				$cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
7143				        "raw");
7144		}
7145		git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7146		git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7147	} else {
7148		print "<div class=\"page_nav\">\n" .
7149		      "<br/><br/></div>\n" .
7150		      "<div class=\"title\">".esc_html($hash)."</div>\n";
7151	}
7152	git_print_page_path($file_name, "blob", $hash_base);
7153	print "<div class=\"page_body\">\n";
7154	if ($mimetype =~ m!^image/!) {
7155		print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;
7156		if ($file_name) {
7157			print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
7158		}
7159		print qq! src="! .
7160		      esc_attr(href(action=>"blob_plain", hash=>$hash,
7161		           hash_base=>$hash_base, file_name=>$file_name)) .
7162		      qq!" />\n!;
7163	} else {
7164		my $nr;
7165		while (my $line = <$fd>) {
7166			chomp $line;
7167			$nr++;
7168			$line = untabify($line);
7169			printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
7170			       $nr, esc_attr(href(-replay => 1)), $nr, $nr,
7171			       $highlight ? sanitize($line) : esc_html($line, -nbsp=>1);
7172		}
7173	}
7174	close $fd
7175		or print "Reading blob failed.\n";
7176	print "</div>";
7177	git_footer_html();
7178}
7179
7180sub git_tree {
7181	if (!defined $hash_base) {
7182		$hash_base = "HEAD";
7183	}
7184	if (!defined $hash) {
7185		if (defined $file_name) {
7186			$hash = git_get_hash_by_path($hash_base, $file_name, "tree");
7187		} else {
7188			$hash = $hash_base;
7189		}
7190	}
7191	die_error(404, "No such tree") unless defined($hash);
7192
7193	my $show_sizes = gitweb_check_feature('show-sizes');
7194	my $have_blame = gitweb_check_feature('blame');
7195
7196	my @entries = ();
7197	{
7198		local $/ = "\0";
7199		open my $fd, "-|", git_cmd(), "ls-tree", '-z',
7200			($show_sizes ? '-l' : ()), @extra_options, $hash
7201			or die_error(500, "Open git-ls-tree failed");
7202		@entries = map { chomp; $_ } <$fd>;
7203		close $fd
7204			or die_error(404, "Reading tree failed");
7205	}
7206
7207	my $refs = git_get_references();
7208	my $ref = format_ref_marker($refs, $hash_base);
7209	git_header_html();
7210	my $basedir = '';
7211	if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7212		my @views_nav = ();
7213		if (defined $file_name) {
7214			push @views_nav,
7215				$cgi->a({-href => href(action=>"history", -replay=>1)},
7216				        "history"),
7217				$cgi->a({-href => href(action=>"tree",
7218				                       hash_base=>"HEAD", file_name=>$file_name)},
7219				        "HEAD"),
7220		}
7221		my $snapshot_links = format_snapshot_links($hash);
7222		if (defined $snapshot_links) {
7223			# FIXME: Should be available when we have no hash base as well.
7224			push @views_nav, $snapshot_links;
7225		}
7226		git_print_page_nav('tree','', $hash_base, undef, undef,
7227		                   join(' | ', @views_nav));
7228		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
7229	} else {
7230		undef $hash_base;
7231		print "<div class=\"page_nav\">\n";
7232		print "<br/><br/></div>\n";
7233		print "<div class=\"title\">".esc_html($hash)."</div>\n";
7234	}
7235	if (defined $file_name) {
7236		$basedir = $file_name;
7237		if ($basedir ne '' && substr($basedir, -1) ne '/') {
7238			$basedir .= '/';
7239		}
7240		git_print_page_path($file_name, 'tree', $hash_base);
7241	}
7242	print "<div class=\"page_body\">\n";
7243	print "<table class=\"tree\">\n";
7244	my $alternate = 1;
7245	# '..' (top directory) link if possible
7246	if (defined $hash_base &&
7247	    defined $file_name && $file_name =~ m![^/]+$!) {
7248		if ($alternate) {
7249			print "<tr class=\"dark\">\n";
7250		} else {
7251			print "<tr class=\"light\">\n";
7252		}
7253		$alternate ^= 1;
7254
7255		my $up = $file_name;
7256		$up =~ s!/?[^/]+$!!;
7257		undef $up unless $up;
7258		# based on git_print_tree_entry
7259		print '<td class="mode">' . mode_str('040000') . "</td>\n";
7260		print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
7261		print '<td class="list">';
7262		print $cgi->a({-href => href(action=>"tree",
7263		                             hash_base=>$hash_base,
7264		                             file_name=>$up)},
7265		              "..");
7266		print "</td>\n";
7267		print "<td class=\"link\"></td>\n";
7268
7269		print "</tr>\n";
7270	}
7271	foreach my $line (@entries) {
7272		my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
7273
7274		if ($alternate) {
7275			print "<tr class=\"dark\">\n";
7276		} else {
7277			print "<tr class=\"light\">\n";
7278		}
7279		$alternate ^= 1;
7280
7281		git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
7282
7283		print "</tr>\n";
7284	}
7285	print "</table>\n" .
7286	      "</div>";
7287	git_footer_html();
7288}
7289
7290sub sanitize_for_filename {
7291    my $name = shift;
7292
7293    $name =~ s!/!-!g;
7294    $name =~ s/[^[:alnum:]_.-]//g;
7295
7296    return $name;
7297}
7298
7299sub snapshot_name {
7300	my ($project, $hash) = @_;
7301
7302	# path/to/project.git  -> project
7303	# path/to/project/.git -> project
7304	my $name = to_utf8($project);
7305	$name =~ s,([^/])/*\.git$,$1,;
7306	$name = sanitize_for_filename(basename($name));
7307
7308	my $ver = $hash;
7309	if ($hash =~ /^[0-9a-fA-F]+$/) {
7310		# shorten SHA-1 hash
7311		my $full_hash = git_get_full_hash($project, $hash);
7312		if ($full_hash =~ /^$hash/ && length($hash) > 7) {
7313			$ver = git_get_short_hash($project, $hash);
7314		}
7315	} elsif ($hash =~ m!^refs/tags/(.*)$!) {
7316		# tags don't need shortened SHA-1 hash
7317		$ver = $1;
7318	} else {
7319		# branches and other need shortened SHA-1 hash
7320		my $strip_refs = join '|', map { quotemeta } get_branch_refs();
7321		if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
7322			my $ref_dir = (defined $1) ? $1 : '';
7323			$ver = $2;
7324
7325			$ref_dir = sanitize_for_filename($ref_dir);
7326			# for refs neither in heads nor remotes we want to
7327			# add a ref dir to archive name
7328			if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
7329				$ver = $ref_dir . '-' . $ver;
7330			}
7331		}
7332		$ver .= '-' . git_get_short_hash($project, $hash);
7333	}
7334	# special case of sanitization for filename - we change
7335	# slashes to dots instead of dashes
7336	# in case of hierarchical branch names
7337	$ver =~ s!/!.!g;
7338	$ver =~ s/[^[:alnum:]_.-]//g;
7339
7340	# name = project-version_string
7341	$name = "$name-$ver";
7342
7343	return wantarray ? ($name, $name) : $name;
7344}
7345
7346sub exit_if_unmodified_since {
7347	my ($latest_epoch) = @_;
7348	our $cgi;
7349
7350	my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7351	if (defined $if_modified) {
7352		my $since;
7353		if (eval { require HTTP::Date; 1; }) {
7354			$since = HTTP::Date::str2time($if_modified);
7355		} elsif (eval { require Time::ParseDate; 1; }) {
7356			$since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7357		}
7358		if (defined $since && $latest_epoch <= $since) {
7359			my %latest_date = parse_date($latest_epoch);
7360			print $cgi->header(
7361				-last_modified => $latest_date{'rfc2822'},
7362				-status => '304 Not Modified');
7363			goto DONE_GITWEB;
7364		}
7365	}
7366}
7367
7368sub git_snapshot {
7369	my $format = $input_params{'snapshot_format'};
7370	if (!@snapshot_fmts) {
7371		die_error(403, "Snapshots not allowed");
7372	}
7373	# default to first supported snapshot format
7374	$format ||= $snapshot_fmts[0];
7375	if ($format !~ m/^[a-z0-9]+$/) {
7376		die_error(400, "Invalid snapshot format parameter");
7377	} elsif (!exists($known_snapshot_formats{$format})) {
7378		die_error(400, "Unknown snapshot format");
7379	} elsif ($known_snapshot_formats{$format}{'disabled'}) {
7380		die_error(403, "Snapshot format not allowed");
7381	} elsif (!grep($_ eq $format, @snapshot_fmts)) {
7382		die_error(403, "Unsupported snapshot format");
7383	}
7384
7385	my $type = git_get_type("$hash^{}");
7386	if (!$type) {
7387		die_error(404, 'Object does not exist');
7388	}  elsif ($type eq 'blob') {
7389		die_error(400, 'Object is not a tree-ish');
7390	}
7391
7392	my ($name, $prefix) = snapshot_name($project, $hash);
7393	my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
7394
7395	my %co = parse_commit($hash);
7396	exit_if_unmodified_since($co{'committer_epoch'}) if %co;
7397
7398	my $cmd = quote_command(
7399		git_cmd(), 'archive',
7400		"--format=$known_snapshot_formats{$format}{'format'}",
7401		"--prefix=$prefix/", $hash);
7402	if (exists $known_snapshot_formats{$format}{'compressor'}) {
7403		$cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
7404	}
7405
7406	$filename =~ s/(["\\])/\\$1/g;
7407	my %latest_date;
7408	if (%co) {
7409		%latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
7410	}
7411
7412	print $cgi->header(
7413		-type => $known_snapshot_formats{$format}{'type'},
7414		-content_disposition => 'inline; filename="' . $filename . '"',
7415		%co ? (-last_modified => $latest_date{'rfc2822'}) : (),
7416		-status => '200 OK');
7417
7418	open my $fd, "-|", $cmd
7419		or die_error(500, "Execute git-archive failed");
7420	binmode STDOUT, ':raw';
7421	print <$fd>;
7422	binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7423	close $fd;
7424}
7425
7426sub git_log_generic {
7427	my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
7428
7429	my $head = git_get_head_hash($project);
7430	if (!defined $base) {
7431		$base = $head;
7432	}
7433	if (!defined $page) {
7434		$page = 0;
7435	}
7436	my $refs = git_get_references();
7437
7438	my $commit_hash = $base;
7439	if (defined $parent) {
7440		$commit_hash = "$parent..$base";
7441	}
7442	my @commitlist =
7443		parse_commits($commit_hash, 101, (100 * $page),
7444		              defined $file_name ? ($file_name, "--full-history") : ());
7445
7446	my $ftype;
7447	if (!defined $file_hash && defined $file_name) {
7448		# some commits could have deleted file in question,
7449		# and not have it in tree, but one of them has to have it
7450		for (my $i = 0; $i < @commitlist; $i++) {
7451			$file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
7452			last if defined $file_hash;
7453		}
7454	}
7455	if (defined $file_hash) {
7456		$ftype = git_get_type($file_hash);
7457	}
7458	if (defined $file_name && !defined $ftype) {
7459		die_error(500, "Unknown type of object");
7460	}
7461	my %co;
7462	if (defined $file_name) {
7463		%co = parse_commit($base)
7464			or die_error(404, "Unknown commit object");
7465	}
7466
7467
7468	my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
7469	my $next_link = '';
7470	if ($#commitlist >= 100) {
7471		$next_link =
7472			$cgi->a({-href => href(-replay=>1, page=>$page+1),
7473			         -accesskey => "n", -title => "Alt-n"}, "next");
7474	}
7475	my $patch_max = gitweb_get_feature('patches');
7476	if ($patch_max && !defined $file_name) {
7477		if ($patch_max < 0 || @commitlist <= $patch_max) {
7478			$paging_nav .= " &sdot; " .
7479				$cgi->a({-href => href(action=>"patches", -replay=>1)},
7480					"patches");
7481		}
7482	}
7483
7484	git_header_html();
7485	git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
7486	if (defined $file_name) {
7487		git_print_header_div('commit', esc_html($co{'title'}), $base);
7488	} else {
7489		git_print_header_div('summary', $project)
7490	}
7491	git_print_page_path($file_name, $ftype, $hash_base)
7492		if (defined $file_name);
7493
7494	$body_subr->(\@commitlist, 0, 99, $refs, $next_link,
7495	             $file_name, $file_hash, $ftype);
7496
7497	git_footer_html();
7498}
7499
7500sub git_log {
7501	git_log_generic('log', \&git_log_body,
7502	                $hash, $hash_parent);
7503}
7504
7505sub git_commit {
7506	$hash ||= $hash_base || "HEAD";
7507	my %co = parse_commit($hash)
7508	    or die_error(404, "Unknown commit object");
7509
7510	my $parent  = $co{'parent'};
7511	my $parents = $co{'parents'}; # listref
7512
7513	# we need to prepare $formats_nav before any parameter munging
7514	my $formats_nav;
7515	if (!defined $parent) {
7516		# --root commitdiff
7517		$formats_nav .= '(initial)';
7518	} elsif (@$parents == 1) {
7519		# single parent commit
7520		$formats_nav .=
7521			'(parent: ' .
7522			$cgi->a({-href => href(action=>"commit",
7523			                       hash=>$parent)},
7524			        esc_html(substr($parent, 0, 7))) .
7525			')';
7526	} else {
7527		# merge commit
7528		$formats_nav .=
7529			'(merge: ' .
7530			join(' ', map {
7531				$cgi->a({-href => href(action=>"commit",
7532				                       hash=>$_)},
7533				        esc_html(substr($_, 0, 7)));
7534			} @$parents ) .
7535			')';
7536	}
7537	if (gitweb_check_feature('patches') && @$parents <= 1) {
7538		$formats_nav .= " | " .
7539			$cgi->a({-href => href(action=>"patch", -replay=>1)},
7540				"patch");
7541	}
7542
7543	if (!defined $parent) {
7544		$parent = "--root";
7545	}
7546	my @difftree;
7547	open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
7548		@diff_opts,
7549		(@$parents <= 1 ? $parent : '-c'),
7550		$hash, "--"
7551		or die_error(500, "Open git-diff-tree failed");
7552	@difftree = map { chomp; $_ } <$fd>;
7553	close $fd or die_error(404, "Reading git-diff-tree failed");
7554
7555	# non-textual hash id's can be cached
7556	my $expires;
7557	if ($hash =~ m/^$oid_regex$/) {
7558		$expires = "+1d";
7559	}
7560	my $refs = git_get_references();
7561	my $ref = format_ref_marker($refs, $co{'id'});
7562
7563	git_header_html(undef, $expires);
7564	git_print_page_nav('commit', '',
7565	                   $hash, $co{'tree'}, $hash,
7566	                   $formats_nav);
7567
7568	if (defined $co{'parent'}) {
7569		git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
7570	} else {
7571		git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
7572	}
7573	print "<div class=\"title_text\">\n" .
7574	      "<table class=\"object_header\">\n";
7575	git_print_authorship_rows(\%co);
7576	print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
7577	print "<tr>" .
7578	      "<td>tree</td>" .
7579	      "<td class=\"sha1\">" .
7580	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7581	               class => "list"}, $co{'tree'}) .
7582	      "</td>" .
7583	      "<td class=\"link\">" .
7584	      $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7585	              "tree");
7586	my $snapshot_links = format_snapshot_links($hash);
7587	if (defined $snapshot_links) {
7588		print " | " . $snapshot_links;
7589	}
7590	print "</td>" .
7591	      "</tr>\n";
7592
7593	foreach my $par (@$parents) {
7594		print "<tr>" .
7595		      "<td>parent</td>" .
7596		      "<td class=\"sha1\">" .
7597		      $cgi->a({-href => href(action=>"commit", hash=>$par),
7598		               class => "list"}, $par) .
7599		      "</td>" .
7600		      "<td class=\"link\">" .
7601		      $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
7602		      " | " .
7603		      $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
7604		      "</td>" .
7605		      "</tr>\n";
7606	}
7607	print "</table>".
7608	      "</div>\n";
7609
7610	print "<div class=\"page_body\">\n";
7611	git_print_log($co{'comment'});
7612	print "</div>\n";
7613
7614	git_difftree_body(\@difftree, $hash, @$parents);
7615
7616	git_footer_html();
7617}
7618
7619sub git_object {
7620	# object is defined by:
7621	# - hash or hash_base alone
7622	# - hash_base and file_name
7623	my $type;
7624
7625	# - hash or hash_base alone
7626	if ($hash || ($hash_base && !defined $file_name)) {
7627		my $object_id = $hash || $hash_base;
7628
7629		open my $fd, "-|", quote_command(
7630			git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
7631			or die_error(404, "Object does not exist");
7632		$type = <$fd>;
7633		defined $type && chomp $type;
7634		close $fd
7635			or die_error(404, "Object does not exist");
7636
7637	# - hash_base and file_name
7638	} elsif ($hash_base && defined $file_name) {
7639		$file_name =~ s,/+$,,;
7640
7641		system(git_cmd(), "cat-file", '-e', $hash_base) == 0
7642			or die_error(404, "Base object does not exist");
7643
7644		# here errors should not happen
7645		open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
7646			or die_error(500, "Open git-ls-tree failed");
7647		my $line = <$fd>;
7648		close $fd;
7649
7650		#'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa	panic.c'
7651		unless ($line && $line =~ m/^([0-9]+) (.+) ($oid_regex)\t/) {
7652			die_error(404, "File or directory for given base does not exist");
7653		}
7654		$type = $2;
7655		$hash = $3;
7656	} else {
7657		die_error(400, "Not enough information to find object");
7658	}
7659
7660	print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7661	                                  hash=>$hash, hash_base=>$hash_base,
7662	                                  file_name=>$file_name),
7663	                     -status => '302 Found');
7664}
7665
7666sub git_blobdiff {
7667	my $format = shift || 'html';
7668	my $diff_style = $input_params{'diff_style'} || 'inline';
7669
7670	my $fd;
7671	my @difftree;
7672	my %diffinfo;
7673	my $expires;
7674
7675	# preparing $fd and %diffinfo for git_patchset_body
7676	# new style URI
7677	if (defined $hash_base && defined $hash_parent_base) {
7678		if (defined $file_name) {
7679			# read raw output
7680			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7681				$hash_parent_base, $hash_base,
7682				"--", (defined $file_parent ? $file_parent : ()), $file_name
7683				or die_error(500, "Open git-diff-tree failed");
7684			@difftree = map { chomp; $_ } <$fd>;
7685			close $fd
7686				or die_error(404, "Reading git-diff-tree failed");
7687			@difftree
7688				or die_error(404, "Blob diff not found");
7689
7690		} elsif (defined $hash &&
7691		         $hash =~ $oid_regex) {
7692			# try to find filename from $hash
7693
7694			# read filtered raw output
7695			open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7696				$hash_parent_base, $hash_base, "--"
7697				or die_error(500, "Open git-diff-tree failed");
7698			@difftree =
7699				# ':100644 100644 03b21826... 3b93d5e7... M	ls-files.c'
7700				# $hash == to_id
7701				grep { /^:[0-7]{6} [0-7]{6} $oid_regex $hash/ }
7702				map { chomp; $_ } <$fd>;
7703			close $fd
7704				or die_error(404, "Reading git-diff-tree failed");
7705			@difftree
7706				or die_error(404, "Blob diff not found");
7707
7708		} else {
7709			die_error(400, "Missing one of the blob diff parameters");
7710		}
7711
7712		if (@difftree > 1) {
7713			die_error(400, "Ambiguous blob diff specification");
7714		}
7715
7716		%diffinfo = parse_difftree_raw_line($difftree[0]);
7717		$file_parent ||= $diffinfo{'from_file'} || $file_name;
7718		$file_name   ||= $diffinfo{'to_file'};
7719
7720		$hash_parent ||= $diffinfo{'from_id'};
7721		$hash        ||= $diffinfo{'to_id'};
7722
7723		# non-textual hash id's can be cached
7724		if ($hash_base =~ m/^$oid_regex$/ &&
7725		    $hash_parent_base =~ m/^$oid_regex$/) {
7726			$expires = '+1d';
7727		}
7728
7729		# open patch output
7730		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7731			'-p', ($format eq 'html' ? "--full-index" : ()),
7732			$hash_parent_base, $hash_base,
7733			"--", (defined $file_parent ? $file_parent : ()), $file_name
7734			or die_error(500, "Open git-diff-tree failed");
7735	}
7736
7737	# old/legacy style URI -- not generated anymore since 1.4.3.
7738	if (!%diffinfo) {
7739		die_error('404 Not Found', "Missing one of the blob diff parameters")
7740	}
7741
7742	# header
7743	if ($format eq 'html') {
7744		my $formats_nav =
7745			$cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
7746			        "raw");
7747		$formats_nav .= diff_style_nav($diff_style);
7748		git_header_html(undef, $expires);
7749		if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7750			git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7751			git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7752		} else {
7753			print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
7754			print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
7755		}
7756		if (defined $file_name) {
7757			git_print_page_path($file_name, "blob", $hash_base);
7758		} else {
7759			print "<div class=\"page_path\"></div>\n";
7760		}
7761
7762	} elsif ($format eq 'plain') {
7763		print $cgi->header(
7764			-type => 'text/plain',
7765			-charset => 'utf-8',
7766			-expires => $expires,
7767			-content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
7768
7769		print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7770
7771	} else {
7772		die_error(400, "Unknown blobdiff format");
7773	}
7774
7775	# patch
7776	if ($format eq 'html') {
7777		print "<div class=\"page_body\">\n";
7778
7779		git_patchset_body($fd, $diff_style,
7780		                  [ \%diffinfo ], $hash_base, $hash_parent_base);
7781		close $fd;
7782
7783		print "</div>\n"; # class="page_body"
7784		git_footer_html();
7785
7786	} else {
7787		while (my $line = <$fd>) {
7788			$line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7789			$line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
7790
7791			print $line;
7792
7793			last if $line =~ m!^\+\+\+!;
7794		}
7795		local $/ = undef;
7796		print <$fd>;
7797		close $fd;
7798	}
7799}
7800
7801sub git_blobdiff_plain {
7802	git_blobdiff('plain');
7803}
7804
7805# assumes that it is added as later part of already existing navigation,
7806# so it returns "| foo | bar" rather than just "foo | bar"
7807sub diff_style_nav {
7808	my ($diff_style, $is_combined) = @_;
7809	$diff_style ||= 'inline';
7810
7811	return "" if ($is_combined);
7812
7813	my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7814	my %styles = @styles;
7815	@styles =
7816		@styles[ map { $_ * 2 } 0..$#styles/2 ];
7817
7818	return join '',
7819		map { " | ".$_ }
7820		map {
7821			$_ eq $diff_style ? $styles{$_} :
7822			$cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7823		} @styles;
7824}
7825
7826sub git_commitdiff {
7827	my %params = @_;
7828	my $format = $params{-format} || 'html';
7829	my $diff_style = $input_params{'diff_style'} || 'inline';
7830
7831	my ($patch_max) = gitweb_get_feature('patches');
7832	if ($format eq 'patch') {
7833		die_error(403, "Patch view not allowed") unless $patch_max;
7834	}
7835
7836	$hash ||= $hash_base || "HEAD";
7837	my %co = parse_commit($hash)
7838	    or die_error(404, "Unknown commit object");
7839
7840	# choose format for commitdiff for merge
7841	if (! defined $hash_parent && @{$co{'parents'}} > 1) {
7842		$hash_parent = '--cc';
7843	}
7844	# we need to prepare $formats_nav before almost any parameter munging
7845	my $formats_nav;
7846	if ($format eq 'html') {
7847		$formats_nav =
7848			$cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
7849			        "raw");
7850		if ($patch_max && @{$co{'parents'}} <= 1) {
7851			$formats_nav .= " | " .
7852				$cgi->a({-href => href(action=>"patch", -replay=>1)},
7853					"patch");
7854		}
7855		$formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
7856
7857		if (defined $hash_parent &&
7858		    $hash_parent ne '-c' && $hash_parent ne '--cc') {
7859			# commitdiff with two commits given
7860			my $hash_parent_short = $hash_parent;
7861			if ($hash_parent =~ m/^$oid_regex$/) {
7862				$hash_parent_short = substr($hash_parent, 0, 7);
7863			}
7864			$formats_nav .=
7865				' (from';
7866			for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
7867				if ($co{'parents'}[$i] eq $hash_parent) {
7868					$formats_nav .= ' parent ' . ($i+1);
7869					last;
7870				}
7871			}
7872			$formats_nav .= ': ' .
7873				$cgi->a({-href => href(-replay=>1,
7874				                       hash=>$hash_parent, hash_base=>undef)},
7875				        esc_html($hash_parent_short)) .
7876				')';
7877		} elsif (!$co{'parent'}) {
7878			# --root commitdiff
7879			$formats_nav .= ' (initial)';
7880		} elsif (scalar @{$co{'parents'}} == 1) {
7881			# single parent commit
7882			$formats_nav .=
7883				' (parent: ' .
7884				$cgi->a({-href => href(-replay=>1,
7885				                       hash=>$co{'parent'}, hash_base=>undef)},
7886				        esc_html(substr($co{'parent'}, 0, 7))) .
7887				')';
7888		} else {
7889			# merge commit
7890			if ($hash_parent eq '--cc') {
7891				$formats_nav .= ' | ' .
7892					$cgi->a({-href => href(-replay=>1,
7893					                       hash=>$hash, hash_parent=>'-c')},
7894					        'combined');
7895			} else { # $hash_parent eq '-c'
7896				$formats_nav .= ' | ' .
7897					$cgi->a({-href => href(-replay=>1,
7898					                       hash=>$hash, hash_parent=>'--cc')},
7899					        'compact');
7900			}
7901			$formats_nav .=
7902				' (merge: ' .
7903				join(' ', map {
7904					$cgi->a({-href => href(-replay=>1,
7905					                       hash=>$_, hash_base=>undef)},
7906					        esc_html(substr($_, 0, 7)));
7907				} @{$co{'parents'}} ) .
7908				')';
7909		}
7910	}
7911
7912	my $hash_parent_param = $hash_parent;
7913	if (!defined $hash_parent_param) {
7914		# --cc for multiple parents, --root for parentless
7915		$hash_parent_param =
7916			@{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
7917	}
7918
7919	# read commitdiff
7920	my $fd;
7921	my @difftree;
7922	if ($format eq 'html') {
7923		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7924			"--no-commit-id", "--patch-with-raw", "--full-index",
7925			$hash_parent_param, $hash, "--"
7926			or die_error(500, "Open git-diff-tree failed");
7927
7928		while (my $line = <$fd>) {
7929			chomp $line;
7930			# empty line ends raw part of diff-tree output
7931			last unless $line;
7932			push @difftree, scalar parse_difftree_raw_line($line);
7933		}
7934
7935	} elsif ($format eq 'plain') {
7936		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
7937			'-p', $hash_parent_param, $hash, "--"
7938			or die_error(500, "Open git-diff-tree failed");
7939	} elsif ($format eq 'patch') {
7940		# For commit ranges, we limit the output to the number of
7941		# patches specified in the 'patches' feature.
7942		# For single commits, we limit the output to a single patch,
7943		# diverging from the git-format-patch default.
7944		my @commit_spec = ();
7945		if ($hash_parent) {
7946			if ($patch_max > 0) {
7947				push @commit_spec, "-$patch_max";
7948			}
7949			push @commit_spec, '-n', "$hash_parent..$hash";
7950		} else {
7951			if ($params{-single}) {
7952				push @commit_spec, '-1';
7953			} else {
7954				if ($patch_max > 0) {
7955					push @commit_spec, "-$patch_max";
7956				}
7957				push @commit_spec, "-n";
7958			}
7959			push @commit_spec, '--root', $hash;
7960		}
7961		open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
7962			'--encoding=utf8', '--stdout', @commit_spec
7963			or die_error(500, "Open git-format-patch failed");
7964	} else {
7965		die_error(400, "Unknown commitdiff format");
7966	}
7967
7968	# non-textual hash id's can be cached
7969	my $expires;
7970	if ($hash =~ m/^$oid_regex$/) {
7971		$expires = "+1d";
7972	}
7973
7974	# write commit message
7975	if ($format eq 'html') {
7976		my $refs = git_get_references();
7977		my $ref = format_ref_marker($refs, $co{'id'});
7978
7979		git_header_html(undef, $expires);
7980		git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
7981		git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
7982		print "<div class=\"title_text\">\n" .
7983		      "<table class=\"object_header\">\n";
7984		git_print_authorship_rows(\%co);
7985		print "</table>".
7986		      "</div>\n";
7987		print "<div class=\"page_body\">\n";
7988		if (@{$co{'comment'}} > 1) {
7989			print "<div class=\"log\">\n";
7990			git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
7991			print "</div>\n"; # class="log"
7992		}
7993
7994	} elsif ($format eq 'plain') {
7995		my $refs = git_get_references("tags");
7996		my $tagname = git_get_rev_name_tags($hash);
7997		my $filename = basename($project) . "-$hash.patch";
7998
7999		print $cgi->header(
8000			-type => 'text/plain',
8001			-charset => 'utf-8',
8002			-expires => $expires,
8003			-content_disposition => 'inline; filename="' . "$filename" . '"');
8004		my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
8005		print "From: " . to_utf8($co{'author'}) . "\n";
8006		print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
8007		print "Subject: " . to_utf8($co{'title'}) . "\n";
8008
8009		print "X-Git-Tag: $tagname\n" if $tagname;
8010		print "X-Git-Url: " . $cgi->self_url() . "\n\n";
8011
8012		foreach my $line (@{$co{'comment'}}) {
8013			print to_utf8($line) . "\n";
8014		}
8015		print "---\n\n";
8016	} elsif ($format eq 'patch') {
8017		my $filename = basename($project) . "-$hash.patch";
8018
8019		print $cgi->header(
8020			-type => 'text/plain',
8021			-charset => 'utf-8',
8022			-expires => $expires,
8023			-content_disposition => 'inline; filename="' . "$filename" . '"');
8024	}
8025
8026	# write patch
8027	if ($format eq 'html') {
8028		my $use_parents = !defined $hash_parent ||
8029			$hash_parent eq '-c' || $hash_parent eq '--cc';
8030		git_difftree_body(\@difftree, $hash,
8031		                  $use_parents ? @{$co{'parents'}} : $hash_parent);
8032		print "<br/>\n";
8033
8034		git_patchset_body($fd, $diff_style,
8035		                  \@difftree, $hash,
8036		                  $use_parents ? @{$co{'parents'}} : $hash_parent);
8037		close $fd;
8038		print "</div>\n"; # class="page_body"
8039		git_footer_html();
8040
8041	} elsif ($format eq 'plain') {
8042		local $/ = undef;
8043		print <$fd>;
8044		close $fd
8045			or print "Reading git-diff-tree failed\n";
8046	} elsif ($format eq 'patch') {
8047		local $/ = undef;
8048		print <$fd>;
8049		close $fd
8050			or print "Reading git-format-patch failed\n";
8051	}
8052}
8053
8054sub git_commitdiff_plain {
8055	git_commitdiff(-format => 'plain');
8056}
8057
8058# format-patch-style patches
8059sub git_patch {
8060	git_commitdiff(-format => 'patch', -single => 1);
8061}
8062
8063sub git_patches {
8064	git_commitdiff(-format => 'patch');
8065}
8066
8067sub git_history {
8068	git_log_generic('history', \&git_history_body,
8069	                $hash_base, $hash_parent_base,
8070	                $file_name, $hash);
8071}
8072
8073sub git_search {
8074	$searchtype ||= 'commit';
8075
8076	# check if appropriate features are enabled
8077	gitweb_check_feature('search')
8078		or die_error(403, "Search is disabled");
8079	if ($searchtype eq 'pickaxe') {
8080		# pickaxe may take all resources of your box and run for several minutes
8081		# with every query - so decide by yourself how public you make this feature
8082		gitweb_check_feature('pickaxe')
8083			or die_error(403, "Pickaxe search is disabled");
8084	}
8085	if ($searchtype eq 'grep') {
8086		# grep search might be potentially CPU-intensive, too
8087		gitweb_check_feature('grep')
8088			or die_error(403, "Grep search is disabled");
8089	}
8090
8091	if (!defined $searchtext) {
8092		die_error(400, "Text field is empty");
8093	}
8094	if (!defined $hash) {
8095		$hash = git_get_head_hash($project);
8096	}
8097	my %co = parse_commit($hash);
8098	if (!%co) {
8099		die_error(404, "Unknown commit object");
8100	}
8101	if (!defined $page) {
8102		$page = 0;
8103	}
8104
8105	if ($searchtype eq 'commit' ||
8106	    $searchtype eq 'author' ||
8107	    $searchtype eq 'committer') {
8108		git_search_message(%co);
8109	} elsif ($searchtype eq 'pickaxe') {
8110		git_search_changes(%co);
8111	} elsif ($searchtype eq 'grep') {
8112		git_search_files(%co);
8113	} else {
8114		die_error(400, "Unknown search type");
8115	}
8116}
8117
8118sub git_search_help {
8119	git_header_html();
8120	git_print_page_nav('','', $hash,$hash,$hash);
8121	print <<EOT;
8122<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
8123regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
8124the pattern entered is recognized as the POSIX extended
8125<a href="https://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
8126insensitive).</p>
8127<dl>
8128<dt><b>commit</b></dt>
8129<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
8130EOT
8131	my $have_grep = gitweb_check_feature('grep');
8132	if ($have_grep) {
8133		print <<EOT;
8134<dt><b>grep</b></dt>
8135<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
8136    a different one) are searched for the given pattern. On large trees, this search can take
8137a while and put some strain on the server, so please use it with some consideration. Note that
8138due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
8139case-sensitive.</dd>
8140EOT
8141	}
8142	print <<EOT;
8143<dt><b>author</b></dt>
8144<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
8145<dt><b>committer</b></dt>
8146<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
8147EOT
8148	my $have_pickaxe = gitweb_check_feature('pickaxe');
8149	if ($have_pickaxe) {
8150		print <<EOT;
8151<dt><b>pickaxe</b></dt>
8152<dd>All commits that caused the string to appear or disappear from any file (changes that
8153added, removed or "modified" the string) will be listed. This search can take a while and
8154takes a lot of strain on the server, so please use it wisely. Note that since you may be
8155interested even in changes just changing the case as well, this search is case sensitive.</dd>
8156EOT
8157	}
8158	print "</dl>\n";
8159	git_footer_html();
8160}
8161
8162sub git_shortlog {
8163	git_log_generic('shortlog', \&git_shortlog_body,
8164	                $hash, $hash_parent);
8165}
8166
8167## ......................................................................
8168## feeds (RSS, Atom; OPML)
8169
8170sub git_feed {
8171	my $format = shift || 'atom';
8172	my $have_blame = gitweb_check_feature('blame');
8173
8174	# Atom: http://www.atomenabled.org/developers/syndication/
8175	# RSS:  http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
8176	if ($format ne 'rss' && $format ne 'atom') {
8177		die_error(400, "Unknown web feed format");
8178	}
8179
8180	# log/feed of current (HEAD) branch, log of given branch, history of file/directory
8181	my $head = $hash || 'HEAD';
8182	my @commitlist = parse_commits($head, 150, 0, $file_name);
8183
8184	my %latest_commit;
8185	my %latest_date;
8186	my $content_type = "application/$format+xml";
8187	if (defined $cgi->http('HTTP_ACCEPT') &&
8188		 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
8189		# browser (feed reader) prefers text/xml
8190		$content_type = 'text/xml';
8191	}
8192	if (defined($commitlist[0])) {
8193		%latest_commit = %{$commitlist[0]};
8194		my $latest_epoch = $latest_commit{'committer_epoch'};
8195		exit_if_unmodified_since($latest_epoch);
8196		%latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
8197	}
8198	print $cgi->header(
8199		-type => $content_type,
8200		-charset => 'utf-8',
8201		%latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
8202		-status => '200 OK');
8203
8204	# Optimization: skip generating the body if client asks only
8205	# for Last-Modified date.
8206	return if ($cgi->request_method() eq 'HEAD');
8207
8208	# header variables
8209	my $title = "$site_name - $project/$action";
8210	my $feed_type = 'log';
8211	if (defined $hash) {
8212		$title .= " - '$hash'";
8213		$feed_type = 'branch log';
8214		if (defined $file_name) {
8215			$title .= " :: $file_name";
8216			$feed_type = 'history';
8217		}
8218	} elsif (defined $file_name) {
8219		$title .= " - $file_name";
8220		$feed_type = 'history';
8221	}
8222	$title .= " $feed_type";
8223	$title = esc_html($title);
8224	my $descr = git_get_project_description($project);
8225	if (defined $descr) {
8226		$descr = esc_html($descr);
8227	} else {
8228		$descr = "$project " .
8229		         ($format eq 'rss' ? 'RSS' : 'Atom') .
8230		         " feed";
8231	}
8232	my $owner = git_get_project_owner($project);
8233	$owner = esc_html($owner);
8234
8235	#header
8236	my $alt_url;
8237	if (defined $file_name) {
8238		$alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
8239	} elsif (defined $hash) {
8240		$alt_url = href(-full=>1, action=>"log", hash=>$hash);
8241	} else {
8242		$alt_url = href(-full=>1, action=>"summary");
8243	}
8244	$alt_url = esc_attr($alt_url);
8245	print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
8246	if ($format eq 'rss') {
8247		print <<XML;
8248<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
8249<channel>
8250XML
8251		print "<title>$title</title>\n" .
8252		      "<link>$alt_url</link>\n" .
8253		      "<description>$descr</description>\n" .
8254		      "<language>en</language>\n" .
8255		      # project owner is responsible for 'editorial' content
8256		      "<managingEditor>$owner</managingEditor>\n";
8257		if (defined $logo || defined $favicon) {
8258			# prefer the logo to the favicon, since RSS
8259			# doesn't allow both
8260			my $img = esc_url($logo || $favicon);
8261			print "<image>\n" .
8262			      "<url>$img</url>\n" .
8263			      "<title>$title</title>\n" .
8264			      "<link>$alt_url</link>\n" .
8265			      "</image>\n";
8266		}
8267		if (%latest_date) {
8268			print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
8269			print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
8270		}
8271		print "<generator>gitweb v.$version/$git_version</generator>\n";
8272	} elsif ($format eq 'atom') {
8273		print <<XML;
8274<feed xmlns="http://www.w3.org/2005/Atom">
8275XML
8276		print "<title>$title</title>\n" .
8277		      "<subtitle>$descr</subtitle>\n" .
8278		      '<link rel="alternate" type="text/html" href="' .
8279		      $alt_url . '" />' . "\n" .
8280		      '<link rel="self" type="' . $content_type . '" href="' .
8281		      $cgi->self_url() . '" />' . "\n" .
8282		      "<id>" . esc_url(href(-full=>1)) . "</id>\n" .
8283		      # use project owner for feed author
8284		      "<author><name>$owner</name></author>\n";
8285		if (defined $favicon) {
8286			print "<icon>" . esc_url($favicon) . "</icon>\n";
8287		}
8288		if (defined $logo) {
8289			# not twice as wide as tall: 72 x 27 pixels
8290			print "<logo>" . esc_url($logo) . "</logo>\n";
8291		}
8292		if (! %latest_date) {
8293			# dummy date to keep the feed valid until commits trickle in:
8294			print "<updated>1970-01-01T00:00:00Z</updated>\n";
8295		} else {
8296			print "<updated>$latest_date{'iso-8601'}</updated>\n";
8297		}
8298		print "<generator version='$version/$git_version'>gitweb</generator>\n";
8299	}
8300
8301	# contents
8302	for (my $i = 0; $i <= $#commitlist; $i++) {
8303		my %co = %{$commitlist[$i]};
8304		my $commit = $co{'id'};
8305		# we read 150, we always show 30 and the ones more recent than 48 hours
8306		if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
8307			last;
8308		}
8309		my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
8310
8311		# get list of changed files
8312		open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
8313			$co{'parent'} || "--root",
8314			$co{'id'}, "--", (defined $file_name ? $file_name : ())
8315			or next;
8316		my @difftree = map { chomp; $_ } <$fd>;
8317		close $fd
8318			or next;
8319
8320		# print element (entry, item)
8321		my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
8322		if ($format eq 'rss') {
8323			print "<item>\n" .
8324			      "<title>" . esc_html($co{'title'}) . "</title>\n" .
8325			      "<author>" . esc_html($co{'author'}) . "</author>\n" .
8326			      "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
8327			      "<guid isPermaLink=\"true\">$co_url</guid>\n" .
8328			      "<link>" . esc_html($co_url) . "</link>\n" .
8329			      "<description>" . esc_html($co{'title'}) . "</description>\n" .
8330			      "<content:encoded>" .
8331			      "<![CDATA[\n";
8332		} elsif ($format eq 'atom') {
8333			print "<entry>\n" .
8334			      "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
8335			      "<updated>$cd{'iso-8601'}</updated>\n" .
8336			      "<author>\n" .
8337			      "  <name>" . esc_html($co{'author_name'}) . "</name>\n";
8338			if ($co{'author_email'}) {
8339				print "  <email>" . esc_html($co{'author_email'}) . "</email>\n";
8340			}
8341			print "</author>\n" .
8342			      # use committer for contributor
8343			      "<contributor>\n" .
8344			      "  <name>" . esc_html($co{'committer_name'}) . "</name>\n";
8345			if ($co{'committer_email'}) {
8346				print "  <email>" . esc_html($co{'committer_email'}) . "</email>\n";
8347			}
8348			print "</contributor>\n" .
8349			      "<published>$cd{'iso-8601'}</published>\n" .
8350			      "<link rel=\"alternate\" type=\"text/html\" href=\"" . esc_attr($co_url) . "\" />\n" .
8351			      "<id>" . esc_html($co_url) . "</id>\n" .
8352			      "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
8353			      "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
8354		}
8355		my $comment = $co{'comment'};
8356		print "<pre>\n";
8357		foreach my $line (@$comment) {
8358			$line = esc_html($line);
8359			print "$line\n";
8360		}
8361		print "</pre><ul>\n";
8362		foreach my $difftree_line (@difftree) {
8363			my %difftree = parse_difftree_raw_line($difftree_line);
8364			next if !$difftree{'from_id'};
8365
8366			my $file = $difftree{'file'} || $difftree{'to_file'};
8367
8368			print "<li>" .
8369			      "[" .
8370			      $cgi->a({-href => href(-full=>1, action=>"blobdiff",
8371			                             hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
8372			                             hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
8373			                             file_name=>$file, file_parent=>$difftree{'from_file'}),
8374			              -title => "diff"}, 'D');
8375			if ($have_blame) {
8376				print $cgi->a({-href => href(-full=>1, action=>"blame",
8377				                             file_name=>$file, hash_base=>$commit),
8378				              -title => "blame"}, 'B');
8379			}
8380			# if this is not a feed of a file history
8381			if (!defined $file_name || $file_name ne $file) {
8382				print $cgi->a({-href => href(-full=>1, action=>"history",
8383				                             file_name=>$file, hash=>$commit),
8384				              -title => "history"}, 'H');
8385			}
8386			$file = esc_path($file);
8387			print "] ".
8388			      "$file</li>\n";
8389		}
8390		if ($format eq 'rss') {
8391			print "</ul>]]>\n" .
8392			      "</content:encoded>\n" .
8393			      "</item>\n";
8394		} elsif ($format eq 'atom') {
8395			print "</ul>\n</div>\n" .
8396			      "</content>\n" .
8397			      "</entry>\n";
8398		}
8399	}
8400
8401	# end of feed
8402	if ($format eq 'rss') {
8403		print "</channel>\n</rss>\n";
8404	} elsif ($format eq 'atom') {
8405		print "</feed>\n";
8406	}
8407}
8408
8409sub git_rss {
8410	git_feed('rss');
8411}
8412
8413sub git_atom {
8414	git_feed('atom');
8415}
8416
8417sub git_opml {
8418	my @list = git_get_projects_list($project_filter, $strict_export);
8419	if (!@list) {
8420		die_error(404, "No projects found");
8421	}
8422
8423	print $cgi->header(
8424		-type => 'text/xml',
8425		-charset => 'utf-8',
8426		-content_disposition => 'inline; filename="opml.xml"');
8427
8428	my $title = esc_html($site_name);
8429	my $filter = " within subdirectory ";
8430	if (defined $project_filter) {
8431		$filter .= esc_html($project_filter);
8432	} else {
8433		$filter = "";
8434	}
8435	print <<XML;
8436<?xml version="1.0" encoding="utf-8"?>
8437<opml version="1.0">
8438<head>
8439  <title>$title OPML Export$filter</title>
8440</head>
8441<body>
8442<outline text="git RSS feeds">
8443XML
8444
8445	foreach my $pr (@list) {
8446		my %proj = %$pr;
8447		my $head = git_get_head_hash($proj{'path'});
8448		if (!defined $head) {
8449			next;
8450		}
8451		$git_dir = "$projectroot/$proj{'path'}";
8452		my %co = parse_commit($head);
8453		if (!%co) {
8454			next;
8455		}
8456
8457		my $path = esc_html(chop_str($proj{'path'}, 25, 5));
8458		my $rss  = esc_attr(href('project' => $proj{'path'}, 'action' => 'rss', -full => 1));
8459		my $html = esc_attr(href('project' => $proj{'path'}, 'action' => 'summary', -full => 1));
8460		print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
8461	}
8462	print <<XML;
8463</outline>
8464</body>
8465</opml>
8466XML
8467}
8468