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