Merge commit 'refs/top-bases/t/girocco/style-updates' into t/girocco/style-updates
[git/gitweb.git] / gitweb / gitweb.perl
blobc1040e53f56a908220bae364c67468c1dd1f76a1
1 #!/usr/bin/perl
3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
10 use 5.008;
11 use strict;
12 use warnings;
13 use CGI qw(:standard :escapeHTML -nosticky);
14 use CGI::Util qw(unescape);
15 use CGI::Carp qw(fatalsToBrowser set_message);
16 use Encode;
17 use Fcntl ':mode';
18 use File::Find qw();
19 use File::Basename qw(basename);
20 use File::Spec;
21 use Time::HiRes qw(gettimeofday tv_interval);
22 use Time::Local;
23 use constant GITWEB_CACHE_FORMAT => "Gitweb Cache Format 3";
24 binmode STDOUT, ':utf8';
26 if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) {
27 eval 'sub CGI::multi_param { CGI::param(@_) }'
30 our $t0 = [ gettimeofday() ];
31 our $number_of_git_cmds = 0;
32 our ($mdotsep, $barsep, $spcsep);
34 BEGIN {
35 *mdotsep = \'<span class="mdotsep">&#160;&#183;&#160;</span>';
36 *barsep = \'<span class="barsep">&#160;|&#160;</span>';
37 *spcsep = \'<span class="spcsep">&#160</span>';
38 CGI->compile() if $ENV{'MOD_PERL'};
41 our $version = "++GIT_VERSION++";
43 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
44 sub evaluate_uri {
45 our $cgi;
47 our $my_url = $cgi->url();
48 our $my_uri = $cgi->url(-absolute => 1);
50 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
51 # needed and used only for URLs with nonempty PATH_INFO
52 # This must be an absolute URL (i.e. no scheme, host or port), NOT a full one
53 our $base_url = $my_uri || '/';
55 # When the script is used as DirectoryIndex, the URL does not contain the name
56 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
57 # have to do it ourselves. We make $path_info global because it's also used
58 # later on.
60 # Another issue with the script being the DirectoryIndex is that the resulting
61 # $my_url data is not the full script URL: this is good, because we want
62 # generated links to keep implying the script name if it wasn't explicitly
63 # indicated in the URL we're handling, but it means that $my_url cannot be used
64 # as base URL.
65 # Therefore, if we needed to strip PATH_INFO, then we know that we have
66 # to build the base URL ourselves:
67 our $path_info = decode_utf8($ENV{"PATH_INFO"});
68 if ($path_info) {
69 # $path_info has already been URL-decoded by the web server, but
70 # $my_url and $my_uri have not. URL-decode them so we can properly
71 # strip $path_info.
72 $my_url = unescape($my_url);
73 $my_uri = unescape($my_uri);
74 if ($my_url =~ s,\Q$path_info\E$,, &&
75 $my_uri =~ s,\Q$path_info\E$,, &&
76 defined $ENV{'SCRIPT_NAME'}) {
77 $base_url = $ENV{'SCRIPT_NAME'} || '/';
81 # target of the home link on top of all pages
82 our $home_link = $my_uri || "/";
85 # core git executable to use
86 # this can just be "git" if your webserver has a sensible PATH
87 our $GIT = "++GIT_BINDIR++/git";
89 # absolute fs-path which will be prepended to the project path
90 #our $projectroot = "/pub/scm";
91 our $projectroot = "++GITWEB_PROJECTROOT++";
93 # fs traversing limit for getting project list
94 # the number is relative to the projectroot
95 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
97 # string of the home link on top of all pages
98 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
100 # extra breadcrumbs preceding the home link
101 our @extra_breadcrumbs = ();
103 # name of your site or organization to appear in page titles
104 # replace this with something more descriptive for clearer bookmarks
105 our $site_name = "++GITWEB_SITENAME++"
106 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
108 # html snippet to include in the <head> section of each page
109 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
110 # filename of html text to include at top of each page
111 our $site_header = "++GITWEB_SITE_HEADER++";
112 # html text to include at home page
113 our $home_text = "++GITWEB_HOMETEXT++";
114 # filename of html text to include at bottom of each page
115 our $site_footer = "++GITWEB_SITE_FOOTER++";
117 # URI of stylesheets
118 our @stylesheets = ("++GITWEB_CSS++");
119 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
120 our $stylesheet = undef;
121 # URI of GIT logo (72x27 size)
122 our $logo = "++GITWEB_LOGO++";
123 # URI of GIT favicon, assumed to be image/png type
124 our $favicon = "++GITWEB_FAVICON++";
125 # URI of gitweb.js (JavaScript code for gitweb)
126 our $javascript = "++GITWEB_JS++";
128 # URI and label (title) of GIT logo link
129 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
130 #our $logo_label = "git documentation";
131 our $logo_url = "http://git-scm.com/";
132 our $logo_label = "git homepage";
134 # source of projects list
135 our $projects_list = "++GITWEB_LIST++";
137 # the width (in characters) of the projects list "Description" column
138 our $projects_list_description_width = 25;
140 # group projects by category on the projects list
141 # (enabled if this variable evaluates to true)
142 our $projects_list_group_categories = 0;
144 # default category if none specified
145 # (leave the empty string for no category)
146 our $project_list_default_category = "";
148 # default order of projects list
149 # valid values are none, project, descr, owner, and age
150 our $default_projects_order = "project";
152 # default order of refs list
153 # valid values are age and name
154 our $default_refs_order = "age";
156 # show repository only if this file exists
157 # (only effective if this variable evaluates to true)
158 our $export_ok = "++GITWEB_EXPORT_OK++";
160 # don't generate age column on the projects list page
161 our $omit_age_column = 0;
163 # use contents of this file (in iso, iso-strict or raw format) as
164 # the last activity data if it exists and is a valid date
165 our $lastactivity_file = undef;
167 # don't generate information about owners of repositories
168 our $omit_owner=0;
170 # owner link hook given owner name (full and NOT obfuscated)
171 # should return full URL-escaped link to attach to owner, for example:
172 # sub { return "/showowner.cgi?owner=".CGI::Util::escape($_[0]); }
173 our $owner_link_hook = undef;
175 # show repository only if this subroutine returns true
176 # when given the path to the project, for example:
177 # sub { return -e "$_[0]/git-daemon-export-ok"; }
178 our $export_auth_hook = undef;
180 # only allow viewing of repositories also shown on the overview page
181 our $strict_export = "++GITWEB_STRICT_EXPORT++";
183 # base URL for bundle info link shown on summary page, but only if
184 # this config item is defined AND a 'bundles' subdirectory exists
185 # in the project's repository.
186 # i.e. full URL is "git_base_bundles_url/$project/bundles"
187 our $git_base_bundles_url = undef;
189 # list of git base URLs used for URL to where fetch project from,
190 # i.e. full URL is "$git_base_url/$project"
191 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
193 # URLs designated for pushing new changes, extended by the
194 # project name (i.e. "$git_base_push_url[0]/$project")
195 our @git_base_push_urls = ();
197 # https hint html inserted right after any https push URL (undef for none)
198 our $https_hint_html = undef;
200 # default blob_plain mimetype and default charset for text/plain blob
201 our $default_blob_plain_mimetype = 'application/octet-stream';
202 our $default_text_plain_charset = undef;
204 # file to use for guessing MIME types before trying /etc/mime.types
205 # (relative to the current git repository)
206 our $mimetypes_file = undef;
208 # assume this charset if line contains non-UTF-8 characters;
209 # it should be valid encoding (see Encoding::Supported(3pm) for list),
210 # for which encoding all byte sequences are valid, for example
211 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
212 # could be even 'utf-8' for the old behavior)
213 our $fallback_encoding = 'latin1';
215 # rename detection options for git-diff and git-diff-tree
216 # - default is '-M', with the cost proportional to
217 # (number of removed files) * (number of new files).
218 # - more costly is '-C' (which implies '-M'), with the cost proportional to
219 # (number of changed files + number of removed files) * (number of new files)
220 # - even more costly is '-C', '--find-copies-harder' with cost
221 # (number of files in the original tree) * (number of new files)
222 # - one might want to include '-B' option, e.g. '-B', '-M'
223 our @diff_opts = ('-M'); # taken from git_commit
225 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
226 # the directory must exist and be writable by the process running gitweb.
227 # additionally some actions must be selected for caching in %html_cache_actions
228 # - default is 'htmlcache'
229 our $html_cache_dir = 'htmlcache';
231 # which actions to cache in $html_cache_dir
232 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
233 # process running gitweb, then any actions selected here will have their output
234 # cached and the cache file will be returned instead of regenerating the page
235 # if it exists. For this to be useful, an external process must create the
236 # 'changed' file (if it does not already exist) in the $html_cache_dir whenever
237 # the project information has been changed. Alternatively it may create a
238 # "$action.changed" file (if it does not exist) instead to limit the changes
239 # to just "$action" instead of any action. If 'changed' or "$action.changed"
240 # exist, then the cached version will never be used for "$action" and a new
241 # cache page will be regenerated (and the "changed" files removed as appropriate).
243 # Additionally if $projlist_cache_lifetime is > 0 AND the forks feature has been
244 # enabled ('$feature{'forks'}{'default'} = [1];') then additionally an external
245 # process must create the 'forkchange' file or update its timestamp if it already
246 # exists whenever a fork is added to or removed from the project (as well as
247 # create the 'changed' or "$action.changed" file). Otherwise the "forks"
248 # section on the summary page may remain out-of-date indefinately.
250 # - default is none
251 # currently only caching of the summary page is supported
252 # - to enable caching of the summary page use:
253 # $html_cache_actions{'summary'} = 1;
254 our %html_cache_actions = ();
256 # utility to automatically produce a default README.html if README.html is
257 # enabled and it does not exist or is 0 bytes in length. If this is set to an
258 # executable utility that takes an absolute path to a .git directory as its
259 # first argument and outputs an HTML fragment to use for README.html, then
260 # it will be called when README.html is enabled but empty or missing.
261 our $git_automatic_readme_html = undef;
263 # Disables features that would allow repository owners to inject script into
264 # the gitweb domain.
265 our $prevent_xss = 0;
267 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
268 # Only used when highlight is enabled or snapshots with compressors are enabled.
269 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
271 # Path to the highlight executable to use (must be the one from
272 # http://www.andre-simon.de due to assumptions about parameters and output).
273 # Useful if highlight is not installed on your webserver's PATH.
274 # [Default: highlight]
275 our $highlight_bin = "++HIGHLIGHT_BIN++";
277 # Whether to include project list on the gitweb front page; 0 means yes,
278 # 1 means no list but show tag cloud if enabled (all projects still need
279 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
280 # (very fast)
281 our $frontpage_no_project_list = 0;
283 # projects list cache for busy sites with many projects;
284 # if you set this to non-zero, it will be used as the cached
285 # index lifetime in minutes
287 # the cached list version is stored in $cache_dir/$cache_name and can
288 # be tweaked by other scripts running with the same uid as gitweb -
289 # use this ONLY at secure installations; only single gitweb project
290 # root per system is supported, unless you tweak configuration!
291 our $projlist_cache_lifetime = 0; # in minutes
292 # FHS compliant $cache_dir would be "/var/cache/gitweb"
293 our $cache_dir =
294 (defined $ENV{'TMPDIR'} ? $ENV{'TMPDIR'} : '/tmp').'/gitweb';
295 our $projlist_cache_name = 'gitweb.index.cache';
296 our $cache_grpshared = 0;
298 # information about snapshot formats that gitweb is capable of serving
299 our %known_snapshot_formats = (
300 # name => {
301 # 'display' => display name,
302 # 'type' => mime type,
303 # 'suffix' => filename suffix,
304 # 'format' => --format for git-archive,
305 # 'compressor' => [compressor command and arguments]
306 # (array reference, optional)
307 # 'disabled' => boolean (optional)}
309 'tgz' => {
310 'display' => 'tar.gz',
311 'type' => 'application/x-gzip',
312 'suffix' => '.tar.gz',
313 'format' => 'tar',
314 'compressor' => ['gzip', '-n']},
316 'tbz2' => {
317 'display' => 'tar.bz2',
318 'type' => 'application/x-bzip2',
319 'suffix' => '.tar.bz2',
320 'format' => 'tar',
321 'compressor' => ['bzip2']},
323 'txz' => {
324 'display' => 'tar.xz',
325 'type' => 'application/x-xz',
326 'suffix' => '.tar.xz',
327 'format' => 'tar',
328 'compressor' => ['xz'],
329 'disabled' => 1},
331 'zip' => {
332 'display' => 'zip',
333 'type' => 'application/x-zip',
334 'suffix' => '.zip',
335 'format' => 'zip'},
338 # Aliases so we understand old gitweb.snapshot values in repository
339 # configuration.
340 our %known_snapshot_format_aliases = (
341 'gzip' => 'tgz',
342 'bzip2' => 'tbz2',
343 'xz' => 'txz',
345 # backward compatibility: legacy gitweb config support
346 'x-gzip' => undef, 'gz' => undef,
347 'x-bzip2' => undef, 'bz2' => undef,
348 'x-zip' => undef, '' => undef,
351 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
352 # are changed, it may be appropriate to change these values too via
353 # $GITWEB_CONFIG.
354 our %avatar_size = (
355 'default' => 16,
356 'double' => 32
359 # Used to set the maximum load that we will still respond to gitweb queries.
360 # If server load exceed this value then return "503 server busy" error.
361 # If gitweb cannot determined server load, it is taken to be 0.
362 # Leave it undefined (or set to 'undef') to turn off load checking.
363 our $maxload = 300;
365 # configuration for 'highlight' (http://www.andre-simon.de/)
366 # match by basename
367 our %highlight_basename = (
368 #'Program' => 'py',
369 #'Library' => 'py',
370 'SConstruct' => 'py', # SCons equivalent of Makefile
371 'Makefile' => 'make',
372 'makefile' => 'make',
373 'GNUmakefile' => 'make',
374 'BSDmakefile' => 'make',
376 # match by shebang regex
377 our %highlight_shebang = (
378 # Each entry has a key which is the syntax to use and
379 # a value which is either a qr regex or an array of qr regexs to match
380 # against the first 128 (less if the blob is shorter) BYTES of the blob.
381 # We match /usr/bin/env items separately to require "/usr/bin/env" and
382 # allow a limited subset of NAME=value items to appear.
383 'awk' => [ qr,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
384 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
385 'make' => [ qr,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
386 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
387 'php' => [ qr,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
388 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
389 'pl' => [ qr,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
390 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
391 'py' => [ qr,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
392 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
393 'sh' => [ qr,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
394 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
395 'rb' => [ qr,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
396 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
398 # match by extension
399 our %highlight_ext = (
400 # main extensions, defining name of syntax;
401 # see files in /usr/share/highlight/langDefs/ directory
402 (map { $_ => $_ } qw(
403 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
404 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
405 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
406 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
407 go haskell hcl html httpd hx icl icn idl idlang ili
408 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
409 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
410 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
411 objc octave oorexx os oz pas php pike pl pl1 pov pro
412 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
413 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
414 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
415 yaiff znn)),
416 # alternate extensions, see /etc/highlight/filetypes.conf
417 (map { $_ => '4gl' } qw(informix)),
418 (map { $_ => 'a4c' } qw(ascend)),
419 (map { $_ => 'abp' } qw(abp4)),
420 (map { $_ => 'ada' } qw(a adb ads gnad)),
421 (map { $_ => 'ahk' } qw(autohotkey)),
422 (map { $_ => 'ampl' } qw(dat run)),
423 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
424 (map { $_ => 'as' } qw(actionscript)),
425 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
426 (map { $_ => 'asp' } qw(asa)),
427 (map { $_ => 'aspect' } qw(was wud)),
428 (map { $_ => 'ats' } qw(dats)),
429 (map { $_ => 'au3' } qw(autoit)),
430 (map { $_ => 'bat' } qw(cmd)),
431 (map { $_ => 'bb' } qw(blitzbasic)),
432 (map { $_ => 'bib' } qw(bibtex)),
433 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
434 (map { $_ => 'cb' } qw(clearbasic)),
435 (map { $_ => 'cfc' } qw(cfm coldfusion)),
436 (map { $_ => 'chl' } qw(chill)),
437 (map { $_ => 'cob' } qw(cbl cobol)),
438 (map { $_ => 'cs' } qw(csharp)),
439 (map { $_ => 'diff' } qw(patch)),
440 (map { $_ => 'dot' } qw(graphviz)),
441 (map { $_ => 'e' } qw(eiffel se)),
442 (map { $_ => 'erl' } qw(erlang hrl)),
443 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
444 (map { $_ => 'exp' } qw(express)),
445 (map { $_ => 'f90' } qw(f95)),
446 (map { $_ => 'flx' } qw(felix)),
447 (map { $_ => 'for' } qw(f f77 ftn)),
448 (map { $_ => 'fs' } qw(fsharp fsx)),
449 (map { $_ => 'haskell' } qw(hs)),
450 (map { $_ => 'html' } qw(htm xhtml)),
451 (map { $_ => 'hx' } qw(haxe)),
452 (map { $_ => 'icl' } qw(clean)),
453 (map { $_ => 'icn' } qw(icon)),
454 (map { $_ => 'ili' } qw(interlis)),
455 (map { $_ => 'inp' } qw(fame)),
456 (map { $_ => 'iss' } qw(innosetup)),
457 (map { $_ => 'j' } qw(jasmin)),
458 (map { $_ => 'java' } qw(groovy grv)),
459 (map { $_ => 'lbn' } qw(luban)),
460 (map { $_ => 'lgt' } qw(logtalk)),
461 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
462 (map { $_ => 'ls' } qw(lotus)),
463 (map { $_ => 'lsl' } qw(lindenscript)),
464 (map { $_ => 'ly' } qw(lilypond)),
465 (map { $_ => 'make' } qw(mak mk kmk)),
466 (map { $_ => 'mel' } qw(maya)),
467 (map { $_ => 'mib' } qw(smi snmp)),
468 (map { $_ => 'ml' } qw(mli ocaml)),
469 (map { $_ => 'mo' } qw(modelica)),
470 (map { $_ => 'mod2' } qw(def mod)),
471 (map { $_ => 'mod3' } qw(i3 m3)),
472 (map { $_ => 'mpl' } qw(maple)),
473 (map { $_ => 'n' } qw(nemerle)),
474 (map { $_ => 'nas' } qw(nasal)),
475 (map { $_ => 'nrx' } qw(netrexx)),
476 (map { $_ => 'nsi' } qw(nsis)),
477 (map { $_ => 'nut' } qw(squirrel)),
478 (map { $_ => 'oberon' } qw(ooc)),
479 (map { $_ => 'objc' } qw(M m mm)),
480 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
481 (map { $_ => 'pike' } qw(pmod)),
482 (map { $_ => 'pl' } qw(perl plex plx pm)),
483 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
484 (map { $_ => 'progress' } qw(i p w)),
485 (map { $_ => 'py' } qw(python)),
486 (map { $_ => 'pyx' } qw(pyrex)),
487 (map { $_ => 'rb' } qw(pp rjs ruby)),
488 (map { $_ => 'rexx' } qw(rex rx the)),
489 (map { $_ => 'sc' } qw(paradox)),
490 (map { $_ => 'scilab' } qw(sce sci)),
491 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
492 (map { $_ => 'sma' } qw(small)),
493 (map { $_ => 'smalltalk' } qw(gst sq st)),
494 (map { $_ => 'sno' } qw(snobal)),
495 (map { $_ => 'sybase' } qw(sp)),
496 (map { $_ => 'tcl' } qw(itcl wish)),
497 (map { $_ => 'tex' } qw(cls sty)),
498 (map { $_ => 'vb' } qw(bas basic bi vbs)),
499 (map { $_ => 'verilog' } qw(v)),
500 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
501 (map { $_ => 'y' } qw(bison)),
504 # You define site-wide feature defaults here; override them with
505 # $GITWEB_CONFIG as necessary.
506 our %feature = (
507 # feature => {
508 # 'sub' => feature-sub (subroutine),
509 # 'override' => allow-override (boolean),
510 # 'default' => [ default options...] (array reference)}
512 # if feature is overridable (it means that allow-override has true value),
513 # then feature-sub will be called with default options as parameters;
514 # return value of feature-sub indicates if to enable specified feature
516 # if there is no 'sub' key (no feature-sub), then feature cannot be
517 # overridden
519 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
520 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
521 # is enabled
523 # Enable the 'blame' blob view, showing the last commit that modified
524 # each line in the file. This can be very CPU-intensive.
526 # To enable system wide have in $GITWEB_CONFIG
527 # $feature{'blame'}{'default'} = [1];
528 # To have project specific config enable override in $GITWEB_CONFIG
529 # $feature{'blame'}{'override'} = 1;
530 # and in project config gitweb.blame = 0|1;
531 'blame' => {
532 'sub' => sub { feature_bool('blame', @_) },
533 'override' => 0,
534 'default' => [0]},
536 # Enable the 'incremental blame' blob view, which uses javascript to
537 # incrementally show the revisions of lines as they are discovered
538 # in the history. It is better for large histories, files and slow
539 # servers, but requires javascript in the client and can slow down the
540 # browser on large files.
542 # To enable system wide have in $GITWEB_CONFIG
543 # $feature{'blame_incremental'}{'default'} = [1];
544 # To have project specific config enable override in $GITWEB_CONFIG
545 # $feature{'blame_incremental'}{'override'} = 1;
546 # and in project config gitweb.blame_incremental = 0|1;
547 'blame_incremental' => {
548 'sub' => sub { feature_bool('blame_incremental', @_) },
549 'override' => 0,
550 'default' => [0]},
552 # Enable the 'snapshot' link, providing a compressed archive of any
553 # tree. This can potentially generate high traffic if you have large
554 # project.
556 # Value is a list of formats defined in %known_snapshot_formats that
557 # you wish to offer.
558 # To disable system wide have in $GITWEB_CONFIG
559 # $feature{'snapshot'}{'default'} = [];
560 # To have project specific config enable override in $GITWEB_CONFIG
561 # $feature{'snapshot'}{'override'} = 1;
562 # and in project config, a comma-separated list of formats or "none"
563 # to disable. Example: gitweb.snapshot = tbz2,zip;
564 'snapshot' => {
565 'sub' => \&feature_snapshot,
566 'override' => 0,
567 'default' => ['tgz']},
569 # Enable text search, which will list the commits which match author,
570 # committer or commit text to a given string. Enabled by default.
571 # Project specific override is not supported.
573 # Note that this controls all search features, which means that if
574 # it is disabled, then 'grep' and 'pickaxe' search would also be
575 # disabled.
576 'search' => {
577 'override' => 0,
578 'default' => [1]},
580 # Enable grep search, which will list the files in currently selected
581 # tree containing the given string. Enabled by default. This can be
582 # potentially CPU-intensive, of course.
583 # Note that you need to have 'search' feature enabled too.
585 # To enable system wide have in $GITWEB_CONFIG
586 # $feature{'grep'}{'default'} = [1];
587 # To have project specific config enable override in $GITWEB_CONFIG
588 # $feature{'grep'}{'override'} = 1;
589 # and in project config gitweb.grep = 0|1;
590 'grep' => {
591 'sub' => sub { feature_bool('grep', @_) },
592 'override' => 0,
593 'default' => [1]},
595 # Enable the pickaxe search, which will list the commits that modified
596 # a given string in a file. This can be practical and quite faster
597 # alternative to 'blame', but still potentially CPU-intensive.
598 # Note that you need to have 'search' feature enabled too.
600 # To enable system wide have in $GITWEB_CONFIG
601 # $feature{'pickaxe'}{'default'} = [1];
602 # To have project specific config enable override in $GITWEB_CONFIG
603 # $feature{'pickaxe'}{'override'} = 1;
604 # and in project config gitweb.pickaxe = 0|1;
605 'pickaxe' => {
606 'sub' => sub { feature_bool('pickaxe', @_) },
607 'override' => 0,
608 'default' => [1]},
610 # Enable showing size of blobs in a 'tree' view, in a separate
611 # column, similar to what 'ls -l' does. This cost a bit of IO.
613 # To disable system wide have in $GITWEB_CONFIG
614 # $feature{'show-sizes'}{'default'} = [0];
615 # To have project specific config enable override in $GITWEB_CONFIG
616 # $feature{'show-sizes'}{'override'} = 1;
617 # and in project config gitweb.showsizes = 0|1;
618 'show-sizes' => {
619 'sub' => sub { feature_bool('showsizes', @_) },
620 'override' => 0,
621 'default' => [1]},
623 # Make gitweb use an alternative format of the URLs which can be
624 # more readable and natural-looking: project name is embedded
625 # directly in the path and the query string contains other
626 # auxiliary information. All gitweb installations recognize
627 # URL in either format; this configures in which formats gitweb
628 # generates links.
630 # To enable system wide have in $GITWEB_CONFIG
631 # $feature{'pathinfo'}{'default'} = [1];
632 # Project specific override is not supported.
634 # Note that you will need to change the default location of CSS,
635 # favicon, logo and possibly other files to an absolute URL. Also,
636 # if gitweb.cgi serves as your indexfile, you will need to force
637 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
638 # will also likely want to set $home_link if you're setting $my_uri).
639 'pathinfo' => {
640 'override' => 0,
641 'default' => [0]},
643 # Make gitweb consider projects in project root subdirectories
644 # to be forks of existing projects. Given project $projname.git,
645 # projects matching $projname/*.git will not be shown in the main
646 # projects list, instead a '+' mark will be added to $projname
647 # there and a 'forks' view will be enabled for the project, listing
648 # all the forks. If project list is taken from a file, forks have
649 # to be listed after the main project.
651 # To enable system wide have in $GITWEB_CONFIG
652 # $feature{'forks'}{'default'} = [1];
653 # Project specific override is not supported.
654 'forks' => {
655 'override' => 0,
656 'default' => [0]},
658 # Insert custom links to the action bar of all project pages.
659 # This enables you mainly to link to third-party scripts integrating
660 # into gitweb; e.g. git-browser for graphical history representation
661 # or custom web-based repository administration interface.
663 # The 'default' value consists of a list of triplets in the form
664 # (label, link, position) where position is the label after which
665 # to insert the link and link is a format string where %n expands
666 # to the project name, %f to the project path within the filesystem,
667 # %h to the current hash (h gitweb parameter) and %b to the current
668 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
669 # project name where all '+' characters have been replaced with '%2B'.
671 # To enable system wide have in $GITWEB_CONFIG e.g.
672 # $feature{'actions'}{'default'} = [('graphiclog',
673 # '/git-browser/by-commit.html?r=%n', 'summary')];
674 # Project specific override is not supported.
675 'actions' => {
676 'override' => 0,
677 'default' => []},
679 # Allow gitweb scan project content tags of project repository,
680 # and display the popular Web 2.0-ish "tag cloud" near the projects
681 # list. Note that this is something COMPLETELY different from the
682 # normal Git tags.
684 # gitweb by itself can show existing tags, but it does not handle
685 # tagging itself; you need to do it externally, outside gitweb.
686 # The format is described in git_get_project_ctags() subroutine.
687 # You may want to install the HTML::TagCloud Perl module to get
688 # a pretty tag cloud instead of just a list of tags.
690 # To enable system wide have in $GITWEB_CONFIG
691 # $feature{'ctags'}{'default'} = [1];
692 # Project specific override is not supported.
694 # A value of 0 means no ctags display or editing. A value of
695 # 1 enables ctags display but never editing. A non-empty value
696 # that is not a string of digits enables ctags display AND the
697 # ability to add tags using a form that uses method POST and
698 # an action value set to the configured 'ctags' value.
699 'ctags' => {
700 'override' => 0,
701 'default' => [0]},
703 # The maximum number of patches in a patchset generated in patch
704 # view. Set this to 0 or undef to disable patch view, or to a
705 # negative number to remove any limit.
707 # To disable system wide have in $GITWEB_CONFIG
708 # $feature{'patches'}{'default'} = [0];
709 # To have project specific config enable override in $GITWEB_CONFIG
710 # $feature{'patches'}{'override'} = 1;
711 # and in project config gitweb.patches = 0|n;
712 # where n is the maximum number of patches allowed in a patchset.
713 'patches' => {
714 'sub' => \&feature_patches,
715 'override' => 0,
716 'default' => [16]},
718 # Avatar support. When this feature is enabled, views such as
719 # shortlog or commit will display an avatar associated with
720 # the email of the committer(s) and/or author(s).
722 # Currently available providers are gravatar and picon.
723 # If an unknown provider is specified, the feature is disabled.
725 # Gravatar depends on Digest::MD5.
726 # Picon currently relies on the indiana.edu database.
728 # To enable system wide have in $GITWEB_CONFIG
729 # $feature{'avatar'}{'default'} = ['<provider>'];
730 # where <provider> is either gravatar or picon.
731 # To have project specific config enable override in $GITWEB_CONFIG
732 # $feature{'avatar'}{'override'} = 1;
733 # and in project config gitweb.avatar = <provider>;
734 'avatar' => {
735 'sub' => \&feature_avatar,
736 'override' => 0,
737 'default' => ['']},
739 # Enable displaying how much time and how many git commands
740 # it took to generate and display page. Disabled by default.
741 # Project specific override is not supported.
742 'timed' => {
743 'override' => 0,
744 'default' => [0]},
746 # Enable turning some links into links to actions which require
747 # JavaScript to run (like 'blame_incremental'). Not enabled by
748 # default. Project specific override is currently not supported.
749 'javascript-actions' => {
750 'override' => 0,
751 'default' => [0]},
753 # Enable and configure ability to change common timezone for dates
754 # in gitweb output via JavaScript. Enabled by default.
755 # Project specific override is not supported.
756 'javascript-timezone' => {
757 'override' => 0,
758 'default' => [
759 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
760 # or undef to turn off this feature
761 'gitweb_tz', # name of cookie where to store selected timezone
762 'datetime', # CSS class used to mark up dates for manipulation
765 # Syntax highlighting support. This is based on Daniel Svensson's
766 # and Sham Chukoury's work in gitweb-xmms2.git.
767 # It requires the 'highlight' program present in $PATH,
768 # and therefore is disabled by default.
770 # To enable system wide have in $GITWEB_CONFIG
771 # $feature{'highlight'}{'default'} = [1];
773 'highlight' => {
774 'sub' => sub { feature_bool('highlight', @_) },
775 'override' => 0,
776 'default' => [0]},
778 # Enable displaying of remote heads in the heads list
780 # To enable system wide have in $GITWEB_CONFIG
781 # $feature{'remote_heads'}{'default'} = [1];
782 # To have project specific config enable override in $GITWEB_CONFIG
783 # $feature{'remote_heads'}{'override'} = 1;
784 # and in project config gitweb.remoteheads = 0|1;
785 'remote_heads' => {
786 'sub' => sub { feature_bool('remote_heads', @_) },
787 'override' => 0,
788 'default' => [0]},
790 # Enable showing branches under other refs in addition to heads
792 # To set system wide extra branch refs have in $GITWEB_CONFIG
793 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
794 # To have project specific config enable override in $GITWEB_CONFIG
795 # $feature{'extra-branch-refs'}{'override'} = 1;
796 # and in project config gitweb.extrabranchrefs = dirs of choice
797 # Every directory is separated with whitespace.
799 'extra-branch-refs' => {
800 'sub' => \&feature_extra_branch_refs,
801 'override' => 0,
802 'default' => []},
805 sub gitweb_get_feature {
806 my ($name) = @_;
807 return unless exists $feature{$name};
808 my ($sub, $override, @defaults) = (
809 $feature{$name}{'sub'},
810 $feature{$name}{'override'},
811 @{$feature{$name}{'default'}});
812 # project specific override is possible only if we have project
813 our $git_dir; # global variable, declared later
814 if (!$override || !defined $git_dir) {
815 return @defaults;
817 if (!defined $sub) {
818 warn "feature $name is not overridable";
819 return @defaults;
821 return $sub->(@defaults);
824 # A wrapper to check if a given feature is enabled.
825 # With this, you can say
827 # my $bool_feat = gitweb_check_feature('bool_feat');
828 # gitweb_check_feature('bool_feat') or somecode;
830 # instead of
832 # my ($bool_feat) = gitweb_get_feature('bool_feat');
833 # (gitweb_get_feature('bool_feat'))[0] or somecode;
835 sub gitweb_check_feature {
836 return (gitweb_get_feature(@_))[0];
840 sub feature_bool {
841 my $key = shift;
842 my ($val) = git_get_project_config($key, '--bool');
844 if (!defined $val) {
845 return ($_[0]);
846 } elsif ($val eq 'true') {
847 return (1);
848 } elsif ($val eq 'false') {
849 return (0);
853 sub feature_snapshot {
854 my (@fmts) = @_;
856 my ($val) = git_get_project_config('snapshot');
858 if ($val) {
859 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
862 return @fmts;
865 sub feature_patches {
866 my @val = (git_get_project_config('patches', '--int'));
868 if (@val) {
869 return @val;
872 return ($_[0]);
875 sub feature_avatar {
876 my @val = (git_get_project_config('avatar'));
878 return @val ? @val : @_;
881 sub feature_extra_branch_refs {
882 my (@branch_refs) = @_;
883 my $values = git_get_project_config('extrabranchrefs');
885 if ($values) {
886 $values = config_to_multi ($values);
887 @branch_refs = ();
888 foreach my $value (@{$values}) {
889 push @branch_refs, split /\s+/, $value;
893 return @branch_refs;
896 # checking HEAD file with -e is fragile if the repository was
897 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
898 # and then pruned.
899 sub check_head_link {
900 my ($dir) = @_;
901 return 0 unless -d "$dir/objects" && -x _;
902 return 0 unless -d "$dir/refs" && -x _;
903 my $headfile = "$dir/HEAD";
904 return -l $headfile ?
905 readlink($headfile) =~ /^refs\/heads\// : -f $headfile;
908 sub check_export_ok {
909 my ($dir) = @_;
910 return (check_head_link($dir) &&
911 (!$export_ok || -e "$dir/$export_ok") &&
912 (!$export_auth_hook || $export_auth_hook->($dir)));
915 # process alternate names for backward compatibility
916 # filter out unsupported (unknown) snapshot formats
917 sub filter_snapshot_fmts {
918 my @fmts = @_;
920 @fmts = map {
921 exists $known_snapshot_format_aliases{$_} ?
922 $known_snapshot_format_aliases{$_} : $_} @fmts;
923 @fmts = grep {
924 exists $known_snapshot_formats{$_} &&
925 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
928 sub filter_and_validate_refs {
929 my @refs = @_;
930 my %unique_refs = ();
932 foreach my $ref (@refs) {
933 die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
934 # 'heads' are added implicitly in get_branch_refs().
935 $unique_refs{$ref} = 1 if ($ref ne 'heads');
937 return sort keys %unique_refs;
940 # If it is set to code reference, it is code that it is to be run once per
941 # request, allowing updating configurations that change with each request,
942 # while running other code in config file only once.
944 # Otherwise, if it is false then gitweb would process config file only once;
945 # if it is true then gitweb config would be run for each request.
946 our $per_request_config = 1;
948 # If true and fileno STDIN is 0 and getsockname succeeds and getpeername fails
949 # with ENOTCONN, then FCGI mode will be activated automatically in just the
950 # same way as though the --fcgi option had been given instead.
951 our $auto_fcgi = 0;
953 # read and parse gitweb config file given by its parameter.
954 # returns true on success, false on recoverable error, allowing
955 # to chain this subroutine, using first file that exists.
956 # dies on errors during parsing config file, as it is unrecoverable.
957 sub read_config_file {
958 my $filename = shift;
959 return unless defined $filename;
960 # die if there are errors parsing config file
961 if (-e $filename) {
962 do $filename;
963 die $@ if $@;
964 return 1;
966 return;
969 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
970 sub evaluate_gitweb_config {
971 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
972 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
973 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
975 # Protect against duplications of file names, to not read config twice.
976 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
977 # there possibility of duplication of filename there doesn't matter.
978 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
979 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
981 # Common system-wide settings for convenience.
982 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
983 read_config_file($GITWEB_CONFIG_COMMON);
985 # Use first config file that exists. This means use the per-instance
986 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
987 read_config_file($GITWEB_CONFIG) and return;
988 read_config_file($GITWEB_CONFIG_SYSTEM);
991 our $encode_object;
993 sub evaluate_encoding {
994 my $requested = $fallback_encoding || 'ISO-8859-1';
995 my $obj = Encode::find_encoding($requested) or
996 die_error(400, "Requested fallback encoding not found");
997 if ($obj->name eq 'iso-8859-1') {
998 # Use Windows-1252 instead as required by the HTML 5 standard
999 my $altobj = Encode::find_encoding('Windows-1252');
1000 $obj = $altobj if $altobj;
1002 $encode_object = $obj;
1005 sub evaluate_email_obfuscate {
1006 # email obfuscation
1007 our $email;
1008 if (!$email && eval { require HTML::Email::Obfuscate; 1 }) {
1009 $email = HTML::Email::Obfuscate->new(lite => 1);
1013 # Get loadavg of system, to compare against $maxload.
1014 # Currently it requires '/proc/loadavg' present to get loadavg;
1015 # if it is not present it returns 0, which means no load checking.
1016 sub get_loadavg {
1017 if( -e '/proc/loadavg' ){
1018 open my $fd, '<', '/proc/loadavg'
1019 or return 0;
1020 my @load = split(/\s+/, scalar <$fd>);
1021 close $fd;
1023 # The first three columns measure CPU and IO utilization of the last one,
1024 # five, and 10 minute periods. The fourth column shows the number of
1025 # currently running processes and the total number of processes in the m/n
1026 # format. The last column displays the last process ID used.
1027 return $load[0] || 0;
1029 # additional checks for load average should go here for things that don't export
1030 # /proc/loadavg
1032 return 0;
1035 # version of the core git binary
1036 our $git_version;
1037 sub evaluate_git_version {
1038 our $git_version = $version;
1041 sub check_loadavg {
1042 if (defined $maxload && get_loadavg() > $maxload) {
1043 die_error(503, "The load average on the server is too high");
1047 # ======================================================================
1048 # input validation and dispatch
1050 # input parameters can be collected from a variety of sources (presently, CGI
1051 # and PATH_INFO), so we define an %input_params hash that collects them all
1052 # together during validation: this allows subsequent uses (e.g. href()) to be
1053 # agnostic of the parameter origin
1055 our %input_params = ();
1057 # input parameters are stored with the long parameter name as key. This will
1058 # also be used in the href subroutine to convert parameters to their CGI
1059 # equivalent, and since the href() usage is the most frequent one, we store
1060 # the name -> CGI key mapping here, instead of the reverse.
1062 # XXX: Warning: If you touch this, check the search form for updating,
1063 # too.
1065 our @cgi_param_mapping = (
1066 project => "p",
1067 action => "a",
1068 file_name => "f",
1069 file_parent => "fp",
1070 hash => "h",
1071 hash_parent => "hp",
1072 hash_base => "hb",
1073 hash_parent_base => "hpb",
1074 page => "pg",
1075 order => "o",
1076 searchtext => "s",
1077 searchtype => "st",
1078 snapshot_format => "sf",
1079 ctag_filter => 't',
1080 extra_options => "opt",
1081 search_use_regexp => "sr",
1082 ctag => "by_tag",
1083 diff_style => "ds",
1084 project_filter => "pf",
1085 # this must be last entry (for manipulation from JavaScript)
1086 javascript => "js"
1088 our %cgi_param_mapping = @cgi_param_mapping;
1090 # we will also need to know the possible actions, for validation
1091 our %actions = (
1092 "blame" => \&git_blame,
1093 "blame_incremental" => \&git_blame_incremental,
1094 "blame_data" => \&git_blame_data,
1095 "blobdiff" => \&git_blobdiff,
1096 "blobdiff_plain" => \&git_blobdiff_plain,
1097 "blob" => \&git_blob,
1098 "blob_plain" => \&git_blob_plain,
1099 "commitdiff" => \&git_commitdiff,
1100 "commitdiff_plain" => \&git_commitdiff_plain,
1101 "commit" => \&git_commit,
1102 "forks" => \&git_forks,
1103 "heads" => \&git_heads,
1104 "history" => \&git_history,
1105 "log" => \&git_log,
1106 "patch" => \&git_patch,
1107 "patches" => \&git_patches,
1108 "refs" => \&git_refs,
1109 "remotes" => \&git_remotes,
1110 "rss" => \&git_rss,
1111 "atom" => \&git_atom,
1112 "search" => \&git_search,
1113 "search_help" => \&git_search_help,
1114 "shortlog" => \&git_shortlog,
1115 "summary" => \&git_summary,
1116 "tag" => \&git_tag,
1117 "tags" => \&git_tags,
1118 "tree" => \&git_tree,
1119 "snapshot" => \&git_snapshot,
1120 "object" => \&git_object,
1121 # those below don't need $project
1122 "opml" => \&git_opml,
1123 "frontpage" => \&git_frontpage,
1124 "project_list" => \&git_project_list,
1125 "project_index" => \&git_project_index,
1128 # the only actions we will allow to be cached
1129 my %supported_cache_actions;
1130 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1132 # finally, we have the hash of allowed extra_options for the commands that
1133 # allow them
1134 our %allowed_options = (
1135 "--no-merges" => [ qw(rss atom log shortlog history) ],
1138 # fill %input_params with the CGI parameters. All values except for 'opt'
1139 # should be single values, but opt can be an array. We should probably
1140 # build an array of parameters that can be multi-valued, but since for the time
1141 # being it's only this one, we just single it out
1142 sub evaluate_query_params {
1143 our $cgi;
1145 while (my ($name, $symbol) = each %cgi_param_mapping) {
1146 if ($symbol eq 'opt') {
1147 $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
1148 } else {
1149 $input_params{$name} = decode_utf8($cgi->param($symbol));
1153 # Backwards compatibility - by_tag= <=> t=
1154 if ($input_params{'ctag'}) {
1155 $input_params{'ctag_filter'} = $input_params{'ctag'};
1159 # now read PATH_INFO and update the parameter list for missing parameters
1160 sub evaluate_path_info {
1161 return if defined $input_params{'project'};
1162 return if !$path_info;
1163 $path_info =~ s,^/+,,;
1164 return if !$path_info;
1166 # find which part of PATH_INFO is project
1167 my $project = $path_info;
1168 $project =~ s,/+$,,;
1169 while ($project && !check_head_link("$projectroot/$project")) {
1170 $project =~ s,/*[^/]*$,,;
1172 return unless $project;
1173 $input_params{'project'} = $project;
1175 # do not change any parameters if an action is given using the query string
1176 return if $input_params{'action'};
1177 $path_info =~ s,^\Q$project\E/*,,;
1179 # next, check if we have an action
1180 my $action = $path_info;
1181 $action =~ s,/.*$,,;
1182 if (exists $actions{$action}) {
1183 $path_info =~ s,^$action/*,,;
1184 $input_params{'action'} = $action;
1187 # list of actions that want hash_base instead of hash, but can have no
1188 # pathname (f) parameter
1189 my @wants_base = (
1190 'tree',
1191 'history',
1194 # we want to catch, among others
1195 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1196 my ($parentrefname, $parentpathname, $refname, $pathname) =
1197 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1199 # first, analyze the 'current' part
1200 if (defined $pathname) {
1201 # we got "branch:filename" or "branch:dir/"
1202 # we could use git_get_type(branch:pathname), but:
1203 # - it needs $git_dir
1204 # - it does a git() call
1205 # - the convention of terminating directories with a slash
1206 # makes it superfluous
1207 # - embedding the action in the PATH_INFO would make it even
1208 # more superfluous
1209 $pathname =~ s,^/+,,;
1210 if (!$pathname || substr($pathname, -1) eq "/") {
1211 $input_params{'action'} ||= "tree";
1212 $pathname =~ s,/$,,;
1213 } else {
1214 # the default action depends on whether we had parent info
1215 # or not
1216 if ($parentrefname) {
1217 $input_params{'action'} ||= "blobdiff_plain";
1218 } else {
1219 $input_params{'action'} ||= "blob_plain";
1222 $input_params{'hash_base'} ||= $refname;
1223 $input_params{'file_name'} ||= $pathname;
1224 } elsif (defined $refname) {
1225 # we got "branch". In this case we have to choose if we have to
1226 # set hash or hash_base.
1228 # Most of the actions without a pathname only want hash to be
1229 # set, except for the ones specified in @wants_base that want
1230 # hash_base instead. It should also be noted that hand-crafted
1231 # links having 'history' as an action and no pathname or hash
1232 # set will fail, but that happens regardless of PATH_INFO.
1233 if (defined $parentrefname) {
1234 # if there is parent let the default be 'shortlog' action
1235 # (for http://git.example.com/repo.git/A..B links); if there
1236 # is no parent, dispatch will detect type of object and set
1237 # action appropriately if required (if action is not set)
1238 $input_params{'action'} ||= "shortlog";
1240 if ($input_params{'action'} &&
1241 grep { $_ eq $input_params{'action'} } @wants_base) {
1242 $input_params{'hash_base'} ||= $refname;
1243 } else {
1244 $input_params{'hash'} ||= $refname;
1248 # next, handle the 'parent' part, if present
1249 if (defined $parentrefname) {
1250 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1251 # someproject/blobdiff/oldrev..newrev:/filename
1252 if ($parentpathname) {
1253 $parentpathname =~ s,^/+,,;
1254 $parentpathname =~ s,/$,,;
1255 $input_params{'file_parent'} ||= $parentpathname;
1256 } else {
1257 $input_params{'file_parent'} ||= $input_params{'file_name'};
1259 # we assume that hash_parent_base is wanted if a path was specified,
1260 # or if the action wants hash_base instead of hash
1261 if (defined $input_params{'file_parent'} ||
1262 grep { $_ eq $input_params{'action'} } @wants_base) {
1263 $input_params{'hash_parent_base'} ||= $parentrefname;
1264 } else {
1265 $input_params{'hash_parent'} ||= $parentrefname;
1269 # for the snapshot action, we allow URLs in the form
1270 # $project/snapshot/$hash.ext
1271 # where .ext determines the snapshot and gets removed from the
1272 # passed $refname to provide the $hash.
1274 # To be able to tell that $refname includes the format extension, we
1275 # require the following two conditions to be satisfied:
1276 # - the hash input parameter MUST have been set from the $refname part
1277 # of the URL (i.e. they must be equal)
1278 # - the snapshot format MUST NOT have been defined already (e.g. from
1279 # CGI parameter sf)
1280 # It's also useless to try any matching unless $refname has a dot,
1281 # so we check for that too
1282 if (defined $input_params{'action'} &&
1283 $input_params{'action'} eq 'snapshot' &&
1284 defined $refname && index($refname, '.') != -1 &&
1285 $refname eq $input_params{'hash'} &&
1286 !defined $input_params{'snapshot_format'}) {
1287 # We loop over the known snapshot formats, checking for
1288 # extensions. Allowed extensions are both the defined suffix
1289 # (which includes the initial dot already) and the snapshot
1290 # format key itself, with a prepended dot
1291 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1292 my $hash = $refname;
1293 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1294 next;
1296 my $sfx = $1;
1297 # a valid suffix was found, so set the snapshot format
1298 # and reset the hash parameter
1299 $input_params{'snapshot_format'} = $fmt;
1300 $input_params{'hash'} = $hash;
1301 # we also set the format suffix to the one requested
1302 # in the URL: this way a request for e.g. .tgz returns
1303 # a .tgz instead of a .tar.gz
1304 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1305 last;
1310 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1311 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1312 $searchtext, $search_regexp, $project_filter);
1313 sub evaluate_and_validate_params {
1314 our $action = $input_params{'action'};
1315 if (defined $action) {
1316 if (!is_valid_action($action)) {
1317 die_error(400, "Invalid action parameter");
1321 # parameters which are pathnames
1322 our $project = $input_params{'project'};
1323 if (defined $project) {
1324 if (!is_valid_project($project)) {
1325 undef $project;
1326 die_error(404, "No such project");
1330 our $project_filter = $input_params{'project_filter'};
1331 if (defined $project_filter) {
1332 if (!is_valid_pathname($project_filter)) {
1333 die_error(404, "Invalid project_filter parameter");
1337 our $file_name = $input_params{'file_name'};
1338 if (defined $file_name) {
1339 if (!is_valid_pathname($file_name)) {
1340 die_error(400, "Invalid file parameter");
1344 our $file_parent = $input_params{'file_parent'};
1345 if (defined $file_parent) {
1346 if (!is_valid_pathname($file_parent)) {
1347 die_error(400, "Invalid file parent parameter");
1351 # parameters which are refnames
1352 our $hash = $input_params{'hash'};
1353 if (defined $hash) {
1354 if (!is_valid_refname($hash)) {
1355 die_error(400, "Invalid hash parameter");
1359 our $hash_parent = $input_params{'hash_parent'};
1360 if (defined $hash_parent) {
1361 if (!is_valid_refname($hash_parent)) {
1362 die_error(400, "Invalid hash parent parameter");
1366 our $hash_base = $input_params{'hash_base'};
1367 if (defined $hash_base) {
1368 if (!is_valid_refname($hash_base)) {
1369 die_error(400, "Invalid hash base parameter");
1373 our @extra_options = @{$input_params{'extra_options'}};
1374 # @extra_options is always defined, since it can only be (currently) set from
1375 # CGI, and $cgi->param() returns the empty array in array context if the param
1376 # is not set
1377 foreach my $opt (@extra_options) {
1378 if (not exists $allowed_options{$opt}) {
1379 die_error(400, "Invalid option parameter");
1381 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1382 die_error(400, "Invalid option parameter for this action");
1386 our $hash_parent_base = $input_params{'hash_parent_base'};
1387 if (defined $hash_parent_base) {
1388 if (!is_valid_refname($hash_parent_base)) {
1389 die_error(400, "Invalid hash parent base parameter");
1393 # other parameters
1394 our $page = $input_params{'page'};
1395 if (defined $page) {
1396 if ($page =~ m/[^0-9]/) {
1397 die_error(400, "Invalid page parameter");
1401 our $searchtype = $input_params{'searchtype'};
1402 if (defined $searchtype) {
1403 if ($searchtype =~ m/[^a-z]/) {
1404 die_error(400, "Invalid searchtype parameter");
1408 our $search_use_regexp = $input_params{'search_use_regexp'};
1410 our $searchtext = $input_params{'searchtext'};
1411 our $search_regexp = undef;
1412 if (defined $searchtext) {
1413 if (length($searchtext) < 2) {
1414 die_error(403, "At least two characters are required for search parameter");
1416 if ($search_use_regexp) {
1417 $search_regexp = $searchtext;
1418 if (!eval { qr/$search_regexp/; 1; }) {
1419 (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1420 die_error(400, "Invalid search regexp '$search_regexp'",
1421 esc_html($error));
1423 } else {
1424 $search_regexp = quotemeta $searchtext;
1429 # path to the current git repository
1430 our $git_dir;
1431 sub evaluate_git_dir {
1432 our $git_dir = $project ? "$projectroot/$project" : undef;
1435 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1436 sub configure_gitweb_features {
1437 # list of supported snapshot formats
1438 our @snapshot_fmts = gitweb_get_feature('snapshot');
1439 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1441 # check that the avatar feature is set to a known provider name,
1442 # and for each provider check if the dependencies are satisfied.
1443 # if the provider name is invalid or the dependencies are not met,
1444 # reset $git_avatar to the empty string.
1445 our ($git_avatar) = gitweb_get_feature('avatar');
1446 if ($git_avatar eq 'gravatar') {
1447 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1448 } elsif ($git_avatar eq 'picon') {
1449 # no dependencies
1450 } else {
1451 $git_avatar = '';
1454 our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
1455 @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
1458 sub get_branch_refs {
1459 return ('heads', @extra_branch_refs);
1462 # custom error handler: 'die <message>' is Internal Server Error
1463 sub handle_errors_html {
1464 my $msg = shift; # it is already HTML escaped
1466 # to avoid infinite loop where error occurs in die_error,
1467 # change handler to default handler, disabling handle_errors_html
1468 set_message("Error occurred when inside die_error:\n$msg");
1470 # you cannot jump out of die_error when called as error handler;
1471 # the subroutine set via CGI::Carp::set_message is called _after_
1472 # HTTP headers are already written, so it cannot write them itself
1473 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1475 set_message(\&handle_errors_html);
1477 our $shown_stale_message = 0;
1478 our $cache_dump = undef;
1479 our $cache_dump_mtime = undef;
1481 # dispatch
1482 my $cache_mode_active;
1483 sub dispatch {
1484 $shown_stale_message = 0;
1485 if (!defined $action) {
1486 if (defined $hash) {
1487 $action = git_get_type($hash);
1488 $action or die_error(404, "Object does not exist");
1489 } elsif (defined $hash_base && defined $file_name) {
1490 $action = git_get_type("$hash_base:$file_name");
1491 $action or die_error(404, "File or directory does not exist");
1492 } elsif (defined $project) {
1493 $action = 'summary';
1494 } else {
1495 $action = 'frontpage';
1498 if (!defined($actions{$action})) {
1499 die_error(400, "Unknown action");
1501 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1502 !$project) {
1503 die_error(400, "Project needed");
1506 my $defstyle = $stylesheet;
1507 local $stylesheet = $defstyle;
1508 if ($ENV{'HTTP_COOKIE'} && ";$ENV{'HTTP_COOKIE'};" =~ /;\s*style\s*=\s*([a-z][a-z0-9_-]*)\s*;/) {{
1509 my $stylename = $1;
1510 last unless $ENV{'DOCUMENT_ROOT'} && -r "$ENV{'DOCUMENT_ROOT'}/style/$stylename.css";
1511 $stylesheet = "/style/$stylename.css";
1514 my $cached_page = $supported_cache_actions{$action}
1515 ? cached_action_page($action)
1516 : undef;
1517 goto DUMPCACHE if $cached_page;
1518 local *SAVEOUT = *STDOUT;
1519 $cache_mode_active = $supported_cache_actions{$action}
1520 ? cached_action_start($action)
1521 : undef;
1523 configure_gitweb_features();
1524 $actions{$action}->();
1526 return unless $cache_mode_active;
1528 $cached_page = cached_action_finish($action);
1529 *STDOUT = *SAVEOUT;
1531 DUMPCACHE:
1533 $cache_mode_active = 0;
1534 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1535 binmode STDOUT, ':raw';
1536 our $fcgi_raw_mode = 1;
1537 print expand_gitweb_pi($cached_page, time);
1538 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
1539 $fcgi_raw_mode = 0;
1542 sub reset_timer {
1543 our $t0 = [ gettimeofday() ]
1544 if defined $t0;
1545 our $number_of_git_cmds = 0;
1548 our $first_request = 1;
1549 our $evaluate_uri_force = undef;
1550 sub run_request {
1551 reset_timer();
1553 # do not reuse stale config or project list from prior FCGI request
1554 our $config_file = '';
1555 our $gitweb_project_owner = undef;
1557 # Only allow GET and HEAD methods
1558 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1559 print <<EOT;
1560 Status: 405 Method Not Allowed
1561 Content-Type: text/plain
1562 Allow: GET,HEAD
1564 405 Method Not Allowed
1566 return;
1569 evaluate_uri();
1570 &$evaluate_uri_force() if $evaluate_uri_force;
1571 if ($per_request_config) {
1572 if (ref($per_request_config) eq 'CODE') {
1573 $per_request_config->();
1574 } elsif (!$first_request) {
1575 evaluate_gitweb_config();
1576 evaluate_email_obfuscate();
1579 check_loadavg();
1581 # $projectroot and $projects_list might be set in gitweb config file
1582 $projects_list ||= $projectroot;
1584 evaluate_query_params();
1585 evaluate_path_info();
1586 evaluate_and_validate_params();
1587 evaluate_git_dir();
1589 dispatch();
1592 our $is_last_request = sub { 1 };
1593 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1594 our $CGI = 'CGI';
1595 our $cgi;
1596 our $fcgi_mode = 0;
1597 our $fcgi_nproc_active = 0;
1598 our $fcgi_raw_mode = 0;
1599 sub is_fcgi {
1600 use Errno;
1601 my $stdinfno = fileno STDIN;
1602 return 0 unless defined $stdinfno && $stdinfno == 0;
1603 return 0 unless getsockname STDIN;
1604 return 0 if getpeername STDIN;
1605 return $!{ENOTCONN}?1:0;
1607 sub configure_as_fcgi {
1608 return if $fcgi_mode;
1610 require FCGI;
1611 require CGI::Fast;
1613 # We have gone to great effort to make sure that all incoming data has
1614 # been converted from whatever format it was in into UTF-8. We have
1615 # even taken care to make sure the output handle is in ':utf8' mode.
1616 # Now along comes FCGI and blows it with:
1618 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1619 # and will stop wprking[sic] in a future version of FCGI
1621 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1622 # first encodes everything and then calls the original routine, but
1623 # not if $fcgi_raw_mode is true (then we just call the original routine).
1625 # Note that we could do this by using utf8::is_utf8 to check instead
1626 # of having a $fcgi_raw_mode global, but that would be slower to run
1627 # the test on each element and much slower than skipping the conversion
1628 # entirely when we know we're outputting raw bytes.
1629 my $orig = \&FCGI::Stream::PRINT;
1630 undef *FCGI::Stream::PRINT;
1631 *FCGI::Stream::PRINT = sub {
1632 @_ = (shift, map {my $x=$_; utf8::encode($x); $x} @_)
1633 unless $fcgi_raw_mode;
1634 goto $orig;
1637 our $CGI = 'CGI::Fast';
1639 $fcgi_mode = 1;
1640 $first_request = 0;
1641 my $request_number = 0;
1642 # let each child service 100 requests
1643 our $is_last_request = sub { ++$request_number > 100 };
1645 sub evaluate_argv {
1646 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1647 configure_as_fcgi()
1648 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi());
1650 my $nproc_sub = sub {
1651 my ($arg, $val) = @_;
1652 return unless eval { require FCGI::ProcManager; 1; };
1653 $fcgi_nproc_active = 1;
1654 my $proc_manager = FCGI::ProcManager->new({
1655 n_processes => $val,
1657 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1658 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1659 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1661 if (@ARGV) {
1662 require Getopt::Long;
1663 Getopt::Long::GetOptions(
1664 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1665 'nproc|n=i' => $nproc_sub,
1668 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1669 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1673 sub run {
1674 evaluate_gitweb_config();
1675 evaluate_encoding();
1676 evaluate_email_obfuscate();
1677 evaluate_git_version();
1678 my ($mu, $hl, $subroutine) = ($my_uri, $home_link, '');
1679 $subroutine .= '$my_uri = $mu;' if defined $my_uri && $my_uri ne '';
1680 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1681 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1682 $first_request = 1;
1683 evaluate_argv();
1685 $pre_listen_hook->()
1686 if $pre_listen_hook;
1688 REQUEST:
1689 while ($cgi = $CGI->new()) {
1690 $pre_dispatch_hook->()
1691 if $pre_dispatch_hook;
1693 eval {run_request()};
1695 $post_dispatch_hook->()
1696 if $post_dispatch_hook;
1697 $first_request = 0;
1699 last REQUEST if ($is_last_request->());
1705 run();
1707 if (defined caller) {
1708 # wrapped in a subroutine processing requests,
1709 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1710 return;
1711 } else {
1712 # pure CGI script, serving single request
1713 exit;
1716 ## ======================================================================
1717 ## action links
1719 # possible values of extra options
1720 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1721 # -replay => 1 - start from a current view (replay with modifications)
1722 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1723 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1724 sub href {
1725 my %params = @_;
1726 # default is to use -absolute url() i.e. $my_uri
1727 my $href = $params{-full} ? $my_url : $my_uri;
1729 # implicit -replay, must be first of implicit params
1730 $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1732 $params{'project'} = $project unless exists $params{'project'};
1734 if ($params{-replay}) {
1735 while (my ($name, $symbol) = each %cgi_param_mapping) {
1736 if (!exists $params{$name}) {
1737 $params{$name} = $input_params{$name};
1742 my $use_pathinfo = gitweb_check_feature('pathinfo');
1743 if (defined $params{'project'} &&
1744 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1745 # try to put as many parameters as possible in PATH_INFO:
1746 # - project name
1747 # - action
1748 # - hash_parent or hash_parent_base:/file_parent
1749 # - hash or hash_base:/filename
1750 # - the snapshot_format as an appropriate suffix
1752 # When the script is the root DirectoryIndex for the domain,
1753 # $href here would be something like http://gitweb.example.com/
1754 # Thus, we strip any trailing / from $href, to spare us double
1755 # slashes in the final URL
1756 $href =~ s,/$,,;
1758 # Then add the project name, if present
1759 $href .= "/".esc_path_info($params{'project'});
1760 delete $params{'project'};
1762 # since we destructively absorb parameters, we keep this
1763 # boolean that remembers if we're handling a snapshot
1764 my $is_snapshot = $params{'action'} eq 'snapshot';
1766 # Summary just uses the project path URL, any other action is
1767 # added to the URL
1768 if (defined $params{'action'}) {
1769 $href .= "/".esc_path_info($params{'action'})
1770 unless $params{'action'} eq 'summary';
1771 delete $params{'action'};
1774 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1775 # stripping nonexistent or useless pieces
1776 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1777 || $params{'hash_parent'} || $params{'hash'});
1778 if (defined $params{'hash_base'}) {
1779 if (defined $params{'hash_parent_base'}) {
1780 $href .= esc_path_info($params{'hash_parent_base'});
1781 # skip the file_parent if it's the same as the file_name
1782 if (defined $params{'file_parent'}) {
1783 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1784 delete $params{'file_parent'};
1785 } elsif ($params{'file_parent'} !~ /\.\./) {
1786 $href .= ":/".esc_path_info($params{'file_parent'});
1787 delete $params{'file_parent'};
1790 $href .= "..";
1791 delete $params{'hash_parent'};
1792 delete $params{'hash_parent_base'};
1793 } elsif (defined $params{'hash_parent'}) {
1794 $href .= esc_path_info($params{'hash_parent'}). "..";
1795 delete $params{'hash_parent'};
1798 $href .= esc_path_info($params{'hash_base'});
1799 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1800 $href .= ":/".esc_path_info($params{'file_name'});
1801 delete $params{'file_name'};
1803 delete $params{'hash'};
1804 delete $params{'hash_base'};
1805 } elsif (defined $params{'hash'}) {
1806 $href .= esc_path_info($params{'hash'});
1807 delete $params{'hash'};
1810 # If the action was a snapshot, we can absorb the
1811 # snapshot_format parameter too
1812 if ($is_snapshot) {
1813 my $fmt = $params{'snapshot_format'};
1814 # snapshot_format should always be defined when href()
1815 # is called, but just in case some code forgets, we
1816 # fall back to the default
1817 $fmt ||= $snapshot_fmts[0];
1818 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1819 delete $params{'snapshot_format'};
1823 # now encode the parameters explicitly
1824 my @result = ();
1825 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1826 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1827 if (defined $params{$name}) {
1828 if (ref($params{$name}) eq "ARRAY") {
1829 foreach my $par (@{$params{$name}}) {
1830 push @result, $symbol . "=" . esc_param($par);
1832 } else {
1833 push @result, $symbol . "=" . esc_param($params{$name});
1837 $href .= "?" . join(';', @result) if scalar @result;
1839 # final transformation: trailing spaces must be escaped (URI-encoded)
1840 $href =~ s/(\s+)$/CGI::escape($1)/e;
1842 if ($params{-anchor}) {
1843 $href .= "#".esc_param($params{-anchor});
1846 return $href;
1850 ## ======================================================================
1851 ## validation, quoting/unquoting and escaping
1853 sub is_valid_action {
1854 my $input = shift;
1855 return undef unless exists $actions{$input};
1856 return 1;
1859 sub is_valid_project {
1860 my $input = shift;
1862 return unless defined $input;
1863 if (!is_valid_pathname($input) ||
1864 !(-d "$projectroot/$input") ||
1865 !check_export_ok("$projectroot/$input") ||
1866 ($strict_export && !project_in_list($input))) {
1867 return undef;
1868 } else {
1869 return 1;
1873 sub is_valid_pathname {
1874 my $input = shift;
1876 return undef unless defined $input;
1877 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1878 # at the beginning, at the end, and between slashes.
1879 # also this catches doubled slashes
1880 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1881 return undef;
1883 # no null characters
1884 if ($input =~ m!\0!) {
1885 return undef;
1887 return 1;
1890 sub is_valid_ref_format {
1891 my $input = shift;
1893 return undef unless defined $input;
1894 # restrictions on ref name according to git-check-ref-format
1895 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1896 return undef;
1898 return 1;
1901 sub is_valid_refname {
1902 my $input = shift;
1904 return undef unless defined $input;
1905 # textual hashes are O.K.
1906 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1907 return 1;
1909 # it must be correct pathname
1910 is_valid_pathname($input) or return undef;
1911 # check git-check-ref-format restrictions
1912 is_valid_ref_format($input) or return undef;
1913 return 1;
1916 # decode sequences of octets in utf8 into Perl's internal form,
1917 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1918 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1919 sub to_utf8 {
1920 my $str = shift;
1921 return undef unless defined $str;
1923 if (utf8::is_utf8($str) || utf8::decode($str)) {
1924 return $str;
1925 } else {
1926 return $encode_object->decode($str, Encode::FB_DEFAULT);
1930 # quote unsafe chars, but keep the slash, even when it's not
1931 # correct, but quoted slashes look too horrible in bookmarks
1932 sub esc_param {
1933 my $str = shift;
1934 return undef unless defined $str;
1935 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1936 $str =~ s/ /\+/g;
1937 return $str;
1940 # the quoting rules for path_info fragment are slightly different
1941 sub esc_path_info {
1942 my $str = shift;
1943 return undef unless defined $str;
1945 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1946 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1948 return $str;
1951 # quote unsafe chars in whole URL, so some characters cannot be quoted
1952 sub esc_url {
1953 my $str = shift;
1954 return undef unless defined $str;
1955 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1956 $str =~ s/ /\+/g;
1957 return $str;
1960 # quote unsafe characters in HTML attributes
1961 sub esc_attr {
1963 # for XHTML conformance escaping '"' to '&quot;' is not enough
1964 return esc_html(@_);
1967 # replace invalid utf8 character with SUBSTITUTION sequence
1968 sub esc_html {
1969 my $str = shift;
1970 my %opts = @_;
1972 return undef unless defined $str;
1974 $str = to_utf8($str);
1975 $str = $cgi->escapeHTML($str);
1976 if ($opts{'-nbsp'}) {
1977 $str =~ s/ /&#160;/g;
1979 use bytes;
1980 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1981 return $str;
1984 # quote control characters and escape filename to HTML
1985 sub esc_path {
1986 my $str = shift;
1987 my %opts = @_;
1989 return undef unless defined $str;
1991 $str = to_utf8($str);
1992 $str = $cgi->escapeHTML($str);
1993 if ($opts{'-nbsp'}) {
1994 $str =~ s/ /&#160;/g;
1996 use bytes;
1997 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1998 return $str;
2001 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
2002 sub sanitize {
2003 my $str = shift;
2005 return undef unless defined $str;
2007 $str = to_utf8($str);
2008 use bytes;
2009 $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
2010 return $str;
2013 # Make control characters "printable", using character escape codes (CEC)
2014 sub quot_cec {
2015 my $cntrl = shift;
2016 my %opts = @_;
2017 my %es = ( # character escape codes, aka escape sequences
2018 "\t" => '\t', # tab (HT)
2019 "\n" => '\n', # line feed (LF)
2020 "\r" => '\r', # carrige return (CR)
2021 "\f" => '\f', # form feed (FF)
2022 "\b" => '\b', # backspace (BS)
2023 "\a" => '\a', # alarm (bell) (BEL)
2024 "\e" => '\e', # escape (ESC)
2025 "\013" => '\v', # vertical tab (VT)
2026 "\000" => '\0', # nul character (NUL)
2028 my $chr = ( (exists $es{$cntrl})
2029 ? $es{$cntrl}
2030 : sprintf('\x%02x', ord($cntrl)) );
2031 if ($opts{-nohtml}) {
2032 return $chr;
2033 } else {
2034 return "<span class=\"cntrl\">$chr</span>";
2038 # Alternatively use unicode control pictures codepoints,
2039 # Unicode "printable representation" (PR)
2040 sub quot_upr {
2041 my $cntrl = shift;
2042 my %opts = @_;
2044 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2045 if ($opts{-nohtml}) {
2046 return $chr;
2047 } else {
2048 return "<span class=\"cntrl\">$chr</span>";
2052 # git may return quoted and escaped filenames
2053 sub unquote {
2054 my $str = shift;
2056 sub unq {
2057 my $seq = shift;
2058 my %es = ( # character escape codes, aka escape sequences
2059 't' => "\t", # tab (HT, TAB)
2060 'n' => "\n", # newline (NL)
2061 'r' => "\r", # return (CR)
2062 'f' => "\f", # form feed (FF)
2063 'b' => "\b", # backspace (BS)
2064 'a' => "\a", # alarm (bell) (BEL)
2065 'e' => "\e", # escape (ESC)
2066 'v' => "\013", # vertical tab (VT)
2069 if ($seq =~ m/^[0-7]{1,3}$/) {
2070 # octal char sequence
2071 return chr(oct($seq));
2072 } elsif (exists $es{$seq}) {
2073 # C escape sequence, aka character escape code
2074 return $es{$seq};
2076 # quoted ordinary character
2077 return $seq;
2080 if ($str =~ m/^"(.*)"$/) {
2081 # needs unquoting
2082 $str = $1;
2083 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2085 return $str;
2088 # escape tabs (convert tabs to spaces)
2089 sub untabify {
2090 my $line = shift;
2092 while ((my $pos = index($line, "\t")) != -1) {
2093 if (my $count = (8 - ($pos % 8))) {
2094 my $spaces = ' ' x $count;
2095 $line =~ s/\t/$spaces/;
2099 return $line;
2102 sub project_in_list {
2103 my $project = shift;
2104 my @list = git_get_projects_list();
2105 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2108 sub cached_page_precondition_check {
2109 my $action = shift;
2110 return 1 unless
2111 $action eq 'summary' &&
2112 $projlist_cache_lifetime > 0 &&
2113 gitweb_check_feature('forks');
2115 # Note that ALL the 'forkchange' logic is in this function.
2116 # It does NOT belong in cached_action_page NOR in cached_action_start
2117 # NOR in cached_action_finish. None of those functions should know anything
2118 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2120 # besides the basic 'changed' "$action.changed" check, we may only use
2121 # a summary cache if:
2123 # 1) we are not using a project list cache file
2124 # -OR-
2125 # 2) we are not using the 'forks' feature
2126 # -OR-
2127 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2128 # -OR-
2129 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2130 # -OR-
2131 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2133 # Otherwise we must re-generate the cache because we've had a fork change
2134 # (either a fork was added or a fork was removed) AND the change has been
2135 # picked up in the cache file AND we've not got that in our cached copy
2137 # For (5) regenerating the cached page wouldn't get us anything if the project
2138 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2139 # forks information comes from the project cache file and it's clearly not
2140 # picked up the changes yet so we may continue to use a cached page until it does.
2142 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2143 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2144 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2145 return 1 unless defined($fc_mt) || defined($afc_mt);
2146 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2147 return 1 unless $prj_mt;
2148 my $old_mt = $fc_mt;
2149 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2150 return 1 if $old_mt > $prj_mt;
2152 # We're going to regenerate the cached page because we know the project cache
2153 # has new fork information that we cannot possibly have in our cached copy.
2155 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2156 # them is older than the project cache and one of them is newer, we still
2157 # need to regenerate the page cache, but we will also need to do it again
2158 # in the future because there's yet another fork update not yet in the cache.
2160 # So we make sure to touch "$action.changed" to force a cache regeneration
2161 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2162 # they're older than the project cache (they've served their purpose, we're
2163 # forcing a page regeneration by touching "$action.changed" but the project
2164 # cache was rebuilt since then so there are no more pending fork updates to
2165 # pick up in the future and they need to go).
2167 # For best results, the external code that touches 'forkchange' should always
2168 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2169 # if it does not already exist. That way the cached page will be regenerated
2170 # each time it's requested and ANY fork updates are available in the proj
2171 # cache rather than waiting until they all are before updating.
2173 # Note that we take a shortcut here and will zap 'forkchange' since we know
2174 # that it only affects the 'summary' cache. If, in the future, it affects
2175 # other cache types, it will first need to be propogated down to
2176 # "$action.forkchange" for those types before we zap it.
2178 my $fd;
2179 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2180 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2181 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2183 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2184 # one and not the other.
2186 if (defined $fc_mt && ! defined $afc_mt) {
2187 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2188 -e "$htmlcd/$action.forkchange" and
2189 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2190 unlink "$htmlcd/forkchange";
2193 return 0;
2196 sub cached_action_page {
2197 my $action = shift;
2199 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2200 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2201 return undef if -e "$htmlcd/changed" || -e "$htmlcd/$action.changed";
2202 return undef unless cached_page_precondition_check($action);
2203 open my $fd, '<', "$htmlcd/$action" or return undef;
2204 binmode $fd;
2205 local $/;
2206 my $cached_page = <$fd>;
2207 close $fd or return undef;
2208 return $cached_page;
2211 package Git::Gitweb::CacheFile;
2213 sub TIEHANDLE {
2214 use POSIX qw(:fcntl_h);
2215 my $class = shift;
2216 my $cachefile = shift;
2218 sysopen(my $self, $cachefile, O_WRONLY|O_CREAT|O_EXCL, 0664)
2219 or return undef;
2220 $$self->{'cachefile'} = $cachefile;
2221 $$self->{'opened'} = 1;
2222 $$self->{'contents'} = '';
2223 return bless $self, $class;
2226 sub CLOSE {
2227 my $self = shift;
2228 if ($$self->{'opened'}) {
2229 $$self->{'opened'} = 0;
2230 my $result = close $self;
2231 unlink $$self->{'cachefile'} unless $result;
2232 return $result;
2234 return 0;
2237 sub DESTROY {
2238 my $self = shift;
2239 if ($$self->{'opened'}) {
2240 $self->CLOSE() and unlink $$self->{'cachefile'};
2244 sub PRINT {
2245 my $self = shift;
2246 @_ = (map {my $x=$_; utf8::encode($x); $x} @_) unless $fcgi_raw_mode;
2247 print $self @_ if $$self->{'opened'};
2248 $$self->{'contents'} .= join('', @_);
2249 return 1;
2252 sub PRINTF {
2253 my $self = shift;
2254 my $template = shift;
2255 return $self->PRINT(sprintf $template, @_);
2258 sub contents {
2259 my $self = shift;
2260 return $$self->{'contents'};
2263 package main;
2265 # Caller is responsible for preserving STDOUT beforehand if needed
2266 sub cached_action_start {
2267 my $action = shift;
2269 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2270 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2271 return undef unless -d $htmlcd;
2272 if (-e "$htmlcd/changed") {
2273 foreach my $cacheable (keys(%html_cache_actions)) {
2274 next unless $supported_cache_actions{$cacheable} &&
2275 $html_cache_actions{$cacheable};
2276 my $fd;
2277 open $fd, '>', "$htmlcd/$cacheable.changed"
2278 and close $fd;
2280 unlink "$htmlcd/changed";
2282 local *CACHEFILE;
2283 tie *CACHEFILE, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2284 *STDOUT = *CACHEFILE;
2285 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2286 return 1;
2289 # Caller is responsible for restoring STDOUT afterward if needed
2290 sub cached_action_finish {
2291 my $action = shift;
2293 use File::Spec;
2295 my $obj = tied *STDOUT;
2296 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2297 my $cached_page = $obj->contents;
2298 (my $result = close(STDOUT)) or warn "couldn't close cache file on STDOUT: $!";
2299 # Do not leave STDOUT file descriptor invalid!
2300 local *NULL;
2301 open(NULL, '>', File::Spec->devnull) or die "couldn't open NULL to devnull: $!";
2302 *STDOUT = *NULL;
2303 return $cached_page unless $result;
2304 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2305 return $cached_page unless -d $htmlcd;
2306 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2307 return $cached_page;
2310 my %expand_pi_subs;
2311 BEGIN {%expand_pi_subs = (
2312 'age_string' => \&age_string,
2313 'age_string_date' => \&age_string_date,
2314 'age_string_age' => \&age_string_age,
2315 'compute_timed_interval' => \&compute_timed_interval,
2316 'compute_commands_count' => \&compute_commands_count,
2317 'format_lastrefresh_row' => \&format_lastrefresh_row,
2318 'compute_stylesheet_links' => \&compute_stylesheet_links,
2321 # Expands any <?gitweb...> processing instructions and returns the result
2322 sub expand_gitweb_pi {
2323 my $page = shift;
2324 $page .= '';
2325 my @time_now = gettimeofday();
2326 $page =~ s{<\?gitweb(?:\s+([^\s>]+)([^>]*))?\s*\?>}
2327 {defined($1) ?
2328 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2329 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2330 '') :
2331 '' }goes;
2332 return $page;
2335 ## ----------------------------------------------------------------------
2336 ## HTML aware string manipulation
2338 # Try to chop given string on a word boundary between position
2339 # $len and $len+$add_len. If there is no word boundary there,
2340 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2341 # (marking chopped part) would be longer than given string.
2342 sub chop_str {
2343 my $str = shift;
2344 my $len = shift;
2345 my $add_len = shift || 10;
2346 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2348 # Make sure perl knows it is utf8 encoded so we don't
2349 # cut in the middle of a utf8 multibyte char.
2350 $str = to_utf8($str);
2352 # allow only $len chars, but don't cut a word if it would fit in $add_len
2353 # if it doesn't fit, cut it if it's still longer than the dots we would add
2354 # remove chopped character entities entirely
2356 # when chopping in the middle, distribute $len into left and right part
2357 # return early if chopping wouldn't make string shorter
2358 if ($where eq 'center') {
2359 return $str if ($len + 5 >= length($str)); # filler is length 5
2360 $len = int($len/2);
2361 } else {
2362 return $str if ($len + 4 >= length($str)); # filler is length 4
2365 # regexps: ending and beginning with word part up to $add_len
2366 my $endre = qr/.{$len}\w{0,$add_len}/;
2367 my $begre = qr/\w{0,$add_len}.{$len}/;
2369 if ($where eq 'left') {
2370 $str =~ m/^(.*?)($begre)$/;
2371 my ($lead, $body) = ($1, $2);
2372 if (length($lead) > 4) {
2373 $lead = " ...";
2375 return "$lead$body";
2377 } elsif ($where eq 'center') {
2378 $str =~ m/^($endre)(.*)$/;
2379 my ($left, $str) = ($1, $2);
2380 $str =~ m/^(.*?)($begre)$/;
2381 my ($mid, $right) = ($1, $2);
2382 if (length($mid) > 5) {
2383 $mid = " ... ";
2385 return "$left$mid$right";
2387 } else {
2388 $str =~ m/^($endre)(.*)$/;
2389 my $body = $1;
2390 my $tail = $2;
2391 if (length($tail) > 4) {
2392 $tail = "... ";
2394 return "$body$tail";
2398 # pass-through email filter, obfuscating it when possible
2399 sub email_obfuscate {
2400 our $email;
2401 my ($str) = @_;
2402 if ($email) {
2403 $str = $email->escape_html($str);
2404 # Stock HTML::Email::Obfuscate version likes to produce
2405 # invalid XHTML...
2406 $str =~ s#<(/?)B>#<$1b>#g;
2407 return $str;
2408 } else {
2409 $str = esc_html($str);
2410 $str =~ s/@/&#x40;/;
2411 return $str;
2415 # takes the same arguments as chop_str, but also wraps a <span> around the
2416 # result with a title attribute if it does get chopped. Additionally, the
2417 # string is HTML-escaped.
2418 sub chop_and_escape_str {
2419 my ($str) = @_;
2421 my $chopped = chop_str(@_);
2422 $str = to_utf8($str);
2423 if ($chopped eq $str) {
2424 return email_obfuscate($chopped);
2425 } else {
2426 use bytes;
2427 $str =~ s/[[:cntrl:]]/?/g;
2428 return $cgi->span({-title=>$str}, email_obfuscate($chopped));
2432 # Highlight selected fragments of string, using given CSS class,
2433 # and escape HTML. It is assumed that fragments do not overlap.
2434 # Regions are passed as list of pairs (array references).
2436 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2437 # '<span class="mark">foo</span>bar'
2438 sub esc_html_hl_regions {
2439 my ($str, $css_class, @sel) = @_;
2440 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2441 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2442 return esc_html($str, %opts) unless @sel;
2444 my $out = '';
2445 my $pos = 0;
2447 for my $s (@sel) {
2448 my ($begin, $end) = @$s;
2450 # Don't create empty <span> elements.
2451 next if $end <= $begin;
2453 my $escaped = esc_html(substr($str, $begin, $end - $begin),
2454 %opts);
2456 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
2457 if ($begin - $pos > 0);
2458 $out .= $cgi->span({-class => $css_class}, $escaped);
2460 $pos = $end;
2462 $out .= esc_html(substr($str, $pos), %opts)
2463 if ($pos < length($str));
2465 return $out;
2468 # return positions of beginning and end of each match
2469 sub matchpos_list {
2470 my ($str, $regexp) = @_;
2471 return unless (defined $str && defined $regexp);
2473 my @matches;
2474 while ($str =~ /$regexp/g) {
2475 push @matches, [$-[0], $+[0]];
2477 return @matches;
2480 # highlight match (if any), and escape HTML
2481 sub esc_html_match_hl {
2482 my ($str, $regexp) = @_;
2483 return esc_html($str) unless defined $regexp;
2485 my @matches = matchpos_list($str, $regexp);
2486 return esc_html($str) unless @matches;
2488 return esc_html_hl_regions($str, 'match', @matches);
2492 # highlight match (if any) of shortened string, and escape HTML
2493 sub esc_html_match_hl_chopped {
2494 my ($str, $chopped, $regexp) = @_;
2495 return esc_html_match_hl($str, $regexp) unless defined $chopped;
2497 my @matches = matchpos_list($str, $regexp);
2498 return esc_html($chopped) unless @matches;
2500 # filter matches so that we mark chopped string
2501 my $tail = "... "; # see chop_str
2502 unless ($chopped =~ s/\Q$tail\E$//) {
2503 $tail = '';
2505 my $chop_len = length($chopped);
2506 my $tail_len = length($tail);
2507 my @filtered;
2509 for my $m (@matches) {
2510 if ($m->[0] > $chop_len) {
2511 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2512 last;
2513 } elsif ($m->[1] > $chop_len) {
2514 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2515 last;
2517 push @filtered, $m;
2520 return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
2523 ## ----------------------------------------------------------------------
2524 ## functions returning short strings
2526 # CSS class for given age epoch value (in seconds)
2527 # and reference time (optional, defaults to now) as second value
2528 sub age_class {
2529 my ($age_epoch, $time_now) = @_;
2530 return "noage" unless defined $age_epoch;
2531 defined $time_now or $time_now = time;
2532 my $age = $time_now - $age_epoch;
2534 if ($age < 60*60*2) {
2535 return "age0";
2536 } elsif ($age < 60*60*24*2) {
2537 return "age1";
2538 } else {
2539 return "age2";
2543 # convert age epoch in seconds to "nn units ago" string
2544 # reference time used is now unless second argument passed in
2545 # to get the old behavior, pass 0 as the first argument and
2546 # the time in seconds as the second
2547 sub age_string {
2548 my ($age_epoch, $time_now) = @_;
2549 return "unknown" unless defined $age_epoch;
2550 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2551 defined $time_now or $time_now = time;
2552 my $age = $time_now - $age_epoch;
2553 my $age_str;
2555 if ($age > 60*60*24*365*2) {
2556 $age_str = (int $age/60/60/24/365);
2557 $age_str .= " years ago";
2558 } elsif ($age > 60*60*24*(365/12)*2) {
2559 $age_str = int $age/60/60/24/(365/12);
2560 $age_str .= " months ago";
2561 } elsif ($age > 60*60*24*7*2) {
2562 $age_str = int $age/60/60/24/7;
2563 $age_str .= " weeks ago";
2564 } elsif ($age > 60*60*24*2) {
2565 $age_str = int $age/60/60/24;
2566 $age_str .= " days ago";
2567 } elsif ($age > 60*60*2) {
2568 $age_str = int $age/60/60;
2569 $age_str .= " hours ago";
2570 } elsif ($age > 60*2) {
2571 $age_str = int $age/60;
2572 $age_str .= " min ago";
2573 } elsif ($age > 2) {
2574 $age_str = int $age;
2575 $age_str .= " sec ago";
2576 } else {
2577 $age_str .= " right now";
2579 return $age_str;
2582 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2583 # this is typically shown to the user directly with the age_string_age as a title
2584 sub age_string_date {
2585 my ($age_epoch, $time_now) = @_;
2586 return "unknown" unless defined $age_epoch;
2587 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2588 defined $time_now or $time_now = time;
2589 my $age = $time_now - $age_epoch;
2591 if ($age > 60*60*24*7*2) {
2592 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2593 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2594 } else {
2595 return age_string($age_epoch, $time_now);
2599 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2600 # this is typically used for the 'title' attribute so it will show as a tooltip
2601 sub age_string_age {
2602 my ($age_epoch, $time_now) = @_;
2603 return "unknown" unless defined $age_epoch;
2604 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2605 defined $time_now or $time_now = time;
2606 my $age = $time_now - $age_epoch;
2608 if ($age > 60*60*24*7*2) {
2609 return age_string($age_epoch, $time_now);
2610 } else {
2611 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2612 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2616 use constant {
2617 S_IFINVALID => 0030000,
2618 S_IFGITLINK => 0160000,
2621 # submodule/subproject, a commit object reference
2622 sub S_ISGITLINK {
2623 my $mode = shift;
2625 return (($mode & S_IFMT) == S_IFGITLINK)
2628 # convert file mode in octal to symbolic file mode string
2629 sub mode_str {
2630 my $mode = oct shift;
2632 if (S_ISGITLINK($mode)) {
2633 return 'm---------';
2634 } elsif (S_ISDIR($mode & S_IFMT)) {
2635 return 'drwxr-xr-x';
2636 } elsif (S_ISLNK($mode)) {
2637 return 'lrwxrwxrwx';
2638 } elsif (S_ISREG($mode)) {
2639 # git cares only about the executable bit
2640 if ($mode & S_IXUSR) {
2641 return '-rwxr-xr-x';
2642 } else {
2643 return '-rw-r--r--';
2645 } else {
2646 return '----------';
2650 # convert file mode in octal to file type string
2651 sub file_type {
2652 my $mode = shift;
2654 if ($mode !~ m/^[0-7]+$/) {
2655 return $mode;
2656 } else {
2657 $mode = oct $mode;
2660 if (S_ISGITLINK($mode)) {
2661 return "submodule";
2662 } elsif (S_ISDIR($mode & S_IFMT)) {
2663 return "directory";
2664 } elsif (S_ISLNK($mode)) {
2665 return "symlink";
2666 } elsif (S_ISREG($mode)) {
2667 return "file";
2668 } else {
2669 return "unknown";
2673 # convert file mode in octal to file type description string
2674 sub file_type_long {
2675 my $mode = shift;
2677 if ($mode !~ m/^[0-7]+$/) {
2678 return $mode;
2679 } else {
2680 $mode = oct $mode;
2683 if (S_ISGITLINK($mode)) {
2684 return "submodule";
2685 } elsif (S_ISDIR($mode & S_IFMT)) {
2686 return "directory";
2687 } elsif (S_ISLNK($mode)) {
2688 return "symlink";
2689 } elsif (S_ISREG($mode)) {
2690 if ($mode & S_IXUSR) {
2691 return "executable";
2692 } else {
2693 return "file";
2695 } else {
2696 return "unknown";
2701 ## ----------------------------------------------------------------------
2702 ## functions returning short HTML fragments, or transforming HTML fragments
2703 ## which don't belong to other sections
2705 # format line of commit message.
2706 sub format_log_line_html {
2707 my $line = shift;
2709 $line = esc_html($line, -nbsp=>1);
2710 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
2711 $cgi->a({-href => href(action=>"object", hash=>$1),
2712 -class => "text"}, $1);
2713 }eg unless $line =~ /^\s*git-svn-id:/;
2715 return $line;
2718 # format marker of refs pointing to given object
2720 # the destination action is chosen based on object type and current context:
2721 # - for annotated tags, we choose the tag view unless it's the current view
2722 # already, in which case we go to shortlog view
2723 # - for other refs, we keep the current view if we're in history, shortlog or
2724 # log view, and select shortlog otherwise
2725 sub format_ref_marker {
2726 my ($refs, $id) = @_;
2727 my $markers = '';
2729 if (defined $refs->{$id}) {
2730 foreach my $ref (@{$refs->{$id}}) {
2731 # this code exploits the fact that non-lightweight tags are the
2732 # only indirect objects, and that they are the only objects for which
2733 # we want to use tag instead of shortlog as action
2734 my ($type, $name) = qw();
2735 my $indirect = ($ref =~ s/\^\{\}$//);
2736 # e.g. tags/v2.6.11 or heads/next
2737 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2738 $type = $1;
2739 $name = $2;
2740 } else {
2741 $type = "ref";
2742 $name = $ref;
2745 my $class = $type;
2746 $class .= " indirect" if $indirect;
2748 my $dest_action = "shortlog";
2750 if ($indirect) {
2751 $dest_action = "tag" unless $action eq "tag";
2752 } elsif ($action =~ /^(history|(short)?log)$/) {
2753 $dest_action = $action;
2756 my $dest = "";
2757 $dest .= "refs/" unless $ref =~ m!^refs/!;
2758 $dest .= $ref;
2760 my $link = $cgi->a({
2761 -href => href(
2762 action=>$dest_action,
2763 hash=>$dest
2764 )}, $name);
2766 $markers .= "<span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2767 $link . "</span>";
2771 if ($markers) {
2772 return '<span class="refs">'. $markers . '</span>';
2773 } else {
2774 return "";
2778 # format, perhaps shortened and with markers, title line
2779 sub format_subject_html {
2780 my ($long, $short, $href, $extra) = @_;
2781 $extra = '' unless defined($extra);
2783 if (length($short) < length($long)) {
2784 use bytes;
2785 $long =~ s/[[:cntrl:]]/?/g;
2786 return $cgi->a({-href => $href, -class => "list subject",
2787 -title => to_utf8($long)},
2788 esc_html($short)) . $extra;
2789 } else {
2790 return $cgi->a({-href => $href, -class => "list subject"},
2791 esc_html($long)) . $extra;
2795 # Rather than recomputing the url for an email multiple times, we cache it
2796 # after the first hit. This gives a visible benefit in views where the avatar
2797 # for the same email is used repeatedly (e.g. shortlog).
2798 # The cache is shared by all avatar engines (currently gravatar only), which
2799 # are free to use it as preferred. Since only one avatar engine is used for any
2800 # given page, there's no risk for cache conflicts.
2801 our %avatar_cache = ();
2803 # Compute the picon url for a given email, by using the picon search service over at
2804 # http://www.cs.indiana.edu/picons/search.html
2805 sub picon_url {
2806 my $email = lc shift;
2807 if (!$avatar_cache{$email}) {
2808 my ($user, $domain) = split('@', $email);
2809 $avatar_cache{$email} =
2810 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2811 "$domain/$user/" .
2812 "users+domains+unknown/up/single";
2814 return $avatar_cache{$email};
2817 # Compute the gravatar url for a given email, if it's not in the cache already.
2818 # Gravatar stores only the part of the URL before the size, since that's the
2819 # one computationally more expensive. This also allows reuse of the cache for
2820 # different sizes (for this particular engine).
2821 sub gravatar_url {
2822 my $email = lc shift;
2823 my $size = shift;
2824 $avatar_cache{$email} ||=
2825 "//www.gravatar.com/avatar/" .
2826 Digest::MD5::md5_hex($email) . "?s=";
2827 return $avatar_cache{$email} . $size;
2830 # Insert an avatar for the given $email at the given $size if the feature
2831 # is enabled.
2832 sub git_get_avatar {
2833 my ($email, %opts) = @_;
2834 my $pre_white = ($opts{-pad_before} ? "&#160;" : "");
2835 my $post_white = ($opts{-pad_after} ? "&#160;" : "");
2836 $opts{-size} ||= 'default';
2837 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2838 my $url = "";
2839 if ($git_avatar eq 'gravatar') {
2840 $url = gravatar_url($email, $size);
2841 } elsif ($git_avatar eq 'picon') {
2842 $url = picon_url($email);
2844 # Other providers can be added by extending the if chain, defining $url
2845 # as needed. If no variant puts something in $url, we assume avatars
2846 # are completely disabled/unavailable.
2847 if ($url) {
2848 return $pre_white .
2849 "<img width=\"$size\" " .
2850 "class=\"avatar\" " .
2851 "src=\"".esc_url($url)."\" " .
2852 "alt=\"\" " .
2853 "/>" . $post_white;
2854 } else {
2855 return "";
2859 sub format_search_author {
2860 my ($author, $searchtype, $displaytext) = @_;
2861 my $have_search = gitweb_check_feature('search');
2863 if ($have_search) {
2864 my $performed = "";
2865 if ($searchtype eq 'author') {
2866 $performed = "authored";
2867 } elsif ($searchtype eq 'committer') {
2868 $performed = "committed";
2871 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2872 searchtext=>$author,
2873 searchtype=>$searchtype), class=>"list",
2874 title=>"Search for commits $performed by $author"},
2875 $displaytext);
2877 } else {
2878 return $displaytext;
2882 # format the author name of the given commit with the given tag
2883 # the author name is chopped and escaped according to the other
2884 # optional parameters (see chop_str).
2885 sub format_author_html {
2886 my $tag = shift;
2887 my $co = shift;
2888 my $author = chop_and_escape_str($co->{'author_name'}, @_);
2889 return "<$tag class=\"author\">" .
2890 format_search_author($co->{'author_name'}, "author",
2891 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2892 $author) .
2893 "</$tag>";
2896 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2897 sub format_git_diff_header_line {
2898 my $line = shift;
2899 my $diffinfo = shift;
2900 my ($from, $to) = @_;
2902 if ($diffinfo->{'nparents'}) {
2903 # combined diff
2904 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2905 if ($to->{'href'}) {
2906 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2907 esc_path($to->{'file'}));
2908 } else { # file was deleted (no href)
2909 $line .= esc_path($to->{'file'});
2911 } else {
2912 # "ordinary" diff
2913 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2914 if ($from->{'href'}) {
2915 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2916 'a/' . esc_path($from->{'file'}));
2917 } else { # file was added (no href)
2918 $line .= 'a/' . esc_path($from->{'file'});
2920 $line .= ' ';
2921 if ($to->{'href'}) {
2922 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2923 'b/' . esc_path($to->{'file'}));
2924 } else { # file was deleted
2925 $line .= 'b/' . esc_path($to->{'file'});
2929 return "<div class=\"diff header\">$line</div>\n";
2932 # format extended diff header line, before patch itself
2933 sub format_extended_diff_header_line {
2934 my $line = shift;
2935 my $diffinfo = shift;
2936 my ($from, $to) = @_;
2938 # match <path>
2939 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2940 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2941 esc_path($from->{'file'}));
2943 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2944 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2945 esc_path($to->{'file'}));
2947 # match single <mode>
2948 if ($line =~ m/\s(\d{6})$/) {
2949 $line .= '<span class="info"> (' .
2950 file_type_long($1) .
2951 ')</span>';
2953 # match <hash>
2954 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2955 # can match only for combined diff
2956 $line = 'index ';
2957 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2958 if ($from->{'href'}[$i]) {
2959 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2960 -class=>"hash"},
2961 substr($diffinfo->{'from_id'}[$i],0,7));
2962 } else {
2963 $line .= '0' x 7;
2965 # separator
2966 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2968 $line .= '..';
2969 if ($to->{'href'}) {
2970 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2971 substr($diffinfo->{'to_id'},0,7));
2972 } else {
2973 $line .= '0' x 7;
2976 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2977 # can match only for ordinary diff
2978 my ($from_link, $to_link);
2979 if ($from->{'href'}) {
2980 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2981 substr($diffinfo->{'from_id'},0,7));
2982 } else {
2983 $from_link = '0' x 7;
2985 if ($to->{'href'}) {
2986 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2987 substr($diffinfo->{'to_id'},0,7));
2988 } else {
2989 $to_link = '0' x 7;
2991 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2992 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2995 return $line . "<br/>\n";
2998 # format from-file/to-file diff header
2999 sub format_diff_from_to_header {
3000 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3001 my $line;
3002 my $result = '';
3004 $line = $from_line;
3005 #assert($line =~ m/^---/) if DEBUG;
3006 # no extra formatting for "^--- /dev/null"
3007 if (! $diffinfo->{'nparents'}) {
3008 # ordinary (single parent) diff
3009 if ($line =~ m!^--- "?a/!) {
3010 if ($from->{'href'}) {
3011 $line = '--- a/' .
3012 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
3013 esc_path($from->{'file'}));
3014 } else {
3015 $line = '--- a/' .
3016 esc_path($from->{'file'});
3019 $result .= qq!<div class="diff from_file">$line</div>\n!;
3021 } else {
3022 # combined diff (merge commit)
3023 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3024 if ($from->{'href'}[$i]) {
3025 $line = '--- ' .
3026 $cgi->a({-href=>href(action=>"blobdiff",
3027 hash_parent=>$diffinfo->{'from_id'}[$i],
3028 hash_parent_base=>$parents[$i],
3029 file_parent=>$from->{'file'}[$i],
3030 hash=>$diffinfo->{'to_id'},
3031 hash_base=>$hash,
3032 file_name=>$to->{'file'}),
3033 -class=>"path",
3034 -title=>"diff" . ($i+1)},
3035 $i+1) .
3036 '/' .
3037 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
3038 esc_path($from->{'file'}[$i]));
3039 } else {
3040 $line = '--- /dev/null';
3042 $result .= qq!<div class="diff from_file">$line</div>\n!;
3046 $line = $to_line;
3047 #assert($line =~ m/^\+\+\+/) if DEBUG;
3048 # no extra formatting for "^+++ /dev/null"
3049 if ($line =~ m!^\+\+\+ "?b/!) {
3050 if ($to->{'href'}) {
3051 $line = '+++ b/' .
3052 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
3053 esc_path($to->{'file'}));
3054 } else {
3055 $line = '+++ b/' .
3056 esc_path($to->{'file'});
3059 $result .= qq!<div class="diff to_file">$line</div>\n!;
3061 return $result;
3064 # create note for patch simplified by combined diff
3065 sub format_diff_cc_simplified {
3066 my ($diffinfo, @parents) = @_;
3067 my $result = '';
3069 $result .= "<div class=\"diff header\">" .
3070 "diff --cc ";
3071 if (!is_deleted($diffinfo)) {
3072 $result .= $cgi->a({-href => href(action=>"blob",
3073 hash_base=>$hash,
3074 hash=>$diffinfo->{'to_id'},
3075 file_name=>$diffinfo->{'to_file'}),
3076 -class => "path"},
3077 esc_path($diffinfo->{'to_file'}));
3078 } else {
3079 $result .= esc_path($diffinfo->{'to_file'});
3081 $result .= "</div>\n" . # class="diff header"
3082 "<div class=\"diff nodifferences\">" .
3083 "Simple merge" .
3084 "</div>\n"; # class="diff nodifferences"
3086 return $result;
3089 sub diff_line_class {
3090 my ($line, $from, $to) = @_;
3092 # ordinary diff
3093 my $num_sign = 1;
3094 # combined diff
3095 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3096 $num_sign = scalar @{$from->{'href'}};
3099 my @diff_line_classifier = (
3100 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
3101 { regexp => qr/^\\/, class => "incomplete" },
3102 { regexp => qr/^ {$num_sign}/, class => "ctx" },
3103 # classifier for context must come before classifier add/rem,
3104 # or we would have to use more complicated regexp, for example
3105 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3106 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
3107 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
3109 for my $clsfy (@diff_line_classifier) {
3110 return $clsfy->{'class'}
3111 if ($line =~ $clsfy->{'regexp'});
3114 # fallback
3115 return "";
3118 # assumes that $from and $to are defined and correctly filled,
3119 # and that $line holds a line of chunk header for unified diff
3120 sub format_unidiff_chunk_header {
3121 my ($line, $from, $to) = @_;
3123 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3124 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3126 $from_lines = 0 unless defined $from_lines;
3127 $to_lines = 0 unless defined $to_lines;
3129 if ($from->{'href'}) {
3130 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
3131 -class=>"list"}, $from_text);
3133 if ($to->{'href'}) {
3134 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
3135 -class=>"list"}, $to_text);
3137 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3138 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
3139 return $line;
3142 # assumes that $from and $to are defined and correctly filled,
3143 # and that $line holds a line of chunk header for combined diff
3144 sub format_cc_diff_chunk_header {
3145 my ($line, $from, $to) = @_;
3147 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3148 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3150 @from_text = split(' ', $ranges);
3151 for (my $i = 0; $i < @from_text; ++$i) {
3152 ($from_start[$i], $from_nlines[$i]) =
3153 (split(',', substr($from_text[$i], 1)), 0);
3156 $to_text = pop @from_text;
3157 $to_start = pop @from_start;
3158 $to_nlines = pop @from_nlines;
3160 $line = "<span class=\"chunk_info\">$prefix ";
3161 for (my $i = 0; $i < @from_text; ++$i) {
3162 if ($from->{'href'}[$i]) {
3163 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
3164 -class=>"list"}, $from_text[$i]);
3165 } else {
3166 $line .= $from_text[$i];
3168 $line .= " ";
3170 if ($to->{'href'}) {
3171 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
3172 -class=>"list"}, $to_text);
3173 } else {
3174 $line .= $to_text;
3176 $line .= " $prefix</span>" .
3177 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
3178 return $line;
3181 # process patch (diff) line (not to be used for diff headers),
3182 # returning HTML-formatted (but not wrapped) line.
3183 # If the line is passed as a reference, it is treated as HTML and not
3184 # esc_html()'ed.
3185 sub format_diff_line {
3186 my ($line, $diff_class, $from, $to) = @_;
3188 if (ref($line)) {
3189 $line = $$line;
3190 } else {
3191 chomp $line;
3192 $line = untabify($line);
3194 if ($from && $to && $line =~ m/^\@{2} /) {
3195 $line = format_unidiff_chunk_header($line, $from, $to);
3196 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3197 $line = format_cc_diff_chunk_header($line, $from, $to);
3198 } else {
3199 $line = esc_html($line, -nbsp=>1);
3203 my $diff_classes = "diff diff_body";
3204 $diff_classes .= " $diff_class" if ($diff_class);
3205 $line = "<div class=\"$diff_classes\">$line</div>\n";
3207 return $line;
3210 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3211 # linked. Pass the hash of the tree/commit to snapshot.
3212 sub format_snapshot_links {
3213 my ($hash) = @_;
3214 my $num_fmts = @snapshot_fmts;
3215 if ($num_fmts > 1) {
3216 # A parenthesized list of links bearing format names.
3217 # e.g. "snapshot (_tar.gz_ _zip_)"
3218 return "<span class=\"snapshots\">snapshot (" . join(' ', map
3219 $cgi->a({
3220 -href => href(
3221 action=>"snapshot",
3222 hash=>$hash,
3223 snapshot_format=>$_
3225 }, $known_snapshot_formats{$_}{'display'})
3226 , @snapshot_fmts) . ")</span>";
3227 } elsif ($num_fmts == 1) {
3228 # A single "snapshot" link whose tooltip bears the format name.
3229 # i.e. "_snapshot_"
3230 my ($fmt) = @snapshot_fmts;
3231 return "<span class=\"snapshots\">" .
3232 $cgi->a({
3233 -href => href(
3234 action=>"snapshot",
3235 hash=>$hash,
3236 snapshot_format=>$fmt
3238 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
3239 }, "snapshot") . "</span>";
3240 } else { # $num_fmts == 0
3241 return undef;
3245 ## ......................................................................
3246 ## functions returning values to be passed, perhaps after some
3247 ## transformation, to other functions; e.g. returning arguments to href()
3249 # returns hash to be passed to href to generate gitweb URL
3250 # in -title key it returns description of link
3251 sub get_feed_info {
3252 my $format = shift || 'Atom';
3253 my %res = (action => lc($format));
3254 my $matched_ref = 0;
3256 # feed links are possible only for project views
3257 return unless (defined $project);
3258 # some views should link to OPML, or to generic project feed,
3259 # or don't have specific feed yet (so they should use generic)
3260 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3262 my $branch = undef;
3263 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3264 # (fullname) to differentiate from tag links; this also makes
3265 # possible to detect branch links
3266 for my $ref (get_branch_refs()) {
3267 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3268 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3269 $branch = $1;
3270 $matched_ref = $ref;
3271 last;
3274 # find log type for feed description (title)
3275 my $type = 'log';
3276 if (defined $file_name) {
3277 $type = "history of $file_name";
3278 $type .= "/" if ($action eq 'tree');
3279 $type .= " on '$branch'" if (defined $branch);
3280 } else {
3281 $type = "log of $branch" if (defined $branch);
3284 $res{-title} = $type;
3285 $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);
3286 $res{'file_name'} = $file_name;
3288 return %res;
3291 ## ----------------------------------------------------------------------
3292 ## git utility subroutines, invoking git commands
3294 # returns path to the core git executable and the --git-dir parameter as list
3295 sub git_cmd {
3296 $number_of_git_cmds++;
3297 return $GIT, '--git-dir='.$git_dir;
3300 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3301 sub cmd_pipe {
3303 # In order to be compatible with FCGI mode we must use POSIX
3304 # and access the STDERR_FILENO file descriptor directly
3306 use POSIX qw(STDERR_FILENO dup dup2);
3308 open(my $null, '>', File::Spec->devnull) or die "couldn't open devnull: $!";
3309 (my $saveerr = dup(STDERR_FILENO)) or die "couldn't dup STDERR: $!";
3310 my $dup2ok = dup2(fileno($null), STDERR_FILENO);
3311 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3312 $dup2ok or POSIX::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3313 my $result = open(my $fd, "-|", @_);
3314 $dup2ok = dup2($saveerr, STDERR_FILENO);
3315 POSIX::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3316 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3318 return $result ? $fd : undef;
3321 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3322 sub git_cmd_pipe {
3323 return cmd_pipe git_cmd(), @_;
3326 # quote the given arguments for passing them to the shell
3327 # quote_command("command", "arg 1", "arg with ' and ! characters")
3328 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3329 # Try to avoid using this function wherever possible.
3330 sub quote_command {
3331 return join(' ',
3332 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3335 # get HEAD ref of given project as hash
3336 sub git_get_head_hash {
3337 return git_get_full_hash(shift, 'HEAD');
3340 sub git_get_full_hash {
3341 return git_get_hash(@_);
3344 sub git_get_short_hash {
3345 return git_get_hash(@_, '--short=7');
3348 sub git_get_hash {
3349 my ($project, $hash, @options) = @_;
3350 my $o_git_dir = $git_dir;
3351 my $retval = undef;
3352 $git_dir = "$projectroot/$project";
3353 if (defined(my $fd = git_cmd_pipe 'rev-parse',
3354 '--verify', '-q', @options, $hash)) {
3355 $retval = <$fd>;
3356 chomp $retval if defined $retval;
3357 close $fd;
3359 if (defined $o_git_dir) {
3360 $git_dir = $o_git_dir;
3362 return $retval;
3365 # get type of given object
3366 sub git_get_type {
3367 my $hash = shift;
3369 defined(my $fd = git_cmd_pipe "cat-file", '-t', $hash) or return;
3370 my $type = <$fd>;
3371 close $fd or return;
3372 chomp $type;
3373 return $type;
3376 # repository configuration
3377 our $config_file = '';
3378 our %config;
3380 # store multiple values for single key as anonymous array reference
3381 # single values stored directly in the hash, not as [ <value> ]
3382 sub hash_set_multi {
3383 my ($hash, $key, $value) = @_;
3385 if (!exists $hash->{$key}) {
3386 $hash->{$key} = $value;
3387 } elsif (!ref $hash->{$key}) {
3388 $hash->{$key} = [ $hash->{$key}, $value ];
3389 } else {
3390 push @{$hash->{$key}}, $value;
3394 # return hash of git project configuration
3395 # optionally limited to some section, e.g. 'gitweb'
3396 sub git_parse_project_config {
3397 my $section_regexp = shift;
3398 my %config;
3400 local $/ = "\0";
3402 defined(my $fh = git_cmd_pipe "config", '-z', '-l')
3403 or return;
3405 while (my $keyval = to_utf8(scalar <$fh>)) {
3406 chomp $keyval;
3407 my ($key, $value) = split(/\n/, $keyval, 2);
3409 hash_set_multi(\%config, $key, $value)
3410 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3412 close $fh;
3414 return %config;
3417 # convert config value to boolean: 'true' or 'false'
3418 # no value, number > 0, 'true' and 'yes' values are true
3419 # rest of values are treated as false (never as error)
3420 sub config_to_bool {
3421 my $val = shift;
3423 return 1 if !defined $val; # section.key
3425 # strip leading and trailing whitespace
3426 $val =~ s/^\s+//;
3427 $val =~ s/\s+$//;
3429 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3430 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3433 # convert config value to simple decimal number
3434 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3435 # to be multiplied by 1024, 1048576, or 1073741824
3436 sub config_to_int {
3437 my $val = shift;
3439 # strip leading and trailing whitespace
3440 $val =~ s/^\s+//;
3441 $val =~ s/\s+$//;
3443 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3444 $unit = lc($unit);
3445 # unknown unit is treated as 1
3446 return $num * ($unit eq 'g' ? 1073741824 :
3447 $unit eq 'm' ? 1048576 :
3448 $unit eq 'k' ? 1024 : 1);
3450 return $val;
3453 # convert config value to array reference, if needed
3454 sub config_to_multi {
3455 my $val = shift;
3457 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
3460 sub git_get_project_config {
3461 my ($key, $type) = @_;
3463 return unless defined $git_dir;
3465 # key sanity check
3466 return unless ($key);
3467 # only subsection, if exists, is case sensitive,
3468 # and not lowercased by 'git config -z -l'
3469 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3470 $lo =~ s/_//g;
3471 $key = join(".", lc($hi), $mi, lc($lo));
3472 return if ($lo =~ /\W/ || $hi =~ /\W/);
3473 } else {
3474 $key = lc($key);
3475 $key =~ s/_//g;
3476 return if ($key =~ /\W/);
3478 $key =~ s/^gitweb\.//;
3480 # type sanity check
3481 if (defined $type) {
3482 $type =~ s/^--//;
3483 $type = undef
3484 unless ($type eq 'bool' || $type eq 'int');
3487 # get config
3488 if (!defined $config_file ||
3489 $config_file ne "$git_dir/config") {
3490 %config = git_parse_project_config('gitweb');
3491 $config_file = "$git_dir/config";
3494 # check if config variable (key) exists
3495 return unless exists $config{"gitweb.$key"};
3497 # ensure given type
3498 if (!defined $type) {
3499 return $config{"gitweb.$key"};
3500 } elsif ($type eq 'bool') {
3501 # backward compatibility: 'git config --bool' returns true/false
3502 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
3503 } elsif ($type eq 'int') {
3504 return config_to_int($config{"gitweb.$key"});
3506 return $config{"gitweb.$key"};
3509 # get hash of given path at given ref
3510 sub git_get_hash_by_path {
3511 my $base = shift;
3512 my $path = shift || return undef;
3513 my $type = shift;
3515 $path =~ s,/+$,,;
3517 defined(my $fd = git_cmd_pipe "ls-tree", $base, "--", $path)
3518 or die_error(500, "Open git-ls-tree failed");
3519 my $line = to_utf8(scalar <$fd>);
3520 close $fd or return undef;
3522 if (!defined $line) {
3523 # there is no tree or hash given by $path at $base
3524 return undef;
3527 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3528 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3529 if (defined $type && $type ne $2) {
3530 # type doesn't match
3531 return undef;
3533 return $3;
3536 # get path of entry with given hash at given tree-ish (ref)
3537 # used to get 'from' filename for combined diff (merge commit) for renames
3538 sub git_get_path_by_hash {
3539 my $base = shift || return;
3540 my $hash = shift || return;
3542 local $/ = "\0";
3544 defined(my $fd = git_cmd_pipe "ls-tree", '-r', '-t', '-z', $base)
3545 or return undef;
3546 while (my $line = to_utf8(scalar <$fd>)) {
3547 chomp $line;
3549 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3550 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3551 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3552 close $fd;
3553 return $1;
3556 close $fd;
3557 return undef;
3560 ## ......................................................................
3561 ## git utility functions, directly accessing git repository
3563 # get the value of config variable either from file named as the variable
3564 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3565 # configuration variable in the repository config file.
3566 sub git_get_file_or_project_config {
3567 my ($path, $name) = @_;
3569 $git_dir = "$projectroot/$path";
3570 open my $fd, '<', "$git_dir/$name"
3571 or return git_get_project_config($name);
3572 my $conf = to_utf8(scalar <$fd>);
3573 close $fd;
3574 if (defined $conf) {
3575 chomp $conf;
3577 return $conf;
3580 sub git_get_project_description {
3581 my $path = shift;
3582 return git_get_file_or_project_config($path, 'description');
3585 sub git_get_project_category {
3586 my $path = shift;
3587 return git_get_file_or_project_config($path, 'category');
3591 # supported formats:
3592 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3593 # - if its contents is a number, use it as tag weight,
3594 # - otherwise add a tag with weight 1
3595 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3596 # the same value multiple times increases tag weight
3597 # * `gitweb.ctag' multi-valued repo config variable
3598 sub git_get_project_ctags {
3599 my $project = shift;
3600 my $ctags = {};
3602 $git_dir = "$projectroot/$project";
3603 if (opendir my $dh, "$git_dir/ctags") {
3604 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
3605 foreach my $tagfile (@files) {
3606 open my $ct, '<', $tagfile
3607 or next;
3608 my $val = <$ct>;
3609 chomp $val if $val;
3610 close $ct;
3612 (my $ctag = $tagfile) =~ s#.*/##;
3613 $ctag = to_utf8($ctag);
3614 if ($val =~ /^\d+$/) {
3615 $ctags->{$ctag} = $val;
3616 } else {
3617 $ctags->{$ctag} = 1;
3620 closedir $dh;
3622 } elsif (open my $fh, '<', "$git_dir/ctags") {
3623 while (my $line = to_utf8(scalar <$fh>)) {
3624 chomp $line;
3625 $ctags->{$line}++ if $line;
3627 close $fh;
3629 } else {
3630 my $taglist = config_to_multi(git_get_project_config('ctag'));
3631 foreach my $tag (@$taglist) {
3632 $ctags->{$tag}++;
3636 return $ctags;
3639 # return hash, where keys are content tags ('ctags'),
3640 # and values are sum of weights of given tag in every project
3641 sub git_gather_all_ctags {
3642 my $projects = shift;
3643 my $ctags = {};
3645 foreach my $p (@$projects) {
3646 foreach my $ct (keys %{$p->{'ctags'}}) {
3647 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3651 return $ctags;
3654 sub git_populate_project_tagcloud {
3655 my ($ctags, $action) = @_;
3657 # First, merge different-cased tags; tags vote on casing
3658 my %ctags_lc;
3659 foreach (keys %$ctags) {
3660 $ctags_lc{lc $_}->{count} += $ctags->{$_};
3661 if (not $ctags_lc{lc $_}->{topcount}
3662 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
3663 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
3664 $ctags_lc{lc $_}->{topname} = $_;
3668 my $cloud;
3669 my $matched = $input_params{'ctag_filter'};
3670 if (eval { require HTML::TagCloud; 1; }) {
3671 $cloud = HTML::TagCloud->new;
3672 foreach my $ctag (sort keys %ctags_lc) {
3673 # Pad the title with spaces so that the cloud looks
3674 # less crammed.
3675 my $title = esc_html($ctags_lc{$ctag}->{topname});
3676 $title =~ s/ /&#160;/g;
3677 $title =~ s/^/&#160;/g;
3678 $title =~ s/$/&#160;/g;
3679 if (defined $matched && $matched eq $ctag) {
3680 $title = qq(<span class="match">$title</span>);
3682 $cloud->add($title, href(-replay=>1, action=>$action, ctag_filter=>$ctag),
3683 $ctags_lc{$ctag}->{count});
3685 } else {
3686 $cloud = {};
3687 foreach my $ctag (keys %ctags_lc) {
3688 my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
3689 if (defined $matched && $matched eq $ctag) {
3690 $title = qq(<span class="match">$title</span>);
3692 $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
3693 $cloud->{$ctag}{ctag} =
3694 $cgi->a({-href=>href(-replay=>1, action=>$action, ctag_filter=>$ctag)}, $title);
3697 return $cloud;
3700 sub git_show_project_tagcloud {
3701 my ($cloud, $count) = @_;
3702 if (ref $cloud eq 'HTML::TagCloud') {
3703 return $cloud->html_and_css($count);
3704 } else {
3705 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3706 return
3707 '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
3708 join (', ', map {
3709 $cloud->{$_}->{'ctag'}
3710 } splice(@tags, 0, $count)) .
3711 '</div>';
3715 sub git_get_project_url_list {
3716 my $path = shift;
3718 $git_dir = "$projectroot/$path";
3719 open my $fd, '<', "$git_dir/cloneurl"
3720 or return wantarray ?
3721 @{ config_to_multi(git_get_project_config('url')) } :
3722 config_to_multi(git_get_project_config('url'));
3723 my @git_project_url_list = map { chomp; to_utf8($_) } <$fd>;
3724 close $fd;
3726 return wantarray ? @git_project_url_list : \@git_project_url_list;
3729 sub git_get_projects_list {
3730 my $filter = shift || '';
3731 my $paranoid = shift;
3732 my @list;
3734 if (-d $projects_list) {
3735 # search in directory
3736 my $dir = $projects_list;
3737 # remove the trailing "/"
3738 $dir =~ s!/+$!!;
3739 my $pfxlen = length("$dir");
3740 my $pfxdepth = ($dir =~ tr!/!!);
3741 # when filtering, search only given subdirectory
3742 if ($filter && !$paranoid) {
3743 $dir .= "/$filter";
3744 $dir =~ s!/+$!!;
3747 File::Find::find({
3748 follow_fast => 1, # follow symbolic links
3749 follow_skip => 2, # ignore duplicates
3750 dangling_symlinks => 0, # ignore dangling symlinks, silently
3751 wanted => sub {
3752 # global variables
3753 our $project_maxdepth;
3754 our $projectroot;
3755 # skip project-list toplevel, if we get it.
3756 return if (m!^[/.]$!);
3757 # only directories can be git repositories
3758 return unless (-d $_);
3759 # don't traverse too deep (Find is super slow on os x)
3760 # $project_maxdepth excludes depth of $projectroot
3761 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3762 $File::Find::prune = 1;
3763 return;
3766 my $path = substr($File::Find::name, $pfxlen + 1);
3767 # paranoidly only filter here
3768 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3769 next;
3771 # we check related file in $projectroot
3772 if (check_export_ok("$projectroot/$path")) {
3773 push @list, { path => $path };
3774 $File::Find::prune = 1;
3777 }, "$dir");
3779 } elsif (-f $projects_list) {
3780 # read from file(url-encoded):
3781 # 'git%2Fgit.git Linus+Torvalds'
3782 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3783 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3784 open my $fd, '<', $projects_list or return;
3785 PROJECT:
3786 while (my $line = <$fd>) {
3787 chomp $line;
3788 my ($path, $owner) = split ' ', $line;
3789 $path = unescape($path);
3790 $owner = unescape($owner);
3791 if (!defined $path) {
3792 next;
3794 # if $filter is rpovided, check if $path begins with $filter
3795 if ($filter && $path !~ m!^\Q$filter\E/!) {
3796 next;
3798 if (check_export_ok("$projectroot/$path")) {
3799 my $pr = {
3800 path => $path
3802 if ($owner) {
3803 $pr->{'owner'} = to_utf8($owner);
3805 push @list, $pr;
3808 close $fd;
3810 return @list;
3813 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3814 # as side effects it sets 'forks' field to list of forks for forked projects
3815 sub filter_forks_from_projects_list {
3816 my $projects = shift;
3818 my %trie; # prefix tree of directories (path components)
3819 # generate trie out of those directories that might contain forks
3820 foreach my $pr (@$projects) {
3821 my $path = $pr->{'path'};
3822 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3823 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3824 next unless ($path); # skip '.git' repository: tests, git-instaweb
3825 next unless (-d "$projectroot/$path"); # containing directory exists
3826 $pr->{'forks'} = []; # there can be 0 or more forks of project
3828 # add to trie
3829 my @dirs = split('/', $path);
3830 # walk the trie, until either runs out of components or out of trie
3831 my $ref = \%trie;
3832 while (scalar @dirs &&
3833 exists($ref->{$dirs[0]})) {
3834 $ref = $ref->{shift @dirs};
3836 # create rest of trie structure from rest of components
3837 foreach my $dir (@dirs) {
3838 $ref = $ref->{$dir} = {};
3840 # create end marker, store $pr as a data
3841 $ref->{''} = $pr if (!exists $ref->{''});
3844 # filter out forks, by finding shortest prefix match for paths
3845 my @filtered;
3846 PROJECT:
3847 foreach my $pr (@$projects) {
3848 # trie lookup
3849 my $ref = \%trie;
3850 DIR:
3851 foreach my $dir (split('/', $pr->{'path'})) {
3852 if (exists $ref->{''}) {
3853 # found [shortest] prefix, is a fork - skip it
3854 push @{$ref->{''}{'forks'}}, $pr;
3855 next PROJECT;
3857 if (!exists $ref->{$dir}) {
3858 # not in trie, cannot have prefix, not a fork
3859 push @filtered, $pr;
3860 next PROJECT;
3862 # If the dir is there, we just walk one step down the trie.
3863 $ref = $ref->{$dir};
3865 # we ran out of trie
3866 # (shouldn't happen: it's either no match, or end marker)
3867 push @filtered, $pr;
3870 return @filtered;
3873 # note: fill_project_list_info must be run first,
3874 # for 'descr_long' and 'ctags' to be filled
3875 sub search_projects_list {
3876 my ($projlist, %opts) = @_;
3877 my $tagfilter = $opts{'tagfilter'};
3878 my $search_re = $opts{'search_regexp'};
3880 return @$projlist
3881 unless ($tagfilter || $search_re);
3883 # searching projects require filling to be run before it;
3884 fill_project_list_info($projlist,
3885 $tagfilter ? 'ctags' : (),
3886 $search_re ? ('path', 'descr') : ());
3887 my @projects;
3888 PROJECT:
3889 foreach my $pr (@$projlist) {
3891 if ($tagfilter) {
3892 next unless ref($pr->{'ctags'}) eq 'HASH';
3893 next unless
3894 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3897 if ($search_re) {
3898 my $path = $pr->{'path'};
3899 $path =~ s/\.git$//; # should not be included in search
3900 next unless
3901 $path =~ /$search_re/ ||
3902 $pr->{'descr_long'} =~ /$search_re/;
3905 push @projects, $pr;
3908 return @projects;
3911 our $gitweb_project_owner = undef;
3912 sub git_get_project_list_from_file {
3914 return if (defined $gitweb_project_owner);
3916 $gitweb_project_owner = {};
3917 # read from file (url-encoded):
3918 # 'git%2Fgit.git Linus+Torvalds'
3919 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3920 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3921 if (-f $projects_list) {
3922 open(my $fd, '<', $projects_list);
3923 while (my $line = <$fd>) {
3924 chomp $line;
3925 my ($pr, $ow) = split ' ', $line;
3926 $pr = unescape($pr);
3927 $ow = unescape($ow);
3928 $gitweb_project_owner->{$pr} = to_utf8($ow);
3930 close $fd;
3934 sub git_get_project_owner {
3935 my $proj = shift;
3936 my $owner;
3938 return undef unless $proj;
3939 $git_dir = "$projectroot/$proj";
3941 if (defined $project && $proj eq $project) {
3942 $owner = git_get_project_config('owner');
3944 if (!defined $owner && !defined $gitweb_project_owner) {
3945 git_get_project_list_from_file();
3947 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
3948 $owner = $gitweb_project_owner->{$proj};
3950 if (!defined $owner && (!defined $project || $proj ne $project)) {
3951 $owner = git_get_project_config('owner');
3953 if (!defined $owner) {
3954 $owner = get_file_owner("$git_dir");
3957 return $owner;
3960 sub parse_activity_date {
3961 my $dstr = shift;
3963 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
3964 # Unix timestamp
3965 return 0 + $1;
3967 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
3968 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
3969 my $seconds = timegm(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
3970 defined($z) && $z ne '' or $z = 'Z';
3971 $z =~ s/://;
3972 substr($z,1,0) = '0' if length($z) == 4;
3973 my $off = 0;
3974 if (uc($z) ne 'Z') {
3975 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
3976 $off = -$off if substr($z,0,1) eq '-';
3978 return $seconds - $off;
3980 return undef;
3983 # If $quick is true only look at $lastactivity_file
3984 sub git_get_last_activity {
3985 my ($path, $quick) = @_;
3986 my $fd;
3988 $git_dir = "$projectroot/$path";
3989 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
3990 my $activity = <$fd>;
3991 close $fd;
3992 return (undef) unless defined $activity;
3993 chomp $activity;
3994 return (undef) if $activity eq '';
3995 if (my $timestamp = parse_activity_date($activity)) {
3996 return ($timestamp);
3999 return (undef) if $quick;
4000 defined($fd = git_cmd_pipe 'for-each-ref',
4001 '--format=%(committer)',
4002 '--sort=-committerdate',
4003 '--count=1',
4004 map { "refs/$_" } get_branch_refs ()) or return;
4005 my $most_recent = <$fd>;
4006 close $fd or return (undef);
4007 if (defined $most_recent &&
4008 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4009 my $timestamp = $1;
4010 return ($timestamp);
4012 return (undef);
4015 # Implementation note: when a single remote is wanted, we cannot use 'git
4016 # remote show -n' because that command always work (assuming it's a remote URL
4017 # if it's not defined), and we cannot use 'git remote show' because that would
4018 # try to make a network roundtrip. So the only way to find if that particular
4019 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4020 # and when we find what we want.
4021 sub git_get_remotes_list {
4022 my $wanted = shift;
4023 my %remotes = ();
4025 my $fd = git_cmd_pipe 'remote', '-v';
4026 return unless $fd;
4027 while (my $remote = to_utf8(scalar <$fd>)) {
4028 chomp $remote;
4029 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4030 next if $wanted and not $remote eq $wanted;
4031 my ($url, $key) = ($1, $2);
4033 $remotes{$remote} ||= { 'heads' => [] };
4034 $remotes{$remote}{$key} = $url;
4036 close $fd or return;
4037 return wantarray ? %remotes : \%remotes;
4040 # Takes a hash of remotes as first parameter and fills it by adding the
4041 # available remote heads for each of the indicated remotes.
4042 sub fill_remote_heads {
4043 my $remotes = shift;
4044 my @heads = map { "remotes/$_" } keys %$remotes;
4045 my @remoteheads = git_get_heads_list(undef, @heads);
4046 foreach my $remote (keys %$remotes) {
4047 $remotes->{$remote}{'heads'} = [ grep {
4048 $_->{'name'} =~ s!^$remote/!!
4049 } @remoteheads ];
4053 sub git_get_references {
4054 my $type = shift || "";
4055 my %refs;
4056 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4057 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4058 defined(my $fd = git_cmd_pipe "show-ref", "--dereference",
4059 ($type ? ("--", "refs/$type") : ())) # use -- <pattern> if $type
4060 or return;
4062 while (my $line = to_utf8(scalar <$fd>)) {
4063 chomp $line;
4064 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4065 if (defined $refs{$1}) {
4066 push @{$refs{$1}}, $2;
4067 } else {
4068 $refs{$1} = [ $2 ];
4072 close $fd or return;
4073 return \%refs;
4076 sub git_get_rev_name_tags {
4077 my $hash = shift || return undef;
4079 defined(my $fd = git_cmd_pipe "name-rev", "--tags", $hash)
4080 or return;
4081 my $name_rev = to_utf8(scalar <$fd>);
4082 close $fd;
4084 if ($name_rev =~ m|^$hash tags/(.*)$|) {
4085 return $1;
4086 } else {
4087 # catches also '$hash undefined' output
4088 return undef;
4092 ## ----------------------------------------------------------------------
4093 ## parse to hash functions
4095 sub parse_date {
4096 my $epoch = shift;
4097 my $tz = shift || "-0000";
4099 my %date;
4100 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4101 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4102 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4103 $date{'hour'} = $hour;
4104 $date{'minute'} = $min;
4105 $date{'mday'} = $mday;
4106 $date{'day'} = $days[$wday];
4107 $date{'month'} = $months[$mon];
4108 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4109 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4110 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4111 $mday, $months[$mon], $hour ,$min;
4112 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4113 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4115 my ($tz_sign, $tz_hour, $tz_min) =
4116 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4117 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
4118 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4119 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4120 $date{'hour_local'} = $hour;
4121 $date{'minute_local'} = $min;
4122 $date{'mday_local'} = $mday;
4123 $date{'tz_local'} = $tz;
4124 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4125 1900+$year, $mon+1, $mday,
4126 $hour, $min, $sec, $tz);
4127 return %date;
4130 sub parse_file_date {
4131 my $file = shift;
4132 my $mtime = (stat("$projectroot/$project/$file"))[9];
4133 return () unless defined $mtime;
4134 my $tzoffset = timegm((localtime($mtime))[0..5]) - $mtime;
4135 my $tzstring = '+';
4136 if ($tzoffset <= 0) {
4137 $tzstring = '-';
4138 $tzoffset *= -1;
4140 $tzoffset = int($tzoffset/60);
4141 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4142 return parse_date($mtime, $tzstring);
4145 sub parse_tag {
4146 my $tag_id = shift;
4147 my %tag;
4148 my @comment;
4150 defined(my $fd = git_cmd_pipe "cat-file", "tag", $tag_id) or return;
4151 $tag{'id'} = $tag_id;
4152 while (my $line = to_utf8(scalar <$fd>)) {
4153 chomp $line;
4154 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4155 $tag{'object'} = $1;
4156 } elsif ($line =~ m/^type (.+)$/) {
4157 $tag{'type'} = $1;
4158 } elsif ($line =~ m/^tag (.+)$/) {
4159 $tag{'name'} = $1;
4160 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4161 $tag{'author'} = $1;
4162 $tag{'author_epoch'} = $2;
4163 $tag{'author_tz'} = $3;
4164 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4165 $tag{'author_name'} = $1;
4166 $tag{'author_email'} = $2;
4167 } else {
4168 $tag{'author_name'} = $tag{'author'};
4170 } elsif ($line =~ m/--BEGIN/) {
4171 push @comment, $line;
4172 last;
4173 } elsif ($line eq "") {
4174 last;
4177 push @comment, map(to_utf8($_), <$fd>);
4178 $tag{'comment'} = \@comment;
4179 close $fd or return;
4180 if (!defined $tag{'name'}) {
4181 return
4183 return %tag
4186 sub parse_commit_text {
4187 my ($commit_text, $withparents) = @_;
4188 my @commit_lines = split '\n', $commit_text;
4189 my %co;
4191 pop @commit_lines; # Remove '\0'
4193 if (! @commit_lines) {
4194 return;
4197 my $header = shift @commit_lines;
4198 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4199 return;
4201 ($co{'id'}, my @parents) = split ' ', $header;
4202 while (my $line = shift @commit_lines) {
4203 last if $line eq "\n";
4204 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4205 $co{'tree'} = $1;
4206 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4207 push @parents, $1;
4208 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4209 $co{'author'} = to_utf8($1);
4210 $co{'author_epoch'} = $2;
4211 $co{'author_tz'} = $3;
4212 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4213 $co{'author_name'} = $1;
4214 $co{'author_email'} = $2;
4215 } else {
4216 $co{'author_name'} = $co{'author'};
4218 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4219 $co{'committer'} = to_utf8($1);
4220 $co{'committer_epoch'} = $2;
4221 $co{'committer_tz'} = $3;
4222 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4223 $co{'committer_name'} = $1;
4224 $co{'committer_email'} = $2;
4225 } else {
4226 $co{'committer_name'} = $co{'committer'};
4230 if (!defined $co{'tree'}) {
4231 return;
4233 $co{'parents'} = \@parents;
4234 $co{'parent'} = $parents[0];
4236 @commit_lines = map to_utf8($_), @commit_lines;
4237 foreach my $title (@commit_lines) {
4238 $title =~ s/^ //;
4239 if ($title ne "") {
4240 $co{'title'} = chop_str($title, 80, 5);
4241 # remove leading stuff of merges to make the interesting part visible
4242 if (length($title) > 50) {
4243 $title =~ s/^Automatic //;
4244 $title =~ s/^merge (of|with) /Merge ... /i;
4245 if (length($title) > 50) {
4246 $title =~ s/(http|rsync):\/\///;
4248 if (length($title) > 50) {
4249 $title =~ s/(master|www|rsync)\.//;
4251 if (length($title) > 50) {
4252 $title =~ s/kernel.org:?//;
4254 if (length($title) > 50) {
4255 $title =~ s/\/pub\/scm//;
4258 $co{'title_short'} = chop_str($title, 50, 5);
4259 last;
4262 if (! defined $co{'title'} || $co{'title'} eq "") {
4263 $co{'title'} = $co{'title_short'} = '(no commit message)';
4265 # remove added spaces
4266 foreach my $line (@commit_lines) {
4267 $line =~ s/^ //;
4269 $co{'comment'} = \@commit_lines;
4271 my $age_epoch = $co{'committer_epoch'};
4272 $co{'age_epoch'} = $age_epoch;
4273 my $time_now = time;
4274 $co{'age_string'} = age_string($age_epoch, $time_now);
4275 $co{'age_string_date'} = age_string_date($age_epoch, $time_now);
4276 $co{'age_string_age'} = age_string_age($age_epoch, $time_now);
4277 return %co;
4280 sub parse_commit {
4281 my ($commit_id) = @_;
4282 my %co;
4284 local $/ = "\0";
4286 defined(my $fd = git_cmd_pipe "rev-list",
4287 "--parents",
4288 "--header",
4289 "--max-count=1",
4290 $commit_id,
4291 "--")
4292 or die_error(500, "Open git-rev-list failed");
4293 %co = parse_commit_text(<$fd>, 1);
4294 close $fd;
4296 return %co;
4299 sub parse_commits {
4300 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4301 my @cos;
4303 $maxcount ||= 1;
4304 $skip ||= 0;
4306 local $/ = "\0";
4308 defined(my $fd = git_cmd_pipe "rev-list",
4309 "--header",
4310 @args,
4311 ("--max-count=" . $maxcount),
4312 ("--skip=" . $skip),
4313 @extra_options,
4314 $commit_id,
4315 "--",
4316 ($filename ? ($filename) : ()))
4317 or die_error(500, "Open git-rev-list failed");
4318 while (my $line = <$fd>) {
4319 my %co = parse_commit_text($line);
4320 push @cos, \%co;
4322 close $fd;
4324 return wantarray ? @cos : \@cos;
4327 # parse line of git-diff-tree "raw" output
4328 sub parse_difftree_raw_line {
4329 my $line = shift;
4330 my %res;
4332 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4333 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4334 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4335 $res{'from_mode'} = $1;
4336 $res{'to_mode'} = $2;
4337 $res{'from_id'} = $3;
4338 $res{'to_id'} = $4;
4339 $res{'status'} = $5;
4340 $res{'similarity'} = $6;
4341 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4342 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
4343 } else {
4344 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
4347 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4348 # combined diff (for merge commit)
4349 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4350 $res{'nparents'} = length($1);
4351 $res{'from_mode'} = [ split(' ', $2) ];
4352 $res{'to_mode'} = pop @{$res{'from_mode'}};
4353 $res{'from_id'} = [ split(' ', $3) ];
4354 $res{'to_id'} = pop @{$res{'from_id'}};
4355 $res{'status'} = [ split('', $4) ];
4356 $res{'to_file'} = unquote($5);
4358 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4359 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4360 $res{'commit'} = $1;
4363 return wantarray ? %res : \%res;
4366 # wrapper: return parsed line of git-diff-tree "raw" output
4367 # (the argument might be raw line, or parsed info)
4368 sub parsed_difftree_line {
4369 my $line_or_ref = shift;
4371 if (ref($line_or_ref) eq "HASH") {
4372 # pre-parsed (or generated by hand)
4373 return $line_or_ref;
4374 } else {
4375 return parse_difftree_raw_line($line_or_ref);
4379 # parse line of git-ls-tree output
4380 sub parse_ls_tree_line {
4381 my $line = shift;
4382 my %opts = @_;
4383 my %res;
4385 if ($opts{'-l'}) {
4386 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4387 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4389 $res{'mode'} = $1;
4390 $res{'type'} = $2;
4391 $res{'hash'} = $3;
4392 $res{'size'} = $4;
4393 if ($opts{'-z'}) {
4394 $res{'name'} = $5;
4395 } else {
4396 $res{'name'} = unquote($5);
4398 } else {
4399 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4400 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4402 $res{'mode'} = $1;
4403 $res{'type'} = $2;
4404 $res{'hash'} = $3;
4405 if ($opts{'-z'}) {
4406 $res{'name'} = $4;
4407 } else {
4408 $res{'name'} = unquote($4);
4412 return wantarray ? %res : \%res;
4415 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4416 sub parse_from_to_diffinfo {
4417 my ($diffinfo, $from, $to, @parents) = @_;
4419 if ($diffinfo->{'nparents'}) {
4420 # combined diff
4421 $from->{'file'} = [];
4422 $from->{'href'} = [];
4423 fill_from_file_info($diffinfo, @parents)
4424 unless exists $diffinfo->{'from_file'};
4425 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4426 $from->{'file'}[$i] =
4427 defined $diffinfo->{'from_file'}[$i] ?
4428 $diffinfo->{'from_file'}[$i] :
4429 $diffinfo->{'to_file'};
4430 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4431 $from->{'href'}[$i] = href(action=>"blob",
4432 hash_base=>$parents[$i],
4433 hash=>$diffinfo->{'from_id'}[$i],
4434 file_name=>$from->{'file'}[$i]);
4435 } else {
4436 $from->{'href'}[$i] = undef;
4439 } else {
4440 # ordinary (not combined) diff
4441 $from->{'file'} = $diffinfo->{'from_file'};
4442 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4443 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
4444 hash=>$diffinfo->{'from_id'},
4445 file_name=>$from->{'file'});
4446 } else {
4447 delete $from->{'href'};
4451 $to->{'file'} = $diffinfo->{'to_file'};
4452 if (!is_deleted($diffinfo)) { # file exists in result
4453 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
4454 hash=>$diffinfo->{'to_id'},
4455 file_name=>$to->{'file'});
4456 } else {
4457 delete $to->{'href'};
4461 ## ......................................................................
4462 ## parse to array of hashes functions
4464 sub git_get_heads_list {
4465 my ($limit, @classes) = @_;
4466 @classes = get_branch_refs() unless @classes;
4467 my @patterns = map { "refs/$_" } @classes;
4468 my @headslist;
4470 defined(my $fd = git_cmd_pipe 'for-each-ref',
4471 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
4472 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4473 @patterns)
4474 or return;
4475 while (my $line = to_utf8(scalar <$fd>)) {
4476 my %ref_item;
4478 chomp $line;
4479 my ($refinfo, $committerinfo) = split(/\0/, $line);
4480 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4481 my ($committer, $epoch, $tz) =
4482 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4483 $ref_item{'fullname'} = $name;
4484 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
4485 $name =~ s!^refs/($strip_refs|remotes)/!!;
4486 $ref_item{'name'} = $name;
4487 # for refs neither in 'heads' nor 'remotes' we want to
4488 # show their ref dir
4489 my $ref_dir = (defined $1) ? $1 : '';
4490 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4491 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4494 $ref_item{'id'} = $hash;
4495 $ref_item{'title'} = $title || '(no commit message)';
4496 $ref_item{'epoch'} = $epoch;
4497 if ($epoch) {
4498 $ref_item{'age'} = age_string($ref_item{'epoch'});
4499 } else {
4500 $ref_item{'age'} = "unknown";
4503 push @headslist, \%ref_item;
4505 close $fd;
4507 return wantarray ? @headslist : \@headslist;
4510 sub git_get_tags_list {
4511 my $limit = shift;
4512 my @tagslist;
4513 my $all = shift || 0;
4514 my $order = shift || $default_refs_order;
4515 my $sortkey = $all && $order eq 'name' ? 'refname' : '-creatordate';
4517 defined(my $fd = git_cmd_pipe 'for-each-ref',
4518 ($limit ? '--count='.($limit+1) : ()), "--sort=$sortkey",
4519 '--format=%(objectname) %(objecttype) %(refname) '.
4520 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4521 ($all ? 'refs' : 'refs/tags'))
4522 or return;
4523 while (my $line = to_utf8(scalar <$fd>)) {
4524 my %ref_item;
4526 chomp $line;
4527 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4528 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4529 my ($creator, $epoch, $tz) =
4530 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4531 $ref_item{'fullname'} = $name;
4532 $name =~ s!^refs/!! if $all;
4533 $name =~ s!^refs/tags/!! unless $all;
4535 $ref_item{'type'} = $type;
4536 $ref_item{'id'} = $id;
4537 $ref_item{'name'} = $name;
4538 if ($type eq "tag") {
4539 $ref_item{'subject'} = $title;
4540 $ref_item{'reftype'} = $reftype;
4541 $ref_item{'refid'} = $refid;
4542 } else {
4543 $ref_item{'reftype'} = $type;
4544 $ref_item{'refid'} = $id;
4547 if ($type eq "tag" || $type eq "commit") {
4548 $ref_item{'epoch'} = $epoch;
4549 if ($epoch) {
4550 $ref_item{'age'} = age_string($ref_item{'epoch'});
4551 } else {
4552 $ref_item{'age'} = "unknown";
4556 push @tagslist, \%ref_item;
4558 close $fd;
4560 return wantarray ? @tagslist : \@tagslist;
4563 ## ----------------------------------------------------------------------
4564 ## filesystem-related functions
4566 sub get_file_owner {
4567 my $path = shift;
4569 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4570 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4571 if (!defined $gcos) {
4572 return undef;
4574 my $owner = $gcos;
4575 $owner =~ s/[,;].*$//;
4576 return to_utf8($owner);
4579 # assume that file exists
4580 sub insert_file {
4581 my $filename = shift;
4583 open my $fd, '<', $filename;
4584 while (<$fd>) {
4585 print to_utf8($_);
4587 close $fd;
4590 # return undef on failure
4591 sub collect_output {
4592 defined(my $fd = cmd_pipe @_) or return undef;
4593 if (eof $fd) {
4594 close $fd;
4595 return undef;
4597 my $result = join('', map({ to_utf8($_) } <$fd>));
4598 close $fd or return undef;
4599 return $result;
4602 # return undef on failure
4603 # return '' if only comments
4604 sub collect_html_file {
4605 my $filename = shift;
4607 open my $fd, '<', $filename or return undef;
4608 my $result = join('', map({ to_utf8($_) } <$fd>));
4609 close $fd or return undef;
4610 return undef unless defined($result);
4611 my $test = $result;
4612 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4613 $test =~ s/\s+//s;
4614 return $test eq '' ? '' : $result;
4617 ## ......................................................................
4618 ## mimetype related functions
4620 sub mimetype_guess_file {
4621 my $filename = shift;
4622 my $mimemap = shift;
4623 my $rawmode = shift;
4624 -r $mimemap or return undef;
4626 my %mimemap;
4627 open(my $mh, '<', $mimemap) or return undef;
4628 while (<$mh>) {
4629 next if m/^#/; # skip comments
4630 my ($mimetype, @exts) = split(/\s+/);
4631 foreach my $ext (@exts) {
4632 $mimemap{$ext} = $mimetype;
4635 close($mh);
4637 my ($ext, $ans);
4638 $ext = $1 if $filename =~ /\.([^.]*)$/;
4639 $ans = $mimemap{$ext} if $ext;
4640 if (defined $ans) {
4641 my $l = lc($ans);
4642 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4643 if (!$rawmode) {
4644 $ans = 'text/xml' if $l =~ m!^application/[^\s:;,=]+\+xml$! ||
4645 $l eq 'image/svg+xml' ||
4646 $l eq 'application/xml-dtd' ||
4647 $l eq 'application/xml-external-parsed-entity';
4650 return $ans;
4653 sub mimetype_guess {
4654 my $filename = shift;
4655 my $rawmode = shift;
4656 my $mime;
4657 $filename =~ /\./ or return undef;
4659 if ($mimetypes_file) {
4660 my $file = $mimetypes_file;
4661 if ($file !~ m!^/!) { # if it is relative path
4662 # it is relative to project
4663 $file = "$projectroot/$project/$file";
4665 $mime = mimetype_guess_file($filename, $file, $rawmode);
4667 $mime ||= mimetype_guess_file($filename, '/etc/mime.types', $rawmode);
4668 return $mime;
4671 sub blob_mimetype {
4672 my $fd = shift;
4673 my $filename = shift;
4674 my $rawmode = shift;
4675 my $mime;
4677 # The -T/-B file operators produce the wrong result unless a perlio
4678 # layer is present when the file handle is a pipe that delivers less
4679 # than 512 bytes of data before reaching EOF.
4681 # If we are running in a Perl that uses the stdio layer rather than the
4682 # unix+perlio layers we will end up adding a perlio layer on top of the
4683 # stdio layer and get a second level of buffering. This is harmless
4684 # and it makes the -T/-B file operators work properly in all cases.
4686 binmode $fd, ":perlio" or die_error(500, "Adding perlio layer failed")
4687 unless grep /^perlio$/, PerlIO::get_layers($fd);
4689 $mime = mimetype_guess($filename, $rawmode) if defined $filename;
4691 if (!$mime && $filename) {
4692 if ($filename =~ m/\.html?$/i) {
4693 $mime = 'text/html';
4694 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4695 $mime = 'text/html';
4696 } elsif ($filename =~ m/\.te?xt?$/i) {
4697 $mime = 'text/plain';
4698 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4699 $mime = 'text/plain';
4700 } elsif ($filename =~ m/\.png$/i) {
4701 $mime = 'image/png';
4702 } elsif ($filename =~ m/\.gif$/i) {
4703 $mime = 'image/gif';
4704 } elsif ($filename =~ m/\.jpe?g$/i) {
4705 $mime = 'image/jpeg';
4706 } elsif ($filename =~ m/\.svgz?$/i) {
4707 $mime = 'image/svg+xml';
4711 # just in case
4712 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4714 $mime = -T $fd ? 'text/plain' : 'application/octet-stream' unless $mime;
4716 return $mime;
4719 sub is_ascii {
4720 use bytes;
4721 my $data = shift;
4722 return scalar($data =~ /^[\x00-\x7f]*$/);
4725 sub is_valid_utf8 {
4726 my $data = shift;
4727 return utf8::decode($data);
4730 sub extract_html_charset {
4731 return undef unless $_[0] && "$_[0]</head>" =~ m#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4732 my $head = $1;
4733 return $2 if $head =~ m#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4734 while ($head =~ m#<meta\s+(http-equiv|content)\s*=\s*(['"])\s*([^\2]+?)\s*\2\s*(http-equiv|content)\s*=\s*(['"])\s*([^\5]+?)\s*\5\s*/?>#sig) {
4735 my %kv = (lc($1) => $3, lc($4) => $6);
4736 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4737 return $1 if $he && $c && $he eq 'content-type' &&
4738 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4740 return undef;
4743 sub blob_contenttype {
4744 my ($fd, $file_name, $type) = @_;
4746 $type ||= blob_mimetype($fd, $file_name, 1);
4747 return $type unless $type =~ m!^text/.+!i;
4748 my ($leader, $charset, $htmlcharset);
4749 if ($fd && read($fd, $leader, 32768)) {{
4750 $charset='US-ASCII' if is_ascii($leader);
4751 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8($leader);
4752 $charset='ISO-8859-1' unless $charset;
4753 $htmlcharset = extract_html_charset($leader) if $type eq 'text/html';
4754 if ($htmlcharset && $charset ne 'US-ASCII') {
4755 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4758 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4759 my $defcharset = $default_text_plain_charset || '';
4760 $defcharset =~ s/^\s+//;
4761 $defcharset =~ s/\s+$//;
4762 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4763 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4766 # peek the first upto 128 bytes off a file handle
4767 sub peek128bytes {
4768 my $fd = shift;
4770 use IO::Handle;
4771 use bytes;
4773 my $prefix128;
4774 return '' unless $fd && read($fd, $prefix128, 128);
4776 # In the general case, we're guaranteed only to be able to ungetc one
4777 # character (provided, of course, we actually got a character first).
4779 # However, we know:
4781 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4782 # already been called at least once on the file handle before us
4784 # 2) we have an $fd positioned at the start of the input stream and
4785 # therefore know we were positioned at a buffer boundary before
4786 # reading the initial upto 128 bytes
4788 # 3) the buffer size is at least 512 bytes
4790 # 4) we are careful to only unget raw bytes
4792 # 5) we are attempting to unget exactly the same number of bytes we got
4794 # Given the above conditions we will ALWAYS be able to safely unget
4795 # the $prefix128 value we just got.
4797 # In fact, we could read up to 511 bytes and still be sure.
4798 # (Reading 512 might pop us into the next internal buffer, but probably
4799 # not since that could break the always able to unget at least the one
4800 # you just got guarantee.)
4802 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4804 return $prefix128;
4807 # guess file syntax for syntax highlighting; return undef if no highlighting
4808 # the name of syntax can (in the future) depend on syntax highlighter used
4809 sub guess_file_syntax {
4810 my ($fd, $mimetype, $file_name) = @_;
4811 return undef unless $fd && defined $file_name &&
4812 defined $mimetype && $mimetype =~ m!^text/.+!i;
4813 my $basename = basename($file_name, '.in');
4814 return $highlight_basename{$basename}
4815 if exists $highlight_basename{$basename};
4817 # Peek to see if there's a shebang or xml line.
4818 # We always operate on bytes when testing this.
4820 use bytes;
4821 my $shebang = peek128bytes($fd);
4822 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4823 foreach my $key (keys %highlight_shebang) {
4824 my $ar = ref($highlight_shebang{$key}) ?
4825 $highlight_shebang{$key} :
4826 [$highlight_shebang{key}];
4827 map {return $key if $shebang =~ /$_/} @$ar;
4830 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4833 $basename =~ /\.([^.]*)$/;
4834 my $ext = $1 or return undef;
4835 return $highlight_ext{$ext}
4836 if exists $highlight_ext{$ext};
4838 return undef;
4841 # run highlighter and return FD of its output,
4842 # or return original FD if no highlighting
4843 sub run_highlighter {
4844 my ($fd, $syntax) = @_;
4845 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4847 defined(my $hifd = cmd_pipe $posix_shell_bin, '-c',
4848 quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
4849 quote_command($highlight_bin).
4850 " --replace-tabs=8 --fragment --syntax $syntax")
4851 or die_error(500, "Couldn't open file or run syntax highlighter");
4852 if (eof $hifd) {
4853 # just in case, should not happen as we tested !eof($fd) above
4854 return $fd if close($hifd);
4856 # should not happen
4857 !$! or die_error(500, "Couldn't close syntax highighter pipe");
4859 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4860 # instead of dying horribly on this, just skip the highlighting
4861 # but do output a message about it to STDERR that will end up in the log
4862 print STDERR "warning: skipping failed highlight for --syntax $syntax: ".
4863 sprintf("child exit status 0x%x\n", $?);
4864 return $fd
4866 close $fd;
4867 return ($hifd, 1);
4870 ## ======================================================================
4871 ## functions printing HTML: header, footer, error page
4873 sub get_page_title {
4874 my $title = to_utf8($site_name);
4876 unless (defined $project) {
4877 if (defined $project_filter) {
4878 $title .= " - projects in '" . esc_path($project_filter) . "'";
4880 return $title;
4882 $title .= " - " . to_utf8($project);
4884 return $title unless (defined $action);
4885 my $action_print = $action eq 'blame_incremental' ? 'blame' : $action;
4886 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4888 return $title unless (defined $file_name);
4889 $title .= " - " . esc_path($file_name);
4890 if ($action eq "tree" && $file_name !~ m|/$|) {
4891 $title .= "/";
4894 return $title;
4897 sub get_content_type_html {
4898 # We do not ever emit application/xhtml+xml since that gives us
4899 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4900 # strict, which is troublesome for example when showing user-supplied
4901 # README.html files.
4902 return 'text/html';
4905 sub print_feed_meta {
4906 if (defined $project) {
4907 my %href_params = get_feed_info();
4908 if (!exists $href_params{'-title'}) {
4909 $href_params{'-title'} = 'log';
4912 foreach my $format (qw(RSS Atom)) {
4913 my $type = lc($format);
4914 my %link_attr = (
4915 '-rel' => 'alternate',
4916 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
4917 '-type' => "application/$type+xml"
4920 $href_params{'extra_options'} = undef;
4921 $href_params{'action'} = $type;
4922 $link_attr{'-href'} = href(%href_params);
4923 print "<link ".
4924 "rel=\"$link_attr{'-rel'}\" ".
4925 "title=\"$link_attr{'-title'}\" ".
4926 "href=\"$link_attr{'-href'}\" ".
4927 "type=\"$link_attr{'-type'}\" ".
4928 "/>\n";
4930 $href_params{'extra_options'} = '--no-merges';
4931 $link_attr{'-href'} = href(%href_params);
4932 $link_attr{'-title'} .= ' (no merges)';
4933 print "<link ".
4934 "rel=\"$link_attr{'-rel'}\" ".
4935 "title=\"$link_attr{'-title'}\" ".
4936 "href=\"$link_attr{'-href'}\" ".
4937 "type=\"$link_attr{'-type'}\" ".
4938 "/>\n";
4941 } else {
4942 printf('<link rel="alternate" title="%s projects list" '.
4943 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4944 esc_attr($site_name), href(project=>undef, action=>"project_index"));
4945 printf('<link rel="alternate" title="%s projects feeds" '.
4946 'href="%s" type="text/x-opml" />'."\n",
4947 esc_attr($site_name), href(project=>undef, action=>"opml"));
4951 sub compute_stylesheet_links {
4952 return "<?gitweb compute_stylesheet_links?>" if $cache_mode_active;
4954 # include each stylesheet that exists, providing backwards capability
4955 # for those people who defined $stylesheet in a config file
4956 if (defined $stylesheet) {
4957 return '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4958 } else {
4959 my $sheets = '';
4960 foreach my $stylesheet (@stylesheets) {
4961 next unless $stylesheet;
4962 $sheets .= '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4964 return $sheets;
4968 sub print_header_links {
4969 my $status = shift;
4971 print compute_stylesheet_links();
4972 print_feed_meta()
4973 if ($status eq '200 OK');
4974 if (defined $favicon) {
4975 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
4979 sub print_nav_breadcrumbs_path {
4980 my $dirprefix = undef;
4981 while (my $part = shift) {
4982 $dirprefix .= "/" if defined $dirprefix;
4983 $dirprefix .= $part;
4984 print $cgi->a({-href => href(project => undef,
4985 project_filter => $dirprefix,
4986 action => "project_list")},
4987 esc_html($part)) . " / ";
4991 sub print_nav_breadcrumbs {
4992 my %opts = @_;
4994 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4995 print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / ";
4997 if (defined $project) {
4998 my @dirname = split '/', $project;
4999 my $projectbasename = pop @dirname;
5000 print_nav_breadcrumbs_path(@dirname);
5001 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
5002 if (defined $action) {
5003 my $action_print = $action ;
5004 $action_print = 'blame' if $action_print eq 'blame_incremental';
5005 if (defined $opts{-action_extra}) {
5006 $action_print = $cgi->a({-href => href(action=>$action)},
5007 $action);
5009 print " / $action_print";
5011 if (defined $opts{-action_extra}) {
5012 print " / $opts{-action_extra}";
5014 print "\n";
5015 } elsif (defined $project_filter) {
5016 print_nav_breadcrumbs_path(split '/', $project_filter);
5020 sub print_search_form {
5021 if (!defined $searchtext) {
5022 $searchtext = "";
5024 my $search_hash;
5025 if (defined $hash_base) {
5026 $search_hash = $hash_base;
5027 } elsif (defined $hash) {
5028 $search_hash = $hash;
5029 } else {
5030 $search_hash = "HEAD";
5032 # We can't use href() here because we need to encode the
5033 # URL parameters into the form, not into the action link.
5034 my $action = $my_uri;
5035 my $use_pathinfo = gitweb_check_feature('pathinfo');
5036 if ($use_pathinfo) {
5037 # See notes about doubled / in href()
5038 $action =~ s,/$,,;
5039 $action .= "/".esc_path_info($project);
5041 print $cgi->start_form(-method => "get", -action => $action) .
5042 "<div class=\"search\">\n" .
5043 (!$use_pathinfo &&
5044 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
5045 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
5046 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
5047 $cgi->popup_menu(-name => 'st', -default => 'commit',
5048 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5049 $cgi->sup($cgi->a({-href => href(action=>"search_help"),
5050 -title => "search help" },
5051 "<span style=\"padding-bottom:1em\">?&#160;</span>")) . " search:\n",
5052 $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
5053 "<span title=\"Extended regular expression\">" .
5054 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5055 -checked => $search_use_regexp) .
5056 "</span>" .
5057 "</div>" .
5058 $cgi->end_form() . "\n";
5061 sub git_header_html {
5062 my $status = shift || "200 OK";
5063 my $expires = shift;
5064 my %opts = @_;
5066 my $title = get_page_title();
5067 my $content_type = get_content_type_html();
5068 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
5069 -status=> $status, -expires => $expires)
5070 unless ($opts{'-no_http_header'});
5071 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
5072 print <<EOF;
5073 <?xml version="1.0" encoding="utf-8"?>
5074 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5075 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5076 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5077 <!-- git core binaries version $git_version -->
5078 <head>
5079 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5080 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5081 <meta name="robots" content="index, nofollow"/>
5082 <title>$title</title>
5083 <script type="text/javascript">/* <![CDATA[ */
5084 function fixBlameLinks() {
5085 var allLinks = document.getElementsByTagName("a");
5086 for (var i = 0; i < allLinks.length; i++) {
5087 var link = allLinks.item(i);
5088 if (link.className == 'blamelink')
5089 link.href = link.href.replace("/blame/", "/blame_incremental/");
5092 /* ]]> */</script>
5094 # the stylesheet, favicon etc urls won't work correctly with path_info
5095 # unless we set the appropriate base URL
5096 if ($ENV{'PATH_INFO'}) {
5097 print "<base href=\"".esc_url($base_url)."\" />\n";
5099 print_header_links($status);
5101 if (defined $site_html_head_string) {
5102 print to_utf8($site_html_head_string);
5105 print "</head>\n" .
5106 "<body><span class=\"body\">\n";
5108 if (defined $site_header && -f $site_header) {
5109 insert_file($site_header);
5112 print "<div class=\"page_header\">\n";
5113 print "<span class=\"logo-container\"><span class=\"logo-default\">";
5114 if (defined $logo) {
5115 print $cgi->a({-href => esc_url($logo_url),
5116 -title => $logo_label,
5117 -class => "logo-link"},
5118 $cgi->img({-src => esc_url($logo),
5119 -width => 72, -height => 27,
5120 -alt => "git",
5121 -class => "logo"}));
5123 print "</span></span><span class=\"banner-container\">";
5124 print_nav_breadcrumbs(%opts);
5125 print "</span></div>\n";
5127 my $have_search = gitweb_check_feature('search');
5128 if (defined $project && $have_search) {
5129 print_search_form();
5133 sub compute_timed_interval {
5134 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5135 return tv_interval($t0, [ gettimeofday() ]);
5138 sub compute_commands_count {
5139 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5140 my $s = $number_of_git_cmds == 1 ? '' : 's';
5141 return '<span id="generating_cmd">'.
5142 $number_of_git_cmds.
5143 "</span> git command$s";
5146 sub git_footer_html {
5147 my $feed_class = 'rss_logo';
5149 print "<div class=\"page_footer\">\n";
5150 if (defined $project) {
5151 my $descr = git_get_project_description($project);
5152 if (defined $descr) {
5153 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
5156 my %href_params = get_feed_info();
5157 if (!%href_params) {
5158 $feed_class .= ' generic';
5160 $href_params{'-title'} ||= 'log';
5162 foreach my $format (qw(RSS Atom)) {
5163 $href_params{'action'} = lc($format);
5164 print $cgi->a({-href => href(%href_params),
5165 -title => "$href_params{'-title'} $format feed",
5166 -class => $feed_class}, $format)."\n";
5169 } else {
5170 print $cgi->a({-href => href(project=>undef, action=>"opml",
5171 project_filter => $project_filter),
5172 -class => $feed_class}, "OPML") . " ";
5173 print $cgi->a({-href => href(project=>undef, action=>"project_index",
5174 project_filter => $project_filter),
5175 -class => $feed_class}, "TXT") . "\n";
5177 print "</div>\n"; # class="page_footer"
5179 if (defined $t0 && gitweb_check_feature('timed')) {
5180 print "<div id=\"generating_info\">\n";
5181 print 'This page took '.
5182 '<span id="generating_time" class="time_span">'.
5183 compute_timed_interval().
5184 ' seconds </span>'.
5185 ' and '.
5186 compute_commands_count().
5187 " to generate.\n";
5188 print "</div>\n"; # class="page_footer"
5191 if (defined $site_footer && -f $site_footer) {
5192 insert_file($site_footer);
5195 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
5196 if (defined $action &&
5197 $action eq 'blame_incremental') {
5198 print qq!<script type="text/javascript">\n!.
5199 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
5200 qq! "!. href() .qq!");\n!.
5201 qq!</script>\n!;
5202 } else {
5203 my ($jstimezone, $tz_cookie, $datetime_class) =
5204 gitweb_get_feature('javascript-timezone');
5206 print qq!<script type="text/javascript">\n!.
5207 qq!window.onload = function () {\n!;
5208 if (gitweb_check_feature('blame_incremental')) {
5209 print qq! fixBlameLinks();\n!;
5211 if (gitweb_check_feature('javascript-actions')) {
5212 print qq! fixLinks();\n!;
5214 if ($jstimezone && $tz_cookie && $datetime_class) {
5215 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
5216 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
5218 print qq!};\n!.
5219 qq!</script>\n!;
5222 print "</span></body>\n" .
5223 "</html>";
5226 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5227 # Example: die_error(404, 'Hash not found')
5228 # By convention, use the following status codes (as defined in RFC 2616):
5229 # 400: Invalid or missing CGI parameters, or
5230 # requested object exists but has wrong type.
5231 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5232 # this server or project.
5233 # 404: Requested object/revision/project doesn't exist.
5234 # 500: The server isn't configured properly, or
5235 # an internal error occurred (e.g. failed assertions caused by bugs), or
5236 # an unknown error occurred (e.g. the git binary died unexpectedly).
5237 # 503: The server is currently unavailable (because it is overloaded,
5238 # or down for maintenance). Generally, this is a temporary state.
5239 sub die_error {
5240 my $status = shift || 500;
5241 my $error = esc_html(shift) || "Internal Server Error";
5242 my $extra = shift;
5243 my %opts = @_;
5245 my %http_responses = (
5246 400 => '400 Bad Request',
5247 403 => '403 Forbidden',
5248 404 => '404 Not Found',
5249 500 => '500 Internal Server Error',
5250 503 => '503 Service Unavailable',
5252 git_header_html($http_responses{$status}, undef, %opts);
5253 print <<EOF;
5254 <div class="page_body">
5255 <br /><br />
5256 $status - $error
5257 <br />
5259 if (defined $extra) {
5260 print "<hr />\n" .
5261 "$extra\n";
5263 print "</div>\n";
5265 git_footer_html();
5266 CORE::die
5267 unless ($opts{'-error_handler'});
5270 ## ----------------------------------------------------------------------
5271 ## functions printing or outputting HTML: navigation
5273 # $content is wrapped in a span with class 'tab'
5274 # If $selected is true it also has class 'selected'
5275 # If $disabled is true it also has class 'disabled'
5276 # Whether or not a tab can be disabled and selected at the same time
5277 # is up to the caller
5278 # If $extra_classes is non-empty, it is a whitespace-separated list of
5279 # additional class names to include
5280 # Note that $content MUST already be html-escaped as needed because
5281 # it is included verbatim. And so are any extra class names.
5282 sub tabspan {
5283 my ($content, $selected, $disabled, $extra_classes) = @_;
5284 my @classes = ("tab");
5285 push(@classes, "selected") if $selected;
5286 push(@classes, "disabled") if $disabled;
5287 push(@classes, split(' ', $extra_classes)) if $extra_classes;
5288 return "<span class=\"" . join(" ", @classes) . "\">". $content . "</span>";
5291 sub git_print_page_nav {
5292 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5293 $extra = '' if !defined $extra; # pager or formats
5294 $extra = "<span class=\"page_nav_sub\">".$extra."</span>" if $extra ne '';
5296 my @navs = qw(summary log commit commitdiff tree refs);
5297 if ($suppress) {
5298 @navs = grep { $_ ne $suppress } @navs;
5301 my %arg = map { $_ => {action=>$_} } @navs;
5302 if (defined $head) {
5303 for (qw(commit commitdiff)) {
5304 $arg{$_}{'hash'} = $head;
5306 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5307 $arg{'log'}{'hash'} = $head;
5311 $arg{'log'}{'action'} = 'shortlog';
5312 if ($current eq 'log') {
5313 $current = 'shortlog';
5314 } elsif ($current eq 'shortlog') {
5315 $current = 'log';
5317 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5318 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5320 my @actions = gitweb_get_feature('actions');
5321 my $escname = $project;
5322 $escname =~ s/[+]/%2B/g;
5323 my %repl = (
5324 '%' => '%',
5325 'n' => $project, # project name
5326 'f' => $git_dir, # project path within filesystem
5327 'h' => $treehead || '', # current hash ('h' parameter)
5328 'b' => $treebase || '', # hash base ('hb' parameter)
5329 'e' => $escname, # project name with '+' escaped
5331 while (@actions) {
5332 my ($label, $link, $pos) = splice(@actions,0,3);
5333 # insert
5334 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
5335 # munch munch
5336 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5337 $arg{$label}{'_href'} = $link;
5340 print "<div class=\"page_nav\">\n" .
5341 (join $barsep,
5342 map { $_ eq $current ?
5343 tabspan($_, 1) :
5344 tabspan($cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_"))
5345 } @navs);
5346 print "<br/>\n$extra<br/>\n" .
5347 "</div>\n";
5350 # returns a submenu for the nagivation of the refs views (tags, heads,
5351 # remotes) with the current view disabled and the remotes view only
5352 # available if the feature is enabled
5353 sub format_ref_views {
5354 my ($current) = @_;
5355 my @ref_views = qw{tags heads};
5356 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
5357 return join $barsep, map {
5358 $_ eq $current ? tabspan($_, 1) :
5359 tabspan($cgi->a({-href => href(action=>$_)}, $_))
5360 } @ref_views
5363 sub format_paging_nav {
5364 my ($action, $page, $has_next_link) = @_;
5365 my $paging_nav = "<span class=\"paging_nav\">";
5367 if ($page > 0) {
5368 $paging_nav .= tabspan(
5369 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first")) .
5370 $mdotsep . tabspan(
5371 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5372 -accesskey => "p", -title => "Alt-p"}, "prev"));
5373 } else {
5374 $paging_nav .= tabspan("first", 1).${mdotsep}.tabspan("prev", 0, 1);
5377 if ($has_next_link) {
5378 $paging_nav .= $mdotsep . tabspan(
5379 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5380 -accesskey => "n", -title => "Alt-n"}, "next"));
5381 } else {
5382 $paging_nav .= ${mdotsep}.tabspan("next", 0, 1);
5385 return $paging_nav."</span>";
5388 sub format_log_nav {
5389 my ($action, $page, $has_next_link, $extra) = @_;
5390 my $paging_nav;
5391 defined $extra or $extra = '';
5392 $extra eq '' or $extra .= $barsep;
5394 if ($action eq 'shortlog') {
5395 $paging_nav .= tabspan('shortlog', 1);
5396 } else {
5397 $paging_nav .= tabspan($cgi->a({-href => href(action=>'shortlog', -replay=>1)}, 'shortlog'));
5399 $paging_nav .= $barsep;
5400 if ($action eq 'log') {
5401 $paging_nav .= tabspan('fulllog', 1);
5402 } else {
5403 $paging_nav .= tabspan($cgi->a({-href => href(action=>'log', -replay=>1)}, 'fulllog'));
5406 $paging_nav .= $barsep . $extra . format_paging_nav($action, $page, $has_next_link);
5407 return $paging_nav;
5410 ## ......................................................................
5411 ## functions printing or outputting HTML: div
5413 sub git_print_header_div {
5414 my ($action, $title, $hash, $hash_base, $extra) = @_;
5415 my %args = ();
5416 defined $extra or $extra = '';
5418 $args{'action'} = $action;
5419 $args{'hash'} = $hash if $hash;
5420 $args{'hash_base'} = $hash_base if $hash_base;
5422 my $link1 = $cgi->a({-href => href(%args), -class => "title"},
5423 $title ? $title : $action);
5424 my $link2 = $cgi->a({-href => href(%args), -class => "cover"}, "");
5425 print "<div class=\"header\">\n" . '<span class="title">' .
5426 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5429 sub format_repo_url {
5430 my ($name, $url) = @_;
5431 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5434 # Group output by placing it in a DIV element and adding a header.
5435 # Options for start_div() can be provided by passing a hash reference as the
5436 # first parameter to the function.
5437 # Options to git_print_header_div() can be provided by passing an array
5438 # reference. This must follow the options to start_div if they are present.
5439 # The content can be a scalar, which is output as-is, a scalar reference, which
5440 # is output after html escaping, an IO handle passed either as *handle or
5441 # *handle{IO}, or a function reference. In the latter case all following
5442 # parameters will be taken as argument to the content function call.
5443 sub git_print_section {
5444 my ($div_args, $header_args, $content);
5445 my $arg = shift;
5446 if (ref($arg) eq 'HASH') {
5447 $div_args = $arg;
5448 $arg = shift;
5450 if (ref($arg) eq 'ARRAY') {
5451 $header_args = $arg;
5452 $arg = shift;
5454 $content = $arg;
5456 print $cgi->start_div($div_args);
5457 git_print_header_div(@$header_args);
5459 if (ref($content) eq 'CODE') {
5460 $content->(@_);
5461 } elsif (ref($content) eq 'SCALAR') {
5462 print esc_html($$content);
5463 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5464 while (<$content>) {
5465 print to_utf8($_);
5467 } elsif (!ref($content) && defined($content)) {
5468 print $content;
5471 print $cgi->end_div;
5474 sub format_timestamp_html {
5475 my $date = shift;
5476 my $useatnight = shift;
5477 defined($useatnight) or $useatnight = 1;
5478 my $strtime = $date->{'rfc2822'};
5480 my (undef, undef, $datetime_class) =
5481 gitweb_get_feature('javascript-timezone');
5482 if ($datetime_class) {
5483 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
5486 my $localtime_format = '(%d %02d:%02d %s)';
5487 if ($useatnight && $date->{'hour_local'} < 6) {
5488 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5490 $strtime .= ' ' .
5491 sprintf($localtime_format, $date->{'mday_local'},
5492 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5494 return $strtime;
5497 sub format_lastrefresh_row {
5498 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5499 my %rd = parse_file_date('.last_refresh');
5500 if (defined $rd{'rfc2822'}) {
5501 return "<tr id=\"metadata_lrefresh\"><td>last&#160;refresh</td>" .
5502 "<td>".format_timestamp_html(\%rd,0)."</td></tr>";
5504 return "";
5507 # Outputs the author name and date in long form
5508 sub git_print_authorship {
5509 my $co = shift;
5510 my %opts = @_;
5511 my $tag = $opts{-tag} || 'div';
5512 my $author = $co->{'author_name'};
5514 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
5515 print "<$tag class=\"author_date\">" .
5516 format_search_author($author, "author", esc_html($author)) .
5517 " [".format_timestamp_html(\%ad)."]".
5518 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
5519 "</$tag>\n";
5522 # Outputs table rows containing the full author or committer information,
5523 # in the format expected for 'commit' view (& similar).
5524 # Parameters are a commit hash reference, followed by the list of people
5525 # to output information for. If the list is empty it defaults to both
5526 # author and committer.
5527 sub git_print_authorship_rows {
5528 my $co = shift;
5529 # too bad we can't use @people = @_ || ('author', 'committer')
5530 my @people = @_;
5531 @people = ('author', 'committer') unless @people;
5532 foreach my $who (@people) {
5533 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5534 print "<tr><td>$who</td><td>" .
5535 format_search_author($co->{"${who}_name"}, $who,
5536 esc_html($co->{"${who}_name"})) . " " .
5537 format_search_author($co->{"${who}_email"}, $who,
5538 esc_html("<" . $co->{"${who}_email"} . ">")) .
5539 "</td><td rowspan=\"2\">" .
5540 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
5541 "</td></tr>\n" .
5542 "<tr>" .
5543 "<td></td><td>" .
5544 format_timestamp_html(\%wd) .
5545 "</td>" .
5546 "</tr>\n";
5550 sub git_print_page_path {
5551 my $name = shift;
5552 my $type = shift;
5553 my $hb = shift;
5556 print "<div class=\"page_path\">";
5557 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
5558 -title => 'tree root'}, to_utf8("[$project]"));
5559 print " / ";
5560 if (defined $name) {
5561 my @dirname = split '/', $name;
5562 my $basename = pop @dirname;
5563 my $fullname = '';
5565 foreach my $dir (@dirname) {
5566 $fullname .= ($fullname ? '/' : '') . $dir;
5567 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
5568 hash_base=>$hb),
5569 -title => $fullname}, esc_path($dir));
5570 print " / ";
5572 if (defined $type && $type eq 'blob') {
5573 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
5574 hash_base=>$hb),
5575 -title => $name}, esc_path($basename));
5576 } elsif (defined $type && $type eq 'tree') {
5577 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
5578 hash_base=>$hb),
5579 -title => $name}, esc_path($basename));
5580 print " / ";
5581 } else {
5582 print esc_path($basename);
5585 print "<br/></div>\n";
5588 sub git_print_log {
5589 my $log = shift;
5590 my %opts = @_;
5592 if ($opts{'-remove_title'}) {
5593 # remove title, i.e. first line of log
5594 shift @$log;
5596 # remove leading empty lines
5597 while (defined $log->[0] && $log->[0] eq "") {
5598 shift @$log;
5601 # print log
5602 my $skip_blank_line = 0;
5603 foreach my $line (@$log) {
5604 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5605 if (! $opts{'-remove_signoff'}) {
5606 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
5607 $skip_blank_line = 1;
5609 next;
5612 if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
5613 if (! $opts{'-remove_signoff'}) {
5614 print "<span class=\"signoff\">" . esc_html($1) . ": " .
5615 "<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
5616 "</span><br/>\n";
5617 $skip_blank_line = 1;
5619 next;
5622 # print only one empty line
5623 # do not print empty line after signoff
5624 if ($line eq "") {
5625 next if ($skip_blank_line);
5626 $skip_blank_line = 1;
5627 } else {
5628 $skip_blank_line = 0;
5631 print format_log_line_html($line) . "<br/>\n";
5634 if ($opts{'-final_empty_line'}) {
5635 # end with single empty line
5636 print "<br/>\n" unless $skip_blank_line;
5640 # return link target (what link points to)
5641 sub git_get_link_target {
5642 my $hash = shift;
5643 my $link_target;
5645 # read link
5646 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
5647 or return;
5649 local $/ = undef;
5650 $link_target = to_utf8(scalar <$fd>);
5652 close $fd
5653 or return;
5655 return $link_target;
5658 # given link target, and the directory (basedir) the link is in,
5659 # return target of link relative to top directory (top tree);
5660 # return undef if it is not possible (including absolute links).
5661 sub normalize_link_target {
5662 my ($link_target, $basedir) = @_;
5664 # absolute symlinks (beginning with '/') cannot be normalized
5665 return if (substr($link_target, 0, 1) eq '/');
5667 # normalize link target to path from top (root) tree (dir)
5668 my $path;
5669 if ($basedir) {
5670 $path = $basedir . '/' . $link_target;
5671 } else {
5672 # we are in top (root) tree (dir)
5673 $path = $link_target;
5676 # remove //, /./, and /../
5677 my @path_parts;
5678 foreach my $part (split('/', $path)) {
5679 # discard '.' and ''
5680 next if (!$part || $part eq '.');
5681 # handle '..'
5682 if ($part eq '..') {
5683 if (@path_parts) {
5684 pop @path_parts;
5685 } else {
5686 # link leads outside repository (outside top dir)
5687 return;
5689 } else {
5690 push @path_parts, $part;
5693 $path = join('/', @path_parts);
5695 return $path;
5698 # print tree entry (row of git_tree), but without encompassing <tr> element
5699 sub git_print_tree_entry {
5700 my ($t, $basedir, $hash_base, $have_blame) = @_;
5702 my %base_key = ();
5703 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5705 # The format of a table row is: mode list link. Where mode is
5706 # the mode of the entry, list is the name of the entry, an href,
5707 # and link is the action links of the entry.
5709 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
5710 if (exists $t->{'size'}) {
5711 print "<td class=\"size\">$t->{'size'}</td>\n";
5713 if ($t->{'type'} eq "blob") {
5714 print "<td class=\"list\">" .
5715 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
5716 file_name=>"$basedir$t->{'name'}", %base_key),
5717 -class => "list"}, esc_path($t->{'name'}));
5718 if (S_ISLNK(oct $t->{'mode'})) {
5719 my $link_target = git_get_link_target($t->{'hash'});
5720 if ($link_target) {
5721 my $norm_target = normalize_link_target($link_target, $basedir);
5722 if (defined $norm_target) {
5723 print " -> " .
5724 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
5725 file_name=>$norm_target),
5726 -title => $norm_target}, esc_path($link_target));
5727 } else {
5728 print " -> " . esc_path($link_target);
5732 print "</td>\n";
5733 print "<td class=\"link\">";
5734 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
5735 file_name=>"$basedir$t->{'name'}", %base_key)},
5736 "blob");
5737 if ($have_blame) {
5738 print $barsep .
5739 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
5740 file_name=>"$basedir$t->{'name'}", %base_key),
5741 -class => "blamelink"},
5742 "blame");
5744 if (defined $hash_base) {
5745 print $barsep .
5746 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
5747 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
5748 "history");
5750 print $barsep .
5751 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
5752 file_name=>"$basedir$t->{'name'}")},
5753 "raw");
5754 print "</td>\n";
5756 } elsif ($t->{'type'} eq "tree") {
5757 print "<td class=\"list\">";
5758 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
5759 file_name=>"$basedir$t->{'name'}",
5760 %base_key)},
5761 esc_path($t->{'name'}));
5762 print "</td>\n";
5763 print "<td class=\"link\">";
5764 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
5765 file_name=>"$basedir$t->{'name'}",
5766 %base_key)},
5767 "tree");
5768 if (defined $hash_base) {
5769 print $barsep .
5770 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
5771 file_name=>"$basedir$t->{'name'}")},
5772 "history");
5774 print "</td>\n";
5775 } else {
5776 # unknown object: we can only present history for it
5777 # (this includes 'commit' object, i.e. submodule support)
5778 print "<td class=\"list\">" .
5779 esc_path($t->{'name'}) .
5780 "</td>\n";
5781 print "<td class=\"link\">";
5782 if (defined $hash_base) {
5783 print $cgi->a({-href => href(action=>"history",
5784 hash_base=>$hash_base,
5785 file_name=>"$basedir$t->{'name'}")},
5786 "history");
5788 print "</td>\n";
5792 ## ......................................................................
5793 ## functions printing large fragments of HTML
5795 # get pre-image filenames for merge (combined) diff
5796 sub fill_from_file_info {
5797 my ($diff, @parents) = @_;
5799 $diff->{'from_file'} = [ ];
5800 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5801 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5802 if ($diff->{'status'}[$i] eq 'R' ||
5803 $diff->{'status'}[$i] eq 'C') {
5804 $diff->{'from_file'}[$i] =
5805 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
5809 return $diff;
5812 # is current raw difftree line of file deletion
5813 sub is_deleted {
5814 my $diffinfo = shift;
5816 return $diffinfo->{'to_id'} eq ('0' x 40);
5819 # does patch correspond to [previous] difftree raw line
5820 # $diffinfo - hashref of parsed raw diff format
5821 # $patchinfo - hashref of parsed patch diff format
5822 # (the same keys as in $diffinfo)
5823 sub is_patch_split {
5824 my ($diffinfo, $patchinfo) = @_;
5826 return defined $diffinfo && defined $patchinfo
5827 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5831 sub git_difftree_body {
5832 my ($difftree, $hash, @parents) = @_;
5833 my ($parent) = $parents[0];
5834 my $have_blame = gitweb_check_feature('blame');
5835 print "<div class=\"list_head\">\n";
5836 if ($#{$difftree} > 10) {
5837 print(($#{$difftree} + 1) . " files changed:\n");
5839 print "</div>\n";
5841 print "<table class=\"" .
5842 (@parents > 1 ? "combined " : "") .
5843 "diff_tree\">\n";
5845 # header only for combined diff in 'commitdiff' view
5846 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
5847 if ($has_header) {
5848 # table header
5849 print "<thead><tr>\n" .
5850 "<th></th><th></th>\n"; # filename, patchN link
5851 for (my $i = 0; $i < @parents; $i++) {
5852 my $par = $parents[$i];
5853 print "<th>" .
5854 $cgi->a({-href => href(action=>"commitdiff",
5855 hash=>$hash, hash_parent=>$par),
5856 -title => 'commitdiff to parent number ' .
5857 ($i+1) . ': ' . substr($par,0,7)},
5858 $i+1) .
5859 "&#160;</th>\n";
5861 print "</tr></thead>\n<tbody>\n";
5864 my $alternate = 1;
5865 my $patchno = 0;
5866 foreach my $line (@{$difftree}) {
5867 my $diff = parsed_difftree_line($line);
5869 if ($alternate) {
5870 print "<tr class=\"dark\">\n";
5871 } else {
5872 print "<tr class=\"light\">\n";
5874 $alternate ^= 1;
5876 if (exists $diff->{'nparents'}) { # combined diff
5878 fill_from_file_info($diff, @parents)
5879 unless exists $diff->{'from_file'};
5881 if (!is_deleted($diff)) {
5882 # file exists in the result (child) commit
5883 print "<td>" .
5884 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5885 file_name=>$diff->{'to_file'},
5886 hash_base=>$hash),
5887 -class => "list"}, esc_path($diff->{'to_file'})) .
5888 "</td>\n";
5889 } else {
5890 print "<td>" .
5891 esc_path($diff->{'to_file'}) .
5892 "</td>\n";
5895 if ($action eq 'commitdiff') {
5896 # link to patch
5897 $patchno++;
5898 print "<td class=\"link\">" .
5899 $cgi->a({-href => href(-anchor=>"patch$patchno")},
5900 "patch") .
5901 $barsep .
5902 "</td>\n";
5905 my $has_history = 0;
5906 my $not_deleted = 0;
5907 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5908 my $hash_parent = $parents[$i];
5909 my $from_hash = $diff->{'from_id'}[$i];
5910 my $from_path = $diff->{'from_file'}[$i];
5911 my $status = $diff->{'status'}[$i];
5913 $has_history ||= ($status ne 'A');
5914 $not_deleted ||= ($status ne 'D');
5916 if ($status eq 'A') {
5917 print "<td class=\"link\" align=\"right\">$barsep</td>\n";
5918 } elsif ($status eq 'D') {
5919 print "<td class=\"link\">" .
5920 $cgi->a({-href => href(action=>"blob",
5921 hash_base=>$hash,
5922 hash=>$from_hash,
5923 file_name=>$from_path)},
5924 "blob" . ($i+1)) .
5925 "$barsep</td>\n";
5926 } else {
5927 if ($diff->{'to_id'} eq $from_hash) {
5928 print "<td class=\"link nochange\">";
5929 } else {
5930 print "<td class=\"link\">";
5932 print $cgi->a({-href => href(action=>"blobdiff",
5933 hash=>$diff->{'to_id'},
5934 hash_parent=>$from_hash,
5935 hash_base=>$hash,
5936 hash_parent_base=>$hash_parent,
5937 file_name=>$diff->{'to_file'},
5938 file_parent=>$from_path)},
5939 "diff" . ($i+1)) .
5940 "$barsep</td>\n";
5944 print "<td class=\"link\">";
5945 if ($not_deleted) {
5946 print $cgi->a({-href => href(action=>"blob",
5947 hash=>$diff->{'to_id'},
5948 file_name=>$diff->{'to_file'},
5949 hash_base=>$hash)},
5950 "blob");
5951 print $barsep if ($has_history);
5953 if ($has_history) {
5954 print $cgi->a({-href => href(action=>"history",
5955 file_name=>$diff->{'to_file'},
5956 hash_base=>$hash)},
5957 "history");
5959 print "</td>\n";
5961 print "</tr>\n";
5962 next; # instead of 'else' clause, to avoid extra indent
5964 # else ordinary diff
5966 my ($to_mode_oct, $to_mode_str, $to_file_type);
5967 my ($from_mode_oct, $from_mode_str, $from_file_type);
5968 if ($diff->{'to_mode'} ne ('0' x 6)) {
5969 $to_mode_oct = oct $diff->{'to_mode'};
5970 if (S_ISREG($to_mode_oct)) { # only for regular file
5971 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
5973 $to_file_type = file_type($diff->{'to_mode'});
5975 if ($diff->{'from_mode'} ne ('0' x 6)) {
5976 $from_mode_oct = oct $diff->{'from_mode'};
5977 if (S_ISREG($from_mode_oct)) { # only for regular file
5978 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5980 $from_file_type = file_type($diff->{'from_mode'});
5983 if ($diff->{'status'} eq "A") { # created
5984 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5985 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
5986 $mode_chng .= "]</span>";
5987 print "<td>";
5988 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5989 hash_base=>$hash, file_name=>$diff->{'file'}),
5990 -class => "list"}, esc_path($diff->{'file'}));
5991 print "</td>\n";
5992 print "<td>$mode_chng</td>\n";
5993 print "<td class=\"link\">";
5994 if ($action eq 'commitdiff') {
5995 # link to patch
5996 $patchno++;
5997 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5998 "patch") .
5999 $barsep;
6001 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6002 hash_base=>$hash, file_name=>$diff->{'file'})},
6003 "blob");
6004 print "</td>\n";
6006 } elsif ($diff->{'status'} eq "D") { # deleted
6007 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6008 print "<td>";
6009 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
6010 hash_base=>$parent, file_name=>$diff->{'file'}),
6011 -class => "list"}, esc_path($diff->{'file'}));
6012 print "</td>\n";
6013 print "<td>$mode_chng</td>\n";
6014 print "<td class=\"link\">";
6015 if ($action eq 'commitdiff') {
6016 # link to patch
6017 $patchno++;
6018 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6019 "patch") .
6020 $barsep;
6022 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
6023 hash_base=>$parent, file_name=>$diff->{'file'})},
6024 "blob") . $barsep;
6025 if ($have_blame) {
6026 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
6027 file_name=>$diff->{'file'}),
6028 -class => "blamelink"},
6029 "blame") . $barsep;
6031 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
6032 file_name=>$diff->{'file'})},
6033 "history");
6034 print "</td>\n";
6036 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6037 my $mode_chnge = "";
6038 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6039 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6040 if ($from_file_type ne $to_file_type) {
6041 $mode_chnge .= " from $from_file_type to $to_file_type";
6043 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6044 if ($from_mode_str && $to_mode_str) {
6045 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6046 } elsif ($to_mode_str) {
6047 $mode_chnge .= " mode: $to_mode_str";
6050 $mode_chnge .= "]</span>\n";
6052 print "<td>";
6053 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6054 hash_base=>$hash, file_name=>$diff->{'file'}),
6055 -class => "list"}, esc_path($diff->{'file'}));
6056 print "</td>\n";
6057 print "<td>$mode_chnge</td>\n";
6058 print "<td class=\"link\">";
6059 if ($action eq 'commitdiff') {
6060 # link to patch
6061 $patchno++;
6062 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6063 "patch") .
6064 $barsep;
6065 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6066 # "commit" view and modified file (not onlu mode changed)
6067 print $cgi->a({-href => href(action=>"blobdiff",
6068 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
6069 hash_base=>$hash, hash_parent_base=>$parent,
6070 file_name=>$diff->{'file'})},
6071 "diff") .
6072 $barsep;
6074 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6075 hash_base=>$hash, file_name=>$diff->{'file'})},
6076 "blob") . $barsep;
6077 if ($have_blame) {
6078 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
6079 file_name=>$diff->{'file'}),
6080 -class => "blamelink"},
6081 "blame") . $barsep;
6083 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
6084 file_name=>$diff->{'file'})},
6085 "history");
6086 print "</td>\n";
6088 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6089 my %status_name = ('R' => 'moved', 'C' => 'copied');
6090 my $nstatus = $status_name{$diff->{'status'}};
6091 my $mode_chng = "";
6092 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6093 # mode also for directories, so we cannot use $to_mode_str
6094 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6096 print "<td>" .
6097 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
6098 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
6099 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
6100 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6101 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
6102 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
6103 -class => "list"}, esc_path($diff->{'from_file'})) .
6104 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6105 "<td class=\"link\">";
6106 if ($action eq 'commitdiff') {
6107 # link to patch
6108 $patchno++;
6109 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6110 "patch") .
6111 $barsep;
6112 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6113 # "commit" view and modified file (not only pure rename or copy)
6114 print $cgi->a({-href => href(action=>"blobdiff",
6115 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
6116 hash_base=>$hash, hash_parent_base=>$parent,
6117 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
6118 "diff") .
6119 $barsep;
6121 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6122 hash_base=>$parent, file_name=>$diff->{'to_file'})},
6123 "blob") . $barsep;
6124 if ($have_blame) {
6125 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
6126 file_name=>$diff->{'to_file'}),
6127 -class => "blamelink"},
6128 "blame") . $barsep;
6130 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
6131 file_name=>$diff->{'to_file'})},
6132 "history");
6133 print "</td>\n";
6135 } # we should not encounter Unmerged (U) or Unknown (X) status
6136 print "</tr>\n";
6138 print "</tbody>" if $has_header;
6139 print "</table>\n";
6142 # Print context lines and then rem/add lines in a side-by-side manner.
6143 sub print_sidebyside_diff_lines {
6144 my ($ctx, $rem, $add) = @_;
6146 # print context block before add/rem block
6147 if (@$ctx) {
6148 print join '',
6149 '<div class="chunk_block ctx">',
6150 '<div class="old">',
6151 @$ctx,
6152 '</div>',
6153 '<div class="new">',
6154 @$ctx,
6155 '</div>',
6156 '</div>';
6159 if (!@$add) {
6160 # pure removal
6161 print join '',
6162 '<div class="chunk_block rem">',
6163 '<div class="old">',
6164 @$rem,
6165 '</div>',
6166 '</div>';
6167 } elsif (!@$rem) {
6168 # pure addition
6169 print join '',
6170 '<div class="chunk_block add">',
6171 '<div class="new">',
6172 @$add,
6173 '</div>',
6174 '</div>';
6175 } else {
6176 print join '',
6177 '<div class="chunk_block chg">',
6178 '<div class="old">',
6179 @$rem,
6180 '</div>',
6181 '<div class="new">',
6182 @$add,
6183 '</div>',
6184 '</div>';
6188 # Print context lines and then rem/add lines in inline manner.
6189 sub print_inline_diff_lines {
6190 my ($ctx, $rem, $add) = @_;
6192 print @$ctx, @$rem, @$add;
6195 # Format removed and added line, mark changed part and HTML-format them.
6196 # Implementation is based on contrib/diff-highlight
6197 sub format_rem_add_lines_pair {
6198 my ($rem, $add, $num_parents) = @_;
6200 # We need to untabify lines before split()'ing them;
6201 # otherwise offsets would be invalid.
6202 chomp $rem;
6203 chomp $add;
6204 $rem = untabify($rem);
6205 $add = untabify($add);
6207 my @rem = split(//, $rem);
6208 my @add = split(//, $add);
6209 my ($esc_rem, $esc_add);
6210 # Ignore leading +/- characters for each parent.
6211 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6212 my ($prefix_has_nonspace, $suffix_has_nonspace);
6214 my $shorter = (@rem < @add) ? @rem : @add;
6215 while ($prefix_len < $shorter) {
6216 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6218 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6219 $prefix_len++;
6222 while ($prefix_len + $suffix_len < $shorter) {
6223 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6225 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6226 $suffix_len++;
6229 # Mark lines that are different from each other, but have some common
6230 # part that isn't whitespace. If lines are completely different, don't
6231 # mark them because that would make output unreadable, especially if
6232 # diff consists of multiple lines.
6233 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6234 $esc_rem = esc_html_hl_regions($rem, 'marked',
6235 [$prefix_len, @rem - $suffix_len], -nbsp=>1);
6236 $esc_add = esc_html_hl_regions($add, 'marked',
6237 [$prefix_len, @add - $suffix_len], -nbsp=>1);
6238 } else {
6239 $esc_rem = esc_html($rem, -nbsp=>1);
6240 $esc_add = esc_html($add, -nbsp=>1);
6243 return format_diff_line(\$esc_rem, 'rem'),
6244 format_diff_line(\$esc_add, 'add');
6247 # HTML-format diff context, removed and added lines.
6248 sub format_ctx_rem_add_lines {
6249 my ($ctx, $rem, $add, $num_parents) = @_;
6250 my (@new_ctx, @new_rem, @new_add);
6251 my $can_highlight = 0;
6252 my $is_combined = ($num_parents > 1);
6254 # Highlight if every removed line has a corresponding added line.
6255 if (@$add > 0 && @$add == @$rem) {
6256 $can_highlight = 1;
6258 # Highlight lines in combined diff only if the chunk contains
6259 # diff between the same version, e.g.
6261 # - a
6262 # - b
6263 # + c
6264 # + d
6266 # Otherwise the highlightling would be confusing.
6267 if ($is_combined) {
6268 for (my $i = 0; $i < @$add; $i++) {
6269 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6270 my $prefix_add = substr($add->[$i], 0, $num_parents);
6272 $prefix_rem =~ s/-/+/g;
6274 if ($prefix_rem ne $prefix_add) {
6275 $can_highlight = 0;
6276 last;
6282 if ($can_highlight) {
6283 for (my $i = 0; $i < @$add; $i++) {
6284 my ($line_rem, $line_add) = format_rem_add_lines_pair(
6285 $rem->[$i], $add->[$i], $num_parents);
6286 push @new_rem, $line_rem;
6287 push @new_add, $line_add;
6289 } else {
6290 @new_rem = map { format_diff_line($_, 'rem') } @$rem;
6291 @new_add = map { format_diff_line($_, 'add') } @$add;
6294 @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
6296 return (\@new_ctx, \@new_rem, \@new_add);
6299 # Print context lines and then rem/add lines.
6300 sub print_diff_lines {
6301 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6302 my $is_combined = $num_parents > 1;
6304 ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
6305 $num_parents);
6307 if ($diff_style eq 'sidebyside' && !$is_combined) {
6308 print_sidebyside_diff_lines($ctx, $rem, $add);
6309 } else {
6310 # default 'inline' style and unknown styles
6311 print_inline_diff_lines($ctx, $rem, $add);
6315 sub print_diff_chunk {
6316 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6317 my (@ctx, @rem, @add);
6319 # The class of the previous line.
6320 my $prev_class = '';
6322 return unless @chunk;
6324 # incomplete last line might be among removed or added lines,
6325 # or both, or among context lines: find which
6326 for (my $i = 1; $i < @chunk; $i++) {
6327 if ($chunk[$i][0] eq 'incomplete') {
6328 $chunk[$i][0] = $chunk[$i-1][0];
6332 # guardian
6333 push @chunk, ["", ""];
6335 foreach my $line_info (@chunk) {
6336 my ($class, $line) = @$line_info;
6338 # print chunk headers
6339 if ($class && $class eq 'chunk_header') {
6340 print format_diff_line($line, $class, $from, $to);
6341 next;
6344 ## print from accumulator when have some add/rem lines or end
6345 # of chunk (flush context lines), or when have add and rem
6346 # lines and new block is reached (otherwise add/rem lines could
6347 # be reordered)
6348 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6349 (@rem && @add && $class ne $prev_class)) {
6350 print_diff_lines(\@ctx, \@rem, \@add,
6351 $diff_style, $num_parents);
6352 @ctx = @rem = @add = ();
6355 ## adding lines to accumulator
6356 # guardian value
6357 last unless $line;
6358 # rem, add or change
6359 if ($class eq 'rem') {
6360 push @rem, $line;
6361 } elsif ($class eq 'add') {
6362 push @add, $line;
6364 # context line
6365 if ($class eq 'ctx') {
6366 push @ctx, $line;
6369 $prev_class = $class;
6373 sub git_patchset_body {
6374 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6375 my ($hash_parent) = $hash_parents[0];
6377 my $is_combined = (@hash_parents > 1);
6378 my $patch_idx = 0;
6379 my $patch_number = 0;
6380 my $patch_line;
6381 my $diffinfo;
6382 my $to_name;
6383 my (%from, %to);
6384 my @chunk; # for side-by-side diff
6386 print "<div class=\"patchset\">\n";
6388 # skip to first patch
6389 while ($patch_line = to_utf8(scalar <$fd>)) {
6390 chomp $patch_line;
6392 last if ($patch_line =~ m/^diff /);
6395 PATCH:
6396 while ($patch_line) {
6398 # parse "git diff" header line
6399 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6400 # $1 is from_name, which we do not use
6401 $to_name = unquote($2);
6402 $to_name =~ s!^b/!!;
6403 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6404 # $1 is 'cc' or 'combined', which we do not use
6405 $to_name = unquote($2);
6406 } else {
6407 $to_name = undef;
6410 # check if current patch belong to current raw line
6411 # and parse raw git-diff line if needed
6412 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
6413 # this is continuation of a split patch
6414 print "<div class=\"patch cont\">\n";
6415 } else {
6416 # advance raw git-diff output if needed
6417 $patch_idx++ if defined $diffinfo;
6419 # read and prepare patch information
6420 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6422 # compact combined diff output can have some patches skipped
6423 # find which patch (using pathname of result) we are at now;
6424 if ($is_combined) {
6425 while ($to_name ne $diffinfo->{'to_file'}) {
6426 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6427 format_diff_cc_simplified($diffinfo, @hash_parents) .
6428 "</div>\n"; # class="patch"
6430 $patch_idx++;
6431 $patch_number++;
6433 last if $patch_idx > $#$difftree;
6434 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6438 # modifies %from, %to hashes
6439 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
6441 # this is first patch for raw difftree line with $patch_idx index
6442 # we index @$difftree array from 0, but number patches from 1
6443 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6446 # git diff header
6447 #assert($patch_line =~ m/^diff /) if DEBUG;
6448 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6449 $patch_number++;
6450 # print "git diff" header
6451 print format_git_diff_header_line($patch_line, $diffinfo,
6452 \%from, \%to);
6454 # print extended diff header
6455 print "<div class=\"diff extended_header\">\n";
6456 EXTENDED_HEADER:
6457 while ($patch_line = to_utf8(scalar<$fd>)) {
6458 chomp $patch_line;
6460 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
6462 print format_extended_diff_header_line($patch_line, $diffinfo,
6463 \%from, \%to);
6465 print "</div>\n"; # class="diff extended_header"
6467 # from-file/to-file diff header
6468 if (! $patch_line) {
6469 print "</div>\n"; # class="patch"
6470 last PATCH;
6472 next PATCH if ($patch_line =~ m/^diff /);
6473 #assert($patch_line =~ m/^---/) if DEBUG;
6475 my $last_patch_line = $patch_line;
6476 $patch_line = to_utf8(scalar <$fd>);
6477 chomp $patch_line;
6478 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6480 print format_diff_from_to_header($last_patch_line, $patch_line,
6481 $diffinfo, \%from, \%to,
6482 @hash_parents);
6484 # the patch itself
6485 LINE:
6486 while ($patch_line = to_utf8(scalar <$fd>)) {
6487 chomp $patch_line;
6489 next PATCH if ($patch_line =~ m/^diff /);
6491 my $class = diff_line_class($patch_line, \%from, \%to);
6493 if ($class eq 'chunk_header') {
6494 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
6495 @chunk = ();
6498 push @chunk, [ $class, $patch_line ];
6501 } continue {
6502 if (@chunk) {
6503 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
6504 @chunk = ();
6506 print "</div>\n"; # class="patch"
6509 # for compact combined (--cc) format, with chunk and patch simplification
6510 # the patchset might be empty, but there might be unprocessed raw lines
6511 for (++$patch_idx if $patch_number > 0;
6512 $patch_idx < @$difftree;
6513 ++$patch_idx) {
6514 # read and prepare patch information
6515 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6517 # generate anchor for "patch" links in difftree / whatchanged part
6518 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6519 format_diff_cc_simplified($diffinfo, @hash_parents) .
6520 "</div>\n"; # class="patch"
6522 $patch_number++;
6525 if ($patch_number == 0) {
6526 if (@hash_parents > 1) {
6527 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6528 } else {
6529 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6533 print "</div>\n"; # class="patchset"
6536 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6538 sub git_project_search_form {
6539 my ($searchtext, $search_use_regexp) = @_;
6541 my $limit = '';
6542 if ($project_filter) {
6543 $limit = " in '$project_filter'";
6546 print "<div class=\"projsearch\">\n";
6547 print $cgi->start_form(-method => 'get', -action => $my_uri) .
6548 $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
6549 print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
6550 if (defined $project_filter);
6551 print $cgi->textfield(-name => 's', -value => $searchtext,
6552 -title => "Search project by name and description$limit",
6553 -size => 60) . "\n" .
6554 "<span title=\"Extended regular expression\">" .
6555 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
6556 -checked => $search_use_regexp) .
6557 "</span>\n" .
6558 $cgi->submit(-name => 'btnS', -value => 'Search') .
6559 $cgi->end_form() . "\n" .
6560 "<span class=\"projectlist_link\">" .
6561 $cgi->a({-href => href(project => undef, searchtext => undef,
6562 action => 'project_list',
6563 project_filter => $project_filter)},
6564 esc_html("List all projects$limit")) . "</span><br />\n";
6565 print "<span class=\"projectlist_link\">" .
6566 $cgi->a({-href => href(project => undef, searchtext => undef,
6567 action => 'project_list',
6568 project_filter => undef)},
6569 esc_html("List all projects")) . "</span>\n" if $project_filter;
6570 print "</div>\n";
6573 # entry for given @keys needs filling if at least one of keys in list
6574 # is not present in %$project_info
6575 sub project_info_needs_filling {
6576 my ($project_info, @keys) = @_;
6578 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6579 foreach my $key (@keys) {
6580 if (!exists $project_info->{$key}) {
6581 return 1;
6584 return;
6587 sub git_cache_file_format {
6588 return GITWEB_CACHE_FORMAT .
6589 (gitweb_check_feature('forks') ? " (forks)" : "");
6592 sub git_retrieve_cache_file {
6593 my $cache_file = shift;
6595 use Storable qw(retrieve);
6597 if ((my $dump = eval { retrieve($cache_file) })) {
6598 return $$dump[1] if
6599 ref($dump) eq 'ARRAY' &&
6600 @$dump == 2 &&
6601 ref($$dump[1]) eq 'ARRAY' &&
6602 @{$$dump[1]} == 2 &&
6603 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6604 ref(${$$dump[1]}[1]) eq 'HASH' &&
6605 $$dump[0] eq git_cache_file_format();
6608 return undef;
6611 sub git_store_cache_file {
6612 my ($cache_file, $cachedata) = @_;
6614 use File::Basename qw(dirname);
6615 use File::stat;
6616 use POSIX qw(:fcntl_h);
6617 use Storable qw(store_fd);
6619 my $result = undef;
6620 my $cache_d = dirname($cache_file);
6621 my $mask = umask();
6622 umask($mask & ~0070) if $cache_grpshared;
6623 if ((-d $cache_d || mkdir($cache_d, $cache_grpshared ? 0770 : 0700)) &&
6624 sysopen(my $fd, "$cache_file.lock", O_WRONLY|O_CREAT|O_EXCL, $cache_grpshared ? 0660 : 0600)) {
6625 store_fd([git_cache_file_format(), $cachedata], $fd);
6626 close $fd;
6627 rename "$cache_file.lock", $cache_file;
6628 $result = stat($cache_file)->mtime;
6630 umask($mask) if $cache_grpshared;
6631 return $result;
6634 sub verify_cached_project {
6635 my ($hashref, $path) = @_;
6636 return undef unless $path;
6637 delete $$hashref{$path}, return undef unless is_valid_project($path);
6638 return $$hashref{$path} if exists $$hashref{$path};
6640 # A valid project was requested but it's not yet in the cache
6641 # Manufacture a minimal project entry (path, name, description)
6642 # Also provide age, but only if it's available via $lastactivity_file
6644 my %proj = ('path' => $path);
6645 my $val = git_get_project_description($path);
6646 defined $val or $val = '';
6647 $proj{'descr_long'} = $val;
6648 $proj{'descr'} = chop_str($val, $projects_list_description_width, 5);
6649 unless ($omit_owner) {
6650 $val = git_get_project_owner($path);
6651 defined $val or $val = '';
6652 $proj{'owner'} = $val;
6654 unless ($omit_age_column) {
6655 ($val) = git_get_last_activity($path, 1);
6656 $proj{'age_epoch'} = $val if defined $val;
6658 $$hashref{$path} = \%proj;
6659 return \%proj;
6662 sub git_filter_cached_projects {
6663 my ($cache, $projlist, $verify) = @_;
6664 my $hashref = $$cache[1];
6665 my $sub = $verify ?
6666 sub {verify_cached_project($hashref, $_[0])} :
6667 sub {$$hashref{$_[0]}};
6668 return map {
6669 my $c = &$sub($_->{'path'});
6670 defined $c ? ($_ = $c) : ()
6671 } @$projlist;
6674 # fills project list info (age, description, owner, category, forks, etc.)
6675 # for each project in the list, removing invalid projects from
6676 # returned list, or fill only specified info.
6678 # Invalid projects are removed from the returned list if and only if you
6679 # ask 'age_epoch' to be filled, because they are the only fields
6680 # that run unconditionally git command that requires repository, and
6681 # therefore do always check if project repository is invalid.
6683 # USAGE:
6684 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6685 # ensures that 'descr_long' and 'ctags' fields are filled
6686 # * @project_list = fill_project_list_info(\@project_list)
6687 # ensures that all fields are filled (and invalid projects removed)
6689 # NOTE: modifies $projlist, but does not remove entries from it
6690 sub fill_project_list_info {
6691 my ($projlist, @wanted_keys) = @_;
6693 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6694 return fill_project_list_info_uncached($projlist, @wanted_keys)
6695 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6697 use File::stat;
6699 my $cache_lifetime = $rebuild ? 0 : $projlist_cache_lifetime;
6700 my $cache_file = "$cache_dir/$projlist_cache_name";
6702 my @projects;
6703 my $stale = 0;
6704 my $now = time();
6705 my $cache_mtime;
6706 if ($cache_lifetime && -f $cache_file) {
6707 $cache_mtime = stat($cache_file)->mtime;
6708 $cache_dump = undef if $cache_mtime &&
6709 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6711 if (defined $cache_mtime && # caching is on and $cache_file exists
6712 $cache_mtime + $cache_lifetime*60 > $now &&
6713 ($cache_dump || ($cache_dump = git_retrieve_cache_file($cache_file)))) {
6714 # Cache hit.
6715 $cache_dump_mtime = $cache_mtime;
6716 $stale = $now - $cache_mtime;
6717 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6718 gitweb_check_feature('forks');
6719 @projects = git_filter_cached_projects($cache_dump, $projlist, $verify);
6721 } else { # Cache miss.
6722 if (defined $cache_mtime) {
6723 # Postpone timeout by two minutes so that we get
6724 # enough time to do our job, or to be more exact
6725 # make cache expire after two minutes from now.
6726 my $time = $now - $cache_lifetime*60 + 120;
6727 utime $time, $time, $cache_file;
6729 my @all_projects = git_get_projects_list();
6730 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6731 fill_project_list_info_uncached(\@all_projects);
6732 map { $all_projects_filled{$_->{'path'}} = $_ }
6733 filter_forks_from_projects_list([values(%all_projects_filled)])
6734 if gitweb_check_feature('forks');
6735 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6736 \%all_projects_filled];
6737 $cache_dump_mtime = git_store_cache_file($cache_file, $cache_dump);
6738 @projects = git_filter_cached_projects($cache_dump, $projlist);
6741 if ($cache_lifetime && $stale > 0) {
6742 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6743 unless $shown_stale_message;
6744 $shown_stale_message = 1;
6747 return @projects;
6750 sub fill_project_list_info_uncached {
6751 my ($projlist, @wanted_keys) = @_;
6752 my @projects;
6753 my $filter_set = sub { return @_; };
6754 if (@wanted_keys) {
6755 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6756 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6759 my $show_ctags = gitweb_check_feature('ctags');
6760 PROJECT:
6761 foreach my $pr (@$projlist) {
6762 if (project_info_needs_filling($pr, $filter_set->('age_epoch'))) {
6763 my (@activity) = git_get_last_activity($pr->{'path'});
6764 unless (@activity) {
6765 next PROJECT;
6767 ($pr->{'age_epoch'}) = @activity;
6769 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
6770 my $descr = git_get_project_description($pr->{'path'}) || "";
6771 $descr = to_utf8($descr);
6772 $pr->{'descr_long'} = $descr;
6773 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
6775 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
6776 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
6778 if ($show_ctags &&
6779 project_info_needs_filling($pr, $filter_set->('ctags'))) {
6780 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
6782 if ($projects_list_group_categories &&
6783 project_info_needs_filling($pr, $filter_set->('category'))) {
6784 my $cat = git_get_project_category($pr->{'path'}) ||
6785 $project_list_default_category;
6786 $pr->{'category'} = to_utf8($cat);
6789 push @projects, $pr;
6792 return @projects;
6795 sub sort_projects_list {
6796 my ($projlist, $order) = @_;
6798 sub order_str {
6799 my $key = shift;
6800 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6803 sub order_reverse_num_then_undef {
6804 my $key = shift;
6805 return sub {
6806 defined $a->{$key} ?
6807 (defined $b->{$key} ? $b->{$key} <=> $a->{$key} : -1) :
6808 (defined $b->{$key} ? 1 : 0)
6812 my %orderings = (
6813 project => order_str('path'),
6814 descr => order_str('descr_long'),
6815 owner => order_str('owner'),
6816 age => order_reverse_num_then_undef('age_epoch'),
6819 my $ordering = $orderings{$order};
6820 return defined $ordering ? sort $ordering @$projlist : @$projlist;
6823 # returns a hash of categories, containing the list of project
6824 # belonging to each category
6825 sub build_projlist_by_category {
6826 my ($projlist, $from, $to) = @_;
6827 my %categories;
6829 $from = 0 unless defined $from;
6830 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6832 for (my $i = $from; $i <= $to; $i++) {
6833 my $pr = $projlist->[$i];
6834 push @{$categories{ $pr->{'category'} }}, $pr;
6837 return wantarray ? %categories : \%categories;
6840 # print 'sort by' <th> element, generating 'sort by $name' replay link
6841 # if that order is not selected
6842 sub print_sort_th {
6843 print format_sort_th(@_);
6846 sub format_sort_th {
6847 my ($name, $order, $header) = @_;
6848 my $sort_th = "";
6849 $header ||= ucfirst($name);
6851 if ($order eq $name) {
6852 $sort_th .= "<th>$header</th>\n";
6853 } else {
6854 $sort_th .= "<th>" .
6855 $cgi->a({-href => href(-replay=>1, order=>$name),
6856 -class => "header"}, $header) .
6857 "</th>\n";
6860 return $sort_th;
6863 sub git_project_list_rows {
6864 my ($projlist, $from, $to, $check_forks) = @_;
6866 $from = 0 unless defined $from;
6867 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6869 my $now = time;
6870 my $alternate = 1;
6871 for (my $i = $from; $i <= $to; $i++) {
6872 my $pr = $projlist->[$i];
6874 if ($alternate) {
6875 print "<tr class=\"dark\">\n";
6876 } else {
6877 print "<tr class=\"light\">\n";
6879 $alternate ^= 1;
6881 if ($check_forks) {
6882 print "<td>";
6883 if ($pr->{'forks'}) {
6884 my $nforks = scalar @{$pr->{'forks'}};
6885 my $s = $nforks == 1 ? '' : 's';
6886 if ($nforks > 0) {
6887 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
6888 -title => "$nforks fork$s"}, "+");
6889 } else {
6890 print $cgi->span({-title => "$nforks fork$s"}, "+");
6893 print "</td>\n";
6895 my $path = $pr->{'path'};
6896 my $dotgit = $path =~ s/\.git$// ? '.git' : '';
6897 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
6898 -class => "list"},
6899 esc_html_match_hl($path, $search_regexp).$dotgit) .
6900 "</td>\n" .
6901 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
6902 -class => "list",
6903 -title => $pr->{'descr_long'}},
6904 $search_regexp
6905 ? esc_html_match_hl_chopped($pr->{'descr_long'},
6906 $pr->{'descr'}, $search_regexp)
6907 : esc_html($pr->{'descr'})) .
6908 "</td>\n";
6909 unless ($omit_owner) {
6910 print "<td><i>" . ($owner_link_hook
6911 ? $cgi->a({-href => $owner_link_hook->($pr->{'owner'}), -class => "list"},
6912 chop_and_escape_str($pr->{'owner'}, 15))
6913 : chop_and_escape_str($pr->{'owner'}, 15)) . "</i></td>\n";
6915 unless ($omit_age_column) {
6916 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6917 $age_string = defined $age_epoch ? age_string($age_epoch, $now) : "No commits";
6918 print "<td class=\"". age_class($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6920 print"<td class=\"link\">" .
6921 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . $barsep .
6922 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "log") . $barsep .
6923 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
6924 ($pr->{'forks'} ? $barsep . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
6925 "</td>\n" .
6926 "</tr>\n";
6930 sub git_project_list_body {
6931 # actually uses global variable $project
6932 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6933 my @projects = @$projlist;
6935 my $check_forks = gitweb_check_feature('forks');
6936 my $show_ctags = gitweb_check_feature('ctags');
6937 my $tagfilter = $show_ctags ? $input_params{'ctag_filter'} : undef;
6938 $check_forks = undef
6939 if ($tagfilter || $search_regexp);
6941 # filtering out forks before filling info allows us to do less work
6942 if ($check_forks) {
6943 @projects = filter_forks_from_projects_list(\@projects);
6944 push @projects, { 'path' => "$project_filter.git" }
6945 if $project_filter && $keep_top && is_valid_project("$project_filter.git");
6947 # search_projects_list pre-fills required info
6948 @projects = search_projects_list(\@projects,
6949 'search_regexp' => $search_regexp,
6950 'tagfilter' => $tagfilter)
6951 if ($tagfilter || $search_regexp);
6952 # fill the rest
6953 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
6954 push @all_fields, 'age_epoch' unless($omit_age_column);
6955 push @all_fields, 'owner' unless($omit_owner);
6956 @projects = fill_project_list_info(\@projects, @all_fields);
6958 $order ||= $default_projects_order;
6959 $from = 0 unless defined $from;
6960 $to = $#projects if (!defined $to || $#projects < $to);
6962 # short circuit
6963 if ($from > $to) {
6964 print "<center>\n".
6965 "<b>No such projects found</b><br />\n".
6966 "Click ".$cgi->a({-href=>href(project=>undef,action=>'project_list')},"here")." to view all projects<br />\n".
6967 "</center>\n<br />\n";
6968 return;
6971 @projects = sort_projects_list(\@projects, $order);
6973 if ($show_ctags) {
6974 my $ctags = git_gather_all_ctags(\@projects);
6975 my $cloud = git_populate_project_tagcloud($ctags, $ctags_action||'project_list');
6976 print git_show_project_tagcloud($cloud, 64);
6979 print "<table class=\"project_list\">\n";
6980 unless ($no_header) {
6981 print "<tr>\n";
6982 if ($check_forks) {
6983 print "<th></th>\n";
6985 print_sort_th('project', $order, 'Project');
6986 print_sort_th('descr', $order, 'Description');
6987 print_sort_th('owner', $order, 'Owner') unless $omit_owner;
6988 print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
6989 print "<th></th>\n" . # for links
6990 "</tr>\n";
6993 if ($projects_list_group_categories) {
6994 # only display categories with projects in the $from-$to window
6995 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
6996 my %categories = build_projlist_by_category(\@projects, $from, $to);
6997 foreach my $cat (sort keys %categories) {
6998 unless ($cat eq "") {
6999 print "<tr>\n";
7000 if ($check_forks) {
7001 print "<td></td>\n";
7003 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
7004 print "</tr>\n";
7007 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
7009 } else {
7010 git_project_list_rows(\@projects, $from, $to, $check_forks);
7013 if (defined $extra) {
7014 print "<tr class=\"extra\">\n";
7015 if ($check_forks) {
7016 print "<td></td>\n";
7018 print "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7019 "</tr>\n";
7021 print "</table>\n";
7024 sub git_log_body {
7025 # uses global variable $project
7026 my ($commitlist, $from, $to, $refs, $extra) = @_;
7028 $from = 0 unless defined $from;
7029 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7031 for (my $i = 0; $i <= $to; $i++) {
7032 my %co = %{$commitlist->[$i]};
7033 next if !%co;
7034 my $commit = $co{'id'};
7035 my $ref = format_ref_marker($refs, $commit);
7036 git_print_header_div('commit',
7037 "<span class=\"age\">$co{'age_string'}</span>" .
7038 esc_html($co{'title'}),
7039 $commit, undef, $ref);
7040 print "<div class=\"title_text\">\n" .
7041 "<div class=\"log_link\">\n" .
7042 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
7043 $barsep .
7044 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
7045 $barsep .
7046 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
7047 "<br/>\n" .
7048 "</div>\n";
7049 git_print_authorship(\%co, -tag => 'span');
7050 print "<br/>\n</div>\n";
7052 print "<div class=\"log_body\">\n";
7053 git_print_log($co{'comment'}, -final_empty_line=> 1);
7054 print "</div>\n";
7056 if ($extra) {
7057 print "<div class=\"page_nav_trailer\">\n";
7058 print "$extra\n";
7059 print "</div>\n";
7063 sub git_shortlog_body {
7064 # uses global variable $project
7065 my ($commitlist, $from, $to, $refs, $extra) = @_;
7067 $from = 0 unless defined $from;
7068 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7070 print "<table class=\"shortlog\">\n";
7071 my $alternate = 1;
7072 for (my $i = $from; $i <= $to; $i++) {
7073 my %co = %{$commitlist->[$i]};
7074 my $commit = $co{'id'};
7075 my $ref = format_ref_marker($refs, $commit);
7076 if ($alternate) {
7077 print "<tr class=\"dark\">\n";
7078 } else {
7079 print "<tr class=\"light\">\n";
7081 $alternate ^= 1;
7082 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7083 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7084 format_author_html('td', \%co, 10) . "<td>";
7085 print format_subject_html($co{'title'}, $co{'title_short'},
7086 href(action=>"commit", hash=>$commit), $ref);
7087 print "</td>\n" .
7088 "<td class=\"link\">" .
7089 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . $barsep .
7090 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . $barsep .
7091 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
7092 my $snapshot_links = format_snapshot_links($commit);
7093 if (defined $snapshot_links) {
7094 print $barsep . $snapshot_links;
7096 print "</td>\n" .
7097 "</tr>\n";
7099 if (defined $extra) {
7100 print "<tr class=\"extra\">\n" .
7101 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7102 "</tr>\n";
7104 print "</table>\n";
7107 sub git_history_body {
7108 # Warning: assumes constant type (blob or tree) during history
7109 my ($commitlist, $from, $to, $refs, $extra,
7110 $file_name, $file_hash, $ftype) = @_;
7112 $from = 0 unless defined $from;
7113 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7115 print "<table class=\"history\">\n";
7116 my $alternate = 1;
7117 for (my $i = $from; $i <= $to; $i++) {
7118 my %co = %{$commitlist->[$i]};
7119 if (!%co) {
7120 next;
7122 my $commit = $co{'id'};
7124 my $ref = format_ref_marker($refs, $commit);
7126 if ($alternate) {
7127 print "<tr class=\"dark\">\n";
7128 } else {
7129 print "<tr class=\"light\">\n";
7131 $alternate ^= 1;
7132 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7133 # shortlog: format_author_html('td', \%co, 10)
7134 format_author_html('td', \%co, 15, 3) . "<td>";
7135 # originally git_history used chop_str($co{'title'}, 50)
7136 print format_subject_html($co{'title'}, $co{'title_short'},
7137 href(action=>"commit", hash=>$commit), $ref);
7138 print "</td>\n" .
7139 "<td class=\"link\">" .
7140 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . $barsep .
7141 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
7143 if ($ftype eq 'blob') {
7144 my $blob_current = $file_hash;
7145 my $blob_parent = git_get_hash_by_path($commit, $file_name);
7146 if (defined $blob_current && defined $blob_parent &&
7147 $blob_current ne $blob_parent) {
7148 print $barsep .
7149 $cgi->a({-href => href(action=>"blobdiff",
7150 hash=>$blob_current, hash_parent=>$blob_parent,
7151 hash_base=>$hash_base, hash_parent_base=>$commit,
7152 file_name=>$file_name)},
7153 "diff to current");
7156 print "</td>\n" .
7157 "</tr>\n";
7159 if (defined $extra) {
7160 print "<tr class=\"extra\">\n" .
7161 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7162 "</tr>\n";
7164 print "</table>\n";
7167 sub git_tags_body {
7168 # uses global variable $project
7169 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7170 $from = 0 unless defined $from;
7171 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7172 $order ||= $default_refs_order;
7174 print "<table class=\"tags\">\n";
7175 if ($full) {
7176 print "<tr class=\"tags_header\">\n";
7177 print_sort_th('age', $order, 'Last Change');
7178 print_sort_th('name', $order, 'Name');
7179 print "<th></th>\n" . # for comment
7180 "<th></th>\n" . # for tag
7181 "<th></th>\n" . # for links
7182 "</tr>\n";
7184 my $alternate = 1;
7185 for (my $i = $from; $i <= $to; $i++) {
7186 my $entry = $taglist->[$i];
7187 my %tag = %$entry;
7188 my $comment = $tag{'subject'};
7189 my $comment_short;
7190 if (defined $comment) {
7191 $comment_short = chop_str($comment, 30, 5);
7193 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7194 if ($alternate) {
7195 print "<tr class=\"dark\">\n";
7196 } else {
7197 print "<tr class=\"light\">\n";
7199 $alternate ^= 1;
7200 if (defined $tag{'age'}) {
7201 print "<td><i>$tag{'age'}</i></td>\n";
7202 } else {
7203 print "<td></td>\n";
7205 print(($curr ? "<td class=\"current_head\">" : "<td>") .
7206 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
7207 -class => "list name"}, esc_html($tag{'name'})) .
7208 "</td>\n" .
7209 "<td>");
7210 if (defined $comment) {
7211 print format_subject_html($comment, $comment_short,
7212 href(action=>"tag", hash=>$tag{'id'}));
7214 print "</td>\n" .
7215 "<td class=\"selflink\">";
7216 if ($tag{'type'} eq "tag") {
7217 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
7218 } else {
7219 print "&#160;";
7221 print "</td>\n" .
7222 "<td class=\"link\">" . $barsep .
7223 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
7224 if ($tag{'reftype'} eq "commit") {
7225 print $barsep . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "log");
7226 print $barsep . $cgi->a({-href => href(action=>"tree", hash=>$tag{'fullname'})}, "tree") if $full;
7227 } elsif ($tag{'reftype'} eq "blob") {
7228 print $barsep . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
7230 print "</td>\n" .
7231 "</tr>";
7233 if (defined $extra) {
7234 print "<tr class=\"extra\">\n" .
7235 "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7236 "</tr>\n";
7238 print "</table>\n";
7241 sub git_heads_body {
7242 # uses global variable $project
7243 my ($headlist, $head_at, $from, $to, $extra) = @_;
7244 $from = 0 unless defined $from;
7245 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7247 print "<table class=\"heads\">\n";
7248 my $alternate = 1;
7249 for (my $i = $from; $i <= $to; $i++) {
7250 my $entry = $headlist->[$i];
7251 my %ref = %$entry;
7252 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7253 if ($alternate) {
7254 print "<tr class=\"dark\">\n";
7255 } else {
7256 print "<tr class=\"light\">\n";
7258 $alternate ^= 1;
7259 print "<td><i>$ref{'age'}</i></td>\n" .
7260 ($curr ? "<td class=\"current_head\">" : "<td>") .
7261 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
7262 -class => "list name"},esc_html($ref{'name'})) .
7263 "</td>\n" .
7264 "<td class=\"link\">" .
7265 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "log") . $barsep .
7266 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
7267 "</td>\n" .
7268 "</tr>";
7270 if (defined $extra) {
7271 print "<tr class=\"extra\">\n" .
7272 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7273 "</tr>\n";
7275 print "</table>\n";
7278 # Display a single remote block
7279 sub git_remote_block {
7280 my ($remote, $rdata, $limit, $head) = @_;
7282 my $heads = $rdata->{'heads'};
7283 my $fetch = $rdata->{'fetch'};
7284 my $push = $rdata->{'push'};
7286 my $urls_table = "<table class=\"projects_list\">\n" ;
7288 if (defined $fetch) {
7289 if ($fetch eq $push) {
7290 $urls_table .= format_repo_url("URL", $fetch);
7291 } else {
7292 $urls_table .= format_repo_url("Fetch&#160;URL", $fetch);
7293 $urls_table .= format_repo_url("Push&#160;URL", $push) if defined $push;
7295 } elsif (defined $push) {
7296 $urls_table .= format_repo_url("Push&#160;URL", $push);
7297 } else {
7298 $urls_table .= format_repo_url("", "No remote URL");
7301 $urls_table .= "</table>\n";
7303 my $dots;
7304 if (defined $limit && $limit < @$heads) {
7305 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
7308 print $urls_table;
7309 git_heads_body($heads, $head, 0, $limit, $dots);
7312 # Display a list of remote names with the respective fetch and push URLs
7313 sub git_remotes_list {
7314 my ($remotedata, $limit) = @_;
7315 print "<table class=\"heads\">\n";
7316 my $alternate = 1;
7317 my @remotes = sort keys %$remotedata;
7319 my $limited = $limit && $limit < @remotes;
7321 $#remotes = $limit - 1 if $limited;
7323 while (my $remote = shift @remotes) {
7324 my $rdata = $remotedata->{$remote};
7325 my $fetch = $rdata->{'fetch'};
7326 my $push = $rdata->{'push'};
7327 if ($alternate) {
7328 print "<tr class=\"dark\">\n";
7329 } else {
7330 print "<tr class=\"light\">\n";
7332 $alternate ^= 1;
7333 print "<td>" .
7334 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
7335 -class=> "list name"},esc_html($remote)) .
7336 "</td>";
7337 print "<td class=\"link\">" .
7338 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
7339 $barsep .
7340 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
7341 "</td>";
7343 print "</tr>\n";
7346 if ($limited) {
7347 print "<tr>\n" .
7348 "<td colspan=\"3\">" .
7349 $cgi->a({-href => href(action=>"remotes")}, "...") .
7350 "</td>\n" . "</tr>\n";
7353 print "</table>";
7356 # Display remote heads grouped by remote, unless there are too many
7357 # remotes, in which case we only display the remote names
7358 sub git_remotes_body {
7359 my ($remotedata, $limit, $head) = @_;
7360 if ($limit and $limit < keys %$remotedata) {
7361 git_remotes_list($remotedata, $limit);
7362 } else {
7363 fill_remote_heads($remotedata);
7364 while (my ($remote, $rdata) = each %$remotedata) {
7365 git_print_section({-class=>"remote", -id=>$remote},
7366 ["remotes", $remote, $remote], sub {
7367 git_remote_block($remote, $rdata, $limit, $head);
7373 sub git_search_message {
7374 my %co = @_;
7376 my $greptype;
7377 if ($searchtype eq 'commit') {
7378 $greptype = "--grep=";
7379 } elsif ($searchtype eq 'author') {
7380 $greptype = "--author=";
7381 } elsif ($searchtype eq 'committer') {
7382 $greptype = "--committer=";
7384 $greptype .= $searchtext;
7385 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
7386 $greptype, '--regexp-ignore-case',
7387 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
7389 my $paging_nav = "<span class=\"paging_nav\">";
7390 if ($page > 0) {
7391 $paging_nav .= tabspan(
7392 $cgi->a({-href => href(-replay=>1, page=>undef)},
7393 "first")) .
7394 $mdotsep . tabspan(
7395 $cgi->a({-href => href(-replay=>1, page=>$page-1),
7396 -accesskey => "p", -title => "Alt-p"}, "prev"));
7397 } else {
7398 $paging_nav .= tabspan("first", 1, 0).${mdotsep}.tabspan("prev", 0, 1);
7400 my $next_link = '';
7401 if ($#commitlist >= 100) {
7402 $next_link = tabspan(
7403 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7404 -accesskey => "n", -title => "Alt-n"}, "next"));
7405 $paging_nav .= "${mdotsep}$next_link";
7406 } else {
7407 $paging_nav .= ${mdotsep}.tabspan("next", 0, 1);
7410 git_header_html();
7412 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav."</span>");
7413 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7414 if ($page == 0 && !@commitlist) {
7415 print "<p>No match.</p>\n";
7416 } else {
7417 git_search_grep_body(\@commitlist, 0, 99, $next_link);
7420 git_footer_html();
7423 sub git_search_changes {
7424 my %co = @_;
7426 local $/ = "\n";
7427 defined(my $fd = git_cmd_pipe '--no-pager', 'log', @diff_opts,
7428 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7429 ($search_use_regexp ? '--pickaxe-regex' : ()))
7430 or die_error(500, "Open git-log failed");
7432 git_header_html();
7434 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7435 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7437 print "<table class=\"pickaxe search\">\n";
7438 my $alternate = 1;
7439 undef %co;
7440 my @files;
7441 while (my $line = to_utf8(scalar <$fd>)) {
7442 chomp $line;
7443 next unless $line;
7445 my %set = parse_difftree_raw_line($line);
7446 if (defined $set{'commit'}) {
7447 # finish previous commit
7448 if (%co) {
7449 print "</td>\n" .
7450 "<td class=\"link\">" .
7451 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
7452 "commit") .
7453 $barsep .
7454 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
7455 hash_base=>$co{'id'})},
7456 "tree") .
7457 "</td>\n" .
7458 "</tr>\n";
7461 if ($alternate) {
7462 print "<tr class=\"dark\">\n";
7463 } else {
7464 print "<tr class=\"light\">\n";
7466 $alternate ^= 1;
7467 %co = parse_commit($set{'commit'});
7468 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
7469 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7470 "<td><i>$author</i></td>\n" .
7471 "<td>" .
7472 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7473 -class => "list subject"},
7474 chop_and_escape_str($co{'title'}, 50) . "<br/>");
7475 } elsif (defined $set{'to_id'}) {
7476 next if ($set{'to_id'} =~ m/^0{40}$/);
7478 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
7479 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
7480 -class => "list"},
7481 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
7482 "<br/>\n";
7485 close $fd;
7487 # finish last commit (warning: repetition!)
7488 if (%co) {
7489 print "</td>\n" .
7490 "<td class=\"link\">" .
7491 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
7492 "commit") .
7493 $barsep .
7494 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
7495 hash_base=>$co{'id'})},
7496 "tree") .
7497 "</td>\n" .
7498 "</tr>\n";
7501 print "</table>\n";
7503 git_footer_html();
7506 sub git_search_files {
7507 my %co = @_;
7509 local $/ = "\n";
7510 defined(my $fd = git_cmd_pipe 'grep', '-n', '-z',
7511 $search_use_regexp ? ('-E', '-i') : '-F',
7512 $searchtext, $co{'tree'})
7513 or die_error(500, "Open git-grep failed");
7515 git_header_html();
7517 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7518 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7520 print "<table class=\"grep_search\">\n";
7521 my $alternate = 1;
7522 my $matches = 0;
7523 my $lastfile = '';
7524 my $file_href;
7525 while (my $line = to_utf8(scalar <$fd>)) {
7526 chomp $line;
7527 my ($file, $lno, $ltext, $binary);
7528 last if ($matches++ > 1000);
7529 if ($line =~ /^Binary file (.+) matches$/) {
7530 $file = $1;
7531 $binary = 1;
7532 } else {
7533 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7534 $file =~ s/^$co{'tree'}://;
7536 if ($file ne $lastfile) {
7537 $lastfile and print "</td></tr>\n";
7538 if ($alternate++) {
7539 print "<tr class=\"dark\">\n";
7540 } else {
7541 print "<tr class=\"light\">\n";
7543 $file_href = href(action=>"blob", hash_base=>$co{'id'},
7544 file_name=>$file);
7545 print "<td class=\"list\">".
7546 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
7547 print "</td><td>\n";
7548 $lastfile = $file;
7550 if ($binary) {
7551 print "<div class=\"binary\">Binary file</div>\n";
7552 } else {
7553 $ltext = untabify($ltext);
7554 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7555 $ltext = esc_html($1, -nbsp=>1);
7556 $ltext .= '<span class="match">';
7557 $ltext .= esc_html($2, -nbsp=>1);
7558 $ltext .= '</span>';
7559 $ltext .= esc_html($3, -nbsp=>1);
7560 } else {
7561 $ltext = esc_html($ltext, -nbsp=>1);
7563 print "<div class=\"pre\">" .
7564 $cgi->a({-href => $file_href.'#l'.$lno,
7565 -class => "linenr"}, sprintf('%4i', $lno)) .
7566 ' ' . $ltext . "</div>\n";
7569 if ($lastfile) {
7570 print "</td></tr>\n";
7571 if ($matches > 1000) {
7572 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7574 } else {
7575 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7577 close $fd;
7579 print "</table>\n";
7581 git_footer_html();
7584 sub git_search_grep_body {
7585 my ($commitlist, $from, $to, $extra) = @_;
7586 $from = 0 unless defined $from;
7587 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7589 print "<table class=\"commit_search\">\n";
7590 my $alternate = 1;
7591 for (my $i = $from; $i <= $to; $i++) {
7592 my %co = %{$commitlist->[$i]};
7593 if (!%co) {
7594 next;
7596 my $commit = $co{'id'};
7597 if ($alternate) {
7598 print "<tr class=\"dark\">\n";
7599 } else {
7600 print "<tr class=\"light\">\n";
7602 $alternate ^= 1;
7603 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7604 format_author_html('td', \%co, 15, 5) .
7605 "<td>" .
7606 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7607 -class => "list subject"},
7608 chop_and_escape_str($co{'title'}, 50) . "<br/>");
7609 my $comment = $co{'comment'};
7610 foreach my $line (@$comment) {
7611 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7612 my ($lead, $match, $trail) = ($1, $2, $3);
7613 $match = chop_str($match, 70, 5, 'center');
7614 my $contextlen = int((80 - length($match))/2);
7615 $contextlen = 30 if ($contextlen > 30);
7616 $lead = chop_str($lead, $contextlen, 10, 'left');
7617 $trail = chop_str($trail, $contextlen, 10, 'right');
7619 $lead = esc_html($lead);
7620 $match = esc_html($match);
7621 $trail = esc_html($trail);
7623 print "$lead<span class=\"match\">$match</span>$trail<br />";
7626 print "</td>\n" .
7627 "<td class=\"link\">" .
7628 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7629 $barsep .
7630 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
7631 $barsep .
7632 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7633 print "</td>\n" .
7634 "</tr>\n";
7636 if (defined $extra) {
7637 print "<tr class=\"extra\">\n" .
7638 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7639 "</tr>\n";
7641 print "</table>\n";
7644 ## ======================================================================
7645 ## ======================================================================
7646 ## actions
7648 sub git_project_list_load {
7649 my $empty_list_ok = shift;
7650 my $order = $input_params{'order'};
7651 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7652 die_error(400, "Unknown order parameter");
7655 my @list = git_get_projects_list($project_filter, $strict_export);
7656 if ($project_filter && (!@list || !gitweb_check_feature('forks'))) {
7657 push @list, { 'path' => "$project_filter.git" }
7658 if is_valid_project("$project_filter.git");
7660 if (!@list) {
7661 die_error(404, "No projects found") unless $empty_list_ok;
7664 return (\@list, $order);
7667 sub git_frontpage {
7668 my ($projlist, $order);
7670 if ($frontpage_no_project_list) {
7671 $project = undef;
7672 $project_filter = undef;
7673 } else {
7674 ($projlist, $order) = git_project_list_load(1);
7676 git_header_html();
7677 if (defined $home_text && -f $home_text) {
7678 print "<div class=\"index_include\">\n";
7679 insert_file($home_text);
7680 print "</div>\n";
7682 git_project_search_form($searchtext, $search_use_regexp);
7683 if ($frontpage_no_project_list) {
7684 my $show_ctags = gitweb_check_feature('ctags');
7685 if ($frontpage_no_project_list == 1 and $show_ctags) {
7686 my @projects = git_get_projects_list($project_filter, $strict_export);
7687 @projects = filter_forks_from_projects_list(\@projects) if gitweb_check_feature('forks');
7688 @projects = fill_project_list_info(\@projects, 'ctags');
7689 my $ctags = git_gather_all_ctags(\@projects);
7690 my $cloud = git_populate_project_tagcloud($ctags, 'project_list');
7691 print git_show_project_tagcloud($cloud, 64);
7693 } else {
7694 git_project_list_body($projlist, $order, undef, undef, undef, undef, undef, 1);
7696 git_footer_html();
7699 sub git_project_list {
7700 my ($projlist, $order) = git_project_list_load();
7701 git_header_html();
7702 if (!$frontpage_no_project_list && defined $home_text && -f $home_text) {
7703 print "<div class=\"index_include\">\n";
7704 insert_file($home_text);
7705 print "</div>\n";
7707 git_project_search_form();
7708 git_project_list_body($projlist, $order, undef, undef, undef, undef, undef, 1);
7709 git_footer_html();
7712 sub git_forks {
7713 my $order = $input_params{'order'};
7714 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7715 die_error(400, "Unknown order parameter");
7718 my $filter = $project;
7719 $filter =~ s/\.git$//;
7720 my @list = git_get_projects_list($filter);
7721 if (!@list) {
7722 die_error(404, "No forks found");
7725 git_header_html();
7726 git_print_page_nav('','');
7727 git_print_header_div('summary', "$project forks");
7728 git_project_list_body(\@list, $order, undef, undef, undef, undef, 'forks');
7729 git_footer_html();
7732 sub git_project_index {
7733 my @projects = git_get_projects_list($project_filter, $strict_export);
7734 if (!@projects) {
7735 die_error(404, "No projects found");
7738 print $cgi->header(
7739 -type => 'text/plain',
7740 -charset => 'utf-8',
7741 -content_disposition => 'inline; filename="index.aux"');
7743 foreach my $pr (@projects) {
7744 if (!exists $pr->{'owner'}) {
7745 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
7748 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7749 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7750 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
7751 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
7752 $path =~ s/ /\+/g;
7753 $owner =~ s/ /\+/g;
7755 print "$path $owner\n";
7759 sub git_summary {
7760 my $descr = git_get_project_description($project) || "none";
7761 my %co = parse_commit("HEAD");
7762 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7763 my $head = $co{'id'};
7764 my $remote_heads = gitweb_check_feature('remote_heads');
7766 my $owner = git_get_project_owner($project);
7767 my $homepage = git_get_project_config('homepage');
7768 my $base_url = git_get_project_config('baseurl');
7770 my $refs = git_get_references();
7771 # These get_*_list functions return one more to allow us to see if
7772 # there are more ...
7773 my @taglist = git_get_tags_list(16);
7774 my @headlist = git_get_heads_list(16);
7775 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
7776 my @forklist;
7777 my $check_forks = gitweb_check_feature('forks');
7779 if ($check_forks) {
7780 # find forks of a project
7781 my $filter = $project;
7782 $filter =~ s/\.git$//;
7783 @forklist = git_get_projects_list($filter);
7784 # filter out forks of forks
7785 @forklist = filter_forks_from_projects_list(\@forklist)
7786 if (@forklist);
7789 git_header_html();
7790 git_print_page_nav('summary','', $head);
7792 if ($check_forks and $project =~ m#/#) {
7793 my $xproject = $project; $xproject =~ s#/[^/]+$#.git#; #
7794 my $r = $cgi->a({-href=> href(project => $xproject, action => 'summary')}, $xproject);
7795 print <<EOT;
7796 <div class="forkinfo">
7797 This project is a fork of the $r project. If you have that one
7798 already cloned locally, you can use
7799 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7800 to save bandwidth during cloning.
7801 </div>
7805 print "<div class=\"title\">&#160;</div>\n";
7806 print "<table class=\"projects_list\">\n" .
7807 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
7808 if ($homepage) {
7809 print "<tr id=\"metadata_homepage\"><td>homepage&#160;URL</td><td>" . $cgi->a({-href => $homepage}, $homepage) . "</td></tr>\n";
7811 if ($base_url) {
7812 print "<tr id=\"metadata_baseurl\"><td>repository&#160;URL</td><td>" . esc_html($base_url) . "</td></tr>\n";
7814 if ($owner and not $omit_owner) {
7815 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7816 ? $cgi->a({-href => $owner_link_hook->($owner)}, email_obfuscate($owner))
7817 : email_obfuscate($owner)) . "</td></tr>\n";
7819 if (defined $cd{'rfc2822'}) {
7820 print "<tr id=\"metadata_lchange\"><td>last&#160;change</td>" .
7821 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
7823 print format_lastrefresh_row(), "\n";
7825 # use per project git URL list in $projectroot/$project/cloneurl
7826 # or make project git URL from git base URL and project name
7827 my $url_tag = $base_url ? "mirror&#160;URL" : "URL";
7828 my @url_list = git_get_project_url_list($project);
7829 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
7830 foreach my $git_url (@url_list) {
7831 next unless $git_url;
7832 print format_repo_url($url_tag, $git_url);
7833 $url_tag = "";
7835 @url_list = map { "$_/$project" } @git_base_push_urls;
7836 if ((git_get_project_config("showpush", '--bool')||'false') eq "true" ||
7837 -f "$projectroot/$project/.nofetch") {
7838 $url_tag = "push&#160;URL";
7839 foreach my $git_push_url (@url_list) {
7840 next unless $git_push_url;
7841 my $hint = $https_hint_html && $git_push_url =~ /^https:/i ?
7842 "&#160;$https_hint_html" : '';
7843 print "<tr class=\"metadata_pushurl\"><td>$url_tag</td><td>$git_push_url$hint</td></tr>\n";
7844 $url_tag = "";
7848 if (defined($git_base_bundles_url) && -d "$projectroot/$project/bundles") {
7849 my $projname = $project;
7850 $projname =~ s|^.*/||;
7851 my $url = "$git_base_bundles_url/$project/bundles";
7852 print format_repo_url(
7853 "bundle&#160;info",
7854 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7857 # Tag cloud
7858 my $show_ctags = gitweb_check_feature('ctags');
7859 if ($show_ctags) {
7860 my $ctags = git_get_project_ctags($project);
7861 if (%$ctags || $show_ctags !~ /^\d+$/) {
7862 # without ability to add tags, don't show if there are none
7863 my $cloud = git_populate_project_tagcloud($ctags, 'project_list');
7864 print "<tr id=\"metadata_ctags\">" .
7865 "<td style=\"vertical-align:middle\">content&#160;tags<br />";
7866 print "</td>\n<td>" unless %$ctags;
7867 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7868 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7869 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7870 unless $show_ctags =~ /^\d+$/;
7871 print "</td>\n<td>" if %$ctags;
7872 print git_show_project_tagcloud($cloud, 48)."</td>" .
7873 "</tr>\n";
7877 print "</table>\n";
7879 # If XSS prevention is on, we don't include README.html.
7880 # TODO: Allow a readme in some safe format.
7881 if (!$prevent_xss) {
7882 my $readme = -s "$projectroot/$project/README.html"
7883 ? collect_html_file("$projectroot/$project/README.html")
7884 : collect_output($git_automatic_readme_html, "$projectroot/$project");
7885 if (defined($readme)) {
7886 $readme =~ s/^\s+//s;
7887 $readme =~ s/\s+$//s;
7888 print "<div class=\"title\">readme</div>\n",
7889 "<div id=\"readme\" class=\"readme\">\n",
7890 $readme,
7891 "\n</div>\n"
7892 if $readme ne '';
7896 # we need to request one more than 16 (0..15) to check if
7897 # those 16 are all
7898 my @commitlist = $head ? parse_commits($head, 17) : ();
7899 if (@commitlist) {
7900 git_print_header_div('shortlog');
7901 git_shortlog_body(\@commitlist, 0, 15, $refs,
7902 $#commitlist <= 15 ? undef :
7903 $cgi->a({-href => href(action=>"shortlog")}, "..."));
7906 if (@taglist) {
7907 git_print_header_div('tags');
7908 git_tags_body(\@taglist, 0, 15,
7909 $#taglist <= 15 ? undef :
7910 $cgi->a({-href => href(action=>"tags")}, "..."));
7913 if (@headlist) {
7914 git_print_header_div('heads');
7915 git_heads_body(\@headlist, $head, 0, 15,
7916 $#headlist <= 15 ? undef :
7917 $cgi->a({-href => href(action=>"heads")}, "..."));
7920 if (%remotedata) {
7921 git_print_header_div('remotes');
7922 git_remotes_body(\%remotedata, 15, $head);
7925 if (@forklist) {
7926 git_print_header_div('forks');
7927 git_project_list_body(\@forklist, 'age', 0, 15,
7928 $#forklist <= 15 ? undef :
7929 $cgi->a({-href => href(action=>"forks")}, "..."),
7930 'no_header', 'forks');
7933 git_footer_html();
7936 sub git_tag {
7937 my %tag = parse_tag($hash);
7939 if (! %tag) {
7940 die_error(404, "Unknown tag object");
7943 my $fullhash;
7944 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
7945 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
7947 my $head = git_get_head_hash($project);
7948 git_header_html();
7949 git_print_page_nav('','', $head,undef,$head);
7950 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
7951 print "<div class=\"title_text\">\n" .
7952 "<table class=\"object_header\">\n" .
7953 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
7954 "<tr>\n" .
7955 "<td>object</td>\n" .
7956 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
7957 $tag{'object'}) . "</td>\n" .
7958 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
7959 $tag{'type'}) . "</td>\n" .
7960 "</tr>\n";
7961 if (defined($tag{'author'})) {
7962 git_print_authorship_rows(\%tag, 'author');
7964 print "</table>\n\n" .
7965 "</div>\n";
7966 print "<div class=\"page_body\">";
7967 my $comment = $tag{'comment'};
7968 foreach my $line (@$comment) {
7969 chomp $line;
7970 print esc_html($line, -nbsp=>1) . "<br/>\n";
7972 print "</div>\n";
7973 git_footer_html();
7976 sub git_blame_common {
7977 my $format = shift || 'porcelain';
7978 if ($format eq 'porcelain' && $input_params{'javascript'}) {
7979 $format = 'incremental';
7980 $action = 'blame_incremental'; # for page title etc
7983 # permissions
7984 gitweb_check_feature('blame')
7985 or die_error(403, "Blame view not allowed");
7987 # error checking
7988 die_error(400, "No file name given") unless $file_name;
7989 $hash_base ||= git_get_head_hash($project);
7990 die_error(404, "Couldn't find base commit") unless $hash_base;
7991 my %co = parse_commit($hash_base)
7992 or die_error(404, "Commit not found");
7993 my $ftype = "blob";
7994 if (!defined $hash) {
7995 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
7996 or die_error(404, "Error looking up file");
7997 } else {
7998 $ftype = git_get_type($hash);
7999 if ($ftype !~ "blob") {
8000 die_error(400, "Object is not a blob");
8004 my $fd;
8005 if ($format eq 'incremental') {
8006 # get file contents (as base)
8007 defined($fd = git_cmd_pipe 'cat-file', 'blob', $hash)
8008 or die_error(500, "Open git-cat-file failed");
8009 } elsif ($format eq 'data') {
8010 # run git-blame --incremental
8011 defined($fd = git_cmd_pipe "blame", "--incremental",
8012 $hash_base, "--", $file_name)
8013 or die_error(500, "Open git-blame --incremental failed");
8014 } else {
8015 # run git-blame --porcelain
8016 defined($fd = git_cmd_pipe "blame", '-p',
8017 $hash_base, '--', $file_name)
8018 or die_error(500, "Open git-blame --porcelain failed");
8021 # incremental blame data returns early
8022 if ($format eq 'data') {
8023 print $cgi->header(
8024 -type=>"text/plain", -charset => "utf-8",
8025 -status=> "200 OK");
8026 local $| = 1; # output autoflush
8027 while (<$fd>) {
8028 print to_utf8($_);
8030 close $fd
8031 or print "ERROR $!\n";
8033 print 'END';
8034 if (defined $t0 && gitweb_check_feature('timed')) {
8035 print ' '.
8036 tv_interval($t0, [ gettimeofday() ]).
8037 ' '.$number_of_git_cmds;
8039 print "\n";
8041 return;
8044 # page header
8045 git_header_html();
8046 my $formats_nav = tabspan(
8047 $cgi->a({-href => href(action=>"blob", -replay=>1)},
8048 "blob"));
8049 $formats_nav .=
8050 $barsep . tabspan(
8051 $cgi->a({-href => href(action=>"history", -replay=>1)},
8052 "history")) .
8053 $barsep . tabspan(
8054 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
8055 "HEAD"));
8056 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8057 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
8058 git_print_page_path($file_name, $ftype, $hash_base);
8060 # page body
8061 if ($format eq 'incremental') {
8062 print "<noscript>\n<div class=\"error\"><center><b>\n".
8063 "This page requires JavaScript to run.\n Use ".
8064 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
8065 'this page').
8066 " instead.\n".
8067 "</b></center></div>\n</noscript>\n";
8069 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
8072 print qq!<div class="page_body">\n!;
8073 print qq!<div id="progress_info">... / ...</div>\n!
8074 if ($format eq 'incremental');
8075 print qq!<table id="blame_table" class="blame" width="100%">\n!.
8076 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8077 qq!<thead>\n!.
8078 qq!<tr><th nowrap="nowrap" style="white-space:nowrap">!.
8079 qq!Commit&#160;<a href="javascript:extra_blame_columns()" id="columns_expander" !.
8080 qq!title="toggles blame author information display">[+]</a></th>!.
8081 qq!<th class="extra_column">Author</th><th class="extra_column">Date</th>!.
8082 qq!<th>Line</th><th width="100%">Data</th></tr>\n!.
8083 qq!</thead>\n!.
8084 qq!<tbody>\n!;
8086 my @rev_color = qw(light dark);
8087 my $num_colors = scalar(@rev_color);
8088 my $current_color = 0;
8090 if ($format eq 'incremental') {
8091 my $color_class = $rev_color[$current_color];
8093 #contents of a file
8094 my $linenr = 0;
8095 LINE:
8096 while (my $line = to_utf8(scalar <$fd>)) {
8097 chomp $line;
8098 $linenr++;
8100 print qq!<tr id="l$linenr" class="$color_class">!.
8101 qq!<td class="sha1"><a href=""> </a></td>!.
8102 qq!<td class="extra_column" nowrap="nowrap"></td>!.
8103 qq!<td class="extra_column" nowrap="nowrap"></td>!.
8104 qq!<td class="linenr">!.
8105 qq!<a class="linenr" href="">$linenr</a></td>!;
8106 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
8107 print qq!</tr>\n!;
8110 } else { # porcelain, i.e. ordinary blame
8111 my %metainfo = (); # saves information about commits
8113 # blame data
8114 LINE:
8115 while (my $line = to_utf8(scalar <$fd>)) {
8116 chomp $line;
8117 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8118 # no <lines in group> for subsequent lines in group of lines
8119 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8120 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8121 if (!exists $metainfo{$full_rev}) {
8122 $metainfo{$full_rev} = { 'nprevious' => 0 };
8124 my $meta = $metainfo{$full_rev};
8125 my $data;
8126 while ($data = to_utf8(scalar <$fd>)) {
8127 chomp $data;
8128 last if ($data =~ s/^\t//); # contents of line
8129 if ($data =~ /^(\S+)(?: (.*))?$/) {
8130 $meta->{$1} = $2 unless exists $meta->{$1};
8132 if ($data =~ /^previous /) {
8133 $meta->{'nprevious'}++;
8136 my $short_rev = substr($full_rev, 0, 8);
8137 my $author = $meta->{'author'};
8138 my %date =
8139 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
8140 my $date = $date{'iso-tz'};
8141 if ($group_size) {
8142 $current_color = ($current_color + 1) % $num_colors;
8144 my $tr_class = $rev_color[$current_color];
8145 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8146 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8147 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8148 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8149 if ($group_size) {
8150 my $rowspan = $group_size > 1 ? " rowspan=\"$group_size\"" : "";
8151 print "<td class=\"sha1\"";
8152 print " title=\"". esc_html($author) . ", $date\"";
8153 print "$rowspan>";
8154 print $cgi->a({-href => href(action=>"commit",
8155 hash=>$full_rev,
8156 file_name=>$file_name)},
8157 esc_html($short_rev));
8158 if ($group_size >= 2) {
8159 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8160 if (@author_initials) {
8161 print "<br />" .
8162 esc_html(join('', @author_initials));
8163 # or join('.', ...)
8166 print "</td>\n";
8167 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html($author) . "</td>";
8168 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8170 # 'previous' <sha1 of parent commit> <filename at commit>
8171 if (exists $meta->{'previous'} &&
8172 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8173 $meta->{'parent'} = $1;
8174 $meta->{'file_parent'} = unquote($2);
8176 my $linenr_commit =
8177 exists($meta->{'parent'}) ?
8178 $meta->{'parent'} : $full_rev;
8179 my $linenr_filename =
8180 exists($meta->{'file_parent'}) ?
8181 $meta->{'file_parent'} : unquote($meta->{'filename'});
8182 my $blamed = href(action => 'blame',
8183 file_name => $linenr_filename,
8184 hash_base => $linenr_commit);
8185 print "<td class=\"linenr\">";
8186 print $cgi->a({ -href => "$blamed#l$orig_lineno",
8187 -class => "linenr" },
8188 esc_html($lineno));
8189 print "</td>";
8190 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
8191 print "</tr>\n";
8192 } # end while
8196 # footer
8197 print "</tbody>\n".
8198 "</table>\n"; # class="blame"
8199 print "</div>\n"; # class="blame_body"
8200 close $fd
8201 or print "Reading blob failed\n";
8203 git_footer_html();
8206 sub git_blame {
8207 git_blame_common();
8210 sub git_blame_incremental {
8211 git_blame_common('incremental');
8214 sub git_blame_data {
8215 git_blame_common('data');
8218 sub git_tags {
8219 my $head = git_get_head_hash($project);
8220 git_header_html();
8221 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
8222 git_print_header_div('summary', $project);
8224 my @tagslist = git_get_tags_list();
8225 if (@tagslist) {
8226 git_tags_body(\@tagslist);
8228 git_footer_html();
8231 sub git_refs {
8232 my $order = $input_params{'order'};
8233 if (defined $order && $order !~ m/age|name/) {
8234 die_error(400, "Unknown order parameter");
8237 my $head = git_get_head_hash($project);
8238 git_header_html();
8239 git_print_page_nav('','', $head,undef,$head,format_ref_views('refs'));
8240 git_print_header_div('summary', $project);
8242 my @refslist = git_get_tags_list(undef, 1, $order);
8243 if (@refslist) {
8244 git_tags_body(\@refslist, undef, undef, undef, $head, 1, $order);
8246 git_footer_html();
8249 sub git_heads {
8250 my $head = git_get_head_hash($project);
8251 git_header_html();
8252 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
8253 git_print_header_div('summary', $project);
8255 my @headslist = git_get_heads_list();
8256 if (@headslist) {
8257 git_heads_body(\@headslist, $head);
8259 git_footer_html();
8262 # used both for single remote view and for list of all the remotes
8263 sub git_remotes {
8264 gitweb_check_feature('remote_heads')
8265 or die_error(403, "Remote heads view is disabled");
8267 my $head = git_get_head_hash($project);
8268 my $remote = $input_params{'hash'};
8270 my $remotedata = git_get_remotes_list($remote);
8271 die_error(500, "Unable to get remote information") unless defined $remotedata;
8273 unless (%$remotedata) {
8274 die_error(404, defined $remote ?
8275 "Remote $remote not found" :
8276 "No remotes found");
8279 git_header_html(undef, undef, -action_extra => $remote);
8280 git_print_page_nav('', '', $head, undef, $head,
8281 format_ref_views($remote ? '' : 'remotes'));
8283 fill_remote_heads($remotedata);
8284 if (defined $remote) {
8285 git_print_header_div('remotes', "$remote remote for $project");
8286 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
8287 } else {
8288 git_print_header_div('summary', "$project remotes");
8289 git_remotes_body($remotedata, undef, $head);
8292 git_footer_html();
8295 sub git_blob_plain {
8296 my $type = shift;
8297 my $expires;
8299 if (!defined $hash) {
8300 if (defined $file_name) {
8301 my $base = $hash_base || git_get_head_hash($project);
8302 $hash = git_get_hash_by_path($base, $file_name, "blob")
8303 or die_error(404, "Cannot find file");
8304 } else {
8305 die_error(400, "No file name defined");
8307 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8308 # blobs defined by non-textual hash id's can be cached
8309 $expires = "+1d";
8312 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
8313 or die_error(500, "Open git-cat-file blob '$hash' failed");
8314 binmode($fd);
8316 # content-type (can include charset)
8317 my $leader;
8318 ($type, $leader) = blob_contenttype($fd, $file_name, $type);
8320 # "save as" filename, even when no $file_name is given
8321 my $save_as = "$hash";
8322 if (defined $file_name) {
8323 $save_as = $file_name;
8324 } elsif ($type =~ m/^text\//) {
8325 $save_as .= '.txt';
8328 # With XSS prevention on, blobs of all types except a few known safe
8329 # ones are served with "Content-Disposition: attachment" to make sure
8330 # they don't run in our security domain. For certain image types,
8331 # blob view writes an <img> tag referring to blob_plain view, and we
8332 # want to be sure not to break that by serving the image as an
8333 # attachment (though Firefox 3 doesn't seem to care).
8334 my $sandbox = $prevent_xss &&
8335 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8337 # serve text/* as text/plain
8338 if ($prevent_xss &&
8339 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8340 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
8341 my $rest = $1;
8342 $rest = defined $rest ? $rest : '';
8343 $type = "text/plain$rest";
8346 print $cgi->header(
8347 -type => $type,
8348 -expires => $expires,
8349 -content_disposition =>
8350 ($sandbox ? 'attachment' : 'inline')
8351 . '; filename="' . $save_as . '"');
8352 binmode STDOUT, ':raw';
8353 $fcgi_raw_mode = 1;
8354 print $leader if defined $leader;
8355 my $buf;
8356 while (read($fd, $buf, 32768)) {
8357 print $buf;
8359 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
8360 $fcgi_raw_mode = 0;
8361 close $fd;
8364 sub git_blob {
8365 my $expires;
8367 my $fullhash;
8368 if (!defined $hash) {
8369 if (defined $file_name) {
8370 my $base = $hash_base || git_get_head_hash($project);
8371 $hash = git_get_hash_by_path($base, $file_name, "blob")
8372 or die_error(404, "Cannot find file");
8373 $fullhash = $hash;
8374 } else {
8375 die_error(400, "No file name defined");
8377 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8378 # blobs defined by non-textual hash id's can be cached
8379 $expires = "+1d";
8380 $fullhash = $hash;
8382 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
8384 my $have_blame = gitweb_check_feature('blame');
8385 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
8386 or die_error(500, "Couldn't cat $file_name, $hash");
8387 binmode($fd);
8388 my $mimetype = blob_mimetype($fd, $file_name);
8389 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8390 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
8391 close $fd;
8392 return git_blob_plain($mimetype);
8394 # we can have blame only for text/* mimetype
8395 $have_blame &&= ($mimetype =~ m!^text/!);
8397 my $highlight = gitweb_check_feature('highlight') && defined $highlight_bin;
8398 my $syntax = guess_file_syntax($fd, $mimetype, $file_name) if $highlight;
8399 my $highlight_mode_active;
8400 ($fd, $highlight_mode_active) = run_highlighter($fd, $syntax) if $syntax;
8402 git_header_html(undef, $expires);
8403 my $formats_nav = '';
8404 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
8405 if (defined $file_name) {
8406 if ($have_blame) {
8407 $formats_nav .= tabspan(
8408 $cgi->a({-href => href(action=>"blame", -replay=>1),
8409 -class => "blamelink"},
8410 "blame")) .
8411 $barsep;
8413 $formats_nav .= tabspan(
8414 $cgi->a({-href => href(action=>"history", -replay=>1)},
8415 "history")) .
8416 $barsep . tabspan(
8417 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
8418 "raw")) .
8419 $barsep . tabspan(
8420 $cgi->a({-href => href(action=>"blob",
8421 hash_base=>"HEAD", file_name=>$file_name)},
8422 "HEAD"));
8423 } else {
8424 $formats_nav .= tabspan(
8425 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
8426 "raw"));
8428 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8429 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
8430 } else {
8431 print "<div class=\"page_nav\">\n" .
8432 "<br/><br/></div>\n" .
8433 "<div class=\"title\">".esc_html($hash)."</div>\n";
8435 git_print_page_path($file_name, "blob", $hash_base);
8436 print "<div class=\"title_text\">\n" .
8437 "<table class=\"object_header\">\n";
8438 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8439 print "</table>".
8440 "</div>\n";
8441 print "<div class=\"page_body\">\n";
8442 if ($mimetype =~ m!^image/!) {
8443 print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;
8444 if ($file_name) {
8445 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
8447 print qq! src="! .
8448 href(action=>"blob_plain", hash=>$hash,
8449 hash_base=>$hash_base, file_name=>$file_name) .
8450 qq!" />\n!;
8451 } else {
8452 my $nr;
8453 while (my $line = to_utf8(scalar <$fd>)) {
8454 chomp $line;
8455 $nr++;
8456 $line = untabify($line);
8457 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
8458 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
8459 $highlight_mode_active ? sanitize($line) : esc_html($line, -nbsp=>1);
8462 close $fd
8463 or print "Reading blob failed.\n";
8464 print "</div>";
8465 git_footer_html();
8468 sub git_tree {
8469 my $fullhash;
8470 if (!defined $hash_base) {
8471 $hash_base = "HEAD";
8473 if (!defined $hash) {
8474 if (defined $file_name) {
8475 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
8476 $fullhash = $hash;
8477 } else {
8478 $hash = $hash_base;
8481 die_error(404, "No such tree") unless defined($hash);
8482 $fullhash = $hash if !$fullhash && $hash =~ m/^[0-9a-fA-F]{40}$/;
8483 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
8485 my $show_sizes = gitweb_check_feature('show-sizes');
8486 my $have_blame = gitweb_check_feature('blame');
8488 my @entries = ();
8490 local $/ = "\0";
8491 defined(my $fd = git_cmd_pipe "ls-tree", '-z',
8492 ($show_sizes ? '-l' : ()), @extra_options, $hash)
8493 or die_error(500, "Open git-ls-tree failed");
8494 @entries = map { chomp; to_utf8($_) } <$fd>;
8495 close $fd
8496 or die_error(404, "Reading tree failed");
8499 git_header_html();
8500 my $basedir = '';
8501 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
8502 my $refs = git_get_references();
8503 my $ref = format_ref_marker($refs, $co{'id'});
8504 my @views_nav = ();
8505 if (defined $file_name) {
8506 push @views_nav,
8507 tabspan($cgi->a({-href => href(action=>"history", -replay=>1)},
8508 "history")),
8509 tabspan($cgi->a({-href => href(action=>"tree",
8510 hash_base=>"HEAD", file_name=>$file_name)},
8511 "HEAD")),
8513 my $snapshot_links = format_snapshot_links($hash);
8514 if (defined $snapshot_links) {
8515 # FIXME: Should be available when we have no hash base as well.
8516 push @views_nav, $snapshot_links;
8518 git_print_page_nav('tree','', $hash_base, undef, undef,
8519 join($barsep, @views_nav));
8520 git_print_header_div('commit', esc_html($co{'title'}), $hash_base, undef, $ref);
8521 } else {
8522 undef $hash_base;
8523 print "<div class=\"page_nav\">\n";
8524 print "<br/><br/></div>\n";
8525 print "<div class=\"title\">".esc_html($hash)."</div>\n";
8527 if (defined $file_name) {
8528 $basedir = $file_name;
8529 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8530 $basedir .= '/';
8532 git_print_page_path($file_name, 'tree', $hash_base);
8534 print "<div class=\"title_text\">\n" .
8535 "<table class=\"object_header\">\n";
8536 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8537 print "</table>".
8538 "</div>\n";
8539 print "<div class=\"page_body\">\n";
8540 print "<table class=\"tree\">\n";
8541 my $alternate = 1;
8542 # '..' (top directory) link if possible
8543 if (defined $hash_base &&
8544 defined $file_name && $file_name =~ m![^/]+$!) {
8545 if ($alternate) {
8546 print "<tr class=\"dark\">\n";
8547 } else {
8548 print "<tr class=\"light\">\n";
8550 $alternate ^= 1;
8552 my $up = $file_name;
8553 $up =~ s!/?[^/]+$!!;
8554 undef $up unless $up;
8555 # based on git_print_tree_entry
8556 print '<td class="mode">' . mode_str('040000') . "</td>\n";
8557 print '<td class="size">&#160;</td>'."\n" if $show_sizes;
8558 print '<td class="list">';
8559 print $cgi->a({-href => href(action=>"tree",
8560 hash_base=>$hash_base,
8561 file_name=>$up)},
8562 "..");
8563 print "</td>\n";
8564 print "<td class=\"link\"></td>\n";
8566 print "</tr>\n";
8568 foreach my $line (@entries) {
8569 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
8571 if ($alternate) {
8572 print "<tr class=\"dark\">\n";
8573 } else {
8574 print "<tr class=\"light\">\n";
8576 $alternate ^= 1;
8578 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
8580 print "</tr>\n";
8582 print "</table>\n" .
8583 "</div>";
8584 git_footer_html();
8587 sub sanitize_for_filename {
8588 my $name = shift;
8590 $name =~ s!/!-!g;
8591 $name =~ s/[^[:alnum:]_.-]//g;
8593 return $name;
8596 sub snapshot_name {
8597 my ($project, $hash) = @_;
8599 # path/to/project.git -> project
8600 # path/to/project/.git -> project
8601 my $name = to_utf8($project);
8602 $name =~ s,([^/])/*\.git$,$1,;
8603 $name = sanitize_for_filename(basename($name));
8605 my $ver = $hash;
8606 if ($hash =~ /^[0-9a-fA-F]+$/) {
8607 # shorten SHA-1 hash
8608 my $full_hash = git_get_full_hash($project, $hash);
8609 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8610 $ver = git_get_short_hash($project, $hash);
8612 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8613 # tags don't need shortened SHA-1 hash
8614 $ver = $1;
8615 } else {
8616 # branches and other need shortened SHA-1 hash
8617 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
8618 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8619 my $ref_dir = (defined $1) ? $1 : '';
8620 $ver = $2;
8622 $ref_dir = sanitize_for_filename($ref_dir);
8623 # for refs neither in heads nor remotes we want to
8624 # add a ref dir to archive name
8625 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8626 $ver = $ref_dir . '-' . $ver;
8629 $ver .= '-' . git_get_short_hash($project, $hash);
8631 # special case of sanitization for filename - we change
8632 # slashes to dots instead of dashes
8633 # in case of hierarchical branch names
8634 $ver =~ s!/!.!g;
8635 $ver =~ s/[^[:alnum:]_.-]//g;
8637 # name = project-version_string
8638 $name = "$name-$ver";
8640 return wantarray ? ($name, $name) : $name;
8643 sub exit_if_unmodified_since {
8644 my ($latest_epoch) = @_;
8645 our $cgi;
8647 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8648 if (defined $if_modified) {
8649 my $since;
8650 if (eval { require HTTP::Date; 1; }) {
8651 $since = HTTP::Date::str2time($if_modified);
8652 } elsif (eval { require Time::ParseDate; 1; }) {
8653 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
8655 if (defined $since && $latest_epoch <= $since) {
8656 my %latest_date = parse_date($latest_epoch);
8657 print $cgi->header(
8658 -last_modified => $latest_date{'rfc2822'},
8659 -status => '304 Not Modified');
8660 CORE::die;
8665 sub git_snapshot {
8666 my $format = $input_params{'snapshot_format'};
8667 if (!@snapshot_fmts) {
8668 die_error(403, "Snapshots not allowed");
8670 # default to first supported snapshot format
8671 $format ||= $snapshot_fmts[0];
8672 if ($format !~ m/^[a-z0-9]+$/) {
8673 die_error(400, "Invalid snapshot format parameter");
8674 } elsif (!exists($known_snapshot_formats{$format})) {
8675 die_error(400, "Unknown snapshot format");
8676 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8677 die_error(403, "Snapshot format not allowed");
8678 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8679 die_error(403, "Unsupported snapshot format");
8682 my $type = git_get_type("$hash^{}");
8683 if (!$type) {
8684 die_error(404, 'Object does not exist');
8685 } elsif ($type eq 'blob') {
8686 die_error(400, 'Object is not a tree-ish');
8689 my ($name, $prefix) = snapshot_name($project, $hash);
8690 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8692 my %co = parse_commit($hash);
8693 exit_if_unmodified_since($co{'committer_epoch'}) if %co;
8695 my @cmd = (
8696 git_cmd(), 'archive',
8697 "--format=$known_snapshot_formats{$format}{'format'}",
8698 "--prefix=$prefix/", $hash);
8699 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8700 @cmd = ($posix_shell_bin, '-c', quote_command(@cmd) .
8701 ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}));
8704 $filename =~ s/(["\\])/\\$1/g;
8705 my %latest_date;
8706 if (%co) {
8707 %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
8710 print $cgi->header(
8711 -type => $known_snapshot_formats{$format}{'type'},
8712 -content_disposition => 'inline; filename="' . $filename . '"',
8713 %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
8714 -status => '200 OK');
8716 defined(my $fd = cmd_pipe @cmd)
8717 or die_error(500, "Execute git-archive failed");
8718 binmode($fd);
8719 binmode STDOUT, ':raw';
8720 $fcgi_raw_mode = 1;
8721 my $buf;
8722 while (read($fd, $buf, 32768)) {
8723 print $buf;
8725 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
8726 $fcgi_raw_mode = 0;
8727 close $fd;
8730 sub git_log_generic {
8731 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8733 my $head = git_get_head_hash($project);
8734 if (!defined $base) {
8735 $base = $head;
8737 if (!defined $page) {
8738 $page = 0;
8740 my $refs = git_get_references();
8742 my $commit_hash = $base;
8743 if (defined $parent) {
8744 $commit_hash = "$parent..$base";
8746 my @commitlist =
8747 parse_commits($commit_hash, 101, (100 * $page),
8748 defined $file_name ? ($file_name, "--full-history") : ());
8750 my $ftype;
8751 if (!defined $file_hash && defined $file_name) {
8752 # some commits could have deleted file in question,
8753 # and not have it in tree, but one of them has to have it
8754 for (my $i = 0; $i < @commitlist; $i++) {
8755 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
8756 last if defined $file_hash;
8759 if (defined $file_hash) {
8760 $ftype = git_get_type($file_hash);
8762 if (defined $file_name && !defined $ftype) {
8763 die_error(500, "Unknown type of object");
8765 my %co;
8766 if (defined $file_name) {
8767 %co = parse_commit($base)
8768 or die_error(404, "Unknown commit object");
8772 my $next_link = '';
8773 if ($#commitlist >= 100) {
8774 $next_link =
8775 $cgi->a({-href => href(-replay=>1, page=>$page+1),
8776 -accesskey => "n", -title => "Alt-n"}, "next");
8778 my $extra = '';
8779 my ($patch_max) = gitweb_get_feature('patches');
8780 if ($patch_max && !defined $file_name) {
8781 if ($patch_max < 0 || @commitlist <= $patch_max) {
8782 $extra = $cgi->a({-href => href(action=>"patches", -replay=>1)},
8783 "patches");
8786 my $paging_nav = format_log_nav($fmt_name, $page, $#commitlist >= 100, $extra);
8789 local $action = 'fulllog';
8790 git_header_html();
8792 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8793 if (defined $file_name) {
8794 git_print_header_div('commit', esc_html($co{'title'}), $base);
8795 } else {
8796 git_print_header_div('summary', $project)
8798 git_print_page_path($file_name, $ftype, $hash_base)
8799 if (defined $file_name);
8801 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
8802 $file_name, $file_hash, $ftype);
8804 git_footer_html();
8807 sub git_log {
8808 git_log_generic('log', \&git_log_body,
8809 $hash, $hash_parent);
8812 sub git_commit {
8813 $hash ||= $hash_base || "HEAD";
8814 my %co = parse_commit($hash)
8815 or die_error(404, "Unknown commit object");
8817 my $parent = $co{'parent'};
8818 my $parents = $co{'parents'}; # listref
8820 # we need to prepare $formats_nav before any parameter munging
8821 my $formats_nav;
8822 if (!defined $parent) {
8823 # --root commitdiff
8824 $formats_nav .= '<span class="parents none">(initial)</span>';
8825 } elsif (@$parents == 1) {
8826 # single parent commit
8827 $formats_nav .=
8828 '<span class="parents single">(parent:&#160;' .
8829 $cgi->a({-href => href(action=>"commit",
8830 hash=>$parent)},
8831 esc_html(substr($parent, 0, 7))) .
8832 ')</span>';
8833 } else {
8834 # merge commit
8835 $formats_nav .=
8836 '<span class="parents multiple">(merge:&#160;' .
8837 join(' ', map {
8838 $cgi->a({-href => href(action=>"commit",
8839 hash=>$_)},
8840 esc_html(substr($_, 0, 7)));
8841 } @$parents ) .
8842 ')</span>';
8844 if (gitweb_check_feature('patches') && @$parents <= 1) {
8845 $formats_nav .= $barsep . tabspan(
8846 $cgi->a({-href => href(action=>"patch", -replay=>1)},
8847 "patch"));
8850 if (!defined $parent) {
8851 $parent = "--root";
8853 my @difftree;
8854 defined(my $fd = git_cmd_pipe "diff-tree", '-r', "--no-commit-id",
8855 @diff_opts,
8856 (@$parents <= 1 ? $parent : '-c'),
8857 $hash, "--")
8858 or die_error(500, "Open git-diff-tree failed");
8859 @difftree = map { chomp; to_utf8($_) } <$fd>;
8860 close $fd or die_error(404, "Reading git-diff-tree failed");
8862 # non-textual hash id's can be cached
8863 my $expires;
8864 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8865 $expires = "+1d";
8867 my $refs = git_get_references();
8868 my $ref = format_ref_marker($refs, $co{'id'});
8870 git_header_html(undef, $expires);
8871 git_print_page_nav('commit', '',
8872 $hash, $co{'tree'}, $hash,
8873 $formats_nav);
8875 if (defined $co{'parent'}) {
8876 git_print_header_div('commitdiff', esc_html($co{'title'}), $hash, undef, $ref);
8877 } else {
8878 git_print_header_div('tree', esc_html($co{'title'}), $co{'tree'}, $hash, $ref);
8880 print "<div class=\"title_text\">\n" .
8881 "<table class=\"object_header\">\n";
8882 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8883 git_print_authorship_rows(\%co);
8884 print "<tr>" .
8885 "<td>tree</td>" .
8886 "<td class=\"sha1\">" .
8887 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
8888 class => "list"}, $co{'tree'}) .
8889 "</td>" .
8890 "<td class=\"link\">" .
8891 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
8892 "tree");
8893 my $snapshot_links = format_snapshot_links($hash);
8894 if (defined $snapshot_links) {
8895 print $barsep . $snapshot_links;
8897 print "</td>" .
8898 "</tr>\n";
8900 foreach my $par (@$parents) {
8901 print "<tr>" .
8902 "<td>parent</td>" .
8903 "<td class=\"sha1\">" .
8904 $cgi->a({-href => href(action=>"commit", hash=>$par),
8905 class => "list"}, $par) .
8906 "</td>" .
8907 "<td class=\"link\">" .
8908 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
8909 $barsep .
8910 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
8911 "</td>" .
8912 "</tr>\n";
8914 print "</table>".
8915 "</div>\n";
8917 print "<div class=\"page_body\">\n";
8918 git_print_log($co{'comment'});
8919 print "</div>\n";
8921 git_difftree_body(\@difftree, $hash, @$parents);
8923 git_footer_html();
8926 sub git_object {
8927 # object is defined by:
8928 # - hash or hash_base alone
8929 # - hash_base and file_name
8930 my $type;
8932 # - hash or hash_base alone
8933 if ($hash || ($hash_base && !defined $file_name)) {
8934 my $object_id = $hash || $hash_base;
8936 defined(my $fd = git_cmd_pipe 'cat-file', '-t', $object_id)
8937 or die_error(404, "Object does not exist");
8938 $type = <$fd>;
8939 chomp $type;
8940 close $fd
8941 or die_error(404, "Object does not exist");
8943 # - hash_base and file_name
8944 } elsif ($hash_base && defined $file_name) {
8945 $file_name =~ s,/+$,,;
8947 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
8948 or die_error(404, "Base object does not exist");
8950 # here errors should not happen
8951 defined(my $fd = git_cmd_pipe "ls-tree", $hash_base, "--", $file_name)
8952 or die_error(500, "Open git-ls-tree failed");
8953 my $line = to_utf8(scalar <$fd>);
8954 close $fd;
8956 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8957 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
8958 die_error(404, "File or directory for given base does not exist");
8960 $type = $2;
8961 $hash = $3;
8962 } else {
8963 die_error(400, "Not enough information to find object");
8966 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
8967 hash=>$hash, hash_base=>$hash_base,
8968 file_name=>$file_name),
8969 -status => '302 Found');
8972 sub git_blobdiff {
8973 my $format = shift || 'html';
8974 my $diff_style = $input_params{'diff_style'} || 'inline';
8976 my $fd;
8977 my @difftree;
8978 my %diffinfo;
8979 my $expires;
8981 # preparing $fd and %diffinfo for git_patchset_body
8982 # new style URI
8983 if (defined $hash_base && defined $hash_parent_base) {
8984 if (defined $file_name) {
8985 # read raw output
8986 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
8987 $hash_parent_base, $hash_base,
8988 "--", (defined $file_parent ? $file_parent : ()), $file_name)
8989 or die_error(500, "Open git-diff-tree failed");
8990 @difftree = map { chomp; to_utf8($_) } <$fd>;
8991 close $fd
8992 or die_error(404, "Reading git-diff-tree failed");
8993 @difftree
8994 or die_error(404, "Blob diff not found");
8996 } elsif (defined $hash &&
8997 $hash =~ /[0-9a-fA-F]{40}/) {
8998 # try to find filename from $hash
9000 # read filtered raw output
9001 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9002 $hash_parent_base, $hash_base, "--")
9003 or die_error(500, "Open git-diff-tree failed");
9004 @difftree =
9005 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9006 # $hash == to_id
9007 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9008 map { chomp; to_utf8($_) } <$fd>;
9009 close $fd
9010 or die_error(404, "Reading git-diff-tree failed");
9011 @difftree
9012 or die_error(404, "Blob diff not found");
9014 } else {
9015 die_error(400, "Missing one of the blob diff parameters");
9018 if (@difftree > 1) {
9019 die_error(400, "Ambiguous blob diff specification");
9022 %diffinfo = parse_difftree_raw_line($difftree[0]);
9023 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9024 $file_name ||= $diffinfo{'to_file'};
9026 $hash_parent ||= $diffinfo{'from_id'};
9027 $hash ||= $diffinfo{'to_id'};
9029 # non-textual hash id's can be cached
9030 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9031 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9032 $expires = '+1d';
9035 # open patch output
9036 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9037 '-p', ($format eq 'html' ? "--full-index" : ()),
9038 $hash_parent_base, $hash_base,
9039 "--", (defined $file_parent ? $file_parent : ()), $file_name)
9040 or die_error(500, "Open git-diff-tree failed");
9043 # old/legacy style URI -- not generated anymore since 1.4.3.
9044 if (!%diffinfo) {
9045 die_error('404 Not Found', "Missing one of the blob diff parameters")
9048 # header
9049 if ($format eq 'html') {
9050 my $formats_nav =
9051 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
9052 "raw");
9053 $formats_nav .= diff_style_nav($diff_style);
9054 git_header_html(undef, $expires);
9055 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
9056 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9057 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
9058 } else {
9059 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9060 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
9062 if (defined $file_name) {
9063 git_print_page_path($file_name, "blob", $hash_base);
9064 } else {
9065 print "<div class=\"page_path\"></div>\n";
9068 } elsif ($format eq 'plain') {
9069 print $cgi->header(
9070 -type => 'text/plain',
9071 -charset => 'utf-8',
9072 -expires => $expires,
9073 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
9075 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9077 } else {
9078 die_error(400, "Unknown blobdiff format");
9081 # patch
9082 if ($format eq 'html') {
9083 print "<div class=\"page_body\">\n";
9085 git_patchset_body($fd, $diff_style,
9086 [ \%diffinfo ], $hash_base, $hash_parent_base);
9087 close $fd;
9089 print "</div>\n"; # class="page_body"
9090 git_footer_html();
9092 } else {
9093 while (my $line = to_utf8(scalar <$fd>)) {
9094 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9095 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9097 print $line;
9099 last if $line =~ m!^\+\+\+!;
9101 while (<$fd>) {
9102 print to_utf8($_);
9104 close $fd;
9108 sub git_blobdiff_plain {
9109 git_blobdiff('plain');
9112 # assumes that it is added as later part of already existing navigation,
9113 # so it returns "| foo | bar" rather than just "foo | bar"
9114 sub diff_style_nav {
9115 my ($diff_style, $is_combined) = @_;
9116 $diff_style ||= 'inline';
9118 return "" if ($is_combined);
9120 my @styles = (inline => 'inline', 'sidebyside' => 'side&#160;by&#160;side');
9121 my %styles = @styles;
9122 @styles =
9123 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9125 return $barsep . '<span class="diffstyles">' . join($barsep,
9126 map {
9127 $_ eq $diff_style ?
9128 '<span class="diffstyle selected">' . $styles{$_} . '</span>' :
9129 '<span class="diffstyle">' .
9130 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_}) .
9131 '</span>'
9132 } @styles) . '</span>';
9135 sub git_commitdiff {
9136 my %params = @_;
9137 my $format = $params{-format} || 'html';
9138 my $diff_style = $input_params{'diff_style'} || 'inline';
9140 my ($patch_max) = gitweb_get_feature('patches');
9141 if ($format eq 'patch') {
9142 die_error(403, "Patch view not allowed") unless $patch_max;
9145 $hash ||= $hash_base || "HEAD";
9146 my %co = parse_commit($hash)
9147 or die_error(404, "Unknown commit object");
9149 # choose format for commitdiff for merge
9150 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
9151 $hash_parent = '--cc';
9153 # we need to prepare $formats_nav before almost any parameter munging
9154 my $formats_nav;
9155 if ($format eq 'html') {
9156 $formats_nav = tabspan(
9157 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
9158 "raw"));
9159 if ($patch_max && @{$co{'parents'}} <= 1) {
9160 $formats_nav .= $barsep . tabspan(
9161 $cgi->a({-href => href(action=>"patch", -replay=>1)},
9162 "patch"));
9164 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
9166 if (defined $hash_parent &&
9167 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9168 # commitdiff with two commits given
9169 my $hash_parent_short = $hash_parent;
9170 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9171 $hash_parent_short = substr($hash_parent, 0, 7);
9173 $formats_nav .= $spcsep . '<span class="parents multiple">' .
9174 '(from';
9175 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
9176 if ($co{'parents'}[$i] eq $hash_parent) {
9177 $formats_nav .= '&#160;parent&#160;' . ($i+1);
9178 last;
9181 $formats_nav .= ':&#160;' .
9182 $cgi->a({-href => href(-replay=>1,
9183 hash=>$hash_parent, hash_base=>undef)},
9184 esc_html($hash_parent_short)) .
9185 ')</span>';
9186 } elsif (!$co{'parent'}) {
9187 # --root commitdiff
9188 $formats_nav .= $spcsep . '<span class="parents none">(initial)</span>';
9189 } elsif (scalar @{$co{'parents'}} == 1) {
9190 # single parent commit
9191 $formats_nav .= $spcsep .
9192 '<span class="parents single">(parent:&#160;' .
9193 $cgi->a({-href => href(-replay=>1,
9194 hash=>$co{'parent'}, hash_base=>undef)},
9195 esc_html(substr($co{'parent'}, 0, 7))) .
9196 ')</span>';
9197 } else {
9198 # merge commit
9199 if ($hash_parent eq '--cc') {
9200 $formats_nav .= $barsep . tabspan(
9201 $cgi->a({-href => href(-replay=>1,
9202 hash=>$hash, hash_parent=>'-c')},
9203 'combined'));
9204 } else { # $hash_parent eq '-c'
9205 $formats_nav .= $barsep . tabspan(
9206 $cgi->a({-href => href(-replay=>1,
9207 hash=>$hash, hash_parent=>'--cc')},
9208 'compact'));
9210 $formats_nav .= $spcsep .
9211 '<span class="parents multiple">(merge:&#160;' .
9212 join(' ', map {
9213 $cgi->a({-href => href(-replay=>1,
9214 hash=>$_, hash_base=>undef)},
9215 esc_html(substr($_, 0, 7)));
9216 } @{$co{'parents'}} ) .
9217 ')</span>';
9221 my $hash_parent_param = $hash_parent;
9222 if (!defined $hash_parent_param) {
9223 # --cc for multiple parents, --root for parentless
9224 $hash_parent_param =
9225 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
9228 # read commitdiff
9229 my $fd;
9230 my @difftree;
9231 if ($format eq 'html') {
9232 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9233 "--no-commit-id", "--patch-with-raw", "--full-index",
9234 $hash_parent_param, $hash, "--")
9235 or die_error(500, "Open git-diff-tree failed");
9237 while (my $line = to_utf8(scalar <$fd>)) {
9238 chomp $line;
9239 # empty line ends raw part of diff-tree output
9240 last unless $line;
9241 push @difftree, scalar parse_difftree_raw_line($line);
9244 } elsif ($format eq 'plain') {
9245 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9246 '-p', $hash_parent_param, $hash, "--")
9247 or die_error(500, "Open git-diff-tree failed");
9248 } elsif ($format eq 'patch') {
9249 # For commit ranges, we limit the output to the number of
9250 # patches specified in the 'patches' feature.
9251 # For single commits, we limit the output to a single patch,
9252 # diverging from the git-format-patch default.
9253 my @commit_spec = ();
9254 if ($hash_parent) {
9255 if ($patch_max > 0) {
9256 push @commit_spec, "-$patch_max";
9258 push @commit_spec, '-n', "$hash_parent..$hash";
9259 } else {
9260 if ($params{-single}) {
9261 push @commit_spec, '-1';
9262 } else {
9263 if ($patch_max > 0) {
9264 push @commit_spec, "-$patch_max";
9266 push @commit_spec, "-n";
9268 push @commit_spec, '--root', $hash;
9270 defined($fd = git_cmd_pipe "format-patch", @diff_opts,
9271 '--encoding=utf8', '--stdout', @commit_spec)
9272 or die_error(500, "Open git-format-patch failed");
9273 } else {
9274 die_error(400, "Unknown commitdiff format");
9277 # non-textual hash id's can be cached
9278 my $expires;
9279 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9280 $expires = "+1d";
9283 # write commit message
9284 if ($format eq 'html') {
9285 my $refs = git_get_references();
9286 my $ref = format_ref_marker($refs, $co{'id'});
9288 git_header_html(undef, $expires);
9289 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9290 git_print_header_div('commit', esc_html($co{'title'}), $hash, undef, $ref);
9291 print "<div class=\"title_text\">\n" .
9292 "<table class=\"object_header\">\n";
9293 git_print_authorship_rows(\%co);
9294 print "</table>".
9295 "</div>\n";
9296 print "<div class=\"page_body\">\n";
9297 if (@{$co{'comment'}} > 1) {
9298 print "<div class=\"log\">\n";
9299 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
9300 print "</div>\n"; # class="log"
9303 } elsif ($format eq 'plain') {
9304 my $refs = git_get_references("tags");
9305 my $tagname = git_get_rev_name_tags($hash);
9306 my $filename = basename($project) . "-$hash.patch";
9308 print $cgi->header(
9309 -type => 'text/plain',
9310 -charset => 'utf-8',
9311 -expires => $expires,
9312 -content_disposition => 'inline; filename="' . "$filename" . '"');
9313 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
9314 print "From: " . to_utf8($co{'author'}) . "\n";
9315 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9316 print "Subject: " . to_utf8($co{'title'}) . "\n";
9318 print "X-Git-Tag: $tagname\n" if $tagname;
9319 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9321 foreach my $line (@{$co{'comment'}}) {
9322 print to_utf8($line) . "\n";
9324 print "---\n\n";
9325 } elsif ($format eq 'patch') {
9326 my $filename = basename($project) . "-$hash.patch";
9328 print $cgi->header(
9329 -type => 'text/plain',
9330 -charset => 'utf-8',
9331 -expires => $expires,
9332 -content_disposition => 'inline; filename="' . "$filename" . '"');
9335 # write patch
9336 if ($format eq 'html') {
9337 my $use_parents = !defined $hash_parent ||
9338 $hash_parent eq '-c' || $hash_parent eq '--cc';
9339 git_difftree_body(\@difftree, $hash,
9340 $use_parents ? @{$co{'parents'}} : $hash_parent);
9341 print "<br/>\n";
9343 git_patchset_body($fd, $diff_style,
9344 \@difftree, $hash,
9345 $use_parents ? @{$co{'parents'}} : $hash_parent);
9346 close $fd;
9347 print "</div>\n"; # class="page_body"
9348 git_footer_html();
9350 } elsif ($format eq 'plain') {
9351 while (<$fd>) {
9352 print to_utf8($_);
9354 close $fd
9355 or print "Reading git-diff-tree failed\n";
9356 } elsif ($format eq 'patch') {
9357 while (<$fd>) {
9358 print to_utf8($_);
9360 close $fd
9361 or print "Reading git-format-patch failed\n";
9365 sub git_commitdiff_plain {
9366 git_commitdiff(-format => 'plain');
9369 # format-patch-style patches
9370 sub git_patch {
9371 git_commitdiff(-format => 'patch', -single => 1);
9374 sub git_patches {
9375 git_commitdiff(-format => 'patch');
9378 sub git_history {
9379 git_log_generic('history', \&git_history_body,
9380 $hash_base, $hash_parent_base,
9381 $file_name, $hash);
9384 sub git_search {
9385 $searchtype ||= 'commit';
9387 # check if appropriate features are enabled
9388 gitweb_check_feature('search')
9389 or die_error(403, "Search is disabled");
9390 if ($searchtype eq 'pickaxe') {
9391 # pickaxe may take all resources of your box and run for several minutes
9392 # with every query - so decide by yourself how public you make this feature
9393 gitweb_check_feature('pickaxe')
9394 or die_error(403, "Pickaxe search is disabled");
9396 if ($searchtype eq 'grep') {
9397 # grep search might be potentially CPU-intensive, too
9398 gitweb_check_feature('grep')
9399 or die_error(403, "Grep search is disabled");
9402 if (!defined $searchtext) {
9403 die_error(400, "Text field is empty");
9405 if (!defined $hash) {
9406 $hash = git_get_head_hash($project);
9408 my %co = parse_commit($hash);
9409 if (!%co) {
9410 die_error(404, "Unknown commit object");
9412 if (!defined $page) {
9413 $page = 0;
9416 if ($searchtype eq 'commit' ||
9417 $searchtype eq 'author' ||
9418 $searchtype eq 'committer') {
9419 git_search_message(%co);
9420 } elsif ($searchtype eq 'pickaxe') {
9421 git_search_changes(%co);
9422 } elsif ($searchtype eq 'grep') {
9423 git_search_files(%co);
9424 } else {
9425 die_error(400, "Unknown search type");
9429 sub git_search_help {
9430 git_header_html();
9431 git_print_page_nav('','', $hash,$hash,$hash);
9432 print <<EOT;
9433 <div class="search_help">
9434 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9435 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9436 the pattern entered is recognized as the POSIX extended
9437 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9438 insensitive).</p>
9439 <dl>
9440 <dt><b>commit</b></dt>
9441 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9443 my $have_grep = gitweb_check_feature('grep');
9444 if ($have_grep) {
9445 print <<EOT;
9446 <dt><b>grep</b></dt>
9447 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9448 a different one) are searched for the given pattern. On large trees, this search can take
9449 a while and put some strain on the server, so please use it with some consideration. Note that
9450 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9451 case-sensitive.</dd>
9454 print <<EOT;
9455 <dt><b>author</b></dt>
9456 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9457 <dt><b>committer</b></dt>
9458 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9460 my $have_pickaxe = gitweb_check_feature('pickaxe');
9461 if ($have_pickaxe) {
9462 print <<EOT;
9463 <dt><b>pickaxe</b></dt>
9464 <dd>All commits that caused the string to appear or disappear from any file (changes that
9465 added, removed or "modified" the string) will be listed. This search can take a while and
9466 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9467 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9470 print "</dl>\n</div>\n";
9471 git_footer_html();
9474 sub git_shortlog {
9475 git_log_generic('shortlog', \&git_shortlog_body,
9476 $hash, $hash_parent);
9479 ## ......................................................................
9480 ## feeds (RSS, Atom; OPML)
9482 sub git_feed {
9483 my $format = shift || 'atom';
9484 my $have_blame = gitweb_check_feature('blame');
9486 # Atom: http://www.atomenabled.org/developers/syndication/
9487 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9488 if ($format ne 'rss' && $format ne 'atom') {
9489 die_error(400, "Unknown web feed format");
9492 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9493 my $head = $hash || 'HEAD';
9494 my @commitlist = parse_commits($head, 150, 0, $file_name);
9496 my %latest_commit;
9497 my %latest_date;
9498 my $content_type = "application/$format+xml";
9499 if (defined $cgi->http('HTTP_ACCEPT') &&
9500 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9501 # browser (feed reader) prefers text/xml
9502 $content_type = 'text/xml';
9504 if (defined($commitlist[0])) {
9505 %latest_commit = %{$commitlist[0]};
9506 my $latest_epoch = $latest_commit{'committer_epoch'};
9507 exit_if_unmodified_since($latest_epoch);
9508 %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
9510 print $cgi->header(
9511 -type => $content_type,
9512 -charset => 'utf-8',
9513 %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
9514 -status => '200 OK');
9516 # Optimization: skip generating the body if client asks only
9517 # for Last-Modified date.
9518 return if ($cgi->request_method() eq 'HEAD');
9520 # header variables
9521 my $title = "$site_name - $project/$action";
9522 my $feed_type = 'log';
9523 if (defined $hash) {
9524 $title .= " - '$hash'";
9525 $feed_type = 'branch log';
9526 if (defined $file_name) {
9527 $title .= " :: $file_name";
9528 $feed_type = 'history';
9530 } elsif (defined $file_name) {
9531 $title .= " - $file_name";
9532 $feed_type = 'history';
9534 $title .= " $feed_type";
9535 $title = esc_html($title);
9536 my $descr = git_get_project_description($project);
9537 if (defined $descr) {
9538 $descr = esc_html($descr);
9539 } else {
9540 $descr = "$project " .
9541 ($format eq 'rss' ? 'RSS' : 'Atom') .
9542 " feed";
9544 my $owner = git_get_project_owner($project);
9545 $owner = esc_html($owner);
9547 #header
9548 my $alt_url;
9549 if (defined $file_name) {
9550 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
9551 } elsif (defined $hash) {
9552 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
9553 } else {
9554 $alt_url = href(-full=>1, action=>"summary");
9556 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
9557 if ($format eq 'rss') {
9558 print <<XML;
9559 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9560 <channel>
9562 print "<title>$title</title>\n" .
9563 "<link>$alt_url</link>\n" .
9564 "<description>$descr</description>\n" .
9565 "<language>en</language>\n" .
9566 # project owner is responsible for 'editorial' content
9567 "<managingEditor>$owner</managingEditor>\n";
9568 if (defined $logo || defined $favicon) {
9569 # prefer the logo to the favicon, since RSS
9570 # doesn't allow both
9571 my $img = esc_url($logo || $favicon);
9572 print "<image>\n" .
9573 "<url>$img</url>\n" .
9574 "<title>$title</title>\n" .
9575 "<link>$alt_url</link>\n" .
9576 "</image>\n";
9578 if (%latest_date) {
9579 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9580 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9582 print "<generator>gitweb v.$version/$git_version</generator>\n";
9583 } elsif ($format eq 'atom') {
9584 print <<XML;
9585 <feed xmlns="http://www.w3.org/2005/Atom">
9587 print "<title>$title</title>\n" .
9588 "<subtitle>$descr</subtitle>\n" .
9589 '<link rel="alternate" type="text/html" href="' .
9590 $alt_url . '" />' . "\n" .
9591 '<link rel="self" type="' . $content_type . '" href="' .
9592 $cgi->self_url() . '" />' . "\n" .
9593 "<id>" . href(-full=>1) . "</id>\n" .
9594 # use project owner for feed author
9595 '<author><name>'. email_obfuscate($owner) . '</name></author>\n';
9596 if (defined $favicon) {
9597 print "<icon>" . esc_url($favicon) . "</icon>\n";
9599 if (defined $logo) {
9600 # not twice as wide as tall: 72 x 27 pixels
9601 print "<logo>" . esc_url($logo) . "</logo>\n";
9603 if (! %latest_date) {
9604 # dummy date to keep the feed valid until commits trickle in:
9605 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9606 } else {
9607 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9609 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9612 # contents
9613 for (my $i = 0; $i <= $#commitlist; $i++) {
9614 my %co = %{$commitlist[$i]};
9615 my $commit = $co{'id'};
9616 # we read 150, we always show 30 and the ones more recent than 48 hours
9617 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9618 last;
9620 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
9622 # get list of changed files
9623 defined(my $fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9624 $co{'parent'} || "--root",
9625 $co{'id'}, "--", (defined $file_name ? $file_name : ()))
9626 or next;
9627 my @difftree = map { chomp; to_utf8($_) } <$fd>;
9628 close $fd
9629 or next;
9631 # print element (entry, item)
9632 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
9633 if ($format eq 'rss') {
9634 print "<item>\n" .
9635 "<title>" . esc_html($co{'title'}) . "</title>\n" .
9636 "<author>" . esc_html($co{'author'}) . "</author>\n" .
9637 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9638 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9639 "<link>$co_url</link>\n" .
9640 "<description>" . esc_html($co{'title'}) . "</description>\n" .
9641 "<content:encoded>" .
9642 "<![CDATA[\n";
9643 } elsif ($format eq 'atom') {
9644 print "<entry>\n" .
9645 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
9646 "<updated>$cd{'iso-8601'}</updated>\n" .
9647 "<author>\n" .
9648 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
9649 if ($co{'author_email'}) {
9650 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
9652 print "</author>\n" .
9653 # use committer for contributor
9654 "<contributor>\n" .
9655 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
9656 if ($co{'committer_email'}) {
9657 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
9659 print "</contributor>\n" .
9660 "<published>$cd{'iso-8601'}</published>\n" .
9661 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9662 "<id>$co_url</id>\n" .
9663 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
9664 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9666 my $comment = $co{'comment'};
9667 print "<pre>\n";
9668 foreach my $line (@$comment) {
9669 $line = esc_html($line);
9670 print "$line\n";
9672 print "</pre><ul>\n";
9673 foreach my $difftree_line (@difftree) {
9674 my %difftree = parse_difftree_raw_line($difftree_line);
9675 next if !$difftree{'from_id'};
9677 my $file = $difftree{'file'} || $difftree{'to_file'};
9679 print "<li>" .
9680 "[" .
9681 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
9682 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
9683 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
9684 file_name=>$file, file_parent=>$difftree{'from_file'}),
9685 -title => "diff"}, 'D');
9686 if ($have_blame) {
9687 print $cgi->a({-href => href(-full=>1, action=>"blame",
9688 file_name=>$file, hash_base=>$commit),
9689 -class => "blamelink",
9690 -title => "blame"}, 'B');
9692 # if this is not a feed of a file history
9693 if (!defined $file_name || $file_name ne $file) {
9694 print $cgi->a({-href => href(-full=>1, action=>"history",
9695 file_name=>$file, hash=>$commit),
9696 -title => "history"}, 'H');
9698 $file = esc_path($file);
9699 print "] ".
9700 "$file</li>\n";
9702 if ($format eq 'rss') {
9703 print "</ul>]]>\n" .
9704 "</content:encoded>\n" .
9705 "</item>\n";
9706 } elsif ($format eq 'atom') {
9707 print "</ul>\n</div>\n" .
9708 "</content>\n" .
9709 "</entry>\n";
9713 # end of feed
9714 if ($format eq 'rss') {
9715 print "</channel>\n</rss>\n";
9716 } elsif ($format eq 'atom') {
9717 print "</feed>\n";
9721 sub git_rss {
9722 git_feed('rss');
9725 sub git_atom {
9726 git_feed('atom');
9729 sub git_opml {
9730 my @list = git_get_projects_list($project_filter, $strict_export);
9731 if (!@list) {
9732 die_error(404, "No projects found");
9735 print $cgi->header(
9736 -type => 'text/xml',
9737 -charset => 'utf-8',
9738 -content_disposition => 'inline; filename="opml.xml"');
9740 my $title = esc_html($site_name);
9741 my $filter = " within subdirectory ";
9742 if (defined $project_filter) {
9743 $filter .= esc_html($project_filter);
9744 } else {
9745 $filter = "";
9747 print <<XML;
9748 <?xml version="1.0" encoding="utf-8"?>
9749 <opml version="1.0">
9750 <head>
9751 <title>$title OPML Export$filter</title>
9752 </head>
9753 <body>
9754 <outline text="git RSS feeds">
9757 foreach my $pr (@list) {
9758 my %proj = %$pr;
9759 my $head = git_get_head_hash($proj{'path'});
9760 if (!defined $head) {
9761 next;
9763 $git_dir = "$projectroot/$proj{'path'}";
9764 my %co = parse_commit($head);
9765 if (!%co) {
9766 next;
9769 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
9770 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
9771 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
9772 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
9774 print <<XML;
9775 </outline>
9776 </body>
9777 </opml>