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