]> git.ipfire.org Git - thirdparty/git.git/blame - gitweb/gitweb.perl
Merge branch 'maint-1.6.1' into maint
[thirdparty/git.git] / gitweb / gitweb.perl
CommitLineData
161332a5
KS
1#!/usr/bin/perl
2
c994d620 3# gitweb - simple web interface to track changes in git repositories
22fafb99 4#
00cd0794
KS
5# (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6# (C) 2005, Christian Gierke
823d5dc8 7#
d8f1c5c2 8# This program is licensed under the GPLv2
161332a5
KS
9
10use strict;
11use warnings;
19806691 12use CGI qw(:standard :escapeHTML -nosticky);
7403d50b 13use CGI::Util qw(unescape);
161332a5 14use CGI::Carp qw(fatalsToBrowser);
40c13813 15use Encode;
b87d78d6 16use Fcntl ':mode';
7a13b999 17use File::Find qw();
cb9c6e5b 18use File::Basename qw(basename);
10bb9036 19binmode STDOUT, ':utf8';
161332a5 20
b1f5f64f 21BEGIN {
3be8e720 22 CGI->compile() if $ENV{'MOD_PERL'};
b1f5f64f
JN
23}
24
4a87b43e 25our $cgi = new CGI;
06c084d2 26our $version = "++GIT_VERSION++";
4a87b43e
DS
27our $my_url = $cgi->url();
28our $my_uri = $cgi->url(-absolute => 1);
3e029299 29
81d3fe9f
GB
30# Base URL for relative URLs in gitweb ($logo, $favicon, ...),
31# needed and used only for URLs with nonempty PATH_INFO
32our $base_url = $my_url;
33
34# When the script is used as DirectoryIndex, the URL does not contain the name
35# of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
36# have to do it ourselves. We make $path_info global because it's also used
37# later on.
38#
39# Another issue with the script being the DirectoryIndex is that the resulting
40# $my_url data is not the full script URL: this is good, because we want
41# generated links to keep implying the script name if it wasn't explicitly
42# indicated in the URL we're handling, but it means that $my_url cannot be used
43# as base URL.
44# Therefore, if we needed to strip PATH_INFO, then we know that we have
45# to build the base URL ourselves:
dde80d9c 46our $path_info = $ENV{"PATH_INFO"};
1b2d297e 47if ($path_info) {
81d3fe9f
GB
48 if ($my_url =~ s,\Q$path_info\E$,, &&
49 $my_uri =~ s,\Q$path_info\E$,, &&
50 defined $ENV{'SCRIPT_NAME'}) {
51 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
52 }
b65910fe
GB
53}
54
e130ddaa
AT
55# core git executable to use
56# this can just be "git" if your webserver has a sensible PATH
06c084d2 57our $GIT = "++GIT_BINDIR++/git";
3f7f2710 58
b87d78d6 59# absolute fs-path which will be prepended to the project path
4a87b43e 60#our $projectroot = "/pub/scm";
06c084d2 61our $projectroot = "++GITWEB_PROJECTROOT++";
b87d78d6 62
ca5e9495
LL
63# fs traversing limit for getting project list
64# the number is relative to the projectroot
65our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
66
b87d78d6 67# target of the home link on top of all pages
6132b7e4 68our $home_link = $my_uri || "/";
b87d78d6 69
2de21fac
YS
70# string of the home link on top of all pages
71our $home_link_str = "++GITWEB_HOME_LINK_STR++";
72
49da1daf
AT
73# name of your site or organization to appear in page titles
74# replace this with something more descriptive for clearer bookmarks
8be2890c
PB
75our $site_name = "++GITWEB_SITENAME++"
76 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
49da1daf 77
b2d3476e
AC
78# filename of html text to include at top of each page
79our $site_header = "++GITWEB_SITE_HEADER++";
8ab1da2c 80# html text to include at home page
06c084d2 81our $home_text = "++GITWEB_HOMETEXT++";
b2d3476e
AC
82# filename of html text to include at bottom of each page
83our $site_footer = "++GITWEB_SITE_FOOTER++";
84
85# URI of stylesheets
86our @stylesheets = ("++GITWEB_CSS++");
887a612f
PB
87# URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
88our $stylesheet = undef;
9a7a62ff 89# URI of GIT logo (72x27 size)
06c084d2 90our $logo = "++GITWEB_LOGO++";
0b5deba1
JN
91# URI of GIT favicon, assumed to be image/png type
92our $favicon = "++GITWEB_FAVICON++";
aedd9425 93
9a7a62ff
JN
94# URI and label (title) of GIT logo link
95#our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
96#our $logo_label = "git documentation";
97our $logo_url = "http://git.or.cz/";
98our $logo_label = "git homepage";
51a7c66a 99
09bd7898 100# source of projects list
06c084d2 101our $projects_list = "++GITWEB_LIST++";
b87d78d6 102
55feb120
MH
103# the width (in characters) of the projects list "Description" column
104our $projects_list_description_width = 25;
105
b06dcf8c
FL
106# default order of projects list
107# valid values are none, project, descr, owner, and age
108our $default_projects_order = "project";
109
32f4aacc
ML
110# show repository only if this file exists
111# (only effective if this variable evaluates to true)
112our $export_ok = "++GITWEB_EXPORT_OK++";
113
dd7f5f10
AG
114# show repository only if this subroutine returns true
115# when given the path to the project, for example:
116# sub { return -e "$_[0]/git-daemon-export-ok"; }
117our $export_auth_hook = undef;
118
32f4aacc
ML
119# only allow viewing of repositories also shown on the overview page
120our $strict_export = "++GITWEB_STRICT_EXPORT++";
121
19a8721e
JN
122# list of git base URLs used for URL to where fetch project from,
123# i.e. full URL is "$git_base_url/$project"
d6b7e0b9 124our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
19a8721e 125
f5aa79d9 126# default blob_plain mimetype and default charset for text/plain blob
4a87b43e
DS
127our $default_blob_plain_mimetype = 'text/plain';
128our $default_text_plain_charset = undef;
f5aa79d9 129
2d007374
PB
130# file to use for guessing MIME types before trying /etc/mime.types
131# (relative to the current git repository)
4a87b43e 132our $mimetypes_file = undef;
2d007374 133
00f429af
MK
134# assume this charset if line contains non-UTF-8 characters;
135# it should be valid encoding (see Encoding::Supported(3pm) for list),
136# for which encoding all byte sequences are valid, for example
137# 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
138# could be even 'utf-8' for the old behavior)
139our $fallback_encoding = 'latin1';
140
69a9b41c
JN
141# rename detection options for git-diff and git-diff-tree
142# - default is '-M', with the cost proportional to
143# (number of removed files) * (number of new files).
144# - more costly is '-C' (which implies '-M'), with the cost proportional to
145# (number of changed files + number of removed files) * (number of new files)
146# - even more costly is '-C', '--find-copies-harder' with cost
147# (number of files in the original tree) * (number of new files)
148# - one might want to include '-B' option, e.g. '-B', '-M'
149our @diff_opts = ('-M'); # taken from git_commit
150
7e1100e9
MM
151# Disables features that would allow repository owners to inject script into
152# the gitweb domain.
153our $prevent_xss = 0;
154
a3c8ab30
MM
155# information about snapshot formats that gitweb is capable of serving
156our %known_snapshot_formats = (
157 # name => {
158 # 'display' => display name,
159 # 'type' => mime type,
160 # 'suffix' => filename suffix,
161 # 'format' => --format for git-archive,
162 # 'compressor' => [compressor command and arguments]
163 # (array reference, optional)}
164 #
165 'tgz' => {
166 'display' => 'tar.gz',
167 'type' => 'application/x-gzip',
168 'suffix' => '.tar.gz',
169 'format' => 'tar',
170 'compressor' => ['gzip']},
171
172 'tbz2' => {
173 'display' => 'tar.bz2',
174 'type' => 'application/x-bzip2',
175 'suffix' => '.tar.bz2',
176 'format' => 'tar',
177 'compressor' => ['bzip2']},
178
179 'zip' => {
180 'display' => 'zip',
181 'type' => 'application/x-zip',
182 'suffix' => '.zip',
183 'format' => 'zip'},
184);
185
186# Aliases so we understand old gitweb.snapshot values in repository
187# configuration.
188our %known_snapshot_format_aliases = (
189 'gzip' => 'tgz',
190 'bzip2' => 'tbz2',
191
192 # backward compatibility: legacy gitweb config support
193 'x-gzip' => undef, 'gz' => undef,
194 'x-bzip2' => undef, 'bz2' => undef,
195 'x-zip' => undef, '' => undef,
196);
197
ddb8d900
AK
198# You define site-wide feature defaults here; override them with
199# $GITWEB_CONFIG as necessary.
952c65fc 200our %feature = (
17848fc6
JN
201 # feature => {
202 # 'sub' => feature-sub (subroutine),
203 # 'override' => allow-override (boolean),
204 # 'default' => [ default options...] (array reference)}
205 #
b4b20b21 206 # if feature is overridable (it means that allow-override has true value),
17848fc6
JN
207 # then feature-sub will be called with default options as parameters;
208 # return value of feature-sub indicates if to enable specified feature
209 #
b4b20b21
JN
210 # if there is no 'sub' key (no feature-sub), then feature cannot be
211 # overriden
212 #
ff3c0ff2
GB
213 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
214 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
215 # is enabled
952c65fc 216
45a3b12c
PB
217 # Enable the 'blame' blob view, showing the last commit that modified
218 # each line in the file. This can be very CPU-intensive.
219
220 # To enable system wide have in $GITWEB_CONFIG
221 # $feature{'blame'}{'default'} = [1];
222 # To have project specific config enable override in $GITWEB_CONFIG
223 # $feature{'blame'}{'override'} = 1;
224 # and in project config gitweb.blame = 0|1;
952c65fc 225 'blame' => {
cdad8170 226 'sub' => sub { feature_bool('blame', @_) },
952c65fc
JN
227 'override' => 0,
228 'default' => [0]},
229
a3c8ab30 230 # Enable the 'snapshot' link, providing a compressed archive of any
45a3b12c
PB
231 # tree. This can potentially generate high traffic if you have large
232 # project.
233
a3c8ab30
MM
234 # Value is a list of formats defined in %known_snapshot_formats that
235 # you wish to offer.
45a3b12c 236 # To disable system wide have in $GITWEB_CONFIG
a3c8ab30 237 # $feature{'snapshot'}{'default'} = [];
45a3b12c 238 # To have project specific config enable override in $GITWEB_CONFIG
bbee1d97 239 # $feature{'snapshot'}{'override'} = 1;
a3c8ab30
MM
240 # and in project config, a comma-separated list of formats or "none"
241 # to disable. Example: gitweb.snapshot = tbz2,zip;
952c65fc
JN
242 'snapshot' => {
243 'sub' => \&feature_snapshot,
244 'override' => 0,
a3c8ab30 245 'default' => ['tgz']},
04f7a94f 246
6be93511
RF
247 # Enable text search, which will list the commits which match author,
248 # committer or commit text to a given string. Enabled by default.
b4b20b21 249 # Project specific override is not supported.
6be93511
RF
250 'search' => {
251 'override' => 0,
252 'default' => [1]},
253
e7738553
PB
254 # Enable grep search, which will list the files in currently selected
255 # tree containing the given string. Enabled by default. This can be
256 # potentially CPU-intensive, of course.
257
258 # To enable system wide have in $GITWEB_CONFIG
259 # $feature{'grep'}{'default'} = [1];
260 # To have project specific config enable override in $GITWEB_CONFIG
261 # $feature{'grep'}{'override'} = 1;
262 # and in project config gitweb.grep = 0|1;
263 'grep' => {
cdad8170 264 'sub' => sub { feature_bool('grep', @_) },
e7738553
PB
265 'override' => 0,
266 'default' => [1]},
267
45a3b12c
PB
268 # Enable the pickaxe search, which will list the commits that modified
269 # a given string in a file. This can be practical and quite faster
270 # alternative to 'blame', but still potentially CPU-intensive.
271
272 # To enable system wide have in $GITWEB_CONFIG
273 # $feature{'pickaxe'}{'default'} = [1];
274 # To have project specific config enable override in $GITWEB_CONFIG
275 # $feature{'pickaxe'}{'override'} = 1;
276 # and in project config gitweb.pickaxe = 0|1;
04f7a94f 277 'pickaxe' => {
cdad8170 278 'sub' => sub { feature_bool('pickaxe', @_) },
04f7a94f
JN
279 'override' => 0,
280 'default' => [1]},
9e756904 281
45a3b12c
PB
282 # Make gitweb use an alternative format of the URLs which can be
283 # more readable and natural-looking: project name is embedded
284 # directly in the path and the query string contains other
285 # auxiliary information. All gitweb installations recognize
286 # URL in either format; this configures in which formats gitweb
287 # generates links.
288
289 # To enable system wide have in $GITWEB_CONFIG
290 # $feature{'pathinfo'}{'default'} = [1];
291 # Project specific override is not supported.
292
293 # Note that you will need to change the default location of CSS,
294 # favicon, logo and possibly other files to an absolute URL. Also,
295 # if gitweb.cgi serves as your indexfile, you will need to force
296 # $my_uri to contain the script name in your $GITWEB_CONFIG.
9e756904
MW
297 'pathinfo' => {
298 'override' => 0,
299 'default' => [0]},
e30496df
PB
300
301 # Make gitweb consider projects in project root subdirectories
302 # to be forks of existing projects. Given project $projname.git,
303 # projects matching $projname/*.git will not be shown in the main
304 # projects list, instead a '+' mark will be added to $projname
305 # there and a 'forks' view will be enabled for the project, listing
c2b8b134
FL
306 # all the forks. If project list is taken from a file, forks have
307 # to be listed after the main project.
e30496df
PB
308
309 # To enable system wide have in $GITWEB_CONFIG
310 # $feature{'forks'}{'default'} = [1];
311 # Project specific override is not supported.
312 'forks' => {
313 'override' => 0,
314 'default' => [0]},
d627f68f
PB
315
316 # Insert custom links to the action bar of all project pages.
317 # This enables you mainly to link to third-party scripts integrating
318 # into gitweb; e.g. git-browser for graphical history representation
319 # or custom web-based repository administration interface.
320
321 # The 'default' value consists of a list of triplets in the form
322 # (label, link, position) where position is the label after which
2b11e059 323 # to insert the link and link is a format string where %n expands
d627f68f
PB
324 # to the project name, %f to the project path within the filesystem,
325 # %h to the current hash (h gitweb parameter) and %b to the current
2b11e059 326 # hash base (hb gitweb parameter); %% expands to %.
d627f68f
PB
327
328 # To enable system wide have in $GITWEB_CONFIG e.g.
329 # $feature{'actions'}{'default'} = [('graphiclog',
330 # '/git-browser/by-commit.html?r=%n', 'summary')];
331 # Project specific override is not supported.
332 'actions' => {
333 'override' => 0,
334 'default' => []},
3e3d4ee7 335
aed93de4
PB
336 # Allow gitweb scan project content tags described in ctags/
337 # of project repository, and display the popular Web 2.0-ish
338 # "tag cloud" near the project list. Note that this is something
339 # COMPLETELY different from the normal Git tags.
340
341 # gitweb by itself can show existing tags, but it does not handle
342 # tagging itself; you need an external application for that.
343 # For an example script, check Girocco's cgi/tagproj.cgi.
344 # You may want to install the HTML::TagCloud Perl module to get
345 # a pretty tag cloud instead of just a list of tags.
346
347 # To enable system wide have in $GITWEB_CONFIG
348 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
349 # Project specific override is not supported.
350 'ctags' => {
351 'override' => 0,
352 'default' => [0]},
9872cd6f
GB
353
354 # The maximum number of patches in a patchset generated in patch
355 # view. Set this to 0 or undef to disable patch view, or to a
356 # negative number to remove any limit.
357
358 # To disable system wide have in $GITWEB_CONFIG
359 # $feature{'patches'}{'default'} = [0];
360 # To have project specific config enable override in $GITWEB_CONFIG
361 # $feature{'patches'}{'override'} = 1;
362 # and in project config gitweb.patches = 0|n;
363 # where n is the maximum number of patches allowed in a patchset.
364 'patches' => {
365 'sub' => \&feature_patches,
366 'override' => 0,
367 'default' => [16]},
ddb8d900
AK
368);
369
a7c5a283 370sub gitweb_get_feature {
ddb8d900 371 my ($name) = @_;
dd1ad5f1 372 return unless exists $feature{$name};
952c65fc
JN
373 my ($sub, $override, @defaults) = (
374 $feature{$name}{'sub'},
375 $feature{$name}{'override'},
376 @{$feature{$name}{'default'}});
ddb8d900 377 if (!$override) { return @defaults; }
a9455919
MW
378 if (!defined $sub) {
379 warn "feature $name is not overrideable";
380 return @defaults;
381 }
ddb8d900
AK
382 return $sub->(@defaults);
383}
384
25b2790f
GB
385# A wrapper to check if a given feature is enabled.
386# With this, you can say
387#
388# my $bool_feat = gitweb_check_feature('bool_feat');
389# gitweb_check_feature('bool_feat') or somecode;
390#
391# instead of
392#
393# my ($bool_feat) = gitweb_get_feature('bool_feat');
394# (gitweb_get_feature('bool_feat'))[0] or somecode;
395#
396sub gitweb_check_feature {
397 return (gitweb_get_feature(@_))[0];
398}
399
400
cdad8170
MK
401sub feature_bool {
402 my $key = shift;
403 my ($val) = git_get_project_config($key, '--bool');
ddb8d900 404
df5d10a3
MC
405 if (!defined $val) {
406 return ($_[0]);
407 } elsif ($val eq 'true') {
cdad8170 408 return (1);
ddb8d900 409 } elsif ($val eq 'false') {
cdad8170 410 return (0);
ddb8d900 411 }
ddb8d900
AK
412}
413
ddb8d900 414sub feature_snapshot {
a3c8ab30 415 my (@fmts) = @_;
ddb8d900
AK
416
417 my ($val) = git_get_project_config('snapshot');
418
a3c8ab30
MM
419 if ($val) {
420 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
ddb8d900
AK
421 }
422
a3c8ab30 423 return @fmts;
de9272f4
LT
424}
425
9872cd6f
GB
426sub feature_patches {
427 my @val = (git_get_project_config('patches', '--int'));
428
429 if (@val) {
430 return @val;
431 }
432
433 return ($_[0]);
434}
435
2172ce4b
JH
436# checking HEAD file with -e is fragile if the repository was
437# initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
438# and then pruned.
439sub check_head_link {
440 my ($dir) = @_;
441 my $headfile = "$dir/HEAD";
442 return ((-e $headfile) ||
443 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
444}
445
446sub check_export_ok {
447 my ($dir) = @_;
448 return (check_head_link($dir) &&
dd7f5f10
AG
449 (!$export_ok || -e "$dir/$export_ok") &&
450 (!$export_auth_hook || $export_auth_hook->($dir)));
2172ce4b
JH
451}
452
a781785d
JN
453# process alternate names for backward compatibility
454# filter out unsupported (unknown) snapshot formats
455sub filter_snapshot_fmts {
456 my @fmts = @_;
457
458 @fmts = map {
459 exists $known_snapshot_format_aliases{$_} ?
460 $known_snapshot_format_aliases{$_} : $_} @fmts;
461 @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
462
463}
464
06c084d2 465our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
17a8b250
GP
466if (-e $GITWEB_CONFIG) {
467 do $GITWEB_CONFIG;
468} else {
469 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
470 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
471}
c8d138a8
JK
472
473# version of the core git binary
66115d36 474our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
c8d138a8
JK
475
476$projects_list ||= $projectroot;
c8d138a8 477
154b4d78 478# ======================================================================
09bd7898 479# input validation and dispatch
1b2d297e
GB
480
481# input parameters can be collected from a variety of sources (presently, CGI
482# and PATH_INFO), so we define an %input_params hash that collects them all
483# together during validation: this allows subsequent uses (e.g. href()) to be
484# agnostic of the parameter origin
485
dde80d9c 486our %input_params = ();
1b2d297e
GB
487
488# input parameters are stored with the long parameter name as key. This will
489# also be used in the href subroutine to convert parameters to their CGI
490# equivalent, and since the href() usage is the most frequent one, we store
491# the name -> CGI key mapping here, instead of the reverse.
492#
493# XXX: Warning: If you touch this, check the search form for updating,
494# too.
495
dde80d9c 496our @cgi_param_mapping = (
1b2d297e
GB
497 project => "p",
498 action => "a",
499 file_name => "f",
500 file_parent => "fp",
501 hash => "h",
502 hash_parent => "hp",
503 hash_base => "hb",
504 hash_parent_base => "hpb",
505 page => "pg",
506 order => "o",
507 searchtext => "s",
508 searchtype => "st",
509 snapshot_format => "sf",
510 extra_options => "opt",
511 search_use_regexp => "sr",
512);
dde80d9c 513our %cgi_param_mapping = @cgi_param_mapping;
1b2d297e
GB
514
515# we will also need to know the possible actions, for validation
dde80d9c 516our %actions = (
1b2d297e
GB
517 "blame" => \&git_blame,
518 "blobdiff" => \&git_blobdiff,
519 "blobdiff_plain" => \&git_blobdiff_plain,
520 "blob" => \&git_blob,
521 "blob_plain" => \&git_blob_plain,
522 "commitdiff" => \&git_commitdiff,
523 "commitdiff_plain" => \&git_commitdiff_plain,
524 "commit" => \&git_commit,
525 "forks" => \&git_forks,
526 "heads" => \&git_heads,
527 "history" => \&git_history,
528 "log" => \&git_log,
9872cd6f 529 "patch" => \&git_patch,
a3411f8a 530 "patches" => \&git_patches,
1b2d297e
GB
531 "rss" => \&git_rss,
532 "atom" => \&git_atom,
533 "search" => \&git_search,
534 "search_help" => \&git_search_help,
535 "shortlog" => \&git_shortlog,
536 "summary" => \&git_summary,
537 "tag" => \&git_tag,
538 "tags" => \&git_tags,
539 "tree" => \&git_tree,
540 "snapshot" => \&git_snapshot,
541 "object" => \&git_object,
542 # those below don't need $project
543 "opml" => \&git_opml,
544 "project_list" => \&git_project_list,
545 "project_index" => \&git_project_index,
546);
547
548# finally, we have the hash of allowed extra_options for the commands that
549# allow them
dde80d9c 550our %allowed_options = (
1b2d297e
GB
551 "--no-merges" => [ qw(rss atom log shortlog history) ],
552);
553
554# fill %input_params with the CGI parameters. All values except for 'opt'
555# should be single values, but opt can be an array. We should probably
556# build an array of parameters that can be multi-valued, but since for the time
557# being it's only this one, we just single it out
558while (my ($name, $symbol) = each %cgi_param_mapping) {
559 if ($symbol eq 'opt') {
560 $input_params{$name} = [ $cgi->param($symbol) ];
561 } else {
562 $input_params{$name} = $cgi->param($symbol);
563 }
564}
565
566# now read PATH_INFO and update the parameter list for missing parameters
567sub evaluate_path_info {
568 return if defined $input_params{'project'};
569 return if !$path_info;
570 $path_info =~ s,^/+,,;
571 return if !$path_info;
572
573 # find which part of PATH_INFO is project
574 my $project = $path_info;
575 $project =~ s,/+$,,;
576 while ($project && !check_head_link("$projectroot/$project")) {
577 $project =~ s,/*[^/]*$,,;
578 }
579 return unless $project;
580 $input_params{'project'} = $project;
581
582 # do not change any parameters if an action is given using the query string
583 return if $input_params{'action'};
584 $path_info =~ s,^\Q$project\E/*,,;
585
d8c28822
GB
586 # next, check if we have an action
587 my $action = $path_info;
588 $action =~ s,/.*$,,;
589 if (exists $actions{$action}) {
590 $path_info =~ s,^$action/*,,;
591 $input_params{'action'} = $action;
592 }
593
594 # list of actions that want hash_base instead of hash, but can have no
595 # pathname (f) parameter
596 my @wants_base = (
597 'tree',
598 'history',
599 );
600
b0be3838
GB
601 # we want to catch
602 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
603 my ($parentrefname, $parentpathname, $refname, $pathname) =
604 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
605
606 # first, analyze the 'current' part
1b2d297e 607 if (defined $pathname) {
d8c28822
GB
608 # we got "branch:filename" or "branch:dir/"
609 # we could use git_get_type(branch:pathname), but:
610 # - it needs $git_dir
611 # - it does a git() call
612 # - the convention of terminating directories with a slash
613 # makes it superfluous
614 # - embedding the action in the PATH_INFO would make it even
615 # more superfluous
1b2d297e
GB
616 $pathname =~ s,^/+,,;
617 if (!$pathname || substr($pathname, -1) eq "/") {
d8c28822 618 $input_params{'action'} ||= "tree";
1b2d297e
GB
619 $pathname =~ s,/$,,;
620 } else {
b0be3838
GB
621 # the default action depends on whether we had parent info
622 # or not
623 if ($parentrefname) {
624 $input_params{'action'} ||= "blobdiff_plain";
625 } else {
626 $input_params{'action'} ||= "blob_plain";
627 }
1b2d297e
GB
628 }
629 $input_params{'hash_base'} ||= $refname;
630 $input_params{'file_name'} ||= $pathname;
631 } elsif (defined $refname) {
d8c28822
GB
632 # we got "branch". In this case we have to choose if we have to
633 # set hash or hash_base.
634 #
635 # Most of the actions without a pathname only want hash to be
636 # set, except for the ones specified in @wants_base that want
637 # hash_base instead. It should also be noted that hand-crafted
638 # links having 'history' as an action and no pathname or hash
639 # set will fail, but that happens regardless of PATH_INFO.
640 $input_params{'action'} ||= "shortlog";
641 if (grep { $_ eq $input_params{'action'} } @wants_base) {
642 $input_params{'hash_base'} ||= $refname;
643 } else {
644 $input_params{'hash'} ||= $refname;
645 }
1b2d297e 646 }
b0be3838
GB
647
648 # next, handle the 'parent' part, if present
649 if (defined $parentrefname) {
650 # a missing pathspec defaults to the 'current' filename, allowing e.g.
651 # someproject/blobdiff/oldrev..newrev:/filename
652 if ($parentpathname) {
653 $parentpathname =~ s,^/+,,;
654 $parentpathname =~ s,/$,,;
655 $input_params{'file_parent'} ||= $parentpathname;
656 } else {
657 $input_params{'file_parent'} ||= $input_params{'file_name'};
658 }
659 # we assume that hash_parent_base is wanted if a path was specified,
660 # or if the action wants hash_base instead of hash
661 if (defined $input_params{'file_parent'} ||
662 grep { $_ eq $input_params{'action'} } @wants_base) {
663 $input_params{'hash_parent_base'} ||= $parentrefname;
664 } else {
665 $input_params{'hash_parent'} ||= $parentrefname;
666 }
667 }
1ec2fb5f
GB
668
669 # for the snapshot action, we allow URLs in the form
670 # $project/snapshot/$hash.ext
671 # where .ext determines the snapshot and gets removed from the
672 # passed $refname to provide the $hash.
673 #
674 # To be able to tell that $refname includes the format extension, we
675 # require the following two conditions to be satisfied:
676 # - the hash input parameter MUST have been set from the $refname part
677 # of the URL (i.e. they must be equal)
678 # - the snapshot format MUST NOT have been defined already (e.g. from
679 # CGI parameter sf)
680 # It's also useless to try any matching unless $refname has a dot,
681 # so we check for that too
682 if (defined $input_params{'action'} &&
683 $input_params{'action'} eq 'snapshot' &&
684 defined $refname && index($refname, '.') != -1 &&
685 $refname eq $input_params{'hash'} &&
686 !defined $input_params{'snapshot_format'}) {
687 # We loop over the known snapshot formats, checking for
688 # extensions. Allowed extensions are both the defined suffix
689 # (which includes the initial dot already) and the snapshot
690 # format key itself, with a prepended dot
691 while (my ($fmt, %opt) = each %known_snapshot_formats) {
692 my $hash = $refname;
693 my $sfx;
694 $hash =~ s/(\Q$opt{'suffix'}\E|\Q.$fmt\E)$//;
695 next unless $sfx = $1;
696 # a valid suffix was found, so set the snapshot format
697 # and reset the hash parameter
698 $input_params{'snapshot_format'} = $fmt;
699 $input_params{'hash'} = $hash;
700 # we also set the format suffix to the one requested
701 # in the URL: this way a request for e.g. .tgz returns
702 # a .tgz instead of a .tar.gz
703 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
704 last;
705 }
706 }
1b2d297e
GB
707}
708evaluate_path_info();
709
710our $action = $input_params{'action'};
09bd7898 711if (defined $action) {
1b2d297e 712 if (!validate_action($action)) {
074afaa0 713 die_error(400, "Invalid action parameter");
b87d78d6 714 }
b87d78d6 715}
44ad2978 716
24d0693a 717# parameters which are pathnames
1b2d297e 718our $project = $input_params{'project'};
13d02165 719if (defined $project) {
1b2d297e 720 if (!validate_project($project)) {
7939fe44 721 undef $project;
074afaa0 722 die_error(404, "No such project");
9cd3d988 723 }
a59d4afd 724}
6191f8e1 725
1b2d297e 726our $file_name = $input_params{'file_name'};
24d0693a
JN
727if (defined $file_name) {
728 if (!validate_pathname($file_name)) {
074afaa0 729 die_error(400, "Invalid file parameter");
24d0693a
JN
730 }
731}
732
1b2d297e 733our $file_parent = $input_params{'file_parent'};
24d0693a
JN
734if (defined $file_parent) {
735 if (!validate_pathname($file_parent)) {
074afaa0 736 die_error(400, "Invalid file parent parameter");
24d0693a
JN
737 }
738}
5c95fab0 739
24d0693a 740# parameters which are refnames
1b2d297e 741our $hash = $input_params{'hash'};
4fac5294 742if (defined $hash) {
24d0693a 743 if (!validate_refname($hash)) {
074afaa0 744 die_error(400, "Invalid hash parameter");
4fac5294 745 }
a59d4afd 746}
6191f8e1 747
1b2d297e 748our $hash_parent = $input_params{'hash_parent'};
c91da262 749if (defined $hash_parent) {
24d0693a 750 if (!validate_refname($hash_parent)) {
074afaa0 751 die_error(400, "Invalid hash parent parameter");
c91da262 752 }
09bd7898
KS
753}
754
1b2d297e 755our $hash_base = $input_params{'hash_base'};
c91da262 756if (defined $hash_base) {
24d0693a 757 if (!validate_refname($hash_base)) {
074afaa0 758 die_error(400, "Invalid hash base parameter");
c91da262 759 }
a59d4afd 760}
6191f8e1 761
1b2d297e
GB
762our @extra_options = @{$input_params{'extra_options'}};
763# @extra_options is always defined, since it can only be (currently) set from
764# CGI, and $cgi->param() returns the empty array in array context if the param
765# is not set
766foreach my $opt (@extra_options) {
767 if (not exists $allowed_options{$opt}) {
768 die_error(400, "Invalid option parameter");
769 }
770 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
771 die_error(400, "Invalid option parameter for this action");
868bc068
MV
772 }
773}
774
1b2d297e 775our $hash_parent_base = $input_params{'hash_parent_base'};
420e92f2 776if (defined $hash_parent_base) {
24d0693a 777 if (!validate_refname($hash_parent_base)) {
074afaa0 778 die_error(400, "Invalid hash parent base parameter");
420e92f2
JN
779 }
780}
781
24d0693a 782# other parameters
1b2d297e 783our $page = $input_params{'page'};
ea4a6df4 784if (defined $page) {
ac8e3f2b 785 if ($page =~ m/[^0-9]/) {
074afaa0 786 die_error(400, "Invalid page parameter");
b87d78d6 787 }
2ad9331e 788}
823d5dc8 789
1b2d297e 790our $searchtype = $input_params{'searchtype'};
e7738553
PB
791if (defined $searchtype) {
792 if ($searchtype =~ m/[^a-z]/) {
074afaa0 793 die_error(400, "Invalid searchtype parameter");
e7738553
PB
794 }
795}
796
1b2d297e 797our $search_use_regexp = $input_params{'search_use_regexp'};
0e559919 798
1b2d297e 799our $searchtext = $input_params{'searchtext'};
7e431ef9 800our $search_regexp;
19806691 801if (defined $searchtext) {
9d032c72 802 if (length($searchtext) < 2) {
074afaa0 803 die_error(403, "At least two characters are required for search parameter");
9d032c72 804 }
0e559919 805 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
19806691
KS
806}
807
645927ce
ML
808# path to the current git repository
809our $git_dir;
810$git_dir = "$projectroot/$project" if $project;
dd70235f 811
5e166843 812# list of supported snapshot formats
a7c5a283 813our @snapshot_fmts = gitweb_get_feature('snapshot');
5e166843
GB
814@snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
815
717b8311 816# dispatch
7f9778b1
GP
817if (!defined $action) {
818 if (defined $hash) {
819 $action = git_get_type($hash);
820 } elsif (defined $hash_base && defined $file_name) {
821 $action = git_get_type("$hash_base:$file_name");
822 } elsif (defined $project) {
823 $action = 'summary';
824 } else {
825 $action = 'project_list';
826 }
77a153fd 827}
8e85cdc4 828if (!defined($actions{$action})) {
074afaa0 829 die_error(400, "Unknown action");
09bd7898 830}
d04d3d42
JN
831if ($action !~ m/^(opml|project_list|project_index)$/ &&
832 !$project) {
074afaa0 833 die_error(400, "Project needed");
d04d3d42 834}
8e85cdc4
ML
835$actions{$action}->();
836exit;
09bd7898 837
06a9d86b
MW
838## ======================================================================
839## action links
840
3562198b 841sub href (%) {
498fe002 842 my %params = @_;
bd5d1e42
JN
843 # default is to use -absolute url() i.e. $my_uri
844 my $href = $params{-full} ? $my_url : $my_uri;
498fe002 845
afa9b620
JN
846 $params{'project'} = $project unless exists $params{'project'};
847
1cad283a 848 if ($params{-replay}) {
1b2d297e 849 while (my ($name, $symbol) = each %cgi_param_mapping) {
1cad283a 850 if (!exists $params{$name}) {
1b2d297e 851 $params{$name} = $input_params{$name};
1cad283a
JN
852 }
853 }
854 }
855
25b2790f 856 my $use_pathinfo = gitweb_check_feature('pathinfo');
fb098a94 857 if ($use_pathinfo and defined $params{'project'}) {
b02bd7a6
GB
858 # try to put as many parameters as possible in PATH_INFO:
859 # - project name
860 # - action
8db49a7f 861 # - hash_parent or hash_parent_base:/file_parent
3550ea71 862 # - hash or hash_base:/filename
c752a0e0 863 # - the snapshot_format as an appropriate suffix
b02bd7a6
GB
864
865 # When the script is the root DirectoryIndex for the domain,
866 # $href here would be something like http://gitweb.example.com/
867 # Thus, we strip any trailing / from $href, to spare us double
868 # slashes in the final URL
869 $href =~ s,/$,,;
870
871 # Then add the project name, if present
fb098a94 872 $href .= "/".esc_url($params{'project'});
9e756904
MW
873 delete $params{'project'};
874
c752a0e0
GB
875 # since we destructively absorb parameters, we keep this
876 # boolean that remembers if we're handling a snapshot
877 my $is_snapshot = $params{'action'} eq 'snapshot';
878
b02bd7a6
GB
879 # Summary just uses the project path URL, any other action is
880 # added to the URL
881 if (defined $params{'action'}) {
882 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
9e756904
MW
883 delete $params{'action'};
884 }
b02bd7a6 885
8db49a7f
GB
886 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
887 # stripping nonexistent or useless pieces
888 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
889 || $params{'hash_parent'} || $params{'hash'});
b02bd7a6 890 if (defined $params{'hash_base'}) {
8db49a7f
GB
891 if (defined $params{'hash_parent_base'}) {
892 $href .= esc_url($params{'hash_parent_base'});
893 # skip the file_parent if it's the same as the file_name
894 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
895 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
896 $href .= ":/".esc_url($params{'file_parent'});
897 delete $params{'file_parent'};
898 }
899 $href .= "..";
900 delete $params{'hash_parent'};
901 delete $params{'hash_parent_base'};
902 } elsif (defined $params{'hash_parent'}) {
903 $href .= esc_url($params{'hash_parent'}). "..";
904 delete $params{'hash_parent'};
905 }
906
907 $href .= esc_url($params{'hash_base'});
908 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
3550ea71 909 $href .= ":/".esc_url($params{'file_name'});
b02bd7a6
GB
910 delete $params{'file_name'};
911 }
912 delete $params{'hash'};
913 delete $params{'hash_base'};
914 } elsif (defined $params{'hash'}) {
8db49a7f 915 $href .= esc_url($params{'hash'});
b02bd7a6
GB
916 delete $params{'hash'};
917 }
c752a0e0
GB
918
919 # If the action was a snapshot, we can absorb the
920 # snapshot_format parameter too
921 if ($is_snapshot) {
922 my $fmt = $params{'snapshot_format'};
923 # snapshot_format should always be defined when href()
924 # is called, but just in case some code forgets, we
925 # fall back to the default
926 $fmt ||= $snapshot_fmts[0];
927 $href .= $known_snapshot_formats{$fmt}{'suffix'};
928 delete $params{'snapshot_format'};
929 }
9e756904
MW
930 }
931
932 # now encode the parameters explicitly
498fe002 933 my @result = ();
1b2d297e
GB
934 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
935 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
498fe002 936 if (defined $params{$name}) {
f22cca44
JN
937 if (ref($params{$name}) eq "ARRAY") {
938 foreach my $par (@{$params{$name}}) {
939 push @result, $symbol . "=" . esc_param($par);
940 }
941 } else {
942 push @result, $symbol . "=" . esc_param($params{$name});
943 }
498fe002
JN
944 }
945 }
9e756904
MW
946 $href .= "?" . join(';', @result) if scalar @result;
947
948 return $href;
06a9d86b
MW
949}
950
951
717b8311
JN
952## ======================================================================
953## validation, quoting/unquoting and escaping
954
1b2d297e
GB
955sub validate_action {
956 my $input = shift || return undef;
957 return undef unless exists $actions{$input};
958 return $input;
959}
960
961sub validate_project {
962 my $input = shift || return undef;
963 if (!validate_pathname($input) ||
964 !(-d "$projectroot/$input") ||
ec26f098 965 !check_export_ok("$projectroot/$input") ||
1b2d297e
GB
966 ($strict_export && !project_in_list($input))) {
967 return undef;
968 } else {
969 return $input;
970 }
971}
972
24d0693a
JN
973sub validate_pathname {
974 my $input = shift || return undef;
717b8311 975
24d0693a
JN
976 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
977 # at the beginning, at the end, and between slashes.
978 # also this catches doubled slashes
979 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
980 return undef;
717b8311 981 }
24d0693a
JN
982 # no null characters
983 if ($input =~ m!\0!) {
717b8311
JN
984 return undef;
985 }
24d0693a
JN
986 return $input;
987}
988
989sub validate_refname {
990 my $input = shift || return undef;
991
992 # textual hashes are O.K.
993 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
994 return $input;
995 }
996 # it must be correct pathname
997 $input = validate_pathname($input)
998 or return undef;
999 # restrictions on ref name according to git-check-ref-format
1000 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
717b8311
JN
1001 return undef;
1002 }
1003 return $input;
1004}
1005
00f429af
MK
1006# decode sequences of octets in utf8 into Perl's internal form,
1007# which is utf-8 with utf8 flag set if needed. gitweb writes out
1008# in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1009sub to_utf8 {
1010 my $str = shift;
e5d3de5c
İD
1011 if (utf8::valid($str)) {
1012 utf8::decode($str);
1013 return $str;
00f429af
MK
1014 } else {
1015 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1016 }
1017}
1018
232ff553
KS
1019# quote unsafe chars, but keep the slash, even when it's not
1020# correct, but quoted slashes look too horrible in bookmarks
1021sub esc_param {
353347b0 1022 my $str = shift;
a2f3db2f 1023 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
18216710 1024 $str =~ s/\+/%2B/g;
a9e60b7d 1025 $str =~ s/ /\+/g;
353347b0
KS
1026 return $str;
1027}
1028
f93bff8d
JN
1029# quote unsafe chars in whole URL, so some charactrs cannot be quoted
1030sub esc_url {
1031 my $str = shift;
1032 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1033 $str =~ s/\+/%2B/g;
1034 $str =~ s/ /\+/g;
1035 return $str;
1036}
1037
232ff553 1038# replace invalid utf8 character with SUBSTITUTION sequence
6255ef08 1039sub esc_html ($;%) {
40c13813 1040 my $str = shift;
6255ef08
JN
1041 my %opts = @_;
1042
00f429af 1043 $str = to_utf8($str);
c390ae97 1044 $str = $cgi->escapeHTML($str);
6255ef08
JN
1045 if ($opts{'-nbsp'}) {
1046 $str =~ s/ /&nbsp;/g;
1047 }
25ffbb27 1048 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
40c13813
KS
1049 return $str;
1050}
1051
391862e3
JN
1052# quote control characters and escape filename to HTML
1053sub esc_path {
1054 my $str = shift;
1055 my %opts = @_;
1056
00f429af 1057 $str = to_utf8($str);
c390ae97 1058 $str = $cgi->escapeHTML($str);
391862e3
JN
1059 if ($opts{'-nbsp'}) {
1060 $str =~ s/ /&nbsp;/g;
1061 }
1062 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1063 return $str;
1064}
1065
1066# Make control characters "printable", using character escape codes (CEC)
1d3bc0cc
JN
1067sub quot_cec {
1068 my $cntrl = shift;
c84c483f 1069 my %opts = @_;
1d3bc0cc 1070 my %es = ( # character escape codes, aka escape sequences
c84c483f
JN
1071 "\t" => '\t', # tab (HT)
1072 "\n" => '\n', # line feed (LF)
1073 "\r" => '\r', # carrige return (CR)
1074 "\f" => '\f', # form feed (FF)
1075 "\b" => '\b', # backspace (BS)
1076 "\a" => '\a', # alarm (bell) (BEL)
1077 "\e" => '\e', # escape (ESC)
1078 "\013" => '\v', # vertical tab (VT)
1079 "\000" => '\0', # nul character (NUL)
1080 );
1d3bc0cc
JN
1081 my $chr = ( (exists $es{$cntrl})
1082 ? $es{$cntrl}
25dfd171 1083 : sprintf('\%2x', ord($cntrl)) );
c84c483f
JN
1084 if ($opts{-nohtml}) {
1085 return $chr;
1086 } else {
1087 return "<span class=\"cntrl\">$chr</span>";
1088 }
1d3bc0cc
JN
1089}
1090
391862e3
JN
1091# Alternatively use unicode control pictures codepoints,
1092# Unicode "printable representation" (PR)
1d3bc0cc
JN
1093sub quot_upr {
1094 my $cntrl = shift;
c84c483f
JN
1095 my %opts = @_;
1096
1d3bc0cc 1097 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
c84c483f
JN
1098 if ($opts{-nohtml}) {
1099 return $chr;
1100 } else {
1101 return "<span class=\"cntrl\">$chr</span>";
1102 }
1d3bc0cc
JN
1103}
1104
232ff553
KS
1105# git may return quoted and escaped filenames
1106sub unquote {
1107 my $str = shift;
403d0906
JN
1108
1109 sub unq {
1110 my $seq = shift;
1111 my %es = ( # character escape codes, aka escape sequences
1112 't' => "\t", # tab (HT, TAB)
1113 'n' => "\n", # newline (NL)
1114 'r' => "\r", # return (CR)
1115 'f' => "\f", # form feed (FF)
1116 'b' => "\b", # backspace (BS)
1117 'a' => "\a", # alarm (bell) (BEL)
1118 'e' => "\e", # escape (ESC)
1119 'v' => "\013", # vertical tab (VT)
1120 );
1121
1122 if ($seq =~ m/^[0-7]{1,3}$/) {
1123 # octal char sequence
1124 return chr(oct($seq));
1125 } elsif (exists $es{$seq}) {
1126 # C escape sequence, aka character escape code
c84c483f 1127 return $es{$seq};
403d0906
JN
1128 }
1129 # quoted ordinary character
1130 return $seq;
1131 }
1132
232ff553 1133 if ($str =~ m/^"(.*)"$/) {
403d0906 1134 # needs unquoting
232ff553 1135 $str = $1;
403d0906 1136 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
232ff553
KS
1137 }
1138 return $str;
1139}
1140
f16db173
JN
1141# escape tabs (convert tabs to spaces)
1142sub untabify {
1143 my $line = shift;
1144
1145 while ((my $pos = index($line, "\t")) != -1) {
1146 if (my $count = (8 - ($pos % 8))) {
1147 my $spaces = ' ' x $count;
1148 $line =~ s/\t/$spaces/;
1149 }
1150 }
1151
1152 return $line;
1153}
1154
32f4aacc
ML
1155sub project_in_list {
1156 my $project = shift;
1157 my @list = git_get_projects_list();
1158 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1159}
1160
717b8311
JN
1161## ----------------------------------------------------------------------
1162## HTML aware string manipulation
1163
b8d97d07
JN
1164# Try to chop given string on a word boundary between position
1165# $len and $len+$add_len. If there is no word boundary there,
1166# chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1167# (marking chopped part) would be longer than given string.
717b8311
JN
1168sub chop_str {
1169 my $str = shift;
1170 my $len = shift;
1171 my $add_len = shift || 10;
b8d97d07 1172 my $where = shift || 'right'; # 'left' | 'center' | 'right'
717b8311 1173
dee2775a
AW
1174 # Make sure perl knows it is utf8 encoded so we don't
1175 # cut in the middle of a utf8 multibyte char.
1176 $str = to_utf8($str);
1177
717b8311
JN
1178 # allow only $len chars, but don't cut a word if it would fit in $add_len
1179 # if it doesn't fit, cut it if it's still longer than the dots we would add
b8d97d07
JN
1180 # remove chopped character entities entirely
1181
1182 # when chopping in the middle, distribute $len into left and right part
1183 # return early if chopping wouldn't make string shorter
1184 if ($where eq 'center') {
1185 return $str if ($len + 5 >= length($str)); # filler is length 5
1186 $len = int($len/2);
1187 } else {
1188 return $str if ($len + 4 >= length($str)); # filler is length 4
1189 }
1190
1191 # regexps: ending and beginning with word part up to $add_len
1192 my $endre = qr/.{$len}\w{0,$add_len}/;
1193 my $begre = qr/\w{0,$add_len}.{$len}/;
1194
1195 if ($where eq 'left') {
1196 $str =~ m/^(.*?)($begre)$/;
1197 my ($lead, $body) = ($1, $2);
1198 if (length($lead) > 4) {
1199 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1200 $lead = " ...";
1201 }
1202 return "$lead$body";
1203
1204 } elsif ($where eq 'center') {
1205 $str =~ m/^($endre)(.*)$/;
1206 my ($left, $str) = ($1, $2);
1207 $str =~ m/^(.*?)($begre)$/;
1208 my ($mid, $right) = ($1, $2);
1209 if (length($mid) > 5) {
1210 $left =~ s/&[^;]*$//;
1211 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1212 $mid = " ... ";
1213 }
1214 return "$left$mid$right";
1215
1216 } else {
1217 $str =~ m/^($endre)(.*)$/;
1218 my $body = $1;
1219 my $tail = $2;
1220 if (length($tail) > 4) {
1221 $body =~ s/&[^;]*$//;
1222 $tail = "... ";
1223 }
1224 return "$body$tail";
717b8311 1225 }
717b8311
JN
1226}
1227
ce58ec91
DS
1228# takes the same arguments as chop_str, but also wraps a <span> around the
1229# result with a title attribute if it does get chopped. Additionally, the
1230# string is HTML-escaped.
1231sub chop_and_escape_str {
b8d97d07 1232 my ($str) = @_;
ce58ec91 1233
b8d97d07 1234 my $chopped = chop_str(@_);
ce58ec91
DS
1235 if ($chopped eq $str) {
1236 return esc_html($chopped);
1237 } else {
850b90a5
JN
1238 $str =~ s/([[:cntrl:]])/?/g;
1239 return $cgi->span({-title=>$str}, esc_html($chopped));
ce58ec91
DS
1240 }
1241}
1242
717b8311
JN
1243## ----------------------------------------------------------------------
1244## functions returning short strings
1245
1f1ab5f0
JN
1246# CSS class for given age value (in seconds)
1247sub age_class {
1248 my $age = shift;
1249
785cdea9
JN
1250 if (!defined $age) {
1251 return "noage";
1252 } elsif ($age < 60*60*2) {
1f1ab5f0
JN
1253 return "age0";
1254 } elsif ($age < 60*60*24*2) {
1255 return "age1";
1256 } else {
1257 return "age2";
1258 }
1259}
1260
717b8311
JN
1261# convert age in seconds to "nn units ago" string
1262sub age_string {
1263 my $age = shift;
1264 my $age_str;
a59d4afd 1265
717b8311
JN
1266 if ($age > 60*60*24*365*2) {
1267 $age_str = (int $age/60/60/24/365);
1268 $age_str .= " years ago";
1269 } elsif ($age > 60*60*24*(365/12)*2) {
1270 $age_str = int $age/60/60/24/(365/12);
1271 $age_str .= " months ago";
1272 } elsif ($age > 60*60*24*7*2) {
1273 $age_str = int $age/60/60/24/7;
1274 $age_str .= " weeks ago";
1275 } elsif ($age > 60*60*24*2) {
1276 $age_str = int $age/60/60/24;
1277 $age_str .= " days ago";
1278 } elsif ($age > 60*60*2) {
1279 $age_str = int $age/60/60;
1280 $age_str .= " hours ago";
1281 } elsif ($age > 60*2) {
1282 $age_str = int $age/60;
1283 $age_str .= " min ago";
1284 } elsif ($age > 2) {
1285 $age_str = int $age;
1286 $age_str .= " sec ago";
f6801d66 1287 } else {
717b8311 1288 $age_str .= " right now";
4c02e3c5 1289 }
717b8311 1290 return $age_str;
161332a5
KS
1291}
1292
01ac1e38
JN
1293use constant {
1294 S_IFINVALID => 0030000,
1295 S_IFGITLINK => 0160000,
1296};
1297
1298# submodule/subproject, a commit object reference
1299sub S_ISGITLINK($) {
1300 my $mode = shift;
1301
1302 return (($mode & S_IFMT) == S_IFGITLINK)
1303}
1304
717b8311
JN
1305# convert file mode in octal to symbolic file mode string
1306sub mode_str {
1307 my $mode = oct shift;
1308
01ac1e38
JN
1309 if (S_ISGITLINK($mode)) {
1310 return 'm---------';
1311 } elsif (S_ISDIR($mode & S_IFMT)) {
717b8311
JN
1312 return 'drwxr-xr-x';
1313 } elsif (S_ISLNK($mode)) {
1314 return 'lrwxrwxrwx';
1315 } elsif (S_ISREG($mode)) {
1316 # git cares only about the executable bit
1317 if ($mode & S_IXUSR) {
1318 return '-rwxr-xr-x';
1319 } else {
1320 return '-rw-r--r--';
1321 };
c994d620 1322 } else {
717b8311 1323 return '----------';
ff7669a5 1324 }
161332a5
KS
1325}
1326
717b8311
JN
1327# convert file mode in octal to file type string
1328sub file_type {
7c5e2ebb
JN
1329 my $mode = shift;
1330
1331 if ($mode !~ m/^[0-7]+$/) {
1332 return $mode;
1333 } else {
1334 $mode = oct $mode;
1335 }
664f4cc5 1336
01ac1e38
JN
1337 if (S_ISGITLINK($mode)) {
1338 return "submodule";
1339 } elsif (S_ISDIR($mode & S_IFMT)) {
717b8311
JN
1340 return "directory";
1341 } elsif (S_ISLNK($mode)) {
1342 return "symlink";
1343 } elsif (S_ISREG($mode)) {
1344 return "file";
1345 } else {
1346 return "unknown";
1347 }
a59d4afd
KS
1348}
1349
744d0ac3
JN
1350# convert file mode in octal to file type description string
1351sub file_type_long {
1352 my $mode = shift;
1353
1354 if ($mode !~ m/^[0-7]+$/) {
1355 return $mode;
1356 } else {
1357 $mode = oct $mode;
1358 }
1359
01ac1e38
JN
1360 if (S_ISGITLINK($mode)) {
1361 return "submodule";
1362 } elsif (S_ISDIR($mode & S_IFMT)) {
744d0ac3
JN
1363 return "directory";
1364 } elsif (S_ISLNK($mode)) {
1365 return "symlink";
1366 } elsif (S_ISREG($mode)) {
1367 if ($mode & S_IXUSR) {
1368 return "executable";
1369 } else {
1370 return "file";
1371 };
1372 } else {
1373 return "unknown";
1374 }
1375}
1376
1377
717b8311
JN
1378## ----------------------------------------------------------------------
1379## functions returning short HTML fragments, or transforming HTML fragments
3dff5379 1380## which don't belong to other sections
b18f9bf4 1381
225932ed 1382# format line of commit message.
717b8311
JN
1383sub format_log_line_html {
1384 my $line = shift;
b18f9bf4 1385
225932ed 1386 $line = esc_html($line, -nbsp=>1);
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";
c1dcf7eb 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";
eee0184d
JH
2068 unless (opendir D, "$git_dir/ctags") {
2069 return $ctags;
2070 }
2071 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir(D)) {
aed93de4
PB
2072 open CT, $_ or next;
2073 my $val = <CT>;
2074 chomp $val;
2075 close CT;
2076 my $ctag = $_; $ctag =~ s#.*/##;
2077 $ctags->{$ctag} = $val;
2078 }
eee0184d 2079 closedir D;
aed93de4
PB
2080 $ctags;
2081}
2082
2083sub git_populate_project_tagcloud {
2084 my $ctags = shift;
2085
2086 # First, merge different-cased tags; tags vote on casing
2087 my %ctags_lc;
2088 foreach (keys %$ctags) {
2089 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2090 if (not $ctags_lc{lc $_}->{topcount}
2091 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2092 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2093 $ctags_lc{lc $_}->{topname} = $_;
2094 }
2095 }
2096
2097 my $cloud;
2098 if (eval { require HTML::TagCloud; 1; }) {
2099 $cloud = HTML::TagCloud->new;
2100 foreach (sort keys %ctags_lc) {
2101 # Pad the title with spaces so that the cloud looks
2102 # less crammed.
2103 my $title = $ctags_lc{$_}->{topname};
2104 $title =~ s/ /&nbsp;/g;
2105 $title =~ s/^/&nbsp;/g;
2106 $title =~ s/$/&nbsp;/g;
2107 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2108 }
2109 } else {
2110 $cloud = \%ctags_lc;
2111 }
2112 $cloud;
2113}
2114
2115sub git_show_project_tagcloud {
2116 my ($cloud, $count) = @_;
2117 print STDERR ref($cloud)."..\n";
2118 if (ref $cloud eq 'HTML::TagCloud') {
2119 return $cloud->html_and_css($count);
2120 } else {
2121 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2122 return '<p align="center">' . join (', ', map {
2123 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2124 } splice(@tags, 0, $count)) . '</p>';
2125 }
2126}
2127
e79ca7cc
JN
2128sub git_get_project_url_list {
2129 my $path = shift;
2130
0e121a2c 2131 $git_dir = "$projectroot/$path";
201945ee 2132 open my $fd, "$git_dir/cloneurl"
0e121a2c
JN
2133 or return wantarray ?
2134 @{ config_to_multi(git_get_project_config('url')) } :
2135 config_to_multi(git_get_project_config('url'));
e79ca7cc
JN
2136 my @git_project_url_list = map { chomp; $_ } <$fd>;
2137 close $fd;
2138
2139 return wantarray ? @git_project_url_list : \@git_project_url_list;
2140}
2141
847e01fb 2142sub git_get_projects_list {
e30496df 2143 my ($filter) = @_;
717b8311
JN
2144 my @list;
2145
e30496df
PB
2146 $filter ||= '';
2147 $filter =~ s/\.git$//;
2148
25b2790f 2149 my $check_forks = gitweb_check_feature('forks');
c2b8b134 2150
717b8311
JN
2151 if (-d $projects_list) {
2152 # search in directory
e30496df 2153 my $dir = $projects_list . ($filter ? "/$filter" : '');
6768d6b8
AK
2154 # remove the trailing "/"
2155 $dir =~ s!/+$!!;
c0011ff8 2156 my $pfxlen = length("$dir");
ca5e9495 2157 my $pfxdepth = ($dir =~ tr!/!!);
c0011ff8
JN
2158
2159 File::Find::find({
2160 follow_fast => 1, # follow symbolic links
d20602ee 2161 follow_skip => 2, # ignore duplicates
c0011ff8
JN
2162 dangling_symlinks => 0, # ignore dangling symlinks, silently
2163 wanted => sub {
2164 # skip project-list toplevel, if we get it.
2165 return if (m!^[/.]$!);
2166 # only directories can be git repositories
2167 return unless (-d $_);
ca5e9495
LL
2168 # don't traverse too deep (Find is super slow on os x)
2169 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2170 $File::Find::prune = 1;
2171 return;
2172 }
c0011ff8
JN
2173
2174 my $subdir = substr($File::Find::name, $pfxlen + 1);
2175 # we check related file in $projectroot
fb3bb3d1
DD
2176 my $path = ($filter ? "$filter/" : '') . $subdir;
2177 if (check_export_ok("$projectroot/$path")) {
2178 push @list, { path => $path };
c0011ff8
JN
2179 $File::Find::prune = 1;
2180 }
2181 },
2182 }, "$dir");
2183
717b8311
JN
2184 } elsif (-f $projects_list) {
2185 # read from file(url-encoded):
2186 # 'git%2Fgit.git Linus+Torvalds'
2187 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2188 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
c2b8b134 2189 my %paths;
dd1ad5f1 2190 open my ($fd), $projects_list or return;
c2b8b134 2191 PROJECT:
717b8311
JN
2192 while (my $line = <$fd>) {
2193 chomp $line;
2194 my ($path, $owner) = split ' ', $line;
2195 $path = unescape($path);
2196 $owner = unescape($owner);
2197 if (!defined $path) {
2198 next;
2199 }
83ee94c1
JH
2200 if ($filter ne '') {
2201 # looking for forks;
2202 my $pfx = substr($path, 0, length($filter));
2203 if ($pfx ne $filter) {
c2b8b134 2204 next PROJECT;
83ee94c1
JH
2205 }
2206 my $sfx = substr($path, length($filter));
2207 if ($sfx !~ /^\/.*\.git$/) {
c2b8b134
FL
2208 next PROJECT;
2209 }
2210 } elsif ($check_forks) {
2211 PATH:
2212 foreach my $filter (keys %paths) {
2213 # looking for forks;
2214 my $pfx = substr($path, 0, length($filter));
2215 if ($pfx ne $filter) {
2216 next PATH;
2217 }
2218 my $sfx = substr($path, length($filter));
2219 if ($sfx !~ /^\/.*\.git$/) {
2220 next PATH;
2221 }
2222 # is a fork, don't include it in
2223 # the list
2224 next PROJECT;
83ee94c1
JH
2225 }
2226 }
2172ce4b 2227 if (check_export_ok("$projectroot/$path")) {
717b8311
JN
2228 my $pr = {
2229 path => $path,
00f429af 2230 owner => to_utf8($owner),
717b8311 2231 };
c2b8b134
FL
2232 push @list, $pr;
2233 (my $forks_path = $path) =~ s/\.git$//;
2234 $paths{$forks_path}++;
717b8311
JN
2235 }
2236 }
2237 close $fd;
2238 }
717b8311
JN
2239 return @list;
2240}
2241
47852450
JH
2242our $gitweb_project_owner = undef;
2243sub git_get_project_list_from_file {
1e0cf030 2244
47852450 2245 return if (defined $gitweb_project_owner);
1e0cf030 2246
47852450 2247 $gitweb_project_owner = {};
1e0cf030
JN
2248 # read from file (url-encoded):
2249 # 'git%2Fgit.git Linus+Torvalds'
2250 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2251 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2252 if (-f $projects_list) {
2253 open (my $fd , $projects_list);
2254 while (my $line = <$fd>) {
2255 chomp $line;
2256 my ($pr, $ow) = split ' ', $line;
2257 $pr = unescape($pr);
2258 $ow = unescape($ow);
47852450 2259 $gitweb_project_owner->{$pr} = to_utf8($ow);
1e0cf030
JN
2260 }
2261 close $fd;
2262 }
47852450
JH
2263}
2264
2265sub git_get_project_owner {
2266 my $project = shift;
2267 my $owner;
2268
2269 return undef unless $project;
b59012ef 2270 $git_dir = "$projectroot/$project";
47852450
JH
2271
2272 if (!defined $gitweb_project_owner) {
2273 git_get_project_list_from_file();
2274 }
2275
2276 if (exists $gitweb_project_owner->{$project}) {
2277 $owner = $gitweb_project_owner->{$project};
2278 }
b59012ef
BR
2279 if (!defined $owner){
2280 $owner = git_get_project_config('owner');
2281 }
1e0cf030 2282 if (!defined $owner) {
b59012ef 2283 $owner = get_file_owner("$git_dir");
1e0cf030
JN
2284 }
2285
2286 return $owner;
2287}
2288
c60c56cc
JN
2289sub git_get_last_activity {
2290 my ($path) = @_;
2291 my $fd;
2292
2293 $git_dir = "$projectroot/$path";
2294 open($fd, "-|", git_cmd(), 'for-each-ref',
0ff5ec70 2295 '--format=%(committer)',
c60c56cc 2296 '--sort=-committerdate',
0ff5ec70 2297 '--count=1',
c60c56cc
JN
2298 'refs/heads') or return;
2299 my $most_recent = <$fd>;
2300 close $fd or return;
785cdea9
JN
2301 if (defined $most_recent &&
2302 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
c60c56cc
JN
2303 my $timestamp = $1;
2304 my $age = time - $timestamp;
2305 return ($age, age_string($age));
2306 }
c956395e 2307 return (undef, undef);
c60c56cc
JN
2308}
2309
847e01fb 2310sub git_get_references {
717b8311
JN
2311 my $type = shift || "";
2312 my %refs;
28b9d9f7
JN
2313 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2314 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2315 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2316 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
9704d75d 2317 or return;
d294e1ca 2318
717b8311
JN
2319 while (my $line = <$fd>) {
2320 chomp $line;
4afbaeff 2321 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
717b8311 2322 if (defined $refs{$1}) {
d294e1ca 2323 push @{$refs{$1}}, $2;
717b8311 2324 } else {
d294e1ca 2325 $refs{$1} = [ $2 ];
717b8311
JN
2326 }
2327 }
2328 }
2329 close $fd or return;
2330 return \%refs;
2331}
2332
56a322f1
JN
2333sub git_get_rev_name_tags {
2334 my $hash = shift || return undef;
2335
25691fbe 2336 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
56a322f1
JN
2337 or return;
2338 my $name_rev = <$fd>;
2339 close $fd;
2340
2341 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2342 return $1;
2343 } else {
2344 # catches also '$hash undefined' output
2345 return undef;
2346 }
2347}
2348
717b8311
JN
2349## ----------------------------------------------------------------------
2350## parse to hash functions
2351
847e01fb 2352sub parse_date {
717b8311
JN
2353 my $epoch = shift;
2354 my $tz = shift || "-0000";
2355
2356 my %date;
2357 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2358 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2359 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2360 $date{'hour'} = $hour;
2361 $date{'minute'} = $min;
2362 $date{'mday'} = $mday;
2363 $date{'day'} = $days[$wday];
2364 $date{'month'} = $months[$mon];
af6feeb2
JN
2365 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2366 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
952c65fc
JN
2367 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2368 $mday, $months[$mon], $hour ,$min;
af6feeb2 2369 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
a62d6d84 2370 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
717b8311
JN
2371
2372 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2373 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2374 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2375 $date{'hour_local'} = $hour;
2376 $date{'minute_local'} = $min;
2377 $date{'tz_local'} = $tz;
af6feeb2
JN
2378 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2379 1900+$year, $mon+1, $mday,
2380 $hour, $min, $sec, $tz);
717b8311
JN
2381 return %date;
2382}
2383
847e01fb 2384sub parse_tag {
ede5e100
KS
2385 my $tag_id = shift;
2386 my %tag;
d8a20ba9 2387 my @comment;
ede5e100 2388
25691fbe 2389 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
d8a20ba9 2390 $tag{'id'} = $tag_id;
ede5e100
KS
2391 while (my $line = <$fd>) {
2392 chomp $line;
2393 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2394 $tag{'object'} = $1;
7ab0d2b6 2395 } elsif ($line =~ m/^type (.+)$/) {
ede5e100 2396 $tag{'type'} = $1;
7ab0d2b6 2397 } elsif ($line =~ m/^tag (.+)$/) {
ede5e100 2398 $tag{'name'} = $1;
d8a20ba9
KS
2399 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2400 $tag{'author'} = $1;
2401 $tag{'epoch'} = $2;
2402 $tag{'tz'} = $3;
2403 } elsif ($line =~ m/--BEGIN/) {
2404 push @comment, $line;
2405 last;
2406 } elsif ($line eq "") {
2407 last;
ede5e100
KS
2408 }
2409 }
d8a20ba9
KS
2410 push @comment, <$fd>;
2411 $tag{'comment'} = \@comment;
19806691 2412 close $fd or return;
ede5e100
KS
2413 if (!defined $tag{'name'}) {
2414 return
2415 };
2416 return %tag
2417}
2418
756bbf54 2419sub parse_commit_text {
ccdfdea0 2420 my ($commit_text, $withparents) = @_;
756bbf54 2421 my @commit_lines = split '\n', $commit_text;
703ac710 2422 my %co;
703ac710 2423
756bbf54
RF
2424 pop @commit_lines; # Remove '\0'
2425
198a2a8a
JN
2426 if (! @commit_lines) {
2427 return;
2428 }
2429
25f422fb 2430 my $header = shift @commit_lines;
198a2a8a 2431 if ($header !~ m/^[0-9a-fA-F]{40}/) {
25f422fb
KS
2432 return;
2433 }
ccdfdea0 2434 ($co{'id'}, my @parents) = split ' ', $header;
19806691 2435 while (my $line = shift @commit_lines) {
b87d78d6 2436 last if $line eq "\n";
7ab0d2b6 2437 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
703ac710 2438 $co{'tree'} = $1;
ccdfdea0 2439 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
208b2dff 2440 push @parents, $1;
022be3d0 2441 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3f714537 2442 $co{'author'} = $1;
185f09e5
KS
2443 $co{'author_epoch'} = $2;
2444 $co{'author_tz'} = $3;
ba00b8c1
JN
2445 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2446 $co{'author_name'} = $1;
2447 $co{'author_email'} = $2;
2bf7a52c
KS
2448 } else {
2449 $co{'author_name'} = $co{'author'};
2450 }
86eed32d
KS
2451 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2452 $co{'committer'} = $1;
185f09e5
KS
2453 $co{'committer_epoch'} = $2;
2454 $co{'committer_tz'} = $3;
991910a9 2455 $co{'committer_name'} = $co{'committer'};
ba00b8c1
JN
2456 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2457 $co{'committer_name'} = $1;
2458 $co{'committer_email'} = $2;
2459 } else {
2460 $co{'committer_name'} = $co{'committer'};
2461 }
703ac710
KS
2462 }
2463 }
ede5e100 2464 if (!defined $co{'tree'}) {
25f422fb 2465 return;
ede5e100 2466 };
208b2dff
RF
2467 $co{'parents'} = \@parents;
2468 $co{'parent'} = $parents[0];
25f422fb 2469
19806691 2470 foreach my $title (@commit_lines) {
c2488d06 2471 $title =~ s/^ //;
19806691 2472 if ($title ne "") {
48c771f4 2473 $co{'title'} = chop_str($title, 80, 5);
19806691
KS
2474 # remove leading stuff of merges to make the interesting part visible
2475 if (length($title) > 50) {
2476 $title =~ s/^Automatic //;
2477 $title =~ s/^merge (of|with) /Merge ... /i;
2478 if (length($title) > 50) {
2479 $title =~ s/(http|rsync):\/\///;
2480 }
2481 if (length($title) > 50) {
2482 $title =~ s/(master|www|rsync)\.//;
2483 }
2484 if (length($title) > 50) {
2485 $title =~ s/kernel.org:?//;
2486 }
2487 if (length($title) > 50) {
2488 $title =~ s/\/pub\/scm//;
2489 }
2490 }
48c771f4 2491 $co{'title_short'} = chop_str($title, 50, 5);
19806691
KS
2492 last;
2493 }
2494 }
53c39676 2495 if (! defined $co{'title'} || $co{'title'} eq "") {
7e0fe5c9
PB
2496 $co{'title'} = $co{'title_short'} = '(no commit message)';
2497 }
25f422fb
KS
2498 # remove added spaces
2499 foreach my $line (@commit_lines) {
2500 $line =~ s/^ //;
2501 }
2502 $co{'comment'} = \@commit_lines;
2ae100df
KS
2503
2504 my $age = time - $co{'committer_epoch'};
2505 $co{'age'} = $age;
d263a6bd 2506 $co{'age_string'} = age_string($age);
71be1e79
KS
2507 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2508 if ($age > 60*60*24*7*2) {
1b1cd421 2509 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79
KS
2510 $co{'age_string_age'} = $co{'age_string'};
2511 } else {
2512 $co{'age_string_date'} = $co{'age_string'};
1b1cd421 2513 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
71be1e79 2514 }
703ac710
KS
2515 return %co;
2516}
2517
756bbf54
RF
2518sub parse_commit {
2519 my ($commit_id) = @_;
2520 my %co;
2521
2522 local $/ = "\0";
2523
2524 open my $fd, "-|", git_cmd(), "rev-list",
ccdfdea0 2525 "--parents",
756bbf54 2526 "--header",
756bbf54
RF
2527 "--max-count=1",
2528 $commit_id,
2529 "--",
074afaa0 2530 or die_error(500, "Open git-rev-list failed");
ccdfdea0 2531 %co = parse_commit_text(<$fd>, 1);
756bbf54
RF
2532 close $fd;
2533
2534 return %co;
2535}
2536
2537sub parse_commits {
311e552e 2538 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
756bbf54
RF
2539 my @cos;
2540
2541 $maxcount ||= 1;
2542 $skip ||= 0;
2543
756bbf54
RF
2544 local $/ = "\0";
2545
2546 open my $fd, "-|", git_cmd(), "rev-list",
2547 "--header",
311e552e 2548 @args,
756bbf54 2549 ("--max-count=" . $maxcount),
f47efbb7 2550 ("--skip=" . $skip),
868bc068 2551 @extra_options,
756bbf54
RF
2552 $commit_id,
2553 "--",
2554 ($filename ? ($filename) : ())
074afaa0 2555 or die_error(500, "Open git-rev-list failed");
756bbf54
RF
2556 while (my $line = <$fd>) {
2557 my %co = parse_commit_text($line);
2558 push @cos, \%co;
2559 }
2560 close $fd;
2561
2562 return wantarray ? @cos : \@cos;
2563}
2564
e8e41a93 2565# parse line of git-diff-tree "raw" output
740e67f9
JN
2566sub parse_difftree_raw_line {
2567 my $line = shift;
2568 my %res;
2569
2570 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2571 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2572 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2573 $res{'from_mode'} = $1;
2574 $res{'to_mode'} = $2;
2575 $res{'from_id'} = $3;
2576 $res{'to_id'} = $4;
4ed4a347 2577 $res{'status'} = $5;
740e67f9
JN
2578 $res{'similarity'} = $6;
2579 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
e8e41a93 2580 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
740e67f9 2581 } else {
9d301456 2582 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
740e67f9
JN
2583 }
2584 }
78bc403a
JN
2585 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2586 # combined diff (for merge commit)
2587 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2588 $res{'nparents'} = length($1);
2589 $res{'from_mode'} = [ split(' ', $2) ];
2590 $res{'to_mode'} = pop @{$res{'from_mode'}};
2591 $res{'from_id'} = [ split(' ', $3) ];
2592 $res{'to_id'} = pop @{$res{'from_id'}};
2593 $res{'status'} = [ split('', $4) ];
2594 $res{'to_file'} = unquote($5);
2595 }
740e67f9 2596 # 'c512b523472485aef4fff9e57b229d9d243c967f'
0edcb37d
JN
2597 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2598 $res{'commit'} = $1;
2599 }
740e67f9
JN
2600
2601 return wantarray ? %res : \%res;
2602}
2603
0cec6db5
JN
2604# wrapper: return parsed line of git-diff-tree "raw" output
2605# (the argument might be raw line, or parsed info)
2606sub parsed_difftree_line {
2607 my $line_or_ref = shift;
2608
2609 if (ref($line_or_ref) eq "HASH") {
2610 # pre-parsed (or generated by hand)
2611 return $line_or_ref;
2612 } else {
2613 return parse_difftree_raw_line($line_or_ref);
2614 }
2615}
2616
cb849b46
JN
2617# parse line of git-ls-tree output
2618sub parse_ls_tree_line ($;%) {
2619 my $line = shift;
2620 my %opts = @_;
2621 my %res;
2622
2623 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8b4b94cc 2624 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
cb849b46
JN
2625
2626 $res{'mode'} = $1;
2627 $res{'type'} = $2;
2628 $res{'hash'} = $3;
2629 if ($opts{'-z'}) {
2630 $res{'name'} = $4;
2631 } else {
2632 $res{'name'} = unquote($4);
2633 }
2634
2635 return wantarray ? %res : \%res;
2636}
2637
90921740
JN
2638# generates _two_ hashes, references to which are passed as 2 and 3 argument
2639sub parse_from_to_diffinfo {
2640 my ($diffinfo, $from, $to, @parents) = @_;
2641
2642 if ($diffinfo->{'nparents'}) {
2643 # combined diff
2644 $from->{'file'} = [];
2645 $from->{'href'} = [];
2646 fill_from_file_info($diffinfo, @parents)
2647 unless exists $diffinfo->{'from_file'};
2648 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
9d301456
JN
2649 $from->{'file'}[$i] =
2650 defined $diffinfo->{'from_file'}[$i] ?
2651 $diffinfo->{'from_file'}[$i] :
2652 $diffinfo->{'to_file'};
90921740
JN
2653 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2654 $from->{'href'}[$i] = href(action=>"blob",
2655 hash_base=>$parents[$i],
2656 hash=>$diffinfo->{'from_id'}[$i],
2657 file_name=>$from->{'file'}[$i]);
2658 } else {
2659 $from->{'href'}[$i] = undef;
2660 }
2661 }
2662 } else {
0cec6db5 2663 # ordinary (not combined) diff
9d301456 2664 $from->{'file'} = $diffinfo->{'from_file'};
90921740
JN
2665 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2666 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2667 hash=>$diffinfo->{'from_id'},
2668 file_name=>$from->{'file'});
2669 } else {
2670 delete $from->{'href'};
2671 }
2672 }
2673
9d301456 2674 $to->{'file'} = $diffinfo->{'to_file'};
90921740
JN
2675 if (!is_deleted($diffinfo)) { # file exists in result
2676 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2677 hash=>$diffinfo->{'to_id'},
2678 file_name=>$to->{'file'});
2679 } else {
2680 delete $to->{'href'};
2681 }
2682}
2683
717b8311
JN
2684## ......................................................................
2685## parse to array of hashes functions
4c02e3c5 2686
cd146408
JN
2687sub git_get_heads_list {
2688 my $limit = shift;
2689 my @headslist;
2690
2691 open my $fd, '-|', git_cmd(), 'for-each-ref',
2692 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2693 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2694 'refs/heads'
c83a77e4
JN
2695 or return;
2696 while (my $line = <$fd>) {
cd146408 2697 my %ref_item;
120ddde2 2698
cd146408
JN
2699 chomp $line;
2700 my ($refinfo, $committerinfo) = split(/\0/, $line);
2701 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2702 my ($committer, $epoch, $tz) =
2703 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 2704 $ref_item{'fullname'} = $name;
cd146408
JN
2705 $name =~ s!^refs/heads/!!;
2706
2707 $ref_item{'name'} = $name;
2708 $ref_item{'id'} = $hash;
2709 $ref_item{'title'} = $title || '(no commit message)';
2710 $ref_item{'epoch'} = $epoch;
2711 if ($epoch) {
2712 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2713 } else {
2714 $ref_item{'age'} = "unknown";
717b8311 2715 }
cd146408
JN
2716
2717 push @headslist, \%ref_item;
c83a77e4
JN
2718 }
2719 close $fd;
2720
cd146408
JN
2721 return wantarray ? @headslist : \@headslist;
2722}
2723
2724sub git_get_tags_list {
2725 my $limit = shift;
2726 my @tagslist;
2727
2728 open my $fd, '-|', git_cmd(), 'for-each-ref',
2729 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2730 '--format=%(objectname) %(objecttype) %(refname) '.
2731 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2732 'refs/tags'
2733 or return;
2734 while (my $line = <$fd>) {
2735 my %ref_item;
7a13b999 2736
cd146408
JN
2737 chomp $line;
2738 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2739 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2740 my ($creator, $epoch, $tz) =
2741 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
bf901f8e 2742 $ref_item{'fullname'} = $name;
cd146408
JN
2743 $name =~ s!^refs/tags/!!;
2744
2745 $ref_item{'type'} = $type;
2746 $ref_item{'id'} = $id;
2747 $ref_item{'name'} = $name;
2748 if ($type eq "tag") {
2749 $ref_item{'subject'} = $title;
2750 $ref_item{'reftype'} = $reftype;
2751 $ref_item{'refid'} = $refid;
2752 } else {
2753 $ref_item{'reftype'} = $type;
2754 $ref_item{'refid'} = $id;
2755 }
2756
2757 if ($type eq "tag" || $type eq "commit") {
2758 $ref_item{'epoch'} = $epoch;
2759 if ($epoch) {
2760 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2761 } else {
2762 $ref_item{'age'} = "unknown";
2763 }
2764 }
991910a9 2765
cd146408 2766 push @tagslist, \%ref_item;
717b8311 2767 }
cd146408
JN
2768 close $fd;
2769
2770 return wantarray ? @tagslist : \@tagslist;
86eed32d
KS
2771}
2772
717b8311
JN
2773## ----------------------------------------------------------------------
2774## filesystem-related functions
022be3d0 2775
c07ad4b9
KS
2776sub get_file_owner {
2777 my $path = shift;
2778
2779 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2780 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2781 if (!defined $gcos) {
2782 return undef;
2783 }
2784 my $owner = $gcos;
2785 $owner =~ s/[,;].*$//;
00f429af 2786 return to_utf8($owner);
c07ad4b9
KS
2787}
2788
2dcb5e1a
JN
2789# assume that file exists
2790sub insert_file {
2791 my $filename = shift;
2792
2793 open my $fd, '<', $filename;
4586864a 2794 print map { to_utf8($_) } <$fd>;
2dcb5e1a
JN
2795 close $fd;
2796}
2797
717b8311
JN
2798## ......................................................................
2799## mimetype related functions
09bd7898 2800
717b8311
JN
2801sub mimetype_guess_file {
2802 my $filename = shift;
2803 my $mimemap = shift;
2804 -r $mimemap or return undef;
2805
2806 my %mimemap;
2807 open(MIME, $mimemap) or return undef;
2808 while (<MIME>) {
618918e5 2809 next if m/^#/; # skip comments
717b8311 2810 my ($mime, $exts) = split(/\t+/);
46b059d7
JH
2811 if (defined $exts) {
2812 my @exts = split(/\s+/, $exts);
2813 foreach my $ext (@exts) {
2814 $mimemap{$ext} = $mime;
2815 }
09bd7898 2816 }
09bd7898 2817 }
717b8311 2818 close(MIME);
09bd7898 2819
8059319a 2820 $filename =~ /\.([^.]*)$/;
717b8311
JN
2821 return $mimemap{$1};
2822}
5996ca08 2823
717b8311
JN
2824sub mimetype_guess {
2825 my $filename = shift;
2826 my $mime;
2827 $filename =~ /\./ or return undef;
5996ca08 2828
717b8311
JN
2829 if ($mimetypes_file) {
2830 my $file = $mimetypes_file;
d5aa50de
JN
2831 if ($file !~ m!^/!) { # if it is relative path
2832 # it is relative to project
2833 $file = "$projectroot/$project/$file";
2834 }
717b8311
JN
2835 $mime = mimetype_guess_file($filename, $file);
2836 }
2837 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2838 return $mime;
5996ca08
FF
2839}
2840
847e01fb 2841sub blob_mimetype {
717b8311
JN
2842 my $fd = shift;
2843 my $filename = shift;
5996ca08 2844
717b8311
JN
2845 if ($filename) {
2846 my $mime = mimetype_guess($filename);
2847 $mime and return $mime;
d8d17b5d 2848 }
717b8311
JN
2849
2850 # just in case
2851 return $default_blob_plain_mimetype unless $fd;
2852
2853 if (-T $fd) {
7f718e8b 2854 return 'text/plain';
717b8311
JN
2855 } elsif (! $filename) {
2856 return 'application/octet-stream';
2857 } elsif ($filename =~ m/\.png$/i) {
2858 return 'image/png';
2859 } elsif ($filename =~ m/\.gif$/i) {
2860 return 'image/gif';
2861 } elsif ($filename =~ m/\.jpe?g$/i) {
2862 return 'image/jpeg';
d8d17b5d 2863 } else {
717b8311 2864 return 'application/octet-stream';
f7ab660c 2865 }
717b8311
JN
2866}
2867
7f718e8b
JN
2868sub blob_contenttype {
2869 my ($fd, $file_name, $type) = @_;
2870
2871 $type ||= blob_mimetype($fd, $file_name);
2872 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
2873 $type .= "; charset=$default_text_plain_charset";
2874 }
2875
2876 return $type;
2877}
2878
717b8311
JN
2879## ======================================================================
2880## functions printing HTML: header, footer, error page
2881
2882sub git_header_html {
2883 my $status = shift || "200 OK";
2884 my $expires = shift;
2885
8be2890c 2886 my $title = "$site_name";
717b8311 2887 if (defined $project) {
00f429af 2888 $title .= " - " . to_utf8($project);
717b8311
JN
2889 if (defined $action) {
2890 $title .= "/$action";
2891 if (defined $file_name) {
403d0906 2892 $title .= " - " . esc_path($file_name);
717b8311
JN
2893 if ($action eq "tree" && $file_name !~ m|/$|) {
2894 $title .= "/";
2895 }
2896 }
2897 }
f7ab660c 2898 }
717b8311
JN
2899 my $content_type;
2900 # require explicit support from the UA if we are to send the page as
2901 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
2902 # we have to do this because MSIE sometimes globs '*/*', pretending to
2903 # support xhtml+xml but choking when it gets what it asked for.
952c65fc
JN
2904 if (defined $cgi->http('HTTP_ACCEPT') &&
2905 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
2906 $cgi->Accept('application/xhtml+xml') != 0) {
717b8311 2907 $content_type = 'application/xhtml+xml';
f7ab660c 2908 } else {
717b8311 2909 $content_type = 'text/html';
f7ab660c 2910 }
952c65fc
JN
2911 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
2912 -status=> $status, -expires => $expires);
45c9a758 2913 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
717b8311
JN
2914 print <<EOF;
2915<?xml version="1.0" encoding="utf-8"?>
2916<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2917<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
d4baf9ea 2918<!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
717b8311
JN
2919<!-- git core binaries version $git_version -->
2920<head>
2921<meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
45c9a758 2922<meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
717b8311
JN
2923<meta name="robots" content="index, nofollow"/>
2924<title>$title</title>
717b8311 2925EOF
41a4d16e
GB
2926 # the stylesheet, favicon etc urls won't work correctly with path_info
2927 # unless we set the appropriate base URL
c3254aee 2928 if ($ENV{'PATH_INFO'}) {
81d3fe9f 2929 print "<base href=\"".esc_url($base_url)."\" />\n";
c3254aee 2930 }
41a4d16e
GB
2931 # print out each stylesheet that exist, providing backwards capability
2932 # for those people who defined $stylesheet in a config file
b2d3476e 2933 if (defined $stylesheet) {
b2d3476e
AC
2934 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2935 } else {
2936 foreach my $stylesheet (@stylesheets) {
2937 next unless $stylesheet;
2938 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
2939 }
2940 }
dd04c428 2941 if (defined $project) {
3562198b
JN
2942 my %href_params = get_feed_info();
2943 if (!exists $href_params{'-title'}) {
2944 $href_params{'-title'} = 'log';
2945 }
2946
2947 foreach my $format qw(RSS Atom) {
2948 my $type = lc($format);
2949 my %link_attr = (
2950 '-rel' => 'alternate',
2951 '-title' => "$project - $href_params{'-title'} - $format feed",
2952 '-type' => "application/$type+xml"
2953 );
2954
2955 $href_params{'action'} = $type;
2956 $link_attr{'-href'} = href(%href_params);
2957 print "<link ".
2958 "rel=\"$link_attr{'-rel'}\" ".
2959 "title=\"$link_attr{'-title'}\" ".
2960 "href=\"$link_attr{'-href'}\" ".
2961 "type=\"$link_attr{'-type'}\" ".
2962 "/>\n";
2963
2964 $href_params{'extra_options'} = '--no-merges';
2965 $link_attr{'-href'} = href(%href_params);
2966 $link_attr{'-title'} .= ' (no merges)';
2967 print "<link ".
2968 "rel=\"$link_attr{'-rel'}\" ".
2969 "title=\"$link_attr{'-title'}\" ".
2970 "href=\"$link_attr{'-href'}\" ".
2971 "type=\"$link_attr{'-type'}\" ".
2972 "/>\n";
2973 }
2974
9d0734ae
JN
2975 } else {
2976 printf('<link rel="alternate" title="%s projects list" '.
3562198b 2977 'href="%s" type="text/plain; charset=utf-8" />'."\n",
9d0734ae 2978 $site_name, href(project=>undef, action=>"project_index"));
af6feeb2 2979 printf('<link rel="alternate" title="%s projects feeds" '.
3562198b 2980 'href="%s" type="text/x-opml" />'."\n",
9d0734ae 2981 $site_name, href(project=>undef, action=>"opml"));
dd04c428 2982 }
0b5deba1 2983 if (defined $favicon) {
3562198b 2984 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
0b5deba1 2985 }
10161355 2986
dd04c428 2987 print "</head>\n" .
b2d3476e
AC
2988 "<body>\n";
2989
2990 if (-f $site_header) {
2dcb5e1a 2991 insert_file($site_header);
b2d3476e
AC
2992 }
2993
2994 print "<div class=\"page_header\">\n" .
9a7a62ff
JN
2995 $cgi->a({-href => esc_url($logo_url),
2996 -title => $logo_label},
2997 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
f93bff8d 2998 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
717b8311 2999 if (defined $project) {
1c2a4f5a 3000 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
717b8311
JN
3001 if (defined $action) {
3002 print " / $action";
3003 }
3004 print "\n";
6be93511 3005 }
d77b5673
PB
3006 print "</div>\n";
3007
25b2790f 3008 my $have_search = gitweb_check_feature('search');
f70dda25 3009 if (defined $project && $have_search) {
717b8311
JN
3010 if (!defined $searchtext) {
3011 $searchtext = "";
3012 }
3013 my $search_hash;
3014 if (defined $hash_base) {
3015 $search_hash = $hash_base;
3016 } elsif (defined $hash) {
3017 $search_hash = $hash;
bddec01d 3018 } else {
717b8311 3019 $search_hash = "HEAD";
bddec01d 3020 }
40375a83 3021 my $action = $my_uri;
25b2790f 3022 my $use_pathinfo = gitweb_check_feature('pathinfo');
40375a83 3023 if ($use_pathinfo) {
85d17a12 3024 $action .= "/".esc_url($project);
40375a83 3025 }
40375a83 3026 print $cgi->startform(-method => "get", -action => $action) .
717b8311 3027 "<div class=\"search\">\n" .
f70dda25
JN
3028 (!$use_pathinfo &&
3029 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3030 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3031 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
88ad729b 3032 $cgi->popup_menu(-name => 'st', -default => 'commit',
e7738553 3033 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
88ad729b
PB
3034 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3035 " search:\n",
717b8311 3036 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
0e559919
PB
3037 "<span title=\"Extended regular expression\">" .
3038 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3039 -checked => $search_use_regexp) .
3040 "</span>" .
717b8311
JN
3041 "</div>" .
3042 $cgi->end_form() . "\n";
b87d78d6 3043 }
717b8311
JN
3044}
3045
3046sub git_footer_html {
3562198b
JN
3047 my $feed_class = 'rss_logo';
3048
717b8311
JN
3049 print "<div class=\"page_footer\">\n";
3050 if (defined $project) {
847e01fb 3051 my $descr = git_get_project_description($project);
717b8311
JN
3052 if (defined $descr) {
3053 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3054 }
3562198b
JN
3055
3056 my %href_params = get_feed_info();
3057 if (!%href_params) {
3058 $feed_class .= ' generic';
3059 }
3060 $href_params{'-title'} ||= 'log';
3061
3062 foreach my $format qw(RSS Atom) {
3063 $href_params{'action'} = lc($format);
3064 print $cgi->a({-href => href(%href_params),
3065 -title => "$href_params{'-title'} $format feed",
3066 -class => $feed_class}, $format)."\n";
3067 }
3068
717b8311 3069 } else {
a1565c44 3070 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3562198b 3071 -class => $feed_class}, "OPML") . " ";
9d0734ae 3072 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3562198b 3073 -class => $feed_class}, "TXT") . "\n";
717b8311 3074 }
3562198b 3075 print "</div>\n"; # class="page_footer"
b2d3476e
AC
3076
3077 if (-f $site_footer) {
2dcb5e1a 3078 insert_file($site_footer);
b2d3476e
AC
3079 }
3080
3081 print "</body>\n" .
717b8311
JN
3082 "</html>";
3083}
3084
074afaa0
LW
3085# die_error(<http_status_code>, <error_message>)
3086# Example: die_error(404, 'Hash not found')
3087# By convention, use the following status codes (as defined in RFC 2616):
3088# 400: Invalid or missing CGI parameters, or
3089# requested object exists but has wrong type.
3090# 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3091# this server or project.
3092# 404: Requested object/revision/project doesn't exist.
3093# 500: The server isn't configured properly, or
3094# an internal error occurred (e.g. failed assertions caused by bugs), or
3095# an unknown error occurred (e.g. the git binary died unexpectedly).
717b8311 3096sub die_error {
074afaa0
LW
3097 my $status = shift || 500;
3098 my $error = shift || "Internal server error";
3099
3100 my %http_responses = (400 => '400 Bad Request',
3101 403 => '403 Forbidden',
3102 404 => '404 Not Found',
3103 500 => '500 Internal Server Error');
3104 git_header_html($http_responses{$status});
59b9f61a
JN
3105 print <<EOF;
3106<div class="page_body">
3107<br /><br />
3108$status - $error
3109<br />
3110</div>
3111EOF
b87d78d6 3112 git_footer_html();
717b8311 3113 exit;
161332a5
KS
3114}
3115
717b8311
JN
3116## ----------------------------------------------------------------------
3117## functions printing or outputting HTML: navigation
3118
847e01fb 3119sub git_print_page_nav {
717b8311
JN
3120 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3121 $extra = '' if !defined $extra; # pager or formats
3122
3123 my @navs = qw(summary shortlog log commit commitdiff tree);
3124 if ($suppress) {
3125 @navs = grep { $_ ne $suppress } @navs;
3126 }
3127
1c2a4f5a 3128 my %arg = map { $_ => {action=>$_} } @navs;
717b8311
JN
3129 if (defined $head) {
3130 for (qw(commit commitdiff)) {
3be8e720 3131 $arg{$_}{'hash'} = $head;
717b8311
JN
3132 }
3133 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3134 for (qw(shortlog log)) {
3be8e720 3135 $arg{$_}{'hash'} = $head;
045e531a 3136 }
6a928415
KS
3137 }
3138 }
d627f68f 3139
3be8e720
JN
3140 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3141 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
717b8311 3142
a7c5a283 3143 my @actions = gitweb_get_feature('actions');
2b11e059
JN
3144 my %repl = (
3145 '%' => '%',
3146 'n' => $project, # project name
3147 'f' => $git_dir, # project path within filesystem
3148 'h' => $treehead || '', # current hash ('h' parameter)
3149 'b' => $treebase || '', # hash base ('hb' parameter)
3150 );
d627f68f 3151 while (@actions) {
2b11e059
JN
3152 my ($label, $link, $pos) = splice(@actions,0,3);
3153 # insert
d627f68f
PB
3154 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3155 # munch munch
2b11e059 3156 $link =~ s/%([%nfhb])/$repl{$1}/g;
d627f68f
PB
3157 $arg{$label}{'_href'} = $link;
3158 }
3159
717b8311
JN
3160 print "<div class=\"page_nav\">\n" .
3161 (join " | ",
1c2a4f5a 3162 map { $_ eq $current ?
d627f68f 3163 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
1c2a4f5a 3164 } @navs);
717b8311
JN
3165 print "<br/>\n$extra<br/>\n" .
3166 "</div>\n";
6a928415
KS
3167}
3168
847e01fb 3169sub format_paging_nav {
1f684dc0 3170 my ($action, $hash, $head, $page, $has_next_link) = @_;
717b8311 3171 my $paging_nav;
594e212b 3172
717b8311
JN
3173
3174 if ($hash ne $head || $page) {
1c2a4f5a 3175 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
594e212b 3176 } else {
717b8311
JN
3177 $paging_nav .= "HEAD";
3178 }
3179
3180 if ($page > 0) {
3181 $paging_nav .= " &sdot; " .
7afd77bf 3182 $cgi->a({-href => href(-replay=>1, page=>$page-1),
26298b5f 3183 -accesskey => "p", -title => "Alt-p"}, "prev");
717b8311
JN
3184 } else {
3185 $paging_nav .= " &sdot; prev";
3186 }
3187
1f684dc0 3188 if ($has_next_link) {
717b8311 3189 $paging_nav .= " &sdot; " .
7afd77bf 3190 $cgi->a({-href => href(-replay=>1, page=>$page+1),
26298b5f 3191 -accesskey => "n", -title => "Alt-n"}, "next");
717b8311
JN
3192 } else {
3193 $paging_nav .= " &sdot; next";
594e212b 3194 }
717b8311
JN
3195
3196 return $paging_nav;
594e212b
JN
3197}
3198
717b8311
JN
3199## ......................................................................
3200## functions printing or outputting HTML: div
3201
847e01fb 3202sub git_print_header_div {
717b8311 3203 my ($action, $title, $hash, $hash_base) = @_;
1c2a4f5a 3204 my %args = ();
717b8311 3205
3be8e720
JN
3206 $args{'action'} = $action;
3207 $args{'hash'} = $hash if $hash;
3208 $args{'hash_base'} = $hash_base if $hash_base;
717b8311
JN
3209
3210 print "<div class=\"header\">\n" .
1c2a4f5a
MW
3211 $cgi->a({-href => href(%args), -class => "title"},
3212 $title ? $title : $action) .
3213 "\n</div>\n";
717b8311 3214}
ede5e100 3215
6fd92a28
JN
3216#sub git_print_authorship (\%) {
3217sub git_print_authorship {
3218 my $co = shift;
3219
a44465cc 3220 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
6fd92a28
JN
3221 print "<div class=\"author_date\">" .
3222 esc_html($co->{'author_name'}) .
a44465cc
JN
3223 " [$ad{'rfc2822'}";
3224 if ($ad{'hour_local'} < 6) {
3225 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3226 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3227 } else {
3228 printf(" (%02d:%02d %s)",
3229 $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'});
3230 }
3231 print "]</div>\n";
6fd92a28
JN
3232}
3233
717b8311
JN
3234sub git_print_page_path {
3235 my $name = shift;
3236 my $type = shift;
59fb1c94 3237 my $hb = shift;
ede5e100 3238
4df118ed
JN
3239
3240 print "<div class=\"page_path\">";
3241 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
00f429af 3242 -title => 'tree root'}, to_utf8("[$project]"));
4df118ed
JN
3243 print " / ";
3244 if (defined $name) {
762c7205
JN
3245 my @dirname = split '/', $name;
3246 my $basename = pop @dirname;
3247 my $fullname = '';
3248
762c7205 3249 foreach my $dir (@dirname) {
16fdb488 3250 $fullname .= ($fullname ? '/' : '') . $dir;
762c7205
JN
3251 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3252 hash_base=>$hb),
edc04e90 3253 -title => $fullname}, esc_path($dir));
26d0a976 3254 print " / ";
762c7205
JN
3255 }
3256 if (defined $type && $type eq 'blob') {
952c65fc 3257 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
762c7205 3258 hash_base=>$hb),
edc04e90 3259 -title => $name}, esc_path($basename));
762c7205
JN
3260 } elsif (defined $type && $type eq 'tree') {
3261 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3262 hash_base=>$hb),
edc04e90 3263 -title => $name}, esc_path($basename));
4df118ed 3264 print " / ";
59fb1c94 3265 } else {
403d0906 3266 print esc_path($basename);
59fb1c94 3267 }
ede5e100 3268 }
4df118ed 3269 print "<br/></div>\n";
ede5e100
KS
3270}
3271
b7f9253d
JN
3272# sub git_print_log (\@;%) {
3273sub git_print_log ($;%) {
d16d093c 3274 my $log = shift;
b7f9253d 3275 my %opts = @_;
d16d093c 3276
b7f9253d
JN
3277 if ($opts{'-remove_title'}) {
3278 # remove title, i.e. first line of log
3279 shift @$log;
3280 }
d16d093c
JN
3281 # remove leading empty lines
3282 while (defined $log->[0] && $log->[0] eq "") {
3283 shift @$log;
3284 }
3285
3286 # print log
3287 my $signoff = 0;
3288 my $empty = 0;
3289 foreach my $line (@$log) {
b7f9253d
JN
3290 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3291 $signoff = 1;
fba20b42 3292 $empty = 0;
b7f9253d
JN
3293 if (! $opts{'-remove_signoff'}) {
3294 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3295 next;
3296 } else {
3297 # remove signoff lines
3298 next;
3299 }
3300 } else {
3301 $signoff = 0;
3302 }
3303
d16d093c
JN
3304 # print only one empty line
3305 # do not print empty line after signoff
3306 if ($line eq "") {
3307 next if ($empty || $signoff);
3308 $empty = 1;
3309 } else {
3310 $empty = 0;
3311 }
b7f9253d
JN
3312
3313 print format_log_line_html($line) . "<br/>\n";
3314 }
3315
3316 if ($opts{'-final_empty_line'}) {
3317 # end with single empty line
3318 print "<br/>\n" unless $empty;
d16d093c
JN
3319 }
3320}
3321
e33fba4c
JN
3322# return link target (what link points to)
3323sub git_get_link_target {
3324 my $hash = shift;
3325 my $link_target;
3326
3327 # read link
3328 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3329 or return;
3330 {
3331 local $/;
3332 $link_target = <$fd>;
3333 }
3334 close $fd
3335 or return;
3336
3337 return $link_target;
3338}
3339
3bf9d570
JN
3340# given link target, and the directory (basedir) the link is in,
3341# return target of link relative to top directory (top tree);
3342# return undef if it is not possible (including absolute links).
3343sub normalize_link_target {
3344 my ($link_target, $basedir, $hash_base) = @_;
3345
3346 # we can normalize symlink target only if $hash_base is provided
3347 return unless $hash_base;
3348
3349 # absolute symlinks (beginning with '/') cannot be normalized
3350 return if (substr($link_target, 0, 1) eq '/');
3351
3352 # normalize link target to path from top (root) tree (dir)
3353 my $path;
3354 if ($basedir) {
3355 $path = $basedir . '/' . $link_target;
3356 } else {
3357 # we are in top (root) tree (dir)
3358 $path = $link_target;
3359 }
3360
3361 # remove //, /./, and /../
3362 my @path_parts;
3363 foreach my $part (split('/', $path)) {
3364 # discard '.' and ''
3365 next if (!$part || $part eq '.');
3366 # handle '..'
3367 if ($part eq '..') {
3368 if (@path_parts) {
3369 pop @path_parts;
3370 } else {
3371 # link leads outside repository (outside top dir)
3372 return;
3373 }
3374 } else {
3375 push @path_parts, $part;
3376 }
3377 }
3378 $path = join('/', @path_parts);
3379
3380 return $path;
3381}
e33fba4c 3382
fa702003
JN
3383# print tree entry (row of git_tree), but without encompassing <tr> element
3384sub git_print_tree_entry {
3385 my ($t, $basedir, $hash_base, $have_blame) = @_;
3386
3387 my %base_key = ();
e33fba4c 3388 $base_key{'hash_base'} = $hash_base if defined $hash_base;
fa702003 3389
4de741b3
LT
3390 # The format of a table row is: mode list link. Where mode is
3391 # the mode of the entry, list is the name of the entry, an href,
3392 # and link is the action links of the entry.
3393
fa702003
JN
3394 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3395 if ($t->{'type'} eq "blob") {
3396 print "<td class=\"list\">" .
4de741b3 3397 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e7fb022a 3398 file_name=>"$basedir$t->{'name'}", %base_key),
e33fba4c
JN
3399 -class => "list"}, esc_path($t->{'name'}));
3400 if (S_ISLNK(oct $t->{'mode'})) {
3401 my $link_target = git_get_link_target($t->{'hash'});
3402 if ($link_target) {
3bf9d570
JN
3403 my $norm_target = normalize_link_target($link_target, $basedir, $hash_base);
3404 if (defined $norm_target) {
3405 print " -> " .
3406 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3407 file_name=>$norm_target),
3408 -title => $norm_target}, esc_path($link_target));
3409 } else {
3410 print " -> " . esc_path($link_target);
3411 }
e33fba4c
JN
3412 }
3413 }
3414 print "</td>\n";
4de741b3 3415 print "<td class=\"link\">";
4777b014 3416 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
e33fba4c
JN
3417 file_name=>"$basedir$t->{'name'}", %base_key)},
3418 "blob");
fa702003 3419 if ($have_blame) {
4777b014
PB
3420 print " | " .
3421 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
e33fba4c
JN
3422 file_name=>"$basedir$t->{'name'}", %base_key)},
3423 "blame");
fa702003
JN
3424 }
3425 if (defined $hash_base) {
4777b014
PB
3426 print " | " .
3427 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003
JN
3428 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3429 "history");
3430 }
3431 print " | " .
6f7ea5fb 3432 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
e7fb022a
JN
3433 file_name=>"$basedir$t->{'name'}")},
3434 "raw");
4de741b3 3435 print "</td>\n";
fa702003
JN
3436
3437 } elsif ($t->{'type'} eq "tree") {
0fa105e7
LT
3438 print "<td class=\"list\">";
3439 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
fa702003 3440 file_name=>"$basedir$t->{'name'}", %base_key)},
403d0906 3441 esc_path($t->{'name'}));
0fa105e7
LT
3442 print "</td>\n";
3443 print "<td class=\"link\">";
4777b014 3444 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
e33fba4c
JN
3445 file_name=>"$basedir$t->{'name'}", %base_key)},
3446 "tree");
fa702003 3447 if (defined $hash_base) {
4777b014
PB
3448 print " | " .
3449 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
fa702003 3450 file_name=>"$basedir$t->{'name'}")},
01ac1e38
JN
3451 "history");
3452 }
3453 print "</td>\n";
3454 } else {
3455 # unknown object: we can only present history for it
3456 # (this includes 'commit' object, i.e. submodule support)
3457 print "<td class=\"list\">" .
3458 esc_path($t->{'name'}) .
3459 "</td>\n";
3460 print "<td class=\"link\">";
3461 if (defined $hash_base) {
3462 print $cgi->a({-href => href(action=>"history",
3463 hash_base=>$hash_base,
3464 file_name=>"$basedir$t->{'name'}")},
fa702003
JN
3465 "history");
3466 }
3467 print "</td>\n";
3468 }
3469}
3470
717b8311
JN
3471## ......................................................................
3472## functions printing large fragments of HTML
3473
0cec6db5 3474# get pre-image filenames for merge (combined) diff
e72c0eaf
JN
3475sub fill_from_file_info {
3476 my ($diff, @parents) = @_;
3477
3478 $diff->{'from_file'} = [ ];
3479 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3480 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3481 if ($diff->{'status'}[$i] eq 'R' ||
3482 $diff->{'status'}[$i] eq 'C') {
3483 $diff->{'from_file'}[$i] =
3484 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3485 }
3486 }
3487
3488 return $diff;
3489}
3490
0cec6db5 3491# is current raw difftree line of file deletion
90921740
JN
3492sub is_deleted {
3493 my $diffinfo = shift;
3494
4ed4a347 3495 return $diffinfo->{'to_id'} eq ('0' x 40);
90921740 3496}
e72c0eaf 3497
0cec6db5
JN
3498# does patch correspond to [previous] difftree raw line
3499# $diffinfo - hashref of parsed raw diff format
3500# $patchinfo - hashref of parsed patch diff format
3501# (the same keys as in $diffinfo)
3502sub is_patch_split {
3503 my ($diffinfo, $patchinfo) = @_;
3504
3505 return defined $diffinfo && defined $patchinfo
9d301456 3506 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
0cec6db5
JN
3507}
3508
3509
4a4a1a53 3510sub git_difftree_body {
ed224dea
JN
3511 my ($difftree, $hash, @parents) = @_;
3512 my ($parent) = $parents[0];
25b2790f 3513 my $have_blame = gitweb_check_feature('blame');
4a4a1a53
JN
3514 print "<div class=\"list_head\">\n";
3515 if ($#{$difftree} > 10) {
3516 print(($#{$difftree} + 1) . " files changed:\n");
3517 }
3518 print "</div>\n";
3519
ed224dea
JN
3520 print "<table class=\"" .
3521 (@parents > 1 ? "combined " : "") .
3522 "diff_tree\">\n";
47598d7a
JN
3523
3524 # header only for combined diff in 'commitdiff' view
3ef408ae 3525 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
47598d7a
JN
3526 if ($has_header) {
3527 # table header
3528 print "<thead><tr>\n" .
3529 "<th></th><th></th>\n"; # filename, patchN link
3530 for (my $i = 0; $i < @parents; $i++) {
3531 my $par = $parents[$i];
3532 print "<th>" .
3533 $cgi->a({-href => href(action=>"commitdiff",
3534 hash=>$hash, hash_parent=>$par),
3535 -title => 'commitdiff to parent number ' .
3536 ($i+1) . ': ' . substr($par,0,7)},
3537 $i+1) .
3538 "&nbsp;</th>\n";
3539 }
3540 print "</tr></thead>\n<tbody>\n";
3541 }
3542
6dd36acd 3543 my $alternate = 1;
b4657e77 3544 my $patchno = 0;
4a4a1a53 3545 foreach my $line (@{$difftree}) {
0cec6db5 3546 my $diff = parsed_difftree_line($line);
4a4a1a53
JN
3547
3548 if ($alternate) {
3549 print "<tr class=\"dark\">\n";
3550 } else {
3551 print "<tr class=\"light\">\n";
3552 }
3553 $alternate ^= 1;
3554
493e01db 3555 if (exists $diff->{'nparents'}) { # combined diff
ed224dea 3556
493e01db
JN
3557 fill_from_file_info($diff, @parents)
3558 unless exists $diff->{'from_file'};
e72c0eaf 3559
90921740 3560 if (!is_deleted($diff)) {
ed224dea
JN
3561 # file exists in the result (child) commit
3562 print "<td>" .
493e01db
JN
3563 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3564 file_name=>$diff->{'to_file'},
ed224dea 3565 hash_base=>$hash),
493e01db 3566 -class => "list"}, esc_path($diff->{'to_file'})) .
ed224dea
JN
3567 "</td>\n";
3568 } else {
3569 print "<td>" .
493e01db 3570 esc_path($diff->{'to_file'}) .
ed224dea
JN
3571 "</td>\n";
3572 }
3573
3574 if ($action eq 'commitdiff') {
3575 # link to patch
3576 $patchno++;
3577 print "<td class=\"link\">" .
3578 $cgi->a({-href => "#patch$patchno"}, "patch") .
3579 " | " .
3580 "</td>\n";
3581 }
3582
3583 my $has_history = 0;
3584 my $not_deleted = 0;
493e01db 3585 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
ed224dea 3586 my $hash_parent = $parents[$i];
493e01db
JN
3587 my $from_hash = $diff->{'from_id'}[$i];
3588 my $from_path = $diff->{'from_file'}[$i];
3589 my $status = $diff->{'status'}[$i];
ed224dea
JN
3590
3591 $has_history ||= ($status ne 'A');
3592 $not_deleted ||= ($status ne 'D');
3593
ed224dea
JN
3594 if ($status eq 'A') {
3595 print "<td class=\"link\" align=\"right\"> | </td>\n";
3596 } elsif ($status eq 'D') {
3597 print "<td class=\"link\">" .
3598 $cgi->a({-href => href(action=>"blob",
3599 hash_base=>$hash,
3600 hash=>$from_hash,
3601 file_name=>$from_path)},
3602 "blob" . ($i+1)) .
3603 " | </td>\n";
3604 } else {
493e01db 3605 if ($diff->{'to_id'} eq $from_hash) {
ed224dea
JN
3606 print "<td class=\"link nochange\">";
3607 } else {
3608 print "<td class=\"link\">";
3609 }
3610 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3611 hash=>$diff->{'to_id'},
ed224dea
JN
3612 hash_parent=>$from_hash,
3613 hash_base=>$hash,
3614 hash_parent_base=>$hash_parent,
493e01db 3615 file_name=>$diff->{'to_file'},
ed224dea
JN
3616 file_parent=>$from_path)},
3617 "diff" . ($i+1)) .
3618 " | </td>\n";
3619 }
3620 }
3621
3622 print "<td class=\"link\">";
3623 if ($not_deleted) {
3624 print $cgi->a({-href => href(action=>"blob",
493e01db
JN
3625 hash=>$diff->{'to_id'},
3626 file_name=>$diff->{'to_file'},
ed224dea
JN
3627 hash_base=>$hash)},
3628 "blob");
3629 print " | " if ($has_history);
3630 }
3631 if ($has_history) {
3632 print $cgi->a({-href => href(action=>"history",
493e01db 3633 file_name=>$diff->{'to_file'},
ed224dea
JN
3634 hash_base=>$hash)},
3635 "history");
3636 }
3637 print "</td>\n";
3638
3639 print "</tr>\n";
3640 next; # instead of 'else' clause, to avoid extra indent
3641 }
3642 # else ordinary diff
3643
e8e41a93
JN
3644 my ($to_mode_oct, $to_mode_str, $to_file_type);
3645 my ($from_mode_oct, $from_mode_str, $from_file_type);
493e01db
JN
3646 if ($diff->{'to_mode'} ne ('0' x 6)) {
3647 $to_mode_oct = oct $diff->{'to_mode'};
e8e41a93
JN
3648 if (S_ISREG($to_mode_oct)) { # only for regular file
3649 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3650 }
493e01db 3651 $to_file_type = file_type($diff->{'to_mode'});
e8e41a93 3652 }
493e01db
JN
3653 if ($diff->{'from_mode'} ne ('0' x 6)) {
3654 $from_mode_oct = oct $diff->{'from_mode'};
e8e41a93
JN
3655 if (S_ISREG($to_mode_oct)) { # only for regular file
3656 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4a4a1a53 3657 }
493e01db 3658 $from_file_type = file_type($diff->{'from_mode'});
e8e41a93
JN
3659 }
3660
493e01db 3661 if ($diff->{'status'} eq "A") { # created
e8e41a93
JN
3662 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3663 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3664 $mode_chng .= "]</span>";
499faeda 3665 print "<td>";
493e01db
JN
3666 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3667 hash_base=>$hash, file_name=>$diff->{'file'}),
3668 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3669 print "</td>\n";
3670 print "<td>$mode_chng</td>\n";
3671 print "<td class=\"link\">";
72dbafa1 3672 if ($action eq 'commitdiff') {
b4657e77
JN
3673 # link to patch
3674 $patchno++;
499faeda 3675 print $cgi->a({-href => "#patch$patchno"}, "patch");
897d1d2e 3676 print " | ";
b4657e77 3677 }
493e01db
JN
3678 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3679 hash_base=>$hash, file_name=>$diff->{'file'})},
3faa541f 3680 "blob");
b4657e77 3681 print "</td>\n";
4a4a1a53 3682
493e01db 3683 } elsif ($diff->{'status'} eq "D") { # deleted
e8e41a93 3684 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
499faeda 3685 print "<td>";
493e01db
JN
3686 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3687 hash_base=>$parent, file_name=>$diff->{'file'}),
3688 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3689 print "</td>\n";
3690 print "<td>$mode_chng</td>\n";
3691 print "<td class=\"link\">";
72dbafa1 3692 if ($action eq 'commitdiff') {
b4657e77
JN
3693 # link to patch
3694 $patchno++;
499faeda
LT
3695 print $cgi->a({-href => "#patch$patchno"}, "patch");
3696 print " | ";
b4657e77 3697 }
493e01db
JN
3698 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3699 hash_base=>$parent, file_name=>$diff->{'file'})},
897d1d2e 3700 "blob") . " | ";
2b2a8c78 3701 if ($have_blame) {
897d1d2e 3702 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
493e01db 3703 file_name=>$diff->{'file'})},
897d1d2e 3704 "blame") . " | ";
2b2a8c78 3705 }
b4657e77 3706 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
493e01db 3707 file_name=>$diff->{'file'})},
e7fb022a 3708 "history");
499faeda 3709 print "</td>\n";
4a4a1a53 3710
493e01db 3711 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4a4a1a53 3712 my $mode_chnge = "";
493e01db 3713 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93 3714 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6e72cf43 3715 if ($from_file_type ne $to_file_type) {
e8e41a93 3716 $mode_chnge .= " from $from_file_type to $to_file_type";
4a4a1a53 3717 }
e8e41a93
JN
3718 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3719 if ($from_mode_str && $to_mode_str) {
3720 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3721 } elsif ($to_mode_str) {
3722 $mode_chnge .= " mode: $to_mode_str";
4a4a1a53
JN
3723 }
3724 }
3725 $mode_chnge .= "]</span>\n";
3726 }
3727 print "<td>";
493e01db
JN
3728 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3729 hash_base=>$hash, file_name=>$diff->{'file'}),
3730 -class => "list"}, esc_path($diff->{'file'}));
499faeda
LT
3731 print "</td>\n";
3732 print "<td>$mode_chnge</td>\n";
3733 print "<td class=\"link\">";
241cc599
JN
3734 if ($action eq 'commitdiff') {
3735 # link to patch
3736 $patchno++;
3737 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3738 " | ";
493e01db 3739 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
3740 # "commit" view and modified file (not onlu mode changed)
3741 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3742 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 3743 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 3744 file_name=>$diff->{'file'})},
241cc599
JN
3745 "diff") .
3746 " | ";
4a4a1a53 3747 }
493e01db
JN
3748 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3749 hash_base=>$hash, file_name=>$diff->{'file'})},
897d1d2e 3750 "blob") . " | ";
2b2a8c78 3751 if ($have_blame) {
897d1d2e 3752 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 3753 file_name=>$diff->{'file'})},
897d1d2e 3754 "blame") . " | ";
2b2a8c78 3755 }
eb51ec9c 3756 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 3757 file_name=>$diff->{'file'})},
e7fb022a 3758 "history");
4a4a1a53
JN
3759 print "</td>\n";
3760
493e01db 3761 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
e8e41a93 3762 my %status_name = ('R' => 'moved', 'C' => 'copied');
493e01db 3763 my $nstatus = $status_name{$diff->{'status'}};
4a4a1a53 3764 my $mode_chng = "";
493e01db 3765 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
e8e41a93
JN
3766 # mode also for directories, so we cannot use $to_mode_str
3767 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4a4a1a53
JN
3768 }
3769 print "<td>" .
e8e41a93 3770 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
493e01db
JN
3771 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3772 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
e8e41a93
JN
3773 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3774 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
493e01db
JN
3775 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3776 -class => "list"}, esc_path($diff->{'from_file'})) .
3777 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
499faeda 3778 "<td class=\"link\">";
241cc599
JN
3779 if ($action eq 'commitdiff') {
3780 # link to patch
3781 $patchno++;
3782 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3783 " | ";
493e01db 3784 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
241cc599
JN
3785 # "commit" view and modified file (not only pure rename or copy)
3786 print $cgi->a({-href => href(action=>"blobdiff",
493e01db 3787 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
241cc599 3788 hash_base=>$hash, hash_parent_base=>$parent,
493e01db 3789 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
241cc599
JN
3790 "diff") .
3791 " | ";
4a4a1a53 3792 }
493e01db
JN
3793 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3794 hash_base=>$parent, file_name=>$diff->{'to_file'})},
897d1d2e 3795 "blob") . " | ";
2b2a8c78 3796 if ($have_blame) {
897d1d2e 3797 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
493e01db 3798 file_name=>$diff->{'to_file'})},
897d1d2e 3799 "blame") . " | ";
2b2a8c78 3800 }
897d1d2e 3801 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
493e01db 3802 file_name=>$diff->{'to_file'})},
e7fb022a 3803 "history");
4a4a1a53 3804 print "</td>\n";
e8e41a93 3805
4a4a1a53
JN
3806 } # we should not encounter Unmerged (U) or Unknown (X) status
3807 print "</tr>\n";
3808 }
47598d7a 3809 print "</tbody>" if $has_header;
4a4a1a53
JN
3810 print "</table>\n";
3811}
3812
eee08903 3813sub git_patchset_body {
e72c0eaf
JN
3814 my ($fd, $difftree, $hash, @hash_parents) = @_;
3815 my ($hash_parent) = $hash_parents[0];
eee08903 3816
0cec6db5 3817 my $is_combined = (@hash_parents > 1);
eee08903 3818 my $patch_idx = 0;
4280cde9 3819 my $patch_number = 0;
6d55f055 3820 my $patch_line;
fe87585e 3821 my $diffinfo;
0cec6db5 3822 my $to_name;
744d0ac3 3823 my (%from, %to);
eee08903
JN
3824
3825 print "<div class=\"patchset\">\n";
3826
6d55f055
JN
3827 # skip to first patch
3828 while ($patch_line = <$fd>) {
157e43b4 3829 chomp $patch_line;
eee08903 3830
6d55f055
JN
3831 last if ($patch_line =~ m/^diff /);
3832 }
3833
3834 PATCH:
3835 while ($patch_line) {
6d55f055 3836
0cec6db5
JN
3837 # parse "git diff" header line
3838 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
3839 # $1 is from_name, which we do not use
3840 $to_name = unquote($2);
3841 $to_name =~ s!^b/!!;
3842 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
3843 # $1 is 'cc' or 'combined', which we do not use
3844 $to_name = unquote($2);
3845 } else {
3846 $to_name = undef;
6d55f055 3847 }
6d55f055
JN
3848
3849 # check if current patch belong to current raw line
3850 # and parse raw git-diff line if needed
0cec6db5 3851 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
2206537c 3852 # this is continuation of a split patch
6d55f055
JN
3853 print "<div class=\"patch cont\">\n";
3854 } else {
3855 # advance raw git-diff output if needed
3856 $patch_idx++ if defined $diffinfo;
eee08903 3857
0cec6db5
JN
3858 # read and prepare patch information
3859 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 3860
0cec6db5
JN
3861 # compact combined diff output can have some patches skipped
3862 # find which patch (using pathname of result) we are at now;
3863 if ($is_combined) {
3864 while ($to_name ne $diffinfo->{'to_file'}) {
cd030c3a
JN
3865 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3866 format_diff_cc_simplified($diffinfo, @hash_parents) .
3867 "</div>\n"; # class="patch"
3868
3869 $patch_idx++;
3870 $patch_number++;
0cec6db5
JN
3871
3872 last if $patch_idx > $#$difftree;
3873 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a 3874 }
0cec6db5 3875 }
711fa742 3876
90921740
JN
3877 # modifies %from, %to hashes
3878 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5f855052 3879
6d55f055
JN
3880 # this is first patch for raw difftree line with $patch_idx index
3881 # we index @$difftree array from 0, but number patches from 1
3882 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
744d0ac3 3883 }
eee08903 3884
0cec6db5
JN
3885 # git diff header
3886 #assert($patch_line =~ m/^diff /) if DEBUG;
3887 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
3888 $patch_number++;
6d55f055 3889 # print "git diff" header
90921740
JN
3890 print format_git_diff_header_line($patch_line, $diffinfo,
3891 \%from, \%to);
6d55f055
JN
3892
3893 # print extended diff header
0cec6db5 3894 print "<div class=\"diff extended_header\">\n";
6d55f055 3895 EXTENDED_HEADER:
0cec6db5
JN
3896 while ($patch_line = <$fd>) {
3897 chomp $patch_line;
3898
3899 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
3900
90921740
JN
3901 print format_extended_diff_header_line($patch_line, $diffinfo,
3902 \%from, \%to);
6d55f055 3903 }
0cec6db5 3904 print "</div>\n"; # class="diff extended_header"
6d55f055
JN
3905
3906 # from-file/to-file diff header
0bdb28c9
JN
3907 if (! $patch_line) {
3908 print "</div>\n"; # class="patch"
3909 last PATCH;
3910 }
66399eff 3911 next PATCH if ($patch_line =~ m/^diff /);
6d55f055 3912 #assert($patch_line =~ m/^---/) if DEBUG;
744d0ac3 3913
0cec6db5 3914 my $last_patch_line = $patch_line;
6d55f055 3915 $patch_line = <$fd>;
6d55f055 3916 chomp $patch_line;
90921740 3917 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
e4e4f825 3918
90921740 3919 print format_diff_from_to_header($last_patch_line, $patch_line,
91af4ce4
JN
3920 $diffinfo, \%from, \%to,
3921 @hash_parents);
e4e4f825 3922
6d55f055
JN
3923 # the patch itself
3924 LINE:
3925 while ($patch_line = <$fd>) {
3926 chomp $patch_line;
e4e4f825 3927
6d55f055 3928 next PATCH if ($patch_line =~ m/^diff /);
e4e4f825 3929
59e3b14e 3930 print format_diff_line($patch_line, \%from, \%to);
eee08903 3931 }
eee08903 3932
6d55f055
JN
3933 } continue {
3934 print "</div>\n"; # class="patch"
eee08903 3935 }
d26c4264 3936
cd030c3a
JN
3937 # for compact combined (--cc) format, with chunk and patch simpliciaction
3938 # patchset might be empty, but there might be unprocessed raw lines
0cec6db5 3939 for (++$patch_idx if $patch_number > 0;
cd030c3a 3940 $patch_idx < @$difftree;
0cec6db5 3941 ++$patch_idx) {
cd030c3a 3942 # read and prepare patch information
0cec6db5 3943 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
cd030c3a
JN
3944
3945 # generate anchor for "patch" links in difftree / whatchanged part
3946 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
3947 format_diff_cc_simplified($diffinfo, @hash_parents) .
3948 "</div>\n"; # class="patch"
3949
3950 $patch_number++;
3951 }
3952
d26c4264
JN
3953 if ($patch_number == 0) {
3954 if (@hash_parents > 1) {
3955 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
3956 } else {
3957 print "<div class=\"diff nodifferences\">No differences found</div>\n";
3958 }
3959 }
eee08903
JN
3960
3961 print "</div>\n"; # class="patchset"
3962}
3963
3964# . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3965
69913415
JN
3966# fills project list info (age, description, owner, forks) for each
3967# project in the list, removing invalid projects from returned list
3968# NOTE: modifies $projlist, but does not remove entries from it
3969sub fill_project_list_info {
3970 my ($projlist, $check_forks) = @_;
e30496df 3971 my @projects;
69913415 3972
25b2790f 3973 my $show_ctags = gitweb_check_feature('ctags');
69913415 3974 PROJECT:
e30496df 3975 foreach my $pr (@$projlist) {
69913415
JN
3976 my (@activity) = git_get_last_activity($pr->{'path'});
3977 unless (@activity) {
3978 next PROJECT;
e30496df 3979 }
69913415 3980 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
e30496df
PB
3981 if (!defined $pr->{'descr'}) {
3982 my $descr = git_get_project_description($pr->{'path'}) || "";
69913415
JN
3983 $descr = to_utf8($descr);
3984 $pr->{'descr_long'} = $descr;
55feb120 3985 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
e30496df
PB
3986 }
3987 if (!defined $pr->{'owner'}) {
76e4f5d0 3988 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
e30496df
PB
3989 }
3990 if ($check_forks) {
3991 my $pname = $pr->{'path'};
83ee94c1
JH
3992 if (($pname =~ s/\.git$//) &&
3993 ($pname !~ /\/$/) &&
3994 (-d "$projectroot/$pname")) {
3995 $pr->{'forks'} = "-d $projectroot/$pname";
69913415 3996 } else {
83ee94c1
JH
3997 $pr->{'forks'} = 0;
3998 }
e30496df 3999 }
aed93de4 4000 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
e30496df
PB
4001 push @projects, $pr;
4002 }
4003
69913415
JN
4004 return @projects;
4005}
4006
6b28da67
PB
4007# print 'sort by' <th> element, generating 'sort by $name' replay link
4008# if that order is not selected
7da0f3a4 4009sub print_sort_th {
6b28da67 4010 my ($name, $order, $header) = @_;
7da0f3a4
JN
4011 $header ||= ucfirst($name);
4012
4013 if ($order eq $name) {
7da0f3a4
JN
4014 print "<th>$header</th>\n";
4015 } else {
4016 print "<th>" .
4017 $cgi->a({-href => href(-replay=>1, order=>$name),
4018 -class => "header"}, $header) .
4019 "</th>\n";
4020 }
4021}
4022
69913415 4023sub git_project_list_body {
42326110 4024 # actually uses global variable $project
69913415
JN
4025 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4026
25b2790f 4027 my $check_forks = gitweb_check_feature('forks');
69913415
JN
4028 my @projects = fill_project_list_info($projlist, $check_forks);
4029
b06dcf8c 4030 $order ||= $default_projects_order;
e30496df
PB
4031 $from = 0 unless defined $from;
4032 $to = $#projects if (!defined $to || $#projects < $to);
4033
6b28da67
PB
4034 my %order_info = (
4035 project => { key => 'path', type => 'str' },
4036 descr => { key => 'descr_long', type => 'str' },
4037 owner => { key => 'owner', type => 'str' },
4038 age => { key => 'age', type => 'num' }
4039 );
4040 my $oi = $order_info{$order};
4041 if ($oi->{'type'} eq 'str') {
4042 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4043 } else {
4044 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4045 }
4046
25b2790f 4047 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
4048 if ($show_ctags) {
4049 my %ctags;
4050 foreach my $p (@projects) {
4051 foreach my $ct (keys %{$p->{'ctags'}}) {
4052 $ctags{$ct} += $p->{'ctags'}->{$ct};
4053 }
4054 }
4055 my $cloud = git_populate_project_tagcloud(\%ctags);
4056 print git_show_project_tagcloud($cloud, 64);
4057 }
4058
e30496df
PB
4059 print "<table class=\"project_list\">\n";
4060 unless ($no_header) {
4061 print "<tr>\n";
4062 if ($check_forks) {
4063 print "<th></th>\n";
4064 }
6b28da67
PB
4065 print_sort_th('project', $order, 'Project');
4066 print_sort_th('descr', $order, 'Description');
4067 print_sort_th('owner', $order, 'Owner');
4068 print_sort_th('age', $order, 'Last Change');
7da0f3a4 4069 print "<th></th>\n" . # for links
e30496df
PB
4070 "</tr>\n";
4071 }
4072 my $alternate = 1;
aed93de4 4073 my $tagfilter = $cgi->param('by_tag');
e30496df
PB
4074 for (my $i = $from; $i <= $to; $i++) {
4075 my $pr = $projects[$i];
42326110 4076
aed93de4 4077 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
0d1d154d
PB
4078 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4079 and not $pr->{'descr_long'} =~ /$searchtext/;
4080 # Weed out forks or non-matching entries of search
42326110
PB
4081 if ($check_forks) {
4082 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4083 $forkbase="^$forkbase" if $forkbase;
0d1d154d
PB
4084 next if not $searchtext and not $tagfilter and $show_ctags
4085 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
42326110
PB
4086 }
4087
e30496df
PB
4088 if ($alternate) {
4089 print "<tr class=\"dark\">\n";
4090 } else {
4091 print "<tr class=\"light\">\n";
4092 }
4093 $alternate ^= 1;
4094 if ($check_forks) {
4095 print "<td>";
4096 if ($pr->{'forks'}) {
83ee94c1 4097 print "<!-- $pr->{'forks'} -->\n";
e30496df
PB
4098 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4099 }
4100 print "</td>\n";
4101 }
4102 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4103 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
e88ce8a4
JN
4104 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4105 -class => "list", -title => $pr->{'descr_long'}},
4106 esc_html($pr->{'descr'})) . "</td>\n" .
d3cd2495 4107 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
e30496df 4108 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
785cdea9 4109 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
e30496df
PB
4110 "<td class=\"link\">" .
4111 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
faa1bbfd 4112 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
e30496df
PB
4113 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4114 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4115 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4116 "</td>\n" .
4117 "</tr>\n";
4118 }
4119 if (defined $extra) {
4120 print "<tr>\n";
4121 if ($check_forks) {
4122 print "<td></td>\n";
4123 }
4124 print "<td colspan=\"5\">$extra</td>\n" .
4125 "</tr>\n";
4126 }
4127 print "</table>\n";
4128}
4129
9f5dcb81
JN
4130sub git_shortlog_body {
4131 # uses global variable $project
190d7fdc 4132 my ($commitlist, $from, $to, $refs, $extra) = @_;
ddb8d900 4133
9f5dcb81 4134 $from = 0 unless defined $from;
190d7fdc 4135 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
9f5dcb81 4136
591ebf65 4137 print "<table class=\"shortlog\">\n";
6dd36acd 4138 my $alternate = 1;
9f5dcb81 4139 for (my $i = $from; $i <= $to; $i++) {
190d7fdc
RF
4140 my %co = %{$commitlist->[$i]};
4141 my $commit = $co{'id'};
847e01fb 4142 my $ref = format_ref_marker($refs, $commit);
9f5dcb81
JN
4143 if ($alternate) {
4144 print "<tr class=\"dark\">\n";
4145 } else {
4146 print "<tr class=\"light\">\n";
4147 }
4148 $alternate ^= 1;
ce58ec91 4149 my $author = chop_and_escape_str($co{'author_name'}, 10);
9f5dcb81
JN
4150 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4151 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4152 "<td><i>" . $author . "</i></td>\n" .
9f5dcb81 4153 "<td>";
952c65fc
JN
4154 print format_subject_html($co{'title'}, $co{'title_short'},
4155 href(action=>"commit", hash=>$commit), $ref);
9f5dcb81
JN
4156 print "</td>\n" .
4157 "<td class=\"link\">" .
4777b014 4158 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
35749ae5 4159 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
55ff35cb 4160 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
a3c8ab30
MM
4161 my $snapshot_links = format_snapshot_links($commit);
4162 if (defined $snapshot_links) {
4163 print " | " . $snapshot_links;
55ff35cb 4164 }
cb9c6e5b 4165 print "</td>\n" .
9f5dcb81
JN
4166 "</tr>\n";
4167 }
4168 if (defined $extra) {
4169 print "<tr>\n" .
4170 "<td colspan=\"4\">$extra</td>\n" .
4171 "</tr>\n";
4172 }
4173 print "</table>\n";
4174}
4175
581860e1
JN
4176sub git_history_body {
4177 # Warning: assumes constant type (blob or tree) during history
a8b983bf 4178 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
8be68352
JN
4179
4180 $from = 0 unless defined $from;
a8b983bf 4181 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
581860e1 4182
591ebf65 4183 print "<table class=\"history\">\n";
6dd36acd 4184 my $alternate = 1;
8be68352 4185 for (my $i = $from; $i <= $to; $i++) {
a8b983bf 4186 my %co = %{$commitlist->[$i]};
581860e1
JN
4187 if (!%co) {
4188 next;
4189 }
a8b983bf 4190 my $commit = $co{'id'};
581860e1
JN
4191
4192 my $ref = format_ref_marker($refs, $commit);
4193
4194 if ($alternate) {
4195 print "<tr class=\"dark\">\n";
4196 } else {
4197 print "<tr class=\"light\">\n";
4198 }
4199 $alternate ^= 1;
e076a0e7 4200 # shortlog uses chop_str($co{'author_name'}, 10)
ce58ec91 4201 my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
581860e1 4202 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4203 "<td><i>" . $author . "</i></td>\n" .
581860e1
JN
4204 "<td>";
4205 # originally git_history used chop_str($co{'title'}, 50)
952c65fc
JN
4206 print format_subject_html($co{'title'}, $co{'title_short'},
4207 href(action=>"commit", hash=>$commit), $ref);
581860e1
JN
4208 print "</td>\n" .
4209 "<td class=\"link\">" .
6d81c5a2
LT
4210 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4211 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
581860e1
JN
4212
4213 if ($ftype eq 'blob') {
4214 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4215 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4216 if (defined $blob_current && defined $blob_parent &&
4217 $blob_current ne $blob_parent) {
4218 print " | " .
420e92f2
JN
4219 $cgi->a({-href => href(action=>"blobdiff",
4220 hash=>$blob_current, hash_parent=>$blob_parent,
4221 hash_base=>$hash_base, hash_parent_base=>$commit,
4222 file_name=>$file_name)},
581860e1
JN
4223 "diff to current");
4224 }
4225 }
4226 print "</td>\n" .
4227 "</tr>\n";
4228 }
4229 if (defined $extra) {
4230 print "<tr>\n" .
4231 "<td colspan=\"4\">$extra</td>\n" .
4232 "</tr>\n";
4233 }
4234 print "</table>\n";
4235}
4236
717b8311
JN
4237sub git_tags_body {
4238 # uses global variable $project
4239 my ($taglist, $from, $to, $extra) = @_;
4240 $from = 0 unless defined $from;
4241 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4242
591ebf65 4243 print "<table class=\"tags\">\n";
6dd36acd 4244 my $alternate = 1;
717b8311
JN
4245 for (my $i = $from; $i <= $to; $i++) {
4246 my $entry = $taglist->[$i];
4247 my %tag = %$entry;
cd146408 4248 my $comment = $tag{'subject'};
717b8311
JN
4249 my $comment_short;
4250 if (defined $comment) {
4251 $comment_short = chop_str($comment, 30, 5);
4252 }
4253 if ($alternate) {
4254 print "<tr class=\"dark\">\n";
4255 } else {
4256 print "<tr class=\"light\">\n";
4257 }
4258 $alternate ^= 1;
27dd1a83
JN
4259 if (defined $tag{'age'}) {
4260 print "<td><i>$tag{'age'}</i></td>\n";
4261 } else {
4262 print "<td></td>\n";
4263 }
4264 print "<td>" .
1c2a4f5a 4265 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
63e4220b 4266 -class => "list name"}, esc_html($tag{'name'})) .
717b8311
JN
4267 "</td>\n" .
4268 "<td>";
4269 if (defined $comment) {
952c65fc
JN
4270 print format_subject_html($comment, $comment_short,
4271 href(action=>"tag", hash=>$tag{'id'}));
717b8311
JN
4272 }
4273 print "</td>\n" .
4274 "<td class=\"selflink\">";
4275 if ($tag{'type'} eq "tag") {
1c2a4f5a 4276 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
717b8311
JN
4277 } else {
4278 print "&nbsp;";
4279 }
4280 print "</td>\n" .
4281 "<td class=\"link\">" . " | " .
1c2a4f5a 4282 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
717b8311 4283 if ($tag{'reftype'} eq "commit") {
bf901f8e
JN
4284 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4285 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
717b8311 4286 } elsif ($tag{'reftype'} eq "blob") {
1c2a4f5a 4287 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
717b8311
JN
4288 }
4289 print "</td>\n" .
4290 "</tr>";
4291 }
4292 if (defined $extra) {
4293 print "<tr>\n" .
4294 "<td colspan=\"5\">$extra</td>\n" .
4295 "</tr>\n";
4296 }
4297 print "</table>\n";
4298}
4299
4300sub git_heads_body {
4301 # uses global variable $project
120ddde2 4302 my ($headlist, $head, $from, $to, $extra) = @_;
717b8311 4303 $from = 0 unless defined $from;
120ddde2 4304 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
717b8311 4305
591ebf65 4306 print "<table class=\"heads\">\n";
6dd36acd 4307 my $alternate = 1;
717b8311 4308 for (my $i = $from; $i <= $to; $i++) {
120ddde2 4309 my $entry = $headlist->[$i];
cd146408
JN
4310 my %ref = %$entry;
4311 my $curr = $ref{'id'} eq $head;
717b8311
JN
4312 if ($alternate) {
4313 print "<tr class=\"dark\">\n";
4314 } else {
4315 print "<tr class=\"light\">\n";
4316 }
4317 $alternate ^= 1;
cd146408
JN
4318 print "<td><i>$ref{'age'}</i></td>\n" .
4319 ($curr ? "<td class=\"current_head\">" : "<td>") .
bf901f8e 4320 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
cd146408 4321 -class => "list name"},esc_html($ref{'name'})) .
717b8311
JN
4322 "</td>\n" .
4323 "<td class=\"link\">" .
bf901f8e
JN
4324 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4325 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4326 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
717b8311
JN
4327 "</td>\n" .
4328 "</tr>";
4329 }
4330 if (defined $extra) {
4331 print "<tr>\n" .
4332 "<td colspan=\"3\">$extra</td>\n" .
4333 "</tr>\n";
4334 }
4335 print "</table>\n";
4336}
4337
8dbc0fce 4338sub git_search_grep_body {
5ad66088 4339 my ($commitlist, $from, $to, $extra) = @_;
8dbc0fce 4340 $from = 0 unless defined $from;
5ad66088 4341 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
8dbc0fce 4342
591ebf65 4343 print "<table class=\"commit_search\">\n";
8dbc0fce
RF
4344 my $alternate = 1;
4345 for (my $i = $from; $i <= $to; $i++) {
5ad66088 4346 my %co = %{$commitlist->[$i]};
8dbc0fce
RF
4347 if (!%co) {
4348 next;
4349 }
5ad66088 4350 my $commit = $co{'id'};
8dbc0fce
RF
4351 if ($alternate) {
4352 print "<tr class=\"dark\">\n";
4353 } else {
4354 print "<tr class=\"light\">\n";
4355 }
4356 $alternate ^= 1;
ce58ec91 4357 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
8dbc0fce 4358 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
e076a0e7 4359 "<td><i>" . $author . "</i></td>\n" .
8dbc0fce 4360 "<td>" .
be8b9063
JH
4361 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4362 -class => "list subject"},
4363 chop_and_escape_str($co{'title'}, 50) . "<br/>");
8dbc0fce
RF
4364 my $comment = $co{'comment'};
4365 foreach my $line (@$comment) {
6dfbb304 4366 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
be8b9063 4367 my ($lead, $match, $trail) = ($1, $2, $3);
b8d97d07
JN
4368 $match = chop_str($match, 70, 5, 'center');
4369 my $contextlen = int((80 - length($match))/2);
4370 $contextlen = 30 if ($contextlen > 30);
4371 $lead = chop_str($lead, $contextlen, 10, 'left');
4372 $trail = chop_str($trail, $contextlen, 10, 'right');
be8b9063
JH
4373
4374 $lead = esc_html($lead);
4375 $match = esc_html($match);
4376 $trail = esc_html($trail);
4377
4378 print "$lead<span class=\"match\">$match</span>$trail<br />";
8dbc0fce
RF
4379 }
4380 }
4381 print "</td>\n" .
4382 "<td class=\"link\">" .
4383 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4384 " | " .
f1fe8f5c
CR
4385 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4386 " | " .
8dbc0fce
RF
4387 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4388 print "</td>\n" .
4389 "</tr>\n";
4390 }
4391 if (defined $extra) {
4392 print "<tr>\n" .
4393 "<td colspan=\"3\">$extra</td>\n" .
4394 "</tr>\n";
4395 }
4396 print "</table>\n";
4397}
4398
717b8311
JN
4399## ======================================================================
4400## ======================================================================
4401## actions
4402
717b8311 4403sub git_project_list {
1b2d297e 4404 my $order = $input_params{'order'};
b06dcf8c 4405 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 4406 die_error(400, "Unknown order parameter");
6326b60c
JN
4407 }
4408
847e01fb 4409 my @list = git_get_projects_list();
717b8311 4410 if (!@list) {
074afaa0 4411 die_error(404, "No projects found");
717b8311 4412 }
6326b60c 4413
717b8311
JN
4414 git_header_html();
4415 if (-f $home_text) {
4416 print "<div class=\"index_include\">\n";
2dcb5e1a 4417 insert_file($home_text);
717b8311 4418 print "</div>\n";
9f5dcb81 4419 }
0d1d154d
PB
4420 print $cgi->startform(-method => "get") .
4421 "<p class=\"projsearch\">Search:\n" .
4422 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4423 "</p>" .
4424 $cgi->end_form() . "\n";
e30496df
PB
4425 git_project_list_body(\@list, $order);
4426 git_footer_html();
4427}
4428
4429sub git_forks {
1b2d297e 4430 my $order = $input_params{'order'};
b06dcf8c 4431 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
074afaa0 4432 die_error(400, "Unknown order parameter");
717b8311 4433 }
e30496df
PB
4434
4435 my @list = git_get_projects_list($project);
4436 if (!@list) {
074afaa0 4437 die_error(404, "No forks found");
9f5dcb81 4438 }
e30496df
PB
4439
4440 git_header_html();
4441 git_print_page_nav('','');
4442 git_print_header_div('summary', "$project forks");
4443 git_project_list_body(\@list, $order);
717b8311 4444 git_footer_html();
9f5dcb81
JN
4445}
4446
fc2b2be0 4447sub git_project_index {
e30496df 4448 my @projects = git_get_projects_list($project);
fc2b2be0
JN
4449
4450 print $cgi->header(
4451 -type => 'text/plain',
4452 -charset => 'utf-8',
ab41dfbf 4453 -content_disposition => 'inline; filename="index.aux"');
fc2b2be0
JN
4454
4455 foreach my $pr (@projects) {
4456 if (!exists $pr->{'owner'}) {
76e4f5d0 4457 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
fc2b2be0
JN
4458 }
4459
4460 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4461 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4462 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4463 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4464 $path =~ s/ /\+/g;
4465 $owner =~ s/ /\+/g;
4466
4467 print "$path $owner\n";
4468 }
4469}
4470
ede5e100 4471sub git_summary {
847e01fb 4472 my $descr = git_get_project_description($project) || "none";
a979d128 4473 my %co = parse_commit("HEAD");
785cdea9 4474 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
a979d128 4475 my $head = $co{'id'};
ede5e100 4476
1e0cf030 4477 my $owner = git_get_project_owner($project);
ede5e100 4478
cd146408 4479 my $refs = git_get_references();
313ce8ce
RF
4480 # These get_*_list functions return one more to allow us to see if
4481 # there are more ...
4482 my @taglist = git_get_tags_list(16);
4483 my @headlist = git_get_heads_list(16);
e30496df 4484 my @forklist;
25b2790f 4485 my $check_forks = gitweb_check_feature('forks');
5dd5ed09
JH
4486
4487 if ($check_forks) {
e30496df
PB
4488 @forklist = git_get_projects_list($project);
4489 }
120ddde2 4490
ede5e100 4491 git_header_html();
847e01fb 4492 git_print_page_nav('summary','', $head);
9f5dcb81 4493
19806691 4494 print "<div class=\"title\">&nbsp;</div>\n";
591ebf65 4495 print "<table class=\"projects_list\">\n" .
a476142f
PB
4496 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4497 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
785cdea9 4498 if (defined $cd{'rfc2822'}) {
a476142f 4499 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
785cdea9
JN
4500 }
4501
e79ca7cc
JN
4502 # use per project git URL list in $projectroot/$project/cloneurl
4503 # or make project git URL from git base URL and project name
19a8721e 4504 my $url_tag = "URL";
e79ca7cc
JN
4505 my @url_list = git_get_project_url_list($project);
4506 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4507 foreach my $git_url (@url_list) {
4508 next unless $git_url;
a476142f 4509 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
19a8721e
JN
4510 $url_tag = "";
4511 }
aed93de4
PB
4512
4513 # Tag cloud
25b2790f 4514 my $show_ctags = gitweb_check_feature('ctags');
aed93de4
PB
4515 if ($show_ctags) {
4516 my $ctags = git_get_project_ctags($project);
4517 my $cloud = git_populate_project_tagcloud($ctags);
4518 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4519 print "</td>\n<td>" unless %$ctags;
4520 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4521 print "</td>\n<td>" if %$ctags;
4522 print git_show_project_tagcloud($cloud, 48);
4523 print "</td></tr>";
4524 }
4525
19a8721e 4526 print "</table>\n";
9f5dcb81 4527
7e1100e9
MM
4528 # If XSS prevention is on, we don't include README.html.
4529 # TODO: Allow a readme in some safe format.
4530 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
2dcb5e1a
JN
4531 print "<div class=\"title\">readme</div>\n" .
4532 "<div class=\"readme\">\n";
4533 insert_file("$projectroot/$project/README.html");
4534 print "\n</div>\n"; # class="readme"
447ef09a
PB
4535 }
4536
313ce8ce
RF
4537 # we need to request one more than 16 (0..15) to check if
4538 # those 16 are all
785cdea9
JN
4539 my @commitlist = $head ? parse_commits($head, 17) : ();
4540 if (@commitlist) {
4541 git_print_header_div('shortlog');
4542 git_shortlog_body(\@commitlist, 0, 15, $refs,
4543 $#commitlist <= 15 ? undef :
4544 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4545 }
ede5e100 4546
120ddde2 4547 if (@taglist) {
847e01fb 4548 git_print_header_div('tags');
120ddde2 4549 git_tags_body(\@taglist, 0, 15,
313ce8ce 4550 $#taglist <= 15 ? undef :
1c2a4f5a 4551 $cgi->a({-href => href(action=>"tags")}, "..."));
ede5e100 4552 }
0db37973 4553
120ddde2 4554 if (@headlist) {
847e01fb 4555 git_print_header_div('heads');
120ddde2 4556 git_heads_body(\@headlist, $head, 0, 15,
313ce8ce 4557 $#headlist <= 15 ? undef :
1c2a4f5a 4558 $cgi->a({-href => href(action=>"heads")}, "..."));
0db37973 4559 }
9f5dcb81 4560
e30496df
PB
4561 if (@forklist) {
4562 git_print_header_div('forks');
f04f27e8 4563 git_project_list_body(\@forklist, 'age', 0, 15,
aaca9675 4564 $#forklist <= 15 ? undef :
e30496df 4565 $cgi->a({-href => href(action=>"forks")}, "..."),
f04f27e8 4566 'no_header');
e30496df
PB
4567 }
4568
ede5e100
KS
4569 git_footer_html();
4570}
4571
d8a20ba9 4572sub git_tag {
847e01fb 4573 my $head = git_get_head_hash($project);
d8a20ba9 4574 git_header_html();
847e01fb
JN
4575 git_print_page_nav('','', $head,undef,$head);
4576 my %tag = parse_tag($hash);
198a2a8a
JN
4577
4578 if (! %tag) {
074afaa0 4579 die_error(404, "Unknown tag object");
198a2a8a
JN
4580 }
4581
847e01fb 4582 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
d8a20ba9 4583 print "<div class=\"title_text\">\n" .
591ebf65 4584 "<table class=\"object_header\">\n" .
e4669df9
KS
4585 "<tr>\n" .
4586 "<td>object</td>\n" .
952c65fc
JN
4587 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4588 $tag{'object'}) . "</td>\n" .
4589 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4590 $tag{'type'}) . "</td>\n" .
e4669df9 4591 "</tr>\n";
d8a20ba9 4592 if (defined($tag{'author'})) {
847e01fb 4593 my %ad = parse_date($tag{'epoch'}, $tag{'tz'});
40c13813 4594 print "<tr><td>author</td><td>" . esc_html($tag{'author'}) . "</td></tr>\n";
952c65fc
JN
4595 print "<tr><td></td><td>" . $ad{'rfc2822'} .
4596 sprintf(" (%02d:%02d %s)", $ad{'hour_local'}, $ad{'minute_local'}, $ad{'tz_local'}) .
4597 "</td></tr>\n";
d8a20ba9
KS
4598 }
4599 print "</table>\n\n" .
4600 "</div>\n";
4601 print "<div class=\"page_body\">";
4602 my $comment = $tag{'comment'};
4603 foreach my $line (@$comment) {
7002243f 4604 chomp $line;
793c400c 4605 print esc_html($line, -nbsp=>1) . "<br/>\n";
d8a20ba9
KS
4606 }
4607 print "</div>\n";
4608 git_footer_html();
4609}
4610
3a5b919c 4611sub git_blame {
d2ce10d7 4612 # permissions
25b2790f 4613 gitweb_check_feature('blame')
d2ce10d7 4614 or die_error(403, "Blame view not allowed");
074afaa0 4615
d2ce10d7 4616 # error checking
074afaa0 4617 die_error(400, "No file name given") unless $file_name;
847e01fb 4618 $hash_base ||= git_get_head_hash($project);
d2ce10d7 4619 die_error(404, "Couldn't find base commit") unless $hash_base;
847e01fb 4620 my %co = parse_commit($hash_base)
074afaa0 4621 or die_error(404, "Commit not found");
d2ce10d7 4622 my $ftype = "blob";
1f2857ea
LT
4623 if (!defined $hash) {
4624 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
074afaa0 4625 or die_error(404, "Error looking up file");
d2ce10d7
JN
4626 } else {
4627 $ftype = git_get_type($hash);
4628 if ($ftype !~ "blob") {
4629 die_error(400, "Object is not a blob");
4630 }
1f2857ea 4631 }
d2ce10d7
JN
4632
4633 # run git-blame --porcelain
4634 open my $fd, "-|", git_cmd(), "blame", '-p',
4635 $hash_base, '--', $file_name
074afaa0 4636 or die_error(500, "Open git-blame failed");
d2ce10d7
JN
4637
4638 # page header
1f2857ea 4639 git_header_html();
0d83ddc4 4640 my $formats_nav =
a3823e5a 4641 $cgi->a({-href => href(action=>"blob", -replay=>1)},
952c65fc
JN
4642 "blob") .
4643 " | " .
a3823e5a
JN
4644 $cgi->a({-href => href(action=>"history", -replay=>1)},
4645 "history") .
cae1862a 4646 " | " .
952c65fc 4647 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
f35274da 4648 "HEAD");
847e01fb
JN
4649 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4650 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
59fb1c94 4651 git_print_page_path($file_name, $ftype, $hash_base);
d2ce10d7
JN
4652
4653 # page body
4654 my @rev_color = qw(light2 dark2);
cc1bf97e
LT
4655 my $num_colors = scalar(@rev_color);
4656 my $current_color = 0;
d2ce10d7
JN
4657 my %metainfo = ();
4658
59b9f61a
JN
4659 print <<HTML;
4660<div class="page_body">
4661<table class="blame">
4662<tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4663HTML
d2ce10d7
JN
4664 LINE:
4665 while (my $line = <$fd>) {
4666 chomp $line;
4667 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4668 # no <lines in group> for subsequent lines in group of lines
d15c55aa 4669 my ($full_rev, $orig_lineno, $lineno, $group_size) =
d2ce10d7 4670 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
eeef88cd
JH
4671 if (!exists $metainfo{$full_rev}) {
4672 $metainfo{$full_rev} = {};
4673 }
4674 my $meta = $metainfo{$full_rev};
d2ce10d7
JN
4675 my $data;
4676 while ($data = <$fd>) {
4677 chomp $data;
4678 last if ($data =~ s/^\t//); # contents of line
4679 if ($data =~ /^(\S+) (.*)$/) {
eeef88cd
JH
4680 $meta->{$1} = $2;
4681 }
4682 }
d2ce10d7 4683 my $short_rev = substr($full_rev, 0, 8);
eeef88cd 4684 my $author = $meta->{'author'};
d2ce10d7
JN
4685 my %date =
4686 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
eeef88cd
JH
4687 my $date = $date{'iso-tz'};
4688 if ($group_size) {
d2ce10d7 4689 $current_color = ($current_color + 1) % $num_colors;
cc1bf97e 4690 }
4a24bfc2 4691 print "<tr id=\"l$lineno\" class=\"$rev_color[$current_color]\">\n";
eeef88cd
JH
4692 if ($group_size) {
4693 print "<td class=\"sha1\"";
5ad0828c 4694 print " title=\"". esc_html($author) . ", $date\"";
eeef88cd
JH
4695 print " rowspan=\"$group_size\"" if ($group_size > 1);
4696 print ">";
4697 print $cgi->a({-href => href(action=>"commit",
a23f0a73
JN
4698 hash=>$full_rev,
4699 file_name=>$file_name)},
d2ce10d7 4700 esc_html($short_rev));
eeef88cd 4701 print "</td>\n";
9dc5f8c9 4702 }
39c19ce2
JN
4703 my $parent_commit;
4704 if (!exists $meta->{'parent'}) {
4705 open (my $dd, "-|", git_cmd(), "rev-parse", "$full_rev^")
4706 or die_error(500, "Open git-rev-parse failed");
4707 $parent_commit = <$dd>;
4708 close $dd;
4709 chomp($parent_commit);
4710 $meta->{'parent'} = $parent_commit;
4711 } else {
4712 $parent_commit = $meta->{'parent'};
4713 }
eeef88cd 4714 my $blamed = href(action => 'blame',
a23f0a73
JN
4715 file_name => $meta->{'filename'},
4716 hash_base => $parent_commit);
eeef88cd
JH
4717 print "<td class=\"linenr\">";
4718 print $cgi->a({ -href => "$blamed#l$orig_lineno",
a23f0a73
JN
4719 -class => "linenr" },
4720 esc_html($lineno));
eeef88cd 4721 print "</td>";
1f2857ea
LT
4722 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4723 print "</tr>\n";
4724 }
4725 print "</table>\n";
4726 print "</div>";
952c65fc
JN
4727 close $fd
4728 or print "Reading blob failed\n";
d2ce10d7
JN
4729
4730 # page footer
1f2857ea
LT
4731 git_footer_html();
4732}
4733
717b8311 4734sub git_tags {
847e01fb 4735 my $head = git_get_head_hash($project);
717b8311 4736 git_header_html();
847e01fb
JN
4737 git_print_page_nav('','', $head,undef,$head);
4738 git_print_header_div('summary', $project);
2d007374 4739
cd146408
JN
4740 my @tagslist = git_get_tags_list();
4741 if (@tagslist) {
4742 git_tags_body(\@tagslist);
2d007374 4743 }
717b8311 4744 git_footer_html();
2d007374
PB
4745}
4746
717b8311 4747sub git_heads {
847e01fb 4748 my $head = git_get_head_hash($project);
717b8311 4749 git_header_html();
847e01fb
JN
4750 git_print_page_nav('','', $head,undef,$head);
4751 git_print_header_div('summary', $project);
930cf7dd 4752
cd146408
JN
4753 my @headslist = git_get_heads_list();
4754 if (@headslist) {
4755 git_heads_body(\@headslist, $head);
f5aa79d9 4756 }
717b8311 4757 git_footer_html();
f5aa79d9
JN
4758}
4759
19806691 4760sub git_blob_plain {
7f718e8b 4761 my $type = shift;
f2e73302 4762 my $expires;
f2e73302 4763
cff0771b 4764 if (!defined $hash) {
5be01bc8 4765 if (defined $file_name) {
847e01fb 4766 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 4767 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 4768 or die_error(404, "Cannot find file");
5be01bc8 4769 } else {
074afaa0 4770 die_error(400, "No file name defined");
5be01bc8 4771 }
800764cf
MW
4772 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4773 # blobs defined by non-textual hash id's can be cached
4774 $expires = "+1d";
5be01bc8 4775 }
800764cf 4776
25691fbe 4777 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 4778 or die_error(500, "Open git-cat-file blob '$hash' failed");
930cf7dd 4779
7f718e8b
JN
4780 # content-type (can include charset)
4781 $type = blob_contenttype($fd, $file_name, $type);
f5aa79d9 4782
7f718e8b 4783 # "save as" filename, even when no $file_name is given
f5aa79d9 4784 my $save_as = "$hash";
9312944d
KS
4785 if (defined $file_name) {
4786 $save_as = $file_name;
f5aa79d9
JN
4787 } elsif ($type =~ m/^text\//) {
4788 $save_as .= '.txt';
9312944d 4789 }
f5aa79d9 4790
7e1100e9
MM
4791 # With XSS prevention on, blobs of all types except a few known safe
4792 # ones are served with "Content-Disposition: attachment" to make sure
4793 # they don't run in our security domain. For certain image types,
4794 # blob view writes an <img> tag referring to blob_plain view, and we
4795 # want to be sure not to break that by serving the image as an
4796 # attachment (though Firefox 3 doesn't seem to care).
4797 my $sandbox = $prevent_xss &&
4798 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4799
f2e73302 4800 print $cgi->header(
7f718e8b
JN
4801 -type => $type,
4802 -expires => $expires,
7e1100e9
MM
4803 -content_disposition =>
4804 ($sandbox ? 'attachment' : 'inline')
4805 . '; filename="' . $save_as . '"');
19806691 4806 undef $/;
ad14e931 4807 binmode STDOUT, ':raw';
19806691 4808 print <$fd>;
ad14e931 4809 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
19806691
KS
4810 $/ = "\n";
4811 close $fd;
4812}
4813
930cf7dd 4814sub git_blob {
f2e73302 4815 my $expires;
f2e73302 4816
cff0771b 4817 if (!defined $hash) {
5be01bc8 4818 if (defined $file_name) {
847e01fb 4819 my $base = $hash_base || git_get_head_hash($project);
5be01bc8 4820 $hash = git_get_hash_by_path($base, $file_name, "blob")
074afaa0 4821 or die_error(404, "Cannot find file");
5be01bc8 4822 } else {
074afaa0 4823 die_error(400, "No file name defined");
5be01bc8 4824 }
800764cf
MW
4825 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4826 # blobs defined by non-textual hash id's can be cached
4827 $expires = "+1d";
5be01bc8 4828 }
800764cf 4829
25b2790f 4830 my $have_blame = gitweb_check_feature('blame');
25691fbe 4831 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
074afaa0 4832 or die_error(500, "Couldn't cat $file_name, $hash");
847e01fb 4833 my $mimetype = blob_mimetype($fd, $file_name);
dfa7c7d2 4834 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
930cf7dd
LT
4835 close $fd;
4836 return git_blob_plain($mimetype);
4837 }
5a4cf334
JN
4838 # we can have blame only for text/* mimetype
4839 $have_blame &&= ($mimetype =~ m!^text/!);
4840
f2e73302 4841 git_header_html(undef, $expires);
0d83ddc4 4842 my $formats_nav = '';
847e01fb 4843 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
930cf7dd
LT
4844 if (defined $file_name) {
4845 if ($have_blame) {
952c65fc 4846 $formats_nav .=
a3823e5a 4847 $cgi->a({-href => href(action=>"blame", -replay=>1)},
952c65fc
JN
4848 "blame") .
4849 " | ";
930cf7dd 4850 }
0d83ddc4 4851 $formats_nav .=
a3823e5a 4852 $cgi->a({-href => href(action=>"history", -replay=>1)},
cae1862a
PB
4853 "history") .
4854 " | " .
a3823e5a 4855 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
35329cc1 4856 "raw") .
952c65fc
JN
4857 " | " .
4858 $cgi->a({-href => href(action=>"blob",
4859 hash_base=>"HEAD", file_name=>$file_name)},
f35274da 4860 "HEAD");
930cf7dd 4861 } else {
952c65fc 4862 $formats_nav .=
a3823e5a
JN
4863 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
4864 "raw");
930cf7dd 4865 }
847e01fb
JN
4866 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4867 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
930cf7dd
LT
4868 } else {
4869 print "<div class=\"page_nav\">\n" .
4870 "<br/><br/></div>\n" .
4871 "<div class=\"title\">$hash</div>\n";
4872 }
59fb1c94 4873 git_print_page_path($file_name, "blob", $hash_base);
930cf7dd 4874 print "<div class=\"page_body\">\n";
dfa7c7d2 4875 if ($mimetype =~ m!^image/!) {
5a4cf334
JN
4876 print qq!<img type="$mimetype"!;
4877 if ($file_name) {
4878 print qq! alt="$file_name" title="$file_name"!;
4879 }
4880 print qq! src="! .
4881 href(action=>"blob_plain", hash=>$hash,
4882 hash_base=>$hash_base, file_name=>$file_name) .
4883 qq!" />\n!;
dfa7c7d2
JN
4884 } else {
4885 my $nr;
4886 while (my $line = <$fd>) {
4887 chomp $line;
4888 $nr++;
4889 $line = untabify($line);
4890 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
4891 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
4892 }
930cf7dd 4893 }
952c65fc
JN
4894 close $fd
4895 or print "Reading blob failed.\n";
930cf7dd
LT
4896 print "</div>";
4897 git_footer_html();
4898}
4899
09bd7898 4900sub git_tree {
6f7ea5fb
LT
4901 if (!defined $hash_base) {
4902 $hash_base = "HEAD";
4903 }
b87d78d6 4904 if (!defined $hash) {
09bd7898 4905 if (defined $file_name) {
6f7ea5fb
LT
4906 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
4907 } else {
4908 $hash = $hash_base;
10dba28d 4909 }
e925f38c 4910 }
2d7a3532 4911 die_error(404, "No such tree") unless defined($hash);
232ff553 4912 $/ = "\0";
25691fbe 4913 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
074afaa0 4914 or die_error(500, "Open git-ls-tree failed");
0881d2d1 4915 my @entries = map { chomp; $_ } <$fd>;
074afaa0 4916 close $fd or die_error(404, "Reading tree failed");
232ff553 4917 $/ = "\n";
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;
c994d620 5812 $/ = "\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;
5882 $/ = "\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}