]> git.ipfire.org Git - thirdparty/git.git/blame - gitweb/gitweb.perl
gitweb: Fix warnings with override permitted but no repo override
[thirdparty/git.git] / gitweb / gitweb.perl
CommitLineData
161332a5
KS
1#!/usr/bin/perl
2
c994d620 3# gitweb - simple web interface to track changes in git repositories
22fafb99 4#
00cd0794
KS
5# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6# (C) 2005, Christian Gierke
823d5dc8 7#
d8f1c5c2 8# This program is licensed under the GPLv2
161332a5
KS
9
10use strict;
11use warnings;
19806691 12use CGI qw(:standard :escapeHTML -nosticky);
7403d50b 13use CGI::Util qw(unescape);
161332a5 14use CGI::Carp qw(fatalsToBrowser);
40c13813 15use Encode;
b87d78d6 16use Fcntl ':mode';
7a13b999 17use File::Find qw();
cb9c6e5b 18use File::Basename qw(basename);
10bb9036 19binmode STDOUT, ':utf8';
161332a5 20
b1f5f64f 21BEGIN {
3be8e720 22 CGI->compile() if $ENV{'MOD_PERL'};
b1f5f64f
JN
23}
24
4a87b43e 25our $cgi = new CGI;
06c084d2 26our $version = "++GIT_VERSION++";
4a87b43e
DS
27our $my_url = $cgi->url();
28our $my_uri = $cgi->url(-absolute => 1);
3e029299 29
81d3fe9f
GB
30# Base URL for relative URLs in gitweb ($logo, $favicon, ...),
31# needed and used only for URLs with nonempty PATH_INFO
32our $base_url = $my_url;
33
34# When the script is used as DirectoryIndex, the URL does not contain the name
35# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
36# have to do it ourselves. We make $path_info global because it's also used
37# later on.
38#
39# Another issue with the script being the DirectoryIndex is that the resulting
40# $my_url data is not the full script URL: this is good, because we want
41# generated links to keep implying the script name if it wasn't explicitly
42# indicated in the URL we're handling, but it means that $my_url cannot be used
43# as base URL.
44# Therefore, if we needed to strip PATH_INFO, then we know that we have
45# to build the base URL ourselves:
dde80d9c 46our $path_info = $ENV{"PATH_INFO"};
1b2d297e 47if ($path_info) {
81d3fe9f
GB
48 if ($my_url =~ s,\Q$path_info\E$,, &&
49 $my_uri =~ s,\Q$path_info\E$,, &&
50 defined $ENV{'SCRIPT_NAME'}) {
51 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
52 }
b65910fe
GB
53}
54
e130ddaa
AT
55# core git executable to use
56# this can just be "git" if your webserver has a sensible PATH
06c084d2 57our $GIT = "++GIT_BINDIR++/git";
3f7f2710 58
b87d78d6 59# absolute fs-path which will be prepended to the project path
4a87b43e 60#our $projectroot = "/pub/scm";
06c084d2 61our $projectroot = "++GITWEB_PROJECTROOT++";
b87d78d6 62
ca5e9495
LL
63# fs traversing limit for getting project list
64# the number is relative to the projectroot
65our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
66
b87d78d6 67# target of the home link on top of all pages
6132b7e4 68our $home_link = $my_uri || "/";
b87d78d6 69
2de21fac
YS
70# string of the home link on top of all pages
71our $home_link_str = "++GITWEB_HOME_LINK_STR++";
72
49da1daf
AT
73# name of your site or organization to appear in page titles
74# replace this with something more descriptive for clearer bookmarks
8be2890c
PB
75our $site_name = "++GITWEB_SITENAME++"
76 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
49da1daf 77
b2d3476e
AC
78# filename of html text to include at top of each page
79our $site_header = "++GITWEB_SITE_HEADER++";
8ab1da2c 80# html text to include at home page
06c084d2 81our $home_text = "++GITWEB_HOMETEXT++";
b2d3476e
AC
82# filename of html text to include at bottom of each page
83our $site_footer = "++GITWEB_SITE_FOOTER++";
84
85# URI of stylesheets
86our @stylesheets = ("++GITWEB_CSS++");
887a612f
PB
87# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
88our $stylesheet = undef;
9a7a62ff 89# URI of GIT logo (72x27 size)
06c084d2 90our $logo = "++GITWEB_LOGO++";
0b5deba1
JN
91# URI of GIT favicon, assumed to be image/png type
92our $favicon = "++GITWEB_FAVICON++";
aedd9425 93
9a7a62ff
JN
94# URI and label (title) of GIT logo link
95#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
96#our $logo_label = "git documentation";
97our $logo_url = "http://git.or.cz/";
98our $logo_label = "git homepage";
51a7c66a 99
09bd7898 100# source of projects list
06c084d2 101our $projects_list = "++GITWEB_LIST++";
b87d78d6 102
55feb120
MH
103# the width (in characters) of the projects list "Description" column
104our $projects_list_description_width = 25;
105
b06dcf8c
FL
106# default order of projects list
107# valid values are none, project, descr, owner, and age
108our $default_projects_order = "project";
109
32f4aacc
ML
110# show repository only if this file exists
111# (only effective if this variable evaluates to true)
112our $export_ok = "++GITWEB_EXPORT_OK++";
113
dd7f5f10
AG
114# show repository only if this subroutine returns true
115# when given the path to the project, for example:
116# sub { return -e "$_[0]/git-daemon-export-ok"; }
117our $export_auth_hook = undef;
118
32f4aacc
ML
119# only allow viewing of repositories also shown on the overview page
120our $strict_export = "++GITWEB_STRICT_EXPORT++";
121
19a8721e
JN
122# list of git base URLs used for URL to where fetch project from,
123# i.e. full URL is "$git_base_url/$project"
d6b7e0b9 124our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
19a8721e 125
f5aa79d9 126# default blob_plain mimetype and default charset for text/plain blob
4a87b43e
DS
127our $default_blob_plain_mimetype = 'text/plain';
128our $default_text_plain_charset = undef;
f5aa79d9 129
2d007374
PB
130# file to use for guessing MIME types before trying /etc/mime.types
131# (relative to the current git repository)
4a87b43e 132our $mimetypes_file = undef;
2d007374 133
00f429af
MK
134# assume this charset if line contains non-UTF-8 characters;
135# it should be valid encoding (see Encoding::Supported(3pm) for list),
136# for which encoding all byte sequences are valid, for example
137# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
138# could be even 'utf-8' for the old behavior)
139our $fallback_encoding = 'latin1';
140
69a9b41c
JN
141# rename detection options for git-diff and git-diff-tree
142# - default is '-M', with the cost proportional to
143# (number of removed files) * (number of new files).
144# - more costly is '-C' (which implies '-M'), with the cost proportional to
145# (number of changed files + number of removed files) * (number of new files)
146# - even more costly is '-C', '--find-copies-harder' with cost
147# (number of files in the original tree) * (number of new files)
148# - one might want to include '-B' option, e.g. '-B', '-M'
149our @diff_opts = ('-M'); # taken from git_commit
150
7e1100e9
MM
151# Disables features that would allow repository owners to inject script into
152# the gitweb domain.
153our $prevent_xss = 0;
154
a3c8ab30
MM
155# information about snapshot formats that gitweb is capable of serving
156our %known_snapshot_formats = (
157 # name => {
158 # 'display' => display name,
159 # 'type' => mime type,
160 # 'suffix' => filename suffix,
161 # 'format' => --format for git-archive,
162 # 'compressor' => [compressor command and arguments]
163 # (array reference, optional)}
164 #
165 'tgz' => {
166 'display' => 'tar.gz',
167 'type' => 'application/x-gzip',
168 'suffix' => '.tar.gz',
169 'format' => 'tar',
170 'compressor' => ['gzip']},
171
172 'tbz2' => {
173 'display' => 'tar.bz2',
174 'type' => 'application/x-bzip2',
175 'suffix' => '.tar.bz2',
176 'format' => 'tar',
177 'compressor' => ['bzip2']},
178
179 'zip' => {
180 'display' => 'zip',
181 'type' => 'application/x-zip',
182 'suffix' => '.zip',
183 'format' => 'zip'},
184);
185
186# Aliases so we understand old gitweb.snapshot values in repository
187# configuration.
188our %known_snapshot_format_aliases = (
189 'gzip' => 'tgz',
190 'bzip2' => 'tbz2',
191
192 # backward compatibility: legacy gitweb config support
193 'x-gzip' => undef, 'gz' => undef,
194 'x-bzip2' => undef, 'bz2' => undef,
195 'x-zip' => undef, '' => undef,
196);
197
ddb8d900
AK
198# You define site-wide feature defaults here; override them with
199# $GITWEB_CONFIG as necessary.
952c65fc 200our %feature = (
17848fc6
JN
201 # feature => {
202 # 'sub' => feature-sub (subroutine),
203 # 'override' => allow-override (boolean),
204 # 'default' => [ default options...] (array reference)}
205 #
b4b20b21 206 # if feature is overridable (it means that allow-override has true value),
17848fc6
JN
207 # then feature-sub will be called with default options as parameters;
208 # return value of feature-sub indicates if to enable specified feature
209 #
b4b20b21
JN
210 # if there is no 'sub' key (no feature-sub), then feature cannot be
211 # overriden
212 #
ff3c0ff2
GB
213 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
214 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
215 # is enabled
952c65fc 216
45a3b12c
PB
217 # Enable the 'blame' blob view, showing the last commit that modified
218 # each line in the file. This can be very CPU-intensive.
219
220 # To enable system wide have in $GITWEB_CONFIG
221 # $feature{'blame'}{'default'} = [1];
222 # To have project specific config enable override in $GITWEB_CONFIG
223 # $feature{'blame'}{'override'} = 1;
224 # and in project config gitweb.blame = 0|1;
952c65fc 225 'blame' => {
cdad8170 226 'sub' => sub { feature_bool('blame', @_) },
952c65fc
JN
227 'override' => 0,
228 'default' => [0]},
229
a3c8ab30 230 # Enable the 'snapshot' link, providing a compressed archive of any
45a3b12c
PB
231 # tree. This can potentially generate high traffic if you have large
232 # project.
233
a3c8ab30
MM
234 # Value is a list of formats defined in %known_snapshot_formats that
235 # you wish to offer.
45a3b12c 236 # To disable system wide have in $GITWEB_CONFIG
a3c8ab30 237 # $feature{'snapshot'}{'default'} = [];
45a3b12c 238 # To have project specific config enable override in $GITWEB_CONFIG
bbee1d97 239 # $feature{'snapshot'}{'override'} = 1;
a3c8ab30
MM
240 # and in project config, a comma-separated list of formats or "none"
241 # to disable. Example: gitweb.snapshot = tbz2,zip;
952c65fc
JN
242 'snapshot' => {
243 'sub' => \&feature_snapshot,
244 'override' => 0,
a3c8ab30 245 'default' => ['tgz']},
04f7a94f 246
6be93511
RF
247 # Enable text search, which will list the commits which match author,
248 # committer or commit text to a given string. Enabled by default.
b4b20b21 249 # Project specific override is not supported.
6be93511
RF
250 'search' => {
251 'override' => 0,
252 'default' => [1]},
253
e7738553
PB
254 # Enable grep search, which will list the files in currently selected
255 # tree containing the given string. Enabled by default. This can be
256 # potentially CPU-intensive, of course.
257
258 # To enable system wide have in $GITWEB_CONFIG
259 # $feature{'grep'}{'default'} = [1];
260 # To have project specific config enable override in $GITWEB_CONFIG
261 # $feature{'grep'}{'override'} = 1;
262 # and in project config gitweb.grep = 0|1;
263 'grep' => {
cdad8170 264 'sub' => sub { feature_bool('grep', @_) },
e7738553
PB
265 'override' => 0,
266 'default' => [1]},
267
45a3b12c
PB
268 # Enable the pickaxe search, which will list the commits that modified
269 # a given string in a file. This can be practical and quite faster
270 # alternative to 'blame', but still potentially CPU-intensive.
271
272 # To enable system wide have in $GITWEB_CONFIG
273 # $feature{'pickaxe'}{'default'} = [1];
274 # To have project specific config enable override in $GITWEB_CONFIG
275 # $feature{'pickaxe'}{'override'} = 1;
276 # and in project config gitweb.pickaxe = 0|1;
04f7a94f 277 'pickaxe' => {
cdad8170 278 'sub' => sub { feature_bool('pickaxe', @_) },
04f7a94f
JN
279 'override' => 0,
280 'default' => [1]},
9e756904 281
45a3b12c
PB
282 # Make gitweb use an alternative format of the URLs which can be
283 # more readable and natural-looking: project name is embedded
284 # directly in the path and the query string contains other
285 # auxiliary information. All gitweb installations recognize
286 # URL in either format; this configures in which formats gitweb
287 # generates links.
288
289 # To enable system wide have in $GITWEB_CONFIG
290 # $feature{'pathinfo'}{'default'} = [1];
291 # Project specific override is not supported.
292
293 # Note that you will need to change the default location of CSS,
294 # favicon, logo and possibly other files to an absolute URL. Also,
295 # if gitweb.cgi serves as your indexfile, you will need to force
296 # $my_uri to contain the script name in your $GITWEB_CONFIG.
9e756904
MW
297 'pathinfo' => {
298 'override' => 0,
299 'default' => [0]},
e30496df
PB
300
301 # Make gitweb consider projects in project root subdirectories
302 # to be forks of existing projects. Given project $projname.git,
303 # projects matching $projname/*.git will not be shown in the main
304 # projects list, instead a '+' mark will be added to $projname
305 # there and a 'forks' view will be enabled for the project, listing
c2b8b134
FL
306 # all the forks. If project list is taken from a file, forks have
307 # to be listed after the main project.
e30496df
PB
308
309 # To enable system wide have in $GITWEB_CONFIG
310 # $feature{'forks'}{'default'} = [1];
311 # Project specific override is not supported.
312 'forks' => {
313 'override' => 0,
314 'default' => [0]},
d627f68f
PB
315
316 # Insert custom links to the action bar of all project pages.
317 # This enables you mainly to link to third-party scripts integrating
318 # into gitweb; e.g. git-browser for graphical history representation
319 # or custom web-based repository administration interface.
320
321 # The 'default' value consists of a list of triplets in the form
322 # (label, link, position) where position is the label after which
2b11e059 323 # to insert the link and link is a format string where %n expands
d627f68f
PB
324 # to the project name, %f to the project path within the filesystem,
325 # %h to the current hash (h gitweb parameter) and %b to the current
2b11e059 326 # hash base (hb gitweb parameter); %% expands to %.
d627f68f
PB
327
328 # To enable system wide have in $GITWEB_CONFIG e.g.
329 # $feature{'actions'}{'default'} = [('graphiclog',
330 # '/git-browser/by-commit.html?r=%n', 'summary')];
331 # Project specific override is not supported.
332 'actions' => {
333 'override' => 0,
334 'default' => []},
3e3d4ee7 335
aed93de4
PB
336 # Allow gitweb scan project content tags described in ctags/
337 # of project repository, and display the popular Web 2.0-ish
338 # "tag cloud" near the project list. Note that this is something
339 # COMPLETELY different from the normal Git tags.
340
341 # gitweb by itself can show existing tags, but it does not handle
342 # tagging itself; you need an external application for that.
343 # For an example script, check Girocco's cgi/tagproj.cgi.
344 # You may want to install the HTML::TagCloud Perl module to get
345 # a pretty tag cloud instead of just a list of tags.
346
347 # To enable system wide have in $GITWEB_CONFIG
348 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
349 # Project specific override is not supported.
350 'ctags' => {
351 'override' => 0,
352 'default' => [0]},
9872cd6f
GB
353
354 # The maximum number of patches in a patchset generated in patch
355 # view. Set this to 0 or undef to disable patch view, or to a
356 # negative number to remove any limit.
357
358 # To disable system wide have in $GITWEB_CONFIG
359 # $feature{'patches'}{'default'} = [0];
360 # To have project specific config enable override in $GITWEB_CONFIG
361 # $feature{'patches'}{'override'} = 1;
362 # and in project config gitweb.patches = 0|n;
363 # where n is the maximum number of patches allowed in a patchset.
364 'patches' => {
365 'sub' => \&feature_patches,
366 'override' => 0,
367 'default' => [16]},
ddb8d900
AK
368);
369
a7c5a283 370sub gitweb_get_feature {
ddb8d900 371 my ($name) = @_;
dd1ad5f1 372 return unless exists $feature{$name};
952c65fc
JN
373 my ($sub, $override, @defaults) = (
374 $feature{$name}{'sub'},
375 $feature{$name}{'override'},
376 @{$feature{$name}{'default'}});
ddb8d900 377 if (!$override) { return @defaults; }
a9455919
MW
378 if (!defined $sub) {
379 warn "feature $name is not overrideable";
380 return @defaults;
381 }
ddb8d900
AK
382 return $sub->(@defaults);
383}
384
25b2790f
GB
385# A wrapper to check if a given feature is enabled.
386# With this, you can say
387#
388# my $bool_feat = gitweb_check_feature('bool_feat');
389# gitweb_check_feature('bool_feat') or somecode;
390#
391# instead of
392#
393# my ($bool_feat) = gitweb_get_feature('bool_feat');
394# (gitweb_get_feature('bool_feat'))[0] or somecode;
395#
396sub gitweb_check_feature {
397 return (gitweb_get_feature(@_))[0];
398}
399
400
cdad8170
MK
401sub feature_bool {
402 my $key = shift;
403 my ($val) = git_get_project_config($key, '--bool');
ddb8d900 404
df5d10a3
MC
405 if (!defined $val) {
406 return ($_[0]);
407 } elsif ($val eq 'true') {
cdad8170 408 return (1);
ddb8d900 409 } elsif ($val eq 'false') {
cdad8170 410 return (0);
ddb8d900 411 }
ddb8d900
AK
412}
413
ddb8d900 414sub feature_snapshot {
a3c8ab30 415 my (@fmts) = @_;
ddb8d900
AK
416
417 my ($val) = git_get_project_config('snapshot');
418
a3c8ab30
MM
419 if ($val) {
420 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
ddb8d900
AK
421 }
422
a3c8ab30 423 return @fmts;
de9272f4
LT
424}
425
9872cd6f
GB
426sub feature_patches {
427 my @val = (git_get_project_config('patches', '--int'));
428
429 if (@val) {
430 return @val;
431 }
432
433 return ($_[0]);
434}
435
2172ce4b
JH
436# checking HEAD file with -e is fragile if the repository was
437# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
438# and then pruned.
439sub check_head_link {
440 my ($dir) = @_;
441 my $headfile = "$dir/HEAD";
442 return ((-e $headfile) ||
443 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
444}
445
446sub check_export_ok {
447 my ($dir) = @_;
448 return (check_head_link($dir) &&
dd7f5f10
AG
449 (!$export_ok || -e "$dir/$export_ok") &&
450 (!$export_auth_hook || $export_auth_hook->($dir)));
2172ce4b
JH
451}
452
a781785d
JN
453# process alternate names for backward compatibility
454# filter out unsupported (unknown) snapshot formats
455sub filter_snapshot_fmts {
456 my @fmts = @_;
457
458 @fmts = map {
459 exists $known_snapshot_format_aliases{$_} ?
460 $known_snapshot_format_aliases{$_} : $_} @fmts;
461 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
462
463}
464
06c084d2 465our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
17a8b250
GP
466if (-e $GITWEB_CONFIG) {
467 do $GITWEB_CONFIG;
468} else {
469 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
470 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
471}
c8d138a8
JK
472
473# version of the core git binary
66115d36 474our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
c8d138a8
JK
475
476$projects_list ||= $projectroot;
c8d138a8 477
154b4d78 478# ======================================================================
09bd7898 479# input validation and dispatch
1b2d297e
GB
480
481# input parameters can be collected from a variety of sources (presently, CGI
482# and PATH_INFO), so we define an %input_params hash that collects them all
483# together during validation: this allows subsequent uses (e.g. href()) to be
484# agnostic of the parameter origin
485
dde80d9c 486our %input_params = ();
1b2d297e
GB
487
488# input parameters are stored with the long parameter name as key. This will
489# also be used in the href subroutine to convert parameters to their CGI
490# equivalent, and since the href() usage is the most frequent one, we store
491# the name -> CGI key mapping here, instead of the reverse.
492#
493# XXX: Warning: If you touch this, check the search form for updating,
494# too.
495
dde80d9c 496our @cgi_param_mapping = (
1b2d297e
GB
497 project => "p",
498 action => "a",
499 file_name => "f",
500 file_parent => "fp",
501 hash => "h",
502 hash_parent => "hp",
503 hash_base => "hb",
504 hash_parent_base => "hpb",
505 page => "pg",
506 order => "o",
507 searchtext => "s",
508 searchtype => "st",
509 snapshot_format => "sf",
510 extra_options => "opt",
511 search_use_regexp => "sr",
512);
dde80d9c 513our %cgi_param_mapping = @cgi_param_mapping;
1b2d297e
GB
514
515# we will also need to know the possible actions, for validation
dde80d9c 516our %actions = (
1b2d297e
GB
517 "blame" => \&git_blame,
518 "blobdiff" => \&git_blobdiff,
519 "blobdiff_plain" => \&git_blobdiff_plain,
520 "blob" => \&git_blob,
521 "blob_plain" => \&git_blob_plain,
522 "commitdiff" => \&git_commitdiff,
523 "commitdiff_plain" => \&git_commitdiff_plain,
524 "commit" => \&git_commit,
525 "forks" => \&git_forks,
526 "heads" => \&git_heads,
527 "history" => \&git_history,
528 "log" => \&git_log,
9872cd6f 529 "patch" => \&git_patch,
a3411f8a 530 "patches" => \&git_patches,
1b2d297e
GB
531 "rss" => \&git_rss,
532 "atom" => \&git_atom,
533 "search" => \&git_search,
534 "search_help" => \&git_search_help,
535 "shortlog" => \&git_shortlog,
536 "summary" => \&git_summary,
537 "tag" => \&git_tag,
538 "tags" => \&git_tags,
539 "tree" => \&git_tree,
540 "snapshot" => \&git_snapshot,
541 "object" => \&git_object,
542 # those below don't need $project
543 "opml" => \&git_opml,
544 "project_list" => \&git_project_list,
545 "project_index" => \&git_project_index,
546);
547
548# finally, we have the hash of allowed extra_options for the commands that
549# allow them
dde80d9c 550our %allowed_options = (
1b2d297e
GB
551 "--no-merges" => [ qw(rss atom log shortlog history) ],
552);
553
554# fill %input_params with the CGI parameters. All values except for 'opt'
555# should be single values, but opt can be an array. We should probably
556# build an array of parameters that can be multi-valued, but since for the time
557# being it's only this one, we just single it out
558while (my ($name, $symbol) = each %cgi_param_mapping) {
559 if ($symbol eq 'opt') {
560 $input_params{$name} = [ $cgi->param($symbol) ];
561 } else {
562 $input_params{$name} = $cgi->param($symbol);
563 }
564}
565
566# now read PATH_INFO and update the parameter list for missing parameters
567sub evaluate_path_info {
568 return if defined $input_params{'project'};
569 return if !$path_info;
570 $path_info =~ s,^/+,,;
571 return if !$path_info;
572
573 # find which part of PATH_INFO is project
574 my $project = $path_info;
575 $project =~ s,/+$,,;
576 while ($project && !check_head_link("$projectroot/$project")) {
577 $project =~ s,/*[^/]*$,,;
578 }
579 return unless $project;
580 $input_params{'project'} = $project;
581
582 # do not change any parameters if an action is given using the query string
583 return if $input_params{'action'};
584 $path_info =~ s,^\Q$project\E/*,,;
585
d8c28822
GB
586 # next, check if we have an action
587 my $action = $path_info;
588 $action =~ s,/.*$,,;
589 if (exists $actions{$action}) {
590 $path_info =~ s,^$action/*,,;
591 $input_params{'action'} = $action;
592 }
593
594 # list of actions that want hash_base instead of hash, but can have no
595 # pathname (f) parameter
596 my @wants_base = (
597 'tree',
598 'history',
599 );
600
b0be3838
GB
601 # we want to catch
602 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
603 my ($parentrefname, $parentpathname, $refname, $pathname) =
604 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
605
606 # first, analyze the 'current' part
1b2d297e 607 if (defined $pathname) {
d8c28822
GB
608 # we got "branch:filename" or "branch:dir/"
609 # we could use git_get_type(branch:pathname), but:
610 # - it needs $git_dir
611 # - it does a git() call
612 # - the convention of terminating directories with a slash
613 # makes it superfluous
614 # - embedding the action in the PATH_INFO would make it even
615 # more superfluous
1b2d297e
GB
616 $pathname =~ s,^/+,,;
617 if (!$pathname || substr($pathname, -1) eq "/") {
d8c28822 618 $input_params{'action'} ||= "tree";
1b2d297e
GB
619 $pathname =~ s,/$,,;
620 } else {
b0be3838
GB
621 # the default action depends on whether we had parent info
622 # or not
623 if ($parentrefname) {
624 $input_params{'action'} ||= "blobdiff_plain";
625 } else {
626 $input_params{'action'} ||= "blob_plain";
627 }
1b2d297e
GB
628 }
629 $input_params{'hash_base'} ||= $refname;
630 $input_params{'file_name'} ||= $pathname;
631 } elsif (defined $refname) {
d8c28822
GB
632 # we got "branch". In this case we have to choose if we have to
633 # set hash or hash_base.
634 #
635 # Most of the actions without a pathname only want hash to be
636 # set, except for the ones specified in @wants_base that want
637 # hash_base instead. It should also be noted that hand-crafted
638 # links having 'history' as an action and no pathname or hash
639 # set will fail, but that happens regardless of PATH_INFO.
640 $input_params{'action'} ||= "shortlog";
641 if (grep { $_ eq $input_params{'action'} } @wants_base) {
642 $input_params{'hash_base'} ||= $refname;
643 } else {
644 $input_params{'hash'} ||= $refname;
645 }
1b2d297e 646 }
b0be3838
GB
647
648 # next, handle the 'parent' part, if present
649 if (defined $parentrefname) {
650 # a missing pathspec defaults to the 'current' filename, allowing e.g.
651 # someproject/blobdiff/oldrev..newrev:/filename
652 if ($parentpathname) {
653 $parentpathname =~ s,^/+,,;
654 $parentpathname =~ s,/$,,;
655 $input_params{'file_parent'} ||= $parentpathname;
656 } else {
657 $input_params{'file_parent'} ||= $input_params{'file_name'};
658 }
659 # we assume that hash_parent_base is wanted if a path was specified,
660 # or if the action wants hash_base instead of hash
661 if (defined $input_params{'file_parent'} ||
662 grep { $_ eq $input_params{'action'} } @wants_base) {
663 $input_params{'hash_parent_base'} ||= $parentrefname;
664 } else {
665 $input_params{'hash_parent'} ||= $parentrefname;
666 }
667 }
1ec2fb5f
GB
668
669 # for the snapshot action, we allow URLs in the form
670 # $project/snapshot/$hash.ext
671 # where .ext determines the snapshot and gets removed from the
672 # passed $refname to provide the $hash.
673 #
674 # To be able to tell that $refname includes the format extension, we
675 # require the following two conditions to be satisfied:
676 # - the hash input parameter MUST have been set from the $refname part
677 # of the URL (i.e. they must be equal)
678 # - the snapshot format MUST NOT have been defined already (e.g. from
679 # CGI parameter sf)
680 # It's also useless to try any matching unless $refname has a dot,
681 # so we check for that too
682 if (defined $input_params{'action'} &&
683 $input_params{'action'} eq 'snapshot' &&
684 defined $refname && index($refname, '.') != -1 &&
685 $refname eq $input_params{'hash'} &&
686 !defined $input_params{'snapshot_format'}) {
687 # We loop over the known snapshot formats, checking for
688 # extensions. Allowed extensions are both the defined suffix
689 # (which includes the initial dot already) and the snapshot
690 # format key itself, with a prepended dot
691 while (my ($fmt, %opt) = each %known_snapshot_formats) {
692 my $hash = $refname;
693 my $sfx;
694 $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//;
695 next unless $sfx = $1;
696 # a valid suffix was found, so set the snapshot format
697 # and reset the hash parameter
698 $input_params{'snapshot_format'} = $fmt;
699 $input_params{'hash'} = $hash;
700 # we also set the format suffix to the one requested
701 # in the URL: this way a request for e.g. .tgz returns
702 # a .tgz instead of a .tar.gz
703 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
704 last;
705 }
706 }
1b2d297e
GB
707}
708evaluate_path_info();
709
710our $action = $input_params{'action'};
09bd7898 711if (defined $action) {
1b2d297e 712 if (!validate_action($action)) {
074afaa0 713 die_error(400, "Invalid action parameter");
b87d78d6 714 }
b87d78d6 715}
44ad2978 716
24d0693a 717# parameters which are pathnames
1b2d297e 718our $project = $input_params{'project'};
13d02165 719if (defined $project) {
1b2d297e 720 if (!validate_project($project)) {
7939fe44 721 undef $project;
074afaa0 722 die_error(404, "No such project");
9cd3d988 723 }
a59d4afd 724}
6191f8e1 725
1b2d297e 726our $file_name = $input_params{'file_name'};
24d0693a
JN
727if (defined $file_name) {
728 if (!validate_pathname($file_name)) {
074afaa0 729 die_error(400, "Invalid file parameter");
24d0693a
JN
730 }
731}
732
1b2d297e 733our $file_parent = $input_params{'file_parent'};
24d0693a
JN
734if (defined $file_parent) {
735 if (!validate_pathname($file_parent)) {
074afaa0 736 die_error(400, "Invalid file parent parameter");
24d0693a
JN
737 }
738}
5c95fab0 739
24d0693a 740# parameters which are refnames
1b2d297e 741our $hash = $input_params{'hash'};
4fac5294 742if (defined $hash) {
24d0693a 743 if (!validate_refname($hash)) {
074afaa0 744 die_error(400, "Invalid hash parameter");
4fac5294 745 }
a59d4afd 746}
6191f8e1 747
1b2d297e 748our $hash_parent = $input_params{'hash_parent'};
c91da262 749if (defined $hash_parent) {
24d0693a 750 if (!validate_refname($hash_parent)) {
074afaa0 751 die_error(400, "Invalid hash parent parameter");
c91da262 752 }
09bd7898
KS
753}
754
1b2d297e 755our $hash_base = $input_params{'hash_base'};
c91da262 756if (defined $hash_base) {
24d0693a 757 if (!validate_refname($hash_base)) {
074afaa0 758 die_error(400, "Invalid hash base parameter");
c91da262 759 }
a59d4afd 760}
6191f8e1 761
1b2d297e
GB
762our @extra_options = @{$input_params{'extra_options'}};
763# @extra_options is always defined, since it can only be (currently) set from
764# CGI, and $cgi->param() returns the empty array in array context if the param
765# is not set
766foreach my $opt (@extra_options) {
767 if (not exists $allowed_options{$opt}) {
768 die_error(400, "Invalid option parameter");
769 }
770 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
771 die_error(400, "Invalid option parameter for this action");
868bc068
MV
772 }
773}
774
1b2d297e 775our $hash_parent_base = $input_params{'hash_parent_base'};
420e92f2 776if (defined $hash_parent_base) {
24d0693a 777 if (!validate_refname($hash_parent_base)) {
074afaa0 778 die_error(400, "Invalid hash parent base parameter");
420e92f2
JN
779 }
780}
781
24d0693a 782# other parameters
1b2d297e 783our $page = $input_params{'page'};
ea4a6df4 784if (defined $page) {
ac8e3f2b 785 if ($page =~ m/[^0-9]/) {
074afaa0 786 die_error(400, "Invalid page parameter");
b87d78d6 787 }
2ad9331e 788}
823d5dc8 789
1b2d297e 790our $searchtype = $input_params{'searchtype'};
e7738553
PB
791if (defined $searchtype) {
792 if ($searchtype =~ m/[^a-z]/) {
074afaa0 793 die_error(400, "Invalid searchtype parameter");
e7738553
PB
794 }
795}
796
1b2d297e 797our $search_use_regexp = $input_params{'search_use_regexp'};
0e559919 798
1b2d297e 799our $searchtext = $input_params{'searchtext'};
7e431ef9 800our $search_regexp;
19806691 801if (defined $searchtext) {
9d032c72 802 if (length($searchtext) < 2) {
074afaa0 803 die_error(403, "At least two characters are required for search parameter");
9d032c72 804 }
0e559919 805 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
19806691
KS
806}
807
645927ce
ML
808# path to the current git repository
809our $git_dir;
810$git_dir = "$projectroot/$project" if $project;
dd70235f 811
5e166843 812# list of supported snapshot formats
a7c5a283 813our @snapshot_fmts = gitweb_get_feature('snapshot');
5e166843
GB
814@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
815
717b8311 816# dispatch
7f9778b1
GP
817if (!defined $action) {
818 if (defined $hash) {
819 $action = git_get_type($hash);
820 } elsif (defined $hash_base && defined $file_name) {
821 $action = git_get_type("$hash_base:$file_name");
822 } elsif (defined $project) {
823 $action = 'summary';
824 } else {
825 $action = 'project_list';
826 }
77a153fd 827}
8e85cdc4 828if (!defined($actions{$action})) {
074afaa0 829 die_error(400, "Unknown action");
09bd7898 830}
d04d3d42
JN
831if ($action !~ m/^(opml|project_list|project_index)$/ &&
832 !$project) {
074afaa0 833 die_error(400, "Project needed");
d04d3d42 834}
8e85cdc4
ML
835$actions{$action}->();
836exit;
09bd7898 837
06a9d86b
MW
838## ======================================================================
839## action links
840
3562198b 841sub href (%) {
498fe002 842 my %params = @_;
bd5d1e42
JN
843 # default is to use -absolute url() i.e. $my_uri
844 my $href = $params{-full} ? $my_url : $my_uri;
498fe002 845
afa9b620
JN
846 $params{'project'} = $project unless exists $params{'project'};
847
1cad283a 848 if ($params{-replay}) {
1b2d297e 849 while (my ($name, $symbol) = each %cgi_param_mapping) {
1cad283a 850 if (!exists $params{$name}) {
1b2d297e 851 $params{$name} = $input_params{$name};
1cad283a
JN
852 }
853 }
854 }
855
25b2790f 856 my $use_pathinfo = gitweb_check_feature('pathinfo');
fb098a94 857 if ($use_pathinfo and defined $params{'project'}) {
b02bd7a6
GB
858 # try to put as many parameters as possible in PATH_INFO:
859 # - project name
860 # - action
8db49a7f 861 # - hash_parent or hash_parent_base:/file_parent
3550ea71 862 # - hash or hash_base:/filename
c752a0e0 863 # - the snapshot_format as an appropriate suffix
b02bd7a6
GB
864
865 # When the script is the root DirectoryIndex for the domain,
866 # $href here would be something like http://gitweb.example.com/
867 # Thus, we strip any trailing / from $href, to spare us double
868 # slashes in the final URL
869 $href =~ s,/$,,;
870
871 # Then add the project name, if present
fb098a94 872 $href .= "/".esc_url($params{'project'});
9e756904
MW
873 delete $params{'project'};
874
c752a0e0
GB
875 # since we destructively absorb parameters, we keep this
876 # boolean that remembers if we're handling a snapshot
877 my $is_snapshot = $params{'action'} eq 'snapshot';
878
b02bd7a6
GB
879 # Summary just uses the project path URL, any other action is
880 # added to the URL
881 if (defined $params{'action'}) {
882 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
9e756904
MW
883 delete $params{'action'};
884 }
b02bd7a6 885
8db49a7f
GB
886 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
887 # stripping nonexistent or useless pieces
888 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
889 || $params{'hash_parent'} || $params{'hash'});
b02bd7a6 890 if (defined $params{'hash_base'}) {
8db49a7f
GB
891 if (defined $params{'hash_parent_base'}) {
892 $href .= esc_url($params{'hash_parent_base'});
893 # skip the file_parent if it's the same as the file_name
894 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
895 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
896 $href .= ":/".esc_url($params{'file_parent'});
897 delete $params{'file_parent'};
898 }
899 $href .= "..";
900 delete $params{'hash_parent'};
901 delete $params{'hash_parent_base'};
902 } elsif (defined $params{'hash_parent'}) {
903 $href .= esc_url($params{'hash_parent'}). "..";
904 delete $params{'hash_parent'};
905 }
906
907 $href .= esc_url($params{'hash_base'});
908 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
3550ea71 909 $href .= ":/".esc_url($params{'file_name'});
b02bd7a6
GB
910 delete $params{'file_name'};
911 }
912 delete $params{'hash'};
913 delete $params{'hash_base'};
914 } elsif (defined $params{'hash'}) {
8db49a7f 915 $href .= esc_url($params{'hash'});
b02bd7a6
GB
916 delete $params{'hash'};
917 }
c752a0e0
GB
918
919 # If the action was a snapshot, we can absorb the
920 # snapshot_format parameter too
921 if ($is_snapshot) {
922 my $fmt = $params{'snapshot_format'};
923 # snapshot_format should always be defined when href()
924 # is called, but just in case some code forgets, we
925 # fall back to the default
926 $fmt ||= $snapshot_fmts[0];
927 $href .= $known_snapshot_formats{$fmt}{'suffix'};
928 delete $params{'snapshot_format'};
929 }
9e756904
MW
930 }
931
932 # now encode the parameters explicitly
498fe002 933 my @result = ();
1b2d297e
GB
934 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
935 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
498fe002 936 if (defined $params{$name}) {
f22cca44
JN
937 if (ref($params{$name}) eq "ARRAY") {
938 foreach my $par (@{$params{$name}}) {
939 push @result, $symbol . "=" . esc_param($par);
940 }
941 } else {
942 push @result, $symbol . "=" . esc_param($params{$name});
943 }
498fe002
JN
944 }
945 }
9e756904
MW
946 $href .= "?" . join(';', @result) if scalar @result;
947
948 return $href;
06a9d86b
MW
949}
950
951
717b8311
JN
952## ======================================================================
953## validation, quoting/unquoting and escaping
954
1b2d297e
GB
955sub validate_action {
956 my $input = shift || return undef;
957 return undef unless exists $actions{$input};
958 return $input;
959}
960
961sub validate_project {
962 my $input = shift || return undef;
963 if (!validate_pathname($input) ||
964 !(-d "$projectroot/$input") ||
ec26f098 965 !check_export_ok("$projectroot/$input") ||
1b2d297e
GB
966 ($strict_export && !project_in_list($input))) {
967 return undef;
968 } else {
969 return $input;
970 }
971}
972
24d0693a
JN
973sub validate_pathname {
974 my $input = shift || return undef;
717b8311 975
24d0693a
JN
976 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
977 # at the beginning, at the end, and between slashes.
978 # also this catches doubled slashes
979 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
980 return undef;
717b8311 981 }
24d0693a
JN
982 # no null characters
983 if ($input =~ m!\0!) {
717b8311
JN
984 return undef;
985 }
24d0693a
JN
986 return $input;
987}
988
989sub validate_refname {
990 my $input = shift || return undef;
991
992 # textual hashes are O.K.
993 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
994 return $input;
995 }
996 # it must be correct pathname
997 $input = validate_pathname($input)
998 or return undef;
999 # restrictions on ref name according to git-check-ref-format
1000 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
717b8311
JN
1001 return undef;
1002 }
1003 return $input;
1004}
1005
00f429af
MK
1006# decode sequences of octets in utf8 into Perl's internal form,
1007# which is utf-8 with utf8 flag set if needed. gitweb writes out
1008# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1009sub to_utf8 {
1010 my $str = shift;
e5d3de5c
İD
1011 if (utf8::valid($str)) {
1012 utf8::decode($str);
1013 return $str;
00f429af
MK
1014 } else {
1015 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1016 }
1017}
1018
232ff553
KS
1019# quote unsafe chars, but keep the slash, even when it's not
1020# correct, but quoted slashes look too horrible in bookmarks
1021sub esc_param {
353347b0 1022 my $str = shift;
a2f3db2f 1023 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
18216710 1024 $str =~ s/\+/%2B/g;
a9e60b7d 1025 $str =~ s/ /\+/g;
353347b0
KS
1026 return $str;
1027}
1028
f93bff8d
JN
1029# quote unsafe chars in whole URL, so some charactrs cannot be quoted
1030sub esc_url {
1031 my $str = shift;
1032 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1033 $str =~ s/\+/%2B/g;
1034 $str =~ s/ /\+/g;
1035 return $str;
1036}
1037
232ff553 1038# replace invalid utf8 character with SUBSTITUTION sequence
6255ef08 1039sub esc_html ($;%) {
40c13813 1040 my $str = shift;
6255ef08
JN
1041 my %opts = @_;
1042
00f429af 1043 $str = to_utf8($str);
c390ae97 1044 $str = $cgi->escapeHTML($str);
6255ef08
JN
1045 if ($opts{'-nbsp'}) {
1046 $str =~ s/ /&nbsp;/g;
1047 }
25ffbb27 1048 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
40c13813
KS
1049 return $str;
1050}
1051
391862e3
JN
1052# quote control characters and escape filename to HTML
1053sub esc_path {
1054 my $str = shift;
1055 my %opts = @_;
1056
00f429af 1057 $str = to_utf8($str);
c390ae97 1058 $str = $cgi->escapeHTML($str);
391862e3
JN
1059 if ($opts{'-nbsp'}) {
1060 $str =~ s/ /&nbsp;/g;
1061 }
1062 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1063 return $str;
1064}
1065
1066# Make control characters "printable", using character escape codes (CEC)
1d3bc0cc
JN
1067sub quot_cec {
1068 my $cntrl = shift;
c84c483f 1069 my %opts = @_;
1d3bc0cc 1070 my %es = ( # character escape codes, aka escape sequences
c84c483f
JN
1071 "\t" => '\t', # tab (HT)
1072 "\n" => '\n', # line feed (LF)
1073 "\r" => '\r', # carrige return (CR)
1074 "\f" => '\f', # form feed (FF)
1075 "\b" => '\b', # backspace (BS)
1076 "\a" => '\a', # alarm (bell) (BEL)
1077 "\e" => '\e', # escape (ESC)
1078 "\013" => '\v', # vertical tab (VT)
1079 "\000" => '\0', # nul character (NUL)
1080 );
1d3bc0cc
JN
1081 my $chr = ( (exists $es{$cntrl})
1082 ? $es{$cntrl}
25dfd171 1083 : sprintf('\%2x', ord($cntrl)) );
c84c483f
JN
1084 if ($opts{-nohtml}) {
1085 return $chr;
1086 } else {
1087 return "<span class=\"cntrl\">$chr</span>";
1088 }
1d3bc0cc
JN
1089}
1090
391862e3
JN
1091# Alternatively use unicode control pictures codepoints,
1092# Unicode "printable representation" (PR)
1d3bc0cc
JN
1093sub quot_upr {
1094 my $cntrl = shift;
c84c483f
JN
1095 my %opts = @_;
1096
1d3bc0cc 1097 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
c84c483f
JN
1098 if ($opts{-nohtml}) {
1099 return $chr;
1100 } else {
1101 return "<span class=\"cntrl\">$chr</span>";
1102 }
1d3bc0cc
JN
1103}
1104
232ff553
KS
1105# git may return quoted and escaped filenames
1106sub unquote {
1107 my $str = shift;
403d0906
JN
1108
1109 sub unq {
1110 my $seq = shift;
1111 my %es = ( # character escape codes, aka escape sequences
1112 't' => "\t", # tab (HT, TAB)
1113 'n' => "\n", # newline (NL)
1114 'r' => "\r", # return (CR)
1115 'f' => "\f", # form feed (FF)
1116 'b' => "\b", # backspace (BS)
1117 'a' => "\a", # alarm (bell) (BEL)
1118 'e' => "\e", # escape (ESC)
1119 'v' => "\013", # vertical tab (VT)
1120 );
1121
1122 if ($seq =~ m/^[0-7]{1,3}$/) {
1123 # octal char sequence
1124 return chr(oct($seq));
1125 } elsif (exists $es{$seq}) {
1126 # C escape sequence, aka character escape code
c84c483f 1127 return $es{$seq};
403d0906
JN
1128 }
1129 # quoted ordinary character
1130 return $seq;
1131 }
1132
232ff553 1133 if ($str =~ m/^"(.*)"$/) {
403d0906 1134 # needs unquoting
232ff553 1135 $str = $1;
403d0906 1136 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
232ff553
KS
1137 }
1138 return $str;
1139}
1140
f16db173
JN
1141# escape tabs (convert tabs to spaces)
1142sub untabify {
1143 my $line = shift;
1144
1145 while ((my $pos = index($line, "\t")) != -1) {
1146 if (my $count = (8 - ($pos % 8))) {
1147 my $spaces = ' ' x $count;
1148 $line =~ s/\t/$spaces/;
1149 }
1150 }
1151
1152 return $line;
1153}
1154
32f4aacc
ML
1155sub project_in_list {
1156 my $project = shift;
1157 my @list = git_get_projects_list();
1158 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1159}
1160
717b8311
JN
1161## ----------------------------------------------------------------------
1162## HTML aware string manipulation
1163
b8d97d07
JN
1164# Try to chop given string on a word boundary between position
1165# $len and $len+$add_len. If there is no word boundary there,
1166# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1167# (marking chopped part) would be longer than given string.
717b8311
JN
1168sub chop_str {
1169 my $str = shift;
1170 my $len = shift;
1171 my $add_len = shift || 10;
b8d97d07 1172 my $where = shift || 'right'; # 'left' | 'center' | 'right'
717b8311 1173
dee2775a
AW
1174 # Make sure perl knows it is utf8 encoded so we don't
1175 # cut in the middle of a utf8 multibyte char.
1176 $str = to_utf8($str);
1177
717b8311
JN
1178 # allow only $len chars, but don't cut a word if it would fit in $add_len
1179 # if it doesn't fit, cut it if it's still longer than the dots we would add
b8d97d07
JN
1180 # remove chopped character entities entirely
1181
1182 # when chopping in the middle, distribute $len into left and right part
1183 # return early if chopping wouldn't make string shorter
1184 if ($where eq 'center') {
1185 return $str if ($len + 5 >= length($str)); # filler is length 5
1186 $len = int($len/2);
1187 } else {
1188 return $str if ($len + 4 >= length($str)); # filler is length 4
1189 }
1190
1191 # regexps: ending and beginning with word part up to $add_len
1192 my $endre = qr/.{$len}\w{0,$add_len}/;
1193 my $begre = qr/\w{0,$add_len}.{$len}/;
1194
1195 if ($where eq 'left') {
1196 $str =~ m/^(.*?)($begre)$/;
1197 my ($lead, $body) = ($1, $2);
1198 if (length($lead) > 4) {
1199 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1200 $lead = " ...";
1201 }
1202 return "$lead$body";
1203
1204 } elsif ($where eq 'center') {
1205 $str =~ m/^($endre)(.*)$/;
1206 my ($left, $str) = ($1, $2);
1207 $str =~ m/^(.*?)($begre)$/;
1208 my ($mid, $right) = ($1, $2);
1209 if (length($mid) > 5) {
1210 $left =~ s/&[^;]*$//;
1211 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1212 $mid = " ... ";
1213 }
1214 return "$left$mid$right";
1215
1216 } else {
1217 $str =~ m/^($endre)(.*)$/;
1218 my $body = $1;
1219 my $tail = $2;
1220 if (length($tail) > 4) {
1221 $body =~ s/&[^;]*$//;
1222 $tail = "... ";
1223 }
1224 return "$body$tail";
717b8311 1225 }
717b8311
JN
1226}
1227
ce58ec91
DS
1228# takes the same arguments as chop_str, but also wraps a <span> around the
1229# result with a title attribute if it does get chopped. Additionally, the
1230# string is HTML-escaped.
1231sub chop_and_escape_str {
b8d97d07 1232 my ($str) = @_;
ce58ec91 1233
b8d97d07 1234 my $chopped = chop_str(@_);
ce58ec91
DS
1235 if ($chopped eq $str) {
1236 return esc_html($chopped);
1237 } else {
850b90a5
JN
1238 $str =~ s/([[:cntrl:]])/?/g;
1239 return $cgi->span({-title=>$str}, esc_html($chopped));
ce58ec91
DS
1240 }
1241}
1242
717b8311
JN
1243## ----------------------------------------------------------------------
1244## functions returning short strings
1245
1f1ab5f0
JN
1246# CSS class for given age value (in seconds)
1247sub age_class {
1248 my $age = shift;
1249
785cdea9
JN
1250 if (!defined $age) {
1251 return "noage";
1252 } elsif ($age < 60*60*2) {
1f1ab5f0
JN
1253 return "age0";
1254 } elsif ($age < 60*60*24*2) {
1255 return "age1";
1256 } else {
1257 return "age2";
1258 }
1259}
1260
717b8311
JN
1261# convert age in seconds to "nn units ago" string
1262sub age_string {
1263 my $age = shift;
1264 my $age_str;
a59d4afd 1265
717b8311
JN
1266 if ($age > 60*60*24*365*2) {
1267 $age_str = (int $age/60/60/24/365);
1268 $age_str .= " years ago";
1269 } elsif ($age > 60*60*24*(365/12)*2) {
1270 $age_str = int $age/60/60/24/(365/12);
1271 $age_str .= " months ago";
1272 } elsif ($age > 60*60*24*7*2) {
1273 $age_str = int $age/60/60/24/7;
1274 $age_str .= " weeks ago";
1275 } elsif ($age > 60*60*24*2) {
1276 $age_str = int $age/60/60/24;
1277 $age_str .= " days ago";
1278 } elsif ($age > 60*60*2) {
1279 $age_str = int $age/60/60;
1280 $age_str .= " hours ago";
1281 } elsif ($age > 60*2) {
1282 $age_str = int $age/60;
1283 $age_str .= " min ago";
1284 } elsif ($age > 2) {
1285 $age_str = int $age;
1286 $age_str .= " sec ago";
f6801d66 1287 } else {
717b8311 1288 $age_str .= " right now";
4c02e3c5 1289 }
717b8311 1290 return $age_str;
161332a5
KS
1291}
1292
01ac1e38
JN
1293use constant {
1294 S_IFINVALID => 0030000,
1295 S_IFGITLINK => 0160000,
1296};
1297
1298# submodule/subproject, a commit object reference
1299sub S_ISGITLINK($) {
1300 my $mode = shift;
1301
1302 return (($mode & S_IFMT) == S_IFGITLINK)
1303}
1304
717b8311
JN
1305# convert file mode in octal to symbolic file mode string
1306sub mode_str {
1307 my $mode = oct shift;
1308
01ac1e38
JN
1309 if (S_ISGITLINK($mode)) {
1310 return 'm---------';
1311 } elsif (S_ISDIR($mode & S_IFMT)) {
717b8311
JN
1312 return 'drwxr-xr-x';
1313 } elsif (S_ISLNK($mode)) {
1314 return 'lrwxrwxrwx';
1315 } elsif (S_ISREG($mode)) {
1316 # git cares only about the executable bit
1317 if ($mode & S_IXUSR) {
1318 return '-rwxr-xr-x';
1319 } else {
1320 return '-rw-r--r--';
1321 };
c994d620 1322 } else {
717b8311 1323 return '----------';
ff7669a5 1324 }
161332a5
KS
1325}
1326
717b8311
JN
1327# convert file mode in octal to file type string
1328sub file_type {
7c5e2ebb
JN
1329 my $mode = shift;
1330
1331 if ($mode !~ m/^[0-7]+$/) {
1332 return $mode;
1333 } else {
1334 $mode = oct $mode;
1335 }
664f4cc5 1336
01ac1e38
JN
1337 if (S_ISGITLINK($mode)) {
1338 return "submodule";
1339 } elsif (S_ISDIR($mode & S_IFMT)) {
717b8311
JN
1340 return "directory";
1341 } elsif (S_ISLNK($mode)) {
1342 return "symlink";
1343 } elsif (S_ISREG($mode)) {
1344 return "file";
1345 } else {
1346 return "unknown";
1347 }
a59d4afd
KS
1348}
1349
744d0ac3
JN
1350# convert file mode in octal to file type description string
1351sub file_type_long {
1352 my $mode = shift;
1353
1354 if ($mode !~ m/^[0-7]+$/) {
1355 return $mode;
1356 } else {
1357 $mode = oct $mode;
1358 }
1359
01ac1e38
JN
1360 if (S_ISGITLINK($mode)) {
1361 return "submodule";
1362 } elsif (S_ISDIR($mode & S_IFMT)) {
744d0ac3
JN
1363 return "directory";
1364 } elsif (S_ISLNK($mode)) {
1365 return "symlink";
1366 } elsif (S_ISREG($mode)) {
1367 if ($mode & S_IXUSR) {
1368 return "executable";
1369 } else {
1370 return "file";
1371 };
1372 } else {
1373 return "unknown";
1374 }
1375}
1376
1377
717b8311
JN
1378## ----------------------------------------------------------------------
1379## functions returning short HTML fragments, or transforming HTML fragments
3dff5379 1380## which don't belong to other sections
b18f9bf4 1381
225932ed 1382# format line of commit message.
717b8311
JN
1383sub format_log_line_html {
1384 my $line = shift;
b18f9bf4 1385
225932ed 1386 $line = esc_html($line, -nbsp=>1);
ccb04f99 1387 if ($line =~ m/\b([0-9a-fA-F]{8,40})\b/) {
717b8311 1388 my $hash_text = $1;
bfe2191f
JN
1389 my $link =
1390 $cgi->a({-href => href(action=>"object", hash=>$hash_text),
1391 -class => "text"}, $hash_text);
1392 $line =~ s/$hash_text/$link/;
b18f9bf4 1393 }
717b8311 1394 return $line;
b18f9bf4
JN
1395}
1396
717b8311 1397# format marker of refs pointing to given object
4afbaeff
GB
1398
1399# the destination action is chosen based on object type and current context:
1400# - for annotated tags, we choose the tag view unless it's the current view
1401# already, in which case we go to shortlog view
1402# - for other refs, we keep the current view if we're in history, shortlog or
1403# log view, and select shortlog otherwise
847e01fb 1404sub format_ref_marker {
717b8311 1405 my ($refs, $id) = @_;
d294e1ca 1406 my $markers = '';
27fb8c40 1407
717b8311 1408 if (defined $refs->{$id}) {
d294e1ca 1409 foreach my $ref (@{$refs->{$id}}) {
4afbaeff
GB
1410 # this code exploits the fact that non-lightweight tags are the
1411 # only indirect objects, and that they are the only objects for which
1412 # we want to use tag instead of shortlog as action
d294e1ca 1413 my ($type, $name) = qw();
4afbaeff 1414 my $indirect = ($ref =~ s/\^\{\}$//);
d294e1ca
JN
1415 # e.g. tags/v2.6.11 or heads/next
1416 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1417 $type = $1;
1418 $name = $2;
1419 } else {
1420 $type = "ref";
1421 $name = $ref;
1422 }
1423
4afbaeff
GB
1424 my $class = $type;
1425 $class .= " indirect" if $indirect;
1426
1427 my $dest_action = "shortlog";
1428
1429 if ($indirect) {
1430 $dest_action = "tag" unless $action eq "tag";
1431 } elsif ($action =~ /^(history|(short)?log)$/) {
1432 $dest_action = $action;
1433 }
1434
1435 my $dest = "";
1436 $dest .= "refs/" unless $ref =~ m!^refs/!;
1437 $dest .= $ref;
1438
1439 my $link = $cgi->a({
1440 -href => href(
1441 action=>$dest_action,
1442 hash=>$dest
1443 )}, $name);
1444
1445 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1446 $link . "</span>";
d294e1ca
JN
1447 }
1448 }
1449
1450 if ($markers) {
1451 return ' <span class="refs">'. $markers . '</span>';
717b8311
JN
1452 } else {
1453 return "";
1454 }
27fb8c40
JN
1455}
1456
17d07443
JN
1457# format, perhaps shortened and with markers, title line
1458sub format_subject_html {
1c2a4f5a 1459 my ($long, $short, $href, $extra) = @_;
17d07443
JN
1460 $extra = '' unless defined($extra);
1461
1462 if (length($short) < length($long)) {
7c278014 1463 return $cgi->a({-href => $href, -class => "list subject",
00f429af 1464 -title => to_utf8($long)},
17d07443
JN
1465 esc_html($short) . $extra);
1466 } else {
7c278014 1467 return $cgi->a({-href => $href, -class => "list subject"},
17d07443
JN
1468 esc_html($long) . $extra);
1469 }
1470}
1471
90921740
JN
1472# format git diff header line, i.e. "diff --(git|combined|cc) ..."
1473sub format_git_diff_header_line {
1474 my $line = shift;
1475 my $diffinfo = shift;
1476 my ($from, $to) = @_;
1477
1478 if ($diffinfo->{'nparents'}) {
1479 # combined diff
1480 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1481 if ($to->{'href'}) {
1482 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1483 esc_path($to->{'file'}));
1484 } else { # file was deleted (no href)
1485 $line .= esc_path($to->{'file'});
1486 }
1487 } else {
1488 # "ordinary" diff
1489 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1490 if ($from->{'href'}) {
1491 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1492 'a/' . esc_path($from->{'file'}));
1493 } else { # file was added (no href)
1494 $line .= 'a/' . esc_path($from->{'file'});
1495 }
1496 $line .= ' ';
1497 if ($to->{'href'}) {
1498 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1499 'b/' . esc_path($to->{'file'}));
1500 } else { # file was deleted
1501 $line .= 'b/' . esc_path($to->{'file'});
1502 }
1503 }
1504
1505 return "<div class=\"diff header\">$line</div>\n";
1506}
1507
1508# format extended diff header line, before patch itself
1509sub format_extended_diff_header_line {
1510 my $line = shift;
1511 my $diffinfo = shift;
1512 my ($from, $to) = @_;
1513
1514 # match <path>
1515 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1516 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1517 esc_path($from->{'file'}));
1518 }
1519 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1520 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1521 esc_path($to->{'file'}));
1522 }
1523 # match single <mode>
1524 if ($line =~ m/\s(\d{6})$/) {
1525 $line .= '<span class="info"> (' .
1526 file_type_long($1) .
1527 ')</span>';
1528 }
1529 # match <hash>
1530 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1531 # can match only for combined diff
1532 $line = 'index ';
1533 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1534 if ($from->{'href'}[$i]) {
1535 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1536 -class=>"hash"},
1537 substr($diffinfo->{'from_id'}[$i],0,7));
1538 } else {
1539 $line .= '0' x 7;
1540 }
1541 # separator
1542 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1543 }
1544 $line .= '..';
1545 if ($to->{'href'}) {
1546 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1547 substr($diffinfo->{'to_id'},0,7));
1548 } else {
1549 $line .= '0' x 7;
1550 }
1551
1552 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1553 # can match only for ordinary diff
1554 my ($from_link, $to_link);
1555 if ($from->{'href'}) {
1556 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1557 substr($diffinfo->{'from_id'},0,7));
1558 } else {
1559 $from_link = '0' x 7;
1560 }
1561 if ($to->{'href'}) {
1562 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1563 substr($diffinfo->{'to_id'},0,7));
1564 } else {
1565 $to_link = '0' x 7;
1566 }
1567 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1568 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1569 }
1570
1571 return $line . "<br/>\n";
1572}
1573
1574# format from-file/to-file diff header
1575sub format_diff_from_to_header {
91af4ce4 1576 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
90921740
JN
1577 my $line;
1578 my $result = '';
1579
1580 $line = $from_line;
1581 #assert($line =~ m/^---/) if DEBUG;
deaa01a9
JN
1582 # no extra formatting for "^--- /dev/null"
1583 if (! $diffinfo->{'nparents'}) {
1584 # ordinary (single parent) diff
1585 if ($line =~ m!^--- "?a/!) {
1586 if ($from->{'href'}) {
1587 $line = '--- a/' .
1588 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1589 esc_path($from->{'file'}));
1590 } else {
1591 $line = '--- a/' .
1592 esc_path($from->{'file'});
1593 }
1594 }
1595 $result .= qq!<div class="diff from_file">$line</div>\n!;
1596
1597 } else {
1598 # combined diff (merge commit)
1599 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1600 if ($from->{'href'}[$i]) {
1601 $line = '--- ' .
91af4ce4
JN
1602 $cgi->a({-href=>href(action=>"blobdiff",
1603 hash_parent=>$diffinfo->{'from_id'}[$i],
1604 hash_parent_base=>$parents[$i],
1605 file_parent=>$from->{'file'}[$i],
1606 hash=>$diffinfo->{'to_id'},
1607 hash_base=>$hash,
1608 file_name=>$to->{'file'}),
1609 -class=>"path",
1610 -title=>"diff" . ($i+1)},
1611 $i+1) .
1612 '/' .
deaa01a9
JN
1613 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1614 esc_path($from->{'file'}[$i]));
1615 } else {
1616 $line = '--- /dev/null';
1617 }
1618 $result .= qq!<div class="diff from_file">$line</div>\n!;
90921740
JN
1619 }
1620 }
90921740
JN
1621
1622 $line = $to_line;
1623 #assert($line =~ m/^\+\+\+/) if DEBUG;
1624 # no extra formatting for "^+++ /dev/null"
1625 if ($line =~ m!^\+\+\+ "?b/!) {
1626 if ($to->{'href'}) {
1627 $line = '+++ b/' .
1628 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1629 esc_path($to->{'file'}));
1630 } else {
1631 $line = '+++ b/' .
1632 esc_path($to->{'file'});
1633 }
1634 }
1635 $result .= qq!<div class="diff to_file">$line</div>\n!;
1636
1637 return $result;
1638}
1639
cd030c3a
JN
1640# create note for patch simplified by combined diff
1641sub format_diff_cc_simplified {
1642 my ($diffinfo, @parents) = @_;
1643 my $result = '';
1644
1645 $result .= "<div class=\"diff header\">" .
1646 "diff --cc ";
1647 if (!is_deleted($diffinfo)) {
1648 $result .= $cgi->a({-href => href(action=>"blob",
1649 hash_base=>$hash,
1650 hash=>$diffinfo->{'to_id'},
1651 file_name=>$diffinfo->{'to_file'}),
1652 -class => "path"},
1653 esc_path($diffinfo->{'to_file'}));
1654 } else {
1655 $result .= esc_path($diffinfo->{'to_file'});
1656 }
1657 $result .= "</div>\n" . # class="diff header"
1658 "<div class=\"diff nodifferences\">" .
1659 "Simple merge" .
1660 "</div>\n"; # class="diff nodifferences"
1661
1662 return $result;
1663}
1664
90921740 1665# format patch (diff) line (not to be used for diff headers)
eee08903
JN
1666sub format_diff_line {
1667 my $line = shift;
59e3b14e 1668 my ($from, $to) = @_;
eee08903
JN
1669 my $diff_class = "";
1670
1671 chomp $line;
1672
e72c0eaf
JN
1673 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1674 # combined diff
1675 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1676 if ($line =~ m/^\@{3}/) {
1677 $diff_class = " chunk_header";
1678 } elsif ($line =~ m/^\\/) {
1679 $diff_class = " incomplete";
1680 } elsif ($prefix =~ tr/+/+/) {
1681 $diff_class = " add";
1682 } elsif ($prefix =~ tr/-/-/) {
1683 $diff_class = " rem";
1684 }
1685 } else {
1686 # assume ordinary diff
1687 my $char = substr($line, 0, 1);
1688 if ($char eq '+') {
1689 $diff_class = " add";
1690 } elsif ($char eq '-') {
1691 $diff_class = " rem";
1692 } elsif ($char eq '@') {
1693 $diff_class = " chunk_header";
1694 } elsif ($char eq "\\") {
1695 $diff_class = " incomplete";
1696 }
eee08903
JN
1697 }
1698 $line = untabify($line);
59e3b14e
JN
1699 if ($from && $to && $line =~ m/^\@{2} /) {
1700 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1701 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1702
1703 $from_lines = 0 unless defined $from_lines;
1704 $to_lines = 0 unless defined $to_lines;
1705
1706 if ($from->{'href'}) {
1707 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1708 -class=>"list"}, $from_text);
1709 }
1710 if ($to->{'href'}) {
1711 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1712 -class=>"list"}, $to_text);
1713 }
1714 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1715 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1716 return "<div class=\"diff$diff_class\">$line</div>\n";
e72c0eaf
JN
1717 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1718 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1719 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1720
1721 @from_text = split(' ', $ranges);
1722 for (my $i = 0; $i < @from_text; ++$i) {
1723 ($from_start[$i], $from_nlines[$i]) =
1724 (split(',', substr($from_text[$i], 1)), 0);
1725 }
1726
1727 $to_text = pop @from_text;
1728 $to_start = pop @from_start;
1729 $to_nlines = pop @from_nlines;
1730
1731 $line = "<span class=\"chunk_info\">$prefix ";
1732 for (my $i = 0; $i < @from_text; ++$i) {
1733 if ($from->{'href'}[$i]) {
1734 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1735 -class=>"list"}, $from_text[$i]);
1736 } else {
1737 $line .= $from_text[$i];
1738 }
1739 $line .= " ";
1740 }
1741 if ($to->{'href'}) {
1742 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1743 -class=>"list"}, $to_text);
1744 } else {
1745 $line .= $to_text;
1746 }
1747 $line .= " $prefix</span>" .
1748 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1749 return "<div class=\"diff$diff_class\">$line</div>\n";
59e3b14e 1750 }
6255ef08 1751 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
eee08903
JN
1752}
1753
a3c8ab30
MM
1754# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1755# linked. Pass the hash of the tree/commit to snapshot.
1756sub format_snapshot_links {
1757 my ($hash) = @_;
a3c8ab30
MM
1758 my $num_fmts = @snapshot_fmts;
1759 if ($num_fmts > 1) {
1760 # A parenthesized list of links bearing format names.
a781785d 1761 # e.g. "snapshot (_tar.gz_ _zip_)"
a3c8ab30
MM
1762 return "snapshot (" . join(' ', map
1763 $cgi->a({
1764 -href => href(
1765 action=>"snapshot",
1766 hash=>$hash,
1767 snapshot_format=>$_
1768 )
1769 }, $known_snapshot_formats{$_}{'display'})
1770 , @snapshot_fmts) . ")";
1771 } elsif ($num_fmts == 1) {
1772 # A single "snapshot" link whose tooltip bears the format name.
a781785d 1773 # i.e. "_snapshot_"
a3c8ab30 1774 my ($fmt) = @snapshot_fmts;
a781785d
JN
1775 return
1776 $cgi->a({
a3c8ab30
MM
1777 -href => href(
1778 action=>"snapshot",
1779 hash=>$hash,
1780 snapshot_format=>$fmt
1781 ),
1782 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1783 }, "snapshot");
1784 } else { # $num_fmts == 0
1785 return undef;
1786 }
1787}
1788
3562198b
JN
1789## ......................................................................
1790## functions returning values to be passed, perhaps after some
1791## transformation, to other functions; e.g. returning arguments to href()
1792
1793# returns hash to be passed to href to generate gitweb URL
1794# in -title key it returns description of link
1795sub get_feed_info {
1796 my $format = shift || 'Atom';
1797 my %res = (action => lc($format));
1798
1799 # feed links are possible only for project views
1800 return unless (defined $project);
1801 # some views should link to OPML, or to generic project feed,
1802 # or don't have specific feed yet (so they should use generic)
1803 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1804
1805 my $branch;
1806 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1807 # from tag links; this also makes possible to detect branch links
1808 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1809 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1810 $branch = $1;
1811 }
1812 # find log type for feed description (title)
1813 my $type = 'log';
1814 if (defined $file_name) {
1815 $type = "history of $file_name";
1816 $type .= "/" if ($action eq 'tree');
1817 $type .= " on '$branch'" if (defined $branch);
1818 } else {
1819 $type = "log of $branch" if (defined $branch);
1820 }
1821
1822 $res{-title} = $type;
1823 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1824 $res{'file_name'} = $file_name;
1825
1826 return %res;
1827}
1828
717b8311
JN
1829## ----------------------------------------------------------------------
1830## git utility subroutines, invoking git commands
42f7eb94 1831
25691fbe
DS
1832# returns path to the core git executable and the --git-dir parameter as list
1833sub git_cmd {
1834 return $GIT, '--git-dir='.$git_dir;
1835}
1836
516381d5
LW
1837# quote the given arguments for passing them to the shell
1838# quote_command("command", "arg 1", "arg with ' and ! characters")
1839# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1840# Try to avoid using this function wherever possible.
1841sub quote_command {
1842 return join(' ',
1843 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
25691fbe
DS
1844}
1845
717b8311 1846# get HEAD ref of given project as hash
847e01fb 1847sub git_get_head_hash {
df2c37a5 1848 my $project = shift;
25691fbe 1849 my $o_git_dir = $git_dir;
df2c37a5 1850 my $retval = undef;
25691fbe
DS
1851 $git_dir = "$projectroot/$project";
1852 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
df2c37a5
JH
1853 my $head = <$fd>;
1854 close $fd;
2c5c008b
KS
1855 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1856 $retval = $1;
df2c37a5
JH
1857 }
1858 }
25691fbe
DS
1859 if (defined $o_git_dir) {
1860 $git_dir = $o_git_dir;
2c5c008b 1861 }
df2c37a5
JH
1862 return $retval;
1863}
1864
717b8311
JN
1865# get type of given object
1866sub git_get_type {
1867 my $hash = shift;
1868
25691fbe 1869 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
717b8311
JN
1870 my $type = <$fd>;
1871 close $fd or return;
1872 chomp $type;
1873 return $type;
1874}
1875
b201927a
JN
1876# repository configuration
1877our $config_file = '';
1878our %config;
1879
1880# store multiple values for single key as anonymous array reference
1881# single values stored directly in the hash, not as [ <value> ]
1882sub hash_set_multi {
1883 my ($hash, $key, $value) = @_;
1884
1885 if (!exists $hash->{$key}) {
1886 $hash->{$key} = $value;
1887 } elsif (!ref $hash->{$key}) {
1888 $hash->{$key} = [ $hash->{$key}, $value ];
1889 } else {
1890 push @{$hash->{$key}}, $value;
1891 }
1892}
1893
1894# return hash of git project configuration
1895# optionally limited to some section, e.g. 'gitweb'
1896sub git_parse_project_config {
1897 my $section_regexp = shift;
1898 my %config;
1899
1900 local $/ = "\0";
1901
1902 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1903 or return;
1904
1905 while (my $keyval = <$fh>) {
1906 chomp $keyval;
1907 my ($key, $value) = split(/\n/, $keyval, 2);
1908
1909 hash_set_multi(\%config, $key, $value)
1910 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1911 }
1912 close $fh;
1913
1914 return %config;
1915}
1916
df5d10a3 1917# convert config value to boolean: 'true' or 'false'
b201927a
JN
1918# no value, number > 0, 'true' and 'yes' values are true
1919# rest of values are treated as false (never as error)
1920sub config_to_bool {
1921 my $val = shift;
1922
df5d10a3
MC
1923 return 1 if !defined $val; # section.key
1924
b201927a
JN
1925 # strip leading and trailing whitespace
1926 $val =~ s/^\s+//;
1927 $val =~ s/\s+$//;
1928
df5d10a3 1929 return (($val =~ /^\d+$/ && $val) || # section.key = 1
b201927a
JN
1930 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1931}
1932
1933# convert config value to simple decimal number
1934# an optional value suffix of 'k', 'm', or 'g' will cause the value
1935# to be multiplied by 1024, 1048576, or 1073741824
1936sub config_to_int {
1937 my $val = shift;
1938
1939 # strip leading and trailing whitespace
1940 $val =~ s/^\s+//;
1941 $val =~ s/\s+$//;
1942
1943 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1944 $unit = lc($unit);
1945 # unknown unit is treated as 1
1946 return $num * ($unit eq 'g' ? 1073741824 :
1947 $unit eq 'm' ? 1048576 :
1948 $unit eq 'k' ? 1024 : 1);
1949 }
1950 return $val;
1951}
1952
1953# convert config value to array reference, if needed
1954sub config_to_multi {
1955 my $val = shift;
1956
d76a585d 1957 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
b201927a
JN
1958}
1959
717b8311 1960sub git_get_project_config {
ddb8d900 1961 my ($key, $type) = @_;
717b8311 1962
b201927a 1963 # key sanity check
717b8311
JN
1964 return unless ($key);
1965 $key =~ s/^gitweb\.//;
1966 return if ($key =~ m/\W/);
1967
b201927a
JN
1968 # type sanity check
1969 if (defined $type) {
1970 $type =~ s/^--//;
1971 $type = undef
1972 unless ($type eq 'bool' || $type eq 'int');
1973 }
1974
1975 # get config
1976 if (!defined $config_file ||
1977 $config_file ne "$git_dir/config") {
1978 %config = git_parse_project_config('gitweb');
1979 $config_file = "$git_dir/config";
1980 }
1981
df5d10a3
MC
1982 # check if config variable (key) exists
1983 return unless exists $config{"gitweb.$key"};
1984
b201927a
JN
1985 # ensure given type
1986 if (!defined $type) {
1987 return $config{"gitweb.$key"};
1988 } elsif ($type eq 'bool') {
1989 # backward compatibility: 'git config --bool' returns true/false
1990 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1991 } elsif ($type eq 'int') {
1992 return config_to_int($config{"gitweb.$key"});
1993 }
1994 return $config{"gitweb.$key"};
717b8311
JN
1995}
1996
717b8311
JN
1997# get hash of given path at given ref
1998sub git_get_hash_by_path {
1999 my $base = shift;
2000 my $path = shift || return undef;
1d782b03 2001 my $type = shift;
717b8311 2002
4b02f483 2003 $path =~ s,/+$,,;
717b8311 2004
25691fbe 2005 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
074afaa0 2006 or die_error(500, "Open git-ls-tree failed");
717b8311
JN
2007 my $line = <$fd>;
2008 close $fd or return undef;
2009
198a2a8a
JN
2010 if (!defined $line) {
2011 # there is no tree or hash given by $path at $base
2012 return undef;
2013 }
2014
717b8311 2015 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8b4b94cc 2016 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1d782b03
JN
2017 if (defined $type && $type ne $2) {
2018 # type doesn't match
2019 return undef;
2020 }
717b8311
JN
2021 return $3;
2022}
2023
ed224dea
JN
2024# get path of entry with given hash at given tree-ish (ref)
2025# used to get 'from' filename for combined diff (merge commit) for renames
2026sub git_get_path_by_hash {
2027 my $base = shift || return;
2028 my $hash = shift || return;
2029
2030 local $/ = "\0";
2031
2032 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2033 or return undef;
2034 while (my $line = <$fd>) {
2035 chomp $line;
2036
2037 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2038 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2039 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2040 close $fd;
2041 return $1;
2042 }
2043 }
2044 close $fd;
2045 return undef;
2046}
2047
717b8311
JN
2048## ......................................................................
2049## git utility functions, directly accessing git repository
2050
847e01fb 2051sub git_get_project_description {
b87d78d6 2052 my $path = shift;
09bd7898 2053
0e121a2c 2054 $git_dir = "$projectroot/$path";
c1dcf7eb 2055 open my $fd, "$git_dir/description"
0e121a2c 2056 or return git_get_project_config('description');
b87d78d6
KS
2057 my $descr = <$fd>;
2058 close $fd;
2eb54efc
JH
2059 if (defined $descr) {
2060 chomp $descr;
2061 }
b87d78d6 2062 return $descr;
12a88f2f
KS
2063}
2064
aed93de4
PB
2065sub git_get_project_ctags {
2066 my $path = shift;
2067 my $ctags = {};
2068
2069 $git_dir = "$projectroot/$path";
eee0184d
JH
2070 unless (opendir D, "$git_dir/ctags") {
2071 return $ctags;
2072 }
2073 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
aed93de4
PB
2074 open CT, $_ or next;
2075 my $val = <CT>;
2076 chomp $val;
2077 close CT;
2078 my $ctag = $_; $ctag =~ s#.*/##;
2079 $ctags->{$ctag} = $val;
2080 }
eee0184d 2081 closedir D;
aed93de4
PB
2082 $ctags;
2083}
2084
2085sub git_populate_project_tagcloud {
2086 my $ctags = shift;
2087
2088 # First, merge different-cased tags; tags vote on casing
2089 my %ctags_lc;
2090 foreach (keys %$ctags) {
2091 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2092 if (not $ctags_lc{lc $_}->{topcount}
2093 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2094 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2095 $ctags_lc{lc $_}->{topname} = $_;
2096 }
2097 }
2098
2099 my $cloud;
2100 if (eval { require HTML::TagCloud; 1; }) {
2101 $cloud = HTML::TagCloud->new;
2102 foreach (sort keys %ctags_lc) {
2103 # Pad the title with spaces so that the cloud looks
2104 # less crammed.
2105 my $title = $ctags_lc{$_}->{topname};
2106 $title =~ s/ /&nbsp;/g;
2107 $title =~ s/^/&nbsp;/g;
2108 $title =~ s/$/&nbsp;/g;
2109 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2110 }
2111 } else {
2112 $cloud = \%ctags_lc;
2113 }
2114 $cloud;
2115}
2116
2117sub git_show_project_tagcloud {
2118 my ($cloud, $count) = @_;
2119 print STDERR ref($cloud)."..\n";
2120 if (ref $cloud eq 'HTML::TagCloud') {
2121 return $cloud->html_and_css($count);
2122 } else {
2123 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2124 return '<p align="center">' . join (', ', map {
2125 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2126 } splice(@tags, 0, $count)) . '</p>';
2127 }
2128}
2129
e79ca7cc
JN
2130sub git_get_project_url_list {
2131 my $path = shift;
2132
0e121a2c 2133 $git_dir = "$projectroot/$path";
201945ee 2134 open my $fd, "$git_dir/cloneurl"
0e121a2c
JN
2135 or return wantarray ?
2136 @{ config_to_multi(git_get_project_config('url')) } :
2137 config_to_multi(git_get_project_config('url'));
e79ca7cc
JN
2138 my @git_project_url_list = map { chomp; $_ } <$fd>;
2139 close $fd;
2140
2141 return wantarray ? @git_project_url_list : \@git_project_url_list;
2142}
2143
847e01fb 2144sub git_get_projects_list {
e30496df 2145 my ($filter) = @_;
717b8311
JN
2146 my @list;
2147
e30496df
PB
2148 $filter ||= '';
2149 $filter =~ s/\.git$//;
2150
25b2790f 2151 my $check_forks = gitweb_check_feature('forks');
c2b8b134 2152
717b8311
JN
2153 if (-d $projects_list) {
2154 # search in directory
e30496df 2155 my $dir = $projects_list . ($filter ? "/$filter" : '');
6768d6b8
AK
2156 # remove the trailing "/"
2157 $dir =~ s!/+$!!;
c0011ff8 2158 my $pfxlen = length("$dir");
ca5e9495 2159 my $pfxdepth = ($dir =~ tr!/!!);
c0011ff8
JN
2160
2161 File::Find::find({
2162 follow_fast => 1, # follow symbolic links
d20602ee 2163 follow_skip => 2, # ignore duplicates
c0011ff8
JN
2164 dangling_symlinks => 0, # ignore dangling symlinks, silently
2165 wanted => sub {
2166 # skip project-list toplevel, if we get it.
2167 return if (m!^[/.]$!);
2168 # only directories can be git repositories
2169 return unless (-d $_);
ca5e9495
LL
2170 # don't traverse too deep (Find is super slow on os x)
2171 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2172 $File::Find::prune = 1;
2173 return;
2174 }
c0011ff8
JN
2175
2176 my $subdir = substr($File::Find::name, $pfxlen + 1);
2177 # we check related file in $projectroot
fb3bb3d1
DD
2178 my $path = ($filter ? "$filter/" : '') . $subdir;
2179 if (check_export_ok("$projectroot/$path")) {
2180 push @list, { path => $path };
c0011ff8
JN
2181 $File::Find::prune = 1;
2182 }
2183 },
2184 }, "$dir");
2185
717b8311
JN
2186 } elsif (-f $projects_list) {
2187 # read from file(url-encoded):
2188 # 'git%2Fgit.git Linus+Torvalds'
2189 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2190 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
c2b8b134 2191 my %paths;
dd1ad5f1 2192 open my ($fd), $projects_list or return;
c2b8b134 2193 PROJECT:
717b8311
JN
2194 while (my $line = <$fd>) {
2195 chomp $line;
2196 my ($path, $owner) = split ' ', $line;
2197 $path = unescape($path);
2198 $owner = unescape($owner);
2199 if (!defined $path) {
2200 next;
2201 }
83ee94c1
JH
2202 if ($filter ne '') {
2203 # looking for forks;
2204 my $pfx = substr($path, 0, length($filter));
2205 if ($pfx ne $filter) {
c2b8b134 2206 next PROJECT;
83ee94c1
JH
2207 }
2208 my $sfx = substr($path, length($filter));
2209 if ($sfx !~ /^\/.*\.git$/) {
c2b8b134
FL
2210 next PROJECT;
2211 }
2212 } elsif ($check_forks) {
2213 PATH:
2214 foreach my $filter (keys %paths) {
2215 # looking for forks;
2216 my $pfx = substr($path, 0, length($filter));
2217 if ($pfx ne $filter) {
2218 next PATH;
2219 }
2220 my $sfx = substr($path, length($filter));
2221 if ($sfx !~ /^\/.*\.git$/) {
2222 next PATH;
2223 }
2224 # is a fork, don't include it in
2225 # the list
2226 next PROJECT;
83ee94c1
JH
2227 }
2228 }
2172ce4b 2229 if (check_export_ok("$projectroot/$path")) {
717b8311
JN
2230 my $pr = {
2231 path => $path,
00f429af 2232 owner => to_utf8($owner),
717b8311 2233 };
c2b8b134
FL
2234 push @list, $pr;
2235 (my $forks_path = $path) =~ s/\.git$//;
2236 $paths{$forks_path}++;
717b8311
JN
2237 }
2238 }
2239 close $fd;
2240 }
717b8311
JN
2241 return @list;
2242}
2243
47852450
JH
2244our $gitweb_project_owner = undef;
2245sub git_get_project_list_from_file {
1e0cf030 2246
47852450 2247 return if (defined $gitweb_project_owner);
1e0cf030 2248
47852450 2249 $gitweb_project_owner = {};
1e0cf030
JN
2250 # read from file (url-encoded):
2251 # 'git%2Fgit.git Linus+Torvalds'
2252 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2253 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2254 if (-f $projects_list) {
2255 open (my $fd , $projects_list);
2256 while (my $line = <$fd>) {
2257 chomp $line;
2258 my ($pr, $ow) = split ' ', $line;
2259 $pr = unescape($pr);
2260 $ow = unescape($ow);
47852450 2261 $gitweb_project_owner->{$pr} = to_utf8($ow);
1e0cf030
JN
2262 }
2263 close $fd;
2264 }
47852450
JH
2265}
2266
2267sub git_get_project_owner {
2268 my $project = shift;
2269 my $owner;
2270
2271 return undef unless $project;
b59012ef 2272 $git_dir = "$projectroot/$project";
47852450
JH
2273
2274 if (!defined $gitweb_project_owner) {
2275 git_get_project_list_from_file();
2276 }
2277
2278 if (exists $gitweb_project_owner->{$project}) {
2279 $owner = $gitweb_project_owner->{$project};
2280 }
b59012ef
BR
2281 if (!defined $owner){
2282 $owner = git_get_project_config('owner');
2283 }
1e0cf030 2284 if (!defined $owner) {
b59012ef 2285 $owner = get_file_owner("$git_dir");
1e0cf030
JN
2286 }
2287
2288 return $owner;
2289}
2290
c60c56cc
JN
2291sub git_get_last_activity {
2292 my ($path) = @_;
2293 my $fd;
2294
2295 $git_dir = "$projectroot/$path";
2296 open($fd, "-|", git_cmd(), 'for-each-ref',
0ff5ec70 2297 '--format=%(committer)',
c60c56cc 2298 '--sort=-committerdate',
0ff5ec70 2299 '--count=1',
c60c56cc
JN
2300 'refs/heads') or return;
2301 my $most_recent = <$fd>;
2302 close $fd or return;
785cdea9
JN
2303 if (defined $most_recent &&
2304 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
c60c56cc
JN
2305 my $timestamp = $1;
2306 my $age = time - $timestamp;
2307 return ($age, age_string($age));
2308 }
c956395e 2309 return (undef, undef);
c60c56cc
JN
2310}
2311
847e01fb 2312sub git_get_references {
717b8311
JN
2313 my $type = shift || "";
2314 my %refs;
28b9d9f7
JN
2315 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2316 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2317 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2318 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
9704d75d 2319 or return;
d294e1ca 2320
717b8311
JN
2321 while (my $line = <$fd>) {
2322 chomp $line;
4afbaeff 2323 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
717b8311 2324 if (defined $refs{$1}) {
d294e1ca 2325 push @{$refs{$1}}, $2;
717b8311 2326 } else {
d294e1ca 2327 $refs{$1} = [ $2 ];
717b8311
JN
2328 }
2329 }
2330 }
2331 close $fd or return;
2332 return \%refs;
2333}
2334
56a322f1
JN
2335sub git_get_rev_name_tags {
2336 my $hash = shift || return undef;
2337
25691fbe 2338 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
56a322f1
JN
2339 or return;
2340 my $name_rev = <$fd>;
2341 close $fd;
2342
2343 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2344 return $1;
2345 } else {
2346 # catches also '$hash undefined' output
2347 return undef;
2348 }
2349}
2350
717b8311
JN
2351## ----------------------------------------------------------------------
2352## parse to hash functions
2353
847e01fb 2354sub parse_date {
717b8311
JN
2355 my $epoch = shift;
2356 my $tz = shift || "-0000";
2357
2358 my %date;
2359 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2360 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2361 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2362 $date{'hour'} = $hour;
2363 $date{'minute'} = $min;
2364 $date{'mday'} = $mday;
2365 $date{'day'} = $days[$wday];
2366 $date{'month'} = $months[$mon];
af6feeb2
JN
2367 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2368 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
952c65fc
JN
2369 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2370 $mday, $months[$mon], $hour ,$min;
af6feeb2 2371 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
a62d6d84 2372 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
717b8311
JN
2373
2374 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2375 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2376 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2377 $date{'hour_local'} = $hour;
2378 $date{'minute_local'} = $min;
2379 $date{'tz_local'} = $tz;
af6feeb2
JN
2380 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2381 1900+$year, $mon+1, $mday,
2382 $hour, $min, $sec, $tz);
717b8311
JN
2383 return %date;
2384}
2385
847e01fb 2386sub parse_tag {
ede5e100
KS
2387 my $tag_id = shift;
2388 my %tag;
d8a20ba9 2389 my @comment;
ede5e100 2390
25691fbe 2391 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
d8a20ba9 2392 $tag{'id'} = $tag_id;
ede5e100
KS
2393 while (my $line = <$fd>) {
2394 chomp $line;
2395 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2396 $tag{'object'} = $1;
7ab0d2b6 2397 } elsif ($line =~ m/^type (.+)$/) {
ede5e100 2398 $tag{'type'} = $1;
7ab0d2b6 2399 } elsif ($line =~ m/^tag (.+)$/) {
ede5e100 2400 $tag{'name'} = $1;
d8a20ba9
KS
2401 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2402 $tag{'author'} = $1;
2403 $tag{'epoch'} = $2;
2404 $tag{'tz'} = $3;
2405 } elsif ($line =~ m/--BEGIN/) {
2406 push @comment, $line;
2407 last;
2408 } elsif ($line eq "") {
2409 last;
ede5e100
KS
2410 }
2411 }
d8a20ba9
KS
2412 push @comment, <$fd>;
2413 $tag{'comment'} = \@comment;
19806691 2414 close $fd or return;
ede5e100
KS
2415 if (!defined $tag{'name'}) {
2416 return
2417 };
2418 return %tag
2419}
2420
756bbf54 2421sub parse_commit_text {
ccdfdea0 2422 my ($commit_text, $withparents) = @_;
756bbf54 2423 my @commit_lines = split '\n', $commit_text;
703ac710 2424 my %co;
703ac710 2425
756bbf54
RF
2426 pop @commit_lines; # Remove '\0'
2427
198a2a8a
JN
2428 if (! @commit_lines) {
2429 return;
2430 }
2431
25f422fb 2432 my $header = shift @commit_lines;
198a2a8a 2433 if ($header !~ m/^[0-9a-fA-F]{40}/) {
25f422fb
KS
2434 return;
2435 }
ccdfdea0 2436 ($co{'id'}, my @parents) = split ' ', $header;
19806691 2437 while (my $line = shift @commit_lines) {
b87d78d6 2438 last if $line eq "\n";
7ab0d2b6 2439 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
703ac710 2440 $co{'tree'} = $1;
ccdfdea0 2441 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
208b2dff 2442 push @parents, $1;
022be3d0 2443 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3f714537 2444 $co{'author'} = $1;
185f09e5
KS
2445 $co{'author_epoch'} = $2;
2446 $co{'author_tz'} = $3;
ba00b8c1
JN
2447 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2448 $co{'author_name'} = $1;
2449 $co{'author_email'} = $2;
2bf7a52c
KS
2450 } else {
2451 $co{'author_name'} = $co{'author'};
2452 }
86eed32d
KS
2453 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2454 $co{'committer'} = $1;
185f09e5
KS
2455 $co{'committer_epoch'} = $2;
2456 $co{'committer_tz'} = $3;
991910a9 2457 $co{'committer_name'} = $co{'committer'};
ba00b8c1
JN
2458 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2459 $co{'committer_name'} = $1;
2460 $co{'committer_email'} = $2;
2461 } else {
2462 $co{'committer_name'} = $co{'committer'};
2463 }
703ac710
KS
2464 }
2465 }
ede5e100 2466 if (!defined $co{'tree'}) {
25f422fb 2467 return;
ede5e100 2468 };
208b2dff
RF
2469 $co{'parents'} = \@parents;
2470 $co{'parent'} = $parents[0];
25f422fb 2471
19806691 2472 foreach my $title (@commit_lines) {
c2488d06 2473 $title =~ s/^ //;
19806691 2474 if ($title ne "") {
48c771f4 2475 $co{'title'} = chop_str($title, 80, 5);
19806691
KS
2476 # remove leading stuff of merges to make the interesting part visible
2477 if (length($title) > 50) {
2478 $title =~ s/^Automatic //;
2479 $title =~ s/^merge (of|with) /Merge ... /i;
2480 if (length($title) > 50) {
2481 $title =~ s/(http|rsync):\/\///;
2482 }
2483 if (length($title) > 50) {
2484 $title =~ s/(master|www|rsync)\.//;
2485 }
2486 if (length($title) > 50) {
2487 $title =~ s/kernel.org:?//;
2488 }
2489 if (length($title) > 50) {
2490 $title =~ s/\/pub\/scm//;
2491 }
2492 }
48c771f4 2493 $co{'title_short'} = chop_str($title, 50, 5);
19806691
KS
2494 last;
2495 }
2496 }
53c39676 2497 if (! defined $co{'title'} || $co{'title'} eq "") {
7e0fe5c9
PB
2498 $co{'title'} = $co{'title_short'} = '(no commit message)';
2499 }
25f422fb
KS
2500 # remove added spaces
2501 foreach my $line (@commit_lines) {
2502 $line =~ s/^ //;
2503 }
2504 $co{'comment'} = \@commit_lines;
2ae100df
KS
2505
2506 my $age = time - $co{'committer_epoch'};
2507 $co{'age'} = $age;
d263a6bd 2508 $co{'age_string'} = age_string($age);
71be1e79
KS
2509 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2510 if ($age > 60*60*24*7*2) {
1b1cd421 2511 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79
KS
2512 $co{'age_string_age'} = $co{'age_string'};
2513 } else {
2514 $co{'age_string_date'} = $co{'age_string'};
1b1cd421 2515 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79 2516 }
703ac710
KS
2517 return %co;
2518}
2519
756bbf54
RF
2520sub parse_commit {
2521 my ($commit_id) = @_;
2522 my %co;
2523
2524 local $/ = "\0";
2525
2526 open my $fd, "-|", git_cmd(), "rev-list",
ccdfdea0 2527 "--parents",
756bbf54 2528 "--header",
756bbf54
RF
2529 "--max-count=1",
2530 $commit_id,
2531 "--",
074afaa0 2532 or die_error(500, "Open git-rev-list failed");
ccdfdea0 2533 %co = parse_commit_text(<$fd>, 1);
756bbf54
RF
2534 close $fd;
2535
2536 return %co;
2537}
2538
2539sub parse_commits {
311e552e 2540 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
756bbf54
RF
2541 my @cos;
2542
2543 $maxcount ||= 1;
2544 $skip ||= 0;
2545
756bbf54
RF
2546 local $/ = "\0";
2547
2548 open my $fd, "-|", git_cmd(), "rev-list",
2549 "--header",
311e552e 2550 @args,
756bbf54 2551 ("--max-count=" . $maxcount),
f47efbb7 2552 ("--skip=" . $skip),
868bc068 2553 @extra_options,
756bbf54
RF
2554 $commit_id,
2555 "--",
2556 ($filename ? ($filename) : ())
074afaa0 2557 or die_error(500, "Open git-rev-list failed");
756bbf54
RF
2558 while (my $line = <$fd>) {
2559 my %co = parse_commit_text($line);
2560 push @cos, \%co;
2561 }
2562 close $fd;
2563
2564 return wantarray ? @cos : \@cos;
2565}
2566
e8e41a93 2567# parse line of git-diff-tree "raw" output
740e67f9
JN
2568sub parse_difftree_raw_line {
2569 my $line = shift;
2570 my %res;
2571
2572 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2573 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2574 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2575 $res{'from_mode'} = $1;
2576 $res{'to_mode'} = $2;
2577 $res{'from_id'} = $3;
2578 $res{'to_id'} = $4;
4ed4a347 2579 $res{'status'} = $5;
740e67f9
JN
2580 $res{'similarity'} = $6;
2581 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
e8e41a93 2582 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
740e67f9 2583 } else {
9d301456 2584 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
740e67f9
JN
2585 }
2586 }
78bc403a
JN
2587 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2588 # combined diff (for merge commit)
2589 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2590 $res{'nparents'} = length($1);
2591 $res{'from_mode'} = [ split(' ', $2) ];
2592 $res{'to_mode'} = pop @{$res{'from_mode'}};
2593 $res{'from_id'} = [ split(' ', $3) ];
2594 $res{'to_id'} = pop @{$res{'from_id'}};
2595 $res{'status'} = [ split('', $4) ];
2596 $res{'to_file'} = unquote($5);
2597 }
740e67f9 2598 # 'c512b523472485aef4fff9e57b229d9d243c967f'
0edcb37d
JN
2599 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2600 $res{'commit'} = $1;
2601 }
740e67f9
JN
2602
2603 return wantarray ? %res : \%res;
2604}
2605
0cec6db5
JN
2606# wrapper: return parsed line of git-diff-tree "raw" output
2607# (the argument might be raw line, or parsed info)
2608sub parsed_difftree_line {
2609 my $line_or_ref = shift;
2610
2611 if (ref($line_or_ref) eq "HASH") {
2612 # pre-parsed (or generated by hand)
2613 return $line_or_ref;
2614 } else {
2615 return parse_difftree_raw_line($line_or_ref);
2616 }
2617}
2618
cb849b46
JN
2619# parse line of git-ls-tree output
2620sub parse_ls_tree_line ($;%) {
2621 my $line = shift;
2622 my %opts = @_;
2623 my %res;
2624
2625 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8b4b94cc 2626 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
cb849b46
JN
2627
2628 $res{'mode'} = $1;
2629 $res{'type'} = $2;
2630 $res{'hash'} = $3;
2631 if ($opts{'-z'}) {
2632 $res{'name'} = $4;
2633 } else {
2634 $res{'name'} = unquote($4);
2635 }
2636
2637 return wantarray ? %res : \%res;
2638}
2639
90921740
JN
2640# generates _two_ hashes, references to which are passed as 2 and 3 argument
2641sub parse_from_to_diffinfo {
2642 my ($diffinfo, $from, $to, @parents) = @_;
2643
2644 if ($diffinfo->{'nparents'}) {
2645 # combined diff
2646 $from->{'file'} = [];
2647 $from->{'href'} = [];
2648 fill_from_file_info($diffinfo, @parents)
2649 unless exists $diffinfo->{'from_file'};
2650 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
9d301456
JN
2651 $from->{'file'}[$i] =
2652 defined $diffinfo->{'from_file'}[$i] ?
2653 $diffinfo->{'from_file'}[$i] :
2654 $diffinfo->{'to_file'};
90921740
JN
2655 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2656 $from->{'href'}[$i] = href(action=>"blob",
2657 hash_base=>$parents[$i],
2658 hash=>$diffinfo->{'from_id'}[$i],
2659 file_name=>$from->{'file'}[$i]);
2660 } else {
2661 $from->{'href'}[$i] = undef;
2662 }
2663 }
2664 } else {
0cec6db5 2665 # ordinary (not combined) diff
9d301456 2666 $from->{'file'} = $diffinfo->{'from_file'};
90921740
JN
2667 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2668 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2669 hash=>$diffinfo->{'from_id'},
2670 file_name=>$from->{'file'});
2671 } else {
2672 delete $from->{'href'};
2673 }
2674 }
2675
9d301456 2676 $to->{'file'} = $diffinfo->{'to_file'};
90921740
JN
2677 if (!is_deleted($diffinfo)) { # file exists in result
2678 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2679 hash=>$diffinfo->{'to_id'},
2680 file_name=>$to->{'file'});
2681 } else {
2682 delete $to->{'href'};
2683 }
2684}
2685
717b8311
JN
2686## ......................................................................
2687## parse to array of hashes functions
4c02e3c5 2688
cd146408
JN
2689sub git_get_heads_list {
2690 my $limit = shift;
2691 my @headslist;
2692
2693 open my $fd, '-|', git_cmd(), 'for-each-ref',
2694 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2695 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2696 'refs/heads'
c83a77e4
JN
2697 or return;
2698 while (my $line = <$fd>) {
cd146408 2699 my %ref_item;
120ddde2 2700
cd146408
JN
2701 chomp $line;
2702 my ($refinfo, $committerinfo) = split(/\0/, $line);
2703 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2704 my ($committer, $epoch, $tz) =
2705 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 2706 $ref_item{'fullname'} = $name;
cd146408
JN
2707 $name =~ s!^refs/heads/!!;
2708
2709 $ref_item{'name'} = $name;
2710 $ref_item{'id'} = $hash;
2711 $ref_item{'title'} = $title || '(no commit message)';
2712 $ref_item{'epoch'} = $epoch;
2713 if ($epoch) {
2714 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2715 } else {
2716 $ref_item{'age'} = "unknown";
717b8311 2717 }
cd146408
JN
2718
2719 push @headslist, \%ref_item;
c83a77e4
JN
2720 }
2721 close $fd;
2722
cd146408
JN
2723 return wantarray ? @headslist : \@headslist;
2724}
2725
2726sub git_get_tags_list {
2727 my $limit = shift;
2728 my @tagslist;
2729
2730 open my $fd, '-|', git_cmd(), 'for-each-ref',
2731 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2732 '--format=%(objectname) %(objecttype) %(refname) '.
2733 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2734 'refs/tags'
2735 or return;
2736 while (my $line = <$fd>) {
2737 my %ref_item;
7a13b999 2738
cd146408
JN
2739 chomp $line;
2740 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2741 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2742 my ($creator, $epoch, $tz) =
2743 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 2744 $ref_item{'fullname'} = $name;
cd146408
JN
2745 $name =~ s!^refs/tags/!!;
2746
2747 $ref_item{'type'} = $type;
2748 $ref_item{'id'} = $id;
2749 $ref_item{'name'} = $name;
2750 if ($type eq "tag") {
2751 $ref_item{'subject'} = $title;
2752 $ref_item{'reftype'} = $reftype;
2753 $ref_item{'refid'} = $refid;
2754 } else {
2755 $ref_item{'reftype'} = $type;
2756 $ref_item{'refid'} = $id;
2757 }
2758
2759 if ($type eq "tag" || $type eq "commit") {
2760 $ref_item{'epoch'} = $epoch;
2761 if ($epoch) {
2762 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2763 } else {
2764 $ref_item{'age'} = "unknown";
2765 }
2766 }
991910a9 2767
cd146408 2768 push @tagslist, \%ref_item;
717b8311 2769 }
cd146408
JN
2770 close $fd;
2771
2772 return wantarray ? @tagslist : \@tagslist;
86eed32d
KS
2773}
2774
717b8311
JN
2775## ----------------------------------------------------------------------
2776## filesystem-related functions
022be3d0 2777
c07ad4b9
KS
2778sub get_file_owner {
2779 my $path = shift;
2780
2781 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2782 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2783 if (!defined $gcos) {
2784 return undef;
2785 }
2786 my $owner = $gcos;
2787 $owner =~ s/[,;].*$//;
00f429af 2788 return to_utf8($owner);
c07ad4b9
KS
2789}
2790
2dcb5e1a
JN
2791# assume that file exists
2792sub insert_file {
2793 my $filename = shift;
2794
2795 open my $fd, '<', $filename;
4586864a 2796 print map { to_utf8($_) } <$fd>;
2dcb5e1a
JN
2797 close $fd;
2798}
2799
717b8311
JN
2800## ......................................................................
2801## mimetype related functions
09bd7898 2802
717b8311
JN
2803sub mimetype_guess_file {
2804 my $filename = shift;
2805 my $mimemap = shift;
2806 -r $mimemap or return undef;
2807
2808 my %mimemap;
2809 open(MIME, $mimemap) or return undef;
2810 while (<MIME>) {
618918e5 2811 next if m/^#/; # skip comments
717b8311 2812 my ($mime, $exts) = split(/\t+/);
46b059d7
JH
2813 if (defined $exts) {
2814 my @exts = split(/\s+/, $exts);
2815 foreach my $ext (@exts) {
2816 $mimemap{$ext} = $mime;
2817 }
09bd7898 2818 }
09bd7898 2819 }
717b8311 2820 close(MIME);
09bd7898 2821
8059319a 2822 $filename =~ /\.([^.]*)$/;
717b8311
JN
2823 return $mimemap{$1};
2824}
5996ca08 2825
717b8311
JN
2826sub mimetype_guess {
2827 my $filename = shift;
2828 my $mime;
2829 $filename =~ /\./ or return undef;
5996ca08 2830
717b8311
JN
2831 if ($mimetypes_file) {
2832 my $file = $mimetypes_file;
d5aa50de
JN
2833 if ($file !~ m!^/!) { # if it is relative path
2834 # it is relative to project
2835 $file = "$projectroot/$project/$file";
2836 }
717b8311
JN
2837 $mime = mimetype_guess_file($filename, $file);
2838 }
2839 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2840 return $mime;
5996ca08
FF
2841}
2842
847e01fb 2843sub blob_mimetype {
717b8311
JN
2844 my $fd = shift;
2845 my $filename = shift;
5996ca08 2846
717b8311
JN
2847 if ($filename) {
2848 my $mime = mimetype_guess($filename);
2849 $mime and return $mime;
d8d17b5d 2850 }
717b8311
JN
2851
2852 # just in case
2853 return $default_blob_plain_mimetype unless $fd;
2854
2855 if (-T $fd) {
7f718e8b 2856 return 'text/plain';
717b8311
JN
2857 } elsif (! $filename) {
2858 return 'application/octet-stream';
2859 } elsif ($filename =~ m/\.png$/i) {
2860 return 'image/png';
2861 } elsif ($filename =~ m/\.gif$/i) {
2862 return 'image/gif';
2863 } elsif ($filename =~ m/\.jpe?g$/i) {
2864 return 'image/jpeg';
d8d17b5d 2865 } else {
717b8311 2866 return 'application/octet-stream';
f7ab660c 2867 }
717b8311
JN
2868}
2869
7f718e8b
JN
2870sub blob_contenttype {
2871 my ($fd, $file_name, $type) = @_;
2872
2873 $type ||= blob_mimetype($fd, $file_name);
2874 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2875 $type .= "; charset=$default_text_plain_charset";
2876 }
2877
2878 return $type;
2879}
2880
717b8311
JN
2881## ======================================================================
2882## functions printing HTML: header, footer, error page
2883
2884sub git_header_html {
2885 my $status = shift || "200 OK";
2886 my $expires = shift;
2887
8be2890c 2888 my $title = "$site_name";
717b8311 2889 if (defined $project) {
00f429af 2890 $title .= " - " . to_utf8($project);
717b8311
JN
2891 if (defined $action) {
2892 $title .= "/$action";
2893 if (defined $file_name) {
403d0906 2894 $title .= " - " . esc_path($file_name);
717b8311
JN
2895 if ($action eq "tree" && $file_name !~ m|/$|) {
2896 $title .= "/";
2897 }
2898 }
2899 }
f7ab660c 2900 }
717b8311
JN
2901 my $content_type;
2902 # require explicit support from the UA if we are to send the page as
2903 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2904 # we have to do this because MSIE sometimes globs '*/*', pretending to
2905 # support xhtml+xml but choking when it gets what it asked for.
952c65fc
JN
2906 if (defined $cgi->http('HTTP_ACCEPT') &&
2907 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2908 $cgi->Accept('application/xhtml+xml') != 0) {
717b8311 2909 $content_type = 'application/xhtml+xml';
f7ab660c 2910 } else {
717b8311 2911 $content_type = 'text/html';
f7ab660c 2912 }
952c65fc
JN
2913 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2914 -status=> $status, -expires => $expires);
45c9a758 2915 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
717b8311
JN
2916 print <<EOF;
2917<?xml version="1.0" encoding="utf-8"?>
2918<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2919<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
d4baf9ea 2920<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
717b8311
JN
2921<!-- git core binaries version $git_version -->
2922<head>
2923<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
45c9a758 2924<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
717b8311
JN
2925<meta name="robots" content="index, nofollow"/>
2926<title>$title</title>
717b8311 2927EOF
41a4d16e
GB
2928 # the stylesheet, favicon etc urls won't work correctly with path_info
2929 # unless we set the appropriate base URL
c3254aee 2930 if ($ENV{'PATH_INFO'}) {
81d3fe9f 2931 print "<base href=\"".esc_url($base_url)."\" />\n";
c3254aee 2932 }
41a4d16e
GB
2933 # print out each stylesheet that exist, providing backwards capability
2934 # for those people who defined $stylesheet in a config file
b2d3476e 2935 if (defined $stylesheet) {
b2d3476e
AC
2936 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2937 } else {
2938 foreach my $stylesheet (@stylesheets) {
2939 next unless $stylesheet;
2940 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2941 }
2942 }
dd04c428 2943 if (defined $project) {
3562198b
JN
2944 my %href_params = get_feed_info();
2945 if (!exists $href_params{'-title'}) {
2946 $href_params{'-title'} = 'log';
2947 }
2948
2949 foreach my $format qw(RSS Atom) {
2950 my $type = lc($format);
2951 my %link_attr = (
2952 '-rel' => 'alternate',
2953 '-title' => "$project - $href_params{'-title'} - $format feed",
2954 '-type' => "application/$type+xml"
2955 );
2956
2957 $href_params{'action'} = $type;
2958 $link_attr{'-href'} = href(%href_params);
2959 print "<link ".
2960 "rel=\"$link_attr{'-rel'}\" ".
2961 "title=\"$link_attr{'-title'}\" ".
2962 "href=\"$link_attr{'-href'}\" ".
2963 "type=\"$link_attr{'-type'}\" ".
2964 "/>\n";
2965
2966 $href_params{'extra_options'} = '--no-merges';
2967 $link_attr{'-href'} = href(%href_params);
2968 $link_attr{'-title'} .= ' (no merges)';
2969 print "<link ".
2970 "rel=\"$link_attr{'-rel'}\" ".
2971 "title=\"$link_attr{'-title'}\" ".
2972 "href=\"$link_attr{'-href'}\" ".
2973 "type=\"$link_attr{'-type'}\" ".
2974 "/>\n";
2975 }
2976
9d0734ae
JN
2977 } else {
2978 printf('<link rel="alternate" title="%s projects list" '.
3562198b 2979 'href="%s" type="text/plain; charset=utf-8" />'."\n",
9d0734ae 2980 $site_name, href(project=>undef, action=>"project_index"));
af6feeb2 2981 printf('<link rel="alternate" title="%s projects feeds" '.
3562198b 2982 'href="%s" type="text/x-opml" />'."\n",
9d0734ae 2983 $site_name, href(project=>undef, action=>"opml"));
dd04c428 2984 }
0b5deba1 2985 if (defined $favicon) {
3562198b 2986 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
0b5deba1 2987 }
10161355 2988
dd04c428 2989 print "</head>\n" .
b2d3476e
AC
2990 "<body>\n";
2991
2992 if (-f $site_header) {
2dcb5e1a 2993 insert_file($site_header);
b2d3476e
AC
2994 }
2995
2996 print "<div class=\"page_header\">\n" .
9a7a62ff
JN
2997 $cgi->a({-href => esc_url($logo_url),
2998 -title => $logo_label},
2999 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
f93bff8d 3000 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
717b8311 3001 if (defined $project) {
1c2a4f5a 3002 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
717b8311
JN
3003 if (defined $action) {
3004 print " / $action";
3005 }
3006 print "\n";
6be93511 3007 }
d77b5673
PB
3008 print "</div>\n";
3009
25b2790f 3010 my $have_search = gitweb_check_feature('search');
f70dda25 3011 if (defined $project && $have_search) {
717b8311
JN
3012 if (!defined $searchtext) {
3013 $searchtext = "";
3014 }
3015 my $search_hash;
3016 if (defined $hash_base) {
3017 $search_hash = $hash_base;
3018 } elsif (defined $hash) {
3019 $search_hash = $hash;
bddec01d 3020 } else {
717b8311 3021 $search_hash = "HEAD";
bddec01d 3022 }
40375a83 3023 my $action = $my_uri;
25b2790f 3024 my $use_pathinfo = gitweb_check_feature('pathinfo');
40375a83 3025 if ($use_pathinfo) {
85d17a12 3026 $action .= "/".esc_url($project);
40375a83 3027 }
40375a83 3028 print $cgi->startform(-method => "get", -action => $action) .
717b8311 3029 "<div class=\"search\">\n" .
f70dda25
JN
3030 (!$use_pathinfo &&
3031 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3032 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3033 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
88ad729b 3034 $cgi->popup_menu(-name => 'st', -default => 'commit',
e7738553 3035 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
88ad729b
PB
3036 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3037 " search:\n",
717b8311 3038 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
0e559919
PB
3039 "<span title=\"Extended regular expression\">" .
3040 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3041 -checked => $search_use_regexp) .
3042 "</span>" .
717b8311
JN
3043 "</div>" .
3044 $cgi->end_form() . "\n";
b87d78d6 3045 }
717b8311
JN
3046}
3047
3048sub git_footer_html {
3562198b
JN
3049 my $feed_class = 'rss_logo';
3050
717b8311
JN
3051 print "<div class=\"page_footer\">\n";
3052 if (defined $project) {
847e01fb 3053 my $descr = git_get_project_description($project);
717b8311
JN
3054 if (defined $descr) {
3055 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3056 }
3562198b
JN
3057
3058 my %href_params = get_feed_info();
3059 if (!%href_params) {
3060 $feed_class .= ' generic';
3061 }
3062 $href_params{'-title'} ||= 'log';
3063
3064 foreach my $format qw(RSS Atom) {
3065 $href_params{'action'} = lc($format);
3066 print $cgi->a({-href => href(%href_params),
3067 -title => "$href_params{'-title'} $format feed",
3068 -class => $feed_class}, $format)."\n";
3069 }
3070
717b8311 3071 } else {
a1565c44 3072 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3562198b 3073 -class => $feed_class}, "OPML") . " ";
9d0734ae 3074 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3562198b 3075 -class => $feed_class}, "TXT") . "\n";
717b8311 3076 }
3562198b 3077 print "</div>\n"; # class="page_footer"
b2d3476e
AC
3078
3079 if (-f $site_footer) {
2dcb5e1a 3080 insert_file($site_footer);
b2d3476e
AC
3081 }
3082
3083 print "</body>\n" .
717b8311
JN
3084 "</html>";
3085}
3086
074afaa0
LW
3087# die_error(<http_status_code>, <error_message>)
3088# Example: die_error(404, 'Hash not found')
3089# By convention, use the following status codes (as defined in RFC 2616):
3090# 400: Invalid or missing CGI parameters, or
3091# requested object exists but has wrong type.
3092# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3093# this server or project.
3094# 404: Requested object/revision/project doesn't exist.
3095# 500: The server isn't configured properly, or
3096# an internal error occurred (e.g. failed assertions caused by bugs), or
3097# an unknown error occurred (e.g. the git binary died unexpectedly).
717b8311 3098sub die_error {
074afaa0
LW
3099 my $status = shift || 500;
3100 my $error = shift || "Internal server error";
3101
3102 my %http_responses = (400 => '400 Bad Request',
3103 403 => '403 Forbidden',
3104 404 => '404 Not Found',
3105 500 => '500 Internal Server Error');
3106 git_header_html($http_responses{$status});
59b9f61a
JN
3107 print <<EOF;
3108<div class="page_body">
3109<br /><br />
3110$status - $error
3111<br />
3112</div>
3113EOF
b87d78d6 3114 git_footer_html();
717b8311 3115 exit;
161332a5
KS
3116}
3117
717b8311
JN
3118## ----------------------------------------------------------------------
3119## functions printing or outputting HTML: navigation
3120
847e01fb 3121sub git_print_page_nav {
717b8311
JN
3122 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3123 $extra = '' if !defined $extra; # pager or formats
3124
3125 my @navs = qw(summary shortlog log commit commitdiff tree);
3126 if ($suppress) {
3127 @navs = grep { $_ ne $suppress } @navs;
3128 }
3129
1c2a4f5a 3130 my %arg = map { $_ => {action=>$_} } @navs;
717b8311
JN
3131 if (defined $head) {
3132 for (qw(commit commitdiff)) {
3be8e720 3133 $arg{$_}{'hash'} = $head;
717b8311
JN
3134 }
3135 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3136 for (qw(shortlog log)) {
3be8e720 3137 $arg{$_}{'hash'} = $head;
045e531a 3138 }
6a928415
KS
3139 }
3140 }
d627f68f 3141
3be8e720
JN
3142 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3143 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
717b8311 3144
a7c5a283 3145 my @actions = gitweb_get_feature('actions');
2b11e059
JN
3146 my %repl = (
3147 '%' => '%',
3148 'n' => $project, # project name
3149 'f' => $git_dir, # project path within filesystem
3150 'h' => $treehead || '', # current hash ('h' parameter)
3151 'b' => $treebase || '', # hash base ('hb' parameter)
3152 );
d627f68f 3153 while (@actions) {
2b11e059
JN
3154 my ($label, $link, $pos) = splice(@actions,0,3);
3155 # insert
d627f68f
PB
3156 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3157 # munch munch
2b11e059 3158 $link =~ s/%([%nfhb])/$repl{$1}/g;
d627f68f
PB
3159 $arg{$label}{'_href'} = $link;
3160 }
3161
717b8311
JN
3162 print "<div class=\"page_nav\">\n" .
3163 (join " | ",
1c2a4f5a 3164 map { $_ eq $current ?
d627f68f 3165 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
1c2a4f5a 3166 } @navs);
717b8311
JN
3167 print "<br/>\n$extra<br/>\n" .
3168 "</div>\n";
6a928415
KS
3169}
3170
847e01fb 3171sub format_paging_nav {
1f684dc0 3172 my ($action, $hash, $head, $page, $has_next_link) = @_;
717b8311 3173 my $paging_nav;
594e212b 3174
717b8311
JN
3175
3176 if ($hash ne $head || $page) {
1c2a4f5a 3177 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
594e212b 3178 } else {
717b8311
JN
3179 $paging_nav .= "HEAD";
3180 }
3181
3182 if ($page > 0) {
3183 $paging_nav .= " &sdot; " .
7afd77bf 3184 $cgi->a({-href => href(-replay=>1, page=>$page-1),
26298b5f 3185 -accesskey => "p", -title => "Alt-p"}, "prev");
717b8311
JN
3186 } else {
3187 $paging_nav .= " &sdot; prev";
3188 }
3189
1f684dc0 3190 if ($has_next_link) {
717b8311 3191 $paging_nav .= " &sdot; " .
7afd77bf 3192 $cgi->a({-href => href(-replay=>1, page=>$page+1),
26298b5f 3193 -accesskey => "n", -title => "Alt-n"}, "next");
717b8311
JN
3194 } else {
3195 $paging_nav .= " &sdot; next";
594e212b 3196 }
717b8311
JN
3197
3198 return $paging_nav;
594e212b
JN
3199}
3200
717b8311
JN
3201## ......................................................................
3202## functions printing or outputting HTML: div
3203
847e01fb 3204sub git_print_header_div {
717b8311 3205 my ($action, $title, $hash, $hash_base) = @_;
1c2a4f5a 3206 my %args = ();
717b8311 3207
3be8e720
JN
3208 $args{'action'} = $action;
3209 $args{'hash'} = $hash if $hash;
3210 $args{'hash_base'} = $hash_base if $hash_base;
717b8311
JN
3211
3212 print "<div class=\"header\">\n" .
1c2a4f5a
MW
3213 $cgi->a({-href => href(%args), -class => "title"},
3214 $title ? $title : $action) .
3215 "\n</div>\n";
717b8311 3216}
ede5e100 3217
6fd92a28
JN
3218#sub git_print_authorship (\%) {
3219sub git_print_authorship {
3220 my $co = shift;
3221
a44465cc 3222 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
6fd92a28
JN
3223 print "<div class=\"author_date\">" .
3224 esc_html($co->{'author_name'}) .
a44465cc
JN
3225 " [$ad{'rfc2822'}";
3226 if ($ad{'hour_local'} < 6) {
3227 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3228 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3229 } else {
3230 printf(" (%02d:%02d %s)",
3231 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3232 }
3233 print "]</div>\n";
6fd92a28
JN
3234}
3235
717b8311
JN
3236sub git_print_page_path {
3237 my $name = shift;
3238 my $type = shift;
59fb1c94 3239 my $hb = shift;
ede5e100 3240
4df118ed
JN
3241
3242 print "<div class=\"page_path\">";
3243 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
00f429af 3244 -title => 'tree root'}, to_utf8("[$project]"));
4df118ed
JN
3245 print " / ";
3246 if (defined $name) {
762c7205
JN
3247 my @dirname = split '/', $name;
3248 my $basename = pop @dirname;
3249 my $fullname = '';
3250
762c7205 3251 foreach my $dir (@dirname) {
16fdb488 3252 $fullname .= ($fullname ? '/' : '') . $dir;
762c7205
JN
3253 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3254 hash_base=>$hb),
edc04e90 3255 -title => $fullname}, esc_path($dir));
26d0a976 3256 print " / ";
762c7205
JN
3257 }
3258 if (defined $type && $type eq 'blob') {
952c65fc 3259 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
762c7205 3260 hash_base=>$hb),
edc04e90 3261 -title => $name}, esc_path($basename));
762c7205
JN
3262 } elsif (defined $type && $type eq 'tree') {
3263 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3264 hash_base=>$hb),
edc04e90 3265 -title => $name}, esc_path($basename));
4df118ed 3266 print " / ";
59fb1c94 3267 } else {
403d0906 3268 print esc_path($basename);
59fb1c94 3269 }
ede5e100 3270 }
4df118ed 3271 print "<br/></div>\n";
ede5e100
KS
3272}
3273
b7f9253d
JN
3274# sub git_print_log (\@;%) {
3275sub git_print_log ($;%) {
d16d093c 3276 my $log = shift;
b7f9253d 3277 my %opts = @_;
d16d093c 3278
b7f9253d
JN
3279 if ($opts{'-remove_title'}) {
3280 # remove title, i.e. first line of log
3281 shift @$log;
3282 }
d16d093c
JN
3283 # remove leading empty lines
3284 while (defined $log->[0] && $log->[0] eq "") {
3285 shift @$log;
3286 }
3287
3288 # print log
3289 my $signoff = 0;
3290 my $empty = 0;
3291 foreach my $line (@$log) {
b7f9253d
JN
3292 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3293 $signoff = 1;
fba20b42 3294 $empty = 0;
b7f9253d
JN
3295 if (! $opts{'-remove_signoff'}) {
3296 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3297 next;
3298 } else {
3299 # remove signoff lines
3300 next;
3301 }
3302 } else {
3303 $signoff = 0;
3304 }
3305
d16d093c
JN
3306 # print only one empty line
3307 # do not print empty line after signoff
3308 if ($line eq "") {
3309 next if ($empty || $signoff);
3310 $empty = 1;
3311 } else {
3312 $empty = 0;
3313 }
b7f9253d
JN
3314
3315 print format_log_line_html($line) . "<br/>\n";
3316 }
3317
3318 if ($opts{'-final_empty_line'}) {
3319 # end with single empty line
3320 print "<br/>\n" unless $empty;
d16d093c
JN
3321 }
3322}
3323
e33fba4c
JN
3324# return link target (what link points to)
3325sub git_get_link_target {
3326 my $hash = shift;
3327 my $link_target;
3328
3329 # read link
3330 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3331 or return;
3332 {
3333 local $/;
3334 $link_target = <$fd>;
3335 }
3336 close $fd
3337 or return;
3338
3339 return $link_target;
3340}
3341
3bf9d570
JN
3342# given link target, and the directory (basedir) the link is in,
3343# return target of link relative to top directory (top tree);
3344# return undef if it is not possible (including absolute links).
3345sub normalize_link_target {
3346 my ($link_target, $basedir, $hash_base) = @_;
3347
3348 # we can normalize symlink target only if $hash_base is provided
3349 return unless $hash_base;
3350
3351 # absolute symlinks (beginning with '/') cannot be normalized
3352 return if (substr($link_target, 0, 1) eq '/');
3353
3354 # normalize link target to path from top (root) tree (dir)
3355 my $path;
3356 if ($basedir) {
3357 $path = $basedir . '/' . $link_target;
3358 } else {
3359 # we are in top (root) tree (dir)
3360 $path = $link_target;
3361 }
3362
3363 # remove //, /./, and /../
3364 my @path_parts;
3365 foreach my $part (split('/', $path)) {
3366 # discard '.' and ''
3367 next if (!$part || $part eq '.');
3368 # handle '..'
3369 if ($part eq '..') {
3370 if (@path_parts) {
3371 pop @path_parts;
3372 } else {
3373 # link leads outside repository (outside top dir)
3374 return;
3375 }
3376 } else {
3377 push @path_parts, $part;
3378 }
3379 }
3380 $path = join('/', @path_parts);
3381
3382 return $path;
3383}
e33fba4c 3384
fa702003
JN
3385# print tree entry (row of git_tree), but without encompassing <tr> element
3386sub git_print_tree_entry {
3387 my ($t, $basedir, $hash_base, $have_blame) = @_;
3388
3389 my %base_key = ();
e33fba4c 3390 $base_key{'hash_base'} = $hash_base if defined $hash_base;
fa702003 3391
4de741b3
LT
3392 # The format of a table row is: mode list link. Where mode is
3393 # the mode of the entry, list is the name of the entry, an href,
3394 # and link is the action links of the entry.
3395
fa702003
JN
3396 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3397 if ($t->{'type'} eq "blob") {
3398 print "<td class=\"list\">" .
4de741b3 3399 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e7fb022a 3400 file_name=>"$basedir$t->{'name'}", %base_key),
e33fba4c
JN
3401 -class => "list"}, esc_path($t->{'name'}));
3402 if (S_ISLNK(oct $t->{'mode'})) {
3403 my $link_target = git_get_link_target($t->{'hash'});
3404 if ($link_target) {
3bf9d570
JN
3405 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3406 if (defined $norm_target) {
3407 print " -> " .
3408 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3409 file_name=>$norm_target),
3410 -title => $norm_target}, esc_path($link_target));
3411 } else {
3412 print " -> " . esc_path($link_target);
3413 }
e33fba4c
JN
3414 }
3415 }
3416 print "</td>\n";
4de741b3 3417 print "<td class=\"link\">";
4777b014 3418 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e33fba4c
JN
3419 file_name=>"$basedir$t->{'name'}", %base_key)},
3420 "blob");
fa702003 3421 if ($have_blame) {
4777b014
PB
3422 print " | " .
3423 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
e33fba4c
JN
3424 file_name=>"$basedir$t->{'name'}", %base_key)},
3425 "blame");
fa702003
JN
3426 }
3427 if (defined $hash_base) {
4777b014
PB
3428 print " | " .
3429 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003
JN
3430 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3431 "history");
3432 }
3433 print " | " .
6f7ea5fb 3434 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
e7fb022a
JN
3435 file_name=>"$basedir$t->{'name'}")},
3436 "raw");
4de741b3 3437 print "</td>\n";
fa702003
JN
3438
3439 } elsif ($t->{'type'} eq "tree") {
0fa105e7
LT
3440 print "<td class=\"list\">";
3441 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
fa702003 3442 file_name=>"$basedir$t->{'name'}", %base_key)},
403d0906 3443 esc_path($t->{'name'}));
0fa105e7
LT
3444 print "</td>\n";
3445 print "<td class=\"link\">";
4777b014 3446 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
e33fba4c
JN
3447 file_name=>"$basedir$t->{'name'}", %base_key)},
3448 "tree");
fa702003 3449 if (defined $hash_base) {
4777b014
PB
3450 print " | " .
3451 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003 3452 file_name=>"$basedir$t->{'name'}")},
01ac1e38
JN
3453 "history");
3454 }
3455 print "</td>\n";
3456 } else {
3457 # unknown object: we can only present history for it
3458 # (this includes 'commit' object, i.e. submodule support)
3459 print "<td class=\"list\">" .
3460 esc_path($t->{'name'}) .
3461 "</td>\n";
3462 print "<td class=\"link\">";
3463 if (defined $hash_base) {
3464 print $cgi->a({-href => href(action=>"history",
3465 hash_base=>$hash_base,
3466 file_name=>"$basedir$t->{'name'}")},
fa702003
JN
3467 "history");
3468 }
3469 print "</td>\n";
3470 }
3471}
3472
717b8311
JN
3473## ......................................................................
3474## functions printing large fragments of HTML
3475
0cec6db5 3476# get pre-image filenames for merge (combined) diff
e72c0eaf
JN
3477sub fill_from_file_info {
3478 my ($diff, @parents) = @_;
3479
3480 $diff->{'from_file'} = [ ];
3481 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3482 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3483 if ($diff->{'status'}[$i] eq 'R' ||
3484 $diff->{'status'}[$i] eq 'C') {
3485 $diff->{'from_file'}[$i] =
3486 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3487 }
3488 }
3489
3490 return $diff;
3491}
3492
0cec6db5 3493# is current raw difftree line of file deletion
90921740
JN
3494sub is_deleted {
3495 my $diffinfo = shift;
3496
4ed4a347 3497 return $diffinfo->{'to_id'} eq ('0' x 40);
90921740 3498}
e72c0eaf 3499
0cec6db5
JN
3500# does patch correspond to [previous] difftree raw line
3501# $diffinfo - hashref of parsed raw diff format
3502# $patchinfo - hashref of parsed patch diff format
3503# (the same keys as in $diffinfo)
3504sub is_patch_split {
3505 my ($diffinfo, $patchinfo) = @_;
3506
3507 return defined $diffinfo && defined $patchinfo
9d301456 3508 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
0cec6db5
JN
3509}
3510
3511
4a4a1a53 3512sub git_difftree_body {
ed224dea
JN
3513 my ($difftree, $hash, @parents) = @_;
3514 my ($parent) = $parents[0];
25b2790f 3515 my $have_blame = gitweb_check_feature('blame');
4a4a1a53
JN
3516 print "<div class=\"list_head\">\n";
3517 if ($#{$difftree} > 10) {
3518 print(($#{$difftree} + 1) . " files changed:\n");
3519 }
3520 print "</div>\n";
3521
ed224dea
JN
3522 print "<table class=\"" .
3523 (@parents > 1 ? "combined " : "") .
3524 "diff_tree\">\n";
47598d7a
JN
3525
3526 # header only for combined diff in 'commitdiff' view
3ef408ae 3527 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
47598d7a
JN
3528 if ($has_header) {
3529 # table header
3530 print "<thead><tr>\n" .
3531 "<th></th><th></th>\n"; # filename, patchN link
3532 for (my $i = 0; $i < @parents; $i++) {
3533 my $par = $parents[$i];
3534 print "<th>" .
3535 $cgi->a({-href => href(action=>"commitdiff",
3536 hash=>$hash, hash_parent=>$par),
3537 -title => 'commitdiff to parent number ' .
3538 ($i+1) . ': ' . substr($par,0,7)},
3539 $i+1) .
3540 "&nbsp;</th>\n";
3541 }
3542 print "</tr></thead>\n<tbody>\n";
3543 }
3544
6dd36acd 3545 my $alternate = 1;
b4657e77 3546 my $patchno = 0;
4a4a1a53 3547 foreach my $line (@{$difftree}) {
0cec6db5 3548 my $diff = parsed_difftree_line($line);
4a4a1a53
JN
3549
3550 if ($alternate) {
3551 print "<tr class=\"dark\">\n";
3552 } else {
3553 print "<tr class=\"light\">\n";
3554 }
3555 $alternate ^= 1;
3556
493e01db 3557 if (exists $diff->{'nparents'}) { # combined diff
ed224dea 3558
493e01db
JN
3559 fill_from_file_info($diff, @parents)
3560 unless exists $diff->{'from_file'};
e72c0eaf 3561
90921740 3562 if (!is_deleted($diff)) {
ed224dea
JN
3563 # file exists in the result (child) commit
3564 print "<td>" .
493e01db
JN
3565 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3566 file_name=>$diff->{'to_file'},
ed224dea 3567 hash_base=>$hash),
493e01db 3568 -class => "list"}, esc_path($diff->{'to_file'})) .
ed224dea
JN
3569 "</td>\n";
3570 } else {
3571 print "<td>" .
493e01db 3572 esc_path($diff->{'to_file'}) .
ed224dea
JN
3573 "</td>\n";
3574 }
3575
3576 if ($action eq 'commitdiff') {
3577 # link to patch
3578 $patchno++;
3579 print "<td class=\"link\">" .
3580 $cgi->a({-href => "#patch$patchno"}, "patch") .
3581 " | " .
3582 "</td>\n";
3583 }
3584
3585 my $has_history = 0;
3586 my $not_deleted = 0;
493e01db 3587 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
ed224dea 3588 my $hash_parent = $parents[$i];
493e01db
JN
3589 my $from_hash = $diff->{'from_id'}[$i];
3590 my $from_path = $diff->{'from_file'}[$i];
3591 my $status = $diff->{'status'}[$i];
ed224dea
JN
3592
3593 $has_history ||= ($status ne 'A');
3594 $not_deleted ||= ($status ne 'D');
3595
ed224dea
JN
3596 if ($status eq 'A') {
3597 print "<td class=\"link\" align=\"right\"> | </td>\n";
3598 } elsif ($status eq 'D') {
3599 print "<td class=\"link\">" .
3600 $cgi->a({-href => href(action=>"blob",
3601 hash_base=>$hash,
3602 hash=>$from_hash,
3603 file_name=>$from_path)},
3604 "blob" . ($i+1)) .
3605 " | </td>\n";
3606 } else {
493e01db 3607 if ($diff->{'to_id'} eq $from_hash) {
ed224dea
JN
3608 print "<td class=\"link nochange\">";
3609 } else {
3610 print "<td class=\"link\">";
3611 }
3612 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3613 hash=>$diff->{'to_id'},
ed224dea
JN
3614 hash_parent=>$from_hash,
3615 hash_base=>$hash,
3616 hash_parent_base=>$hash_parent,
493e01db 3617 file_name=>$diff->{'to_file'},
ed224dea
JN
3618 file_parent=>$from_path)},
3619 "diff" . ($i+1)) .
3620 " | </td>\n";
3621 }
3622 }
3623
3624 print "<td class=\"link\">";
3625 if ($not_deleted) {
3626 print $cgi->a({-href => href(action=>"blob",
493e01db
JN
3627 hash=>$diff->{'to_id'},
3628 file_name=>$diff->{'to_file'},
ed224dea
JN
3629 hash_base=>$hash)},
3630 "blob");
3631 print " | " if ($has_history);
3632 }
3633 if ($has_history) {
3634 print $cgi->a({-href => href(action=>"history",
493e01db 3635 file_name=>$diff->{'to_file'},
ed224dea
JN
3636 hash_base=>$hash)},
3637 "history");
3638 }
3639 print "</td>\n";
3640
3641 print "</tr>\n";
3642 next; # instead of 'else' clause, to avoid extra indent
3643 }
3644 # else ordinary diff
3645
e8e41a93
JN
3646 my ($to_mode_oct, $to_mode_str, $to_file_type);
3647 my ($from_mode_oct, $from_mode_str, $from_file_type);
493e01db
JN
3648 if ($diff->{'to_mode'} ne ('0' x 6)) {
3649 $to_mode_oct = oct $diff->{'to_mode'};
e8e41a93
JN
3650 if (S_ISREG($to_mode_oct)) { # only for regular file
3651 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3652 }
493e01db 3653 $to_file_type = file_type($diff->{'to_mode'});
e8e41a93 3654 }
493e01db
JN
3655 if ($diff->{'from_mode'} ne ('0' x 6)) {
3656 $from_mode_oct = oct $diff->{'from_mode'};
e8e41a93
JN
3657 if (S_ISREG($to_mode_oct)) { # only for regular file
3658 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4a4a1a53 3659 }
493e01db 3660 $from_file_type = file_type($diff->{'from_mode'});
e8e41a93
JN
3661 }
3662
493e01db 3663 if ($diff->{'status'} eq "A") { # created
e8e41a93
JN
3664 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3665 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3666 $mode_chng .= "]</span>";
499faeda 3667 print "<td>";
493e01db
JN
3668 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3669 hash_base=>$hash, file_name=>$diff->{'file'}),
3670 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3671 print "</td>\n";
3672 print "<td>$mode_chng</td>\n";
3673 print "<td class=\"link\">";
72dbafa1 3674 if ($action eq 'commitdiff') {
b4657e77
JN
3675 # link to patch
3676 $patchno++;
499faeda 3677 print $cgi->a({-href => "#patch$patchno"}, "patch");
897d1d2e 3678 print " | ";
b4657e77 3679 }
493e01db
JN
3680 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3681 hash_base=>$hash, file_name=>$diff->{'file'})},
3faa541f 3682 "blob");
b4657e77 3683 print "</td>\n";
4a4a1a53 3684
493e01db 3685 } elsif ($diff->{'status'} eq "D") { # deleted
e8e41a93 3686 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
499faeda 3687 print "<td>";
493e01db
JN
3688 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3689 hash_base=>$parent, file_name=>$diff->{'file'}),
3690 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3691 print "</td>\n";
3692 print "<td>$mode_chng</td>\n";
3693 print "<td class=\"link\">";
72dbafa1 3694 if ($action eq 'commitdiff') {
b4657e77
JN
3695 # link to patch
3696 $patchno++;
499faeda
LT
3697 print $cgi->a({-href => "#patch$patchno"}, "patch");
3698 print " | ";
b4657e77 3699 }
493e01db
JN
3700 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3701 hash_base=>$parent, file_name=>$diff->{'file'})},
897d1d2e 3702 "blob") . " | ";
2b2a8c78 3703 if ($have_blame) {
897d1d2e 3704 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
493e01db 3705 file_name=>$diff->{'file'})},
897d1d2e 3706 "blame") . " | ";
2b2a8c78 3707 }
b4657e77 3708 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
493e01db 3709 file_name=>$diff->{'file'})},
e7fb022a 3710 "history");
499faeda 3711 print "</td>\n";
4a4a1a53 3712
493e01db 3713 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4a4a1a53 3714 my $mode_chnge = "";
493e01db 3715 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93 3716 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6e72cf43 3717 if ($from_file_type ne $to_file_type) {
e8e41a93 3718 $mode_chnge .= " from $from_file_type to $to_file_type";
4a4a1a53 3719 }
e8e41a93
JN
3720 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3721 if ($from_mode_str && $to_mode_str) {
3722 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3723 } elsif ($to_mode_str) {
3724 $mode_chnge .= " mode: $to_mode_str";
4a4a1a53
JN
3725 }
3726 }
3727 $mode_chnge .= "]</span>\n";
3728 }
3729 print "<td>";
493e01db
JN
3730 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3731 hash_base=>$hash, file_name=>$diff->{'file'}),
3732 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3733 print "</td>\n";
3734 print "<td>$mode_chnge</td>\n";
3735 print "<td class=\"link\">";
241cc599
JN
3736 if ($action eq 'commitdiff') {
3737 # link to patch
3738 $patchno++;
3739 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3740 " | ";
493e01db 3741 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
3742 # "commit" view and modified file (not onlu mode changed)
3743 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3744 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 3745 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 3746 file_name=>$diff->{'file'})},
241cc599
JN
3747 "diff") .
3748 " | ";
4a4a1a53 3749 }
493e01db
JN
3750 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3751 hash_base=>$hash, file_name=>$diff->{'file'})},
897d1d2e 3752 "blob") . " | ";
2b2a8c78 3753 if ($have_blame) {
897d1d2e 3754 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 3755 file_name=>$diff->{'file'})},
897d1d2e 3756 "blame") . " | ";
2b2a8c78 3757 }
eb51ec9c 3758 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 3759 file_name=>$diff->{'file'})},
e7fb022a 3760 "history");
4a4a1a53
JN
3761 print "</td>\n";
3762
493e01db 3763 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
e8e41a93 3764 my %status_name = ('R' => 'moved', 'C' => 'copied');
493e01db 3765 my $nstatus = $status_name{$diff->{'status'}};
4a4a1a53 3766 my $mode_chng = "";
493e01db 3767 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93
JN
3768 # mode also for directories, so we cannot use $to_mode_str
3769 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4a4a1a53
JN
3770 }
3771 print "<td>" .
e8e41a93 3772 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
493e01db
JN
3773 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3774 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
e8e41a93
JN
3775 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3776 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
493e01db
JN
3777 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3778 -class => "list"}, esc_path($diff->{'from_file'})) .
3779 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
499faeda 3780 "<td class=\"link\">";
241cc599
JN
3781 if ($action eq 'commitdiff') {
3782 # link to patch
3783 $patchno++;
3784 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3785 " | ";
493e01db 3786 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
3787 # "commit" view and modified file (not only pure rename or copy)
3788 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3789 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 3790 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 3791 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
241cc599
JN
3792 "diff") .
3793 " | ";
4a4a1a53 3794 }
493e01db
JN
3795 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3796 hash_base=>$parent, file_name=>$diff->{'to_file'})},
897d1d2e 3797 "blob") . " | ";
2b2a8c78 3798 if ($have_blame) {
897d1d2e 3799 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 3800 file_name=>$diff->{'to_file'})},
897d1d2e 3801 "blame") . " | ";
2b2a8c78 3802 }
897d1d2e 3803 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 3804 file_name=>$diff->{'to_file'})},
e7fb022a 3805 "history");
4a4a1a53 3806 print "</td>\n";
e8e41a93 3807
4a4a1a53
JN
3808 } # we should not encounter Unmerged (U) or Unknown (X) status
3809 print "</tr>\n";
3810 }
47598d7a 3811 print "</tbody>" if $has_header;
4a4a1a53
JN
3812 print "</table>\n";
3813}
3814
eee08903 3815sub git_patchset_body {
e72c0eaf
JN
3816 my ($fd, $difftree, $hash, @hash_parents) = @_;
3817 my ($hash_parent) = $hash_parents[0];
eee08903 3818
0cec6db5 3819 my $is_combined = (@hash_parents > 1);
eee08903 3820 my $patch_idx = 0;
4280cde9 3821 my $patch_number = 0;
6d55f055 3822 my $patch_line;
fe87585e 3823 my $diffinfo;
0cec6db5 3824 my $to_name;
744d0ac3 3825 my (%from, %to);
eee08903
JN
3826
3827 print "<div class=\"patchset\">\n";
3828
6d55f055
JN
3829 # skip to first patch
3830 while ($patch_line = <$fd>) {
157e43b4 3831 chomp $patch_line;
eee08903 3832
6d55f055
JN
3833 last if ($patch_line =~ m/^diff /);
3834 }
3835
3836 PATCH:
3837 while ($patch_line) {
6d55f055 3838
0cec6db5
JN
3839 # parse "git diff" header line
3840 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3841 # $1 is from_name, which we do not use
3842 $to_name = unquote($2);
3843 $to_name =~ s!^b/!!;
3844 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3845 # $1 is 'cc' or 'combined', which we do not use
3846 $to_name = unquote($2);
3847 } else {
3848 $to_name = undef;
6d55f055 3849 }
6d55f055
JN
3850
3851 # check if current patch belong to current raw line
3852 # and parse raw git-diff line if needed
0cec6db5 3853 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
2206537c 3854 # this is continuation of a split patch
6d55f055
JN
3855 print "<div class=\"patch cont\">\n";
3856 } else {
3857 # advance raw git-diff output if needed
3858 $patch_idx++ if defined $diffinfo;
eee08903 3859
0cec6db5
JN
3860 # read and prepare patch information
3861 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 3862
0cec6db5
JN
3863 # compact combined diff output can have some patches skipped
3864 # find which patch (using pathname of result) we are at now;
3865 if ($is_combined) {
3866 while ($to_name ne $diffinfo->{'to_file'}) {
cd030c3a
JN
3867 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3868 format_diff_cc_simplified($diffinfo, @hash_parents) .
3869 "</div>\n"; # class="patch"
3870
3871 $patch_idx++;
3872 $patch_number++;
0cec6db5
JN
3873
3874 last if $patch_idx > $#$difftree;
3875 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 3876 }
0cec6db5 3877 }
711fa742 3878
90921740
JN
3879 # modifies %from, %to hashes
3880 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5f855052 3881
6d55f055
JN
3882 # this is first patch for raw difftree line with $patch_idx index
3883 # we index @$difftree array from 0, but number patches from 1
3884 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
744d0ac3 3885 }
eee08903 3886
0cec6db5
JN
3887 # git diff header
3888 #assert($patch_line =~ m/^diff /) if DEBUG;
3889 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3890 $patch_number++;
6d55f055 3891 # print "git diff" header
90921740
JN
3892 print format_git_diff_header_line($patch_line, $diffinfo,
3893 \%from, \%to);
6d55f055
JN
3894
3895 # print extended diff header
0cec6db5 3896 print "<div class=\"diff extended_header\">\n";
6d55f055 3897 EXTENDED_HEADER:
0cec6db5
JN
3898 while ($patch_line = <$fd>) {
3899 chomp $patch_line;
3900
3901 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3902
90921740
JN
3903 print format_extended_diff_header_line($patch_line, $diffinfo,
3904 \%from, \%to);
6d55f055 3905 }
0cec6db5 3906 print "</div>\n"; # class="diff extended_header"
6d55f055
JN
3907
3908 # from-file/to-file diff header
0bdb28c9
JN
3909 if (! $patch_line) {
3910 print "</div>\n"; # class="patch"
3911 last PATCH;
3912 }
66399eff 3913 next PATCH if ($patch_line =~ m/^diff /);
6d55f055 3914 #assert($patch_line =~ m/^---/) if DEBUG;
744d0ac3 3915
0cec6db5 3916 my $last_patch_line = $patch_line;
6d55f055 3917 $patch_line = <$fd>;
6d55f055 3918 chomp $patch_line;
90921740 3919 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
e4e4f825 3920
90921740 3921 print format_diff_from_to_header($last_patch_line, $patch_line,
91af4ce4
JN
3922 $diffinfo, \%from, \%to,
3923 @hash_parents);
e4e4f825 3924
6d55f055
JN
3925 # the patch itself
3926 LINE:
3927 while ($patch_line = <$fd>) {
3928 chomp $patch_line;
e4e4f825 3929
6d55f055 3930 next PATCH if ($patch_line =~ m/^diff /);
e4e4f825 3931
59e3b14e 3932 print format_diff_line($patch_line, \%from, \%to);
eee08903 3933 }
eee08903 3934
6d55f055
JN
3935 } continue {
3936 print "</div>\n"; # class="patch"
eee08903 3937 }
d26c4264 3938
cd030c3a
JN
3939 # for compact combined (--cc) format, with chunk and patch simpliciaction
3940 # patchset might be empty, but there might be unprocessed raw lines
0cec6db5 3941 for (++$patch_idx if $patch_number > 0;
cd030c3a 3942 $patch_idx < @$difftree;
0cec6db5 3943 ++$patch_idx) {
cd030c3a 3944 # read and prepare patch information
0cec6db5 3945 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a
JN
3946
3947 # generate anchor for "patch" links in difftree / whatchanged part
3948 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3949 format_diff_cc_simplified($diffinfo, @hash_parents) .
3950 "</div>\n"; # class="patch"
3951
3952 $patch_number++;
3953 }
3954
d26c4264
JN
3955 if ($patch_number == 0) {
3956 if (@hash_parents > 1) {
3957 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3958 } else {
3959 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3960 }
3961 }
eee08903
JN
3962
3963 print "</div>\n"; # class="patchset"
3964}
3965
3966# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3967
69913415
JN
3968# fills project list info (age, description, owner, forks) for each
3969# project in the list, removing invalid projects from returned list
3970# NOTE: modifies $projlist, but does not remove entries from it
3971sub fill_project_list_info {
3972 my ($projlist, $check_forks) = @_;
e30496df 3973 my @projects;
69913415 3974
25b2790f 3975 my $show_ctags = gitweb_check_feature('ctags');
69913415 3976 PROJECT:
e30496df 3977 foreach my $pr (@$projlist) {
69913415
JN
3978 my (@activity) = git_get_last_activity($pr->{'path'});
3979 unless (@activity) {
3980 next PROJECT;
e30496df 3981 }
69913415 3982 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
e30496df
PB
3983 if (!defined $pr->{'descr'}) {
3984 my $descr = git_get_project_description($pr->{'path'}) || "";
69913415
JN
3985 $descr = to_utf8($descr);
3986 $pr->{'descr_long'} = $descr;
55feb120 3987 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
e30496df
PB
3988 }
3989 if (!defined $pr->{'owner'}) {
76e4f5d0 3990 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
e30496df
PB
3991 }
3992 if ($check_forks) {
3993 my $pname = $pr->{'path'};
83ee94c1
JH
3994 if (($pname =~ s/\.git$//) &&
3995 ($pname !~ /\/$/) &&
3996 (-d "$projectroot/$pname")) {
3997 $pr->{'forks'} = "-d $projectroot/$pname";
69913415 3998 } else {
83ee94c1
JH
3999 $pr->{'forks'} = 0;
4000 }
e30496df 4001 }
aed93de4 4002 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
e30496df
PB
4003 push @projects, $pr;
4004 }
4005
69913415
JN
4006 return @projects;
4007}
4008
6b28da67
PB
4009# print 'sort by' <th> element, generating 'sort by $name' replay link
4010# if that order is not selected
7da0f3a4 4011sub print_sort_th {
6b28da67 4012 my ($name, $order, $header) = @_;
7da0f3a4
JN
4013 $header ||= ucfirst($name);
4014
4015 if ($order eq $name) {
7da0f3a4
JN
4016 print "<th>$header</th>\n";
4017 } else {
4018 print "<th>" .
4019 $cgi->a({-href => href(-replay=>1, order=>$name),
4020 -class => "header"}, $header) .
4021 "</th>\n";
4022 }
4023}
4024
69913415 4025sub git_project_list_body {
42326110 4026 # actually uses global variable $project
69913415
JN
4027 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4028
25b2790f 4029 my $check_forks = gitweb_check_feature('forks');
69913415
JN
4030 my @projects = fill_project_list_info($projlist, $check_forks);
4031
b06dcf8c 4032 $order ||= $default_projects_order;
e30496df
PB
4033 $from = 0 unless defined $from;
4034 $to = $#projects if (!defined $to || $#projects < $to);
4035
6b28da67
PB
4036 my %order_info = (
4037 project => { key => 'path', type => 'str' },
4038 descr => { key => 'descr_long', type => 'str' },
4039 owner => { key => 'owner', type => 'str' },
4040 age => { key => 'age', type => 'num' }
4041 );
4042 my $oi = $order_info{$order};
4043 if ($oi->{'type'} eq 'str') {
4044 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4045 } else {
4046 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4047 }
4048
25b2790f 4049 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
4050 if ($show_ctags) {
4051 my %ctags;
4052 foreach my $p (@projects) {
4053 foreach my $ct (keys %{$p->{'ctags'}}) {
4054 $ctags{$ct} += $p->{'ctags'}->{$ct};
4055 }
4056 }
4057 my $cloud = git_populate_project_tagcloud(\%ctags);
4058 print git_show_project_tagcloud($cloud, 64);
4059 }
4060
e30496df
PB
4061 print "<table class=\"project_list\">\n";
4062 unless ($no_header) {
4063 print "<tr>\n";
4064 if ($check_forks) {
4065 print "<th></th>\n";
4066 }
6b28da67
PB
4067 print_sort_th('project', $order, 'Project');
4068 print_sort_th('descr', $order, 'Description');
4069 print_sort_th('owner', $order, 'Owner');
4070 print_sort_th('age', $order, 'Last Change');
7da0f3a4 4071 print "<th></th>\n" . # for links
e30496df
PB
4072 "</tr>\n";
4073 }
4074 my $alternate = 1;
aed93de4 4075 my $tagfilter = $cgi->param('by_tag');
e30496df
PB
4076 for (my $i = $from; $i <= $to; $i++) {
4077 my $pr = $projects[$i];
42326110 4078
aed93de4 4079 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
0d1d154d
PB
4080 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4081 and not $pr->{'descr_long'} =~ /$searchtext/;
4082 # Weed out forks or non-matching entries of search
42326110
PB
4083 if ($check_forks) {
4084 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4085 $forkbase="^$forkbase" if $forkbase;
0d1d154d
PB
4086 next if not $searchtext and not $tagfilter and $show_ctags
4087 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
42326110
PB
4088 }
4089
e30496df
PB
4090 if ($alternate) {
4091 print "<tr class=\"dark\">\n";
4092 } else {
4093 print "<tr class=\"light\">\n";
4094 }
4095 $alternate ^= 1;
4096 if ($check_forks) {
4097 print "<td>";
4098 if ($pr->{'forks'}) {
83ee94c1 4099 print "<!-- $pr->{'forks'} -->\n";
e30496df
PB
4100 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4101 }
4102 print "</td>\n";
4103 }
4104 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4105 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
e88ce8a4
JN
4106 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4107 -class => "list", -title => $pr->{'descr_long'}},
4108 esc_html($pr->{'descr'})) . "</td>\n" .
d3cd2495 4109 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
e30496df 4110 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
785cdea9 4111 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
e30496df
PB
4112 "<td class=\"link\">" .
4113 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
faa1bbfd 4114 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
e30496df
PB
4115 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4116 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4117 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4118 "</td>\n" .
4119 "</tr>\n";
4120 }
4121 if (defined $extra) {
4122 print "<tr>\n";
4123 if ($check_forks) {
4124 print "<td></td>\n";
4125 }
4126 print "<td colspan=\"5\">$extra</td>\n" .
4127 "</tr>\n";
4128 }
4129 print "</table>\n";
4130}
4131
9f5dcb81
JN
4132sub git_shortlog_body {
4133 # uses global variable $project
190d7fdc 4134 my ($commitlist, $from, $to, $refs, $extra) = @_;
ddb8d900 4135
9f5dcb81 4136 $from = 0 unless defined $from;
190d7fdc 4137 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
9f5dcb81 4138
591ebf65 4139 print "<table class=\"shortlog\">\n";
6dd36acd 4140 my $alternate = 1;
9f5dcb81 4141 for (my $i = $from; $i <= $to; $i++) {
190d7fdc
RF
4142 my %co = %{$commitlist->[$i]};
4143 my $commit = $co{'id'};
847e01fb 4144 my $ref = format_ref_marker($refs, $commit);
9f5dcb81
JN
4145 if ($alternate) {
4146 print "<tr class=\"dark\">\n";
4147 } else {
4148 print "<tr class=\"light\">\n";
4149 }
4150 $alternate ^= 1;
ce58ec91 4151 my $author = chop_and_escape_str($co{'author_name'}, 10);
9f5dcb81
JN
4152 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4153 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4154 "<td><i>" . $author . "</i></td>\n" .
9f5dcb81 4155 "<td>";
952c65fc
JN
4156 print format_subject_html($co{'title'}, $co{'title_short'},
4157 href(action=>"commit", hash=>$commit), $ref);
9f5dcb81
JN
4158 print "</td>\n" .
4159 "<td class=\"link\">" .
4777b014 4160 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
35749ae5 4161 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
55ff35cb 4162 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
a3c8ab30
MM
4163 my $snapshot_links = format_snapshot_links($commit);
4164 if (defined $snapshot_links) {
4165 print " | " . $snapshot_links;
55ff35cb 4166 }
cb9c6e5b 4167 print "</td>\n" .
9f5dcb81
JN
4168 "</tr>\n";
4169 }
4170 if (defined $extra) {
4171 print "<tr>\n" .
4172 "<td colspan=\"4\">$extra</td>\n" .
4173 "</tr>\n";
4174 }
4175 print "</table>\n";
4176}
4177
581860e1
JN
4178sub git_history_body {
4179 # Warning: assumes constant type (blob or tree) during history
a8b983bf 4180 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
8be68352
JN
4181
4182 $from = 0 unless defined $from;
a8b983bf 4183 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
581860e1 4184
591ebf65 4185 print "<table class=\"history\">\n";
6dd36acd 4186 my $alternate = 1;
8be68352 4187 for (my $i = $from; $i <= $to; $i++) {
a8b983bf 4188 my %co = %{$commitlist->[$i]};
581860e1
JN
4189 if (!%co) {
4190 next;
4191 }
a8b983bf 4192 my $commit = $co{'id'};
581860e1
JN
4193
4194 my $ref = format_ref_marker($refs, $commit);
4195
4196 if ($alternate) {
4197 print "<tr class=\"dark\">\n";
4198 } else {
4199 print "<tr class=\"light\">\n";
4200 }
4201 $alternate ^= 1;
e076a0e7 4202 # shortlog uses chop_str($co{'author_name'}, 10)
ce58ec91 4203 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
581860e1 4204 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4205 "<td><i>" . $author . "</i></td>\n" .
581860e1
JN
4206 "<td>";
4207 # originally git_history used chop_str($co{'title'}, 50)
952c65fc
JN
4208 print format_subject_html($co{'title'}, $co{'title_short'},
4209 href(action=>"commit", hash=>$commit), $ref);
581860e1
JN
4210 print "</td>\n" .
4211 "<td class=\"link\">" .
6d81c5a2
LT
4212 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4213 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
581860e1
JN
4214
4215 if ($ftype eq 'blob') {
4216 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4217 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4218 if (defined $blob_current && defined $blob_parent &&
4219 $blob_current ne $blob_parent) {
4220 print " | " .
420e92f2
JN
4221 $cgi->a({-href => href(action=>"blobdiff",
4222 hash=>$blob_current, hash_parent=>$blob_parent,
4223 hash_base=>$hash_base, hash_parent_base=>$commit,
4224 file_name=>$file_name)},
581860e1
JN
4225 "diff to current");
4226 }
4227 }
4228 print "</td>\n" .
4229 "</tr>\n";
4230 }
4231 if (defined $extra) {
4232 print "<tr>\n" .
4233 "<td colspan=\"4\">$extra</td>\n" .
4234 "</tr>\n";
4235 }
4236 print "</table>\n";
4237}
4238
717b8311
JN
4239sub git_tags_body {
4240 # uses global variable $project
4241 my ($taglist, $from, $to, $extra) = @_;
4242 $from = 0 unless defined $from;
4243 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4244
591ebf65 4245 print "<table class=\"tags\">\n";
6dd36acd 4246 my $alternate = 1;
717b8311
JN
4247 for (my $i = $from; $i <= $to; $i++) {
4248 my $entry = $taglist->[$i];
4249 my %tag = %$entry;
cd146408 4250 my $comment = $tag{'subject'};
717b8311
JN
4251 my $comment_short;
4252 if (defined $comment) {
4253 $comment_short = chop_str($comment, 30, 5);
4254 }
4255 if ($alternate) {
4256 print "<tr class=\"dark\">\n";
4257 } else {
4258 print "<tr class=\"light\">\n";
4259 }
4260 $alternate ^= 1;
27dd1a83
JN
4261 if (defined $tag{'age'}) {
4262 print "<td><i>$tag{'age'}</i></td>\n";
4263 } else {
4264 print "<td></td>\n";
4265 }
4266 print "<td>" .
1c2a4f5a 4267 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
63e4220b 4268 -class => "list name"}, esc_html($tag{'name'})) .
717b8311
JN
4269 "</td>\n" .
4270 "<td>";
4271 if (defined $comment) {
952c65fc
JN
4272 print format_subject_html($comment, $comment_short,
4273 href(action=>"tag", hash=>$tag{'id'}));
717b8311
JN
4274 }
4275 print "</td>\n" .
4276 "<td class=\"selflink\">";
4277 if ($tag{'type'} eq "tag") {
1c2a4f5a 4278 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
717b8311
JN
4279 } else {
4280 print "&nbsp;";
4281 }
4282 print "</td>\n" .
4283 "<td class=\"link\">" . " | " .
1c2a4f5a 4284 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
717b8311 4285 if ($tag{'reftype'} eq "commit") {
bf901f8e
JN
4286 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4287 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
717b8311 4288 } elsif ($tag{'reftype'} eq "blob") {
1c2a4f5a 4289 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
717b8311
JN
4290 }
4291 print "</td>\n" .
4292 "</tr>";
4293 }
4294 if (defined $extra) {
4295 print "<tr>\n" .
4296 "<td colspan=\"5\">$extra</td>\n" .
4297 "</tr>\n";
4298 }
4299 print "</table>\n";
4300}
4301
4302sub git_heads_body {
4303 # uses global variable $project
120ddde2 4304 my ($headlist, $head, $from, $to, $extra) = @_;
717b8311 4305 $from = 0 unless defined $from;
120ddde2 4306 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
717b8311 4307
591ebf65 4308 print "<table class=\"heads\">\n";
6dd36acd 4309 my $alternate = 1;
717b8311 4310 for (my $i = $from; $i <= $to; $i++) {
120ddde2 4311 my $entry = $headlist->[$i];
cd146408
JN
4312 my %ref = %$entry;
4313 my $curr = $ref{'id'} eq $head;
717b8311
JN
4314 if ($alternate) {
4315 print "<tr class=\"dark\">\n";
4316 } else {
4317 print "<tr class=\"light\">\n";
4318 }
4319 $alternate ^= 1;
cd146408
JN
4320 print "<td><i>$ref{'age'}</i></td>\n" .
4321 ($curr ? "<td class=\"current_head\">" : "<td>") .
bf901f8e 4322 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
cd146408 4323 -class => "list name"},esc_html($ref{'name'})) .
717b8311
JN
4324 "</td>\n" .
4325 "<td class=\"link\">" .
bf901f8e
JN
4326 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4327 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4328 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
717b8311
JN
4329 "</td>\n" .
4330 "</tr>";
4331 }
4332 if (defined $extra) {
4333 print "<tr>\n" .
4334 "<td colspan=\"3\">$extra</td>\n" .
4335 "</tr>\n";
4336 }
4337 print "</table>\n";
4338}
4339
8dbc0fce 4340sub git_search_grep_body {
5ad66088 4341 my ($commitlist, $from, $to, $extra) = @_;
8dbc0fce 4342 $from = 0 unless defined $from;
5ad66088 4343 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
8dbc0fce 4344
591ebf65 4345 print "<table class=\"commit_search\">\n";
8dbc0fce
RF
4346 my $alternate = 1;
4347 for (my $i = $from; $i <= $to; $i++) {
5ad66088 4348 my %co = %{$commitlist->[$i]};
8dbc0fce
RF
4349 if (!%co) {
4350 next;
4351 }
5ad66088 4352 my $commit = $co{'id'};
8dbc0fce
RF
4353 if ($alternate) {
4354 print "<tr class=\"dark\">\n";
4355 } else {
4356 print "<tr class=\"light\">\n";
4357 }
4358 $alternate ^= 1;
ce58ec91 4359 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
8dbc0fce 4360 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4361 "<td><i>" . $author . "</i></td>\n" .
8dbc0fce 4362 "<td>" .
be8b9063
JH
4363 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4364 -class => "list subject"},
4365 chop_and_escape_str($co{'title'}, 50) . "<br/>");
8dbc0fce
RF
4366 my $comment = $co{'comment'};
4367 foreach my $line (@$comment) {
6dfbb304 4368 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
be8b9063 4369 my ($lead, $match, $trail) = ($1, $2, $3);
b8d97d07
JN
4370 $match = chop_str($match, 70, 5, 'center');
4371 my $contextlen = int((80 - length($match))/2);
4372 $contextlen = 30 if ($contextlen > 30);
4373 $lead = chop_str($lead, $contextlen, 10, 'left');
4374 $trail = chop_str($trail, $contextlen, 10, 'right');
be8b9063
JH
4375
4376 $lead = esc_html($lead);
4377 $match = esc_html($match);
4378 $trail = esc_html($trail);
4379
4380 print "$lead<span class=\"match\">$match</span>$trail<br />";
8dbc0fce
RF
4381 }
4382 }
4383 print "</td>\n" .
4384 "<td class=\"link\">" .
4385 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4386 " | " .
f1fe8f5c
CR
4387 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4388 " | " .
8dbc0fce
RF
4389 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4390 print "</td>\n" .
4391 "</tr>\n";
4392 }
4393 if (defined $extra) {
4394 print "<tr>\n" .
4395 "<td colspan=\"3\">$extra</td>\n" .
4396 "</tr>\n";
4397 }
4398 print "</table>\n";
4399}
4400
717b8311
JN
4401## ======================================================================
4402## ======================================================================
4403## actions
4404
717b8311 4405sub git_project_list {
1b2d297e 4406 my $order = $input_params{'order'};
b06dcf8c 4407 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 4408 die_error(400, "Unknown order parameter");
6326b60c
JN
4409 }
4410
847e01fb 4411 my @list = git_get_projects_list();
717b8311 4412 if (!@list) {
074afaa0 4413 die_error(404, "No projects found");
717b8311 4414 }
6326b60c 4415
717b8311
JN
4416 git_header_html();
4417 if (-f $home_text) {
4418 print "<div class=\"index_include\">\n";
2dcb5e1a 4419 insert_file($home_text);
717b8311 4420 print "</div>\n";
9f5dcb81 4421 }
0d1d154d
PB
4422 print $cgi->startform(-method => "get") .
4423 "<p class=\"projsearch\">Search:\n" .
4424 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4425 "</p>" .
4426 $cgi->end_form() . "\n";
e30496df
PB
4427 git_project_list_body(\@list, $order);
4428 git_footer_html();
4429}
4430
4431sub git_forks {
1b2d297e 4432 my $order = $input_params{'order'};
b06dcf8c 4433 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 4434 die_error(400, "Unknown order parameter");
717b8311 4435 }
e30496df
PB
4436
4437 my @list = git_get_projects_list($project);
4438 if (!@list) {
074afaa0 4439 die_error(404, "No forks found");
9f5dcb81 4440 }
e30496df
PB
4441
4442 git_header_html();
4443 git_print_page_nav('','');
4444 git_print_header_div('summary', "$project forks");
4445 git_project_list_body(\@list, $order);
717b8311 4446 git_footer_html();
9f5dcb81
JN
4447}
4448
fc2b2be0 4449sub git_project_index {
e30496df 4450 my @projects = git_get_projects_list($project);
fc2b2be0
JN
4451
4452 print $cgi->header(
4453 -type => 'text/plain',
4454 -charset => 'utf-8',
ab41dfbf 4455 -content_disposition => 'inline; filename="index.aux"');
fc2b2be0
JN
4456
4457 foreach my $pr (@projects) {
4458 if (!exists $pr->{'owner'}) {
76e4f5d0 4459 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
fc2b2be0
JN
4460 }
4461
4462 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4463 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4464 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4465 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4466 $path =~ s/ /\+/g;
4467 $owner =~ s/ /\+/g;
4468
4469 print "$path $owner\n";
4470 }
4471}
4472
ede5e100 4473sub git_summary {
847e01fb 4474 my $descr = git_get_project_description($project) || "none";
a979d128 4475 my %co = parse_commit("HEAD");
785cdea9 4476 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
a979d128 4477 my $head = $co{'id'};
ede5e100 4478
1e0cf030 4479 my $owner = git_get_project_owner($project);
ede5e100 4480
cd146408 4481 my $refs = git_get_references();
313ce8ce
RF
4482 # These get_*_list functions return one more to allow us to see if
4483 # there are more ...
4484 my @taglist = git_get_tags_list(16);
4485 my @headlist = git_get_heads_list(16);
e30496df 4486 my @forklist;
25b2790f 4487 my $check_forks = gitweb_check_feature('forks');
5dd5ed09
JH
4488
4489 if ($check_forks) {
e30496df
PB
4490 @forklist = git_get_projects_list($project);
4491 }
120ddde2 4492
ede5e100 4493 git_header_html();
847e01fb 4494 git_print_page_nav('summary','', $head);
9f5dcb81 4495
19806691 4496 print "<div class=\"title\">&nbsp;</div>\n";
591ebf65 4497 print "<table class=\"projects_list\">\n" .
a476142f
PB
4498 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4499 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
785cdea9 4500 if (defined $cd{'rfc2822'}) {
a476142f 4501 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
785cdea9
JN
4502 }
4503
e79ca7cc
JN
4504 # use per project git URL list in $projectroot/$project/cloneurl
4505 # or make project git URL from git base URL and project name
19a8721e 4506 my $url_tag = "URL";
e79ca7cc
JN
4507 my @url_list = git_get_project_url_list($project);
4508 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4509 foreach my $git_url (@url_list) {
4510 next unless $git_url;
a476142f 4511 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
19a8721e
JN
4512 $url_tag = "";
4513 }
aed93de4
PB
4514
4515 # Tag cloud
25b2790f 4516 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
4517 if ($show_ctags) {
4518 my $ctags = git_get_project_ctags($project);
4519 my $cloud = git_populate_project_tagcloud($ctags);
4520 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4521 print "</td>\n<td>" unless %$ctags;
4522 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4523 print "</td>\n<td>" if %$ctags;
4524 print git_show_project_tagcloud($cloud, 48);
4525 print "</td></tr>";
4526 }
4527
19a8721e 4528 print "</table>\n";
9f5dcb81 4529
7e1100e9
MM
4530 # If XSS prevention is on, we don't include README.html.
4531 # TODO: Allow a readme in some safe format.
4532 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
2dcb5e1a
JN
4533 print "<div class=\"title\">readme</div>\n" .
4534 "<div class=\"readme\">\n";
4535 insert_file("$projectroot/$project/README.html");
4536 print "\n</div>\n"; # class="readme"
447ef09a
PB
4537 }
4538
313ce8ce
RF
4539 # we need to request one more than 16 (0..15) to check if
4540 # those 16 are all
785cdea9
JN
4541 my @commitlist = $head ? parse_commits($head, 17) : ();
4542 if (@commitlist) {
4543 git_print_header_div('shortlog');
4544 git_shortlog_body(\@commitlist, 0, 15, $refs,
4545 $#commitlist <= 15 ? undef :
4546 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4547 }
ede5e100 4548
120ddde2 4549 if (@taglist) {
847e01fb 4550 git_print_header_div('tags');
120ddde2 4551 git_tags_body(\@taglist, 0, 15,
313ce8ce 4552 $#taglist <= 15 ? undef :
1c2a4f5a 4553 $cgi->a({-href => href(action=>"tags")}, "..."));
ede5e100 4554 }
0db37973 4555
120ddde2 4556 if (@headlist) {
847e01fb 4557 git_print_header_div('heads');
120ddde2 4558 git_heads_body(\@headlist, $head, 0, 15,
313ce8ce 4559 $#headlist <= 15 ? undef :
1c2a4f5a 4560 $cgi->a({-href => href(action=>"heads")}, "..."));
0db37973 4561 }
9f5dcb81 4562
e30496df
PB
4563 if (@forklist) {
4564 git_print_header_div('forks');
f04f27e8 4565 git_project_list_body(\@forklist, 'age', 0, 15,
aaca9675 4566 $#forklist <= 15 ? undef :
e30496df 4567 $cgi->a({-href => href(action=>"forks")}, "..."),
f04f27e8 4568 'no_header');
e30496df
PB
4569 }
4570
ede5e100
KS
4571 git_footer_html();
4572}
4573
d8a20ba9 4574sub git_tag {
847e01fb 4575 my $head = git_get_head_hash($project);
d8a20ba9 4576 git_header_html();
847e01fb
JN
4577 git_print_page_nav('','', $head,undef,$head);
4578 my %tag = parse_tag($hash);
198a2a8a
JN
4579
4580 if (! %tag) {
074afaa0 4581 die_error(404, "Unknown tag object");
198a2a8a
JN
4582 }
4583
847e01fb 4584 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
d8a20ba9 4585 print "<div class=\"title_text\">\n" .
591ebf65 4586 "<table class=\"object_header\">\n" .
e4669df9
KS
4587 "<tr>\n" .
4588 "<td>object</td>\n" .
952c65fc
JN
4589 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4590 $tag{'object'}) . "</td>\n" .
4591 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4592 $tag{'type'}) . "</td>\n" .
e4669df9 4593 "</tr>\n";
d8a20ba9 4594 if (defined($tag{'author'})) {
847e01fb 4595 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
40c13813 4596 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
952c65fc
JN
4597 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4598 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4599 "</td></tr>\n";
d8a20ba9
KS
4600 }
4601 print "</table>\n\n" .
4602 "</div>\n";
4603 print "<div class=\"page_body\">";
4604 my $comment = $tag{'comment'};
4605 foreach my $line (@$comment) {
7002243f 4606 chomp $line;
793c400c 4607 print esc_html($line, -nbsp=>1) . "<br/>\n";
d8a20ba9
KS
4608 }
4609 print "</div>\n";
4610 git_footer_html();
4611}
4612
3a5b919c 4613sub git_blame {
d2ce10d7 4614 # permissions
25b2790f 4615 gitweb_check_feature('blame')
d2ce10d7 4616 or die_error(403, "Blame view not allowed");
074afaa0 4617
d2ce10d7 4618 # error checking
074afaa0 4619 die_error(400, "No file name given") unless $file_name;
847e01fb 4620 $hash_base ||= git_get_head_hash($project);
d2ce10d7 4621 die_error(404, "Couldn't find base commit") unless $hash_base;
847e01fb 4622 my %co = parse_commit($hash_base)
074afaa0 4623 or die_error(404, "Commit not found");
d2ce10d7 4624 my $ftype = "blob";
1f2857ea
LT
4625 if (!defined $hash) {
4626 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
074afaa0 4627 or die_error(404, "Error looking up file");
d2ce10d7
JN
4628 } else {
4629 $ftype = git_get_type($hash);
4630 if ($ftype !~ "blob") {
4631 die_error(400, "Object is not a blob");
4632 }
1f2857ea 4633 }
d2ce10d7
JN
4634
4635 # run git-blame --porcelain
4636 open my $fd, "-|", git_cmd(), "blame", '-p',
4637 $hash_base, '--', $file_name
074afaa0 4638 or die_error(500, "Open git-blame failed");
d2ce10d7
JN
4639
4640 # page header
1f2857ea 4641 git_header_html();
0d83ddc4 4642 my $formats_nav =
a3823e5a 4643 $cgi->a({-href => href(action=>"blob", -replay=>1)},
952c65fc
JN
4644 "blob") .
4645 " | " .
a3823e5a
JN
4646 $cgi->a({-href => href(action=>"history", -replay=>1)},
4647 "history") .
cae1862a 4648 " | " .
952c65fc 4649 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
f35274da 4650 "HEAD");
847e01fb
JN
4651 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4652 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
59fb1c94 4653 git_print_page_path($file_name, $ftype, $hash_base);
d2ce10d7
JN
4654
4655 # page body
4656 my @rev_color = qw(light2 dark2);
cc1bf97e
LT
4657 my $num_colors = scalar(@rev_color);
4658 my $current_color = 0;
d2ce10d7
JN
4659 my %metainfo = ();
4660
59b9f61a
JN
4661 print <<HTML;
4662<div class="page_body">
4663<table class="blame">
4664<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4665HTML
d2ce10d7
JN
4666 LINE:
4667 while (my $line = <$fd>) {
4668 chomp $line;
4669 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4670 # no <lines in group> for subsequent lines in group of lines
d15c55aa 4671 my ($full_rev, $orig_lineno, $lineno, $group_size) =
d2ce10d7 4672 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
eeef88cd
JH
4673 if (!exists $metainfo{$full_rev}) {
4674 $metainfo{$full_rev} = {};
4675 }
4676 my $meta = $metainfo{$full_rev};
d2ce10d7
JN
4677 my $data;
4678 while ($data = <$fd>) {
4679 chomp $data;
4680 last if ($data =~ s/^\t//); # contents of line
4681 if ($data =~ /^(\S+) (.*)$/) {
eeef88cd
JH
4682 $meta->{$1} = $2;
4683 }
4684 }
d2ce10d7 4685 my $short_rev = substr($full_rev, 0, 8);
eeef88cd 4686 my $author = $meta->{'author'};
d2ce10d7
JN
4687 my %date =
4688 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
eeef88cd
JH
4689 my $date = $date{'iso-tz'};
4690 if ($group_size) {
d2ce10d7 4691 $current_color = ($current_color + 1) % $num_colors;
cc1bf97e 4692 }
4a24bfc2 4693 print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
eeef88cd
JH
4694 if ($group_size) {
4695 print "<td class=\"sha1\"";
5ad0828c 4696 print " title=\"". esc_html($author) . ", $date\"";
eeef88cd
JH
4697 print " rowspan=\"$group_size\"" if ($group_size > 1);
4698 print ">";
4699 print $cgi->a({-href => href(action=>"commit",
a23f0a73
JN
4700 hash=>$full_rev,
4701 file_name=>$file_name)},
d2ce10d7 4702 esc_html($short_rev));
eeef88cd 4703 print "</td>\n";
9dc5f8c9 4704 }
39c19ce2
JN
4705 my $parent_commit;
4706 if (!exists $meta->{'parent'}) {
4707 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4708 or die_error(500, "Open git-rev-parse failed");
4709 $parent_commit = <$dd>;
4710 close $dd;
4711 chomp($parent_commit);
4712 $meta->{'parent'} = $parent_commit;
4713 } else {
4714 $parent_commit = $meta->{'parent'};
4715 }
eeef88cd 4716 my $blamed = href(action => 'blame',
a23f0a73
JN
4717 file_name => $meta->{'filename'},
4718 hash_base => $parent_commit);
eeef88cd
JH
4719 print "<td class=\"linenr\">";
4720 print $cgi->a({ -href => "$blamed#l$orig_lineno",
a23f0a73
JN
4721 -class => "linenr" },
4722 esc_html($lineno));
eeef88cd 4723 print "</td>";
1f2857ea
LT
4724 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4725 print "</tr>\n";
4726 }
4727 print "</table>\n";
4728 print "</div>";
952c65fc
JN
4729 close $fd
4730 or print "Reading blob failed\n";
d2ce10d7
JN
4731
4732 # page footer
1f2857ea
LT
4733 git_footer_html();
4734}
4735
717b8311 4736sub git_tags {
847e01fb 4737 my $head = git_get_head_hash($project);
717b8311 4738 git_header_html();
847e01fb
JN
4739 git_print_page_nav('','', $head,undef,$head);
4740 git_print_header_div('summary', $project);
2d007374 4741
cd146408
JN
4742 my @tagslist = git_get_tags_list();
4743 if (@tagslist) {
4744 git_tags_body(\@tagslist);
2d007374 4745 }
717b8311 4746 git_footer_html();
2d007374
PB
4747}
4748
717b8311 4749sub git_heads {
847e01fb 4750 my $head = git_get_head_hash($project);
717b8311 4751 git_header_html();
847e01fb
JN
4752 git_print_page_nav('','', $head,undef,$head);
4753 git_print_header_div('summary', $project);
930cf7dd 4754
cd146408
JN
4755 my @headslist = git_get_heads_list();
4756 if (@headslist) {
4757 git_heads_body(\@headslist, $head);
f5aa79d9 4758 }
717b8311 4759 git_footer_html();
f5aa79d9
JN
4760}
4761
19806691 4762sub git_blob_plain {
7f718e8b 4763 my $type = shift;
f2e73302 4764 my $expires;
f2e73302 4765
cff0771b 4766 if (!defined $hash) {
5be01bc8 4767 if (defined $file_name) {
847e01fb 4768 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 4769 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 4770 or die_error(404, "Cannot find file");
5be01bc8 4771 } else {
074afaa0 4772 die_error(400, "No file name defined");
5be01bc8 4773 }
800764cf
MW
4774 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4775 # blobs defined by non-textual hash id's can be cached
4776 $expires = "+1d";
5be01bc8 4777 }
800764cf 4778
25691fbe 4779 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 4780 or die_error(500, "Open git-cat-file blob '$hash' failed");
930cf7dd 4781
7f718e8b
JN
4782 # content-type (can include charset)
4783 $type = blob_contenttype($fd, $file_name, $type);
f5aa79d9 4784
7f718e8b 4785 # "save as" filename, even when no $file_name is given
f5aa79d9 4786 my $save_as = "$hash";
9312944d
KS
4787 if (defined $file_name) {
4788 $save_as = $file_name;
f5aa79d9
JN
4789 } elsif ($type =~ m/^text\//) {
4790 $save_as .= '.txt';
9312944d 4791 }
f5aa79d9 4792
7e1100e9
MM
4793 # With XSS prevention on, blobs of all types except a few known safe
4794 # ones are served with "Content-Disposition: attachment" to make sure
4795 # they don't run in our security domain. For certain image types,
4796 # blob view writes an <img> tag referring to blob_plain view, and we
4797 # want to be sure not to break that by serving the image as an
4798 # attachment (though Firefox 3 doesn't seem to care).
4799 my $sandbox = $prevent_xss &&
4800 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4801
f2e73302 4802 print $cgi->header(
7f718e8b
JN
4803 -type => $type,
4804 -expires => $expires,
7e1100e9
MM
4805 -content_disposition =>
4806 ($sandbox ? 'attachment' : 'inline')
4807 . '; filename="' . $save_as . '"');
19806691 4808 undef $/;
ad14e931 4809 binmode STDOUT, ':raw';
19806691 4810 print <$fd>;
ad14e931 4811 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
19806691
KS
4812 $/ = "\n";
4813 close $fd;
4814}
4815
930cf7dd 4816sub git_blob {
f2e73302 4817 my $expires;
f2e73302 4818
cff0771b 4819 if (!defined $hash) {
5be01bc8 4820 if (defined $file_name) {
847e01fb 4821 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 4822 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 4823 or die_error(404, "Cannot find file");
5be01bc8 4824 } else {
074afaa0 4825 die_error(400, "No file name defined");
5be01bc8 4826 }
800764cf
MW
4827 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4828 # blobs defined by non-textual hash id's can be cached
4829 $expires = "+1d";
5be01bc8 4830 }
800764cf 4831
25b2790f 4832 my $have_blame = gitweb_check_feature('blame');
25691fbe 4833 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 4834 or die_error(500, "Couldn't cat $file_name, $hash");
847e01fb 4835 my $mimetype = blob_mimetype($fd, $file_name);
dfa7c7d2 4836 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
930cf7dd
LT
4837 close $fd;
4838 return git_blob_plain($mimetype);
4839 }
5a4cf334
JN
4840 # we can have blame only for text/* mimetype
4841 $have_blame &&= ($mimetype =~ m!^text/!);
4842
f2e73302 4843 git_header_html(undef, $expires);
0d83ddc4 4844 my $formats_nav = '';
847e01fb 4845 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
930cf7dd
LT
4846 if (defined $file_name) {
4847 if ($have_blame) {
952c65fc 4848 $formats_nav .=
a3823e5a 4849 $cgi->a({-href => href(action=>"blame", -replay=>1)},
952c65fc
JN
4850 "blame") .
4851 " | ";
930cf7dd 4852 }
0d83ddc4 4853 $formats_nav .=
a3823e5a 4854 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
4855 "history") .
4856 " | " .
a3823e5a 4857 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
35329cc1 4858 "raw") .
952c65fc
JN
4859 " | " .
4860 $cgi->a({-href => href(action=>"blob",
4861 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 4862 "HEAD");
930cf7dd 4863 } else {
952c65fc 4864 $formats_nav .=
a3823e5a
JN
4865 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4866 "raw");
930cf7dd 4867 }
847e01fb
JN
4868 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4869 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
930cf7dd
LT
4870 } else {
4871 print "<div class=\"page_nav\">\n" .
4872 "<br/><br/></div>\n" .
4873 "<div class=\"title\">$hash</div>\n";
4874 }
59fb1c94 4875 git_print_page_path($file_name, "blob", $hash_base);
930cf7dd 4876 print "<div class=\"page_body\">\n";
dfa7c7d2 4877 if ($mimetype =~ m!^image/!) {
5a4cf334
JN
4878 print qq!<img type="$mimetype"!;
4879 if ($file_name) {
4880 print qq! alt="$file_name" title="$file_name"!;
4881 }
4882 print qq! src="! .
4883 href(action=>"blob_plain", hash=>$hash,
4884 hash_base=>$hash_base, file_name=>$file_name) .
4885 qq!" />\n!;
dfa7c7d2
JN
4886 } else {
4887 my $nr;
4888 while (my $line = <$fd>) {
4889 chomp $line;
4890 $nr++;
4891 $line = untabify($line);
4892 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4893 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4894 }
930cf7dd 4895 }
952c65fc
JN
4896 close $fd
4897 or print "Reading blob failed.\n";
930cf7dd
LT
4898 print "</div>";
4899 git_footer_html();
4900}
4901
09bd7898 4902sub git_tree {
6f7ea5fb
LT
4903 if (!defined $hash_base) {
4904 $hash_base = "HEAD";
4905 }
b87d78d6 4906 if (!defined $hash) {
09bd7898 4907 if (defined $file_name) {
6f7ea5fb
LT
4908 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4909 } else {
4910 $hash = $hash_base;
10dba28d 4911 }
e925f38c 4912 }
2d7a3532 4913 die_error(404, "No such tree") unless defined($hash);
232ff553 4914 $/ = "\0";
25691fbe 4915 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
074afaa0 4916 or die_error(500, "Open git-ls-tree failed");
0881d2d1 4917 my @entries = map { chomp; $_ } <$fd>;
074afaa0 4918 close $fd or die_error(404, "Reading tree failed");
232ff553 4919 $/ = "\n";
d63577da 4920
847e01fb
JN
4921 my $refs = git_get_references();
4922 my $ref = format_ref_marker($refs, $hash_base);
12a88f2f 4923 git_header_html();
300454fe 4924 my $basedir = '';
25b2790f 4925 my $have_blame = gitweb_check_feature('blame');
847e01fb 4926 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
cae1862a
PB
4927 my @views_nav = ();
4928 if (defined $file_name) {
4929 push @views_nav,
a3823e5a 4930 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
4931 "history"),
4932 $cgi->a({-href => href(action=>"tree",
4933 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 4934 "HEAD"),
cae1862a 4935 }
a3c8ab30
MM
4936 my $snapshot_links = format_snapshot_links($hash);
4937 if (defined $snapshot_links) {
cae1862a 4938 # FIXME: Should be available when we have no hash base as well.
a3c8ab30 4939 push @views_nav, $snapshot_links;
cae1862a
PB
4940 }
4941 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
847e01fb 4942 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
d63577da 4943 } else {
fa702003 4944 undef $hash_base;
d63577da
KS
4945 print "<div class=\"page_nav\">\n";
4946 print "<br/><br/></div>\n";
4947 print "<div class=\"title\">$hash</div>\n";
4948 }
09bd7898 4949 if (defined $file_name) {
300454fe
JN
4950 $basedir = $file_name;
4951 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4952 $basedir .= '/';
4953 }
2d7a3532 4954 git_print_page_path($file_name, 'tree', $hash_base);
09bd7898 4955 }
fbb592a9 4956 print "<div class=\"page_body\">\n";
591ebf65 4957 print "<table class=\"tree\">\n";
6dd36acd 4958 my $alternate = 1;
b6b7fc72
JN
4959 # '..' (top directory) link if possible
4960 if (defined $hash_base &&
4961 defined $file_name && $file_name =~ m![^/]+$!) {
4962 if ($alternate) {
4963 print "<tr class=\"dark\">\n";
4964 } else {
4965 print "<tr class=\"light\">\n";
4966 }
4967 $alternate ^= 1;
4968
4969 my $up = $file_name;
4970 $up =~ s!/?[^/]+$!!;
4971 undef $up unless $up;
4972 # based on git_print_tree_entry
4973 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4974 print '<td class="list">';
4975 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4976 file_name=>$up)},
4977 "..");
4978 print "</td>\n";
4979 print "<td class=\"link\"></td>\n";
4980
4981 print "</tr>\n";
4982 }
161332a5 4983 foreach my $line (@entries) {
cb849b46
JN
4984 my %t = parse_ls_tree_line($line, -z => 1);
4985
bddec01d 4986 if ($alternate) {
c994d620 4987 print "<tr class=\"dark\">\n";
bddec01d 4988 } else {
c994d620 4989 print "<tr class=\"light\">\n";
bddec01d
KS
4990 }
4991 $alternate ^= 1;
cb849b46 4992
300454fe 4993 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
fa702003 4994
42f7eb94 4995 print "</tr>\n";
161332a5 4996 }
42f7eb94
KS
4997 print "</table>\n" .
4998 "</div>";
12a88f2f 4999 git_footer_html();
09bd7898
KS
5000}
5001
cb9c6e5b 5002sub git_snapshot {
1b2d297e 5003 my $format = $input_params{'snapshot_format'};
5e166843 5004 if (!@snapshot_fmts) {
074afaa0 5005 die_error(403, "Snapshots not allowed");
3473e7df
JN
5006 }
5007 # default to first supported snapshot format
5e166843 5008 $format ||= $snapshot_fmts[0];
3473e7df 5009 if ($format !~ m/^[a-z0-9]+$/) {
074afaa0 5010 die_error(400, "Invalid snapshot format parameter");
3473e7df 5011 } elsif (!exists($known_snapshot_formats{$format})) {
074afaa0 5012 die_error(400, "Unknown snapshot format");
5e166843 5013 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
074afaa0 5014 die_error(403, "Unsupported snapshot format");
ddb8d900
AK
5015 }
5016
cb9c6e5b
AK
5017 if (!defined $hash) {
5018 $hash = git_get_head_hash($project);
5019 }
5020
072570ee 5021 my $name = $project;
9a7d9410
ML
5022 $name =~ s,([^/])/*\.git$,$1,;
5023 $name = basename($name);
5024 my $filename = to_utf8($name);
072570ee 5025 $name =~ s/\047/\047\\\047\047/g;
072570ee 5026 my $cmd;
a3c8ab30 5027 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
516381d5
LW
5028 $cmd = quote_command(
5029 git_cmd(), 'archive',
5030 "--format=$known_snapshot_formats{$format}{'format'}",
5031 "--prefix=$name/", $hash);
a3c8ab30 5032 if (exists $known_snapshot_formats{$format}{'compressor'}) {
516381d5 5033 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
072570ee 5034 }
cb9c6e5b 5035
ab41dfbf 5036 print $cgi->header(
a3c8ab30 5037 -type => $known_snapshot_formats{$format}{'type'},
a2a3bf7b 5038 -content_disposition => 'inline; filename="' . "$filename" . '"',
ab41dfbf 5039 -status => '200 OK');
cb9c6e5b 5040
072570ee 5041 open my $fd, "-|", $cmd
074afaa0 5042 or die_error(500, "Execute git-archive failed");
cb9c6e5b
AK
5043 binmode STDOUT, ':raw';
5044 print <$fd>;
5045 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5046 close $fd;
cb9c6e5b
AK
5047}
5048
09bd7898 5049sub git_log {
847e01fb 5050 my $head = git_get_head_hash($project);
0db37973 5051 if (!defined $hash) {
19806691 5052 $hash = $head;
0db37973 5053 }
ea4a6df4
KS
5054 if (!defined $page) {
5055 $page = 0;
b87d78d6 5056 }
847e01fb 5057 my $refs = git_get_references();
ea4a6df4 5058
719dad28 5059 my @commitlist = parse_commits($hash, 101, (100 * $page));
ea4a6df4 5060
1f684dc0 5061 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
0d83ddc4 5062
75bf2cb2
GB
5063 my ($patch_max) = gitweb_get_feature('patches');
5064 if ($patch_max) {
5065 if ($patch_max < 0 || @commitlist <= $patch_max) {
5066 $paging_nav .= " &sdot; " .
5067 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5068 "patches");
5069 }
5070 }
5071
0d83ddc4 5072 git_header_html();
847e01fb 5073 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
0d83ddc4 5074
719dad28 5075 if (!@commitlist) {
847e01fb 5076 my %co = parse_commit($hash);
27fb8c40 5077
847e01fb 5078 git_print_header_div('summary', $project);
e925f38c 5079 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
161332a5 5080 }
719dad28
RF
5081 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5082 for (my $i = 0; $i <= $to; $i++) {
5083 my %co = %{$commitlist[$i]};
b87d78d6 5084 next if !%co;
719dad28
RF
5085 my $commit = $co{'id'};
5086 my $ref = format_ref_marker($refs, $commit);
847e01fb
JN
5087 my %ad = parse_date($co{'author_epoch'});
5088 git_print_header_div('commit',
26298b5f
JN
5089 "<span class=\"age\">$co{'age_string'}</span>" .
5090 esc_html($co{'title'}) . $ref,
5091 $commit);
034df39e
KS
5092 print "<div class=\"title_text\">\n" .
5093 "<div class=\"log_link\">\n" .
1c2a4f5a 5094 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
952c65fc
JN
5095 " | " .
5096 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
6ef4cb2e 5097 " | " .
d7267207 5098 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
eb28240b 5099 "<br/>\n" .
034df39e 5100 "</div>\n" .
40c13813 5101 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
d16d093c
JN
5102 "</div>\n";
5103
5104 print "<div class=\"log_body\">\n";
f2069411 5105 git_print_log($co{'comment'}, -final_empty_line=> 1);
09bd7898 5106 print "</div>\n";
719dad28
RF
5107 }
5108 if ($#commitlist >= 100) {
5109 print "<div class=\"page_nav\">\n";
7afd77bf 5110 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
719dad28
RF
5111 -accesskey => "n", -title => "Alt-n"}, "next");
5112 print "</div>\n";
e334d18c 5113 }
034df39e 5114 git_footer_html();
09bd7898
KS
5115}
5116
5117sub git_commit {
9954f772 5118 $hash ||= $hash_base || "HEAD";
074afaa0
LW
5119 my %co = parse_commit($hash)
5120 or die_error(404, "Unknown commit object");
847e01fb
JN
5121 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5122 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
161332a5 5123
c9d193df
JN
5124 my $parent = $co{'parent'};
5125 my $parents = $co{'parents'}; # listref
5126
5127 # we need to prepare $formats_nav before any parameter munging
5128 my $formats_nav;
5129 if (!defined $parent) {
5130 # --root commitdiff
5131 $formats_nav .= '(initial)';
5132 } elsif (@$parents == 1) {
5133 # single parent commit
5134 $formats_nav .=
5135 '(parent: ' .
5136 $cgi->a({-href => href(action=>"commit",
5137 hash=>$parent)},
5138 esc_html(substr($parent, 0, 7))) .
5139 ')';
5140 } else {
5141 # merge commit
5142 $formats_nav .=
5143 '(merge: ' .
5144 join(' ', map {
f9308a18 5145 $cgi->a({-href => href(action=>"commit",
c9d193df
JN
5146 hash=>$_)},
5147 esc_html(substr($_, 0, 7)));
5148 } @$parents ) .
5149 ')';
5150 }
75bf2cb2
GB
5151 if (gitweb_check_feature('patches')) {
5152 $formats_nav .= " | " .
5153 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5154 "patch");
5155 }
c9d193df 5156
d8a20ba9 5157 if (!defined $parent) {
b9182987 5158 $parent = "--root";
6191f8e1 5159 }
549ab4a3 5160 my @difftree;
208ecb2e
JN
5161 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5162 @diff_opts,
5163 (@$parents <= 1 ? $parent : '-c'),
5164 $hash, "--"
074afaa0 5165 or die_error(500, "Open git-diff-tree failed");
208ecb2e 5166 @difftree = map { chomp; $_ } <$fd>;
074afaa0 5167 close $fd or die_error(404, "Reading git-diff-tree failed");
11044297
KS
5168
5169 # non-textual hash id's can be cached
5170 my $expires;
5171 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5172 $expires = "+1d";
5173 }
847e01fb
JN
5174 my $refs = git_get_references();
5175 my $ref = format_ref_marker($refs, $co{'id'});
ddb8d900 5176
594e212b 5177 git_header_html(undef, $expires);
a144154f 5178 git_print_page_nav('commit', '',
952c65fc 5179 $hash, $co{'tree'}, $hash,
c9d193df 5180 $formats_nav);
4f7b34c9 5181
b87d78d6 5182 if (defined $co{'parent'}) {
847e01fb 5183 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
b87d78d6 5184 } else {
847e01fb 5185 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
b87d78d6 5186 }
6191f8e1 5187 print "<div class=\"title_text\">\n" .
591ebf65 5188 "<table class=\"object_header\">\n";
40c13813 5189 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
bddec01d
KS
5190 "<tr>" .
5191 "<td></td><td> $ad{'rfc2822'}";
927dcec4 5192 if ($ad{'hour_local'} < 6) {
952c65fc
JN
5193 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5194 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
b87d78d6 5195 } else {
952c65fc
JN
5196 printf(" (%02d:%02d %s)",
5197 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
b87d78d6 5198 }
bddec01d
KS
5199 print "</td>" .
5200 "</tr>\n";
40c13813 5201 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
952c65fc
JN
5202 print "<tr><td></td><td> $cd{'rfc2822'}" .
5203 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5204 "</td></tr>\n";
1f1ab5f0 5205 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
bddec01d
KS
5206 print "<tr>" .
5207 "<td>tree</td>" .
1f1ab5f0 5208 "<td class=\"sha1\">" .
952c65fc
JN
5209 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5210 class => "list"}, $co{'tree'}) .
19806691 5211 "</td>" .
952c65fc
JN
5212 "<td class=\"link\">" .
5213 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5214 "tree");
a3c8ab30
MM
5215 my $snapshot_links = format_snapshot_links($hash);
5216 if (defined $snapshot_links) {
5217 print " | " . $snapshot_links;
cb9c6e5b
AK
5218 }
5219 print "</td>" .
bddec01d 5220 "</tr>\n";
549ab4a3 5221
3e029299 5222 foreach my $par (@$parents) {
bddec01d
KS
5223 print "<tr>" .
5224 "<td>parent</td>" .
952c65fc
JN
5225 "<td class=\"sha1\">" .
5226 $cgi->a({-href => href(action=>"commit", hash=>$par),
5227 class => "list"}, $par) .
5228 "</td>" .
bddec01d 5229 "<td class=\"link\">" .
1c2a4f5a 5230 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
952c65fc 5231 " | " .
f2e60947 5232 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
bddec01d
KS
5233 "</td>" .
5234 "</tr>\n";
3e029299 5235 }
7a9b4c5f 5236 print "</table>".
b87d78d6 5237 "</div>\n";
d16d093c 5238
fbb592a9 5239 print "<div class=\"page_body\">\n";
d16d093c 5240 git_print_log($co{'comment'});
927dcec4 5241 print "</div>\n";
4a4a1a53 5242
208ecb2e 5243 git_difftree_body(\@difftree, $hash, @$parents);
4a4a1a53 5244
12a88f2f 5245 git_footer_html();
09bd7898
KS
5246}
5247
ca94601c
JN
5248sub git_object {
5249 # object is defined by:
5250 # - hash or hash_base alone
5251 # - hash_base and file_name
5252 my $type;
5253
5254 # - hash or hash_base alone
5255 if ($hash || ($hash_base && !defined $file_name)) {
5256 my $object_id = $hash || $hash_base;
5257
516381d5
LW
5258 open my $fd, "-|", quote_command(
5259 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
074afaa0 5260 or die_error(404, "Object does not exist");
ca94601c
JN
5261 $type = <$fd>;
5262 chomp $type;
5263 close $fd
074afaa0 5264 or die_error(404, "Object does not exist");
ca94601c
JN
5265
5266 # - hash_base and file_name
5267 } elsif ($hash_base && defined $file_name) {
5268 $file_name =~ s,/+$,,;
5269
5270 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
074afaa0 5271 or die_error(404, "Base object does not exist");
ca94601c
JN
5272
5273 # here errors should not hapen
5274 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
074afaa0 5275 or die_error(500, "Open git-ls-tree failed");
ca94601c
JN
5276 my $line = <$fd>;
5277 close $fd;
5278
5279 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5280 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
074afaa0 5281 die_error(404, "File or directory for given base does not exist");
ca94601c
JN
5282 }
5283 $type = $2;
5284 $hash = $3;
5285 } else {
074afaa0 5286 die_error(400, "Not enough information to find object");
ca94601c
JN
5287 }
5288
5289 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5290 hash=>$hash, hash_base=>$hash_base,
5291 file_name=>$file_name),
5292 -status => '302 Found');
5293}
5294
09bd7898 5295sub git_blobdiff {
9b71b1f6
JN
5296 my $format = shift || 'html';
5297
7c5e2ebb
JN
5298 my $fd;
5299 my @difftree;
5300 my %diffinfo;
9b71b1f6 5301 my $expires;
7c5e2ebb
JN
5302
5303 # preparing $fd and %diffinfo for git_patchset_body
5304 # new style URI
5305 if (defined $hash_base && defined $hash_parent_base) {
5306 if (defined $file_name) {
5307 # read raw output
45bd0c80
JN
5308 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5309 $hash_parent_base, $hash_base,
5ae917ac 5310 "--", (defined $file_parent ? $file_parent : ()), $file_name
074afaa0 5311 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
5312 @difftree = map { chomp; $_ } <$fd>;
5313 close $fd
074afaa0 5314 or die_error(404, "Reading git-diff-tree failed");
7c5e2ebb 5315 @difftree
074afaa0 5316 or die_error(404, "Blob diff not found");
7c5e2ebb 5317
0aea3376
JN
5318 } elsif (defined $hash &&
5319 $hash =~ /[0-9a-fA-F]{40}/) {
5320 # try to find filename from $hash
7c5e2ebb
JN
5321
5322 # read filtered raw output
45bd0c80
JN
5323 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5324 $hash_parent_base, $hash_base, "--"
074afaa0 5325 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
5326 @difftree =
5327 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5328 # $hash == to_id
5329 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5330 map { chomp; $_ } <$fd>;
5331 close $fd
074afaa0 5332 or die_error(404, "Reading git-diff-tree failed");
7c5e2ebb 5333 @difftree
074afaa0 5334 or die_error(404, "Blob diff not found");
7c5e2ebb
JN
5335
5336 } else {
074afaa0 5337 die_error(400, "Missing one of the blob diff parameters");
7c5e2ebb
JN
5338 }
5339
5340 if (@difftree > 1) {
074afaa0 5341 die_error(400, "Ambiguous blob diff specification");
7c5e2ebb
JN
5342 }
5343
5344 %diffinfo = parse_difftree_raw_line($difftree[0]);
9d301456
JN
5345 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5346 $file_name ||= $diffinfo{'to_file'};
7c5e2ebb
JN
5347
5348 $hash_parent ||= $diffinfo{'from_id'};
5349 $hash ||= $diffinfo{'to_id'};
5350
9b71b1f6
JN
5351 # non-textual hash id's can be cached
5352 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5353 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5354 $expires = '+1d';
5355 }
5356
7c5e2ebb 5357 # open patch output
25691fbe 5358 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
957d6ea7
JN
5359 '-p', ($format eq 'html' ? "--full-index" : ()),
5360 $hash_parent_base, $hash_base,
5ae917ac 5361 "--", (defined $file_parent ? $file_parent : ()), $file_name
074afaa0 5362 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
5363 }
5364
b54dc9fd
JH
5365 # old/legacy style URI -- not generated anymore since 1.4.3.
5366 if (!%diffinfo) {
5367 die_error('404 Not Found', "Missing one of the blob diff parameters")
7c5e2ebb
JN
5368 }
5369
5370 # header
9b71b1f6
JN
5371 if ($format eq 'html') {
5372 my $formats_nav =
a3823e5a 5373 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
35329cc1 5374 "raw");
9b71b1f6
JN
5375 git_header_html(undef, $expires);
5376 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5377 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5378 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5379 } else {
5380 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5381 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5382 }
5383 if (defined $file_name) {
5384 git_print_page_path($file_name, "blob", $hash_base);
5385 } else {
5386 print "<div class=\"page_path\"></div>\n";
5387 }
5388
5389 } elsif ($format eq 'plain') {
5390 print $cgi->header(
5391 -type => 'text/plain',
5392 -charset => 'utf-8',
5393 -expires => $expires,
a2a3bf7b 5394 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
9b71b1f6
JN
5395
5396 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5397
7c5e2ebb 5398 } else {
074afaa0 5399 die_error(400, "Unknown blobdiff format");
7c5e2ebb
JN
5400 }
5401
5402 # patch
9b71b1f6
JN
5403 if ($format eq 'html') {
5404 print "<div class=\"page_body\">\n";
7c5e2ebb 5405
9b71b1f6
JN
5406 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5407 close $fd;
7c5e2ebb 5408
9b71b1f6
JN
5409 print "</div>\n"; # class="page_body"
5410 git_footer_html();
5411
5412 } else {
5413 while (my $line = <$fd>) {
403d0906
JN
5414 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5415 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9b71b1f6
JN
5416
5417 print $line;
5418
5419 last if $line =~ m!^\+\+\+!;
5420 }
5421 local $/ = undef;
5422 print <$fd>;
5423 close $fd;
5424 }
09bd7898
KS
5425}
5426
19806691 5427sub git_blobdiff_plain {
9b71b1f6 5428 git_blobdiff('plain');
19806691
KS
5429}
5430
09bd7898 5431sub git_commitdiff {
20209854
GB
5432 my %params = @_;
5433 my $format = $params{-format} || 'html';
9872cd6f 5434
75bf2cb2 5435 my ($patch_max) = gitweb_get_feature('patches');
9872cd6f 5436 if ($format eq 'patch') {
9872cd6f
GB
5437 die_error(403, "Patch view not allowed") unless $patch_max;
5438 }
5439
9954f772 5440 $hash ||= $hash_base || "HEAD";
074afaa0
LW
5441 my %co = parse_commit($hash)
5442 or die_error(404, "Unknown commit object");
151602df 5443
cd030c3a
JN
5444 # choose format for commitdiff for merge
5445 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5446 $hash_parent = '--cc';
5447 }
5448 # we need to prepare $formats_nav before almost any parameter munging
151602df
JN
5449 my $formats_nav;
5450 if ($format eq 'html') {
5451 $formats_nav =
a3823e5a 5452 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
151602df 5453 "raw");
75bf2cb2
GB
5454 if ($patch_max) {
5455 $formats_nav .= " | " .
5456 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5457 "patch");
5458 }
151602df 5459
cd030c3a
JN
5460 if (defined $hash_parent &&
5461 $hash_parent ne '-c' && $hash_parent ne '--cc') {
151602df
JN
5462 # commitdiff with two commits given
5463 my $hash_parent_short = $hash_parent;
5464 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5465 $hash_parent_short = substr($hash_parent, 0, 7);
5466 }
5467 $formats_nav .=
ada3e1f7
JN
5468 ' (from';
5469 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5470 if ($co{'parents'}[$i] eq $hash_parent) {
5471 $formats_nav .= ' parent ' . ($i+1);
5472 last;
5473 }
5474 }
5475 $formats_nav .= ': ' .
151602df
JN
5476 $cgi->a({-href => href(action=>"commitdiff",
5477 hash=>$hash_parent)},
5478 esc_html($hash_parent_short)) .
5479 ')';
5480 } elsif (!$co{'parent'}) {
5481 # --root commitdiff
5482 $formats_nav .= ' (initial)';
5483 } elsif (scalar @{$co{'parents'}} == 1) {
5484 # single parent commit
5485 $formats_nav .=
5486 ' (parent: ' .
5487 $cgi->a({-href => href(action=>"commitdiff",
5488 hash=>$co{'parent'})},
5489 esc_html(substr($co{'parent'}, 0, 7))) .
5490 ')';
5491 } else {
5492 # merge commit
cd030c3a
JN
5493 if ($hash_parent eq '--cc') {
5494 $formats_nav .= ' | ' .
5495 $cgi->a({-href => href(action=>"commitdiff",
5496 hash=>$hash, hash_parent=>'-c')},
5497 'combined');
5498 } else { # $hash_parent eq '-c'
5499 $formats_nav .= ' | ' .
5500 $cgi->a({-href => href(action=>"commitdiff",
5501 hash=>$hash, hash_parent=>'--cc')},
5502 'compact');
5503 }
151602df
JN
5504 $formats_nav .=
5505 ' (merge: ' .
5506 join(' ', map {
5507 $cgi->a({-href => href(action=>"commitdiff",
5508 hash=>$_)},
5509 esc_html(substr($_, 0, 7)));
5510 } @{$co{'parents'}} ) .
5511 ')';
5512 }
5513 }
5514
fb1dde4a 5515 my $hash_parent_param = $hash_parent;
cd030c3a
JN
5516 if (!defined $hash_parent_param) {
5517 # --cc for multiple parents, --root for parentless
fb1dde4a 5518 $hash_parent_param =
cd030c3a 5519 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
bddec01d 5520 }
eee08903
JN
5521
5522 # read commitdiff
5523 my $fd;
5524 my @difftree;
eee08903 5525 if ($format eq 'html') {
25691fbe 5526 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
45bd0c80 5527 "--no-commit-id", "--patch-with-raw", "--full-index",
fb1dde4a 5528 $hash_parent_param, $hash, "--"
074afaa0 5529 or die_error(500, "Open git-diff-tree failed");
eee08903 5530
04408c35
JN
5531 while (my $line = <$fd>) {
5532 chomp $line;
eee08903
JN
5533 # empty line ends raw part of diff-tree output
5534 last unless $line;
493e01db 5535 push @difftree, scalar parse_difftree_raw_line($line);
eee08903 5536 }
eee08903 5537
eee08903 5538 } elsif ($format eq 'plain') {
25691fbe 5539 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
fb1dde4a 5540 '-p', $hash_parent_param, $hash, "--"
074afaa0 5541 or die_error(500, "Open git-diff-tree failed");
9872cd6f
GB
5542 } elsif ($format eq 'patch') {
5543 # For commit ranges, we limit the output to the number of
5544 # patches specified in the 'patches' feature.
5545 # For single commits, we limit the output to a single patch,
5546 # diverging from the git-format-patch default.
5547 my @commit_spec = ();
5548 if ($hash_parent) {
5549 if ($patch_max > 0) {
5550 push @commit_spec, "-$patch_max";
5551 }
5552 push @commit_spec, '-n', "$hash_parent..$hash";
5553 } else {
a3411f8a
GB
5554 if ($params{-single}) {
5555 push @commit_spec, '-1';
5556 } else {
5557 if ($patch_max > 0) {
5558 push @commit_spec, "-$patch_max";
5559 }
5560 push @commit_spec, "-n";
5561 }
5562 push @commit_spec, '--root', $hash;
9872cd6f
GB
5563 }
5564 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5565 '--stdout', @commit_spec
5566 or die_error(500, "Open git-format-patch failed");
eee08903 5567 } else {
074afaa0 5568 die_error(400, "Unknown commitdiff format");
eee08903 5569 }
161332a5 5570
11044297
KS
5571 # non-textual hash id's can be cached
5572 my $expires;
5573 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5574 $expires = "+1d";
5575 }
09bd7898 5576
eee08903
JN
5577 # write commit message
5578 if ($format eq 'html') {
5579 my $refs = git_get_references();
5580 my $ref = format_ref_marker($refs, $co{'id'});
1b1cd421 5581
eee08903
JN
5582 git_header_html(undef, $expires);
5583 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5584 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6fd92a28 5585 git_print_authorship(\%co);
eee08903 5586 print "<div class=\"page_body\">\n";
82560983
JN
5587 if (@{$co{'comment'}} > 1) {
5588 print "<div class=\"log\">\n";
5589 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5590 print "</div>\n"; # class="log"
5591 }
eee08903
JN
5592
5593 } elsif ($format eq 'plain') {
5594 my $refs = git_get_references("tags");
edf735ab 5595 my $tagname = git_get_rev_name_tags($hash);
eee08903
JN
5596 my $filename = basename($project) . "-$hash.patch";
5597
5598 print $cgi->header(
5599 -type => 'text/plain',
5600 -charset => 'utf-8',
5601 -expires => $expires,
a2a3bf7b 5602 -content_disposition => 'inline; filename="' . "$filename" . '"');
eee08903 5603 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7720224c
YS
5604 print "From: " . to_utf8($co{'author'}) . "\n";
5605 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5606 print "Subject: " . to_utf8($co{'title'}) . "\n";
5607
edf735ab 5608 print "X-Git-Tag: $tagname\n" if $tagname;
eee08903 5609 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
edf735ab 5610
eee08903 5611 foreach my $line (@{$co{'comment'}}) {
7720224c 5612 print to_utf8($line) . "\n";
eee08903
JN
5613 }
5614 print "---\n\n";
9872cd6f
GB
5615 } elsif ($format eq 'patch') {
5616 my $filename = basename($project) . "-$hash.patch";
5617
5618 print $cgi->header(
5619 -type => 'text/plain',
5620 -charset => 'utf-8',
5621 -expires => $expires,
5622 -content_disposition => 'inline; filename="' . "$filename" . '"');
1b1cd421 5623 }
1b1cd421 5624
eee08903
JN
5625 # write patch
5626 if ($format eq 'html') {
cd030c3a
JN
5627 my $use_parents = !defined $hash_parent ||
5628 $hash_parent eq '-c' || $hash_parent eq '--cc';
5629 git_difftree_body(\@difftree, $hash,
5630 $use_parents ? @{$co{'parents'}} : $hash_parent);
b4657e77 5631 print "<br/>\n";
1b1cd421 5632
cd030c3a
JN
5633 git_patchset_body($fd, \@difftree, $hash,
5634 $use_parents ? @{$co{'parents'}} : $hash_parent);
157e43b4 5635 close $fd;
eee08903
JN
5636 print "</div>\n"; # class="page_body"
5637 git_footer_html();
5638
5639 } elsif ($format eq 'plain') {
5640 local $/ = undef;
5641 print <$fd>;
5642 close $fd
5643 or print "Reading git-diff-tree failed\n";
9872cd6f
GB
5644 } elsif ($format eq 'patch') {
5645 local $/ = undef;
5646 print <$fd>;
5647 close $fd
5648 or print "Reading git-format-patch failed\n";
19806691
KS
5649 }
5650}
5651
eee08903 5652sub git_commitdiff_plain {
20209854 5653 git_commitdiff(-format => 'plain');
eee08903
JN
5654}
5655
9872cd6f
GB
5656# format-patch-style patches
5657sub git_patch {
a3411f8a
GB
5658 git_commitdiff(-format => 'patch', -single=> 1);
5659}
5660
5661sub git_patches {
20209854 5662 git_commitdiff(-format => 'patch');
eee08903
JN
5663}
5664
09bd7898 5665sub git_history {
c6e1d9ed 5666 if (!defined $hash_base) {
847e01fb 5667 $hash_base = git_get_head_hash($project);
09bd7898 5668 }
8be68352
JN
5669 if (!defined $page) {
5670 $page = 0;
5671 }
63433106 5672 my $ftype;
074afaa0
LW
5673 my %co = parse_commit($hash_base)
5674 or die_error(404, "Unknown commit object");
8be68352 5675
847e01fb 5676 my $refs = git_get_references();
8be68352
JN
5677 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5678
5634cf24 5679 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
074afaa0
LW
5680 $file_name, "--full-history")
5681 or die_error(404, "No such file or directory on given branch");
5634cf24 5682
93d5f061 5683 if (!defined $hash && defined $file_name) {
5634cf24
JN
5684 # some commits could have deleted file in question,
5685 # and not have it in tree, but one of them has to have it
5686 for (my $i = 0; $i <= @commitlist; $i++) {
5687 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5688 last if defined $hash;
5689 }
93d5f061 5690 }
cff0771b 5691 if (defined $hash) {
63433106 5692 $ftype = git_get_type($hash);
cff0771b 5693 }
5634cf24 5694 if (!defined $ftype) {
074afaa0 5695 die_error(500, "Unknown type of object");
5634cf24 5696 }
25691fbe 5697
8be68352
JN
5698 my $paging_nav = '';
5699 if ($page > 0) {
5700 $paging_nav .=
5701 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5702 file_name=>$file_name)},
5703 "first");
5704 $paging_nav .= " &sdot; " .
7afd77bf 5705 $cgi->a({-href => href(-replay=>1, page=>$page-1),
8be68352
JN
5706 -accesskey => "p", -title => "Alt-p"}, "prev");
5707 } else {
5708 $paging_nav .= "first";
5709 $paging_nav .= " &sdot; prev";
5710 }
8be68352 5711 my $next_link = '';
a8b983bf 5712 if ($#commitlist >= 100) {
8be68352 5713 $next_link =
7afd77bf 5714 $cgi->a({-href => href(-replay=>1, page=>$page+1),
a8b983bf 5715 -accesskey => "n", -title => "Alt-n"}, "next");
7afd77bf
JN
5716 $paging_nav .= " &sdot; $next_link";
5717 } else {
5718 $paging_nav .= " &sdot; next";
8be68352
JN
5719 }
5720
5721 git_header_html();
5722 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5723 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5724 git_print_page_path($file_name, $ftype, $hash_base);
5725
a8b983bf 5726 git_history_body(\@commitlist, 0, 99,
8be68352 5727 $refs, $hash_base, $ftype, $next_link);
581860e1 5728
d51e902a 5729 git_footer_html();
161332a5 5730}
19806691
KS
5731
5732sub git_search {
25b2790f 5733 gitweb_check_feature('search') or die_error(403, "Search is disabled");
19806691 5734 if (!defined $searchtext) {
074afaa0 5735 die_error(400, "Text field is empty");
19806691
KS
5736 }
5737 if (!defined $hash) {
847e01fb 5738 $hash = git_get_head_hash($project);
19806691 5739 }
847e01fb 5740 my %co = parse_commit($hash);
19806691 5741 if (!%co) {
074afaa0 5742 die_error(404, "Unknown commit object");
19806691 5743 }
8dbc0fce
RF
5744 if (!defined $page) {
5745 $page = 0;
5746 }
04f7a94f 5747
88ad729b
PB
5748 $searchtype ||= 'commit';
5749 if ($searchtype eq 'pickaxe') {
04f7a94f
JN
5750 # pickaxe may take all resources of your box and run for several minutes
5751 # with every query - so decide by yourself how public you make this feature
25b2790f 5752 gitweb_check_feature('pickaxe')
074afaa0 5753 or die_error(403, "Pickaxe is disabled");
c994d620 5754 }
e7738553 5755 if ($searchtype eq 'grep') {
25b2790f 5756 gitweb_check_feature('grep')
074afaa0 5757 or die_error(403, "Grep is disabled");
e7738553 5758 }
88ad729b 5759
19806691 5760 git_header_html();
19806691 5761
88ad729b 5762 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
8e574fb5
RF
5763 my $greptype;
5764 if ($searchtype eq 'commit') {
5765 $greptype = "--grep=";
5766 } elsif ($searchtype eq 'author') {
5767 $greptype = "--author=";
5768 } elsif ($searchtype eq 'committer') {
5769 $greptype = "--committer=";
5770 }
0270cd0e
JN
5771 $greptype .= $searchtext;
5772 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
0e559919
PB
5773 $greptype, '--regexp-ignore-case',
5774 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
8dbc0fce
RF
5775
5776 my $paging_nav = '';
5777 if ($page > 0) {
5778 $paging_nav .=
5779 $cgi->a({-href => href(action=>"search", hash=>$hash,
0270cd0e
JN
5780 searchtext=>$searchtext,
5781 searchtype=>$searchtype)},
a23f0a73 5782 "first");
8dbc0fce 5783 $paging_nav .= " &sdot; " .
7afd77bf 5784 $cgi->a({-href => href(-replay=>1, page=>$page-1),
a23f0a73 5785 -accesskey => "p", -title => "Alt-p"}, "prev");
8dbc0fce
RF
5786 } else {
5787 $paging_nav .= "first";
5788 $paging_nav .= " &sdot; prev";
5789 }
7afd77bf 5790 my $next_link = '';
5ad66088 5791 if ($#commitlist >= 100) {
7afd77bf
JN
5792 $next_link =
5793 $cgi->a({-href => href(-replay=>1, page=>$page+1),
a23f0a73 5794 -accesskey => "n", -title => "Alt-n"}, "next");
7afd77bf 5795 $paging_nav .= " &sdot; $next_link";
8dbc0fce
RF
5796 } else {
5797 $paging_nav .= " &sdot; next";
5798 }
7afd77bf 5799
5ad66088 5800 if ($#commitlist >= 100) {
8dbc0fce
RF
5801 }
5802
5803 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5804 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5ad66088 5805 git_search_grep_body(\@commitlist, 0, 99, $next_link);
c994d620
KS
5806 }
5807
88ad729b 5808 if ($searchtype eq 'pickaxe') {
8dbc0fce
RF
5809 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5810 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5811
591ebf65 5812 print "<table class=\"pickaxe search\">\n";
8dbc0fce 5813 my $alternate = 1;
c994d620 5814 $/ = "\n";
c582abae
JN
5815 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5816 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5817 ($search_use_regexp ? '--pickaxe-regex' : ());
c994d620
KS
5818 undef %co;
5819 my @files;
5820 while (my $line = <$fd>) {
c582abae
JN
5821 chomp $line;
5822 next unless $line;
5823
5824 my %set = parse_difftree_raw_line($line);
5825 if (defined $set{'commit'}) {
5826 # finish previous commit
c994d620 5827 if (%co) {
c994d620
KS
5828 print "</td>\n" .
5829 "<td class=\"link\">" .
756d2f06 5830 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
952c65fc
JN
5831 " | " .
5832 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
c994d620
KS
5833 print "</td>\n" .
5834 "</tr>\n";
5835 }
c582abae
JN
5836
5837 if ($alternate) {
5838 print "<tr class=\"dark\">\n";
5839 } else {
5840 print "<tr class=\"light\">\n";
5841 }
5842 $alternate ^= 1;
5843 %co = parse_commit($set{'commit'});
5844 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5845 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5846 "<td><i>$author</i></td>\n" .
5847 "<td>" .
5848 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5849 -class => "list subject"},
5850 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5851 } elsif (defined $set{'to_id'}) {
5852 next if ($set{'to_id'} =~ m/^0{40}$/);
5853
5854 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5855 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5856 -class => "list"},
5857 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5858 "<br/>\n";
19806691 5859 }
19806691 5860 }
c994d620 5861 close $fd;
8dbc0fce 5862
c582abae
JN
5863 # finish last commit (warning: repetition!)
5864 if (%co) {
5865 print "</td>\n" .
5866 "<td class=\"link\">" .
5867 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5868 " | " .
5869 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5870 print "</td>\n" .
5871 "</tr>\n";
5872 }
5873
8dbc0fce 5874 print "</table>\n";
19806691 5875 }
e7738553
PB
5876
5877 if ($searchtype eq 'grep') {
5878 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5879 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5880
591ebf65 5881 print "<table class=\"grep_search\">\n";
e7738553
PB
5882 my $alternate = 1;
5883 my $matches = 0;
5884 $/ = "\n";
0e559919
PB
5885 open my $fd, "-|", git_cmd(), 'grep', '-n',
5886 $search_use_regexp ? ('-E', '-i') : '-F',
5887 $searchtext, $co{'tree'};
e7738553
PB
5888 my $lastfile = '';
5889 while (my $line = <$fd>) {
5890 chomp $line;
5891 my ($file, $lno, $ltext, $binary);
5892 last if ($matches++ > 1000);
5893 if ($line =~ /^Binary file (.+) matches$/) {
5894 $file = $1;
5895 $binary = 1;
5896 } else {
5897 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5898 }
5899 if ($file ne $lastfile) {
5900 $lastfile and print "</td></tr>\n";
5901 if ($alternate++) {
5902 print "<tr class=\"dark\">\n";
5903 } else {
5904 print "<tr class=\"light\">\n";
5905 }
5906 print "<td class=\"list\">".
5907 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5908 file_name=>"$file"),
5909 -class => "list"}, esc_path($file));
5910 print "</td><td>\n";
5911 $lastfile = $file;
5912 }
5913 if ($binary) {
5914 print "<div class=\"binary\">Binary file</div>\n";
5915 } else {
5916 $ltext = untabify($ltext);
0e559919 5917 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
e7738553
PB
5918 $ltext = esc_html($1, -nbsp=>1);
5919 $ltext .= '<span class="match">';
5920 $ltext .= esc_html($2, -nbsp=>1);
5921 $ltext .= '</span>';
5922 $ltext .= esc_html($3, -nbsp=>1);
5923 } else {
5924 $ltext = esc_html($ltext, -nbsp=>1);
5925 }
5926 print "<div class=\"pre\">" .
5927 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5928 file_name=>"$file").'#l'.$lno,
5929 -class => "linenr"}, sprintf('%4i', $lno))
5930 . ' ' . $ltext . "</div>\n";
5931 }
5932 }
5933 if ($lastfile) {
5934 print "</td></tr>\n";
5935 if ($matches > 1000) {
5936 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5937 }
5938 } else {
5939 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5940 }
5941 close $fd;
5942
5943 print "</table>\n";
5944 }
19806691
KS
5945 git_footer_html();
5946}
5947
88ad729b
PB
5948sub git_search_help {
5949 git_header_html();
5950 git_print_page_nav('','', $hash,$hash,$hash);
5951 print <<EOT;
0e559919
PB
5952<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5953regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5954the pattern entered is recognized as the POSIX extended
5955<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5956insensitive).</p>
88ad729b
PB
5957<dl>
5958<dt><b>commit</b></dt>
0e559919 5959<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
e7738553 5960EOT
25b2790f 5961 my $have_grep = gitweb_check_feature('grep');
e7738553
PB
5962 if ($have_grep) {
5963 print <<EOT;
5964<dt><b>grep</b></dt>
5965<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
0e559919
PB
5966 a different one) are searched for the given pattern. On large trees, this search can take
5967a while and put some strain on the server, so please use it with some consideration. Note that
5968due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5969case-sensitive.</dd>
e7738553
PB
5970EOT
5971 }
5972 print <<EOT;
88ad729b 5973<dt><b>author</b></dt>
0e559919 5974<dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
88ad729b 5975<dt><b>committer</b></dt>
0e559919 5976<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
88ad729b 5977EOT
25b2790f 5978 my $have_pickaxe = gitweb_check_feature('pickaxe');
88ad729b
PB
5979 if ($have_pickaxe) {
5980 print <<EOT;
5981<dt><b>pickaxe</b></dt>
5982<dd>All commits that caused the string to appear or disappear from any file (changes that
5983added, removed or "modified" the string) will be listed. This search can take a while and
0e559919
PB
5984takes a lot of strain on the server, so please use it wisely. Note that since you may be
5985interested even in changes just changing the case as well, this search is case sensitive.</dd>
88ad729b
PB
5986EOT
5987 }
5988 print "</dl>\n";
5989 git_footer_html();
5990}
5991
19806691 5992sub git_shortlog {
847e01fb 5993 my $head = git_get_head_hash($project);
19806691
KS
5994 if (!defined $hash) {
5995 $hash = $head;
5996 }
ea4a6df4
KS
5997 if (!defined $page) {
5998 $page = 0;
5999 }
847e01fb 6000 my $refs = git_get_references();
ea4a6df4 6001
ec3e97b8
GB
6002 my $commit_hash = $hash;
6003 if (defined $hash_parent) {
6004 $commit_hash = "$hash_parent..$hash";
6005 }
6006 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
ea4a6df4 6007
1f684dc0 6008 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
9f5dcb81 6009 my $next_link = '';
190d7fdc 6010 if ($#commitlist >= 100) {
9f5dcb81 6011 $next_link =
7afd77bf 6012 $cgi->a({-href => href(-replay=>1, page=>$page+1),
190d7fdc 6013 -accesskey => "n", -title => "Alt-n"}, "next");
9f5dcb81 6014 }
75bf2cb2
GB
6015 my $patch_max = gitweb_check_feature('patches');
6016 if ($patch_max) {
6017 if ($patch_max < 0 || @commitlist <= $patch_max) {
6018 $paging_nav .= " &sdot; " .
6019 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6020 "patches");
6021 }
6022 }
9f5dcb81 6023
0d83ddc4 6024 git_header_html();
847e01fb
JN
6025 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6026 git_print_header_div('summary', $project);
0d83ddc4 6027
190d7fdc 6028 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
9f5dcb81 6029
19806691
KS
6030 git_footer_html();
6031}
717b8311
JN
6032
6033## ......................................................................
af6feeb2 6034## feeds (RSS, Atom; OPML)
717b8311 6035
af6feeb2
JN
6036sub git_feed {
6037 my $format = shift || 'atom';
25b2790f 6038 my $have_blame = gitweb_check_feature('blame');
af6feeb2
JN
6039
6040 # Atom: http://www.atomenabled.org/developers/syndication/
6041 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6042 if ($format ne 'rss' && $format ne 'atom') {
074afaa0 6043 die_error(400, "Unknown web feed format");
af6feeb2
JN
6044 }
6045
6046 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6047 my $head = $hash || 'HEAD';
311e552e 6048 my @commitlist = parse_commits($head, 150, 0, $file_name);
af6feeb2
JN
6049
6050 my %latest_commit;
6051 my %latest_date;
6052 my $content_type = "application/$format+xml";
6053 if (defined $cgi->http('HTTP_ACCEPT') &&
6054 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6055 # browser (feed reader) prefers text/xml
6056 $content_type = 'text/xml';
6057 }
b6093a5c
RF
6058 if (defined($commitlist[0])) {
6059 %latest_commit = %{$commitlist[0]};
cd956c73
GB
6060 my $latest_epoch = $latest_commit{'committer_epoch'};
6061 %latest_date = parse_date($latest_epoch);
6062 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6063 if (defined $if_modified) {
6064 my $since;
6065 if (eval { require HTTP::Date; 1; }) {
6066 $since = HTTP::Date::str2time($if_modified);
6067 } elsif (eval { require Time::ParseDate; 1; }) {
6068 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6069 }
6070 if (defined $since && $latest_epoch <= $since) {
6071 print $cgi->header(
6072 -type => $content_type,
6073 -charset => 'utf-8',
6074 -last_modified => $latest_date{'rfc2822'},
6075 -status => '304 Not Modified');
6076 return;
6077 }
6078 }
af6feeb2
JN
6079 print $cgi->header(
6080 -type => $content_type,
6081 -charset => 'utf-8',
6082 -last_modified => $latest_date{'rfc2822'});
6083 } else {
6084 print $cgi->header(
6085 -type => $content_type,
6086 -charset => 'utf-8');
6087 }
6088
6089 # Optimization: skip generating the body if client asks only
6090 # for Last-Modified date.
6091 return if ($cgi->request_method() eq 'HEAD');
6092
6093 # header variables
6094 my $title = "$site_name - $project/$action";
6095 my $feed_type = 'log';
6096 if (defined $hash) {
6097 $title .= " - '$hash'";
6098 $feed_type = 'branch log';
6099 if (defined $file_name) {
6100 $title .= " :: $file_name";
6101 $feed_type = 'history';
6102 }
6103 } elsif (defined $file_name) {
6104 $title .= " - $file_name";
6105 $feed_type = 'history';
6106 }
6107 $title .= " $feed_type";
6108 my $descr = git_get_project_description($project);
6109 if (defined $descr) {
6110 $descr = esc_html($descr);
6111 } else {
6112 $descr = "$project " .
6113 ($format eq 'rss' ? 'RSS' : 'Atom') .
6114 " feed";
6115 }
6116 my $owner = git_get_project_owner($project);
6117 $owner = esc_html($owner);
6118
6119 #header
6120 my $alt_url;
6121 if (defined $file_name) {
6122 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6123 } elsif (defined $hash) {
6124 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6125 } else {
6126 $alt_url = href(-full=>1, action=>"summary");
6127 }
6128 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6129 if ($format eq 'rss') {
6130 print <<XML;
59b9f61a
JN
6131<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6132<channel>
59b9f61a 6133XML
af6feeb2
JN
6134 print "<title>$title</title>\n" .
6135 "<link>$alt_url</link>\n" .
6136 "<description>$descr</description>\n" .
3ac109ae
GB
6137 "<language>en</language>\n" .
6138 # project owner is responsible for 'editorial' content
6139 "<managingEditor>$owner</managingEditor>\n";
1ba68ce2
GB
6140 if (defined $logo || defined $favicon) {
6141 # prefer the logo to the favicon, since RSS
6142 # doesn't allow both
6143 my $img = esc_url($logo || $favicon);
6144 print "<image>\n" .
6145 "<url>$img</url>\n" .
6146 "<title>$title</title>\n" .
6147 "<link>$alt_url</link>\n" .
6148 "</image>\n";
6149 }
0cf31285
GB
6150 if (%latest_date) {
6151 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6152 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6153 }
ad59a7a3 6154 print "<generator>gitweb v.$version/$git_version</generator>\n";
af6feeb2
JN
6155 } elsif ($format eq 'atom') {
6156 print <<XML;
6157<feed xmlns="http://www.w3.org/2005/Atom">
6158XML
6159 print "<title>$title</title>\n" .
6160 "<subtitle>$descr</subtitle>\n" .
6161 '<link rel="alternate" type="text/html" href="' .
6162 $alt_url . '" />' . "\n" .
6163 '<link rel="self" type="' . $content_type . '" href="' .
6164 $cgi->self_url() . '" />' . "\n" .
6165 "<id>" . href(-full=>1) . "</id>\n" .
6166 # use project owner for feed author
6167 "<author><name>$owner</name></author>\n";
6168 if (defined $favicon) {
6169 print "<icon>" . esc_url($favicon) . "</icon>\n";
6170 }
6171 if (defined $logo_url) {
6172 # not twice as wide as tall: 72 x 27 pixels
e1147267 6173 print "<logo>" . esc_url($logo) . "</logo>\n";
af6feeb2
JN
6174 }
6175 if (! %latest_date) {
6176 # dummy date to keep the feed valid until commits trickle in:
6177 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6178 } else {
6179 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6180 }
ad59a7a3 6181 print "<generator version='$version/$git_version'>gitweb</generator>\n";
af6feeb2 6182 }
717b8311 6183
af6feeb2 6184 # contents
b6093a5c
RF
6185 for (my $i = 0; $i <= $#commitlist; $i++) {
6186 my %co = %{$commitlist[$i]};
6187 my $commit = $co{'id'};
717b8311 6188 # we read 150, we always show 30 and the ones more recent than 48 hours
91fd2bf3 6189 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
717b8311
JN
6190 last;
6191 }
91fd2bf3 6192 my %cd = parse_date($co{'author_epoch'});
af6feeb2
JN
6193
6194 # get list of changed files
b6093a5c 6195 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
c906b181
JN
6196 $co{'parent'} || "--root",
6197 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6bcf4b46 6198 or next;
717b8311 6199 my @difftree = map { chomp; $_ } <$fd>;
6bcf4b46
JN
6200 close $fd
6201 or next;
af6feeb2
JN
6202
6203 # print element (entry, item)
e62a641d 6204 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
af6feeb2
JN
6205 if ($format eq 'rss') {
6206 print "<item>\n" .
6207 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6208 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6209 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6210 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6211 "<link>$co_url</link>\n" .
6212 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6213 "<content:encoded>" .
6214 "<![CDATA[\n";
6215 } elsif ($format eq 'atom') {
6216 print "<entry>\n" .
6217 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6218 "<updated>$cd{'iso-8601'}</updated>\n" .
ab23c19d
JN
6219 "<author>\n" .
6220 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6221 if ($co{'author_email'}) {
6222 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6223 }
6224 print "</author>\n" .
af6feeb2 6225 # use committer for contributor
ab23c19d
JN
6226 "<contributor>\n" .
6227 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6228 if ($co{'committer_email'}) {
6229 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6230 }
6231 print "</contributor>\n" .
af6feeb2
JN
6232 "<published>$cd{'iso-8601'}</published>\n" .
6233 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6234 "<id>$co_url</id>\n" .
6235 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6236 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6237 }
717b8311 6238 my $comment = $co{'comment'};
af6feeb2 6239 print "<pre>\n";
717b8311 6240 foreach my $line (@$comment) {
af6feeb2
JN
6241 $line = esc_html($line);
6242 print "$line\n";
717b8311 6243 }
af6feeb2
JN
6244 print "</pre><ul>\n";
6245 foreach my $difftree_line (@difftree) {
6246 my %difftree = parse_difftree_raw_line($difftree_line);
6247 next if !$difftree{'from_id'};
6248
6249 my $file = $difftree{'file'} || $difftree{'to_file'};
6250
6251 print "<li>" .
6252 "[" .
6253 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6254 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6255 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6256 file_name=>$file, file_parent=>$difftree{'from_file'}),
6257 -title => "diff"}, 'D');
6258 if ($have_blame) {
6259 print $cgi->a({-href => href(-full=>1, action=>"blame",
6260 file_name=>$file, hash_base=>$commit),
6261 -title => "blame"}, 'B');
717b8311 6262 }
af6feeb2
JN
6263 # if this is not a feed of a file history
6264 if (!defined $file_name || $file_name ne $file) {
6265 print $cgi->a({-href => href(-full=>1, action=>"history",
6266 file_name=>$file, hash=>$commit),
6267 -title => "history"}, 'H');
6268 }
6269 $file = esc_path($file);
6270 print "] ".
6271 "$file</li>\n";
6272 }
6273 if ($format eq 'rss') {
6274 print "</ul>]]>\n" .
6275 "</content:encoded>\n" .
6276 "</item>\n";
6277 } elsif ($format eq 'atom') {
6278 print "</ul>\n</div>\n" .
6279 "</content>\n" .
6280 "</entry>\n";
717b8311 6281 }
717b8311 6282 }
af6feeb2
JN
6283
6284 # end of feed
6285 if ($format eq 'rss') {
6286 print "</channel>\n</rss>\n";
6287 } elsif ($format eq 'atom') {
6288 print "</feed>\n";
6289 }
6290}
6291
6292sub git_rss {
6293 git_feed('rss');
6294}
6295
6296sub git_atom {
6297 git_feed('atom');
717b8311
JN
6298}
6299
6300sub git_opml {
847e01fb 6301 my @list = git_get_projects_list();
717b8311 6302
ae35785e
GB
6303 print $cgi->header(
6304 -type => 'text/xml',
6305 -charset => 'utf-8',
6306 -content_disposition => 'inline; filename="opml.xml"');
6307
59b9f61a
JN
6308 print <<XML;
6309<?xml version="1.0" encoding="utf-8"?>
6310<opml version="1.0">
6311<head>
8be2890c 6312 <title>$site_name OPML Export</title>
59b9f61a
JN
6313</head>
6314<body>
6315<outline text="git RSS feeds">
6316XML
717b8311
JN
6317
6318 foreach my $pr (@list) {
6319 my %proj = %$pr;
847e01fb 6320 my $head = git_get_head_hash($proj{'path'});
717b8311
JN
6321 if (!defined $head) {
6322 next;
6323 }
25691fbe 6324 $git_dir = "$projectroot/$proj{'path'}";
847e01fb 6325 my %co = parse_commit($head);
717b8311
JN
6326 if (!%co) {
6327 next;
6328 }
6329
6330 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
df63fbbf
GB
6331 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6332 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
717b8311
JN
6333 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6334 }
59b9f61a
JN
6335 print <<XML;
6336</outline>
6337</body>
6338</opml>
6339XML
717b8311 6340}