]> git.ipfire.org Git - thirdparty/git.git/blame - gitweb/gitweb.perl
gitweb: Always use three argument form of open
[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
ccb4b539 691 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1ec2fb5f
GB
692 my $hash = $refname;
693 my $sfx;
ccb4b539 694 $hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//;
1ec2fb5f
GB
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
74fd8728 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
74fd8728 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
74fd8728 1299sub S_ISGITLINK {
01ac1e38
JN
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);
7d233dea
MC
1387 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1388 $cgi->a({-href => href(action=>"object", hash=>$1),
1389 -class => "text"}, $1);
1390 }eg;
1391
717b8311 1392 return $line;
b18f9bf4
JN
1393}
1394
717b8311 1395# format marker of refs pointing to given object
4afbaeff
GB
1396
1397# the destination action is chosen based on object type and current context:
1398# - for annotated tags, we choose the tag view unless it's the current view
1399# already, in which case we go to shortlog view
1400# - for other refs, we keep the current view if we're in history, shortlog or
1401# log view, and select shortlog otherwise
847e01fb 1402sub format_ref_marker {
717b8311 1403 my ($refs, $id) = @_;
d294e1ca 1404 my $markers = '';
27fb8c40 1405
717b8311 1406 if (defined $refs->{$id}) {
d294e1ca 1407 foreach my $ref (@{$refs->{$id}}) {
4afbaeff
GB
1408 # this code exploits the fact that non-lightweight tags are the
1409 # only indirect objects, and that they are the only objects for which
1410 # we want to use tag instead of shortlog as action
d294e1ca 1411 my ($type, $name) = qw();
4afbaeff 1412 my $indirect = ($ref =~ s/\^\{\}$//);
d294e1ca
JN
1413 # e.g. tags/v2.6.11 or heads/next
1414 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1415 $type = $1;
1416 $name = $2;
1417 } else {
1418 $type = "ref";
1419 $name = $ref;
1420 }
1421
4afbaeff
GB
1422 my $class = $type;
1423 $class .= " indirect" if $indirect;
1424
1425 my $dest_action = "shortlog";
1426
1427 if ($indirect) {
1428 $dest_action = "tag" unless $action eq "tag";
1429 } elsif ($action =~ /^(history|(short)?log)$/) {
1430 $dest_action = $action;
1431 }
1432
1433 my $dest = "";
1434 $dest .= "refs/" unless $ref =~ m!^refs/!;
1435 $dest .= $ref;
1436
1437 my $link = $cgi->a({
1438 -href => href(
1439 action=>$dest_action,
1440 hash=>$dest
1441 )}, $name);
1442
1443 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1444 $link . "</span>";
d294e1ca
JN
1445 }
1446 }
1447
1448 if ($markers) {
1449 return ' <span class="refs">'. $markers . '</span>';
717b8311
JN
1450 } else {
1451 return "";
1452 }
27fb8c40
JN
1453}
1454
17d07443
JN
1455# format, perhaps shortened and with markers, title line
1456sub format_subject_html {
1c2a4f5a 1457 my ($long, $short, $href, $extra) = @_;
17d07443
JN
1458 $extra = '' unless defined($extra);
1459
1460 if (length($short) < length($long)) {
7c278014 1461 return $cgi->a({-href => $href, -class => "list subject",
00f429af 1462 -title => to_utf8($long)},
17d07443
JN
1463 esc_html($short) . $extra);
1464 } else {
7c278014 1465 return $cgi->a({-href => $href, -class => "list subject"},
17d07443
JN
1466 esc_html($long) . $extra);
1467 }
1468}
1469
90921740
JN
1470# format git diff header line, i.e. "diff --(git|combined|cc) ..."
1471sub format_git_diff_header_line {
1472 my $line = shift;
1473 my $diffinfo = shift;
1474 my ($from, $to) = @_;
1475
1476 if ($diffinfo->{'nparents'}) {
1477 # combined diff
1478 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1479 if ($to->{'href'}) {
1480 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1481 esc_path($to->{'file'}));
1482 } else { # file was deleted (no href)
1483 $line .= esc_path($to->{'file'});
1484 }
1485 } else {
1486 # "ordinary" diff
1487 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1488 if ($from->{'href'}) {
1489 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1490 'a/' . esc_path($from->{'file'}));
1491 } else { # file was added (no href)
1492 $line .= 'a/' . esc_path($from->{'file'});
1493 }
1494 $line .= ' ';
1495 if ($to->{'href'}) {
1496 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1497 'b/' . esc_path($to->{'file'}));
1498 } else { # file was deleted
1499 $line .= 'b/' . esc_path($to->{'file'});
1500 }
1501 }
1502
1503 return "<div class=\"diff header\">$line</div>\n";
1504}
1505
1506# format extended diff header line, before patch itself
1507sub format_extended_diff_header_line {
1508 my $line = shift;
1509 my $diffinfo = shift;
1510 my ($from, $to) = @_;
1511
1512 # match <path>
1513 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1514 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1515 esc_path($from->{'file'}));
1516 }
1517 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1518 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1519 esc_path($to->{'file'}));
1520 }
1521 # match single <mode>
1522 if ($line =~ m/\s(\d{6})$/) {
1523 $line .= '<span class="info"> (' .
1524 file_type_long($1) .
1525 ')</span>';
1526 }
1527 # match <hash>
1528 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1529 # can match only for combined diff
1530 $line = 'index ';
1531 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1532 if ($from->{'href'}[$i]) {
1533 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1534 -class=>"hash"},
1535 substr($diffinfo->{'from_id'}[$i],0,7));
1536 } else {
1537 $line .= '0' x 7;
1538 }
1539 # separator
1540 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1541 }
1542 $line .= '..';
1543 if ($to->{'href'}) {
1544 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1545 substr($diffinfo->{'to_id'},0,7));
1546 } else {
1547 $line .= '0' x 7;
1548 }
1549
1550 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1551 # can match only for ordinary diff
1552 my ($from_link, $to_link);
1553 if ($from->{'href'}) {
1554 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1555 substr($diffinfo->{'from_id'},0,7));
1556 } else {
1557 $from_link = '0' x 7;
1558 }
1559 if ($to->{'href'}) {
1560 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1561 substr($diffinfo->{'to_id'},0,7));
1562 } else {
1563 $to_link = '0' x 7;
1564 }
1565 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1566 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1567 }
1568
1569 return $line . "<br/>\n";
1570}
1571
1572# format from-file/to-file diff header
1573sub format_diff_from_to_header {
91af4ce4 1574 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
90921740
JN
1575 my $line;
1576 my $result = '';
1577
1578 $line = $from_line;
1579 #assert($line =~ m/^---/) if DEBUG;
deaa01a9
JN
1580 # no extra formatting for "^--- /dev/null"
1581 if (! $diffinfo->{'nparents'}) {
1582 # ordinary (single parent) diff
1583 if ($line =~ m!^--- "?a/!) {
1584 if ($from->{'href'}) {
1585 $line = '--- a/' .
1586 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1587 esc_path($from->{'file'}));
1588 } else {
1589 $line = '--- a/' .
1590 esc_path($from->{'file'});
1591 }
1592 }
1593 $result .= qq!<div class="diff from_file">$line</div>\n!;
1594
1595 } else {
1596 # combined diff (merge commit)
1597 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1598 if ($from->{'href'}[$i]) {
1599 $line = '--- ' .
91af4ce4
JN
1600 $cgi->a({-href=>href(action=>"blobdiff",
1601 hash_parent=>$diffinfo->{'from_id'}[$i],
1602 hash_parent_base=>$parents[$i],
1603 file_parent=>$from->{'file'}[$i],
1604 hash=>$diffinfo->{'to_id'},
1605 hash_base=>$hash,
1606 file_name=>$to->{'file'}),
1607 -class=>"path",
1608 -title=>"diff" . ($i+1)},
1609 $i+1) .
1610 '/' .
deaa01a9
JN
1611 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1612 esc_path($from->{'file'}[$i]));
1613 } else {
1614 $line = '--- /dev/null';
1615 }
1616 $result .= qq!<div class="diff from_file">$line</div>\n!;
90921740
JN
1617 }
1618 }
90921740
JN
1619
1620 $line = $to_line;
1621 #assert($line =~ m/^\+\+\+/) if DEBUG;
1622 # no extra formatting for "^+++ /dev/null"
1623 if ($line =~ m!^\+\+\+ "?b/!) {
1624 if ($to->{'href'}) {
1625 $line = '+++ b/' .
1626 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1627 esc_path($to->{'file'}));
1628 } else {
1629 $line = '+++ b/' .
1630 esc_path($to->{'file'});
1631 }
1632 }
1633 $result .= qq!<div class="diff to_file">$line</div>\n!;
1634
1635 return $result;
1636}
1637
cd030c3a
JN
1638# create note for patch simplified by combined diff
1639sub format_diff_cc_simplified {
1640 my ($diffinfo, @parents) = @_;
1641 my $result = '';
1642
1643 $result .= "<div class=\"diff header\">" .
1644 "diff --cc ";
1645 if (!is_deleted($diffinfo)) {
1646 $result .= $cgi->a({-href => href(action=>"blob",
1647 hash_base=>$hash,
1648 hash=>$diffinfo->{'to_id'},
1649 file_name=>$diffinfo->{'to_file'}),
1650 -class => "path"},
1651 esc_path($diffinfo->{'to_file'}));
1652 } else {
1653 $result .= esc_path($diffinfo->{'to_file'});
1654 }
1655 $result .= "</div>\n" . # class="diff header"
1656 "<div class=\"diff nodifferences\">" .
1657 "Simple merge" .
1658 "</div>\n"; # class="diff nodifferences"
1659
1660 return $result;
1661}
1662
90921740 1663# format patch (diff) line (not to be used for diff headers)
eee08903
JN
1664sub format_diff_line {
1665 my $line = shift;
59e3b14e 1666 my ($from, $to) = @_;
eee08903
JN
1667 my $diff_class = "";
1668
1669 chomp $line;
1670
e72c0eaf
JN
1671 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1672 # combined diff
1673 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1674 if ($line =~ m/^\@{3}/) {
1675 $diff_class = " chunk_header";
1676 } elsif ($line =~ m/^\\/) {
1677 $diff_class = " incomplete";
1678 } elsif ($prefix =~ tr/+/+/) {
1679 $diff_class = " add";
1680 } elsif ($prefix =~ tr/-/-/) {
1681 $diff_class = " rem";
1682 }
1683 } else {
1684 # assume ordinary diff
1685 my $char = substr($line, 0, 1);
1686 if ($char eq '+') {
1687 $diff_class = " add";
1688 } elsif ($char eq '-') {
1689 $diff_class = " rem";
1690 } elsif ($char eq '@') {
1691 $diff_class = " chunk_header";
1692 } elsif ($char eq "\\") {
1693 $diff_class = " incomplete";
1694 }
eee08903
JN
1695 }
1696 $line = untabify($line);
59e3b14e
JN
1697 if ($from && $to && $line =~ m/^\@{2} /) {
1698 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1699 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1700
1701 $from_lines = 0 unless defined $from_lines;
1702 $to_lines = 0 unless defined $to_lines;
1703
1704 if ($from->{'href'}) {
1705 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1706 -class=>"list"}, $from_text);
1707 }
1708 if ($to->{'href'}) {
1709 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1710 -class=>"list"}, $to_text);
1711 }
1712 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1713 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1714 return "<div class=\"diff$diff_class\">$line</div>\n";
e72c0eaf
JN
1715 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1716 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1717 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1718
1719 @from_text = split(' ', $ranges);
1720 for (my $i = 0; $i < @from_text; ++$i) {
1721 ($from_start[$i], $from_nlines[$i]) =
1722 (split(',', substr($from_text[$i], 1)), 0);
1723 }
1724
1725 $to_text = pop @from_text;
1726 $to_start = pop @from_start;
1727 $to_nlines = pop @from_nlines;
1728
1729 $line = "<span class=\"chunk_info\">$prefix ";
1730 for (my $i = 0; $i < @from_text; ++$i) {
1731 if ($from->{'href'}[$i]) {
1732 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1733 -class=>"list"}, $from_text[$i]);
1734 } else {
1735 $line .= $from_text[$i];
1736 }
1737 $line .= " ";
1738 }
1739 if ($to->{'href'}) {
1740 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1741 -class=>"list"}, $to_text);
1742 } else {
1743 $line .= $to_text;
1744 }
1745 $line .= " $prefix</span>" .
1746 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1747 return "<div class=\"diff$diff_class\">$line</div>\n";
59e3b14e 1748 }
6255ef08 1749 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
eee08903
JN
1750}
1751
a3c8ab30
MM
1752# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1753# linked. Pass the hash of the tree/commit to snapshot.
1754sub format_snapshot_links {
1755 my ($hash) = @_;
a3c8ab30
MM
1756 my $num_fmts = @snapshot_fmts;
1757 if ($num_fmts > 1) {
1758 # A parenthesized list of links bearing format names.
a781785d 1759 # e.g. "snapshot (_tar.gz_ _zip_)"
a3c8ab30
MM
1760 return "snapshot (" . join(' ', map
1761 $cgi->a({
1762 -href => href(
1763 action=>"snapshot",
1764 hash=>$hash,
1765 snapshot_format=>$_
1766 )
1767 }, $known_snapshot_formats{$_}{'display'})
1768 , @snapshot_fmts) . ")";
1769 } elsif ($num_fmts == 1) {
1770 # A single "snapshot" link whose tooltip bears the format name.
a781785d 1771 # i.e. "_snapshot_"
a3c8ab30 1772 my ($fmt) = @snapshot_fmts;
a781785d
JN
1773 return
1774 $cgi->a({
a3c8ab30
MM
1775 -href => href(
1776 action=>"snapshot",
1777 hash=>$hash,
1778 snapshot_format=>$fmt
1779 ),
1780 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1781 }, "snapshot");
1782 } else { # $num_fmts == 0
1783 return undef;
1784 }
1785}
1786
3562198b
JN
1787## ......................................................................
1788## functions returning values to be passed, perhaps after some
1789## transformation, to other functions; e.g. returning arguments to href()
1790
1791# returns hash to be passed to href to generate gitweb URL
1792# in -title key it returns description of link
1793sub get_feed_info {
1794 my $format = shift || 'Atom';
1795 my %res = (action => lc($format));
1796
1797 # feed links are possible only for project views
1798 return unless (defined $project);
1799 # some views should link to OPML, or to generic project feed,
1800 # or don't have specific feed yet (so they should use generic)
1801 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1802
1803 my $branch;
1804 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1805 # from tag links; this also makes possible to detect branch links
1806 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1807 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1808 $branch = $1;
1809 }
1810 # find log type for feed description (title)
1811 my $type = 'log';
1812 if (defined $file_name) {
1813 $type = "history of $file_name";
1814 $type .= "/" if ($action eq 'tree');
1815 $type .= " on '$branch'" if (defined $branch);
1816 } else {
1817 $type = "log of $branch" if (defined $branch);
1818 }
1819
1820 $res{-title} = $type;
1821 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1822 $res{'file_name'} = $file_name;
1823
1824 return %res;
1825}
1826
717b8311
JN
1827## ----------------------------------------------------------------------
1828## git utility subroutines, invoking git commands
42f7eb94 1829
25691fbe
DS
1830# returns path to the core git executable and the --git-dir parameter as list
1831sub git_cmd {
1832 return $GIT, '--git-dir='.$git_dir;
1833}
1834
516381d5
LW
1835# quote the given arguments for passing them to the shell
1836# quote_command("command", "arg 1", "arg with ' and ! characters")
1837# => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1838# Try to avoid using this function wherever possible.
1839sub quote_command {
1840 return join(' ',
1841 map( { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ ));
25691fbe
DS
1842}
1843
717b8311 1844# get HEAD ref of given project as hash
847e01fb 1845sub git_get_head_hash {
df2c37a5 1846 my $project = shift;
25691fbe 1847 my $o_git_dir = $git_dir;
df2c37a5 1848 my $retval = undef;
25691fbe
DS
1849 $git_dir = "$projectroot/$project";
1850 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
df2c37a5
JH
1851 my $head = <$fd>;
1852 close $fd;
2c5c008b
KS
1853 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1854 $retval = $1;
df2c37a5
JH
1855 }
1856 }
25691fbe
DS
1857 if (defined $o_git_dir) {
1858 $git_dir = $o_git_dir;
2c5c008b 1859 }
df2c37a5
JH
1860 return $retval;
1861}
1862
717b8311
JN
1863# get type of given object
1864sub git_get_type {
1865 my $hash = shift;
1866
25691fbe 1867 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
717b8311
JN
1868 my $type = <$fd>;
1869 close $fd or return;
1870 chomp $type;
1871 return $type;
1872}
1873
b201927a
JN
1874# repository configuration
1875our $config_file = '';
1876our %config;
1877
1878# store multiple values for single key as anonymous array reference
1879# single values stored directly in the hash, not as [ <value> ]
1880sub hash_set_multi {
1881 my ($hash, $key, $value) = @_;
1882
1883 if (!exists $hash->{$key}) {
1884 $hash->{$key} = $value;
1885 } elsif (!ref $hash->{$key}) {
1886 $hash->{$key} = [ $hash->{$key}, $value ];
1887 } else {
1888 push @{$hash->{$key}}, $value;
1889 }
1890}
1891
1892# return hash of git project configuration
1893# optionally limited to some section, e.g. 'gitweb'
1894sub git_parse_project_config {
1895 my $section_regexp = shift;
1896 my %config;
1897
1898 local $/ = "\0";
1899
1900 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
1901 or return;
1902
1903 while (my $keyval = <$fh>) {
1904 chomp $keyval;
1905 my ($key, $value) = split(/\n/, $keyval, 2);
1906
1907 hash_set_multi(\%config, $key, $value)
1908 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
1909 }
1910 close $fh;
1911
1912 return %config;
1913}
1914
df5d10a3 1915# convert config value to boolean: 'true' or 'false'
b201927a
JN
1916# no value, number > 0, 'true' and 'yes' values are true
1917# rest of values are treated as false (never as error)
1918sub config_to_bool {
1919 my $val = shift;
1920
df5d10a3
MC
1921 return 1 if !defined $val; # section.key
1922
b201927a
JN
1923 # strip leading and trailing whitespace
1924 $val =~ s/^\s+//;
1925 $val =~ s/\s+$//;
1926
df5d10a3 1927 return (($val =~ /^\d+$/ && $val) || # section.key = 1
b201927a
JN
1928 ($val =~ /^(?:true|yes)$/i)); # section.key = true
1929}
1930
1931# convert config value to simple decimal number
1932# an optional value suffix of 'k', 'm', or 'g' will cause the value
1933# to be multiplied by 1024, 1048576, or 1073741824
1934sub config_to_int {
1935 my $val = shift;
1936
1937 # strip leading and trailing whitespace
1938 $val =~ s/^\s+//;
1939 $val =~ s/\s+$//;
1940
1941 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
1942 $unit = lc($unit);
1943 # unknown unit is treated as 1
1944 return $num * ($unit eq 'g' ? 1073741824 :
1945 $unit eq 'm' ? 1048576 :
1946 $unit eq 'k' ? 1024 : 1);
1947 }
1948 return $val;
1949}
1950
1951# convert config value to array reference, if needed
1952sub config_to_multi {
1953 my $val = shift;
1954
d76a585d 1955 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
b201927a
JN
1956}
1957
717b8311 1958sub git_get_project_config {
ddb8d900 1959 my ($key, $type) = @_;
717b8311 1960
b201927a 1961 # key sanity check
717b8311
JN
1962 return unless ($key);
1963 $key =~ s/^gitweb\.//;
1964 return if ($key =~ m/\W/);
1965
b201927a
JN
1966 # type sanity check
1967 if (defined $type) {
1968 $type =~ s/^--//;
1969 $type = undef
1970 unless ($type eq 'bool' || $type eq 'int');
1971 }
1972
1973 # get config
1974 if (!defined $config_file ||
1975 $config_file ne "$git_dir/config") {
1976 %config = git_parse_project_config('gitweb');
1977 $config_file = "$git_dir/config";
1978 }
1979
df5d10a3
MC
1980 # check if config variable (key) exists
1981 return unless exists $config{"gitweb.$key"};
1982
b201927a
JN
1983 # ensure given type
1984 if (!defined $type) {
1985 return $config{"gitweb.$key"};
1986 } elsif ($type eq 'bool') {
1987 # backward compatibility: 'git config --bool' returns true/false
1988 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
1989 } elsif ($type eq 'int') {
1990 return config_to_int($config{"gitweb.$key"});
1991 }
1992 return $config{"gitweb.$key"};
717b8311
JN
1993}
1994
717b8311
JN
1995# get hash of given path at given ref
1996sub git_get_hash_by_path {
1997 my $base = shift;
1998 my $path = shift || return undef;
1d782b03 1999 my $type = shift;
717b8311 2000
4b02f483 2001 $path =~ s,/+$,,;
717b8311 2002
25691fbe 2003 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
074afaa0 2004 or die_error(500, "Open git-ls-tree failed");
717b8311
JN
2005 my $line = <$fd>;
2006 close $fd or return undef;
2007
198a2a8a
JN
2008 if (!defined $line) {
2009 # there is no tree or hash given by $path at $base
2010 return undef;
2011 }
2012
717b8311 2013 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8b4b94cc 2014 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
1d782b03
JN
2015 if (defined $type && $type ne $2) {
2016 # type doesn't match
2017 return undef;
2018 }
717b8311
JN
2019 return $3;
2020}
2021
ed224dea
JN
2022# get path of entry with given hash at given tree-ish (ref)
2023# used to get 'from' filename for combined diff (merge commit) for renames
2024sub git_get_path_by_hash {
2025 my $base = shift || return;
2026 my $hash = shift || return;
2027
2028 local $/ = "\0";
2029
2030 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2031 or return undef;
2032 while (my $line = <$fd>) {
2033 chomp $line;
2034
2035 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2036 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2037 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2038 close $fd;
2039 return $1;
2040 }
2041 }
2042 close $fd;
2043 return undef;
2044}
2045
717b8311
JN
2046## ......................................................................
2047## git utility functions, directly accessing git repository
2048
847e01fb 2049sub git_get_project_description {
b87d78d6 2050 my $path = shift;
09bd7898 2051
0e121a2c 2052 $git_dir = "$projectroot/$path";
dff2b6d4 2053 open my $fd, '<', "$git_dir/description"
0e121a2c 2054 or return git_get_project_config('description');
b87d78d6
KS
2055 my $descr = <$fd>;
2056 close $fd;
2eb54efc
JH
2057 if (defined $descr) {
2058 chomp $descr;
2059 }
b87d78d6 2060 return $descr;
12a88f2f
KS
2061}
2062
aed93de4
PB
2063sub git_get_project_ctags {
2064 my $path = shift;
2065 my $ctags = {};
2066
2067 $git_dir = "$projectroot/$path";
ad87e4f6
JN
2068 opendir my $dh, "$git_dir/ctags"
2069 or return $ctags;
2070 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
dff2b6d4 2071 open my $ct, '<', $_ or next;
ad87e4f6 2072 my $val = <$ct>;
aed93de4 2073 chomp $val;
ad87e4f6 2074 close $ct;
aed93de4
PB
2075 my $ctag = $_; $ctag =~ s#.*/##;
2076 $ctags->{$ctag} = $val;
2077 }
ad87e4f6 2078 closedir $dh;
aed93de4
PB
2079 $ctags;
2080}
2081
2082sub git_populate_project_tagcloud {
2083 my $ctags = shift;
2084
2085 # First, merge different-cased tags; tags vote on casing
2086 my %ctags_lc;
2087 foreach (keys %$ctags) {
2088 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2089 if (not $ctags_lc{lc $_}->{topcount}
2090 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2091 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2092 $ctags_lc{lc $_}->{topname} = $_;
2093 }
2094 }
2095
2096 my $cloud;
2097 if (eval { require HTML::TagCloud; 1; }) {
2098 $cloud = HTML::TagCloud->new;
2099 foreach (sort keys %ctags_lc) {
2100 # Pad the title with spaces so that the cloud looks
2101 # less crammed.
2102 my $title = $ctags_lc{$_}->{topname};
2103 $title =~ s/ /&nbsp;/g;
2104 $title =~ s/^/&nbsp;/g;
2105 $title =~ s/$/&nbsp;/g;
2106 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2107 }
2108 } else {
2109 $cloud = \%ctags_lc;
2110 }
2111 $cloud;
2112}
2113
2114sub git_show_project_tagcloud {
2115 my ($cloud, $count) = @_;
2116 print STDERR ref($cloud)."..\n";
2117 if (ref $cloud eq 'HTML::TagCloud') {
2118 return $cloud->html_and_css($count);
2119 } else {
2120 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2121 return '<p align="center">' . join (', ', map {
2122 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2123 } splice(@tags, 0, $count)) . '</p>';
2124 }
2125}
2126
e79ca7cc
JN
2127sub git_get_project_url_list {
2128 my $path = shift;
2129
0e121a2c 2130 $git_dir = "$projectroot/$path";
dff2b6d4 2131 open my $fd, '<', "$git_dir/cloneurl"
0e121a2c
JN
2132 or return wantarray ?
2133 @{ config_to_multi(git_get_project_config('url')) } :
2134 config_to_multi(git_get_project_config('url'));
e79ca7cc
JN
2135 my @git_project_url_list = map { chomp; $_ } <$fd>;
2136 close $fd;
2137
2138 return wantarray ? @git_project_url_list : \@git_project_url_list;
2139}
2140
847e01fb 2141sub git_get_projects_list {
e30496df 2142 my ($filter) = @_;
717b8311
JN
2143 my @list;
2144
e30496df
PB
2145 $filter ||= '';
2146 $filter =~ s/\.git$//;
2147
25b2790f 2148 my $check_forks = gitweb_check_feature('forks');
c2b8b134 2149
717b8311
JN
2150 if (-d $projects_list) {
2151 # search in directory
e30496df 2152 my $dir = $projects_list . ($filter ? "/$filter" : '');
6768d6b8
AK
2153 # remove the trailing "/"
2154 $dir =~ s!/+$!!;
c0011ff8 2155 my $pfxlen = length("$dir");
ca5e9495 2156 my $pfxdepth = ($dir =~ tr!/!!);
c0011ff8
JN
2157
2158 File::Find::find({
2159 follow_fast => 1, # follow symbolic links
d20602ee 2160 follow_skip => 2, # ignore duplicates
c0011ff8
JN
2161 dangling_symlinks => 0, # ignore dangling symlinks, silently
2162 wanted => sub {
2163 # skip project-list toplevel, if we get it.
2164 return if (m!^[/.]$!);
2165 # only directories can be git repositories
2166 return unless (-d $_);
ca5e9495
LL
2167 # don't traverse too deep (Find is super slow on os x)
2168 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2169 $File::Find::prune = 1;
2170 return;
2171 }
c0011ff8
JN
2172
2173 my $subdir = substr($File::Find::name, $pfxlen + 1);
2174 # we check related file in $projectroot
fb3bb3d1
DD
2175 my $path = ($filter ? "$filter/" : '') . $subdir;
2176 if (check_export_ok("$projectroot/$path")) {
2177 push @list, { path => $path };
c0011ff8
JN
2178 $File::Find::prune = 1;
2179 }
2180 },
2181 }, "$dir");
2182
717b8311
JN
2183 } elsif (-f $projects_list) {
2184 # read from file(url-encoded):
2185 # 'git%2Fgit.git Linus+Torvalds'
2186 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2187 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
c2b8b134 2188 my %paths;
dff2b6d4 2189 open my $fd, '<', $projects_list or return;
c2b8b134 2190 PROJECT:
717b8311
JN
2191 while (my $line = <$fd>) {
2192 chomp $line;
2193 my ($path, $owner) = split ' ', $line;
2194 $path = unescape($path);
2195 $owner = unescape($owner);
2196 if (!defined $path) {
2197 next;
2198 }
83ee94c1
JH
2199 if ($filter ne '') {
2200 # looking for forks;
2201 my $pfx = substr($path, 0, length($filter));
2202 if ($pfx ne $filter) {
c2b8b134 2203 next PROJECT;
83ee94c1
JH
2204 }
2205 my $sfx = substr($path, length($filter));
2206 if ($sfx !~ /^\/.*\.git$/) {
c2b8b134
FL
2207 next PROJECT;
2208 }
2209 } elsif ($check_forks) {
2210 PATH:
2211 foreach my $filter (keys %paths) {
2212 # looking for forks;
2213 my $pfx = substr($path, 0, length($filter));
2214 if ($pfx ne $filter) {
2215 next PATH;
2216 }
2217 my $sfx = substr($path, length($filter));
2218 if ($sfx !~ /^\/.*\.git$/) {
2219 next PATH;
2220 }
2221 # is a fork, don't include it in
2222 # the list
2223 next PROJECT;
83ee94c1
JH
2224 }
2225 }
2172ce4b 2226 if (check_export_ok("$projectroot/$path")) {
717b8311
JN
2227 my $pr = {
2228 path => $path,
00f429af 2229 owner => to_utf8($owner),
717b8311 2230 };
c2b8b134
FL
2231 push @list, $pr;
2232 (my $forks_path = $path) =~ s/\.git$//;
2233 $paths{$forks_path}++;
717b8311
JN
2234 }
2235 }
2236 close $fd;
2237 }
717b8311
JN
2238 return @list;
2239}
2240
47852450
JH
2241our $gitweb_project_owner = undef;
2242sub git_get_project_list_from_file {
1e0cf030 2243
47852450 2244 return if (defined $gitweb_project_owner);
1e0cf030 2245
47852450 2246 $gitweb_project_owner = {};
1e0cf030
JN
2247 # read from file (url-encoded):
2248 # 'git%2Fgit.git Linus+Torvalds'
2249 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2250 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2251 if (-f $projects_list) {
dff2b6d4 2252 open(my $fd, '<', $projects_list);
1e0cf030
JN
2253 while (my $line = <$fd>) {
2254 chomp $line;
2255 my ($pr, $ow) = split ' ', $line;
2256 $pr = unescape($pr);
2257 $ow = unescape($ow);
47852450 2258 $gitweb_project_owner->{$pr} = to_utf8($ow);
1e0cf030
JN
2259 }
2260 close $fd;
2261 }
47852450
JH
2262}
2263
2264sub git_get_project_owner {
2265 my $project = shift;
2266 my $owner;
2267
2268 return undef unless $project;
b59012ef 2269 $git_dir = "$projectroot/$project";
47852450
JH
2270
2271 if (!defined $gitweb_project_owner) {
2272 git_get_project_list_from_file();
2273 }
2274
2275 if (exists $gitweb_project_owner->{$project}) {
2276 $owner = $gitweb_project_owner->{$project};
2277 }
b59012ef
BR
2278 if (!defined $owner){
2279 $owner = git_get_project_config('owner');
2280 }
1e0cf030 2281 if (!defined $owner) {
b59012ef 2282 $owner = get_file_owner("$git_dir");
1e0cf030
JN
2283 }
2284
2285 return $owner;
2286}
2287
c60c56cc
JN
2288sub git_get_last_activity {
2289 my ($path) = @_;
2290 my $fd;
2291
2292 $git_dir = "$projectroot/$path";
2293 open($fd, "-|", git_cmd(), 'for-each-ref',
0ff5ec70 2294 '--format=%(committer)',
c60c56cc 2295 '--sort=-committerdate',
0ff5ec70 2296 '--count=1',
c60c56cc
JN
2297 'refs/heads') or return;
2298 my $most_recent = <$fd>;
2299 close $fd or return;
785cdea9
JN
2300 if (defined $most_recent &&
2301 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
c60c56cc
JN
2302 my $timestamp = $1;
2303 my $age = time - $timestamp;
2304 return ($age, age_string($age));
2305 }
c956395e 2306 return (undef, undef);
c60c56cc
JN
2307}
2308
847e01fb 2309sub git_get_references {
717b8311
JN
2310 my $type = shift || "";
2311 my %refs;
28b9d9f7
JN
2312 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2313 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2314 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2315 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
9704d75d 2316 or return;
d294e1ca 2317
717b8311
JN
2318 while (my $line = <$fd>) {
2319 chomp $line;
4afbaeff 2320 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
717b8311 2321 if (defined $refs{$1}) {
d294e1ca 2322 push @{$refs{$1}}, $2;
717b8311 2323 } else {
d294e1ca 2324 $refs{$1} = [ $2 ];
717b8311
JN
2325 }
2326 }
2327 }
2328 close $fd or return;
2329 return \%refs;
2330}
2331
56a322f1
JN
2332sub git_get_rev_name_tags {
2333 my $hash = shift || return undef;
2334
25691fbe 2335 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
56a322f1
JN
2336 or return;
2337 my $name_rev = <$fd>;
2338 close $fd;
2339
2340 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2341 return $1;
2342 } else {
2343 # catches also '$hash undefined' output
2344 return undef;
2345 }
2346}
2347
717b8311
JN
2348## ----------------------------------------------------------------------
2349## parse to hash functions
2350
847e01fb 2351sub parse_date {
717b8311
JN
2352 my $epoch = shift;
2353 my $tz = shift || "-0000";
2354
2355 my %date;
2356 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2357 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2358 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2359 $date{'hour'} = $hour;
2360 $date{'minute'} = $min;
2361 $date{'mday'} = $mday;
2362 $date{'day'} = $days[$wday];
2363 $date{'month'} = $months[$mon];
af6feeb2
JN
2364 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2365 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
952c65fc
JN
2366 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2367 $mday, $months[$mon], $hour ,$min;
af6feeb2 2368 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
a62d6d84 2369 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
717b8311
JN
2370
2371 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2372 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2373 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2374 $date{'hour_local'} = $hour;
2375 $date{'minute_local'} = $min;
2376 $date{'tz_local'} = $tz;
af6feeb2
JN
2377 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2378 1900+$year, $mon+1, $mday,
2379 $hour, $min, $sec, $tz);
717b8311
JN
2380 return %date;
2381}
2382
847e01fb 2383sub parse_tag {
ede5e100
KS
2384 my $tag_id = shift;
2385 my %tag;
d8a20ba9 2386 my @comment;
ede5e100 2387
25691fbe 2388 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
d8a20ba9 2389 $tag{'id'} = $tag_id;
ede5e100
KS
2390 while (my $line = <$fd>) {
2391 chomp $line;
2392 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2393 $tag{'object'} = $1;
7ab0d2b6 2394 } elsif ($line =~ m/^type (.+)$/) {
ede5e100 2395 $tag{'type'} = $1;
7ab0d2b6 2396 } elsif ($line =~ m/^tag (.+)$/) {
ede5e100 2397 $tag{'name'} = $1;
d8a20ba9
KS
2398 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2399 $tag{'author'} = $1;
2400 $tag{'epoch'} = $2;
2401 $tag{'tz'} = $3;
2402 } elsif ($line =~ m/--BEGIN/) {
2403 push @comment, $line;
2404 last;
2405 } elsif ($line eq "") {
2406 last;
ede5e100
KS
2407 }
2408 }
d8a20ba9
KS
2409 push @comment, <$fd>;
2410 $tag{'comment'} = \@comment;
19806691 2411 close $fd or return;
ede5e100
KS
2412 if (!defined $tag{'name'}) {
2413 return
2414 };
2415 return %tag
2416}
2417
756bbf54 2418sub parse_commit_text {
ccdfdea0 2419 my ($commit_text, $withparents) = @_;
756bbf54 2420 my @commit_lines = split '\n', $commit_text;
703ac710 2421 my %co;
703ac710 2422
756bbf54
RF
2423 pop @commit_lines; # Remove '\0'
2424
198a2a8a
JN
2425 if (! @commit_lines) {
2426 return;
2427 }
2428
25f422fb 2429 my $header = shift @commit_lines;
198a2a8a 2430 if ($header !~ m/^[0-9a-fA-F]{40}/) {
25f422fb
KS
2431 return;
2432 }
ccdfdea0 2433 ($co{'id'}, my @parents) = split ' ', $header;
19806691 2434 while (my $line = shift @commit_lines) {
b87d78d6 2435 last if $line eq "\n";
7ab0d2b6 2436 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
703ac710 2437 $co{'tree'} = $1;
ccdfdea0 2438 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
208b2dff 2439 push @parents, $1;
022be3d0 2440 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3f714537 2441 $co{'author'} = $1;
185f09e5
KS
2442 $co{'author_epoch'} = $2;
2443 $co{'author_tz'} = $3;
ba00b8c1
JN
2444 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2445 $co{'author_name'} = $1;
2446 $co{'author_email'} = $2;
2bf7a52c
KS
2447 } else {
2448 $co{'author_name'} = $co{'author'};
2449 }
86eed32d
KS
2450 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2451 $co{'committer'} = $1;
185f09e5
KS
2452 $co{'committer_epoch'} = $2;
2453 $co{'committer_tz'} = $3;
991910a9 2454 $co{'committer_name'} = $co{'committer'};
ba00b8c1
JN
2455 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2456 $co{'committer_name'} = $1;
2457 $co{'committer_email'} = $2;
2458 } else {
2459 $co{'committer_name'} = $co{'committer'};
2460 }
703ac710
KS
2461 }
2462 }
ede5e100 2463 if (!defined $co{'tree'}) {
25f422fb 2464 return;
ede5e100 2465 };
208b2dff
RF
2466 $co{'parents'} = \@parents;
2467 $co{'parent'} = $parents[0];
25f422fb 2468
19806691 2469 foreach my $title (@commit_lines) {
c2488d06 2470 $title =~ s/^ //;
19806691 2471 if ($title ne "") {
48c771f4 2472 $co{'title'} = chop_str($title, 80, 5);
19806691
KS
2473 # remove leading stuff of merges to make the interesting part visible
2474 if (length($title) > 50) {
2475 $title =~ s/^Automatic //;
2476 $title =~ s/^merge (of|with) /Merge ... /i;
2477 if (length($title) > 50) {
2478 $title =~ s/(http|rsync):\/\///;
2479 }
2480 if (length($title) > 50) {
2481 $title =~ s/(master|www|rsync)\.//;
2482 }
2483 if (length($title) > 50) {
2484 $title =~ s/kernel.org:?//;
2485 }
2486 if (length($title) > 50) {
2487 $title =~ s/\/pub\/scm//;
2488 }
2489 }
48c771f4 2490 $co{'title_short'} = chop_str($title, 50, 5);
19806691
KS
2491 last;
2492 }
2493 }
53c39676 2494 if (! defined $co{'title'} || $co{'title'} eq "") {
7e0fe5c9
PB
2495 $co{'title'} = $co{'title_short'} = '(no commit message)';
2496 }
25f422fb
KS
2497 # remove added spaces
2498 foreach my $line (@commit_lines) {
2499 $line =~ s/^ //;
2500 }
2501 $co{'comment'} = \@commit_lines;
2ae100df
KS
2502
2503 my $age = time - $co{'committer_epoch'};
2504 $co{'age'} = $age;
d263a6bd 2505 $co{'age_string'} = age_string($age);
71be1e79
KS
2506 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2507 if ($age > 60*60*24*7*2) {
1b1cd421 2508 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79
KS
2509 $co{'age_string_age'} = $co{'age_string'};
2510 } else {
2511 $co{'age_string_date'} = $co{'age_string'};
1b1cd421 2512 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79 2513 }
703ac710
KS
2514 return %co;
2515}
2516
756bbf54
RF
2517sub parse_commit {
2518 my ($commit_id) = @_;
2519 my %co;
2520
2521 local $/ = "\0";
2522
2523 open my $fd, "-|", git_cmd(), "rev-list",
ccdfdea0 2524 "--parents",
756bbf54 2525 "--header",
756bbf54
RF
2526 "--max-count=1",
2527 $commit_id,
2528 "--",
074afaa0 2529 or die_error(500, "Open git-rev-list failed");
ccdfdea0 2530 %co = parse_commit_text(<$fd>, 1);
756bbf54
RF
2531 close $fd;
2532
2533 return %co;
2534}
2535
2536sub parse_commits {
311e552e 2537 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
756bbf54
RF
2538 my @cos;
2539
2540 $maxcount ||= 1;
2541 $skip ||= 0;
2542
756bbf54
RF
2543 local $/ = "\0";
2544
2545 open my $fd, "-|", git_cmd(), "rev-list",
2546 "--header",
311e552e 2547 @args,
756bbf54 2548 ("--max-count=" . $maxcount),
f47efbb7 2549 ("--skip=" . $skip),
868bc068 2550 @extra_options,
756bbf54
RF
2551 $commit_id,
2552 "--",
2553 ($filename ? ($filename) : ())
074afaa0 2554 or die_error(500, "Open git-rev-list failed");
756bbf54
RF
2555 while (my $line = <$fd>) {
2556 my %co = parse_commit_text($line);
2557 push @cos, \%co;
2558 }
2559 close $fd;
2560
2561 return wantarray ? @cos : \@cos;
2562}
2563
e8e41a93 2564# parse line of git-diff-tree "raw" output
740e67f9
JN
2565sub parse_difftree_raw_line {
2566 my $line = shift;
2567 my %res;
2568
2569 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2570 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2571 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2572 $res{'from_mode'} = $1;
2573 $res{'to_mode'} = $2;
2574 $res{'from_id'} = $3;
2575 $res{'to_id'} = $4;
4ed4a347 2576 $res{'status'} = $5;
740e67f9
JN
2577 $res{'similarity'} = $6;
2578 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
e8e41a93 2579 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
740e67f9 2580 } else {
9d301456 2581 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
740e67f9
JN
2582 }
2583 }
78bc403a
JN
2584 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2585 # combined diff (for merge commit)
2586 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2587 $res{'nparents'} = length($1);
2588 $res{'from_mode'} = [ split(' ', $2) ];
2589 $res{'to_mode'} = pop @{$res{'from_mode'}};
2590 $res{'from_id'} = [ split(' ', $3) ];
2591 $res{'to_id'} = pop @{$res{'from_id'}};
2592 $res{'status'} = [ split('', $4) ];
2593 $res{'to_file'} = unquote($5);
2594 }
740e67f9 2595 # 'c512b523472485aef4fff9e57b229d9d243c967f'
0edcb37d
JN
2596 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2597 $res{'commit'} = $1;
2598 }
740e67f9
JN
2599
2600 return wantarray ? %res : \%res;
2601}
2602
0cec6db5
JN
2603# wrapper: return parsed line of git-diff-tree "raw" output
2604# (the argument might be raw line, or parsed info)
2605sub parsed_difftree_line {
2606 my $line_or_ref = shift;
2607
2608 if (ref($line_or_ref) eq "HASH") {
2609 # pre-parsed (or generated by hand)
2610 return $line_or_ref;
2611 } else {
2612 return parse_difftree_raw_line($line_or_ref);
2613 }
2614}
2615
cb849b46 2616# parse line of git-ls-tree output
74fd8728 2617sub parse_ls_tree_line {
cb849b46
JN
2618 my $line = shift;
2619 my %opts = @_;
2620 my %res;
2621
2622 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8b4b94cc 2623 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
cb849b46
JN
2624
2625 $res{'mode'} = $1;
2626 $res{'type'} = $2;
2627 $res{'hash'} = $3;
2628 if ($opts{'-z'}) {
2629 $res{'name'} = $4;
2630 } else {
2631 $res{'name'} = unquote($4);
2632 }
2633
2634 return wantarray ? %res : \%res;
2635}
2636
90921740
JN
2637# generates _two_ hashes, references to which are passed as 2 and 3 argument
2638sub parse_from_to_diffinfo {
2639 my ($diffinfo, $from, $to, @parents) = @_;
2640
2641 if ($diffinfo->{'nparents'}) {
2642 # combined diff
2643 $from->{'file'} = [];
2644 $from->{'href'} = [];
2645 fill_from_file_info($diffinfo, @parents)
2646 unless exists $diffinfo->{'from_file'};
2647 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
9d301456
JN
2648 $from->{'file'}[$i] =
2649 defined $diffinfo->{'from_file'}[$i] ?
2650 $diffinfo->{'from_file'}[$i] :
2651 $diffinfo->{'to_file'};
90921740
JN
2652 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2653 $from->{'href'}[$i] = href(action=>"blob",
2654 hash_base=>$parents[$i],
2655 hash=>$diffinfo->{'from_id'}[$i],
2656 file_name=>$from->{'file'}[$i]);
2657 } else {
2658 $from->{'href'}[$i] = undef;
2659 }
2660 }
2661 } else {
0cec6db5 2662 # ordinary (not combined) diff
9d301456 2663 $from->{'file'} = $diffinfo->{'from_file'};
90921740
JN
2664 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2665 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2666 hash=>$diffinfo->{'from_id'},
2667 file_name=>$from->{'file'});
2668 } else {
2669 delete $from->{'href'};
2670 }
2671 }
2672
9d301456 2673 $to->{'file'} = $diffinfo->{'to_file'};
90921740
JN
2674 if (!is_deleted($diffinfo)) { # file exists in result
2675 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2676 hash=>$diffinfo->{'to_id'},
2677 file_name=>$to->{'file'});
2678 } else {
2679 delete $to->{'href'};
2680 }
2681}
2682
717b8311
JN
2683## ......................................................................
2684## parse to array of hashes functions
4c02e3c5 2685
cd146408
JN
2686sub git_get_heads_list {
2687 my $limit = shift;
2688 my @headslist;
2689
2690 open my $fd, '-|', git_cmd(), 'for-each-ref',
2691 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2692 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2693 'refs/heads'
c83a77e4
JN
2694 or return;
2695 while (my $line = <$fd>) {
cd146408 2696 my %ref_item;
120ddde2 2697
cd146408
JN
2698 chomp $line;
2699 my ($refinfo, $committerinfo) = split(/\0/, $line);
2700 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2701 my ($committer, $epoch, $tz) =
2702 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 2703 $ref_item{'fullname'} = $name;
cd146408
JN
2704 $name =~ s!^refs/heads/!!;
2705
2706 $ref_item{'name'} = $name;
2707 $ref_item{'id'} = $hash;
2708 $ref_item{'title'} = $title || '(no commit message)';
2709 $ref_item{'epoch'} = $epoch;
2710 if ($epoch) {
2711 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2712 } else {
2713 $ref_item{'age'} = "unknown";
717b8311 2714 }
cd146408
JN
2715
2716 push @headslist, \%ref_item;
c83a77e4
JN
2717 }
2718 close $fd;
2719
cd146408
JN
2720 return wantarray ? @headslist : \@headslist;
2721}
2722
2723sub git_get_tags_list {
2724 my $limit = shift;
2725 my @tagslist;
2726
2727 open my $fd, '-|', git_cmd(), 'for-each-ref',
2728 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2729 '--format=%(objectname) %(objecttype) %(refname) '.
2730 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2731 'refs/tags'
2732 or return;
2733 while (my $line = <$fd>) {
2734 my %ref_item;
7a13b999 2735
cd146408
JN
2736 chomp $line;
2737 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2738 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2739 my ($creator, $epoch, $tz) =
2740 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 2741 $ref_item{'fullname'} = $name;
cd146408
JN
2742 $name =~ s!^refs/tags/!!;
2743
2744 $ref_item{'type'} = $type;
2745 $ref_item{'id'} = $id;
2746 $ref_item{'name'} = $name;
2747 if ($type eq "tag") {
2748 $ref_item{'subject'} = $title;
2749 $ref_item{'reftype'} = $reftype;
2750 $ref_item{'refid'} = $refid;
2751 } else {
2752 $ref_item{'reftype'} = $type;
2753 $ref_item{'refid'} = $id;
2754 }
2755
2756 if ($type eq "tag" || $type eq "commit") {
2757 $ref_item{'epoch'} = $epoch;
2758 if ($epoch) {
2759 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2760 } else {
2761 $ref_item{'age'} = "unknown";
2762 }
2763 }
991910a9 2764
cd146408 2765 push @tagslist, \%ref_item;
717b8311 2766 }
cd146408
JN
2767 close $fd;
2768
2769 return wantarray ? @tagslist : \@tagslist;
86eed32d
KS
2770}
2771
717b8311
JN
2772## ----------------------------------------------------------------------
2773## filesystem-related functions
022be3d0 2774
c07ad4b9
KS
2775sub get_file_owner {
2776 my $path = shift;
2777
2778 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2779 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2780 if (!defined $gcos) {
2781 return undef;
2782 }
2783 my $owner = $gcos;
2784 $owner =~ s/[,;].*$//;
00f429af 2785 return to_utf8($owner);
c07ad4b9
KS
2786}
2787
2dcb5e1a
JN
2788# assume that file exists
2789sub insert_file {
2790 my $filename = shift;
2791
2792 open my $fd, '<', $filename;
4586864a 2793 print map { to_utf8($_) } <$fd>;
2dcb5e1a
JN
2794 close $fd;
2795}
2796
717b8311
JN
2797## ......................................................................
2798## mimetype related functions
09bd7898 2799
717b8311
JN
2800sub mimetype_guess_file {
2801 my $filename = shift;
2802 my $mimemap = shift;
2803 -r $mimemap or return undef;
2804
2805 my %mimemap;
dff2b6d4 2806 open(my $mh, '<', $mimemap) or return undef;
ad87e4f6 2807 while (<$mh>) {
618918e5 2808 next if m/^#/; # skip comments
ad87e4f6 2809 my ($mimetype, $exts) = split(/\t+/);
46b059d7
JH
2810 if (defined $exts) {
2811 my @exts = split(/\s+/, $exts);
2812 foreach my $ext (@exts) {
ad87e4f6 2813 $mimemap{$ext} = $mimetype;
46b059d7 2814 }
09bd7898 2815 }
09bd7898 2816 }
ad87e4f6 2817 close($mh);
09bd7898 2818
8059319a 2819 $filename =~ /\.([^.]*)$/;
717b8311
JN
2820 return $mimemap{$1};
2821}
5996ca08 2822
717b8311
JN
2823sub mimetype_guess {
2824 my $filename = shift;
2825 my $mime;
2826 $filename =~ /\./ or return undef;
5996ca08 2827
717b8311
JN
2828 if ($mimetypes_file) {
2829 my $file = $mimetypes_file;
d5aa50de
JN
2830 if ($file !~ m!^/!) { # if it is relative path
2831 # it is relative to project
2832 $file = "$projectroot/$project/$file";
2833 }
717b8311
JN
2834 $mime = mimetype_guess_file($filename, $file);
2835 }
2836 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2837 return $mime;
5996ca08
FF
2838}
2839
847e01fb 2840sub blob_mimetype {
717b8311
JN
2841 my $fd = shift;
2842 my $filename = shift;
5996ca08 2843
717b8311
JN
2844 if ($filename) {
2845 my $mime = mimetype_guess($filename);
2846 $mime and return $mime;
d8d17b5d 2847 }
717b8311
JN
2848
2849 # just in case
2850 return $default_blob_plain_mimetype unless $fd;
2851
2852 if (-T $fd) {
7f718e8b 2853 return 'text/plain';
717b8311
JN
2854 } elsif (! $filename) {
2855 return 'application/octet-stream';
2856 } elsif ($filename =~ m/\.png$/i) {
2857 return 'image/png';
2858 } elsif ($filename =~ m/\.gif$/i) {
2859 return 'image/gif';
2860 } elsif ($filename =~ m/\.jpe?g$/i) {
2861 return 'image/jpeg';
d8d17b5d 2862 } else {
717b8311 2863 return 'application/octet-stream';
f7ab660c 2864 }
717b8311
JN
2865}
2866
7f718e8b
JN
2867sub blob_contenttype {
2868 my ($fd, $file_name, $type) = @_;
2869
2870 $type ||= blob_mimetype($fd, $file_name);
2871 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2872 $type .= "; charset=$default_text_plain_charset";
2873 }
2874
2875 return $type;
2876}
2877
717b8311
JN
2878## ======================================================================
2879## functions printing HTML: header, footer, error page
2880
2881sub git_header_html {
2882 my $status = shift || "200 OK";
2883 my $expires = shift;
2884
8be2890c 2885 my $title = "$site_name";
717b8311 2886 if (defined $project) {
00f429af 2887 $title .= " - " . to_utf8($project);
717b8311
JN
2888 if (defined $action) {
2889 $title .= "/$action";
2890 if (defined $file_name) {
403d0906 2891 $title .= " - " . esc_path($file_name);
717b8311
JN
2892 if ($action eq "tree" && $file_name !~ m|/$|) {
2893 $title .= "/";
2894 }
2895 }
2896 }
f7ab660c 2897 }
717b8311
JN
2898 my $content_type;
2899 # require explicit support from the UA if we are to send the page as
2900 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2901 # we have to do this because MSIE sometimes globs '*/*', pretending to
2902 # support xhtml+xml but choking when it gets what it asked for.
952c65fc
JN
2903 if (defined $cgi->http('HTTP_ACCEPT') &&
2904 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2905 $cgi->Accept('application/xhtml+xml') != 0) {
717b8311 2906 $content_type = 'application/xhtml+xml';
f7ab660c 2907 } else {
717b8311 2908 $content_type = 'text/html';
f7ab660c 2909 }
952c65fc
JN
2910 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2911 -status=> $status, -expires => $expires);
45c9a758 2912 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
717b8311
JN
2913 print <<EOF;
2914<?xml version="1.0" encoding="utf-8"?>
2915<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2916<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
d4baf9ea 2917<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
717b8311
JN
2918<!-- git core binaries version $git_version -->
2919<head>
2920<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
45c9a758 2921<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
717b8311
JN
2922<meta name="robots" content="index, nofollow"/>
2923<title>$title</title>
717b8311 2924EOF
41a4d16e
GB
2925 # the stylesheet, favicon etc urls won't work correctly with path_info
2926 # unless we set the appropriate base URL
c3254aee 2927 if ($ENV{'PATH_INFO'}) {
81d3fe9f 2928 print "<base href=\"".esc_url($base_url)."\" />\n";
c3254aee 2929 }
41a4d16e
GB
2930 # print out each stylesheet that exist, providing backwards capability
2931 # for those people who defined $stylesheet in a config file
b2d3476e 2932 if (defined $stylesheet) {
b2d3476e
AC
2933 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2934 } else {
2935 foreach my $stylesheet (@stylesheets) {
2936 next unless $stylesheet;
2937 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2938 }
2939 }
dd04c428 2940 if (defined $project) {
3562198b
JN
2941 my %href_params = get_feed_info();
2942 if (!exists $href_params{'-title'}) {
2943 $href_params{'-title'} = 'log';
2944 }
2945
2946 foreach my $format qw(RSS Atom) {
2947 my $type = lc($format);
2948 my %link_attr = (
2949 '-rel' => 'alternate',
2950 '-title' => "$project - $href_params{'-title'} - $format feed",
2951 '-type' => "application/$type+xml"
2952 );
2953
2954 $href_params{'action'} = $type;
2955 $link_attr{'-href'} = href(%href_params);
2956 print "<link ".
2957 "rel=\"$link_attr{'-rel'}\" ".
2958 "title=\"$link_attr{'-title'}\" ".
2959 "href=\"$link_attr{'-href'}\" ".
2960 "type=\"$link_attr{'-type'}\" ".
2961 "/>\n";
2962
2963 $href_params{'extra_options'} = '--no-merges';
2964 $link_attr{'-href'} = href(%href_params);
2965 $link_attr{'-title'} .= ' (no merges)';
2966 print "<link ".
2967 "rel=\"$link_attr{'-rel'}\" ".
2968 "title=\"$link_attr{'-title'}\" ".
2969 "href=\"$link_attr{'-href'}\" ".
2970 "type=\"$link_attr{'-type'}\" ".
2971 "/>\n";
2972 }
2973
9d0734ae
JN
2974 } else {
2975 printf('<link rel="alternate" title="%s projects list" '.
3562198b 2976 'href="%s" type="text/plain; charset=utf-8" />'."\n",
9d0734ae 2977 $site_name, href(project=>undef, action=>"project_index"));
af6feeb2 2978 printf('<link rel="alternate" title="%s projects feeds" '.
3562198b 2979 'href="%s" type="text/x-opml" />'."\n",
9d0734ae 2980 $site_name, href(project=>undef, action=>"opml"));
dd04c428 2981 }
0b5deba1 2982 if (defined $favicon) {
3562198b 2983 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
0b5deba1 2984 }
10161355 2985
dd04c428 2986 print "</head>\n" .
b2d3476e
AC
2987 "<body>\n";
2988
2989 if (-f $site_header) {
2dcb5e1a 2990 insert_file($site_header);
b2d3476e
AC
2991 }
2992
2993 print "<div class=\"page_header\">\n" .
9a7a62ff
JN
2994 $cgi->a({-href => esc_url($logo_url),
2995 -title => $logo_label},
2996 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
f93bff8d 2997 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
717b8311 2998 if (defined $project) {
1c2a4f5a 2999 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
717b8311
JN
3000 if (defined $action) {
3001 print " / $action";
3002 }
3003 print "\n";
6be93511 3004 }
d77b5673
PB
3005 print "</div>\n";
3006
25b2790f 3007 my $have_search = gitweb_check_feature('search');
f70dda25 3008 if (defined $project && $have_search) {
717b8311
JN
3009 if (!defined $searchtext) {
3010 $searchtext = "";
3011 }
3012 my $search_hash;
3013 if (defined $hash_base) {
3014 $search_hash = $hash_base;
3015 } elsif (defined $hash) {
3016 $search_hash = $hash;
bddec01d 3017 } else {
717b8311 3018 $search_hash = "HEAD";
bddec01d 3019 }
40375a83 3020 my $action = $my_uri;
25b2790f 3021 my $use_pathinfo = gitweb_check_feature('pathinfo');
40375a83 3022 if ($use_pathinfo) {
85d17a12 3023 $action .= "/".esc_url($project);
40375a83 3024 }
40375a83 3025 print $cgi->startform(-method => "get", -action => $action) .
717b8311 3026 "<div class=\"search\">\n" .
f70dda25
JN
3027 (!$use_pathinfo &&
3028 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3029 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3030 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
88ad729b 3031 $cgi->popup_menu(-name => 'st', -default => 'commit',
e7738553 3032 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
88ad729b
PB
3033 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3034 " search:\n",
717b8311 3035 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
0e559919
PB
3036 "<span title=\"Extended regular expression\">" .
3037 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3038 -checked => $search_use_regexp) .
3039 "</span>" .
717b8311
JN
3040 "</div>" .
3041 $cgi->end_form() . "\n";
b87d78d6 3042 }
717b8311
JN
3043}
3044
3045sub git_footer_html {
3562198b
JN
3046 my $feed_class = 'rss_logo';
3047
717b8311
JN
3048 print "<div class=\"page_footer\">\n";
3049 if (defined $project) {
847e01fb 3050 my $descr = git_get_project_description($project);
717b8311
JN
3051 if (defined $descr) {
3052 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3053 }
3562198b
JN
3054
3055 my %href_params = get_feed_info();
3056 if (!%href_params) {
3057 $feed_class .= ' generic';
3058 }
3059 $href_params{'-title'} ||= 'log';
3060
3061 foreach my $format qw(RSS Atom) {
3062 $href_params{'action'} = lc($format);
3063 print $cgi->a({-href => href(%href_params),
3064 -title => "$href_params{'-title'} $format feed",
3065 -class => $feed_class}, $format)."\n";
3066 }
3067
717b8311 3068 } else {
a1565c44 3069 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3562198b 3070 -class => $feed_class}, "OPML") . " ";
9d0734ae 3071 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3562198b 3072 -class => $feed_class}, "TXT") . "\n";
717b8311 3073 }
3562198b 3074 print "</div>\n"; # class="page_footer"
b2d3476e
AC
3075
3076 if (-f $site_footer) {
2dcb5e1a 3077 insert_file($site_footer);
b2d3476e
AC
3078 }
3079
3080 print "</body>\n" .
717b8311
JN
3081 "</html>";
3082}
3083
074afaa0
LW
3084# die_error(<http_status_code>, <error_message>)
3085# Example: die_error(404, 'Hash not found')
3086# By convention, use the following status codes (as defined in RFC 2616):
3087# 400: Invalid or missing CGI parameters, or
3088# requested object exists but has wrong type.
3089# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3090# this server or project.
3091# 404: Requested object/revision/project doesn't exist.
3092# 500: The server isn't configured properly, or
3093# an internal error occurred (e.g. failed assertions caused by bugs), or
3094# an unknown error occurred (e.g. the git binary died unexpectedly).
717b8311 3095sub die_error {
074afaa0
LW
3096 my $status = shift || 500;
3097 my $error = shift || "Internal server error";
3098
3099 my %http_responses = (400 => '400 Bad Request',
3100 403 => '403 Forbidden',
3101 404 => '404 Not Found',
3102 500 => '500 Internal Server Error');
3103 git_header_html($http_responses{$status});
59b9f61a
JN
3104 print <<EOF;
3105<div class="page_body">
3106<br /><br />
3107$status - $error
3108<br />
3109</div>
3110EOF
b87d78d6 3111 git_footer_html();
717b8311 3112 exit;
161332a5
KS
3113}
3114
717b8311
JN
3115## ----------------------------------------------------------------------
3116## functions printing or outputting HTML: navigation
3117
847e01fb 3118sub git_print_page_nav {
717b8311
JN
3119 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3120 $extra = '' if !defined $extra; # pager or formats
3121
3122 my @navs = qw(summary shortlog log commit commitdiff tree);
3123 if ($suppress) {
3124 @navs = grep { $_ ne $suppress } @navs;
3125 }
3126
1c2a4f5a 3127 my %arg = map { $_ => {action=>$_} } @navs;
717b8311
JN
3128 if (defined $head) {
3129 for (qw(commit commitdiff)) {
3be8e720 3130 $arg{$_}{'hash'} = $head;
717b8311
JN
3131 }
3132 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3133 for (qw(shortlog log)) {
3be8e720 3134 $arg{$_}{'hash'} = $head;
045e531a 3135 }
6a928415
KS
3136 }
3137 }
d627f68f 3138
3be8e720
JN
3139 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3140 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
717b8311 3141
a7c5a283 3142 my @actions = gitweb_get_feature('actions');
2b11e059
JN
3143 my %repl = (
3144 '%' => '%',
3145 'n' => $project, # project name
3146 'f' => $git_dir, # project path within filesystem
3147 'h' => $treehead || '', # current hash ('h' parameter)
3148 'b' => $treebase || '', # hash base ('hb' parameter)
3149 );
d627f68f 3150 while (@actions) {
2b11e059
JN
3151 my ($label, $link, $pos) = splice(@actions,0,3);
3152 # insert
d627f68f
PB
3153 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3154 # munch munch
2b11e059 3155 $link =~ s/%([%nfhb])/$repl{$1}/g;
d627f68f
PB
3156 $arg{$label}{'_href'} = $link;
3157 }
3158
717b8311
JN
3159 print "<div class=\"page_nav\">\n" .
3160 (join " | ",
1c2a4f5a 3161 map { $_ eq $current ?
d627f68f 3162 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
1c2a4f5a 3163 } @navs);
717b8311
JN
3164 print "<br/>\n$extra<br/>\n" .
3165 "</div>\n";
6a928415
KS
3166}
3167
847e01fb 3168sub format_paging_nav {
1f684dc0 3169 my ($action, $hash, $head, $page, $has_next_link) = @_;
717b8311 3170 my $paging_nav;
594e212b 3171
717b8311
JN
3172
3173 if ($hash ne $head || $page) {
1c2a4f5a 3174 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
594e212b 3175 } else {
717b8311
JN
3176 $paging_nav .= "HEAD";
3177 }
3178
3179 if ($page > 0) {
3180 $paging_nav .= " &sdot; " .
7afd77bf 3181 $cgi->a({-href => href(-replay=>1, page=>$page-1),
26298b5f 3182 -accesskey => "p", -title => "Alt-p"}, "prev");
717b8311
JN
3183 } else {
3184 $paging_nav .= " &sdot; prev";
3185 }
3186
1f684dc0 3187 if ($has_next_link) {
717b8311 3188 $paging_nav .= " &sdot; " .
7afd77bf 3189 $cgi->a({-href => href(-replay=>1, page=>$page+1),
26298b5f 3190 -accesskey => "n", -title => "Alt-n"}, "next");
717b8311
JN
3191 } else {
3192 $paging_nav .= " &sdot; next";
594e212b 3193 }
717b8311
JN
3194
3195 return $paging_nav;
594e212b
JN
3196}
3197
717b8311
JN
3198## ......................................................................
3199## functions printing or outputting HTML: div
3200
847e01fb 3201sub git_print_header_div {
717b8311 3202 my ($action, $title, $hash, $hash_base) = @_;
1c2a4f5a 3203 my %args = ();
717b8311 3204
3be8e720
JN
3205 $args{'action'} = $action;
3206 $args{'hash'} = $hash if $hash;
3207 $args{'hash_base'} = $hash_base if $hash_base;
717b8311
JN
3208
3209 print "<div class=\"header\">\n" .
1c2a4f5a
MW
3210 $cgi->a({-href => href(%args), -class => "title"},
3211 $title ? $title : $action) .
3212 "\n</div>\n";
717b8311 3213}
ede5e100 3214
6fd92a28
JN
3215sub git_print_authorship {
3216 my $co = shift;
3217
a44465cc 3218 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
6fd92a28
JN
3219 print "<div class=\"author_date\">" .
3220 esc_html($co->{'author_name'}) .
a44465cc
JN
3221 " [$ad{'rfc2822'}";
3222 if ($ad{'hour_local'} < 6) {
3223 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3224 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3225 } else {
3226 printf(" (%02d:%02d %s)",
3227 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3228 }
3229 print "]</div>\n";
6fd92a28
JN
3230}
3231
717b8311
JN
3232sub git_print_page_path {
3233 my $name = shift;
3234 my $type = shift;
59fb1c94 3235 my $hb = shift;
ede5e100 3236
4df118ed
JN
3237
3238 print "<div class=\"page_path\">";
3239 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
00f429af 3240 -title => 'tree root'}, to_utf8("[$project]"));
4df118ed
JN
3241 print " / ";
3242 if (defined $name) {
762c7205
JN
3243 my @dirname = split '/', $name;
3244 my $basename = pop @dirname;
3245 my $fullname = '';
3246
762c7205 3247 foreach my $dir (@dirname) {
16fdb488 3248 $fullname .= ($fullname ? '/' : '') . $dir;
762c7205
JN
3249 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3250 hash_base=>$hb),
edc04e90 3251 -title => $fullname}, esc_path($dir));
26d0a976 3252 print " / ";
762c7205
JN
3253 }
3254 if (defined $type && $type eq 'blob') {
952c65fc 3255 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
762c7205 3256 hash_base=>$hb),
edc04e90 3257 -title => $name}, esc_path($basename));
762c7205
JN
3258 } elsif (defined $type && $type eq 'tree') {
3259 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3260 hash_base=>$hb),
edc04e90 3261 -title => $name}, esc_path($basename));
4df118ed 3262 print " / ";
59fb1c94 3263 } else {
403d0906 3264 print esc_path($basename);
59fb1c94 3265 }
ede5e100 3266 }
4df118ed 3267 print "<br/></div>\n";
ede5e100
KS
3268}
3269
74fd8728 3270sub git_print_log {
d16d093c 3271 my $log = shift;
b7f9253d 3272 my %opts = @_;
d16d093c 3273
b7f9253d
JN
3274 if ($opts{'-remove_title'}) {
3275 # remove title, i.e. first line of log
3276 shift @$log;
3277 }
d16d093c
JN
3278 # remove leading empty lines
3279 while (defined $log->[0] && $log->[0] eq "") {
3280 shift @$log;
3281 }
3282
3283 # print log
3284 my $signoff = 0;
3285 my $empty = 0;
3286 foreach my $line (@$log) {
b7f9253d
JN
3287 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3288 $signoff = 1;
fba20b42 3289 $empty = 0;
b7f9253d
JN
3290 if (! $opts{'-remove_signoff'}) {
3291 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3292 next;
3293 } else {
3294 # remove signoff lines
3295 next;
3296 }
3297 } else {
3298 $signoff = 0;
3299 }
3300
d16d093c
JN
3301 # print only one empty line
3302 # do not print empty line after signoff
3303 if ($line eq "") {
3304 next if ($empty || $signoff);
3305 $empty = 1;
3306 } else {
3307 $empty = 0;
3308 }
b7f9253d
JN
3309
3310 print format_log_line_html($line) . "<br/>\n";
3311 }
3312
3313 if ($opts{'-final_empty_line'}) {
3314 # end with single empty line
3315 print "<br/>\n" unless $empty;
d16d093c
JN
3316 }
3317}
3318
e33fba4c
JN
3319# return link target (what link points to)
3320sub git_get_link_target {
3321 my $hash = shift;
3322 my $link_target;
3323
3324 # read link
3325 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3326 or return;
3327 {
34122b57 3328 local $/ = undef;
e33fba4c
JN
3329 $link_target = <$fd>;
3330 }
3331 close $fd
3332 or return;
3333
3334 return $link_target;
3335}
3336
3bf9d570
JN
3337# given link target, and the directory (basedir) the link is in,
3338# return target of link relative to top directory (top tree);
3339# return undef if it is not possible (including absolute links).
3340sub normalize_link_target {
3341 my ($link_target, $basedir, $hash_base) = @_;
3342
3343 # we can normalize symlink target only if $hash_base is provided
3344 return unless $hash_base;
3345
3346 # absolute symlinks (beginning with '/') cannot be normalized
3347 return if (substr($link_target, 0, 1) eq '/');
3348
3349 # normalize link target to path from top (root) tree (dir)
3350 my $path;
3351 if ($basedir) {
3352 $path = $basedir . '/' . $link_target;
3353 } else {
3354 # we are in top (root) tree (dir)
3355 $path = $link_target;
3356 }
3357
3358 # remove //, /./, and /../
3359 my @path_parts;
3360 foreach my $part (split('/', $path)) {
3361 # discard '.' and ''
3362 next if (!$part || $part eq '.');
3363 # handle '..'
3364 if ($part eq '..') {
3365 if (@path_parts) {
3366 pop @path_parts;
3367 } else {
3368 # link leads outside repository (outside top dir)
3369 return;
3370 }
3371 } else {
3372 push @path_parts, $part;
3373 }
3374 }
3375 $path = join('/', @path_parts);
3376
3377 return $path;
3378}
e33fba4c 3379
fa702003
JN
3380# print tree entry (row of git_tree), but without encompassing <tr> element
3381sub git_print_tree_entry {
3382 my ($t, $basedir, $hash_base, $have_blame) = @_;
3383
3384 my %base_key = ();
e33fba4c 3385 $base_key{'hash_base'} = $hash_base if defined $hash_base;
fa702003 3386
4de741b3
LT
3387 # The format of a table row is: mode list link. Where mode is
3388 # the mode of the entry, list is the name of the entry, an href,
3389 # and link is the action links of the entry.
3390
fa702003
JN
3391 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3392 if ($t->{'type'} eq "blob") {
3393 print "<td class=\"list\">" .
4de741b3 3394 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e7fb022a 3395 file_name=>"$basedir$t->{'name'}", %base_key),
e33fba4c
JN
3396 -class => "list"}, esc_path($t->{'name'}));
3397 if (S_ISLNK(oct $t->{'mode'})) {
3398 my $link_target = git_get_link_target($t->{'hash'});
3399 if ($link_target) {
3bf9d570
JN
3400 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3401 if (defined $norm_target) {
3402 print " -> " .
3403 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3404 file_name=>$norm_target),
3405 -title => $norm_target}, esc_path($link_target));
3406 } else {
3407 print " -> " . esc_path($link_target);
3408 }
e33fba4c
JN
3409 }
3410 }
3411 print "</td>\n";
4de741b3 3412 print "<td class=\"link\">";
4777b014 3413 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e33fba4c
JN
3414 file_name=>"$basedir$t->{'name'}", %base_key)},
3415 "blob");
fa702003 3416 if ($have_blame) {
4777b014
PB
3417 print " | " .
3418 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
e33fba4c
JN
3419 file_name=>"$basedir$t->{'name'}", %base_key)},
3420 "blame");
fa702003
JN
3421 }
3422 if (defined $hash_base) {
4777b014
PB
3423 print " | " .
3424 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003
JN
3425 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3426 "history");
3427 }
3428 print " | " .
6f7ea5fb 3429 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
e7fb022a
JN
3430 file_name=>"$basedir$t->{'name'}")},
3431 "raw");
4de741b3 3432 print "</td>\n";
fa702003
JN
3433
3434 } elsif ($t->{'type'} eq "tree") {
0fa105e7
LT
3435 print "<td class=\"list\">";
3436 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
fa702003 3437 file_name=>"$basedir$t->{'name'}", %base_key)},
403d0906 3438 esc_path($t->{'name'}));
0fa105e7
LT
3439 print "</td>\n";
3440 print "<td class=\"link\">";
4777b014 3441 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
e33fba4c
JN
3442 file_name=>"$basedir$t->{'name'}", %base_key)},
3443 "tree");
fa702003 3444 if (defined $hash_base) {
4777b014
PB
3445 print " | " .
3446 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003 3447 file_name=>"$basedir$t->{'name'}")},
01ac1e38
JN
3448 "history");
3449 }
3450 print "</td>\n";
3451 } else {
3452 # unknown object: we can only present history for it
3453 # (this includes 'commit' object, i.e. submodule support)
3454 print "<td class=\"list\">" .
3455 esc_path($t->{'name'}) .
3456 "</td>\n";
3457 print "<td class=\"link\">";
3458 if (defined $hash_base) {
3459 print $cgi->a({-href => href(action=>"history",
3460 hash_base=>$hash_base,
3461 file_name=>"$basedir$t->{'name'}")},
fa702003
JN
3462 "history");
3463 }
3464 print "</td>\n";
3465 }
3466}
3467
717b8311
JN
3468## ......................................................................
3469## functions printing large fragments of HTML
3470
0cec6db5 3471# get pre-image filenames for merge (combined) diff
e72c0eaf
JN
3472sub fill_from_file_info {
3473 my ($diff, @parents) = @_;
3474
3475 $diff->{'from_file'} = [ ];
3476 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3477 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3478 if ($diff->{'status'}[$i] eq 'R' ||
3479 $diff->{'status'}[$i] eq 'C') {
3480 $diff->{'from_file'}[$i] =
3481 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3482 }
3483 }
3484
3485 return $diff;
3486}
3487
0cec6db5 3488# is current raw difftree line of file deletion
90921740
JN
3489sub is_deleted {
3490 my $diffinfo = shift;
3491
4ed4a347 3492 return $diffinfo->{'to_id'} eq ('0' x 40);
90921740 3493}
e72c0eaf 3494
0cec6db5
JN
3495# does patch correspond to [previous] difftree raw line
3496# $diffinfo - hashref of parsed raw diff format
3497# $patchinfo - hashref of parsed patch diff format
3498# (the same keys as in $diffinfo)
3499sub is_patch_split {
3500 my ($diffinfo, $patchinfo) = @_;
3501
3502 return defined $diffinfo && defined $patchinfo
9d301456 3503 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
0cec6db5
JN
3504}
3505
3506
4a4a1a53 3507sub git_difftree_body {
ed224dea
JN
3508 my ($difftree, $hash, @parents) = @_;
3509 my ($parent) = $parents[0];
25b2790f 3510 my $have_blame = gitweb_check_feature('blame');
4a4a1a53
JN
3511 print "<div class=\"list_head\">\n";
3512 if ($#{$difftree} > 10) {
3513 print(($#{$difftree} + 1) . " files changed:\n");
3514 }
3515 print "</div>\n";
3516
ed224dea
JN
3517 print "<table class=\"" .
3518 (@parents > 1 ? "combined " : "") .
3519 "diff_tree\">\n";
47598d7a
JN
3520
3521 # header only for combined diff in 'commitdiff' view
3ef408ae 3522 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
47598d7a
JN
3523 if ($has_header) {
3524 # table header
3525 print "<thead><tr>\n" .
3526 "<th></th><th></th>\n"; # filename, patchN link
3527 for (my $i = 0; $i < @parents; $i++) {
3528 my $par = $parents[$i];
3529 print "<th>" .
3530 $cgi->a({-href => href(action=>"commitdiff",
3531 hash=>$hash, hash_parent=>$par),
3532 -title => 'commitdiff to parent number ' .
3533 ($i+1) . ': ' . substr($par,0,7)},
3534 $i+1) .
3535 "&nbsp;</th>\n";
3536 }
3537 print "</tr></thead>\n<tbody>\n";
3538 }
3539
6dd36acd 3540 my $alternate = 1;
b4657e77 3541 my $patchno = 0;
4a4a1a53 3542 foreach my $line (@{$difftree}) {
0cec6db5 3543 my $diff = parsed_difftree_line($line);
4a4a1a53
JN
3544
3545 if ($alternate) {
3546 print "<tr class=\"dark\">\n";
3547 } else {
3548 print "<tr class=\"light\">\n";
3549 }
3550 $alternate ^= 1;
3551
493e01db 3552 if (exists $diff->{'nparents'}) { # combined diff
ed224dea 3553
493e01db
JN
3554 fill_from_file_info($diff, @parents)
3555 unless exists $diff->{'from_file'};
e72c0eaf 3556
90921740 3557 if (!is_deleted($diff)) {
ed224dea
JN
3558 # file exists in the result (child) commit
3559 print "<td>" .
493e01db
JN
3560 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3561 file_name=>$diff->{'to_file'},
ed224dea 3562 hash_base=>$hash),
493e01db 3563 -class => "list"}, esc_path($diff->{'to_file'})) .
ed224dea
JN
3564 "</td>\n";
3565 } else {
3566 print "<td>" .
493e01db 3567 esc_path($diff->{'to_file'}) .
ed224dea
JN
3568 "</td>\n";
3569 }
3570
3571 if ($action eq 'commitdiff') {
3572 # link to patch
3573 $patchno++;
3574 print "<td class=\"link\">" .
3575 $cgi->a({-href => "#patch$patchno"}, "patch") .
3576 " | " .
3577 "</td>\n";
3578 }
3579
3580 my $has_history = 0;
3581 my $not_deleted = 0;
493e01db 3582 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
ed224dea 3583 my $hash_parent = $parents[$i];
493e01db
JN
3584 my $from_hash = $diff->{'from_id'}[$i];
3585 my $from_path = $diff->{'from_file'}[$i];
3586 my $status = $diff->{'status'}[$i];
ed224dea
JN
3587
3588 $has_history ||= ($status ne 'A');
3589 $not_deleted ||= ($status ne 'D');
3590
ed224dea
JN
3591 if ($status eq 'A') {
3592 print "<td class=\"link\" align=\"right\"> | </td>\n";
3593 } elsif ($status eq 'D') {
3594 print "<td class=\"link\">" .
3595 $cgi->a({-href => href(action=>"blob",
3596 hash_base=>$hash,
3597 hash=>$from_hash,
3598 file_name=>$from_path)},
3599 "blob" . ($i+1)) .
3600 " | </td>\n";
3601 } else {
493e01db 3602 if ($diff->{'to_id'} eq $from_hash) {
ed224dea
JN
3603 print "<td class=\"link nochange\">";
3604 } else {
3605 print "<td class=\"link\">";
3606 }
3607 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3608 hash=>$diff->{'to_id'},
ed224dea
JN
3609 hash_parent=>$from_hash,
3610 hash_base=>$hash,
3611 hash_parent_base=>$hash_parent,
493e01db 3612 file_name=>$diff->{'to_file'},
ed224dea
JN
3613 file_parent=>$from_path)},
3614 "diff" . ($i+1)) .
3615 " | </td>\n";
3616 }
3617 }
3618
3619 print "<td class=\"link\">";
3620 if ($not_deleted) {
3621 print $cgi->a({-href => href(action=>"blob",
493e01db
JN
3622 hash=>$diff->{'to_id'},
3623 file_name=>$diff->{'to_file'},
ed224dea
JN
3624 hash_base=>$hash)},
3625 "blob");
3626 print " | " if ($has_history);
3627 }
3628 if ($has_history) {
3629 print $cgi->a({-href => href(action=>"history",
493e01db 3630 file_name=>$diff->{'to_file'},
ed224dea
JN
3631 hash_base=>$hash)},
3632 "history");
3633 }
3634 print "</td>\n";
3635
3636 print "</tr>\n";
3637 next; # instead of 'else' clause, to avoid extra indent
3638 }
3639 # else ordinary diff
3640
e8e41a93
JN
3641 my ($to_mode_oct, $to_mode_str, $to_file_type);
3642 my ($from_mode_oct, $from_mode_str, $from_file_type);
493e01db
JN
3643 if ($diff->{'to_mode'} ne ('0' x 6)) {
3644 $to_mode_oct = oct $diff->{'to_mode'};
e8e41a93
JN
3645 if (S_ISREG($to_mode_oct)) { # only for regular file
3646 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3647 }
493e01db 3648 $to_file_type = file_type($diff->{'to_mode'});
e8e41a93 3649 }
493e01db
JN
3650 if ($diff->{'from_mode'} ne ('0' x 6)) {
3651 $from_mode_oct = oct $diff->{'from_mode'};
e8e41a93
JN
3652 if (S_ISREG($to_mode_oct)) { # only for regular file
3653 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4a4a1a53 3654 }
493e01db 3655 $from_file_type = file_type($diff->{'from_mode'});
e8e41a93
JN
3656 }
3657
493e01db 3658 if ($diff->{'status'} eq "A") { # created
e8e41a93
JN
3659 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3660 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3661 $mode_chng .= "]</span>";
499faeda 3662 print "<td>";
493e01db
JN
3663 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3664 hash_base=>$hash, file_name=>$diff->{'file'}),
3665 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3666 print "</td>\n";
3667 print "<td>$mode_chng</td>\n";
3668 print "<td class=\"link\">";
72dbafa1 3669 if ($action eq 'commitdiff') {
b4657e77
JN
3670 # link to patch
3671 $patchno++;
499faeda 3672 print $cgi->a({-href => "#patch$patchno"}, "patch");
897d1d2e 3673 print " | ";
b4657e77 3674 }
493e01db
JN
3675 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3676 hash_base=>$hash, file_name=>$diff->{'file'})},
3faa541f 3677 "blob");
b4657e77 3678 print "</td>\n";
4a4a1a53 3679
493e01db 3680 } elsif ($diff->{'status'} eq "D") { # deleted
e8e41a93 3681 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
499faeda 3682 print "<td>";
493e01db
JN
3683 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3684 hash_base=>$parent, file_name=>$diff->{'file'}),
3685 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3686 print "</td>\n";
3687 print "<td>$mode_chng</td>\n";
3688 print "<td class=\"link\">";
72dbafa1 3689 if ($action eq 'commitdiff') {
b4657e77
JN
3690 # link to patch
3691 $patchno++;
499faeda
LT
3692 print $cgi->a({-href => "#patch$patchno"}, "patch");
3693 print " | ";
b4657e77 3694 }
493e01db
JN
3695 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3696 hash_base=>$parent, file_name=>$diff->{'file'})},
897d1d2e 3697 "blob") . " | ";
2b2a8c78 3698 if ($have_blame) {
897d1d2e 3699 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
493e01db 3700 file_name=>$diff->{'file'})},
897d1d2e 3701 "blame") . " | ";
2b2a8c78 3702 }
b4657e77 3703 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
493e01db 3704 file_name=>$diff->{'file'})},
e7fb022a 3705 "history");
499faeda 3706 print "</td>\n";
4a4a1a53 3707
493e01db 3708 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4a4a1a53 3709 my $mode_chnge = "";
493e01db 3710 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93 3711 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6e72cf43 3712 if ($from_file_type ne $to_file_type) {
e8e41a93 3713 $mode_chnge .= " from $from_file_type to $to_file_type";
4a4a1a53 3714 }
e8e41a93
JN
3715 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3716 if ($from_mode_str && $to_mode_str) {
3717 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3718 } elsif ($to_mode_str) {
3719 $mode_chnge .= " mode: $to_mode_str";
4a4a1a53
JN
3720 }
3721 }
3722 $mode_chnge .= "]</span>\n";
3723 }
3724 print "<td>";
493e01db
JN
3725 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3726 hash_base=>$hash, file_name=>$diff->{'file'}),
3727 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3728 print "</td>\n";
3729 print "<td>$mode_chnge</td>\n";
3730 print "<td class=\"link\">";
241cc599
JN
3731 if ($action eq 'commitdiff') {
3732 # link to patch
3733 $patchno++;
3734 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3735 " | ";
493e01db 3736 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
3737 # "commit" view and modified file (not onlu mode changed)
3738 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3739 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 3740 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 3741 file_name=>$diff->{'file'})},
241cc599
JN
3742 "diff") .
3743 " | ";
4a4a1a53 3744 }
493e01db
JN
3745 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3746 hash_base=>$hash, file_name=>$diff->{'file'})},
897d1d2e 3747 "blob") . " | ";
2b2a8c78 3748 if ($have_blame) {
897d1d2e 3749 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 3750 file_name=>$diff->{'file'})},
897d1d2e 3751 "blame") . " | ";
2b2a8c78 3752 }
eb51ec9c 3753 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 3754 file_name=>$diff->{'file'})},
e7fb022a 3755 "history");
4a4a1a53
JN
3756 print "</td>\n";
3757
493e01db 3758 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
e8e41a93 3759 my %status_name = ('R' => 'moved', 'C' => 'copied');
493e01db 3760 my $nstatus = $status_name{$diff->{'status'}};
4a4a1a53 3761 my $mode_chng = "";
493e01db 3762 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93
JN
3763 # mode also for directories, so we cannot use $to_mode_str
3764 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4a4a1a53
JN
3765 }
3766 print "<td>" .
e8e41a93 3767 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
493e01db
JN
3768 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3769 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
e8e41a93
JN
3770 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3771 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
493e01db
JN
3772 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3773 -class => "list"}, esc_path($diff->{'from_file'})) .
3774 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
499faeda 3775 "<td class=\"link\">";
241cc599
JN
3776 if ($action eq 'commitdiff') {
3777 # link to patch
3778 $patchno++;
3779 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3780 " | ";
493e01db 3781 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
3782 # "commit" view and modified file (not only pure rename or copy)
3783 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3784 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 3785 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 3786 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
241cc599
JN
3787 "diff") .
3788 " | ";
4a4a1a53 3789 }
493e01db
JN
3790 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3791 hash_base=>$parent, file_name=>$diff->{'to_file'})},
897d1d2e 3792 "blob") . " | ";
2b2a8c78 3793 if ($have_blame) {
897d1d2e 3794 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 3795 file_name=>$diff->{'to_file'})},
897d1d2e 3796 "blame") . " | ";
2b2a8c78 3797 }
897d1d2e 3798 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 3799 file_name=>$diff->{'to_file'})},
e7fb022a 3800 "history");
4a4a1a53 3801 print "</td>\n";
e8e41a93 3802
4a4a1a53
JN
3803 } # we should not encounter Unmerged (U) or Unknown (X) status
3804 print "</tr>\n";
3805 }
47598d7a 3806 print "</tbody>" if $has_header;
4a4a1a53
JN
3807 print "</table>\n";
3808}
3809
eee08903 3810sub git_patchset_body {
e72c0eaf
JN
3811 my ($fd, $difftree, $hash, @hash_parents) = @_;
3812 my ($hash_parent) = $hash_parents[0];
eee08903 3813
0cec6db5 3814 my $is_combined = (@hash_parents > 1);
eee08903 3815 my $patch_idx = 0;
4280cde9 3816 my $patch_number = 0;
6d55f055 3817 my $patch_line;
fe87585e 3818 my $diffinfo;
0cec6db5 3819 my $to_name;
744d0ac3 3820 my (%from, %to);
eee08903
JN
3821
3822 print "<div class=\"patchset\">\n";
3823
6d55f055
JN
3824 # skip to first patch
3825 while ($patch_line = <$fd>) {
157e43b4 3826 chomp $patch_line;
eee08903 3827
6d55f055
JN
3828 last if ($patch_line =~ m/^diff /);
3829 }
3830
3831 PATCH:
3832 while ($patch_line) {
6d55f055 3833
0cec6db5
JN
3834 # parse "git diff" header line
3835 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3836 # $1 is from_name, which we do not use
3837 $to_name = unquote($2);
3838 $to_name =~ s!^b/!!;
3839 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3840 # $1 is 'cc' or 'combined', which we do not use
3841 $to_name = unquote($2);
3842 } else {
3843 $to_name = undef;
6d55f055 3844 }
6d55f055
JN
3845
3846 # check if current patch belong to current raw line
3847 # and parse raw git-diff line if needed
0cec6db5 3848 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
2206537c 3849 # this is continuation of a split patch
6d55f055
JN
3850 print "<div class=\"patch cont\">\n";
3851 } else {
3852 # advance raw git-diff output if needed
3853 $patch_idx++ if defined $diffinfo;
eee08903 3854
0cec6db5
JN
3855 # read and prepare patch information
3856 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 3857
0cec6db5
JN
3858 # compact combined diff output can have some patches skipped
3859 # find which patch (using pathname of result) we are at now;
3860 if ($is_combined) {
3861 while ($to_name ne $diffinfo->{'to_file'}) {
cd030c3a
JN
3862 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3863 format_diff_cc_simplified($diffinfo, @hash_parents) .
3864 "</div>\n"; # class="patch"
3865
3866 $patch_idx++;
3867 $patch_number++;
0cec6db5
JN
3868
3869 last if $patch_idx > $#$difftree;
3870 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 3871 }
0cec6db5 3872 }
711fa742 3873
90921740
JN
3874 # modifies %from, %to hashes
3875 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5f855052 3876
6d55f055
JN
3877 # this is first patch for raw difftree line with $patch_idx index
3878 # we index @$difftree array from 0, but number patches from 1
3879 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
744d0ac3 3880 }
eee08903 3881
0cec6db5
JN
3882 # git diff header
3883 #assert($patch_line =~ m/^diff /) if DEBUG;
3884 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3885 $patch_number++;
6d55f055 3886 # print "git diff" header
90921740
JN
3887 print format_git_diff_header_line($patch_line, $diffinfo,
3888 \%from, \%to);
6d55f055
JN
3889
3890 # print extended diff header
0cec6db5 3891 print "<div class=\"diff extended_header\">\n";
6d55f055 3892 EXTENDED_HEADER:
0cec6db5
JN
3893 while ($patch_line = <$fd>) {
3894 chomp $patch_line;
3895
3896 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3897
90921740
JN
3898 print format_extended_diff_header_line($patch_line, $diffinfo,
3899 \%from, \%to);
6d55f055 3900 }
0cec6db5 3901 print "</div>\n"; # class="diff extended_header"
6d55f055
JN
3902
3903 # from-file/to-file diff header
0bdb28c9
JN
3904 if (! $patch_line) {
3905 print "</div>\n"; # class="patch"
3906 last PATCH;
3907 }
66399eff 3908 next PATCH if ($patch_line =~ m/^diff /);
6d55f055 3909 #assert($patch_line =~ m/^---/) if DEBUG;
744d0ac3 3910
0cec6db5 3911 my $last_patch_line = $patch_line;
6d55f055 3912 $patch_line = <$fd>;
6d55f055 3913 chomp $patch_line;
90921740 3914 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
e4e4f825 3915
90921740 3916 print format_diff_from_to_header($last_patch_line, $patch_line,
91af4ce4
JN
3917 $diffinfo, \%from, \%to,
3918 @hash_parents);
e4e4f825 3919
6d55f055
JN
3920 # the patch itself
3921 LINE:
3922 while ($patch_line = <$fd>) {
3923 chomp $patch_line;
e4e4f825 3924
6d55f055 3925 next PATCH if ($patch_line =~ m/^diff /);
e4e4f825 3926
59e3b14e 3927 print format_diff_line($patch_line, \%from, \%to);
eee08903 3928 }
eee08903 3929
6d55f055
JN
3930 } continue {
3931 print "</div>\n"; # class="patch"
eee08903 3932 }
d26c4264 3933
cd030c3a
JN
3934 # for compact combined (--cc) format, with chunk and patch simpliciaction
3935 # patchset might be empty, but there might be unprocessed raw lines
0cec6db5 3936 for (++$patch_idx if $patch_number > 0;
cd030c3a 3937 $patch_idx < @$difftree;
0cec6db5 3938 ++$patch_idx) {
cd030c3a 3939 # read and prepare patch information
0cec6db5 3940 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a
JN
3941
3942 # generate anchor for "patch" links in difftree / whatchanged part
3943 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3944 format_diff_cc_simplified($diffinfo, @hash_parents) .
3945 "</div>\n"; # class="patch"
3946
3947 $patch_number++;
3948 }
3949
d26c4264
JN
3950 if ($patch_number == 0) {
3951 if (@hash_parents > 1) {
3952 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3953 } else {
3954 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3955 }
3956 }
eee08903
JN
3957
3958 print "</div>\n"; # class="patchset"
3959}
3960
3961# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3962
69913415
JN
3963# fills project list info (age, description, owner, forks) for each
3964# project in the list, removing invalid projects from returned list
3965# NOTE: modifies $projlist, but does not remove entries from it
3966sub fill_project_list_info {
3967 my ($projlist, $check_forks) = @_;
e30496df 3968 my @projects;
69913415 3969
25b2790f 3970 my $show_ctags = gitweb_check_feature('ctags');
69913415 3971 PROJECT:
e30496df 3972 foreach my $pr (@$projlist) {
69913415
JN
3973 my (@activity) = git_get_last_activity($pr->{'path'});
3974 unless (@activity) {
3975 next PROJECT;
e30496df 3976 }
69913415 3977 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
e30496df
PB
3978 if (!defined $pr->{'descr'}) {
3979 my $descr = git_get_project_description($pr->{'path'}) || "";
69913415
JN
3980 $descr = to_utf8($descr);
3981 $pr->{'descr_long'} = $descr;
55feb120 3982 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
e30496df
PB
3983 }
3984 if (!defined $pr->{'owner'}) {
76e4f5d0 3985 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
e30496df
PB
3986 }
3987 if ($check_forks) {
3988 my $pname = $pr->{'path'};
83ee94c1
JH
3989 if (($pname =~ s/\.git$//) &&
3990 ($pname !~ /\/$/) &&
3991 (-d "$projectroot/$pname")) {
3992 $pr->{'forks'} = "-d $projectroot/$pname";
69913415 3993 } else {
83ee94c1
JH
3994 $pr->{'forks'} = 0;
3995 }
e30496df 3996 }
aed93de4 3997 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
e30496df
PB
3998 push @projects, $pr;
3999 }
4000
69913415
JN
4001 return @projects;
4002}
4003
6b28da67
PB
4004# print 'sort by' <th> element, generating 'sort by $name' replay link
4005# if that order is not selected
7da0f3a4 4006sub print_sort_th {
6b28da67 4007 my ($name, $order, $header) = @_;
7da0f3a4
JN
4008 $header ||= ucfirst($name);
4009
4010 if ($order eq $name) {
7da0f3a4
JN
4011 print "<th>$header</th>\n";
4012 } else {
4013 print "<th>" .
4014 $cgi->a({-href => href(-replay=>1, order=>$name),
4015 -class => "header"}, $header) .
4016 "</th>\n";
4017 }
4018}
4019
69913415 4020sub git_project_list_body {
42326110 4021 # actually uses global variable $project
69913415
JN
4022 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4023
25b2790f 4024 my $check_forks = gitweb_check_feature('forks');
69913415
JN
4025 my @projects = fill_project_list_info($projlist, $check_forks);
4026
b06dcf8c 4027 $order ||= $default_projects_order;
e30496df
PB
4028 $from = 0 unless defined $from;
4029 $to = $#projects if (!defined $to || $#projects < $to);
4030
6b28da67
PB
4031 my %order_info = (
4032 project => { key => 'path', type => 'str' },
4033 descr => { key => 'descr_long', type => 'str' },
4034 owner => { key => 'owner', type => 'str' },
4035 age => { key => 'age', type => 'num' }
4036 );
4037 my $oi = $order_info{$order};
4038 if ($oi->{'type'} eq 'str') {
4039 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4040 } else {
4041 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4042 }
4043
25b2790f 4044 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
4045 if ($show_ctags) {
4046 my %ctags;
4047 foreach my $p (@projects) {
4048 foreach my $ct (keys %{$p->{'ctags'}}) {
4049 $ctags{$ct} += $p->{'ctags'}->{$ct};
4050 }
4051 }
4052 my $cloud = git_populate_project_tagcloud(\%ctags);
4053 print git_show_project_tagcloud($cloud, 64);
4054 }
4055
e30496df
PB
4056 print "<table class=\"project_list\">\n";
4057 unless ($no_header) {
4058 print "<tr>\n";
4059 if ($check_forks) {
4060 print "<th></th>\n";
4061 }
6b28da67
PB
4062 print_sort_th('project', $order, 'Project');
4063 print_sort_th('descr', $order, 'Description');
4064 print_sort_th('owner', $order, 'Owner');
4065 print_sort_th('age', $order, 'Last Change');
7da0f3a4 4066 print "<th></th>\n" . # for links
e30496df
PB
4067 "</tr>\n";
4068 }
4069 my $alternate = 1;
aed93de4 4070 my $tagfilter = $cgi->param('by_tag');
e30496df
PB
4071 for (my $i = $from; $i <= $to; $i++) {
4072 my $pr = $projects[$i];
42326110 4073
aed93de4 4074 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
0d1d154d
PB
4075 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4076 and not $pr->{'descr_long'} =~ /$searchtext/;
4077 # Weed out forks or non-matching entries of search
42326110
PB
4078 if ($check_forks) {
4079 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4080 $forkbase="^$forkbase" if $forkbase;
0d1d154d
PB
4081 next if not $searchtext and not $tagfilter and $show_ctags
4082 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
42326110
PB
4083 }
4084
e30496df
PB
4085 if ($alternate) {
4086 print "<tr class=\"dark\">\n";
4087 } else {
4088 print "<tr class=\"light\">\n";
4089 }
4090 $alternate ^= 1;
4091 if ($check_forks) {
4092 print "<td>";
4093 if ($pr->{'forks'}) {
83ee94c1 4094 print "<!-- $pr->{'forks'} -->\n";
e30496df
PB
4095 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4096 }
4097 print "</td>\n";
4098 }
4099 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4100 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
e88ce8a4
JN
4101 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4102 -class => "list", -title => $pr->{'descr_long'}},
4103 esc_html($pr->{'descr'})) . "</td>\n" .
d3cd2495 4104 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
e30496df 4105 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
785cdea9 4106 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
e30496df
PB
4107 "<td class=\"link\">" .
4108 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
faa1bbfd 4109 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
e30496df
PB
4110 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4111 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4112 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4113 "</td>\n" .
4114 "</tr>\n";
4115 }
4116 if (defined $extra) {
4117 print "<tr>\n";
4118 if ($check_forks) {
4119 print "<td></td>\n";
4120 }
4121 print "<td colspan=\"5\">$extra</td>\n" .
4122 "</tr>\n";
4123 }
4124 print "</table>\n";
4125}
4126
9f5dcb81
JN
4127sub git_shortlog_body {
4128 # uses global variable $project
190d7fdc 4129 my ($commitlist, $from, $to, $refs, $extra) = @_;
ddb8d900 4130
9f5dcb81 4131 $from = 0 unless defined $from;
190d7fdc 4132 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
9f5dcb81 4133
591ebf65 4134 print "<table class=\"shortlog\">\n";
6dd36acd 4135 my $alternate = 1;
9f5dcb81 4136 for (my $i = $from; $i <= $to; $i++) {
190d7fdc
RF
4137 my %co = %{$commitlist->[$i]};
4138 my $commit = $co{'id'};
847e01fb 4139 my $ref = format_ref_marker($refs, $commit);
9f5dcb81
JN
4140 if ($alternate) {
4141 print "<tr class=\"dark\">\n";
4142 } else {
4143 print "<tr class=\"light\">\n";
4144 }
4145 $alternate ^= 1;
ce58ec91 4146 my $author = chop_and_escape_str($co{'author_name'}, 10);
9f5dcb81
JN
4147 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4148 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4149 "<td><i>" . $author . "</i></td>\n" .
9f5dcb81 4150 "<td>";
952c65fc
JN
4151 print format_subject_html($co{'title'}, $co{'title_short'},
4152 href(action=>"commit", hash=>$commit), $ref);
9f5dcb81
JN
4153 print "</td>\n" .
4154 "<td class=\"link\">" .
4777b014 4155 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
35749ae5 4156 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
55ff35cb 4157 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
a3c8ab30
MM
4158 my $snapshot_links = format_snapshot_links($commit);
4159 if (defined $snapshot_links) {
4160 print " | " . $snapshot_links;
55ff35cb 4161 }
cb9c6e5b 4162 print "</td>\n" .
9f5dcb81
JN
4163 "</tr>\n";
4164 }
4165 if (defined $extra) {
4166 print "<tr>\n" .
4167 "<td colspan=\"4\">$extra</td>\n" .
4168 "</tr>\n";
4169 }
4170 print "</table>\n";
4171}
4172
581860e1
JN
4173sub git_history_body {
4174 # Warning: assumes constant type (blob or tree) during history
a8b983bf 4175 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
8be68352
JN
4176
4177 $from = 0 unless defined $from;
a8b983bf 4178 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
581860e1 4179
591ebf65 4180 print "<table class=\"history\">\n";
6dd36acd 4181 my $alternate = 1;
8be68352 4182 for (my $i = $from; $i <= $to; $i++) {
a8b983bf 4183 my %co = %{$commitlist->[$i]};
581860e1
JN
4184 if (!%co) {
4185 next;
4186 }
a8b983bf 4187 my $commit = $co{'id'};
581860e1
JN
4188
4189 my $ref = format_ref_marker($refs, $commit);
4190
4191 if ($alternate) {
4192 print "<tr class=\"dark\">\n";
4193 } else {
4194 print "<tr class=\"light\">\n";
4195 }
4196 $alternate ^= 1;
e076a0e7 4197 # shortlog uses chop_str($co{'author_name'}, 10)
ce58ec91 4198 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
581860e1 4199 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4200 "<td><i>" . $author . "</i></td>\n" .
581860e1
JN
4201 "<td>";
4202 # originally git_history used chop_str($co{'title'}, 50)
952c65fc
JN
4203 print format_subject_html($co{'title'}, $co{'title_short'},
4204 href(action=>"commit", hash=>$commit), $ref);
581860e1
JN
4205 print "</td>\n" .
4206 "<td class=\"link\">" .
6d81c5a2
LT
4207 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4208 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
581860e1
JN
4209
4210 if ($ftype eq 'blob') {
4211 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4212 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4213 if (defined $blob_current && defined $blob_parent &&
4214 $blob_current ne $blob_parent) {
4215 print " | " .
420e92f2
JN
4216 $cgi->a({-href => href(action=>"blobdiff",
4217 hash=>$blob_current, hash_parent=>$blob_parent,
4218 hash_base=>$hash_base, hash_parent_base=>$commit,
4219 file_name=>$file_name)},
581860e1
JN
4220 "diff to current");
4221 }
4222 }
4223 print "</td>\n" .
4224 "</tr>\n";
4225 }
4226 if (defined $extra) {
4227 print "<tr>\n" .
4228 "<td colspan=\"4\">$extra</td>\n" .
4229 "</tr>\n";
4230 }
4231 print "</table>\n";
4232}
4233
717b8311
JN
4234sub git_tags_body {
4235 # uses global variable $project
4236 my ($taglist, $from, $to, $extra) = @_;
4237 $from = 0 unless defined $from;
4238 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4239
591ebf65 4240 print "<table class=\"tags\">\n";
6dd36acd 4241 my $alternate = 1;
717b8311
JN
4242 for (my $i = $from; $i <= $to; $i++) {
4243 my $entry = $taglist->[$i];
4244 my %tag = %$entry;
cd146408 4245 my $comment = $tag{'subject'};
717b8311
JN
4246 my $comment_short;
4247 if (defined $comment) {
4248 $comment_short = chop_str($comment, 30, 5);
4249 }
4250 if ($alternate) {
4251 print "<tr class=\"dark\">\n";
4252 } else {
4253 print "<tr class=\"light\">\n";
4254 }
4255 $alternate ^= 1;
27dd1a83
JN
4256 if (defined $tag{'age'}) {
4257 print "<td><i>$tag{'age'}</i></td>\n";
4258 } else {
4259 print "<td></td>\n";
4260 }
4261 print "<td>" .
1c2a4f5a 4262 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
63e4220b 4263 -class => "list name"}, esc_html($tag{'name'})) .
717b8311
JN
4264 "</td>\n" .
4265 "<td>";
4266 if (defined $comment) {
952c65fc
JN
4267 print format_subject_html($comment, $comment_short,
4268 href(action=>"tag", hash=>$tag{'id'}));
717b8311
JN
4269 }
4270 print "</td>\n" .
4271 "<td class=\"selflink\">";
4272 if ($tag{'type'} eq "tag") {
1c2a4f5a 4273 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
717b8311
JN
4274 } else {
4275 print "&nbsp;";
4276 }
4277 print "</td>\n" .
4278 "<td class=\"link\">" . " | " .
1c2a4f5a 4279 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
717b8311 4280 if ($tag{'reftype'} eq "commit") {
bf901f8e
JN
4281 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4282 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
717b8311 4283 } elsif ($tag{'reftype'} eq "blob") {
1c2a4f5a 4284 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
717b8311
JN
4285 }
4286 print "</td>\n" .
4287 "</tr>";
4288 }
4289 if (defined $extra) {
4290 print "<tr>\n" .
4291 "<td colspan=\"5\">$extra</td>\n" .
4292 "</tr>\n";
4293 }
4294 print "</table>\n";
4295}
4296
4297sub git_heads_body {
4298 # uses global variable $project
120ddde2 4299 my ($headlist, $head, $from, $to, $extra) = @_;
717b8311 4300 $from = 0 unless defined $from;
120ddde2 4301 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
717b8311 4302
591ebf65 4303 print "<table class=\"heads\">\n";
6dd36acd 4304 my $alternate = 1;
717b8311 4305 for (my $i = $from; $i <= $to; $i++) {
120ddde2 4306 my $entry = $headlist->[$i];
cd146408
JN
4307 my %ref = %$entry;
4308 my $curr = $ref{'id'} eq $head;
717b8311
JN
4309 if ($alternate) {
4310 print "<tr class=\"dark\">\n";
4311 } else {
4312 print "<tr class=\"light\">\n";
4313 }
4314 $alternate ^= 1;
cd146408
JN
4315 print "<td><i>$ref{'age'}</i></td>\n" .
4316 ($curr ? "<td class=\"current_head\">" : "<td>") .
bf901f8e 4317 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
cd146408 4318 -class => "list name"},esc_html($ref{'name'})) .
717b8311
JN
4319 "</td>\n" .
4320 "<td class=\"link\">" .
bf901f8e
JN
4321 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4322 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4323 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
717b8311
JN
4324 "</td>\n" .
4325 "</tr>";
4326 }
4327 if (defined $extra) {
4328 print "<tr>\n" .
4329 "<td colspan=\"3\">$extra</td>\n" .
4330 "</tr>\n";
4331 }
4332 print "</table>\n";
4333}
4334
8dbc0fce 4335sub git_search_grep_body {
5ad66088 4336 my ($commitlist, $from, $to, $extra) = @_;
8dbc0fce 4337 $from = 0 unless defined $from;
5ad66088 4338 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
8dbc0fce 4339
591ebf65 4340 print "<table class=\"commit_search\">\n";
8dbc0fce
RF
4341 my $alternate = 1;
4342 for (my $i = $from; $i <= $to; $i++) {
5ad66088 4343 my %co = %{$commitlist->[$i]};
8dbc0fce
RF
4344 if (!%co) {
4345 next;
4346 }
5ad66088 4347 my $commit = $co{'id'};
8dbc0fce
RF
4348 if ($alternate) {
4349 print "<tr class=\"dark\">\n";
4350 } else {
4351 print "<tr class=\"light\">\n";
4352 }
4353 $alternate ^= 1;
ce58ec91 4354 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
8dbc0fce 4355 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4356 "<td><i>" . $author . "</i></td>\n" .
8dbc0fce 4357 "<td>" .
be8b9063
JH
4358 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4359 -class => "list subject"},
4360 chop_and_escape_str($co{'title'}, 50) . "<br/>");
8dbc0fce
RF
4361 my $comment = $co{'comment'};
4362 foreach my $line (@$comment) {
6dfbb304 4363 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
be8b9063 4364 my ($lead, $match, $trail) = ($1, $2, $3);
b8d97d07
JN
4365 $match = chop_str($match, 70, 5, 'center');
4366 my $contextlen = int((80 - length($match))/2);
4367 $contextlen = 30 if ($contextlen > 30);
4368 $lead = chop_str($lead, $contextlen, 10, 'left');
4369 $trail = chop_str($trail, $contextlen, 10, 'right');
be8b9063
JH
4370
4371 $lead = esc_html($lead);
4372 $match = esc_html($match);
4373 $trail = esc_html($trail);
4374
4375 print "$lead<span class=\"match\">$match</span>$trail<br />";
8dbc0fce
RF
4376 }
4377 }
4378 print "</td>\n" .
4379 "<td class=\"link\">" .
4380 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4381 " | " .
f1fe8f5c
CR
4382 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4383 " | " .
8dbc0fce
RF
4384 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4385 print "</td>\n" .
4386 "</tr>\n";
4387 }
4388 if (defined $extra) {
4389 print "<tr>\n" .
4390 "<td colspan=\"3\">$extra</td>\n" .
4391 "</tr>\n";
4392 }
4393 print "</table>\n";
4394}
4395
717b8311
JN
4396## ======================================================================
4397## ======================================================================
4398## actions
4399
717b8311 4400sub git_project_list {
1b2d297e 4401 my $order = $input_params{'order'};
b06dcf8c 4402 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 4403 die_error(400, "Unknown order parameter");
6326b60c
JN
4404 }
4405
847e01fb 4406 my @list = git_get_projects_list();
717b8311 4407 if (!@list) {
074afaa0 4408 die_error(404, "No projects found");
717b8311 4409 }
6326b60c 4410
717b8311
JN
4411 git_header_html();
4412 if (-f $home_text) {
4413 print "<div class=\"index_include\">\n";
2dcb5e1a 4414 insert_file($home_text);
717b8311 4415 print "</div>\n";
9f5dcb81 4416 }
0d1d154d
PB
4417 print $cgi->startform(-method => "get") .
4418 "<p class=\"projsearch\">Search:\n" .
4419 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4420 "</p>" .
4421 $cgi->end_form() . "\n";
e30496df
PB
4422 git_project_list_body(\@list, $order);
4423 git_footer_html();
4424}
4425
4426sub git_forks {
1b2d297e 4427 my $order = $input_params{'order'};
b06dcf8c 4428 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 4429 die_error(400, "Unknown order parameter");
717b8311 4430 }
e30496df
PB
4431
4432 my @list = git_get_projects_list($project);
4433 if (!@list) {
074afaa0 4434 die_error(404, "No forks found");
9f5dcb81 4435 }
e30496df
PB
4436
4437 git_header_html();
4438 git_print_page_nav('','');
4439 git_print_header_div('summary', "$project forks");
4440 git_project_list_body(\@list, $order);
717b8311 4441 git_footer_html();
9f5dcb81
JN
4442}
4443
fc2b2be0 4444sub git_project_index {
e30496df 4445 my @projects = git_get_projects_list($project);
fc2b2be0
JN
4446
4447 print $cgi->header(
4448 -type => 'text/plain',
4449 -charset => 'utf-8',
ab41dfbf 4450 -content_disposition => 'inline; filename="index.aux"');
fc2b2be0
JN
4451
4452 foreach my $pr (@projects) {
4453 if (!exists $pr->{'owner'}) {
76e4f5d0 4454 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
fc2b2be0
JN
4455 }
4456
4457 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4458 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4459 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4460 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4461 $path =~ s/ /\+/g;
4462 $owner =~ s/ /\+/g;
4463
4464 print "$path $owner\n";
4465 }
4466}
4467
ede5e100 4468sub git_summary {
847e01fb 4469 my $descr = git_get_project_description($project) || "none";
a979d128 4470 my %co = parse_commit("HEAD");
785cdea9 4471 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
a979d128 4472 my $head = $co{'id'};
ede5e100 4473
1e0cf030 4474 my $owner = git_get_project_owner($project);
ede5e100 4475
cd146408 4476 my $refs = git_get_references();
313ce8ce
RF
4477 # These get_*_list functions return one more to allow us to see if
4478 # there are more ...
4479 my @taglist = git_get_tags_list(16);
4480 my @headlist = git_get_heads_list(16);
e30496df 4481 my @forklist;
25b2790f 4482 my $check_forks = gitweb_check_feature('forks');
5dd5ed09
JH
4483
4484 if ($check_forks) {
e30496df
PB
4485 @forklist = git_get_projects_list($project);
4486 }
120ddde2 4487
ede5e100 4488 git_header_html();
847e01fb 4489 git_print_page_nav('summary','', $head);
9f5dcb81 4490
19806691 4491 print "<div class=\"title\">&nbsp;</div>\n";
591ebf65 4492 print "<table class=\"projects_list\">\n" .
a476142f
PB
4493 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4494 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
785cdea9 4495 if (defined $cd{'rfc2822'}) {
a476142f 4496 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
785cdea9
JN
4497 }
4498
e79ca7cc
JN
4499 # use per project git URL list in $projectroot/$project/cloneurl
4500 # or make project git URL from git base URL and project name
19a8721e 4501 my $url_tag = "URL";
e79ca7cc
JN
4502 my @url_list = git_get_project_url_list($project);
4503 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4504 foreach my $git_url (@url_list) {
4505 next unless $git_url;
a476142f 4506 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
19a8721e
JN
4507 $url_tag = "";
4508 }
aed93de4
PB
4509
4510 # Tag cloud
25b2790f 4511 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
4512 if ($show_ctags) {
4513 my $ctags = git_get_project_ctags($project);
4514 my $cloud = git_populate_project_tagcloud($ctags);
4515 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4516 print "</td>\n<td>" unless %$ctags;
4517 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4518 print "</td>\n<td>" if %$ctags;
4519 print git_show_project_tagcloud($cloud, 48);
4520 print "</td></tr>";
4521 }
4522
19a8721e 4523 print "</table>\n";
9f5dcb81 4524
7e1100e9
MM
4525 # If XSS prevention is on, we don't include README.html.
4526 # TODO: Allow a readme in some safe format.
4527 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
2dcb5e1a
JN
4528 print "<div class=\"title\">readme</div>\n" .
4529 "<div class=\"readme\">\n";
4530 insert_file("$projectroot/$project/README.html");
4531 print "\n</div>\n"; # class="readme"
447ef09a
PB
4532 }
4533
313ce8ce
RF
4534 # we need to request one more than 16 (0..15) to check if
4535 # those 16 are all
785cdea9
JN
4536 my @commitlist = $head ? parse_commits($head, 17) : ();
4537 if (@commitlist) {
4538 git_print_header_div('shortlog');
4539 git_shortlog_body(\@commitlist, 0, 15, $refs,
4540 $#commitlist <= 15 ? undef :
4541 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4542 }
ede5e100 4543
120ddde2 4544 if (@taglist) {
847e01fb 4545 git_print_header_div('tags');
120ddde2 4546 git_tags_body(\@taglist, 0, 15,
313ce8ce 4547 $#taglist <= 15 ? undef :
1c2a4f5a 4548 $cgi->a({-href => href(action=>"tags")}, "..."));
ede5e100 4549 }
0db37973 4550
120ddde2 4551 if (@headlist) {
847e01fb 4552 git_print_header_div('heads');
120ddde2 4553 git_heads_body(\@headlist, $head, 0, 15,
313ce8ce 4554 $#headlist <= 15 ? undef :
1c2a4f5a 4555 $cgi->a({-href => href(action=>"heads")}, "..."));
0db37973 4556 }
9f5dcb81 4557
e30496df
PB
4558 if (@forklist) {
4559 git_print_header_div('forks');
f04f27e8 4560 git_project_list_body(\@forklist, 'age', 0, 15,
aaca9675 4561 $#forklist <= 15 ? undef :
e30496df 4562 $cgi->a({-href => href(action=>"forks")}, "..."),
f04f27e8 4563 'no_header');
e30496df
PB
4564 }
4565
ede5e100
KS
4566 git_footer_html();
4567}
4568
d8a20ba9 4569sub git_tag {
847e01fb 4570 my $head = git_get_head_hash($project);
d8a20ba9 4571 git_header_html();
847e01fb
JN
4572 git_print_page_nav('','', $head,undef,$head);
4573 my %tag = parse_tag($hash);
198a2a8a
JN
4574
4575 if (! %tag) {
074afaa0 4576 die_error(404, "Unknown tag object");
198a2a8a
JN
4577 }
4578
847e01fb 4579 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
d8a20ba9 4580 print "<div class=\"title_text\">\n" .
591ebf65 4581 "<table class=\"object_header\">\n" .
e4669df9
KS
4582 "<tr>\n" .
4583 "<td>object</td>\n" .
952c65fc
JN
4584 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4585 $tag{'object'}) . "</td>\n" .
4586 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4587 $tag{'type'}) . "</td>\n" .
e4669df9 4588 "</tr>\n";
d8a20ba9 4589 if (defined($tag{'author'})) {
847e01fb 4590 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
40c13813 4591 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
952c65fc
JN
4592 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4593 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4594 "</td></tr>\n";
d8a20ba9
KS
4595 }
4596 print "</table>\n\n" .
4597 "</div>\n";
4598 print "<div class=\"page_body\">";
4599 my $comment = $tag{'comment'};
4600 foreach my $line (@$comment) {
7002243f 4601 chomp $line;
793c400c 4602 print esc_html($line, -nbsp=>1) . "<br/>\n";
d8a20ba9
KS
4603 }
4604 print "</div>\n";
4605 git_footer_html();
4606}
4607
3a5b919c 4608sub git_blame {
d2ce10d7 4609 # permissions
25b2790f 4610 gitweb_check_feature('blame')
d2ce10d7 4611 or die_error(403, "Blame view not allowed");
074afaa0 4612
d2ce10d7 4613 # error checking
074afaa0 4614 die_error(400, "No file name given") unless $file_name;
847e01fb 4615 $hash_base ||= git_get_head_hash($project);
d2ce10d7 4616 die_error(404, "Couldn't find base commit") unless $hash_base;
847e01fb 4617 my %co = parse_commit($hash_base)
074afaa0 4618 or die_error(404, "Commit not found");
d2ce10d7 4619 my $ftype = "blob";
1f2857ea
LT
4620 if (!defined $hash) {
4621 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
074afaa0 4622 or die_error(404, "Error looking up file");
d2ce10d7
JN
4623 } else {
4624 $ftype = git_get_type($hash);
4625 if ($ftype !~ "blob") {
4626 die_error(400, "Object is not a blob");
4627 }
1f2857ea 4628 }
d2ce10d7
JN
4629
4630 # run git-blame --porcelain
4631 open my $fd, "-|", git_cmd(), "blame", '-p',
4632 $hash_base, '--', $file_name
074afaa0 4633 or die_error(500, "Open git-blame failed");
d2ce10d7
JN
4634
4635 # page header
1f2857ea 4636 git_header_html();
0d83ddc4 4637 my $formats_nav =
a3823e5a 4638 $cgi->a({-href => href(action=>"blob", -replay=>1)},
952c65fc
JN
4639 "blob") .
4640 " | " .
a3823e5a
JN
4641 $cgi->a({-href => href(action=>"history", -replay=>1)},
4642 "history") .
cae1862a 4643 " | " .
952c65fc 4644 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
f35274da 4645 "HEAD");
847e01fb
JN
4646 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4647 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
59fb1c94 4648 git_print_page_path($file_name, $ftype, $hash_base);
d2ce10d7
JN
4649
4650 # page body
4651 my @rev_color = qw(light2 dark2);
cc1bf97e
LT
4652 my $num_colors = scalar(@rev_color);
4653 my $current_color = 0;
d2ce10d7
JN
4654 my %metainfo = ();
4655
59b9f61a
JN
4656 print <<HTML;
4657<div class="page_body">
4658<table class="blame">
4659<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4660HTML
d2ce10d7
JN
4661 LINE:
4662 while (my $line = <$fd>) {
4663 chomp $line;
4664 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4665 # no <lines in group> for subsequent lines in group of lines
d15c55aa 4666 my ($full_rev, $orig_lineno, $lineno, $group_size) =
d2ce10d7 4667 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
eeef88cd
JH
4668 if (!exists $metainfo{$full_rev}) {
4669 $metainfo{$full_rev} = {};
4670 }
4671 my $meta = $metainfo{$full_rev};
d2ce10d7
JN
4672 my $data;
4673 while ($data = <$fd>) {
4674 chomp $data;
4675 last if ($data =~ s/^\t//); # contents of line
4676 if ($data =~ /^(\S+) (.*)$/) {
eeef88cd
JH
4677 $meta->{$1} = $2;
4678 }
4679 }
d2ce10d7 4680 my $short_rev = substr($full_rev, 0, 8);
eeef88cd 4681 my $author = $meta->{'author'};
d2ce10d7
JN
4682 my %date =
4683 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
eeef88cd
JH
4684 my $date = $date{'iso-tz'};
4685 if ($group_size) {
d2ce10d7 4686 $current_color = ($current_color + 1) % $num_colors;
cc1bf97e 4687 }
4a24bfc2 4688 print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
eeef88cd
JH
4689 if ($group_size) {
4690 print "<td class=\"sha1\"";
5ad0828c 4691 print " title=\"". esc_html($author) . ", $date\"";
eeef88cd
JH
4692 print " rowspan=\"$group_size\"" if ($group_size > 1);
4693 print ">";
4694 print $cgi->a({-href => href(action=>"commit",
a23f0a73
JN
4695 hash=>$full_rev,
4696 file_name=>$file_name)},
d2ce10d7 4697 esc_html($short_rev));
eeef88cd 4698 print "</td>\n";
9dc5f8c9 4699 }
39c19ce2
JN
4700 my $parent_commit;
4701 if (!exists $meta->{'parent'}) {
4702 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4703 or die_error(500, "Open git-rev-parse failed");
4704 $parent_commit = <$dd>;
4705 close $dd;
4706 chomp($parent_commit);
4707 $meta->{'parent'} = $parent_commit;
4708 } else {
4709 $parent_commit = $meta->{'parent'};
4710 }
eeef88cd 4711 my $blamed = href(action => 'blame',
a23f0a73
JN
4712 file_name => $meta->{'filename'},
4713 hash_base => $parent_commit);
eeef88cd
JH
4714 print "<td class=\"linenr\">";
4715 print $cgi->a({ -href => "$blamed#l$orig_lineno",
a23f0a73
JN
4716 -class => "linenr" },
4717 esc_html($lineno));
eeef88cd 4718 print "</td>";
1f2857ea
LT
4719 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4720 print "</tr>\n";
4721 }
4722 print "</table>\n";
4723 print "</div>";
952c65fc
JN
4724 close $fd
4725 or print "Reading blob failed\n";
d2ce10d7
JN
4726
4727 # page footer
1f2857ea
LT
4728 git_footer_html();
4729}
4730
717b8311 4731sub git_tags {
847e01fb 4732 my $head = git_get_head_hash($project);
717b8311 4733 git_header_html();
847e01fb
JN
4734 git_print_page_nav('','', $head,undef,$head);
4735 git_print_header_div('summary', $project);
2d007374 4736
cd146408
JN
4737 my @tagslist = git_get_tags_list();
4738 if (@tagslist) {
4739 git_tags_body(\@tagslist);
2d007374 4740 }
717b8311 4741 git_footer_html();
2d007374
PB
4742}
4743
717b8311 4744sub git_heads {
847e01fb 4745 my $head = git_get_head_hash($project);
717b8311 4746 git_header_html();
847e01fb
JN
4747 git_print_page_nav('','', $head,undef,$head);
4748 git_print_header_div('summary', $project);
930cf7dd 4749
cd146408
JN
4750 my @headslist = git_get_heads_list();
4751 if (@headslist) {
4752 git_heads_body(\@headslist, $head);
f5aa79d9 4753 }
717b8311 4754 git_footer_html();
f5aa79d9
JN
4755}
4756
19806691 4757sub git_blob_plain {
7f718e8b 4758 my $type = shift;
f2e73302 4759 my $expires;
f2e73302 4760
cff0771b 4761 if (!defined $hash) {
5be01bc8 4762 if (defined $file_name) {
847e01fb 4763 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 4764 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 4765 or die_error(404, "Cannot find file");
5be01bc8 4766 } else {
074afaa0 4767 die_error(400, "No file name defined");
5be01bc8 4768 }
800764cf
MW
4769 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4770 # blobs defined by non-textual hash id's can be cached
4771 $expires = "+1d";
5be01bc8 4772 }
800764cf 4773
25691fbe 4774 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 4775 or die_error(500, "Open git-cat-file blob '$hash' failed");
930cf7dd 4776
7f718e8b
JN
4777 # content-type (can include charset)
4778 $type = blob_contenttype($fd, $file_name, $type);
f5aa79d9 4779
7f718e8b 4780 # "save as" filename, even when no $file_name is given
f5aa79d9 4781 my $save_as = "$hash";
9312944d
KS
4782 if (defined $file_name) {
4783 $save_as = $file_name;
f5aa79d9
JN
4784 } elsif ($type =~ m/^text\//) {
4785 $save_as .= '.txt';
9312944d 4786 }
f5aa79d9 4787
7e1100e9
MM
4788 # With XSS prevention on, blobs of all types except a few known safe
4789 # ones are served with "Content-Disposition: attachment" to make sure
4790 # they don't run in our security domain. For certain image types,
4791 # blob view writes an <img> tag referring to blob_plain view, and we
4792 # want to be sure not to break that by serving the image as an
4793 # attachment (though Firefox 3 doesn't seem to care).
4794 my $sandbox = $prevent_xss &&
4795 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4796
f2e73302 4797 print $cgi->header(
7f718e8b
JN
4798 -type => $type,
4799 -expires => $expires,
7e1100e9
MM
4800 -content_disposition =>
4801 ($sandbox ? 'attachment' : 'inline')
4802 . '; filename="' . $save_as . '"');
34122b57 4803 local $/ = undef;
ad14e931 4804 binmode STDOUT, ':raw';
19806691 4805 print <$fd>;
ad14e931 4806 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
19806691
KS
4807 close $fd;
4808}
4809
930cf7dd 4810sub git_blob {
f2e73302 4811 my $expires;
f2e73302 4812
cff0771b 4813 if (!defined $hash) {
5be01bc8 4814 if (defined $file_name) {
847e01fb 4815 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 4816 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 4817 or die_error(404, "Cannot find file");
5be01bc8 4818 } else {
074afaa0 4819 die_error(400, "No file name defined");
5be01bc8 4820 }
800764cf
MW
4821 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4822 # blobs defined by non-textual hash id's can be cached
4823 $expires = "+1d";
5be01bc8 4824 }
800764cf 4825
25b2790f 4826 my $have_blame = gitweb_check_feature('blame');
25691fbe 4827 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 4828 or die_error(500, "Couldn't cat $file_name, $hash");
847e01fb 4829 my $mimetype = blob_mimetype($fd, $file_name);
dfa7c7d2 4830 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
930cf7dd
LT
4831 close $fd;
4832 return git_blob_plain($mimetype);
4833 }
5a4cf334
JN
4834 # we can have blame only for text/* mimetype
4835 $have_blame &&= ($mimetype =~ m!^text/!);
4836
f2e73302 4837 git_header_html(undef, $expires);
0d83ddc4 4838 my $formats_nav = '';
847e01fb 4839 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
930cf7dd
LT
4840 if (defined $file_name) {
4841 if ($have_blame) {
952c65fc 4842 $formats_nav .=
a3823e5a 4843 $cgi->a({-href => href(action=>"blame", -replay=>1)},
952c65fc
JN
4844 "blame") .
4845 " | ";
930cf7dd 4846 }
0d83ddc4 4847 $formats_nav .=
a3823e5a 4848 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
4849 "history") .
4850 " | " .
a3823e5a 4851 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
35329cc1 4852 "raw") .
952c65fc
JN
4853 " | " .
4854 $cgi->a({-href => href(action=>"blob",
4855 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 4856 "HEAD");
930cf7dd 4857 } else {
952c65fc 4858 $formats_nav .=
a3823e5a
JN
4859 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4860 "raw");
930cf7dd 4861 }
847e01fb
JN
4862 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4863 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
930cf7dd
LT
4864 } else {
4865 print "<div class=\"page_nav\">\n" .
4866 "<br/><br/></div>\n" .
4867 "<div class=\"title\">$hash</div>\n";
4868 }
59fb1c94 4869 git_print_page_path($file_name, "blob", $hash_base);
930cf7dd 4870 print "<div class=\"page_body\">\n";
dfa7c7d2 4871 if ($mimetype =~ m!^image/!) {
5a4cf334
JN
4872 print qq!<img type="$mimetype"!;
4873 if ($file_name) {
4874 print qq! alt="$file_name" title="$file_name"!;
4875 }
4876 print qq! src="! .
4877 href(action=>"blob_plain", hash=>$hash,
4878 hash_base=>$hash_base, file_name=>$file_name) .
4879 qq!" />\n!;
dfa7c7d2
JN
4880 } else {
4881 my $nr;
4882 while (my $line = <$fd>) {
4883 chomp $line;
4884 $nr++;
4885 $line = untabify($line);
4886 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4887 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4888 }
930cf7dd 4889 }
952c65fc
JN
4890 close $fd
4891 or print "Reading blob failed.\n";
930cf7dd
LT
4892 print "</div>";
4893 git_footer_html();
4894}
4895
09bd7898 4896sub git_tree {
6f7ea5fb
LT
4897 if (!defined $hash_base) {
4898 $hash_base = "HEAD";
4899 }
b87d78d6 4900 if (!defined $hash) {
09bd7898 4901 if (defined $file_name) {
6f7ea5fb
LT
4902 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4903 } else {
4904 $hash = $hash_base;
10dba28d 4905 }
e925f38c 4906 }
2d7a3532 4907 die_error(404, "No such tree") unless defined($hash);
34122b57
JN
4908
4909 my @entries = ();
4910 {
4911 local $/ = "\0";
4912 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
4913 or die_error(500, "Open git-ls-tree failed");
4914 @entries = map { chomp; $_ } <$fd>;
4915 close $fd
4916 or die_error(404, "Reading tree failed");
4917 }
d63577da 4918
847e01fb
JN
4919 my $refs = git_get_references();
4920 my $ref = format_ref_marker($refs, $hash_base);
12a88f2f 4921 git_header_html();
300454fe 4922 my $basedir = '';
25b2790f 4923 my $have_blame = gitweb_check_feature('blame');
847e01fb 4924 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
cae1862a
PB
4925 my @views_nav = ();
4926 if (defined $file_name) {
4927 push @views_nav,
a3823e5a 4928 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
4929 "history"),
4930 $cgi->a({-href => href(action=>"tree",
4931 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 4932 "HEAD"),
cae1862a 4933 }
a3c8ab30
MM
4934 my $snapshot_links = format_snapshot_links($hash);
4935 if (defined $snapshot_links) {
cae1862a 4936 # FIXME: Should be available when we have no hash base as well.
a3c8ab30 4937 push @views_nav, $snapshot_links;
cae1862a
PB
4938 }
4939 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
847e01fb 4940 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
d63577da 4941 } else {
fa702003 4942 undef $hash_base;
d63577da
KS
4943 print "<div class=\"page_nav\">\n";
4944 print "<br/><br/></div>\n";
4945 print "<div class=\"title\">$hash</div>\n";
4946 }
09bd7898 4947 if (defined $file_name) {
300454fe
JN
4948 $basedir = $file_name;
4949 if ($basedir ne '' && substr($basedir, -1) ne '/') {
4950 $basedir .= '/';
4951 }
2d7a3532 4952 git_print_page_path($file_name, 'tree', $hash_base);
09bd7898 4953 }
fbb592a9 4954 print "<div class=\"page_body\">\n";
591ebf65 4955 print "<table class=\"tree\">\n";
6dd36acd 4956 my $alternate = 1;
b6b7fc72
JN
4957 # '..' (top directory) link if possible
4958 if (defined $hash_base &&
4959 defined $file_name && $file_name =~ m![^/]+$!) {
4960 if ($alternate) {
4961 print "<tr class=\"dark\">\n";
4962 } else {
4963 print "<tr class=\"light\">\n";
4964 }
4965 $alternate ^= 1;
4966
4967 my $up = $file_name;
4968 $up =~ s!/?[^/]+$!!;
4969 undef $up unless $up;
4970 # based on git_print_tree_entry
4971 print '<td class="mode">' . mode_str('040000') . "</td>\n";
4972 print '<td class="list">';
4973 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
4974 file_name=>$up)},
4975 "..");
4976 print "</td>\n";
4977 print "<td class=\"link\"></td>\n";
4978
4979 print "</tr>\n";
4980 }
161332a5 4981 foreach my $line (@entries) {
cb849b46
JN
4982 my %t = parse_ls_tree_line($line, -z => 1);
4983
bddec01d 4984 if ($alternate) {
c994d620 4985 print "<tr class=\"dark\">\n";
bddec01d 4986 } else {
c994d620 4987 print "<tr class=\"light\">\n";
bddec01d
KS
4988 }
4989 $alternate ^= 1;
cb849b46 4990
300454fe 4991 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
fa702003 4992
42f7eb94 4993 print "</tr>\n";
161332a5 4994 }
42f7eb94
KS
4995 print "</table>\n" .
4996 "</div>";
12a88f2f 4997 git_footer_html();
09bd7898
KS
4998}
4999
cb9c6e5b 5000sub git_snapshot {
1b2d297e 5001 my $format = $input_params{'snapshot_format'};
5e166843 5002 if (!@snapshot_fmts) {
074afaa0 5003 die_error(403, "Snapshots not allowed");
3473e7df
JN
5004 }
5005 # default to first supported snapshot format
5e166843 5006 $format ||= $snapshot_fmts[0];
3473e7df 5007 if ($format !~ m/^[a-z0-9]+$/) {
074afaa0 5008 die_error(400, "Invalid snapshot format parameter");
3473e7df 5009 } elsif (!exists($known_snapshot_formats{$format})) {
074afaa0 5010 die_error(400, "Unknown snapshot format");
5e166843 5011 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
074afaa0 5012 die_error(403, "Unsupported snapshot format");
ddb8d900
AK
5013 }
5014
cb9c6e5b
AK
5015 if (!defined $hash) {
5016 $hash = git_get_head_hash($project);
5017 }
5018
072570ee 5019 my $name = $project;
9a7d9410
ML
5020 $name =~ s,([^/])/*\.git$,$1,;
5021 $name = basename($name);
5022 my $filename = to_utf8($name);
072570ee 5023 $name =~ s/\047/\047\\\047\047/g;
072570ee 5024 my $cmd;
a3c8ab30 5025 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
516381d5
LW
5026 $cmd = quote_command(
5027 git_cmd(), 'archive',
5028 "--format=$known_snapshot_formats{$format}{'format'}",
5029 "--prefix=$name/", $hash);
a3c8ab30 5030 if (exists $known_snapshot_formats{$format}{'compressor'}) {
516381d5 5031 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
072570ee 5032 }
cb9c6e5b 5033
ab41dfbf 5034 print $cgi->header(
a3c8ab30 5035 -type => $known_snapshot_formats{$format}{'type'},
a2a3bf7b 5036 -content_disposition => 'inline; filename="' . "$filename" . '"',
ab41dfbf 5037 -status => '200 OK');
cb9c6e5b 5038
072570ee 5039 open my $fd, "-|", $cmd
074afaa0 5040 or die_error(500, "Execute git-archive failed");
cb9c6e5b
AK
5041 binmode STDOUT, ':raw';
5042 print <$fd>;
5043 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5044 close $fd;
cb9c6e5b
AK
5045}
5046
09bd7898 5047sub git_log {
847e01fb 5048 my $head = git_get_head_hash($project);
0db37973 5049 if (!defined $hash) {
19806691 5050 $hash = $head;
0db37973 5051 }
ea4a6df4
KS
5052 if (!defined $page) {
5053 $page = 0;
b87d78d6 5054 }
847e01fb 5055 my $refs = git_get_references();
ea4a6df4 5056
719dad28 5057 my @commitlist = parse_commits($hash, 101, (100 * $page));
ea4a6df4 5058
1f684dc0 5059 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
0d83ddc4 5060
75bf2cb2
GB
5061 my ($patch_max) = gitweb_get_feature('patches');
5062 if ($patch_max) {
5063 if ($patch_max < 0 || @commitlist <= $patch_max) {
5064 $paging_nav .= " &sdot; " .
5065 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5066 "patches");
5067 }
5068 }
5069
0d83ddc4 5070 git_header_html();
847e01fb 5071 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
0d83ddc4 5072
719dad28 5073 if (!@commitlist) {
847e01fb 5074 my %co = parse_commit($hash);
27fb8c40 5075
847e01fb 5076 git_print_header_div('summary', $project);
e925f38c 5077 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
161332a5 5078 }
719dad28
RF
5079 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5080 for (my $i = 0; $i <= $to; $i++) {
5081 my %co = %{$commitlist[$i]};
b87d78d6 5082 next if !%co;
719dad28
RF
5083 my $commit = $co{'id'};
5084 my $ref = format_ref_marker($refs, $commit);
847e01fb
JN
5085 my %ad = parse_date($co{'author_epoch'});
5086 git_print_header_div('commit',
26298b5f
JN
5087 "<span class=\"age\">$co{'age_string'}</span>" .
5088 esc_html($co{'title'}) . $ref,
5089 $commit);
034df39e
KS
5090 print "<div class=\"title_text\">\n" .
5091 "<div class=\"log_link\">\n" .
1c2a4f5a 5092 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
952c65fc
JN
5093 " | " .
5094 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
6ef4cb2e 5095 " | " .
d7267207 5096 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
eb28240b 5097 "<br/>\n" .
034df39e 5098 "</div>\n" .
40c13813 5099 "<i>" . esc_html($co{'author_name'}) . " [$ad{'rfc2822'}]</i><br/>\n" .
d16d093c
JN
5100 "</div>\n";
5101
5102 print "<div class=\"log_body\">\n";
f2069411 5103 git_print_log($co{'comment'}, -final_empty_line=> 1);
09bd7898 5104 print "</div>\n";
719dad28
RF
5105 }
5106 if ($#commitlist >= 100) {
5107 print "<div class=\"page_nav\">\n";
7afd77bf 5108 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
719dad28
RF
5109 -accesskey => "n", -title => "Alt-n"}, "next");
5110 print "</div>\n";
e334d18c 5111 }
034df39e 5112 git_footer_html();
09bd7898
KS
5113}
5114
5115sub git_commit {
9954f772 5116 $hash ||= $hash_base || "HEAD";
074afaa0
LW
5117 my %co = parse_commit($hash)
5118 or die_error(404, "Unknown commit object");
847e01fb
JN
5119 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5120 my %cd = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
161332a5 5121
c9d193df
JN
5122 my $parent = $co{'parent'};
5123 my $parents = $co{'parents'}; # listref
5124
5125 # we need to prepare $formats_nav before any parameter munging
5126 my $formats_nav;
5127 if (!defined $parent) {
5128 # --root commitdiff
5129 $formats_nav .= '(initial)';
5130 } elsif (@$parents == 1) {
5131 # single parent commit
5132 $formats_nav .=
5133 '(parent: ' .
5134 $cgi->a({-href => href(action=>"commit",
5135 hash=>$parent)},
5136 esc_html(substr($parent, 0, 7))) .
5137 ')';
5138 } else {
5139 # merge commit
5140 $formats_nav .=
5141 '(merge: ' .
5142 join(' ', map {
f9308a18 5143 $cgi->a({-href => href(action=>"commit",
c9d193df
JN
5144 hash=>$_)},
5145 esc_html(substr($_, 0, 7)));
5146 } @$parents ) .
5147 ')';
5148 }
75bf2cb2
GB
5149 if (gitweb_check_feature('patches')) {
5150 $formats_nav .= " | " .
5151 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5152 "patch");
5153 }
c9d193df 5154
d8a20ba9 5155 if (!defined $parent) {
b9182987 5156 $parent = "--root";
6191f8e1 5157 }
549ab4a3 5158 my @difftree;
208ecb2e
JN
5159 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5160 @diff_opts,
5161 (@$parents <= 1 ? $parent : '-c'),
5162 $hash, "--"
074afaa0 5163 or die_error(500, "Open git-diff-tree failed");
208ecb2e 5164 @difftree = map { chomp; $_ } <$fd>;
074afaa0 5165 close $fd or die_error(404, "Reading git-diff-tree failed");
11044297
KS
5166
5167 # non-textual hash id's can be cached
5168 my $expires;
5169 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5170 $expires = "+1d";
5171 }
847e01fb
JN
5172 my $refs = git_get_references();
5173 my $ref = format_ref_marker($refs, $co{'id'});
ddb8d900 5174
594e212b 5175 git_header_html(undef, $expires);
a144154f 5176 git_print_page_nav('commit', '',
952c65fc 5177 $hash, $co{'tree'}, $hash,
c9d193df 5178 $formats_nav);
4f7b34c9 5179
b87d78d6 5180 if (defined $co{'parent'}) {
847e01fb 5181 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
b87d78d6 5182 } else {
847e01fb 5183 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
b87d78d6 5184 }
6191f8e1 5185 print "<div class=\"title_text\">\n" .
591ebf65 5186 "<table class=\"object_header\">\n";
40c13813 5187 print "<tr><td>author</td><td>" . esc_html($co{'author'}) . "</td></tr>\n".
bddec01d
KS
5188 "<tr>" .
5189 "<td></td><td> $ad{'rfc2822'}";
927dcec4 5190 if ($ad{'hour_local'} < 6) {
952c65fc
JN
5191 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
5192 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
b87d78d6 5193 } else {
952c65fc
JN
5194 printf(" (%02d:%02d %s)",
5195 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
b87d78d6 5196 }
bddec01d
KS
5197 print "</td>" .
5198 "</tr>\n";
40c13813 5199 print "<tr><td>committer</td><td>" . esc_html($co{'committer'}) . "</td></tr>\n";
952c65fc
JN
5200 print "<tr><td></td><td> $cd{'rfc2822'}" .
5201 sprintf(" (%02d:%02d %s)", $cd{'hour_local'}, $cd{'minute_local'}, $cd{'tz_local'}) .
5202 "</td></tr>\n";
1f1ab5f0 5203 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
bddec01d
KS
5204 print "<tr>" .
5205 "<td>tree</td>" .
1f1ab5f0 5206 "<td class=\"sha1\">" .
952c65fc
JN
5207 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5208 class => "list"}, $co{'tree'}) .
19806691 5209 "</td>" .
952c65fc
JN
5210 "<td class=\"link\">" .
5211 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5212 "tree");
a3c8ab30
MM
5213 my $snapshot_links = format_snapshot_links($hash);
5214 if (defined $snapshot_links) {
5215 print " | " . $snapshot_links;
cb9c6e5b
AK
5216 }
5217 print "</td>" .
bddec01d 5218 "</tr>\n";
549ab4a3 5219
3e029299 5220 foreach my $par (@$parents) {
bddec01d
KS
5221 print "<tr>" .
5222 "<td>parent</td>" .
952c65fc
JN
5223 "<td class=\"sha1\">" .
5224 $cgi->a({-href => href(action=>"commit", hash=>$par),
5225 class => "list"}, $par) .
5226 "</td>" .
bddec01d 5227 "<td class=\"link\">" .
1c2a4f5a 5228 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
952c65fc 5229 " | " .
f2e60947 5230 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
bddec01d
KS
5231 "</td>" .
5232 "</tr>\n";
3e029299 5233 }
7a9b4c5f 5234 print "</table>".
b87d78d6 5235 "</div>\n";
d16d093c 5236
fbb592a9 5237 print "<div class=\"page_body\">\n";
d16d093c 5238 git_print_log($co{'comment'});
927dcec4 5239 print "</div>\n";
4a4a1a53 5240
208ecb2e 5241 git_difftree_body(\@difftree, $hash, @$parents);
4a4a1a53 5242
12a88f2f 5243 git_footer_html();
09bd7898
KS
5244}
5245
ca94601c
JN
5246sub git_object {
5247 # object is defined by:
5248 # - hash or hash_base alone
5249 # - hash_base and file_name
5250 my $type;
5251
5252 # - hash or hash_base alone
5253 if ($hash || ($hash_base && !defined $file_name)) {
5254 my $object_id = $hash || $hash_base;
5255
516381d5
LW
5256 open my $fd, "-|", quote_command(
5257 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
074afaa0 5258 or die_error(404, "Object does not exist");
ca94601c
JN
5259 $type = <$fd>;
5260 chomp $type;
5261 close $fd
074afaa0 5262 or die_error(404, "Object does not exist");
ca94601c
JN
5263
5264 # - hash_base and file_name
5265 } elsif ($hash_base && defined $file_name) {
5266 $file_name =~ s,/+$,,;
5267
5268 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
074afaa0 5269 or die_error(404, "Base object does not exist");
ca94601c
JN
5270
5271 # here errors should not hapen
5272 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
074afaa0 5273 or die_error(500, "Open git-ls-tree failed");
ca94601c
JN
5274 my $line = <$fd>;
5275 close $fd;
5276
5277 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5278 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
074afaa0 5279 die_error(404, "File or directory for given base does not exist");
ca94601c
JN
5280 }
5281 $type = $2;
5282 $hash = $3;
5283 } else {
074afaa0 5284 die_error(400, "Not enough information to find object");
ca94601c
JN
5285 }
5286
5287 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5288 hash=>$hash, hash_base=>$hash_base,
5289 file_name=>$file_name),
5290 -status => '302 Found');
5291}
5292
09bd7898 5293sub git_blobdiff {
9b71b1f6
JN
5294 my $format = shift || 'html';
5295
7c5e2ebb
JN
5296 my $fd;
5297 my @difftree;
5298 my %diffinfo;
9b71b1f6 5299 my $expires;
7c5e2ebb
JN
5300
5301 # preparing $fd and %diffinfo for git_patchset_body
5302 # new style URI
5303 if (defined $hash_base && defined $hash_parent_base) {
5304 if (defined $file_name) {
5305 # read raw output
45bd0c80
JN
5306 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5307 $hash_parent_base, $hash_base,
5ae917ac 5308 "--", (defined $file_parent ? $file_parent : ()), $file_name
074afaa0 5309 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
5310 @difftree = map { chomp; $_ } <$fd>;
5311 close $fd
074afaa0 5312 or die_error(404, "Reading git-diff-tree failed");
7c5e2ebb 5313 @difftree
074afaa0 5314 or die_error(404, "Blob diff not found");
7c5e2ebb 5315
0aea3376
JN
5316 } elsif (defined $hash &&
5317 $hash =~ /[0-9a-fA-F]{40}/) {
5318 # try to find filename from $hash
7c5e2ebb
JN
5319
5320 # read filtered raw output
45bd0c80
JN
5321 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5322 $hash_parent_base, $hash_base, "--"
074afaa0 5323 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
5324 @difftree =
5325 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5326 # $hash == to_id
5327 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5328 map { chomp; $_ } <$fd>;
5329 close $fd
074afaa0 5330 or die_error(404, "Reading git-diff-tree failed");
7c5e2ebb 5331 @difftree
074afaa0 5332 or die_error(404, "Blob diff not found");
7c5e2ebb
JN
5333
5334 } else {
074afaa0 5335 die_error(400, "Missing one of the blob diff parameters");
7c5e2ebb
JN
5336 }
5337
5338 if (@difftree > 1) {
074afaa0 5339 die_error(400, "Ambiguous blob diff specification");
7c5e2ebb
JN
5340 }
5341
5342 %diffinfo = parse_difftree_raw_line($difftree[0]);
9d301456
JN
5343 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5344 $file_name ||= $diffinfo{'to_file'};
7c5e2ebb
JN
5345
5346 $hash_parent ||= $diffinfo{'from_id'};
5347 $hash ||= $diffinfo{'to_id'};
5348
9b71b1f6
JN
5349 # non-textual hash id's can be cached
5350 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5351 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5352 $expires = '+1d';
5353 }
5354
7c5e2ebb 5355 # open patch output
25691fbe 5356 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
957d6ea7
JN
5357 '-p', ($format eq 'html' ? "--full-index" : ()),
5358 $hash_parent_base, $hash_base,
5ae917ac 5359 "--", (defined $file_parent ? $file_parent : ()), $file_name
074afaa0 5360 or die_error(500, "Open git-diff-tree failed");
7c5e2ebb
JN
5361 }
5362
b54dc9fd
JH
5363 # old/legacy style URI -- not generated anymore since 1.4.3.
5364 if (!%diffinfo) {
5365 die_error('404 Not Found', "Missing one of the blob diff parameters")
7c5e2ebb
JN
5366 }
5367
5368 # header
9b71b1f6
JN
5369 if ($format eq 'html') {
5370 my $formats_nav =
a3823e5a 5371 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
35329cc1 5372 "raw");
9b71b1f6
JN
5373 git_header_html(undef, $expires);
5374 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5375 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5376 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5377 } else {
5378 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5379 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5380 }
5381 if (defined $file_name) {
5382 git_print_page_path($file_name, "blob", $hash_base);
5383 } else {
5384 print "<div class=\"page_path\"></div>\n";
5385 }
5386
5387 } elsif ($format eq 'plain') {
5388 print $cgi->header(
5389 -type => 'text/plain',
5390 -charset => 'utf-8',
5391 -expires => $expires,
a2a3bf7b 5392 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
9b71b1f6
JN
5393
5394 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5395
7c5e2ebb 5396 } else {
074afaa0 5397 die_error(400, "Unknown blobdiff format");
7c5e2ebb
JN
5398 }
5399
5400 # patch
9b71b1f6
JN
5401 if ($format eq 'html') {
5402 print "<div class=\"page_body\">\n";
7c5e2ebb 5403
9b71b1f6
JN
5404 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5405 close $fd;
7c5e2ebb 5406
9b71b1f6
JN
5407 print "</div>\n"; # class="page_body"
5408 git_footer_html();
5409
5410 } else {
5411 while (my $line = <$fd>) {
403d0906
JN
5412 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5413 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9b71b1f6
JN
5414
5415 print $line;
5416
5417 last if $line =~ m!^\+\+\+!;
5418 }
5419 local $/ = undef;
5420 print <$fd>;
5421 close $fd;
5422 }
09bd7898
KS
5423}
5424
19806691 5425sub git_blobdiff_plain {
9b71b1f6 5426 git_blobdiff('plain');
19806691
KS
5427}
5428
09bd7898 5429sub git_commitdiff {
20209854
GB
5430 my %params = @_;
5431 my $format = $params{-format} || 'html';
9872cd6f 5432
75bf2cb2 5433 my ($patch_max) = gitweb_get_feature('patches');
9872cd6f 5434 if ($format eq 'patch') {
9872cd6f
GB
5435 die_error(403, "Patch view not allowed") unless $patch_max;
5436 }
5437
9954f772 5438 $hash ||= $hash_base || "HEAD";
074afaa0
LW
5439 my %co = parse_commit($hash)
5440 or die_error(404, "Unknown commit object");
151602df 5441
cd030c3a
JN
5442 # choose format for commitdiff for merge
5443 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5444 $hash_parent = '--cc';
5445 }
5446 # we need to prepare $formats_nav before almost any parameter munging
151602df
JN
5447 my $formats_nav;
5448 if ($format eq 'html') {
5449 $formats_nav =
a3823e5a 5450 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
151602df 5451 "raw");
75bf2cb2
GB
5452 if ($patch_max) {
5453 $formats_nav .= " | " .
5454 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5455 "patch");
5456 }
151602df 5457
cd030c3a
JN
5458 if (defined $hash_parent &&
5459 $hash_parent ne '-c' && $hash_parent ne '--cc') {
151602df
JN
5460 # commitdiff with two commits given
5461 my $hash_parent_short = $hash_parent;
5462 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5463 $hash_parent_short = substr($hash_parent, 0, 7);
5464 }
5465 $formats_nav .=
ada3e1f7
JN
5466 ' (from';
5467 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5468 if ($co{'parents'}[$i] eq $hash_parent) {
5469 $formats_nav .= ' parent ' . ($i+1);
5470 last;
5471 }
5472 }
5473 $formats_nav .= ': ' .
151602df
JN
5474 $cgi->a({-href => href(action=>"commitdiff",
5475 hash=>$hash_parent)},
5476 esc_html($hash_parent_short)) .
5477 ')';
5478 } elsif (!$co{'parent'}) {
5479 # --root commitdiff
5480 $formats_nav .= ' (initial)';
5481 } elsif (scalar @{$co{'parents'}} == 1) {
5482 # single parent commit
5483 $formats_nav .=
5484 ' (parent: ' .
5485 $cgi->a({-href => href(action=>"commitdiff",
5486 hash=>$co{'parent'})},
5487 esc_html(substr($co{'parent'}, 0, 7))) .
5488 ')';
5489 } else {
5490 # merge commit
cd030c3a
JN
5491 if ($hash_parent eq '--cc') {
5492 $formats_nav .= ' | ' .
5493 $cgi->a({-href => href(action=>"commitdiff",
5494 hash=>$hash, hash_parent=>'-c')},
5495 'combined');
5496 } else { # $hash_parent eq '-c'
5497 $formats_nav .= ' | ' .
5498 $cgi->a({-href => href(action=>"commitdiff",
5499 hash=>$hash, hash_parent=>'--cc')},
5500 'compact');
5501 }
151602df
JN
5502 $formats_nav .=
5503 ' (merge: ' .
5504 join(' ', map {
5505 $cgi->a({-href => href(action=>"commitdiff",
5506 hash=>$_)},
5507 esc_html(substr($_, 0, 7)));
5508 } @{$co{'parents'}} ) .
5509 ')';
5510 }
5511 }
5512
fb1dde4a 5513 my $hash_parent_param = $hash_parent;
cd030c3a
JN
5514 if (!defined $hash_parent_param) {
5515 # --cc for multiple parents, --root for parentless
fb1dde4a 5516 $hash_parent_param =
cd030c3a 5517 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
bddec01d 5518 }
eee08903
JN
5519
5520 # read commitdiff
5521 my $fd;
5522 my @difftree;
eee08903 5523 if ($format eq 'html') {
25691fbe 5524 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
45bd0c80 5525 "--no-commit-id", "--patch-with-raw", "--full-index",
fb1dde4a 5526 $hash_parent_param, $hash, "--"
074afaa0 5527 or die_error(500, "Open git-diff-tree failed");
eee08903 5528
04408c35
JN
5529 while (my $line = <$fd>) {
5530 chomp $line;
eee08903
JN
5531 # empty line ends raw part of diff-tree output
5532 last unless $line;
493e01db 5533 push @difftree, scalar parse_difftree_raw_line($line);
eee08903 5534 }
eee08903 5535
eee08903 5536 } elsif ($format eq 'plain') {
25691fbe 5537 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
fb1dde4a 5538 '-p', $hash_parent_param, $hash, "--"
074afaa0 5539 or die_error(500, "Open git-diff-tree failed");
9872cd6f
GB
5540 } elsif ($format eq 'patch') {
5541 # For commit ranges, we limit the output to the number of
5542 # patches specified in the 'patches' feature.
5543 # For single commits, we limit the output to a single patch,
5544 # diverging from the git-format-patch default.
5545 my @commit_spec = ();
5546 if ($hash_parent) {
5547 if ($patch_max > 0) {
5548 push @commit_spec, "-$patch_max";
5549 }
5550 push @commit_spec, '-n', "$hash_parent..$hash";
5551 } else {
a3411f8a
GB
5552 if ($params{-single}) {
5553 push @commit_spec, '-1';
5554 } else {
5555 if ($patch_max > 0) {
5556 push @commit_spec, "-$patch_max";
5557 }
5558 push @commit_spec, "-n";
5559 }
5560 push @commit_spec, '--root', $hash;
9872cd6f
GB
5561 }
5562 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5563 '--stdout', @commit_spec
5564 or die_error(500, "Open git-format-patch failed");
eee08903 5565 } else {
074afaa0 5566 die_error(400, "Unknown commitdiff format");
eee08903 5567 }
161332a5 5568
11044297
KS
5569 # non-textual hash id's can be cached
5570 my $expires;
5571 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5572 $expires = "+1d";
5573 }
09bd7898 5574
eee08903
JN
5575 # write commit message
5576 if ($format eq 'html') {
5577 my $refs = git_get_references();
5578 my $ref = format_ref_marker($refs, $co{'id'});
1b1cd421 5579
eee08903
JN
5580 git_header_html(undef, $expires);
5581 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5582 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6fd92a28 5583 git_print_authorship(\%co);
eee08903 5584 print "<div class=\"page_body\">\n";
82560983
JN
5585 if (@{$co{'comment'}} > 1) {
5586 print "<div class=\"log\">\n";
5587 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5588 print "</div>\n"; # class="log"
5589 }
eee08903
JN
5590
5591 } elsif ($format eq 'plain') {
5592 my $refs = git_get_references("tags");
edf735ab 5593 my $tagname = git_get_rev_name_tags($hash);
eee08903
JN
5594 my $filename = basename($project) . "-$hash.patch";
5595
5596 print $cgi->header(
5597 -type => 'text/plain',
5598 -charset => 'utf-8',
5599 -expires => $expires,
a2a3bf7b 5600 -content_disposition => 'inline; filename="' . "$filename" . '"');
eee08903 5601 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
7720224c
YS
5602 print "From: " . to_utf8($co{'author'}) . "\n";
5603 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5604 print "Subject: " . to_utf8($co{'title'}) . "\n";
5605
edf735ab 5606 print "X-Git-Tag: $tagname\n" if $tagname;
eee08903 5607 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
edf735ab 5608
eee08903 5609 foreach my $line (@{$co{'comment'}}) {
7720224c 5610 print to_utf8($line) . "\n";
eee08903
JN
5611 }
5612 print "---\n\n";
9872cd6f
GB
5613 } elsif ($format eq 'patch') {
5614 my $filename = basename($project) . "-$hash.patch";
5615
5616 print $cgi->header(
5617 -type => 'text/plain',
5618 -charset => 'utf-8',
5619 -expires => $expires,
5620 -content_disposition => 'inline; filename="' . "$filename" . '"');
1b1cd421 5621 }
1b1cd421 5622
eee08903
JN
5623 # write patch
5624 if ($format eq 'html') {
cd030c3a
JN
5625 my $use_parents = !defined $hash_parent ||
5626 $hash_parent eq '-c' || $hash_parent eq '--cc';
5627 git_difftree_body(\@difftree, $hash,
5628 $use_parents ? @{$co{'parents'}} : $hash_parent);
b4657e77 5629 print "<br/>\n";
1b1cd421 5630
cd030c3a
JN
5631 git_patchset_body($fd, \@difftree, $hash,
5632 $use_parents ? @{$co{'parents'}} : $hash_parent);
157e43b4 5633 close $fd;
eee08903
JN
5634 print "</div>\n"; # class="page_body"
5635 git_footer_html();
5636
5637 } elsif ($format eq 'plain') {
5638 local $/ = undef;
5639 print <$fd>;
5640 close $fd
5641 or print "Reading git-diff-tree failed\n";
9872cd6f
GB
5642 } elsif ($format eq 'patch') {
5643 local $/ = undef;
5644 print <$fd>;
5645 close $fd
5646 or print "Reading git-format-patch failed\n";
19806691
KS
5647 }
5648}
5649
eee08903 5650sub git_commitdiff_plain {
20209854 5651 git_commitdiff(-format => 'plain');
eee08903
JN
5652}
5653
9872cd6f
GB
5654# format-patch-style patches
5655sub git_patch {
a3411f8a
GB
5656 git_commitdiff(-format => 'patch', -single=> 1);
5657}
5658
5659sub git_patches {
20209854 5660 git_commitdiff(-format => 'patch');
eee08903
JN
5661}
5662
09bd7898 5663sub git_history {
c6e1d9ed 5664 if (!defined $hash_base) {
847e01fb 5665 $hash_base = git_get_head_hash($project);
09bd7898 5666 }
8be68352
JN
5667 if (!defined $page) {
5668 $page = 0;
5669 }
63433106 5670 my $ftype;
074afaa0
LW
5671 my %co = parse_commit($hash_base)
5672 or die_error(404, "Unknown commit object");
8be68352 5673
847e01fb 5674 my $refs = git_get_references();
8be68352
JN
5675 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5676
5634cf24 5677 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
074afaa0
LW
5678 $file_name, "--full-history")
5679 or die_error(404, "No such file or directory on given branch");
5634cf24 5680
93d5f061 5681 if (!defined $hash && defined $file_name) {
5634cf24
JN
5682 # some commits could have deleted file in question,
5683 # and not have it in tree, but one of them has to have it
5684 for (my $i = 0; $i <= @commitlist; $i++) {
5685 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5686 last if defined $hash;
5687 }
93d5f061 5688 }
cff0771b 5689 if (defined $hash) {
63433106 5690 $ftype = git_get_type($hash);
cff0771b 5691 }
5634cf24 5692 if (!defined $ftype) {
074afaa0 5693 die_error(500, "Unknown type of object");
5634cf24 5694 }
25691fbe 5695
8be68352
JN
5696 my $paging_nav = '';
5697 if ($page > 0) {
5698 $paging_nav .=
5699 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5700 file_name=>$file_name)},
5701 "first");
5702 $paging_nav .= " &sdot; " .
7afd77bf 5703 $cgi->a({-href => href(-replay=>1, page=>$page-1),
8be68352
JN
5704 -accesskey => "p", -title => "Alt-p"}, "prev");
5705 } else {
5706 $paging_nav .= "first";
5707 $paging_nav .= " &sdot; prev";
5708 }
8be68352 5709 my $next_link = '';
a8b983bf 5710 if ($#commitlist >= 100) {
8be68352 5711 $next_link =
7afd77bf 5712 $cgi->a({-href => href(-replay=>1, page=>$page+1),
a8b983bf 5713 -accesskey => "n", -title => "Alt-n"}, "next");
7afd77bf
JN
5714 $paging_nav .= " &sdot; $next_link";
5715 } else {
5716 $paging_nav .= " &sdot; next";
8be68352
JN
5717 }
5718
5719 git_header_html();
5720 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5721 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5722 git_print_page_path($file_name, $ftype, $hash_base);
5723
a8b983bf 5724 git_history_body(\@commitlist, 0, 99,
8be68352 5725 $refs, $hash_base, $ftype, $next_link);
581860e1 5726
d51e902a 5727 git_footer_html();
161332a5 5728}
19806691
KS
5729
5730sub git_search {
25b2790f 5731 gitweb_check_feature('search') or die_error(403, "Search is disabled");
19806691 5732 if (!defined $searchtext) {
074afaa0 5733 die_error(400, "Text field is empty");
19806691
KS
5734 }
5735 if (!defined $hash) {
847e01fb 5736 $hash = git_get_head_hash($project);
19806691 5737 }
847e01fb 5738 my %co = parse_commit($hash);
19806691 5739 if (!%co) {
074afaa0 5740 die_error(404, "Unknown commit object");
19806691 5741 }
8dbc0fce
RF
5742 if (!defined $page) {
5743 $page = 0;
5744 }
04f7a94f 5745
88ad729b
PB
5746 $searchtype ||= 'commit';
5747 if ($searchtype eq 'pickaxe') {
04f7a94f
JN
5748 # pickaxe may take all resources of your box and run for several minutes
5749 # with every query - so decide by yourself how public you make this feature
25b2790f 5750 gitweb_check_feature('pickaxe')
074afaa0 5751 or die_error(403, "Pickaxe is disabled");
c994d620 5752 }
e7738553 5753 if ($searchtype eq 'grep') {
25b2790f 5754 gitweb_check_feature('grep')
074afaa0 5755 or die_error(403, "Grep is disabled");
e7738553 5756 }
88ad729b 5757
19806691 5758 git_header_html();
19806691 5759
88ad729b 5760 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
8e574fb5
RF
5761 my $greptype;
5762 if ($searchtype eq 'commit') {
5763 $greptype = "--grep=";
5764 } elsif ($searchtype eq 'author') {
5765 $greptype = "--author=";
5766 } elsif ($searchtype eq 'committer') {
5767 $greptype = "--committer=";
5768 }
0270cd0e
JN
5769 $greptype .= $searchtext;
5770 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
0e559919
PB
5771 $greptype, '--regexp-ignore-case',
5772 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
8dbc0fce
RF
5773
5774 my $paging_nav = '';
5775 if ($page > 0) {
5776 $paging_nav .=
5777 $cgi->a({-href => href(action=>"search", hash=>$hash,
0270cd0e
JN
5778 searchtext=>$searchtext,
5779 searchtype=>$searchtype)},
a23f0a73 5780 "first");
8dbc0fce 5781 $paging_nav .= " &sdot; " .
7afd77bf 5782 $cgi->a({-href => href(-replay=>1, page=>$page-1),
a23f0a73 5783 -accesskey => "p", -title => "Alt-p"}, "prev");
8dbc0fce
RF
5784 } else {
5785 $paging_nav .= "first";
5786 $paging_nav .= " &sdot; prev";
5787 }
7afd77bf 5788 my $next_link = '';
5ad66088 5789 if ($#commitlist >= 100) {
7afd77bf
JN
5790 $next_link =
5791 $cgi->a({-href => href(-replay=>1, page=>$page+1),
a23f0a73 5792 -accesskey => "n", -title => "Alt-n"}, "next");
7afd77bf 5793 $paging_nav .= " &sdot; $next_link";
8dbc0fce
RF
5794 } else {
5795 $paging_nav .= " &sdot; next";
5796 }
7afd77bf 5797
5ad66088 5798 if ($#commitlist >= 100) {
8dbc0fce
RF
5799 }
5800
5801 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5802 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5ad66088 5803 git_search_grep_body(\@commitlist, 0, 99, $next_link);
c994d620
KS
5804 }
5805
88ad729b 5806 if ($searchtype eq 'pickaxe') {
8dbc0fce
RF
5807 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5808 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5809
591ebf65 5810 print "<table class=\"pickaxe search\">\n";
8dbc0fce 5811 my $alternate = 1;
34122b57 5812 local $/ = "\n";
c582abae
JN
5813 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5814 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5815 ($search_use_regexp ? '--pickaxe-regex' : ());
c994d620
KS
5816 undef %co;
5817 my @files;
5818 while (my $line = <$fd>) {
c582abae
JN
5819 chomp $line;
5820 next unless $line;
5821
5822 my %set = parse_difftree_raw_line($line);
5823 if (defined $set{'commit'}) {
5824 # finish previous commit
c994d620 5825 if (%co) {
c994d620
KS
5826 print "</td>\n" .
5827 "<td class=\"link\">" .
756d2f06 5828 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
952c65fc
JN
5829 " | " .
5830 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
c994d620
KS
5831 print "</td>\n" .
5832 "</tr>\n";
5833 }
c582abae
JN
5834
5835 if ($alternate) {
5836 print "<tr class=\"dark\">\n";
5837 } else {
5838 print "<tr class=\"light\">\n";
5839 }
5840 $alternate ^= 1;
5841 %co = parse_commit($set{'commit'});
5842 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
5843 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
5844 "<td><i>$author</i></td>\n" .
5845 "<td>" .
5846 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
5847 -class => "list subject"},
5848 chop_and_escape_str($co{'title'}, 50) . "<br/>");
5849 } elsif (defined $set{'to_id'}) {
5850 next if ($set{'to_id'} =~ m/^0{40}$/);
5851
5852 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
5853 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
5854 -class => "list"},
5855 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
5856 "<br/>\n";
19806691 5857 }
19806691 5858 }
c994d620 5859 close $fd;
8dbc0fce 5860
c582abae
JN
5861 # finish last commit (warning: repetition!)
5862 if (%co) {
5863 print "</td>\n" .
5864 "<td class=\"link\">" .
5865 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
5866 " | " .
5867 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
5868 print "</td>\n" .
5869 "</tr>\n";
5870 }
5871
8dbc0fce 5872 print "</table>\n";
19806691 5873 }
e7738553
PB
5874
5875 if ($searchtype eq 'grep') {
5876 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5877 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5878
591ebf65 5879 print "<table class=\"grep_search\">\n";
e7738553
PB
5880 my $alternate = 1;
5881 my $matches = 0;
34122b57 5882 local $/ = "\n";
0e559919
PB
5883 open my $fd, "-|", git_cmd(), 'grep', '-n',
5884 $search_use_regexp ? ('-E', '-i') : '-F',
5885 $searchtext, $co{'tree'};
e7738553
PB
5886 my $lastfile = '';
5887 while (my $line = <$fd>) {
5888 chomp $line;
5889 my ($file, $lno, $ltext, $binary);
5890 last if ($matches++ > 1000);
5891 if ($line =~ /^Binary file (.+) matches$/) {
5892 $file = $1;
5893 $binary = 1;
5894 } else {
5895 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
5896 }
5897 if ($file ne $lastfile) {
5898 $lastfile and print "</td></tr>\n";
5899 if ($alternate++) {
5900 print "<tr class=\"dark\">\n";
5901 } else {
5902 print "<tr class=\"light\">\n";
5903 }
5904 print "<td class=\"list\">".
5905 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5906 file_name=>"$file"),
5907 -class => "list"}, esc_path($file));
5908 print "</td><td>\n";
5909 $lastfile = $file;
5910 }
5911 if ($binary) {
5912 print "<div class=\"binary\">Binary file</div>\n";
5913 } else {
5914 $ltext = untabify($ltext);
0e559919 5915 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
e7738553
PB
5916 $ltext = esc_html($1, -nbsp=>1);
5917 $ltext .= '<span class="match">';
5918 $ltext .= esc_html($2, -nbsp=>1);
5919 $ltext .= '</span>';
5920 $ltext .= esc_html($3, -nbsp=>1);
5921 } else {
5922 $ltext = esc_html($ltext, -nbsp=>1);
5923 }
5924 print "<div class=\"pre\">" .
5925 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
5926 file_name=>"$file").'#l'.$lno,
5927 -class => "linenr"}, sprintf('%4i', $lno))
5928 . ' ' . $ltext . "</div>\n";
5929 }
5930 }
5931 if ($lastfile) {
5932 print "</td></tr>\n";
5933 if ($matches > 1000) {
5934 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
5935 }
5936 } else {
5937 print "<div class=\"diff nodifferences\">No matches found</div>\n";
5938 }
5939 close $fd;
5940
5941 print "</table>\n";
5942 }
19806691
KS
5943 git_footer_html();
5944}
5945
88ad729b
PB
5946sub git_search_help {
5947 git_header_html();
5948 git_print_page_nav('','', $hash,$hash,$hash);
5949 print <<EOT;
0e559919
PB
5950<p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
5951regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
5952the pattern entered is recognized as the POSIX extended
5953<a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
5954insensitive).</p>
88ad729b
PB
5955<dl>
5956<dt><b>commit</b></dt>
0e559919 5957<dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
e7738553 5958EOT
25b2790f 5959 my $have_grep = gitweb_check_feature('grep');
e7738553
PB
5960 if ($have_grep) {
5961 print <<EOT;
5962<dt><b>grep</b></dt>
5963<dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
0e559919
PB
5964 a different one) are searched for the given pattern. On large trees, this search can take
5965a while and put some strain on the server, so please use it with some consideration. Note that
5966due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
5967case-sensitive.</dd>
e7738553
PB
5968EOT
5969 }
5970 print <<EOT;
88ad729b 5971<dt><b>author</b></dt>
0e559919 5972<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 5973<dt><b>committer</b></dt>
0e559919 5974<dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
88ad729b 5975EOT
25b2790f 5976 my $have_pickaxe = gitweb_check_feature('pickaxe');
88ad729b
PB
5977 if ($have_pickaxe) {
5978 print <<EOT;
5979<dt><b>pickaxe</b></dt>
5980<dd>All commits that caused the string to appear or disappear from any file (changes that
5981added, removed or "modified" the string) will be listed. This search can take a while and
0e559919
PB
5982takes a lot of strain on the server, so please use it wisely. Note that since you may be
5983interested even in changes just changing the case as well, this search is case sensitive.</dd>
88ad729b
PB
5984EOT
5985 }
5986 print "</dl>\n";
5987 git_footer_html();
5988}
5989
19806691 5990sub git_shortlog {
847e01fb 5991 my $head = git_get_head_hash($project);
19806691
KS
5992 if (!defined $hash) {
5993 $hash = $head;
5994 }
ea4a6df4
KS
5995 if (!defined $page) {
5996 $page = 0;
5997 }
847e01fb 5998 my $refs = git_get_references();
ea4a6df4 5999
ec3e97b8
GB
6000 my $commit_hash = $hash;
6001 if (defined $hash_parent) {
6002 $commit_hash = "$hash_parent..$hash";
6003 }
6004 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
ea4a6df4 6005
1f684dc0 6006 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
9f5dcb81 6007 my $next_link = '';
190d7fdc 6008 if ($#commitlist >= 100) {
9f5dcb81 6009 $next_link =
7afd77bf 6010 $cgi->a({-href => href(-replay=>1, page=>$page+1),
190d7fdc 6011 -accesskey => "n", -title => "Alt-n"}, "next");
9f5dcb81 6012 }
75bf2cb2
GB
6013 my $patch_max = gitweb_check_feature('patches');
6014 if ($patch_max) {
6015 if ($patch_max < 0 || @commitlist <= $patch_max) {
6016 $paging_nav .= " &sdot; " .
6017 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6018 "patches");
6019 }
6020 }
9f5dcb81 6021
0d83ddc4 6022 git_header_html();
847e01fb
JN
6023 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6024 git_print_header_div('summary', $project);
0d83ddc4 6025
190d7fdc 6026 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
9f5dcb81 6027
19806691
KS
6028 git_footer_html();
6029}
717b8311
JN
6030
6031## ......................................................................
af6feeb2 6032## feeds (RSS, Atom; OPML)
717b8311 6033
af6feeb2
JN
6034sub git_feed {
6035 my $format = shift || 'atom';
25b2790f 6036 my $have_blame = gitweb_check_feature('blame');
af6feeb2
JN
6037
6038 # Atom: http://www.atomenabled.org/developers/syndication/
6039 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6040 if ($format ne 'rss' && $format ne 'atom') {
074afaa0 6041 die_error(400, "Unknown web feed format");
af6feeb2
JN
6042 }
6043
6044 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6045 my $head = $hash || 'HEAD';
311e552e 6046 my @commitlist = parse_commits($head, 150, 0, $file_name);
af6feeb2
JN
6047
6048 my %latest_commit;
6049 my %latest_date;
6050 my $content_type = "application/$format+xml";
6051 if (defined $cgi->http('HTTP_ACCEPT') &&
6052 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6053 # browser (feed reader) prefers text/xml
6054 $content_type = 'text/xml';
6055 }
b6093a5c
RF
6056 if (defined($commitlist[0])) {
6057 %latest_commit = %{$commitlist[0]};
cd956c73
GB
6058 my $latest_epoch = $latest_commit{'committer_epoch'};
6059 %latest_date = parse_date($latest_epoch);
6060 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6061 if (defined $if_modified) {
6062 my $since;
6063 if (eval { require HTTP::Date; 1; }) {
6064 $since = HTTP::Date::str2time($if_modified);
6065 } elsif (eval { require Time::ParseDate; 1; }) {
6066 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6067 }
6068 if (defined $since && $latest_epoch <= $since) {
6069 print $cgi->header(
6070 -type => $content_type,
6071 -charset => 'utf-8',
6072 -last_modified => $latest_date{'rfc2822'},
6073 -status => '304 Not Modified');
6074 return;
6075 }
6076 }
af6feeb2
JN
6077 print $cgi->header(
6078 -type => $content_type,
6079 -charset => 'utf-8',
6080 -last_modified => $latest_date{'rfc2822'});
6081 } else {
6082 print $cgi->header(
6083 -type => $content_type,
6084 -charset => 'utf-8');
6085 }
6086
6087 # Optimization: skip generating the body if client asks only
6088 # for Last-Modified date.
6089 return if ($cgi->request_method() eq 'HEAD');
6090
6091 # header variables
6092 my $title = "$site_name - $project/$action";
6093 my $feed_type = 'log';
6094 if (defined $hash) {
6095 $title .= " - '$hash'";
6096 $feed_type = 'branch log';
6097 if (defined $file_name) {
6098 $title .= " :: $file_name";
6099 $feed_type = 'history';
6100 }
6101 } elsif (defined $file_name) {
6102 $title .= " - $file_name";
6103 $feed_type = 'history';
6104 }
6105 $title .= " $feed_type";
6106 my $descr = git_get_project_description($project);
6107 if (defined $descr) {
6108 $descr = esc_html($descr);
6109 } else {
6110 $descr = "$project " .
6111 ($format eq 'rss' ? 'RSS' : 'Atom') .
6112 " feed";
6113 }
6114 my $owner = git_get_project_owner($project);
6115 $owner = esc_html($owner);
6116
6117 #header
6118 my $alt_url;
6119 if (defined $file_name) {
6120 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6121 } elsif (defined $hash) {
6122 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6123 } else {
6124 $alt_url = href(-full=>1, action=>"summary");
6125 }
6126 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6127 if ($format eq 'rss') {
6128 print <<XML;
59b9f61a
JN
6129<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6130<channel>
59b9f61a 6131XML
af6feeb2
JN
6132 print "<title>$title</title>\n" .
6133 "<link>$alt_url</link>\n" .
6134 "<description>$descr</description>\n" .
3ac109ae
GB
6135 "<language>en</language>\n" .
6136 # project owner is responsible for 'editorial' content
6137 "<managingEditor>$owner</managingEditor>\n";
1ba68ce2
GB
6138 if (defined $logo || defined $favicon) {
6139 # prefer the logo to the favicon, since RSS
6140 # doesn't allow both
6141 my $img = esc_url($logo || $favicon);
6142 print "<image>\n" .
6143 "<url>$img</url>\n" .
6144 "<title>$title</title>\n" .
6145 "<link>$alt_url</link>\n" .
6146 "</image>\n";
6147 }
0cf31285
GB
6148 if (%latest_date) {
6149 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6150 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6151 }
ad59a7a3 6152 print "<generator>gitweb v.$version/$git_version</generator>\n";
af6feeb2
JN
6153 } elsif ($format eq 'atom') {
6154 print <<XML;
6155<feed xmlns="http://www.w3.org/2005/Atom">
6156XML
6157 print "<title>$title</title>\n" .
6158 "<subtitle>$descr</subtitle>\n" .
6159 '<link rel="alternate" type="text/html" href="' .
6160 $alt_url . '" />' . "\n" .
6161 '<link rel="self" type="' . $content_type . '" href="' .
6162 $cgi->self_url() . '" />' . "\n" .
6163 "<id>" . href(-full=>1) . "</id>\n" .
6164 # use project owner for feed author
6165 "<author><name>$owner</name></author>\n";
6166 if (defined $favicon) {
6167 print "<icon>" . esc_url($favicon) . "</icon>\n";
6168 }
6169 if (defined $logo_url) {
6170 # not twice as wide as tall: 72 x 27 pixels
e1147267 6171 print "<logo>" . esc_url($logo) . "</logo>\n";
af6feeb2
JN
6172 }
6173 if (! %latest_date) {
6174 # dummy date to keep the feed valid until commits trickle in:
6175 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6176 } else {
6177 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6178 }
ad59a7a3 6179 print "<generator version='$version/$git_version'>gitweb</generator>\n";
af6feeb2 6180 }
717b8311 6181
af6feeb2 6182 # contents
b6093a5c
RF
6183 for (my $i = 0; $i <= $#commitlist; $i++) {
6184 my %co = %{$commitlist[$i]};
6185 my $commit = $co{'id'};
717b8311 6186 # we read 150, we always show 30 and the ones more recent than 48 hours
91fd2bf3 6187 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
717b8311
JN
6188 last;
6189 }
91fd2bf3 6190 my %cd = parse_date($co{'author_epoch'});
af6feeb2
JN
6191
6192 # get list of changed files
b6093a5c 6193 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
c906b181
JN
6194 $co{'parent'} || "--root",
6195 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6bcf4b46 6196 or next;
717b8311 6197 my @difftree = map { chomp; $_ } <$fd>;
6bcf4b46
JN
6198 close $fd
6199 or next;
af6feeb2
JN
6200
6201 # print element (entry, item)
e62a641d 6202 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
af6feeb2
JN
6203 if ($format eq 'rss') {
6204 print "<item>\n" .
6205 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6206 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6207 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6208 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6209 "<link>$co_url</link>\n" .
6210 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6211 "<content:encoded>" .
6212 "<![CDATA[\n";
6213 } elsif ($format eq 'atom') {
6214 print "<entry>\n" .
6215 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6216 "<updated>$cd{'iso-8601'}</updated>\n" .
ab23c19d
JN
6217 "<author>\n" .
6218 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6219 if ($co{'author_email'}) {
6220 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6221 }
6222 print "</author>\n" .
af6feeb2 6223 # use committer for contributor
ab23c19d
JN
6224 "<contributor>\n" .
6225 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6226 if ($co{'committer_email'}) {
6227 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6228 }
6229 print "</contributor>\n" .
af6feeb2
JN
6230 "<published>$cd{'iso-8601'}</published>\n" .
6231 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6232 "<id>$co_url</id>\n" .
6233 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6234 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6235 }
717b8311 6236 my $comment = $co{'comment'};
af6feeb2 6237 print "<pre>\n";
717b8311 6238 foreach my $line (@$comment) {
af6feeb2
JN
6239 $line = esc_html($line);
6240 print "$line\n";
717b8311 6241 }
af6feeb2
JN
6242 print "</pre><ul>\n";
6243 foreach my $difftree_line (@difftree) {
6244 my %difftree = parse_difftree_raw_line($difftree_line);
6245 next if !$difftree{'from_id'};
6246
6247 my $file = $difftree{'file'} || $difftree{'to_file'};
6248
6249 print "<li>" .
6250 "[" .
6251 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6252 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6253 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6254 file_name=>$file, file_parent=>$difftree{'from_file'}),
6255 -title => "diff"}, 'D');
6256 if ($have_blame) {
6257 print $cgi->a({-href => href(-full=>1, action=>"blame",
6258 file_name=>$file, hash_base=>$commit),
6259 -title => "blame"}, 'B');
717b8311 6260 }
af6feeb2
JN
6261 # if this is not a feed of a file history
6262 if (!defined $file_name || $file_name ne $file) {
6263 print $cgi->a({-href => href(-full=>1, action=>"history",
6264 file_name=>$file, hash=>$commit),
6265 -title => "history"}, 'H');
6266 }
6267 $file = esc_path($file);
6268 print "] ".
6269 "$file</li>\n";
6270 }
6271 if ($format eq 'rss') {
6272 print "</ul>]]>\n" .
6273 "</content:encoded>\n" .
6274 "</item>\n";
6275 } elsif ($format eq 'atom') {
6276 print "</ul>\n</div>\n" .
6277 "</content>\n" .
6278 "</entry>\n";
717b8311 6279 }
717b8311 6280 }
af6feeb2
JN
6281
6282 # end of feed
6283 if ($format eq 'rss') {
6284 print "</channel>\n</rss>\n";
6285 } elsif ($format eq 'atom') {
6286 print "</feed>\n";
6287 }
6288}
6289
6290sub git_rss {
6291 git_feed('rss');
6292}
6293
6294sub git_atom {
6295 git_feed('atom');
717b8311
JN
6296}
6297
6298sub git_opml {
847e01fb 6299 my @list = git_get_projects_list();
717b8311 6300
ae35785e
GB
6301 print $cgi->header(
6302 -type => 'text/xml',
6303 -charset => 'utf-8',
6304 -content_disposition => 'inline; filename="opml.xml"');
6305
59b9f61a
JN
6306 print <<XML;
6307<?xml version="1.0" encoding="utf-8"?>
6308<opml version="1.0">
6309<head>
8be2890c 6310 <title>$site_name OPML Export</title>
59b9f61a
JN
6311</head>
6312<body>
6313<outline text="git RSS feeds">
6314XML
717b8311
JN
6315
6316 foreach my $pr (@list) {
6317 my %proj = %$pr;
847e01fb 6318 my $head = git_get_head_hash($proj{'path'});
717b8311
JN
6319 if (!defined $head) {
6320 next;
6321 }
25691fbe 6322 $git_dir = "$projectroot/$proj{'path'}";
847e01fb 6323 my %co = parse_commit($head);
717b8311
JN
6324 if (!%co) {
6325 next;
6326 }
6327
6328 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
df63fbbf
GB
6329 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6330 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
717b8311
JN
6331 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6332 }
59b9f61a
JN
6333 print <<XML;
6334</outline>
6335</body>
6336</opml>
6337XML
717b8311 6338}