Merge commit 'refs/top-bases/t/girocco/style-updates' into t/girocco/style-updates
[git/gitweb.git] / gitweb / gitweb.perl
blob20ae951b2f546308c2c63e75a01b1f3ea6945d43
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 ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1679 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1680 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1681 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1682 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1683 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1684 $first_request = 1;
1685 evaluate_argv();
1687 $pre_listen_hook->()
1688 if $pre_listen_hook;
1690 REQUEST:
1691 while ($cgi = $CGI->new()) {
1692 $pre_dispatch_hook->()
1693 if $pre_dispatch_hook;
1695 eval {run_request()};
1697 $post_dispatch_hook->()
1698 if $post_dispatch_hook;
1699 $first_request = 0;
1701 last REQUEST if ($is_last_request->());
1707 run();
1709 if (defined caller) {
1710 # wrapped in a subroutine processing requests,
1711 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1712 return;
1713 } else {
1714 # pure CGI script, serving single request
1715 exit;
1718 ## ======================================================================
1719 ## action links
1721 # possible values of extra options
1722 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1723 # -replay => 1 - start from a current view (replay with modifications)
1724 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1725 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1726 sub href {
1727 my %params = @_;
1728 # default is to use -absolute url() i.e. $my_uri
1729 my $href = $params{-full} ? $my_url : $my_uri;
1731 # implicit -replay, must be first of implicit params
1732 $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1734 $params{'project'} = $project unless exists $params{'project'};
1736 if ($params{-replay}) {
1737 while (my ($name, $symbol) = each %cgi_param_mapping) {
1738 if (!exists $params{$name}) {
1739 $params{$name} = $input_params{$name};
1744 my $use_pathinfo = gitweb_check_feature('pathinfo');
1745 if (defined $params{'project'} &&
1746 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1747 # try to put as many parameters as possible in PATH_INFO:
1748 # - project name
1749 # - action
1750 # - hash_parent or hash_parent_base:/file_parent
1751 # - hash or hash_base:/filename
1752 # - the snapshot_format as an appropriate suffix
1754 # When the script is the root DirectoryIndex for the domain,
1755 # $href here would be something like http://gitweb.example.com/
1756 # Thus, we strip any trailing / from $href, to spare us double
1757 # slashes in the final URL
1758 $href =~ s,/$,,;
1760 # Then add the project name, if present
1761 $href .= "/".esc_path_info($params{'project'});
1762 delete $params{'project'};
1764 # since we destructively absorb parameters, we keep this
1765 # boolean that remembers if we're handling a snapshot
1766 my $is_snapshot = $params{'action'} eq 'snapshot';
1768 # Summary just uses the project path URL, any other action is
1769 # added to the URL
1770 if (defined $params{'action'}) {
1771 $href .= "/".esc_path_info($params{'action'})
1772 unless $params{'action'} eq 'summary';
1773 delete $params{'action'};
1776 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1777 # stripping nonexistent or useless pieces
1778 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1779 || $params{'hash_parent'} || $params{'hash'});
1780 if (defined $params{'hash_base'}) {
1781 if (defined $params{'hash_parent_base'}) {
1782 $href .= esc_path_info($params{'hash_parent_base'});
1783 # skip the file_parent if it's the same as the file_name
1784 if (defined $params{'file_parent'}) {
1785 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1786 delete $params{'file_parent'};
1787 } elsif ($params{'file_parent'} !~ /\.\./) {
1788 $href .= ":/".esc_path_info($params{'file_parent'});
1789 delete $params{'file_parent'};
1792 $href .= "..";
1793 delete $params{'hash_parent'};
1794 delete $params{'hash_parent_base'};
1795 } elsif (defined $params{'hash_parent'}) {
1796 $href .= esc_path_info($params{'hash_parent'}). "..";
1797 delete $params{'hash_parent'};
1800 $href .= esc_path_info($params{'hash_base'});
1801 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1802 $href .= ":/".esc_path_info($params{'file_name'});
1803 delete $params{'file_name'};
1805 delete $params{'hash'};
1806 delete $params{'hash_base'};
1807 } elsif (defined $params{'hash'}) {
1808 $href .= esc_path_info($params{'hash'});
1809 delete $params{'hash'};
1812 # If the action was a snapshot, we can absorb the
1813 # snapshot_format parameter too
1814 if ($is_snapshot) {
1815 my $fmt = $params{'snapshot_format'};
1816 # snapshot_format should always be defined when href()
1817 # is called, but just in case some code forgets, we
1818 # fall back to the default
1819 $fmt ||= $snapshot_fmts[0];
1820 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1821 delete $params{'snapshot_format'};
1825 # now encode the parameters explicitly
1826 my @result = ();
1827 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1828 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1829 if (defined $params{$name}) {
1830 if (ref($params{$name}) eq "ARRAY") {
1831 foreach my $par (@{$params{$name}}) {
1832 push @result, $symbol . "=" . esc_param($par);
1834 } else {
1835 push @result, $symbol . "=" . esc_param($params{$name});
1839 $href .= "?" . join(';', @result) if scalar @result;
1841 # final transformation: trailing spaces must be escaped (URI-encoded)
1842 $href =~ s/(\s+)$/CGI::escape($1)/e;
1844 if ($params{-anchor}) {
1845 $href .= "#".esc_param($params{-anchor});
1848 return $href;
1852 ## ======================================================================
1853 ## validation, quoting/unquoting and escaping
1855 sub is_valid_action {
1856 my $input = shift;
1857 return undef unless exists $actions{$input};
1858 return 1;
1861 sub is_valid_project {
1862 my $input = shift;
1864 return unless defined $input;
1865 if (!is_valid_pathname($input) ||
1866 !(-d "$projectroot/$input") ||
1867 !check_export_ok("$projectroot/$input") ||
1868 ($strict_export && !project_in_list($input))) {
1869 return undef;
1870 } else {
1871 return 1;
1875 sub is_valid_pathname {
1876 my $input = shift;
1878 return undef unless defined $input;
1879 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1880 # at the beginning, at the end, and between slashes.
1881 # also this catches doubled slashes
1882 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1883 return undef;
1885 # no null characters
1886 if ($input =~ m!\0!) {
1887 return undef;
1889 return 1;
1892 sub is_valid_ref_format {
1893 my $input = shift;
1895 return undef unless defined $input;
1896 # restrictions on ref name according to git-check-ref-format
1897 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1898 return undef;
1900 return 1;
1903 sub is_valid_refname {
1904 my $input = shift;
1906 return undef unless defined $input;
1907 # textual hashes are O.K.
1908 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1909 return 1;
1911 # it must be correct pathname
1912 is_valid_pathname($input) or return undef;
1913 # check git-check-ref-format restrictions
1914 is_valid_ref_format($input) or return undef;
1915 return 1;
1918 # decode sequences of octets in utf8 into Perl's internal form,
1919 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1920 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1921 sub to_utf8 {
1922 my $str = shift;
1923 return undef unless defined $str;
1925 if (utf8::is_utf8($str) || utf8::decode($str)) {
1926 return $str;
1927 } else {
1928 return $encode_object->decode($str, Encode::FB_DEFAULT);
1932 # quote unsafe chars, but keep the slash, even when it's not
1933 # correct, but quoted slashes look too horrible in bookmarks
1934 sub esc_param {
1935 my $str = shift;
1936 return undef unless defined $str;
1937 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1938 $str =~ s/ /\+/g;
1939 return $str;
1942 # the quoting rules for path_info fragment are slightly different
1943 sub esc_path_info {
1944 my $str = shift;
1945 return undef unless defined $str;
1947 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1948 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1950 return $str;
1953 # quote unsafe chars in whole URL, so some characters cannot be quoted
1954 sub esc_url {
1955 my $str = shift;
1956 return undef unless defined $str;
1957 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1958 $str =~ s/ /\+/g;
1959 return $str;
1962 # quote unsafe characters in HTML attributes
1963 sub esc_attr {
1965 # for XHTML conformance escaping '"' to '&quot;' is not enough
1966 return esc_html(@_);
1969 # replace invalid utf8 character with SUBSTITUTION sequence
1970 sub esc_html {
1971 my $str = shift;
1972 my %opts = @_;
1974 return undef unless defined $str;
1976 $str = to_utf8($str);
1977 $str = $cgi->escapeHTML($str);
1978 if ($opts{'-nbsp'}) {
1979 $str =~ s/ /&#160;/g;
1981 use bytes;
1982 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1983 return $str;
1986 # quote control characters and escape filename to HTML
1987 sub esc_path {
1988 my $str = shift;
1989 my %opts = @_;
1991 return undef unless defined $str;
1993 $str = to_utf8($str);
1994 $str = $cgi->escapeHTML($str);
1995 if ($opts{'-nbsp'}) {
1996 $str =~ s/ /&#160;/g;
1998 use bytes;
1999 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
2000 return $str;
2003 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
2004 sub sanitize {
2005 my $str = shift;
2007 return undef unless defined $str;
2009 $str = to_utf8($str);
2010 use bytes;
2011 $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
2012 return $str;
2015 # Make control characters "printable", using character escape codes (CEC)
2016 sub quot_cec {
2017 my $cntrl = shift;
2018 my %opts = @_;
2019 my %es = ( # character escape codes, aka escape sequences
2020 "\t" => '\t', # tab (HT)
2021 "\n" => '\n', # line feed (LF)
2022 "\r" => '\r', # carrige return (CR)
2023 "\f" => '\f', # form feed (FF)
2024 "\b" => '\b', # backspace (BS)
2025 "\a" => '\a', # alarm (bell) (BEL)
2026 "\e" => '\e', # escape (ESC)
2027 "\013" => '\v', # vertical tab (VT)
2028 "\000" => '\0', # nul character (NUL)
2030 my $chr = ( (exists $es{$cntrl})
2031 ? $es{$cntrl}
2032 : sprintf('\x%02x', ord($cntrl)) );
2033 if ($opts{-nohtml}) {
2034 return $chr;
2035 } else {
2036 return "<span class=\"cntrl\">$chr</span>";
2040 # Alternatively use unicode control pictures codepoints,
2041 # Unicode "printable representation" (PR)
2042 sub quot_upr {
2043 my $cntrl = shift;
2044 my %opts = @_;
2046 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2047 if ($opts{-nohtml}) {
2048 return $chr;
2049 } else {
2050 return "<span class=\"cntrl\">$chr</span>";
2054 # git may return quoted and escaped filenames
2055 sub unquote {
2056 my $str = shift;
2058 sub unq {
2059 my $seq = shift;
2060 my %es = ( # character escape codes, aka escape sequences
2061 't' => "\t", # tab (HT, TAB)
2062 'n' => "\n", # newline (NL)
2063 'r' => "\r", # return (CR)
2064 'f' => "\f", # form feed (FF)
2065 'b' => "\b", # backspace (BS)
2066 'a' => "\a", # alarm (bell) (BEL)
2067 'e' => "\e", # escape (ESC)
2068 'v' => "\013", # vertical tab (VT)
2071 if ($seq =~ m/^[0-7]{1,3}$/) {
2072 # octal char sequence
2073 return chr(oct($seq));
2074 } elsif (exists $es{$seq}) {
2075 # C escape sequence, aka character escape code
2076 return $es{$seq};
2078 # quoted ordinary character
2079 return $seq;
2082 if ($str =~ m/^"(.*)"$/) {
2083 # needs unquoting
2084 $str = $1;
2085 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2087 return $str;
2090 # escape tabs (convert tabs to spaces)
2091 sub untabify {
2092 my $line = shift;
2094 while ((my $pos = index($line, "\t")) != -1) {
2095 if (my $count = (8 - ($pos % 8))) {
2096 my $spaces = ' ' x $count;
2097 $line =~ s/\t/$spaces/;
2101 return $line;
2104 sub project_in_list {
2105 my $project = shift;
2106 my @list = git_get_projects_list();
2107 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2110 sub cached_page_precondition_check {
2111 my $action = shift;
2112 return 1 unless
2113 $action eq 'summary' &&
2114 $projlist_cache_lifetime > 0 &&
2115 gitweb_check_feature('forks');
2117 # Note that ALL the 'forkchange' logic is in this function.
2118 # It does NOT belong in cached_action_page NOR in cached_action_start
2119 # NOR in cached_action_finish. None of those functions should know anything
2120 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2122 # besides the basic 'changed' "$action.changed" check, we may only use
2123 # a summary cache if:
2125 # 1) we are not using a project list cache file
2126 # -OR-
2127 # 2) we are not using the 'forks' feature
2128 # -OR-
2129 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2130 # -OR-
2131 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2132 # -OR-
2133 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2135 # Otherwise we must re-generate the cache because we've had a fork change
2136 # (either a fork was added or a fork was removed) AND the change has been
2137 # picked up in the cache file AND we've not got that in our cached copy
2139 # For (5) regenerating the cached page wouldn't get us anything if the project
2140 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2141 # forks information comes from the project cache file and it's clearly not
2142 # picked up the changes yet so we may continue to use a cached page until it does.
2144 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2145 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2146 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2147 return 1 unless defined($fc_mt) || defined($afc_mt);
2148 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2149 return 1 unless $prj_mt;
2150 my $old_mt = $fc_mt;
2151 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2152 return 1 if $old_mt > $prj_mt;
2154 # We're going to regenerate the cached page because we know the project cache
2155 # has new fork information that we cannot possibly have in our cached copy.
2157 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2158 # them is older than the project cache and one of them is newer, we still
2159 # need to regenerate the page cache, but we will also need to do it again
2160 # in the future because there's yet another fork update not yet in the cache.
2162 # So we make sure to touch "$action.changed" to force a cache regeneration
2163 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2164 # they're older than the project cache (they've served their purpose, we're
2165 # forcing a page regeneration by touching "$action.changed" but the project
2166 # cache was rebuilt since then so there are no more pending fork updates to
2167 # pick up in the future and they need to go).
2169 # For best results, the external code that touches 'forkchange' should always
2170 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2171 # if it does not already exist. That way the cached page will be regenerated
2172 # each time it's requested and ANY fork updates are available in the proj
2173 # cache rather than waiting until they all are before updating.
2175 # Note that we take a shortcut here and will zap 'forkchange' since we know
2176 # that it only affects the 'summary' cache. If, in the future, it affects
2177 # other cache types, it will first need to be propogated down to
2178 # "$action.forkchange" for those types before we zap it.
2180 my $fd;
2181 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2182 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2183 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2185 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2186 # one and not the other.
2188 if (defined $fc_mt && ! defined $afc_mt) {
2189 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2190 -e "$htmlcd/$action.forkchange" and
2191 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2192 unlink "$htmlcd/forkchange";
2195 return 0;
2198 sub cached_action_page {
2199 my $action = shift;
2201 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2202 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2203 return undef if -e "$htmlcd/changed" || -e "$htmlcd/$action.changed";
2204 return undef unless cached_page_precondition_check($action);
2205 open my $fd, '<', "$htmlcd/$action" or return undef;
2206 binmode $fd;
2207 local $/;
2208 my $cached_page = <$fd>;
2209 close $fd or return undef;
2210 return $cached_page;
2213 package Git::Gitweb::CacheFile;
2215 sub TIEHANDLE {
2216 use POSIX qw(:fcntl_h);
2217 my $class = shift;
2218 my $cachefile = shift;
2220 sysopen(my $self, $cachefile, O_WRONLY|O_CREAT|O_EXCL, 0664)
2221 or return undef;
2222 $$self->{'cachefile'} = $cachefile;
2223 $$self->{'opened'} = 1;
2224 $$self->{'contents'} = '';
2225 return bless $self, $class;
2228 sub CLOSE {
2229 my $self = shift;
2230 if ($$self->{'opened'}) {
2231 $$self->{'opened'} = 0;
2232 my $result = close $self;
2233 unlink $$self->{'cachefile'} unless $result;
2234 return $result;
2236 return 0;
2239 sub DESTROY {
2240 my $self = shift;
2241 if ($$self->{'opened'}) {
2242 $self->CLOSE() and unlink $$self->{'cachefile'};
2246 sub PRINT {
2247 my $self = shift;
2248 @_ = (map {my $x=$_; utf8::encode($x); $x} @_) unless $fcgi_raw_mode;
2249 print $self @_ if $$self->{'opened'};
2250 $$self->{'contents'} .= join('', @_);
2251 return 1;
2254 sub PRINTF {
2255 my $self = shift;
2256 my $template = shift;
2257 return $self->PRINT(sprintf $template, @_);
2260 sub contents {
2261 my $self = shift;
2262 return $$self->{'contents'};
2265 package main;
2267 # Caller is responsible for preserving STDOUT beforehand if needed
2268 sub cached_action_start {
2269 my $action = shift;
2271 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2272 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2273 return undef unless -d $htmlcd;
2274 if (-e "$htmlcd/changed") {
2275 foreach my $cacheable (keys(%html_cache_actions)) {
2276 next unless $supported_cache_actions{$cacheable} &&
2277 $html_cache_actions{$cacheable};
2278 my $fd;
2279 open $fd, '>', "$htmlcd/$cacheable.changed"
2280 and close $fd;
2282 unlink "$htmlcd/changed";
2284 local *CACHEFILE;
2285 tie *CACHEFILE, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2286 *STDOUT = *CACHEFILE;
2287 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2288 return 1;
2291 # Caller is responsible for restoring STDOUT afterward if needed
2292 sub cached_action_finish {
2293 my $action = shift;
2295 use File::Spec;
2297 my $obj = tied *STDOUT;
2298 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2299 my $cached_page = $obj->contents;
2300 (my $result = close(STDOUT)) or warn "couldn't close cache file on STDOUT: $!";
2301 # Do not leave STDOUT file descriptor invalid!
2302 local *NULL;
2303 open(NULL, '>', File::Spec->devnull) or die "couldn't open NULL to devnull: $!";
2304 *STDOUT = *NULL;
2305 return $cached_page unless $result;
2306 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2307 return $cached_page unless -d $htmlcd;
2308 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2309 return $cached_page;
2312 my %expand_pi_subs;
2313 BEGIN {%expand_pi_subs = (
2314 'age_string' => \&age_string,
2315 'age_string_date' => \&age_string_date,
2316 'age_string_age' => \&age_string_age,
2317 'compute_timed_interval' => \&compute_timed_interval,
2318 'compute_commands_count' => \&compute_commands_count,
2319 'format_lastrefresh_row' => \&format_lastrefresh_row,
2320 'compute_stylesheet_links' => \&compute_stylesheet_links,
2323 # Expands any <?gitweb...> processing instructions and returns the result
2324 sub expand_gitweb_pi {
2325 my $page = shift;
2326 $page .= '';
2327 my @time_now = gettimeofday();
2328 $page =~ s{<\?gitweb(?:\s+([^\s>]+)([^>]*))?\s*\?>}
2329 {defined($1) ?
2330 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2331 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2332 '') :
2333 '' }goes;
2334 return $page;
2337 ## ----------------------------------------------------------------------
2338 ## HTML aware string manipulation
2340 # Try to chop given string on a word boundary between position
2341 # $len and $len+$add_len. If there is no word boundary there,
2342 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2343 # (marking chopped part) would be longer than given string.
2344 sub chop_str {
2345 my $str = shift;
2346 my $len = shift;
2347 my $add_len = shift || 10;
2348 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2350 # Make sure perl knows it is utf8 encoded so we don't
2351 # cut in the middle of a utf8 multibyte char.
2352 $str = to_utf8($str);
2354 # allow only $len chars, but don't cut a word if it would fit in $add_len
2355 # if it doesn't fit, cut it if it's still longer than the dots we would add
2356 # remove chopped character entities entirely
2358 # when chopping in the middle, distribute $len into left and right part
2359 # return early if chopping wouldn't make string shorter
2360 if ($where eq 'center') {
2361 return $str if ($len + 5 >= length($str)); # filler is length 5
2362 $len = int($len/2);
2363 } else {
2364 return $str if ($len + 4 >= length($str)); # filler is length 4
2367 # regexps: ending and beginning with word part up to $add_len
2368 my $endre = qr/.{$len}\w{0,$add_len}/;
2369 my $begre = qr/\w{0,$add_len}.{$len}/;
2371 if ($where eq 'left') {
2372 $str =~ m/^(.*?)($begre)$/;
2373 my ($lead, $body) = ($1, $2);
2374 if (length($lead) > 4) {
2375 $lead = " ...";
2377 return "$lead$body";
2379 } elsif ($where eq 'center') {
2380 $str =~ m/^($endre)(.*)$/;
2381 my ($left, $str) = ($1, $2);
2382 $str =~ m/^(.*?)($begre)$/;
2383 my ($mid, $right) = ($1, $2);
2384 if (length($mid) > 5) {
2385 $mid = " ... ";
2387 return "$left$mid$right";
2389 } else {
2390 $str =~ m/^($endre)(.*)$/;
2391 my $body = $1;
2392 my $tail = $2;
2393 if (length($tail) > 4) {
2394 $tail = "... ";
2396 return "$body$tail";
2400 # pass-through email filter, obfuscating it when possible
2401 sub email_obfuscate {
2402 our $email;
2403 my ($str) = @_;
2404 if ($email) {
2405 $str = $email->escape_html($str);
2406 # Stock HTML::Email::Obfuscate version likes to produce
2407 # invalid XHTML...
2408 $str =~ s#<(/?)B>#<$1b>#g;
2409 return $str;
2410 } else {
2411 $str = esc_html($str);
2412 $str =~ s/@/&#x40;/;
2413 return $str;
2417 # takes the same arguments as chop_str, but also wraps a <span> around the
2418 # result with a title attribute if it does get chopped. Additionally, the
2419 # string is HTML-escaped.
2420 sub chop_and_escape_str {
2421 my ($str) = @_;
2423 my $chopped = chop_str(@_);
2424 $str = to_utf8($str);
2425 if ($chopped eq $str) {
2426 return email_obfuscate($chopped);
2427 } else {
2428 use bytes;
2429 $str =~ s/[[:cntrl:]]/?/g;
2430 return $cgi->span({-title=>$str}, email_obfuscate($chopped));
2434 # Highlight selected fragments of string, using given CSS class,
2435 # and escape HTML. It is assumed that fragments do not overlap.
2436 # Regions are passed as list of pairs (array references).
2438 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2439 # '<span class="mark">foo</span>bar'
2440 sub esc_html_hl_regions {
2441 my ($str, $css_class, @sel) = @_;
2442 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2443 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2444 return esc_html($str, %opts) unless @sel;
2446 my $out = '';
2447 my $pos = 0;
2449 for my $s (@sel) {
2450 my ($begin, $end) = @$s;
2452 # Don't create empty <span> elements.
2453 next if $end <= $begin;
2455 my $escaped = esc_html(substr($str, $begin, $end - $begin),
2456 %opts);
2458 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
2459 if ($begin - $pos > 0);
2460 $out .= $cgi->span({-class => $css_class}, $escaped);
2462 $pos = $end;
2464 $out .= esc_html(substr($str, $pos), %opts)
2465 if ($pos < length($str));
2467 return $out;
2470 # return positions of beginning and end of each match
2471 sub matchpos_list {
2472 my ($str, $regexp) = @_;
2473 return unless (defined $str && defined $regexp);
2475 my @matches;
2476 while ($str =~ /$regexp/g) {
2477 push @matches, [$-[0], $+[0]];
2479 return @matches;
2482 # highlight match (if any), and escape HTML
2483 sub esc_html_match_hl {
2484 my ($str, $regexp) = @_;
2485 return esc_html($str) unless defined $regexp;
2487 my @matches = matchpos_list($str, $regexp);
2488 return esc_html($str) unless @matches;
2490 return esc_html_hl_regions($str, 'match', @matches);
2494 # highlight match (if any) of shortened string, and escape HTML
2495 sub esc_html_match_hl_chopped {
2496 my ($str, $chopped, $regexp) = @_;
2497 return esc_html_match_hl($str, $regexp) unless defined $chopped;
2499 my @matches = matchpos_list($str, $regexp);
2500 return esc_html($chopped) unless @matches;
2502 # filter matches so that we mark chopped string
2503 my $tail = "... "; # see chop_str
2504 unless ($chopped =~ s/\Q$tail\E$//) {
2505 $tail = '';
2507 my $chop_len = length($chopped);
2508 my $tail_len = length($tail);
2509 my @filtered;
2511 for my $m (@matches) {
2512 if ($m->[0] > $chop_len) {
2513 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2514 last;
2515 } elsif ($m->[1] > $chop_len) {
2516 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2517 last;
2519 push @filtered, $m;
2522 return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
2525 ## ----------------------------------------------------------------------
2526 ## functions returning short strings
2528 # CSS class for given age epoch value (in seconds)
2529 # and reference time (optional, defaults to now) as second value
2530 sub age_class {
2531 my ($age_epoch, $time_now) = @_;
2532 return "noage" unless defined $age_epoch;
2533 defined $time_now or $time_now = time;
2534 my $age = $time_now - $age_epoch;
2536 if ($age < 60*60*2) {
2537 return "age0";
2538 } elsif ($age < 60*60*24*2) {
2539 return "age1";
2540 } else {
2541 return "age2";
2545 # convert age epoch in seconds to "nn units ago" string
2546 # reference time used is now unless second argument passed in
2547 # to get the old behavior, pass 0 as the first argument and
2548 # the time in seconds as the second
2549 sub age_string {
2550 my ($age_epoch, $time_now) = @_;
2551 return "unknown" unless defined $age_epoch;
2552 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2553 defined $time_now or $time_now = time;
2554 my $age = $time_now - $age_epoch;
2555 my $age_str;
2557 if ($age > 60*60*24*365*2) {
2558 $age_str = (int $age/60/60/24/365);
2559 $age_str .= " years ago";
2560 } elsif ($age > 60*60*24*(365/12)*2) {
2561 $age_str = int $age/60/60/24/(365/12);
2562 $age_str .= " months ago";
2563 } elsif ($age > 60*60*24*7*2) {
2564 $age_str = int $age/60/60/24/7;
2565 $age_str .= " weeks ago";
2566 } elsif ($age > 60*60*24*2) {
2567 $age_str = int $age/60/60/24;
2568 $age_str .= " days ago";
2569 } elsif ($age > 60*60*2) {
2570 $age_str = int $age/60/60;
2571 $age_str .= " hours ago";
2572 } elsif ($age > 60*2) {
2573 $age_str = int $age/60;
2574 $age_str .= " min ago";
2575 } elsif ($age > 2) {
2576 $age_str = int $age;
2577 $age_str .= " sec ago";
2578 } else {
2579 $age_str .= " right now";
2581 return $age_str;
2584 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2585 # this is typically shown to the user directly with the age_string_age as a title
2586 sub age_string_date {
2587 my ($age_epoch, $time_now) = @_;
2588 return "unknown" unless defined $age_epoch;
2589 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2590 defined $time_now or $time_now = time;
2591 my $age = $time_now - $age_epoch;
2593 if ($age > 60*60*24*7*2) {
2594 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2595 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2596 } else {
2597 return age_string($age_epoch, $time_now);
2601 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2602 # this is typically used for the 'title' attribute so it will show as a tooltip
2603 sub age_string_age {
2604 my ($age_epoch, $time_now) = @_;
2605 return "unknown" unless defined $age_epoch;
2606 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2607 defined $time_now or $time_now = time;
2608 my $age = $time_now - $age_epoch;
2610 if ($age > 60*60*24*7*2) {
2611 return age_string($age_epoch, $time_now);
2612 } else {
2613 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2614 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2618 use constant {
2619 S_IFINVALID => 0030000,
2620 S_IFGITLINK => 0160000,
2623 # submodule/subproject, a commit object reference
2624 sub S_ISGITLINK {
2625 my $mode = shift;
2627 return (($mode & S_IFMT) == S_IFGITLINK)
2630 # convert file mode in octal to symbolic file mode string
2631 sub mode_str {
2632 my $mode = oct shift;
2634 if (S_ISGITLINK($mode)) {
2635 return 'm---------';
2636 } elsif (S_ISDIR($mode & S_IFMT)) {
2637 return 'drwxr-xr-x';
2638 } elsif (S_ISLNK($mode)) {
2639 return 'lrwxrwxrwx';
2640 } elsif (S_ISREG($mode)) {
2641 # git cares only about the executable bit
2642 if ($mode & S_IXUSR) {
2643 return '-rwxr-xr-x';
2644 } else {
2645 return '-rw-r--r--';
2647 } else {
2648 return '----------';
2652 # convert file mode in octal to file type string
2653 sub file_type {
2654 my $mode = shift;
2656 if ($mode !~ m/^[0-7]+$/) {
2657 return $mode;
2658 } else {
2659 $mode = oct $mode;
2662 if (S_ISGITLINK($mode)) {
2663 return "submodule";
2664 } elsif (S_ISDIR($mode & S_IFMT)) {
2665 return "directory";
2666 } elsif (S_ISLNK($mode)) {
2667 return "symlink";
2668 } elsif (S_ISREG($mode)) {
2669 return "file";
2670 } else {
2671 return "unknown";
2675 # convert file mode in octal to file type description string
2676 sub file_type_long {
2677 my $mode = shift;
2679 if ($mode !~ m/^[0-7]+$/) {
2680 return $mode;
2681 } else {
2682 $mode = oct $mode;
2685 if (S_ISGITLINK($mode)) {
2686 return "submodule";
2687 } elsif (S_ISDIR($mode & S_IFMT)) {
2688 return "directory";
2689 } elsif (S_ISLNK($mode)) {
2690 return "symlink";
2691 } elsif (S_ISREG($mode)) {
2692 if ($mode & S_IXUSR) {
2693 return "executable";
2694 } else {
2695 return "file";
2697 } else {
2698 return "unknown";
2703 ## ----------------------------------------------------------------------
2704 ## functions returning short HTML fragments, or transforming HTML fragments
2705 ## which don't belong to other sections
2707 # format line of commit message.
2708 sub format_log_line_html {
2709 my $line = shift;
2711 $line = esc_html($line, -nbsp=>1);
2712 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
2713 $cgi->a({-href => href(action=>"object", hash=>$1),
2714 -class => "text"}, $1);
2715 }eg unless $line =~ /^\s*git-svn-id:/;
2717 return $line;
2720 # format marker of refs pointing to given object
2722 # the destination action is chosen based on object type and current context:
2723 # - for annotated tags, we choose the tag view unless it's the current view
2724 # already, in which case we go to shortlog view
2725 # - for other refs, we keep the current view if we're in history, shortlog or
2726 # log view, and select shortlog otherwise
2727 sub format_ref_marker {
2728 my ($refs, $id) = @_;
2729 my $markers = '';
2731 if (defined $refs->{$id}) {
2732 foreach my $ref (@{$refs->{$id}}) {
2733 # this code exploits the fact that non-lightweight tags are the
2734 # only indirect objects, and that they are the only objects for which
2735 # we want to use tag instead of shortlog as action
2736 my ($type, $name) = qw();
2737 my $indirect = ($ref =~ s/\^\{\}$//);
2738 # e.g. tags/v2.6.11 or heads/next
2739 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2740 $type = $1;
2741 $name = $2;
2742 } else {
2743 $type = "ref";
2744 $name = $ref;
2747 my $class = $type;
2748 $class .= " indirect" if $indirect;
2750 my $dest_action = "shortlog";
2752 if ($indirect) {
2753 $dest_action = "tag" unless $action eq "tag";
2754 } elsif ($action =~ /^(history|(short)?log)$/) {
2755 $dest_action = $action;
2758 my $dest = "";
2759 $dest .= "refs/" unless $ref =~ m!^refs/!;
2760 $dest .= $ref;
2762 my $link = $cgi->a({
2763 -href => href(
2764 action=>$dest_action,
2765 hash=>$dest
2766 )}, $name);
2768 $markers .= "<span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2769 $link . "</span>";
2773 if ($markers) {
2774 return '<span class="refs">'. $markers . '</span>';
2775 } else {
2776 return "";
2780 # format, perhaps shortened and with markers, title line
2781 sub format_subject_html {
2782 my ($long, $short, $href, $extra) = @_;
2783 $extra = '' unless defined($extra);
2785 if (length($short) < length($long)) {
2786 use bytes;
2787 $long =~ s/[[:cntrl:]]/?/g;
2788 return $cgi->a({-href => $href, -class => "list subject",
2789 -title => to_utf8($long)},
2790 esc_html($short)) . $extra;
2791 } else {
2792 return $cgi->a({-href => $href, -class => "list subject"},
2793 esc_html($long)) . $extra;
2797 # Rather than recomputing the url for an email multiple times, we cache it
2798 # after the first hit. This gives a visible benefit in views where the avatar
2799 # for the same email is used repeatedly (e.g. shortlog).
2800 # The cache is shared by all avatar engines (currently gravatar only), which
2801 # are free to use it as preferred. Since only one avatar engine is used for any
2802 # given page, there's no risk for cache conflicts.
2803 our %avatar_cache = ();
2805 # Compute the picon url for a given email, by using the picon search service over at
2806 # http://www.cs.indiana.edu/picons/search.html
2807 sub picon_url {
2808 my $email = lc shift;
2809 if (!$avatar_cache{$email}) {
2810 my ($user, $domain) = split('@', $email);
2811 $avatar_cache{$email} =
2812 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2813 "$domain/$user/" .
2814 "users+domains+unknown/up/single";
2816 return $avatar_cache{$email};
2819 # Compute the gravatar url for a given email, if it's not in the cache already.
2820 # Gravatar stores only the part of the URL before the size, since that's the
2821 # one computationally more expensive. This also allows reuse of the cache for
2822 # different sizes (for this particular engine).
2823 sub gravatar_url {
2824 my $email = lc shift;
2825 my $size = shift;
2826 $avatar_cache{$email} ||=
2827 "//www.gravatar.com/avatar/" .
2828 Digest::MD5::md5_hex($email) . "?s=";
2829 return $avatar_cache{$email} . $size;
2832 # Insert an avatar for the given $email at the given $size if the feature
2833 # is enabled.
2834 sub git_get_avatar {
2835 my ($email, %opts) = @_;
2836 my $pre_white = ($opts{-pad_before} ? "&#160;" : "");
2837 my $post_white = ($opts{-pad_after} ? "&#160;" : "");
2838 $opts{-size} ||= 'default';
2839 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2840 my $url = "";
2841 if ($git_avatar eq 'gravatar') {
2842 $url = gravatar_url($email, $size);
2843 } elsif ($git_avatar eq 'picon') {
2844 $url = picon_url($email);
2846 # Other providers can be added by extending the if chain, defining $url
2847 # as needed. If no variant puts something in $url, we assume avatars
2848 # are completely disabled/unavailable.
2849 if ($url) {
2850 return $pre_white .
2851 "<img width=\"$size\" " .
2852 "class=\"avatar\" " .
2853 "src=\"".esc_url($url)."\" " .
2854 "alt=\"\" " .
2855 "/>" . $post_white;
2856 } else {
2857 return "";
2861 sub format_search_author {
2862 my ($author, $searchtype, $displaytext) = @_;
2863 my $have_search = gitweb_check_feature('search');
2865 if ($have_search) {
2866 my $performed = "";
2867 if ($searchtype eq 'author') {
2868 $performed = "authored";
2869 } elsif ($searchtype eq 'committer') {
2870 $performed = "committed";
2873 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2874 searchtext=>$author,
2875 searchtype=>$searchtype), class=>"list",
2876 title=>"Search for commits $performed by $author"},
2877 $displaytext);
2879 } else {
2880 return $displaytext;
2884 # format the author name of the given commit with the given tag
2885 # the author name is chopped and escaped according to the other
2886 # optional parameters (see chop_str).
2887 sub format_author_html {
2888 my $tag = shift;
2889 my $co = shift;
2890 my $author = chop_and_escape_str($co->{'author_name'}, @_);
2891 return "<$tag class=\"author\">" .
2892 format_search_author($co->{'author_name'}, "author",
2893 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2894 $author) .
2895 "</$tag>";
2898 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2899 sub format_git_diff_header_line {
2900 my $line = shift;
2901 my $diffinfo = shift;
2902 my ($from, $to) = @_;
2904 if ($diffinfo->{'nparents'}) {
2905 # combined diff
2906 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2907 if ($to->{'href'}) {
2908 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2909 esc_path($to->{'file'}));
2910 } else { # file was deleted (no href)
2911 $line .= esc_path($to->{'file'});
2913 } else {
2914 # "ordinary" diff
2915 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2916 if ($from->{'href'}) {
2917 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2918 'a/' . esc_path($from->{'file'}));
2919 } else { # file was added (no href)
2920 $line .= 'a/' . esc_path($from->{'file'});
2922 $line .= ' ';
2923 if ($to->{'href'}) {
2924 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2925 'b/' . esc_path($to->{'file'}));
2926 } else { # file was deleted
2927 $line .= 'b/' . esc_path($to->{'file'});
2931 return "<div class=\"diff header\">$line</div>\n";
2934 # format extended diff header line, before patch itself
2935 sub format_extended_diff_header_line {
2936 my $line = shift;
2937 my $diffinfo = shift;
2938 my ($from, $to) = @_;
2940 # match <path>
2941 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2942 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2943 esc_path($from->{'file'}));
2945 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2946 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2947 esc_path($to->{'file'}));
2949 # match single <mode>
2950 if ($line =~ m/\s(\d{6})$/) {
2951 $line .= '<span class="info"> (' .
2952 file_type_long($1) .
2953 ')</span>';
2955 # match <hash>
2956 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2957 # can match only for combined diff
2958 $line = 'index ';
2959 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2960 if ($from->{'href'}[$i]) {
2961 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2962 -class=>"hash"},
2963 substr($diffinfo->{'from_id'}[$i],0,7));
2964 } else {
2965 $line .= '0' x 7;
2967 # separator
2968 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2970 $line .= '..';
2971 if ($to->{'href'}) {
2972 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2973 substr($diffinfo->{'to_id'},0,7));
2974 } else {
2975 $line .= '0' x 7;
2978 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2979 # can match only for ordinary diff
2980 my ($from_link, $to_link);
2981 if ($from->{'href'}) {
2982 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2983 substr($diffinfo->{'from_id'},0,7));
2984 } else {
2985 $from_link = '0' x 7;
2987 if ($to->{'href'}) {
2988 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2989 substr($diffinfo->{'to_id'},0,7));
2990 } else {
2991 $to_link = '0' x 7;
2993 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2994 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2997 return $line . "<br/>\n";
3000 # format from-file/to-file diff header
3001 sub format_diff_from_to_header {
3002 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3003 my $line;
3004 my $result = '';
3006 $line = $from_line;
3007 #assert($line =~ m/^---/) if DEBUG;
3008 # no extra formatting for "^--- /dev/null"
3009 if (! $diffinfo->{'nparents'}) {
3010 # ordinary (single parent) diff
3011 if ($line =~ m!^--- "?a/!) {
3012 if ($from->{'href'}) {
3013 $line = '--- a/' .
3014 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
3015 esc_path($from->{'file'}));
3016 } else {
3017 $line = '--- a/' .
3018 esc_path($from->{'file'});
3021 $result .= qq!<div class="diff from_file">$line</div>\n!;
3023 } else {
3024 # combined diff (merge commit)
3025 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3026 if ($from->{'href'}[$i]) {
3027 $line = '--- ' .
3028 $cgi->a({-href=>href(action=>"blobdiff",
3029 hash_parent=>$diffinfo->{'from_id'}[$i],
3030 hash_parent_base=>$parents[$i],
3031 file_parent=>$from->{'file'}[$i],
3032 hash=>$diffinfo->{'to_id'},
3033 hash_base=>$hash,
3034 file_name=>$to->{'file'}),
3035 -class=>"path",
3036 -title=>"diff" . ($i+1)},
3037 $i+1) .
3038 '/' .
3039 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
3040 esc_path($from->{'file'}[$i]));
3041 } else {
3042 $line = '--- /dev/null';
3044 $result .= qq!<div class="diff from_file">$line</div>\n!;
3048 $line = $to_line;
3049 #assert($line =~ m/^\+\+\+/) if DEBUG;
3050 # no extra formatting for "^+++ /dev/null"
3051 if ($line =~ m!^\+\+\+ "?b/!) {
3052 if ($to->{'href'}) {
3053 $line = '+++ b/' .
3054 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
3055 esc_path($to->{'file'}));
3056 } else {
3057 $line = '+++ b/' .
3058 esc_path($to->{'file'});
3061 $result .= qq!<div class="diff to_file">$line</div>\n!;
3063 return $result;
3066 # create note for patch simplified by combined diff
3067 sub format_diff_cc_simplified {
3068 my ($diffinfo, @parents) = @_;
3069 my $result = '';
3071 $result .= "<div class=\"diff header\">" .
3072 "diff --cc ";
3073 if (!is_deleted($diffinfo)) {
3074 $result .= $cgi->a({-href => href(action=>"blob",
3075 hash_base=>$hash,
3076 hash=>$diffinfo->{'to_id'},
3077 file_name=>$diffinfo->{'to_file'}),
3078 -class => "path"},
3079 esc_path($diffinfo->{'to_file'}));
3080 } else {
3081 $result .= esc_path($diffinfo->{'to_file'});
3083 $result .= "</div>\n" . # class="diff header"
3084 "<div class=\"diff nodifferences\">" .
3085 "Simple merge" .
3086 "</div>\n"; # class="diff nodifferences"
3088 return $result;
3091 sub diff_line_class {
3092 my ($line, $from, $to) = @_;
3094 # ordinary diff
3095 my $num_sign = 1;
3096 # combined diff
3097 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3098 $num_sign = scalar @{$from->{'href'}};
3101 my @diff_line_classifier = (
3102 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
3103 { regexp => qr/^\\/, class => "incomplete" },
3104 { regexp => qr/^ {$num_sign}/, class => "ctx" },
3105 # classifier for context must come before classifier add/rem,
3106 # or we would have to use more complicated regexp, for example
3107 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3108 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
3109 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
3111 for my $clsfy (@diff_line_classifier) {
3112 return $clsfy->{'class'}
3113 if ($line =~ $clsfy->{'regexp'});
3116 # fallback
3117 return "";
3120 # assumes that $from and $to are defined and correctly filled,
3121 # and that $line holds a line of chunk header for unified diff
3122 sub format_unidiff_chunk_header {
3123 my ($line, $from, $to) = @_;
3125 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3126 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3128 $from_lines = 0 unless defined $from_lines;
3129 $to_lines = 0 unless defined $to_lines;
3131 if ($from->{'href'}) {
3132 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
3133 -class=>"list"}, $from_text);
3135 if ($to->{'href'}) {
3136 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
3137 -class=>"list"}, $to_text);
3139 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3140 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
3141 return $line;
3144 # assumes that $from and $to are defined and correctly filled,
3145 # and that $line holds a line of chunk header for combined diff
3146 sub format_cc_diff_chunk_header {
3147 my ($line, $from, $to) = @_;
3149 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3150 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3152 @from_text = split(' ', $ranges);
3153 for (my $i = 0; $i < @from_text; ++$i) {
3154 ($from_start[$i], $from_nlines[$i]) =
3155 (split(',', substr($from_text[$i], 1)), 0);
3158 $to_text = pop @from_text;
3159 $to_start = pop @from_start;
3160 $to_nlines = pop @from_nlines;
3162 $line = "<span class=\"chunk_info\">$prefix ";
3163 for (my $i = 0; $i < @from_text; ++$i) {
3164 if ($from->{'href'}[$i]) {
3165 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
3166 -class=>"list"}, $from_text[$i]);
3167 } else {
3168 $line .= $from_text[$i];
3170 $line .= " ";
3172 if ($to->{'href'}) {
3173 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
3174 -class=>"list"}, $to_text);
3175 } else {
3176 $line .= $to_text;
3178 $line .= " $prefix</span>" .
3179 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
3180 return $line;
3183 # process patch (diff) line (not to be used for diff headers),
3184 # returning HTML-formatted (but not wrapped) line.
3185 # If the line is passed as a reference, it is treated as HTML and not
3186 # esc_html()'ed.
3187 sub format_diff_line {
3188 my ($line, $diff_class, $from, $to) = @_;
3190 if (ref($line)) {
3191 $line = $$line;
3192 } else {
3193 chomp $line;
3194 $line = untabify($line);
3196 if ($from && $to && $line =~ m/^\@{2} /) {
3197 $line = format_unidiff_chunk_header($line, $from, $to);
3198 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3199 $line = format_cc_diff_chunk_header($line, $from, $to);
3200 } else {
3201 $line = esc_html($line, -nbsp=>1);
3205 my $diff_classes = "diff diff_body";
3206 $diff_classes .= " $diff_class" if ($diff_class);
3207 $line = "<div class=\"$diff_classes\">$line</div>\n";
3209 return $line;
3212 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3213 # linked. Pass the hash of the tree/commit to snapshot.
3214 sub format_snapshot_links {
3215 my ($hash) = @_;
3216 my $num_fmts = @snapshot_fmts;
3217 if ($num_fmts > 1) {
3218 # A parenthesized list of links bearing format names.
3219 # e.g. "snapshot (_tar.gz_ _zip_)"
3220 return "<span class=\"snapshots\">snapshot (" . join(' ', map
3221 $cgi->a({
3222 -href => href(
3223 action=>"snapshot",
3224 hash=>$hash,
3225 snapshot_format=>$_
3227 }, $known_snapshot_formats{$_}{'display'})
3228 , @snapshot_fmts) . ")</span>";
3229 } elsif ($num_fmts == 1) {
3230 # A single "snapshot" link whose tooltip bears the format name.
3231 # i.e. "_snapshot_"
3232 my ($fmt) = @snapshot_fmts;
3233 return "<span class=\"snapshots\">" .
3234 $cgi->a({
3235 -href => href(
3236 action=>"snapshot",
3237 hash=>$hash,
3238 snapshot_format=>$fmt
3240 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
3241 }, "snapshot") . "</span>";
3242 } else { # $num_fmts == 0
3243 return undef;
3247 ## ......................................................................
3248 ## functions returning values to be passed, perhaps after some
3249 ## transformation, to other functions; e.g. returning arguments to href()
3251 # returns hash to be passed to href to generate gitweb URL
3252 # in -title key it returns description of link
3253 sub get_feed_info {
3254 my $format = shift || 'Atom';
3255 my %res = (action => lc($format));
3256 my $matched_ref = 0;
3258 # feed links are possible only for project views
3259 return unless (defined $project);
3260 # some views should link to OPML, or to generic project feed,
3261 # or don't have specific feed yet (so they should use generic)
3262 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3264 my $branch = undef;
3265 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3266 # (fullname) to differentiate from tag links; this also makes
3267 # possible to detect branch links
3268 for my $ref (get_branch_refs()) {
3269 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3270 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3271 $branch = $1;
3272 $matched_ref = $ref;
3273 last;
3276 # find log type for feed description (title)
3277 my $type = 'log';
3278 if (defined $file_name) {
3279 $type = "history of $file_name";
3280 $type .= "/" if ($action eq 'tree');
3281 $type .= " on '$branch'" if (defined $branch);
3282 } else {
3283 $type = "log of $branch" if (defined $branch);
3286 $res{-title} = $type;
3287 $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);
3288 $res{'file_name'} = $file_name;
3290 return %res;
3293 ## ----------------------------------------------------------------------
3294 ## git utility subroutines, invoking git commands
3296 # returns path to the core git executable and the --git-dir parameter as list
3297 sub git_cmd {
3298 $number_of_git_cmds++;
3299 return $GIT, '--git-dir='.$git_dir;
3302 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3303 sub cmd_pipe {
3305 # In order to be compatible with FCGI mode we must use POSIX
3306 # and access the STDERR_FILENO file descriptor directly
3308 use POSIX qw(STDERR_FILENO dup dup2);
3310 open(my $null, '>', File::Spec->devnull) or die "couldn't open devnull: $!";
3311 (my $saveerr = dup(STDERR_FILENO)) or die "couldn't dup STDERR: $!";
3312 my $dup2ok = dup2(fileno($null), STDERR_FILENO);
3313 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3314 $dup2ok or POSIX::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3315 my $result = open(my $fd, "-|", @_);
3316 $dup2ok = dup2($saveerr, STDERR_FILENO);
3317 POSIX::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3318 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3320 return $result ? $fd : undef;
3323 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3324 sub git_cmd_pipe {
3325 return cmd_pipe git_cmd(), @_;
3328 # quote the given arguments for passing them to the shell
3329 # quote_command("command", "arg 1", "arg with ' and ! characters")
3330 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3331 # Try to avoid using this function wherever possible.
3332 sub quote_command {
3333 return join(' ',
3334 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3337 # get HEAD ref of given project as hash
3338 sub git_get_head_hash {
3339 return git_get_full_hash(shift, 'HEAD');
3342 sub git_get_full_hash {
3343 return git_get_hash(@_);
3346 sub git_get_short_hash {
3347 return git_get_hash(@_, '--short=7');
3350 sub git_get_hash {
3351 my ($project, $hash, @options) = @_;
3352 my $o_git_dir = $git_dir;
3353 my $retval = undef;
3354 $git_dir = "$projectroot/$project";
3355 if (defined(my $fd = git_cmd_pipe 'rev-parse',
3356 '--verify', '-q', @options, $hash)) {
3357 $retval = <$fd>;
3358 chomp $retval if defined $retval;
3359 close $fd;
3361 if (defined $o_git_dir) {
3362 $git_dir = $o_git_dir;
3364 return $retval;
3367 # get type of given object
3368 sub git_get_type {
3369 my $hash = shift;
3371 defined(my $fd = git_cmd_pipe "cat-file", '-t', $hash) or return;
3372 my $type = <$fd>;
3373 close $fd or return;
3374 chomp $type;
3375 return $type;
3378 # repository configuration
3379 our $config_file = '';
3380 our %config;
3382 # store multiple values for single key as anonymous array reference
3383 # single values stored directly in the hash, not as [ <value> ]
3384 sub hash_set_multi {
3385 my ($hash, $key, $value) = @_;
3387 if (!exists $hash->{$key}) {
3388 $hash->{$key} = $value;
3389 } elsif (!ref $hash->{$key}) {
3390 $hash->{$key} = [ $hash->{$key}, $value ];
3391 } else {
3392 push @{$hash->{$key}}, $value;
3396 # return hash of git project configuration
3397 # optionally limited to some section, e.g. 'gitweb'
3398 sub git_parse_project_config {
3399 my $section_regexp = shift;
3400 my %config;
3402 local $/ = "\0";
3404 defined(my $fh = git_cmd_pipe "config", '-z', '-l')
3405 or return;
3407 while (my $keyval = to_utf8(scalar <$fh>)) {
3408 chomp $keyval;
3409 my ($key, $value) = split(/\n/, $keyval, 2);
3411 hash_set_multi(\%config, $key, $value)
3412 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3414 close $fh;
3416 return %config;
3419 # convert config value to boolean: 'true' or 'false'
3420 # no value, number > 0, 'true' and 'yes' values are true
3421 # rest of values are treated as false (never as error)
3422 sub config_to_bool {
3423 my $val = shift;
3425 return 1 if !defined $val; # section.key
3427 # strip leading and trailing whitespace
3428 $val =~ s/^\s+//;
3429 $val =~ s/\s+$//;
3431 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3432 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3435 # convert config value to simple decimal number
3436 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3437 # to be multiplied by 1024, 1048576, or 1073741824
3438 sub config_to_int {
3439 my $val = shift;
3441 # strip leading and trailing whitespace
3442 $val =~ s/^\s+//;
3443 $val =~ s/\s+$//;
3445 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3446 $unit = lc($unit);
3447 # unknown unit is treated as 1
3448 return $num * ($unit eq 'g' ? 1073741824 :
3449 $unit eq 'm' ? 1048576 :
3450 $unit eq 'k' ? 1024 : 1);
3452 return $val;
3455 # convert config value to array reference, if needed
3456 sub config_to_multi {
3457 my $val = shift;
3459 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
3462 sub git_get_project_config {
3463 my ($key, $type) = @_;
3465 return unless defined $git_dir;
3467 # key sanity check
3468 return unless ($key);
3469 # only subsection, if exists, is case sensitive,
3470 # and not lowercased by 'git config -z -l'
3471 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3472 $lo =~ s/_//g;
3473 $key = join(".", lc($hi), $mi, lc($lo));
3474 return if ($lo =~ /\W/ || $hi =~ /\W/);
3475 } else {
3476 $key = lc($key);
3477 $key =~ s/_//g;
3478 return if ($key =~ /\W/);
3480 $key =~ s/^gitweb\.//;
3482 # type sanity check
3483 if (defined $type) {
3484 $type =~ s/^--//;
3485 $type = undef
3486 unless ($type eq 'bool' || $type eq 'int');
3489 # get config
3490 if (!defined $config_file ||
3491 $config_file ne "$git_dir/config") {
3492 %config = git_parse_project_config('gitweb');
3493 $config_file = "$git_dir/config";
3496 # check if config variable (key) exists
3497 return unless exists $config{"gitweb.$key"};
3499 # ensure given type
3500 if (!defined $type) {
3501 return $config{"gitweb.$key"};
3502 } elsif ($type eq 'bool') {
3503 # backward compatibility: 'git config --bool' returns true/false
3504 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
3505 } elsif ($type eq 'int') {
3506 return config_to_int($config{"gitweb.$key"});
3508 return $config{"gitweb.$key"};
3511 # get hash of given path at given ref
3512 sub git_get_hash_by_path {
3513 my $base = shift;
3514 my $path = shift || return undef;
3515 my $type = shift;
3517 $path =~ s,/+$,,;
3519 defined(my $fd = git_cmd_pipe "ls-tree", $base, "--", $path)
3520 or die_error(500, "Open git-ls-tree failed");
3521 my $line = to_utf8(scalar <$fd>);
3522 close $fd or return undef;
3524 if (!defined $line) {
3525 # there is no tree or hash given by $path at $base
3526 return undef;
3529 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3530 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3531 if (defined $type && $type ne $2) {
3532 # type doesn't match
3533 return undef;
3535 return $3;
3538 # get path of entry with given hash at given tree-ish (ref)
3539 # used to get 'from' filename for combined diff (merge commit) for renames
3540 sub git_get_path_by_hash {
3541 my $base = shift || return;
3542 my $hash = shift || return;
3544 local $/ = "\0";
3546 defined(my $fd = git_cmd_pipe "ls-tree", '-r', '-t', '-z', $base)
3547 or return undef;
3548 while (my $line = to_utf8(scalar <$fd>)) {
3549 chomp $line;
3551 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3552 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3553 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3554 close $fd;
3555 return $1;
3558 close $fd;
3559 return undef;
3562 ## ......................................................................
3563 ## git utility functions, directly accessing git repository
3565 # get the value of config variable either from file named as the variable
3566 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3567 # configuration variable in the repository config file.
3568 sub git_get_file_or_project_config {
3569 my ($path, $name) = @_;
3571 $git_dir = "$projectroot/$path";
3572 open my $fd, '<', "$git_dir/$name"
3573 or return git_get_project_config($name);
3574 my $conf = to_utf8(scalar <$fd>);
3575 close $fd;
3576 if (defined $conf) {
3577 chomp $conf;
3579 return $conf;
3582 sub git_get_project_description {
3583 my $path = shift;
3584 return git_get_file_or_project_config($path, 'description');
3587 sub git_get_project_category {
3588 my $path = shift;
3589 return git_get_file_or_project_config($path, 'category');
3593 # supported formats:
3594 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3595 # - if its contents is a number, use it as tag weight,
3596 # - otherwise add a tag with weight 1
3597 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3598 # the same value multiple times increases tag weight
3599 # * `gitweb.ctag' multi-valued repo config variable
3600 sub git_get_project_ctags {
3601 my $project = shift;
3602 my $ctags = {};
3604 $git_dir = "$projectroot/$project";
3605 if (opendir my $dh, "$git_dir/ctags") {
3606 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
3607 foreach my $tagfile (@files) {
3608 open my $ct, '<', $tagfile
3609 or next;
3610 my $val = <$ct>;
3611 chomp $val if $val;
3612 close $ct;
3614 (my $ctag = $tagfile) =~ s#.*/##;
3615 $ctag = to_utf8($ctag);
3616 if ($val =~ /^\d+$/) {
3617 $ctags->{$ctag} = $val;
3618 } else {
3619 $ctags->{$ctag} = 1;
3622 closedir $dh;
3624 } elsif (open my $fh, '<', "$git_dir/ctags") {
3625 while (my $line = to_utf8(scalar <$fh>)) {
3626 chomp $line;
3627 $ctags->{$line}++ if $line;
3629 close $fh;
3631 } else {
3632 my $taglist = config_to_multi(git_get_project_config('ctag'));
3633 foreach my $tag (@$taglist) {
3634 $ctags->{$tag}++;
3638 return $ctags;
3641 # return hash, where keys are content tags ('ctags'),
3642 # and values are sum of weights of given tag in every project
3643 sub git_gather_all_ctags {
3644 my $projects = shift;
3645 my $ctags = {};
3647 foreach my $p (@$projects) {
3648 foreach my $ct (keys %{$p->{'ctags'}}) {
3649 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3653 return $ctags;
3656 sub git_populate_project_tagcloud {
3657 my ($ctags, $action) = @_;
3659 # First, merge different-cased tags; tags vote on casing
3660 my %ctags_lc;
3661 foreach (keys %$ctags) {
3662 $ctags_lc{lc $_}->{count} += $ctags->{$_};
3663 if (not $ctags_lc{lc $_}->{topcount}
3664 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
3665 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
3666 $ctags_lc{lc $_}->{topname} = $_;
3670 my $cloud;
3671 my $matched = $input_params{'ctag_filter'};
3672 if (eval { require HTML::TagCloud; 1; }) {
3673 $cloud = HTML::TagCloud->new;
3674 foreach my $ctag (sort keys %ctags_lc) {
3675 # Pad the title with spaces so that the cloud looks
3676 # less crammed.
3677 my $title = esc_html($ctags_lc{$ctag}->{topname});
3678 $title =~ s/ /&#160;/g;
3679 $title =~ s/^/&#160;/g;
3680 $title =~ s/$/&#160;/g;
3681 if (defined $matched && $matched eq $ctag) {
3682 $title = qq(<span class="match">$title</span>);
3684 $cloud->add($title, href(-replay=>1, action=>$action, ctag_filter=>$ctag),
3685 $ctags_lc{$ctag}->{count});
3687 } else {
3688 $cloud = {};
3689 foreach my $ctag (keys %ctags_lc) {
3690 my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
3691 if (defined $matched && $matched eq $ctag) {
3692 $title = qq(<span class="match">$title</span>);
3694 $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
3695 $cloud->{$ctag}{ctag} =
3696 $cgi->a({-href=>href(-replay=>1, action=>$action, ctag_filter=>$ctag)}, $title);
3699 return $cloud;
3702 sub git_show_project_tagcloud {
3703 my ($cloud, $count) = @_;
3704 if (ref $cloud eq 'HTML::TagCloud') {
3705 return $cloud->html_and_css($count);
3706 } else {
3707 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3708 return
3709 '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
3710 join (', ', map {
3711 $cloud->{$_}->{'ctag'}
3712 } splice(@tags, 0, $count)) .
3713 '</div>';
3717 sub git_get_project_url_list {
3718 my $path = shift;
3720 $git_dir = "$projectroot/$path";
3721 open my $fd, '<', "$git_dir/cloneurl"
3722 or return wantarray ?
3723 @{ config_to_multi(git_get_project_config('url')) } :
3724 config_to_multi(git_get_project_config('url'));
3725 my @git_project_url_list = map { chomp; to_utf8($_) } <$fd>;
3726 close $fd;
3728 return wantarray ? @git_project_url_list : \@git_project_url_list;
3731 sub git_get_projects_list {
3732 my $filter = shift || '';
3733 my $paranoid = shift;
3734 my @list;
3736 if (-d $projects_list) {
3737 # search in directory
3738 my $dir = $projects_list;
3739 # remove the trailing "/"
3740 $dir =~ s!/+$!!;
3741 my $pfxlen = length("$dir");
3742 my $pfxdepth = ($dir =~ tr!/!!);
3743 # when filtering, search only given subdirectory
3744 if ($filter && !$paranoid) {
3745 $dir .= "/$filter";
3746 $dir =~ s!/+$!!;
3749 File::Find::find({
3750 follow_fast => 1, # follow symbolic links
3751 follow_skip => 2, # ignore duplicates
3752 dangling_symlinks => 0, # ignore dangling symlinks, silently
3753 wanted => sub {
3754 # global variables
3755 our $project_maxdepth;
3756 our $projectroot;
3757 # skip project-list toplevel, if we get it.
3758 return if (m!^[/.]$!);
3759 # only directories can be git repositories
3760 return unless (-d $_);
3761 # don't traverse too deep (Find is super slow on os x)
3762 # $project_maxdepth excludes depth of $projectroot
3763 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3764 $File::Find::prune = 1;
3765 return;
3768 my $path = substr($File::Find::name, $pfxlen + 1);
3769 # paranoidly only filter here
3770 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3771 next;
3773 # we check related file in $projectroot
3774 if (check_export_ok("$projectroot/$path")) {
3775 push @list, { path => $path };
3776 $File::Find::prune = 1;
3779 }, "$dir");
3781 } elsif (-f $projects_list) {
3782 # read from file(url-encoded):
3783 # 'git%2Fgit.git Linus+Torvalds'
3784 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3785 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3786 open my $fd, '<', $projects_list or return;
3787 PROJECT:
3788 while (my $line = <$fd>) {
3789 chomp $line;
3790 my ($path, $owner) = split ' ', $line;
3791 $path = unescape($path);
3792 $owner = unescape($owner);
3793 if (!defined $path) {
3794 next;
3796 # if $filter is rpovided, check if $path begins with $filter
3797 if ($filter && $path !~ m!^\Q$filter\E/!) {
3798 next;
3800 if (check_export_ok("$projectroot/$path")) {
3801 my $pr = {
3802 path => $path
3804 if ($owner) {
3805 $pr->{'owner'} = to_utf8($owner);
3807 push @list, $pr;
3810 close $fd;
3812 return @list;
3815 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3816 # as side effects it sets 'forks' field to list of forks for forked projects
3817 sub filter_forks_from_projects_list {
3818 my $projects = shift;
3820 my %trie; # prefix tree of directories (path components)
3821 # generate trie out of those directories that might contain forks
3822 foreach my $pr (@$projects) {
3823 my $path = $pr->{'path'};
3824 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3825 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3826 next unless ($path); # skip '.git' repository: tests, git-instaweb
3827 next unless (-d "$projectroot/$path"); # containing directory exists
3828 $pr->{'forks'} = []; # there can be 0 or more forks of project
3830 # add to trie
3831 my @dirs = split('/', $path);
3832 # walk the trie, until either runs out of components or out of trie
3833 my $ref = \%trie;
3834 while (scalar @dirs &&
3835 exists($ref->{$dirs[0]})) {
3836 $ref = $ref->{shift @dirs};
3838 # create rest of trie structure from rest of components
3839 foreach my $dir (@dirs) {
3840 $ref = $ref->{$dir} = {};
3842 # create end marker, store $pr as a data
3843 $ref->{''} = $pr if (!exists $ref->{''});
3846 # filter out forks, by finding shortest prefix match for paths
3847 my @filtered;
3848 PROJECT:
3849 foreach my $pr (@$projects) {
3850 # trie lookup
3851 my $ref = \%trie;
3852 DIR:
3853 foreach my $dir (split('/', $pr->{'path'})) {
3854 if (exists $ref->{''}) {
3855 # found [shortest] prefix, is a fork - skip it
3856 push @{$ref->{''}{'forks'}}, $pr;
3857 next PROJECT;
3859 if (!exists $ref->{$dir}) {
3860 # not in trie, cannot have prefix, not a fork
3861 push @filtered, $pr;
3862 next PROJECT;
3864 # If the dir is there, we just walk one step down the trie.
3865 $ref = $ref->{$dir};
3867 # we ran out of trie
3868 # (shouldn't happen: it's either no match, or end marker)
3869 push @filtered, $pr;
3872 return @filtered;
3875 # note: fill_project_list_info must be run first,
3876 # for 'descr_long' and 'ctags' to be filled
3877 sub search_projects_list {
3878 my ($projlist, %opts) = @_;
3879 my $tagfilter = $opts{'tagfilter'};
3880 my $search_re = $opts{'search_regexp'};
3882 return @$projlist
3883 unless ($tagfilter || $search_re);
3885 # searching projects require filling to be run before it;
3886 fill_project_list_info($projlist,
3887 $tagfilter ? 'ctags' : (),
3888 $search_re ? ('path', 'descr') : ());
3889 my @projects;
3890 PROJECT:
3891 foreach my $pr (@$projlist) {
3893 if ($tagfilter) {
3894 next unless ref($pr->{'ctags'}) eq 'HASH';
3895 next unless
3896 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3899 if ($search_re) {
3900 my $path = $pr->{'path'};
3901 $path =~ s/\.git$//; # should not be included in search
3902 next unless
3903 $path =~ /$search_re/ ||
3904 $pr->{'descr_long'} =~ /$search_re/;
3907 push @projects, $pr;
3910 return @projects;
3913 our $gitweb_project_owner = undef;
3914 sub git_get_project_list_from_file {
3916 return if (defined $gitweb_project_owner);
3918 $gitweb_project_owner = {};
3919 # read from file (url-encoded):
3920 # 'git%2Fgit.git Linus+Torvalds'
3921 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3922 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3923 if (-f $projects_list) {
3924 open(my $fd, '<', $projects_list);
3925 while (my $line = <$fd>) {
3926 chomp $line;
3927 my ($pr, $ow) = split ' ', $line;
3928 $pr = unescape($pr);
3929 $ow = unescape($ow);
3930 $gitweb_project_owner->{$pr} = to_utf8($ow);
3932 close $fd;
3936 sub git_get_project_owner {
3937 my $proj = shift;
3938 my $owner;
3940 return undef unless $proj;
3941 $git_dir = "$projectroot/$proj";
3943 if (defined $project && $proj eq $project) {
3944 $owner = git_get_project_config('owner');
3946 if (!defined $owner && !defined $gitweb_project_owner) {
3947 git_get_project_list_from_file();
3949 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
3950 $owner = $gitweb_project_owner->{$proj};
3952 if (!defined $owner && (!defined $project || $proj ne $project)) {
3953 $owner = git_get_project_config('owner');
3955 if (!defined $owner) {
3956 $owner = get_file_owner("$git_dir");
3959 return $owner;
3962 sub parse_activity_date {
3963 my $dstr = shift;
3965 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
3966 # Unix timestamp
3967 return 0 + $1;
3969 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
3970 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
3971 my $seconds = timegm(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
3972 defined($z) && $z ne '' or $z = 'Z';
3973 $z =~ s/://;
3974 substr($z,1,0) = '0' if length($z) == 4;
3975 my $off = 0;
3976 if (uc($z) ne 'Z') {
3977 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
3978 $off = -$off if substr($z,0,1) eq '-';
3980 return $seconds - $off;
3982 return undef;
3985 # If $quick is true only look at $lastactivity_file
3986 sub git_get_last_activity {
3987 my ($path, $quick) = @_;
3988 my $fd;
3990 $git_dir = "$projectroot/$path";
3991 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
3992 my $activity = <$fd>;
3993 close $fd;
3994 return (undef) unless defined $activity;
3995 chomp $activity;
3996 return (undef) if $activity eq '';
3997 if (my $timestamp = parse_activity_date($activity)) {
3998 return ($timestamp);
4001 return (undef) if $quick;
4002 defined($fd = git_cmd_pipe 'for-each-ref',
4003 '--format=%(committer)',
4004 '--sort=-committerdate',
4005 '--count=1',
4006 map { "refs/$_" } get_branch_refs ()) or return;
4007 my $most_recent = <$fd>;
4008 close $fd or return (undef);
4009 if (defined $most_recent &&
4010 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4011 my $timestamp = $1;
4012 return ($timestamp);
4014 return (undef);
4017 # Implementation note: when a single remote is wanted, we cannot use 'git
4018 # remote show -n' because that command always work (assuming it's a remote URL
4019 # if it's not defined), and we cannot use 'git remote show' because that would
4020 # try to make a network roundtrip. So the only way to find if that particular
4021 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4022 # and when we find what we want.
4023 sub git_get_remotes_list {
4024 my $wanted = shift;
4025 my %remotes = ();
4027 my $fd = git_cmd_pipe 'remote', '-v';
4028 return unless $fd;
4029 while (my $remote = to_utf8(scalar <$fd>)) {
4030 chomp $remote;
4031 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4032 next if $wanted and not $remote eq $wanted;
4033 my ($url, $key) = ($1, $2);
4035 $remotes{$remote} ||= { 'heads' => [] };
4036 $remotes{$remote}{$key} = $url;
4038 close $fd or return;
4039 return wantarray ? %remotes : \%remotes;
4042 # Takes a hash of remotes as first parameter and fills it by adding the
4043 # available remote heads for each of the indicated remotes.
4044 sub fill_remote_heads {
4045 my $remotes = shift;
4046 my @heads = map { "remotes/$_" } keys %$remotes;
4047 my @remoteheads = git_get_heads_list(undef, @heads);
4048 foreach my $remote (keys %$remotes) {
4049 $remotes->{$remote}{'heads'} = [ grep {
4050 $_->{'name'} =~ s!^$remote/!!
4051 } @remoteheads ];
4055 sub git_get_references {
4056 my $type = shift || "";
4057 my %refs;
4058 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4059 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4060 defined(my $fd = git_cmd_pipe "show-ref", "--dereference",
4061 ($type ? ("--", "refs/$type") : ())) # use -- <pattern> if $type
4062 or return;
4064 while (my $line = to_utf8(scalar <$fd>)) {
4065 chomp $line;
4066 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4067 if (defined $refs{$1}) {
4068 push @{$refs{$1}}, $2;
4069 } else {
4070 $refs{$1} = [ $2 ];
4074 close $fd or return;
4075 return \%refs;
4078 sub git_get_rev_name_tags {
4079 my $hash = shift || return undef;
4081 defined(my $fd = git_cmd_pipe "name-rev", "--tags", $hash)
4082 or return;
4083 my $name_rev = to_utf8(scalar <$fd>);
4084 close $fd;
4086 if ($name_rev =~ m|^$hash tags/(.*)$|) {
4087 return $1;
4088 } else {
4089 # catches also '$hash undefined' output
4090 return undef;
4094 ## ----------------------------------------------------------------------
4095 ## parse to hash functions
4097 sub parse_date {
4098 my $epoch = shift;
4099 my $tz = shift || "-0000";
4101 my %date;
4102 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4103 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4104 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4105 $date{'hour'} = $hour;
4106 $date{'minute'} = $min;
4107 $date{'mday'} = $mday;
4108 $date{'day'} = $days[$wday];
4109 $date{'month'} = $months[$mon];
4110 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4111 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4112 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4113 $mday, $months[$mon], $hour ,$min;
4114 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4115 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4117 my ($tz_sign, $tz_hour, $tz_min) =
4118 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4119 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
4120 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4121 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4122 $date{'hour_local'} = $hour;
4123 $date{'minute_local'} = $min;
4124 $date{'mday_local'} = $mday;
4125 $date{'tz_local'} = $tz;
4126 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4127 1900+$year, $mon+1, $mday,
4128 $hour, $min, $sec, $tz);
4129 return %date;
4132 sub parse_file_date {
4133 my $file = shift;
4134 my $mtime = (stat("$projectroot/$project/$file"))[9];
4135 return () unless defined $mtime;
4136 my $tzoffset = timegm((localtime($mtime))[0..5]) - $mtime;
4137 my $tzstring = '+';
4138 if ($tzoffset <= 0) {
4139 $tzstring = '-';
4140 $tzoffset *= -1;
4142 $tzoffset = int($tzoffset/60);
4143 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4144 return parse_date($mtime, $tzstring);
4147 sub parse_tag {
4148 my $tag_id = shift;
4149 my %tag;
4150 my @comment;
4152 defined(my $fd = git_cmd_pipe "cat-file", "tag", $tag_id) or return;
4153 $tag{'id'} = $tag_id;
4154 while (my $line = to_utf8(scalar <$fd>)) {
4155 chomp $line;
4156 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4157 $tag{'object'} = $1;
4158 } elsif ($line =~ m/^type (.+)$/) {
4159 $tag{'type'} = $1;
4160 } elsif ($line =~ m/^tag (.+)$/) {
4161 $tag{'name'} = $1;
4162 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4163 $tag{'author'} = $1;
4164 $tag{'author_epoch'} = $2;
4165 $tag{'author_tz'} = $3;
4166 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4167 $tag{'author_name'} = $1;
4168 $tag{'author_email'} = $2;
4169 } else {
4170 $tag{'author_name'} = $tag{'author'};
4172 } elsif ($line =~ m/--BEGIN/) {
4173 push @comment, $line;
4174 last;
4175 } elsif ($line eq "") {
4176 last;
4179 push @comment, map(to_utf8($_), <$fd>);
4180 $tag{'comment'} = \@comment;
4181 close $fd or return;
4182 if (!defined $tag{'name'}) {
4183 return
4185 return %tag
4188 sub parse_commit_text {
4189 my ($commit_text, $withparents) = @_;
4190 my @commit_lines = split '\n', $commit_text;
4191 my %co;
4193 pop @commit_lines; # Remove '\0'
4195 if (! @commit_lines) {
4196 return;
4199 my $header = shift @commit_lines;
4200 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4201 return;
4203 ($co{'id'}, my @parents) = split ' ', $header;
4204 while (my $line = shift @commit_lines) {
4205 last if $line eq "\n";
4206 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4207 $co{'tree'} = $1;
4208 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4209 push @parents, $1;
4210 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4211 $co{'author'} = to_utf8($1);
4212 $co{'author_epoch'} = $2;
4213 $co{'author_tz'} = $3;
4214 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4215 $co{'author_name'} = $1;
4216 $co{'author_email'} = $2;
4217 } else {
4218 $co{'author_name'} = $co{'author'};
4220 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4221 $co{'committer'} = to_utf8($1);
4222 $co{'committer_epoch'} = $2;
4223 $co{'committer_tz'} = $3;
4224 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4225 $co{'committer_name'} = $1;
4226 $co{'committer_email'} = $2;
4227 } else {
4228 $co{'committer_name'} = $co{'committer'};
4232 if (!defined $co{'tree'}) {
4233 return;
4235 $co{'parents'} = \@parents;
4236 $co{'parent'} = $parents[0];
4238 @commit_lines = map to_utf8($_), @commit_lines;
4239 foreach my $title (@commit_lines) {
4240 $title =~ s/^ //;
4241 if ($title ne "") {
4242 $co{'title'} = chop_str($title, 80, 5);
4243 # remove leading stuff of merges to make the interesting part visible
4244 if (length($title) > 50) {
4245 $title =~ s/^Automatic //;
4246 $title =~ s/^merge (of|with) /Merge ... /i;
4247 if (length($title) > 50) {
4248 $title =~ s/(http|rsync):\/\///;
4250 if (length($title) > 50) {
4251 $title =~ s/(master|www|rsync)\.//;
4253 if (length($title) > 50) {
4254 $title =~ s/kernel.org:?//;
4256 if (length($title) > 50) {
4257 $title =~ s/\/pub\/scm//;
4260 $co{'title_short'} = chop_str($title, 50, 5);
4261 last;
4264 if (! defined $co{'title'} || $co{'title'} eq "") {
4265 $co{'title'} = $co{'title_short'} = '(no commit message)';
4267 # remove added spaces
4268 foreach my $line (@commit_lines) {
4269 $line =~ s/^ //;
4271 $co{'comment'} = \@commit_lines;
4273 my $age_epoch = $co{'committer_epoch'};
4274 $co{'age_epoch'} = $age_epoch;
4275 my $time_now = time;
4276 $co{'age_string'} = age_string($age_epoch, $time_now);
4277 $co{'age_string_date'} = age_string_date($age_epoch, $time_now);
4278 $co{'age_string_age'} = age_string_age($age_epoch, $time_now);
4279 return %co;
4282 sub parse_commit {
4283 my ($commit_id) = @_;
4284 my %co;
4286 local $/ = "\0";
4288 defined(my $fd = git_cmd_pipe "rev-list",
4289 "--parents",
4290 "--header",
4291 "--max-count=1",
4292 $commit_id,
4293 "--")
4294 or die_error(500, "Open git-rev-list failed");
4295 %co = parse_commit_text(<$fd>, 1);
4296 close $fd;
4298 return %co;
4301 sub parse_commits {
4302 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4303 my @cos;
4305 $maxcount ||= 1;
4306 $skip ||= 0;
4308 local $/ = "\0";
4310 defined(my $fd = git_cmd_pipe "rev-list",
4311 "--header",
4312 @args,
4313 ("--max-count=" . $maxcount),
4314 ("--skip=" . $skip),
4315 @extra_options,
4316 $commit_id,
4317 "--",
4318 ($filename ? ($filename) : ()))
4319 or die_error(500, "Open git-rev-list failed");
4320 while (my $line = <$fd>) {
4321 my %co = parse_commit_text($line);
4322 push @cos, \%co;
4324 close $fd;
4326 return wantarray ? @cos : \@cos;
4329 # parse line of git-diff-tree "raw" output
4330 sub parse_difftree_raw_line {
4331 my $line = shift;
4332 my %res;
4334 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4335 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4336 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4337 $res{'from_mode'} = $1;
4338 $res{'to_mode'} = $2;
4339 $res{'from_id'} = $3;
4340 $res{'to_id'} = $4;
4341 $res{'status'} = $5;
4342 $res{'similarity'} = $6;
4343 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4344 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
4345 } else {
4346 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
4349 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4350 # combined diff (for merge commit)
4351 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4352 $res{'nparents'} = length($1);
4353 $res{'from_mode'} = [ split(' ', $2) ];
4354 $res{'to_mode'} = pop @{$res{'from_mode'}};
4355 $res{'from_id'} = [ split(' ', $3) ];
4356 $res{'to_id'} = pop @{$res{'from_id'}};
4357 $res{'status'} = [ split('', $4) ];
4358 $res{'to_file'} = unquote($5);
4360 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4361 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4362 $res{'commit'} = $1;
4365 return wantarray ? %res : \%res;
4368 # wrapper: return parsed line of git-diff-tree "raw" output
4369 # (the argument might be raw line, or parsed info)
4370 sub parsed_difftree_line {
4371 my $line_or_ref = shift;
4373 if (ref($line_or_ref) eq "HASH") {
4374 # pre-parsed (or generated by hand)
4375 return $line_or_ref;
4376 } else {
4377 return parse_difftree_raw_line($line_or_ref);
4381 # parse line of git-ls-tree output
4382 sub parse_ls_tree_line {
4383 my $line = shift;
4384 my %opts = @_;
4385 my %res;
4387 if ($opts{'-l'}) {
4388 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4389 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4391 $res{'mode'} = $1;
4392 $res{'type'} = $2;
4393 $res{'hash'} = $3;
4394 $res{'size'} = $4;
4395 if ($opts{'-z'}) {
4396 $res{'name'} = $5;
4397 } else {
4398 $res{'name'} = unquote($5);
4400 } else {
4401 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4402 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4404 $res{'mode'} = $1;
4405 $res{'type'} = $2;
4406 $res{'hash'} = $3;
4407 if ($opts{'-z'}) {
4408 $res{'name'} = $4;
4409 } else {
4410 $res{'name'} = unquote($4);
4414 return wantarray ? %res : \%res;
4417 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4418 sub parse_from_to_diffinfo {
4419 my ($diffinfo, $from, $to, @parents) = @_;
4421 if ($diffinfo->{'nparents'}) {
4422 # combined diff
4423 $from->{'file'} = [];
4424 $from->{'href'} = [];
4425 fill_from_file_info($diffinfo, @parents)
4426 unless exists $diffinfo->{'from_file'};
4427 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4428 $from->{'file'}[$i] =
4429 defined $diffinfo->{'from_file'}[$i] ?
4430 $diffinfo->{'from_file'}[$i] :
4431 $diffinfo->{'to_file'};
4432 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4433 $from->{'href'}[$i] = href(action=>"blob",
4434 hash_base=>$parents[$i],
4435 hash=>$diffinfo->{'from_id'}[$i],
4436 file_name=>$from->{'file'}[$i]);
4437 } else {
4438 $from->{'href'}[$i] = undef;
4441 } else {
4442 # ordinary (not combined) diff
4443 $from->{'file'} = $diffinfo->{'from_file'};
4444 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4445 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
4446 hash=>$diffinfo->{'from_id'},
4447 file_name=>$from->{'file'});
4448 } else {
4449 delete $from->{'href'};
4453 $to->{'file'} = $diffinfo->{'to_file'};
4454 if (!is_deleted($diffinfo)) { # file exists in result
4455 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
4456 hash=>$diffinfo->{'to_id'},
4457 file_name=>$to->{'file'});
4458 } else {
4459 delete $to->{'href'};
4463 ## ......................................................................
4464 ## parse to array of hashes functions
4466 sub git_get_heads_list {
4467 my ($limit, @classes) = @_;
4468 @classes = get_branch_refs() unless @classes;
4469 my @patterns = map { "refs/$_" } @classes;
4470 my @headslist;
4472 defined(my $fd = git_cmd_pipe 'for-each-ref',
4473 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
4474 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4475 @patterns)
4476 or return;
4477 while (my $line = to_utf8(scalar <$fd>)) {
4478 my %ref_item;
4480 chomp $line;
4481 my ($refinfo, $committerinfo) = split(/\0/, $line);
4482 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4483 my ($committer, $epoch, $tz) =
4484 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4485 $ref_item{'fullname'} = $name;
4486 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
4487 $name =~ s!^refs/($strip_refs|remotes)/!!;
4488 $ref_item{'name'} = $name;
4489 # for refs neither in 'heads' nor 'remotes' we want to
4490 # show their ref dir
4491 my $ref_dir = (defined $1) ? $1 : '';
4492 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4493 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4496 $ref_item{'id'} = $hash;
4497 $ref_item{'title'} = $title || '(no commit message)';
4498 $ref_item{'epoch'} = $epoch;
4499 if ($epoch) {
4500 $ref_item{'age'} = age_string($ref_item{'epoch'});
4501 } else {
4502 $ref_item{'age'} = "unknown";
4505 push @headslist, \%ref_item;
4507 close $fd;
4509 return wantarray ? @headslist : \@headslist;
4512 sub git_get_tags_list {
4513 my $limit = shift;
4514 my @tagslist;
4515 my $all = shift || 0;
4516 my $order = shift || $default_refs_order;
4517 my $sortkey = $all && $order eq 'name' ? 'refname' : '-creatordate';
4519 defined(my $fd = git_cmd_pipe 'for-each-ref',
4520 ($limit ? '--count='.($limit+1) : ()), "--sort=$sortkey",
4521 '--format=%(objectname) %(objecttype) %(refname) '.
4522 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4523 ($all ? 'refs' : 'refs/tags'))
4524 or return;
4525 while (my $line = to_utf8(scalar <$fd>)) {
4526 my %ref_item;
4528 chomp $line;
4529 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4530 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4531 my ($creator, $epoch, $tz) =
4532 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4533 $ref_item{'fullname'} = $name;
4534 $name =~ s!^refs/!! if $all;
4535 $name =~ s!^refs/tags/!! unless $all;
4537 $ref_item{'type'} = $type;
4538 $ref_item{'id'} = $id;
4539 $ref_item{'name'} = $name;
4540 if ($type eq "tag") {
4541 $ref_item{'subject'} = $title;
4542 $ref_item{'reftype'} = $reftype;
4543 $ref_item{'refid'} = $refid;
4544 } else {
4545 $ref_item{'reftype'} = $type;
4546 $ref_item{'refid'} = $id;
4549 if ($type eq "tag" || $type eq "commit") {
4550 $ref_item{'epoch'} = $epoch;
4551 if ($epoch) {
4552 $ref_item{'age'} = age_string($ref_item{'epoch'});
4553 } else {
4554 $ref_item{'age'} = "unknown";
4558 push @tagslist, \%ref_item;
4560 close $fd;
4562 return wantarray ? @tagslist : \@tagslist;
4565 ## ----------------------------------------------------------------------
4566 ## filesystem-related functions
4568 sub get_file_owner {
4569 my $path = shift;
4571 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4572 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4573 if (!defined $gcos) {
4574 return undef;
4576 my $owner = $gcos;
4577 $owner =~ s/[,;].*$//;
4578 return to_utf8($owner);
4581 # assume that file exists
4582 sub insert_file {
4583 my $filename = shift;
4585 open my $fd, '<', $filename;
4586 while (<$fd>) {
4587 print to_utf8($_);
4589 close $fd;
4592 # return undef on failure
4593 sub collect_output {
4594 defined(my $fd = cmd_pipe @_) or return undef;
4595 if (eof $fd) {
4596 close $fd;
4597 return undef;
4599 my $result = join('', map({ to_utf8($_) } <$fd>));
4600 close $fd or return undef;
4601 return $result;
4604 # return undef on failure
4605 # return '' if only comments
4606 sub collect_html_file {
4607 my $filename = shift;
4609 open my $fd, '<', $filename or return undef;
4610 my $result = join('', map({ to_utf8($_) } <$fd>));
4611 close $fd or return undef;
4612 return undef unless defined($result);
4613 my $test = $result;
4614 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4615 $test =~ s/\s+//s;
4616 return $test eq '' ? '' : $result;
4619 ## ......................................................................
4620 ## mimetype related functions
4622 sub mimetype_guess_file {
4623 my $filename = shift;
4624 my $mimemap = shift;
4625 my $rawmode = shift;
4626 -r $mimemap or return undef;
4628 my %mimemap;
4629 open(my $mh, '<', $mimemap) or return undef;
4630 while (<$mh>) {
4631 next if m/^#/; # skip comments
4632 my ($mimetype, @exts) = split(/\s+/);
4633 foreach my $ext (@exts) {
4634 $mimemap{$ext} = $mimetype;
4637 close($mh);
4639 my ($ext, $ans);
4640 $ext = $1 if $filename =~ /\.([^.]*)$/;
4641 $ans = $mimemap{$ext} if $ext;
4642 if (defined $ans) {
4643 my $l = lc($ans);
4644 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4645 if (!$rawmode) {
4646 $ans = 'text/xml' if $l =~ m!^application/[^\s:;,=]+\+xml$! ||
4647 $l eq 'image/svg+xml' ||
4648 $l eq 'application/xml-dtd' ||
4649 $l eq 'application/xml-external-parsed-entity';
4652 return $ans;
4655 sub mimetype_guess {
4656 my $filename = shift;
4657 my $rawmode = shift;
4658 my $mime;
4659 $filename =~ /\./ or return undef;
4661 if ($mimetypes_file) {
4662 my $file = $mimetypes_file;
4663 if ($file !~ m!^/!) { # if it is relative path
4664 # it is relative to project
4665 $file = "$projectroot/$project/$file";
4667 $mime = mimetype_guess_file($filename, $file, $rawmode);
4669 $mime ||= mimetype_guess_file($filename, '/etc/mime.types', $rawmode);
4670 return $mime;
4673 sub blob_mimetype {
4674 my $fd = shift;
4675 my $filename = shift;
4676 my $rawmode = shift;
4677 my $mime;
4679 # The -T/-B file operators produce the wrong result unless a perlio
4680 # layer is present when the file handle is a pipe that delivers less
4681 # than 512 bytes of data before reaching EOF.
4683 # If we are running in a Perl that uses the stdio layer rather than the
4684 # unix+perlio layers we will end up adding a perlio layer on top of the
4685 # stdio layer and get a second level of buffering. This is harmless
4686 # and it makes the -T/-B file operators work properly in all cases.
4688 binmode $fd, ":perlio" or die_error(500, "Adding perlio layer failed")
4689 unless grep /^perlio$/, PerlIO::get_layers($fd);
4691 $mime = mimetype_guess($filename, $rawmode) if defined $filename;
4693 if (!$mime && $filename) {
4694 if ($filename =~ m/\.html?$/i) {
4695 $mime = 'text/html';
4696 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4697 $mime = 'text/html';
4698 } elsif ($filename =~ m/\.te?xt?$/i) {
4699 $mime = 'text/plain';
4700 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4701 $mime = 'text/plain';
4702 } elsif ($filename =~ m/\.png$/i) {
4703 $mime = 'image/png';
4704 } elsif ($filename =~ m/\.gif$/i) {
4705 $mime = 'image/gif';
4706 } elsif ($filename =~ m/\.jpe?g$/i) {
4707 $mime = 'image/jpeg';
4708 } elsif ($filename =~ m/\.svgz?$/i) {
4709 $mime = 'image/svg+xml';
4713 # just in case
4714 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4716 $mime = -T $fd ? 'text/plain' : 'application/octet-stream' unless $mime;
4718 return $mime;
4721 sub is_ascii {
4722 use bytes;
4723 my $data = shift;
4724 return scalar($data =~ /^[\x00-\x7f]*$/);
4727 sub is_valid_utf8 {
4728 my $data = shift;
4729 return utf8::decode($data);
4732 sub extract_html_charset {
4733 return undef unless $_[0] && "$_[0]</head>" =~ m#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4734 my $head = $1;
4735 return $2 if $head =~ m#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4736 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) {
4737 my %kv = (lc($1) => $3, lc($4) => $6);
4738 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4739 return $1 if $he && $c && $he eq 'content-type' &&
4740 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4742 return undef;
4745 sub blob_contenttype {
4746 my ($fd, $file_name, $type) = @_;
4748 $type ||= blob_mimetype($fd, $file_name, 1);
4749 return $type unless $type =~ m!^text/.+!i;
4750 my ($leader, $charset, $htmlcharset);
4751 if ($fd && read($fd, $leader, 32768)) {{
4752 $charset='US-ASCII' if is_ascii($leader);
4753 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8($leader);
4754 $charset='ISO-8859-1' unless $charset;
4755 $htmlcharset = extract_html_charset($leader) if $type eq 'text/html';
4756 if ($htmlcharset && $charset ne 'US-ASCII') {
4757 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4760 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4761 my $defcharset = $default_text_plain_charset || '';
4762 $defcharset =~ s/^\s+//;
4763 $defcharset =~ s/\s+$//;
4764 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4765 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4768 # peek the first upto 128 bytes off a file handle
4769 sub peek128bytes {
4770 my $fd = shift;
4772 use IO::Handle;
4773 use bytes;
4775 my $prefix128;
4776 return '' unless $fd && read($fd, $prefix128, 128);
4778 # In the general case, we're guaranteed only to be able to ungetc one
4779 # character (provided, of course, we actually got a character first).
4781 # However, we know:
4783 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4784 # already been called at least once on the file handle before us
4786 # 2) we have an $fd positioned at the start of the input stream and
4787 # therefore know we were positioned at a buffer boundary before
4788 # reading the initial upto 128 bytes
4790 # 3) the buffer size is at least 512 bytes
4792 # 4) we are careful to only unget raw bytes
4794 # 5) we are attempting to unget exactly the same number of bytes we got
4796 # Given the above conditions we will ALWAYS be able to safely unget
4797 # the $prefix128 value we just got.
4799 # In fact, we could read up to 511 bytes and still be sure.
4800 # (Reading 512 might pop us into the next internal buffer, but probably
4801 # not since that could break the always able to unget at least the one
4802 # you just got guarantee.)
4804 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4806 return $prefix128;
4809 # guess file syntax for syntax highlighting; return undef if no highlighting
4810 # the name of syntax can (in the future) depend on syntax highlighter used
4811 sub guess_file_syntax {
4812 my ($fd, $mimetype, $file_name) = @_;
4813 return undef unless $fd && defined $file_name &&
4814 defined $mimetype && $mimetype =~ m!^text/.+!i;
4815 my $basename = basename($file_name, '.in');
4816 return $highlight_basename{$basename}
4817 if exists $highlight_basename{$basename};
4819 # Peek to see if there's a shebang or xml line.
4820 # We always operate on bytes when testing this.
4822 use bytes;
4823 my $shebang = peek128bytes($fd);
4824 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4825 foreach my $key (keys %highlight_shebang) {
4826 my $ar = ref($highlight_shebang{$key}) ?
4827 $highlight_shebang{$key} :
4828 [$highlight_shebang{key}];
4829 map {return $key if $shebang =~ /$_/} @$ar;
4832 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4835 $basename =~ /\.([^.]*)$/;
4836 my $ext = $1 or return undef;
4837 return $highlight_ext{$ext}
4838 if exists $highlight_ext{$ext};
4840 return undef;
4843 # run highlighter and return FD of its output,
4844 # or return original FD if no highlighting
4845 sub run_highlighter {
4846 my ($fd, $syntax) = @_;
4847 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4849 defined(my $hifd = cmd_pipe $posix_shell_bin, '-c',
4850 quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
4851 quote_command($highlight_bin).
4852 " --replace-tabs=8 --fragment --syntax $syntax")
4853 or die_error(500, "Couldn't open file or run syntax highlighter");
4854 if (eof $hifd) {
4855 # just in case, should not happen as we tested !eof($fd) above
4856 return $fd if close($hifd);
4858 # should not happen
4859 !$! or die_error(500, "Couldn't close syntax highighter pipe");
4861 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4862 # instead of dying horribly on this, just skip the highlighting
4863 # but do output a message about it to STDERR that will end up in the log
4864 print STDERR "warning: skipping failed highlight for --syntax $syntax: ".
4865 sprintf("child exit status 0x%x\n", $?);
4866 return $fd
4868 close $fd;
4869 return ($hifd, 1);
4872 ## ======================================================================
4873 ## functions printing HTML: header, footer, error page
4875 sub get_page_title {
4876 my $title = to_utf8($site_name);
4878 unless (defined $project) {
4879 if (defined $project_filter) {
4880 $title .= " - projects in '" . esc_path($project_filter) . "'";
4882 return $title;
4884 $title .= " - " . to_utf8($project);
4886 return $title unless (defined $action);
4887 my $action_print = $action eq 'blame_incremental' ? 'blame' : $action;
4888 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4890 return $title unless (defined $file_name);
4891 $title .= " - " . esc_path($file_name);
4892 if ($action eq "tree" && $file_name !~ m|/$|) {
4893 $title .= "/";
4896 return $title;
4899 sub get_content_type_html {
4900 # We do not ever emit application/xhtml+xml since that gives us
4901 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4902 # strict, which is troublesome for example when showing user-supplied
4903 # README.html files.
4904 return 'text/html';
4907 sub print_feed_meta {
4908 if (defined $project) {
4909 my %href_params = get_feed_info();
4910 if (!exists $href_params{'-title'}) {
4911 $href_params{'-title'} = 'log';
4914 foreach my $format (qw(RSS Atom)) {
4915 my $type = lc($format);
4916 my %link_attr = (
4917 '-rel' => 'alternate',
4918 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
4919 '-type' => "application/$type+xml"
4922 $href_params{'extra_options'} = undef;
4923 $href_params{'action'} = $type;
4924 $link_attr{'-href'} = href(%href_params);
4925 print "<link ".
4926 "rel=\"$link_attr{'-rel'}\" ".
4927 "title=\"$link_attr{'-title'}\" ".
4928 "href=\"$link_attr{'-href'}\" ".
4929 "type=\"$link_attr{'-type'}\" ".
4930 "/>\n";
4932 $href_params{'extra_options'} = '--no-merges';
4933 $link_attr{'-href'} = href(%href_params);
4934 $link_attr{'-title'} .= ' (no merges)';
4935 print "<link ".
4936 "rel=\"$link_attr{'-rel'}\" ".
4937 "title=\"$link_attr{'-title'}\" ".
4938 "href=\"$link_attr{'-href'}\" ".
4939 "type=\"$link_attr{'-type'}\" ".
4940 "/>\n";
4943 } else {
4944 printf('<link rel="alternate" title="%s projects list" '.
4945 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4946 esc_attr($site_name), href(project=>undef, action=>"project_index"));
4947 printf('<link rel="alternate" title="%s projects feeds" '.
4948 'href="%s" type="text/x-opml" />'."\n",
4949 esc_attr($site_name), href(project=>undef, action=>"opml"));
4953 sub compute_stylesheet_links {
4954 return "<?gitweb compute_stylesheet_links?>" if $cache_mode_active;
4956 # include each stylesheet that exists, providing backwards capability
4957 # for those people who defined $stylesheet in a config file
4958 if (defined $stylesheet) {
4959 return '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4960 } else {
4961 my $sheets = '';
4962 foreach my $stylesheet (@stylesheets) {
4963 next unless $stylesheet;
4964 $sheets .= '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4966 return $sheets;
4970 sub print_header_links {
4971 my $status = shift;
4973 print compute_stylesheet_links();
4974 print_feed_meta()
4975 if ($status eq '200 OK');
4976 if (defined $favicon) {
4977 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
4981 sub print_nav_breadcrumbs_path {
4982 my $dirprefix = undef;
4983 while (my $part = shift) {
4984 $dirprefix .= "/" if defined $dirprefix;
4985 $dirprefix .= $part;
4986 print $cgi->a({-href => href(project => undef,
4987 project_filter => $dirprefix,
4988 action => "project_list")},
4989 esc_html($part)) . " / ";
4993 sub print_nav_breadcrumbs {
4994 my %opts = @_;
4996 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4997 print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / ";
4999 if (defined $project) {
5000 my @dirname = split '/', $project;
5001 my $projectbasename = pop @dirname;
5002 print_nav_breadcrumbs_path(@dirname);
5003 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
5004 if (defined $action) {
5005 my $action_print = $action ;
5006 $action_print = 'blame' if $action_print eq 'blame_incremental';
5007 if (defined $opts{-action_extra}) {
5008 $action_print = $cgi->a({-href => href(action=>$action)},
5009 $action);
5011 print " / $action_print";
5013 if (defined $opts{-action_extra}) {
5014 print " / $opts{-action_extra}";
5016 print "\n";
5017 } elsif (defined $project_filter) {
5018 print_nav_breadcrumbs_path(split '/', $project_filter);
5022 sub print_search_form {
5023 if (!defined $searchtext) {
5024 $searchtext = "";
5026 my $search_hash;
5027 if (defined $hash_base) {
5028 $search_hash = $hash_base;
5029 } elsif (defined $hash) {
5030 $search_hash = $hash;
5031 } else {
5032 $search_hash = "HEAD";
5034 # We can't use href() here because we need to encode the
5035 # URL parameters into the form, not into the action link.
5036 my $action = $my_uri;
5037 my $use_pathinfo = gitweb_check_feature('pathinfo');
5038 if ($use_pathinfo) {
5039 # See notes about doubled / in href()
5040 $action =~ s,/$,,;
5041 $action .= "/".esc_path_info($project);
5043 print $cgi->start_form(-method => "get", -action => $action) .
5044 "<div class=\"search\">\n" .
5045 (!$use_pathinfo &&
5046 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
5047 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
5048 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
5049 $cgi->popup_menu(-name => 'st', -default => 'commit',
5050 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5051 $cgi->sup($cgi->a({-href => href(action=>"search_help"),
5052 -title => "search help" },
5053 "<span style=\"padding-bottom:1em\">?&#160;</span>")) . " search:\n",
5054 $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
5055 "<span title=\"Extended regular expression\">" .
5056 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5057 -checked => $search_use_regexp) .
5058 "</span>" .
5059 "</div>" .
5060 $cgi->end_form() . "\n";
5063 sub git_header_html {
5064 my $status = shift || "200 OK";
5065 my $expires = shift;
5066 my %opts = @_;
5068 my $title = get_page_title();
5069 my $content_type = get_content_type_html();
5070 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
5071 -status=> $status, -expires => $expires)
5072 unless ($opts{'-no_http_header'});
5073 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
5074 print <<EOF;
5075 <?xml version="1.0" encoding="utf-8"?>
5076 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5077 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5078 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5079 <!-- git core binaries version $git_version -->
5080 <head>
5081 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5082 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5083 <meta name="robots" content="index, nofollow"/>
5084 <title>$title</title>
5085 <script type="text/javascript">/* <![CDATA[ */
5086 function fixBlameLinks() {
5087 var allLinks = document.getElementsByTagName("a");
5088 for (var i = 0; i < allLinks.length; i++) {
5089 var link = allLinks.item(i);
5090 if (link.className == 'blamelink')
5091 link.href = link.href.replace("/blame/", "/blame_incremental/");
5094 /* ]]> */</script>
5096 # the stylesheet, favicon etc urls won't work correctly with path_info
5097 # unless we set the appropriate base URL
5098 if ($ENV{'PATH_INFO'}) {
5099 print "<base href=\"".esc_url($base_url)."\" />\n";
5101 print_header_links($status);
5103 if (defined $site_html_head_string) {
5104 print to_utf8($site_html_head_string);
5107 print "</head>\n" .
5108 "<body><span class=\"body\">\n";
5110 if (defined $site_header && -f $site_header) {
5111 insert_file($site_header);
5114 print "<div class=\"page_header\">\n";
5115 print "<span class=\"logo-container\"><span class=\"logo-default\">";
5116 if (defined $logo) {
5117 print $cgi->a({-href => esc_url($logo_url),
5118 -title => $logo_label,
5119 -class => "logo-link"},
5120 $cgi->img({-src => esc_url($logo),
5121 -width => 72, -height => 27,
5122 -alt => "git",
5123 -class => "logo"}));
5125 print "</span></span><span class=\"banner-container\">";
5126 print_nav_breadcrumbs(%opts);
5127 print "</span></div>\n";
5129 my $have_search = gitweb_check_feature('search');
5130 if (defined $project && $have_search) {
5131 print_search_form();
5135 sub compute_timed_interval {
5136 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5137 return tv_interval($t0, [ gettimeofday() ]);
5140 sub compute_commands_count {
5141 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5142 my $s = $number_of_git_cmds == 1 ? '' : 's';
5143 return '<span id="generating_cmd">'.
5144 $number_of_git_cmds.
5145 "</span> git command$s";
5148 sub git_footer_html {
5149 my $feed_class = 'rss_logo';
5151 print "<div class=\"page_footer\">\n";
5152 if (defined $project) {
5153 my $descr = git_get_project_description($project);
5154 if (defined $descr) {
5155 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
5158 my %href_params = get_feed_info();
5159 if (!%href_params) {
5160 $feed_class .= ' generic';
5162 $href_params{'-title'} ||= 'log';
5164 foreach my $format (qw(RSS Atom)) {
5165 $href_params{'action'} = lc($format);
5166 print $cgi->a({-href => href(%href_params),
5167 -title => "$href_params{'-title'} $format feed",
5168 -class => $feed_class}, $format)."\n";
5171 } else {
5172 print $cgi->a({-href => href(project=>undef, action=>"opml",
5173 project_filter => $project_filter),
5174 -class => $feed_class}, "OPML") . " ";
5175 print $cgi->a({-href => href(project=>undef, action=>"project_index",
5176 project_filter => $project_filter),
5177 -class => $feed_class}, "TXT") . "\n";
5179 print "</div>\n"; # class="page_footer"
5181 if (defined $t0 && gitweb_check_feature('timed')) {
5182 print "<div id=\"generating_info\">\n";
5183 print 'This page took '.
5184 '<span id="generating_time" class="time_span">'.
5185 compute_timed_interval().
5186 ' seconds </span>'.
5187 ' and '.
5188 compute_commands_count().
5189 " to generate.\n";
5190 print "</div>\n"; # class="page_footer"
5193 if (defined $site_footer && -f $site_footer) {
5194 insert_file($site_footer);
5197 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
5198 if (defined $action &&
5199 $action eq 'blame_incremental') {
5200 print qq!<script type="text/javascript">\n!.
5201 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
5202 qq! "!. href() .qq!");\n!.
5203 qq!</script>\n!;
5204 } else {
5205 my ($jstimezone, $tz_cookie, $datetime_class) =
5206 gitweb_get_feature('javascript-timezone');
5208 print qq!<script type="text/javascript">\n!.
5209 qq!window.onload = function () {\n!;
5210 if (gitweb_check_feature('blame_incremental')) {
5211 print qq! fixBlameLinks();\n!;
5213 if (gitweb_check_feature('javascript-actions')) {
5214 print qq! fixLinks();\n!;
5216 if ($jstimezone && $tz_cookie && $datetime_class) {
5217 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
5218 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
5220 print qq!};\n!.
5221 qq!</script>\n!;
5224 print "</span></body>\n" .
5225 "</html>";
5228 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5229 # Example: die_error(404, 'Hash not found')
5230 # By convention, use the following status codes (as defined in RFC 2616):
5231 # 400: Invalid or missing CGI parameters, or
5232 # requested object exists but has wrong type.
5233 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5234 # this server or project.
5235 # 404: Requested object/revision/project doesn't exist.
5236 # 500: The server isn't configured properly, or
5237 # an internal error occurred (e.g. failed assertions caused by bugs), or
5238 # an unknown error occurred (e.g. the git binary died unexpectedly).
5239 # 503: The server is currently unavailable (because it is overloaded,
5240 # or down for maintenance). Generally, this is a temporary state.
5241 sub die_error {
5242 my $status = shift || 500;
5243 my $error = esc_html(shift) || "Internal Server Error";
5244 my $extra = shift;
5245 my %opts = @_;
5247 my %http_responses = (
5248 400 => '400 Bad Request',
5249 403 => '403 Forbidden',
5250 404 => '404 Not Found',
5251 500 => '500 Internal Server Error',
5252 503 => '503 Service Unavailable',
5254 git_header_html($http_responses{$status}, undef, %opts);
5255 print <<EOF;
5256 <div class="page_body">
5257 <br /><br />
5258 $status - $error
5259 <br />
5261 if (defined $extra) {
5262 print "<hr />\n" .
5263 "$extra\n";
5265 print "</div>\n";
5267 git_footer_html();
5268 CORE::die
5269 unless ($opts{'-error_handler'});
5272 ## ----------------------------------------------------------------------
5273 ## functions printing or outputting HTML: navigation
5275 # $content is wrapped in a span with class 'tab'
5276 # If $selected is true it also has class 'selected'
5277 # If $disabled is true it also has class 'disabled'
5278 # Whether or not a tab can be disabled and selected at the same time
5279 # is up to the caller
5280 # If $extra_classes is non-empty, it is a whitespace-separated list of
5281 # additional class names to include
5282 # Note that $content MUST already be html-escaped as needed because
5283 # it is included verbatim. And so are any extra class names.
5284 sub tabspan {
5285 my ($content, $selected, $disabled, $extra_classes) = @_;
5286 my @classes = ("tab");
5287 push(@classes, "selected") if $selected;
5288 push(@classes, "disabled") if $disabled;
5289 push(@classes, split(' ', $extra_classes)) if $extra_classes;
5290 return "<span class=\"" . join(" ", @classes) . "\">". $content . "</span>";
5293 sub git_print_page_nav {
5294 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5295 $extra = '' if !defined $extra; # pager or formats
5296 $extra = "<span class=\"page_nav_sub\">".$extra."</span>" if $extra ne '';
5298 my @navs = qw(summary log commit commitdiff tree refs);
5299 if ($suppress) {
5300 @navs = grep { $_ ne $suppress } @navs;
5303 my %arg = map { $_ => {action=>$_} } @navs;
5304 if (defined $head) {
5305 for (qw(commit commitdiff)) {
5306 $arg{$_}{'hash'} = $head;
5308 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5309 $arg{'log'}{'hash'} = $head;
5313 $arg{'log'}{'action'} = 'shortlog';
5314 if ($current eq 'log') {
5315 $current = 'shortlog';
5316 } elsif ($current eq 'shortlog') {
5317 $current = 'log';
5319 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5320 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5322 my @actions = gitweb_get_feature('actions');
5323 my $escname = $project;
5324 $escname =~ s/[+]/%2B/g;
5325 my %repl = (
5326 '%' => '%',
5327 'n' => $project, # project name
5328 'f' => $git_dir, # project path within filesystem
5329 'h' => $treehead || '', # current hash ('h' parameter)
5330 'b' => $treebase || '', # hash base ('hb' parameter)
5331 'e' => $escname, # project name with '+' escaped
5333 while (@actions) {
5334 my ($label, $link, $pos) = splice(@actions,0,3);
5335 # insert
5336 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
5337 # munch munch
5338 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5339 $arg{$label}{'_href'} = $link;
5342 print "<div class=\"page_nav\">\n" .
5343 (join $barsep,
5344 map { $_ eq $current ?
5345 tabspan($_, 1) :
5346 tabspan($cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_"))
5347 } @navs);
5348 print "<br/>\n$extra<br/>\n" .
5349 "</div>\n";
5352 # returns a submenu for the nagivation of the refs views (tags, heads,
5353 # remotes) with the current view disabled and the remotes view only
5354 # available if the feature is enabled
5355 sub format_ref_views {
5356 my ($current) = @_;
5357 my @ref_views = qw{tags heads};
5358 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
5359 return join $barsep, map {
5360 $_ eq $current ? tabspan($_, 1) :
5361 tabspan($cgi->a({-href => href(action=>$_)}, $_))
5362 } @ref_views
5365 sub format_paging_nav {
5366 my ($action, $page, $has_next_link) = @_;
5367 my $paging_nav = "<span class=\"paging_nav\">";
5369 if ($page > 0) {
5370 $paging_nav .= tabspan(
5371 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first")) .
5372 $mdotsep . tabspan(
5373 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5374 -accesskey => "p", -title => "Alt-p"}, "prev"));
5375 } else {
5376 $paging_nav .= tabspan("first", 1).${mdotsep}.tabspan("prev", 0, 1);
5379 if ($has_next_link) {
5380 $paging_nav .= $mdotsep . tabspan(
5381 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5382 -accesskey => "n", -title => "Alt-n"}, "next"));
5383 } else {
5384 $paging_nav .= ${mdotsep}.tabspan("next", 0, 1);
5387 return $paging_nav."</span>";
5390 sub format_log_nav {
5391 my ($action, $page, $has_next_link, $extra) = @_;
5392 my $paging_nav;
5393 defined $extra or $extra = '';
5394 $extra eq '' or $extra .= $barsep;
5396 if ($action eq 'shortlog') {
5397 $paging_nav .= tabspan('shortlog', 1);
5398 } else {
5399 $paging_nav .= tabspan($cgi->a({-href => href(action=>'shortlog', -replay=>1)}, 'shortlog'));
5401 $paging_nav .= $barsep;
5402 if ($action eq 'log') {
5403 $paging_nav .= tabspan('fulllog', 1);
5404 } else {
5405 $paging_nav .= tabspan($cgi->a({-href => href(action=>'log', -replay=>1)}, 'fulllog'));
5408 $paging_nav .= $barsep . $extra . format_paging_nav($action, $page, $has_next_link);
5409 return $paging_nav;
5412 ## ......................................................................
5413 ## functions printing or outputting HTML: div
5415 sub git_print_header_div {
5416 my ($action, $title, $hash, $hash_base, $extra) = @_;
5417 my %args = ();
5418 defined $extra or $extra = '';
5420 $args{'action'} = $action;
5421 $args{'hash'} = $hash if $hash;
5422 $args{'hash_base'} = $hash_base if $hash_base;
5424 my $link1 = $cgi->a({-href => href(%args), -class => "title"},
5425 $title ? $title : $action);
5426 my $link2 = $cgi->a({-href => href(%args), -class => "cover"}, "");
5427 print "<div class=\"header\">\n" . '<span class="title">' .
5428 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5431 sub format_repo_url {
5432 my ($name, $url) = @_;
5433 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5436 # Group output by placing it in a DIV element and adding a header.
5437 # Options for start_div() can be provided by passing a hash reference as the
5438 # first parameter to the function.
5439 # Options to git_print_header_div() can be provided by passing an array
5440 # reference. This must follow the options to start_div if they are present.
5441 # The content can be a scalar, which is output as-is, a scalar reference, which
5442 # is output after html escaping, an IO handle passed either as *handle or
5443 # *handle{IO}, or a function reference. In the latter case all following
5444 # parameters will be taken as argument to the content function call.
5445 sub git_print_section {
5446 my ($div_args, $header_args, $content);
5447 my $arg = shift;
5448 if (ref($arg) eq 'HASH') {
5449 $div_args = $arg;
5450 $arg = shift;
5452 if (ref($arg) eq 'ARRAY') {
5453 $header_args = $arg;
5454 $arg = shift;
5456 $content = $arg;
5458 print $cgi->start_div($div_args);
5459 git_print_header_div(@$header_args);
5461 if (ref($content) eq 'CODE') {
5462 $content->(@_);
5463 } elsif (ref($content) eq 'SCALAR') {
5464 print esc_html($$content);
5465 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5466 while (<$content>) {
5467 print to_utf8($_);
5469 } elsif (!ref($content) && defined($content)) {
5470 print $content;
5473 print $cgi->end_div;
5476 sub format_timestamp_html {
5477 my $date = shift;
5478 my $useatnight = shift;
5479 defined($useatnight) or $useatnight = 1;
5480 my $strtime = $date->{'rfc2822'};
5482 my (undef, undef, $datetime_class) =
5483 gitweb_get_feature('javascript-timezone');
5484 if ($datetime_class) {
5485 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
5488 my $localtime_format = '(%d %02d:%02d %s)';
5489 if ($useatnight && $date->{'hour_local'} < 6) {
5490 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5492 $strtime .= ' ' .
5493 sprintf($localtime_format, $date->{'mday_local'},
5494 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5496 return $strtime;
5499 sub format_lastrefresh_row {
5500 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5501 my %rd = parse_file_date('.last_refresh');
5502 if (defined $rd{'rfc2822'}) {
5503 return "<tr id=\"metadata_lrefresh\"><td>last&#160;refresh</td>" .
5504 "<td>".format_timestamp_html(\%rd,0)."</td></tr>";
5506 return "";
5509 # Outputs the author name and date in long form
5510 sub git_print_authorship {
5511 my $co = shift;
5512 my %opts = @_;
5513 my $tag = $opts{-tag} || 'div';
5514 my $author = $co->{'author_name'};
5516 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
5517 print "<$tag class=\"author_date\">" .
5518 format_search_author($author, "author", esc_html($author)) .
5519 " [".format_timestamp_html(\%ad)."]".
5520 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
5521 "</$tag>\n";
5524 # Outputs table rows containing the full author or committer information,
5525 # in the format expected for 'commit' view (& similar).
5526 # Parameters are a commit hash reference, followed by the list of people
5527 # to output information for. If the list is empty it defaults to both
5528 # author and committer.
5529 sub git_print_authorship_rows {
5530 my $co = shift;
5531 # too bad we can't use @people = @_ || ('author', 'committer')
5532 my @people = @_;
5533 @people = ('author', 'committer') unless @people;
5534 foreach my $who (@people) {
5535 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5536 print "<tr><td>$who</td><td>" .
5537 format_search_author($co->{"${who}_name"}, $who,
5538 esc_html($co->{"${who}_name"})) . " " .
5539 format_search_author($co->{"${who}_email"}, $who,
5540 esc_html("<" . $co->{"${who}_email"} . ">")) .
5541 "</td><td rowspan=\"2\">" .
5542 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
5543 "</td></tr>\n" .
5544 "<tr>" .
5545 "<td></td><td>" .
5546 format_timestamp_html(\%wd) .
5547 "</td>" .
5548 "</tr>\n";
5552 sub git_print_page_path {
5553 my $name = shift;
5554 my $type = shift;
5555 my $hb = shift;
5558 print "<div class=\"page_path\">";
5559 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
5560 -title => 'tree root'}, to_utf8("[$project]"));
5561 print " / ";
5562 if (defined $name) {
5563 my @dirname = split '/', $name;
5564 my $basename = pop @dirname;
5565 my $fullname = '';
5567 foreach my $dir (@dirname) {
5568 $fullname .= ($fullname ? '/' : '') . $dir;
5569 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
5570 hash_base=>$hb),
5571 -title => $fullname}, esc_path($dir));
5572 print " / ";
5574 if (defined $type && $type eq 'blob') {
5575 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
5576 hash_base=>$hb),
5577 -title => $name}, esc_path($basename));
5578 } elsif (defined $type && $type eq 'tree') {
5579 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
5580 hash_base=>$hb),
5581 -title => $name}, esc_path($basename));
5582 print " / ";
5583 } else {
5584 print esc_path($basename);
5587 print "<br/></div>\n";
5590 sub git_print_log {
5591 my $log = shift;
5592 my %opts = @_;
5594 if ($opts{'-remove_title'}) {
5595 # remove title, i.e. first line of log
5596 shift @$log;
5598 # remove leading empty lines
5599 while (defined $log->[0] && $log->[0] eq "") {
5600 shift @$log;
5603 # print log
5604 my $skip_blank_line = 0;
5605 foreach my $line (@$log) {
5606 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5607 if (! $opts{'-remove_signoff'}) {
5608 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
5609 $skip_blank_line = 1;
5611 next;
5614 if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
5615 if (! $opts{'-remove_signoff'}) {
5616 print "<span class=\"signoff\">" . esc_html($1) . ": " .
5617 "<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
5618 "</span><br/>\n";
5619 $skip_blank_line = 1;
5621 next;
5624 # print only one empty line
5625 # do not print empty line after signoff
5626 if ($line eq "") {
5627 next if ($skip_blank_line);
5628 $skip_blank_line = 1;
5629 } else {
5630 $skip_blank_line = 0;
5633 print format_log_line_html($line) . "<br/>\n";
5636 if ($opts{'-final_empty_line'}) {
5637 # end with single empty line
5638 print "<br/>\n" unless $skip_blank_line;
5642 # return link target (what link points to)
5643 sub git_get_link_target {
5644 my $hash = shift;
5645 my $link_target;
5647 # read link
5648 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
5649 or return;
5651 local $/ = undef;
5652 $link_target = to_utf8(scalar <$fd>);
5654 close $fd
5655 or return;
5657 return $link_target;
5660 # given link target, and the directory (basedir) the link is in,
5661 # return target of link relative to top directory (top tree);
5662 # return undef if it is not possible (including absolute links).
5663 sub normalize_link_target {
5664 my ($link_target, $basedir) = @_;
5666 # absolute symlinks (beginning with '/') cannot be normalized
5667 return if (substr($link_target, 0, 1) eq '/');
5669 # normalize link target to path from top (root) tree (dir)
5670 my $path;
5671 if ($basedir) {
5672 $path = $basedir . '/' . $link_target;
5673 } else {
5674 # we are in top (root) tree (dir)
5675 $path = $link_target;
5678 # remove //, /./, and /../
5679 my @path_parts;
5680 foreach my $part (split('/', $path)) {
5681 # discard '.' and ''
5682 next if (!$part || $part eq '.');
5683 # handle '..'
5684 if ($part eq '..') {
5685 if (@path_parts) {
5686 pop @path_parts;
5687 } else {
5688 # link leads outside repository (outside top dir)
5689 return;
5691 } else {
5692 push @path_parts, $part;
5695 $path = join('/', @path_parts);
5697 return $path;
5700 # print tree entry (row of git_tree), but without encompassing <tr> element
5701 sub git_print_tree_entry {
5702 my ($t, $basedir, $hash_base, $have_blame) = @_;
5704 my %base_key = ();
5705 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5707 # The format of a table row is: mode list link. Where mode is
5708 # the mode of the entry, list is the name of the entry, an href,
5709 # and link is the action links of the entry.
5711 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
5712 if (exists $t->{'size'}) {
5713 print "<td class=\"size\">$t->{'size'}</td>\n";
5715 if ($t->{'type'} eq "blob") {
5716 print "<td class=\"list\">" .
5717 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
5718 file_name=>"$basedir$t->{'name'}", %base_key),
5719 -class => "list"}, esc_path($t->{'name'}));
5720 if (S_ISLNK(oct $t->{'mode'})) {
5721 my $link_target = git_get_link_target($t->{'hash'});
5722 if ($link_target) {
5723 my $norm_target = normalize_link_target($link_target, $basedir);
5724 if (defined $norm_target) {
5725 print " -> " .
5726 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
5727 file_name=>$norm_target),
5728 -title => $norm_target}, esc_path($link_target));
5729 } else {
5730 print " -> " . esc_path($link_target);
5734 print "</td>\n";
5735 print "<td class=\"link\">";
5736 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
5737 file_name=>"$basedir$t->{'name'}", %base_key)},
5738 "blob");
5739 if ($have_blame) {
5740 print $barsep .
5741 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
5742 file_name=>"$basedir$t->{'name'}", %base_key),
5743 -class => "blamelink"},
5744 "blame");
5746 if (defined $hash_base) {
5747 print $barsep .
5748 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
5749 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
5750 "history");
5752 print $barsep .
5753 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
5754 file_name=>"$basedir$t->{'name'}")},
5755 "raw");
5756 print "</td>\n";
5758 } elsif ($t->{'type'} eq "tree") {
5759 print "<td class=\"list\">";
5760 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
5761 file_name=>"$basedir$t->{'name'}",
5762 %base_key)},
5763 esc_path($t->{'name'}));
5764 print "</td>\n";
5765 print "<td class=\"link\">";
5766 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
5767 file_name=>"$basedir$t->{'name'}",
5768 %base_key)},
5769 "tree");
5770 if (defined $hash_base) {
5771 print $barsep .
5772 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
5773 file_name=>"$basedir$t->{'name'}")},
5774 "history");
5776 print "</td>\n";
5777 } else {
5778 # unknown object: we can only present history for it
5779 # (this includes 'commit' object, i.e. submodule support)
5780 print "<td class=\"list\">" .
5781 esc_path($t->{'name'}) .
5782 "</td>\n";
5783 print "<td class=\"link\">";
5784 if (defined $hash_base) {
5785 print $cgi->a({-href => href(action=>"history",
5786 hash_base=>$hash_base,
5787 file_name=>"$basedir$t->{'name'}")},
5788 "history");
5790 print "</td>\n";
5794 ## ......................................................................
5795 ## functions printing large fragments of HTML
5797 # get pre-image filenames for merge (combined) diff
5798 sub fill_from_file_info {
5799 my ($diff, @parents) = @_;
5801 $diff->{'from_file'} = [ ];
5802 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5803 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5804 if ($diff->{'status'}[$i] eq 'R' ||
5805 $diff->{'status'}[$i] eq 'C') {
5806 $diff->{'from_file'}[$i] =
5807 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
5811 return $diff;
5814 # is current raw difftree line of file deletion
5815 sub is_deleted {
5816 my $diffinfo = shift;
5818 return $diffinfo->{'to_id'} eq ('0' x 40);
5821 # does patch correspond to [previous] difftree raw line
5822 # $diffinfo - hashref of parsed raw diff format
5823 # $patchinfo - hashref of parsed patch diff format
5824 # (the same keys as in $diffinfo)
5825 sub is_patch_split {
5826 my ($diffinfo, $patchinfo) = @_;
5828 return defined $diffinfo && defined $patchinfo
5829 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5833 sub git_difftree_body {
5834 my ($difftree, $hash, @parents) = @_;
5835 my ($parent) = $parents[0];
5836 my $have_blame = gitweb_check_feature('blame');
5837 print "<div class=\"list_head\">\n";
5838 if ($#{$difftree} > 10) {
5839 print(($#{$difftree} + 1) . " files changed:\n");
5841 print "</div>\n";
5843 print "<table class=\"" .
5844 (@parents > 1 ? "combined " : "") .
5845 "diff_tree\">\n";
5847 # header only for combined diff in 'commitdiff' view
5848 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
5849 if ($has_header) {
5850 # table header
5851 print "<thead><tr>\n" .
5852 "<th></th><th></th>\n"; # filename, patchN link
5853 for (my $i = 0; $i < @parents; $i++) {
5854 my $par = $parents[$i];
5855 print "<th>" .
5856 $cgi->a({-href => href(action=>"commitdiff",
5857 hash=>$hash, hash_parent=>$par),
5858 -title => 'commitdiff to parent number ' .
5859 ($i+1) . ': ' . substr($par,0,7)},
5860 $i+1) .
5861 "&#160;</th>\n";
5863 print "</tr></thead>\n<tbody>\n";
5866 my $alternate = 1;
5867 my $patchno = 0;
5868 foreach my $line (@{$difftree}) {
5869 my $diff = parsed_difftree_line($line);
5871 if ($alternate) {
5872 print "<tr class=\"dark\">\n";
5873 } else {
5874 print "<tr class=\"light\">\n";
5876 $alternate ^= 1;
5878 if (exists $diff->{'nparents'}) { # combined diff
5880 fill_from_file_info($diff, @parents)
5881 unless exists $diff->{'from_file'};
5883 if (!is_deleted($diff)) {
5884 # file exists in the result (child) commit
5885 print "<td>" .
5886 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5887 file_name=>$diff->{'to_file'},
5888 hash_base=>$hash),
5889 -class => "list"}, esc_path($diff->{'to_file'})) .
5890 "</td>\n";
5891 } else {
5892 print "<td>" .
5893 esc_path($diff->{'to_file'}) .
5894 "</td>\n";
5897 if ($action eq 'commitdiff') {
5898 # link to patch
5899 $patchno++;
5900 print "<td class=\"link\">" .
5901 $cgi->a({-href => href(-anchor=>"patch$patchno")},
5902 "patch") .
5903 $barsep .
5904 "</td>\n";
5907 my $has_history = 0;
5908 my $not_deleted = 0;
5909 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5910 my $hash_parent = $parents[$i];
5911 my $from_hash = $diff->{'from_id'}[$i];
5912 my $from_path = $diff->{'from_file'}[$i];
5913 my $status = $diff->{'status'}[$i];
5915 $has_history ||= ($status ne 'A');
5916 $not_deleted ||= ($status ne 'D');
5918 if ($status eq 'A') {
5919 print "<td class=\"link\" align=\"right\">$barsep</td>\n";
5920 } elsif ($status eq 'D') {
5921 print "<td class=\"link\">" .
5922 $cgi->a({-href => href(action=>"blob",
5923 hash_base=>$hash,
5924 hash=>$from_hash,
5925 file_name=>$from_path)},
5926 "blob" . ($i+1)) .
5927 "$barsep</td>\n";
5928 } else {
5929 if ($diff->{'to_id'} eq $from_hash) {
5930 print "<td class=\"link nochange\">";
5931 } else {
5932 print "<td class=\"link\">";
5934 print $cgi->a({-href => href(action=>"blobdiff",
5935 hash=>$diff->{'to_id'},
5936 hash_parent=>$from_hash,
5937 hash_base=>$hash,
5938 hash_parent_base=>$hash_parent,
5939 file_name=>$diff->{'to_file'},
5940 file_parent=>$from_path)},
5941 "diff" . ($i+1)) .
5942 "$barsep</td>\n";
5946 print "<td class=\"link\">";
5947 if ($not_deleted) {
5948 print $cgi->a({-href => href(action=>"blob",
5949 hash=>$diff->{'to_id'},
5950 file_name=>$diff->{'to_file'},
5951 hash_base=>$hash)},
5952 "blob");
5953 print $barsep if ($has_history);
5955 if ($has_history) {
5956 print $cgi->a({-href => href(action=>"history",
5957 file_name=>$diff->{'to_file'},
5958 hash_base=>$hash)},
5959 "history");
5961 print "</td>\n";
5963 print "</tr>\n";
5964 next; # instead of 'else' clause, to avoid extra indent
5966 # else ordinary diff
5968 my ($to_mode_oct, $to_mode_str, $to_file_type);
5969 my ($from_mode_oct, $from_mode_str, $from_file_type);
5970 if ($diff->{'to_mode'} ne ('0' x 6)) {
5971 $to_mode_oct = oct $diff->{'to_mode'};
5972 if (S_ISREG($to_mode_oct)) { # only for regular file
5973 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
5975 $to_file_type = file_type($diff->{'to_mode'});
5977 if ($diff->{'from_mode'} ne ('0' x 6)) {
5978 $from_mode_oct = oct $diff->{'from_mode'};
5979 if (S_ISREG($from_mode_oct)) { # only for regular file
5980 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5982 $from_file_type = file_type($diff->{'from_mode'});
5985 if ($diff->{'status'} eq "A") { # created
5986 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5987 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
5988 $mode_chng .= "]</span>";
5989 print "<td>";
5990 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5991 hash_base=>$hash, file_name=>$diff->{'file'}),
5992 -class => "list"}, esc_path($diff->{'file'}));
5993 print "</td>\n";
5994 print "<td>$mode_chng</td>\n";
5995 print "<td class=\"link\">";
5996 if ($action eq 'commitdiff') {
5997 # link to patch
5998 $patchno++;
5999 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6000 "patch") .
6001 $barsep;
6003 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6004 hash_base=>$hash, file_name=>$diff->{'file'})},
6005 "blob");
6006 print "</td>\n";
6008 } elsif ($diff->{'status'} eq "D") { # deleted
6009 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6010 print "<td>";
6011 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
6012 hash_base=>$parent, file_name=>$diff->{'file'}),
6013 -class => "list"}, esc_path($diff->{'file'}));
6014 print "</td>\n";
6015 print "<td>$mode_chng</td>\n";
6016 print "<td class=\"link\">";
6017 if ($action eq 'commitdiff') {
6018 # link to patch
6019 $patchno++;
6020 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6021 "patch") .
6022 $barsep;
6024 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
6025 hash_base=>$parent, file_name=>$diff->{'file'})},
6026 "blob") . $barsep;
6027 if ($have_blame) {
6028 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
6029 file_name=>$diff->{'file'}),
6030 -class => "blamelink"},
6031 "blame") . $barsep;
6033 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
6034 file_name=>$diff->{'file'})},
6035 "history");
6036 print "</td>\n";
6038 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6039 my $mode_chnge = "";
6040 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6041 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6042 if ($from_file_type ne $to_file_type) {
6043 $mode_chnge .= " from $from_file_type to $to_file_type";
6045 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6046 if ($from_mode_str && $to_mode_str) {
6047 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6048 } elsif ($to_mode_str) {
6049 $mode_chnge .= " mode: $to_mode_str";
6052 $mode_chnge .= "]</span>\n";
6054 print "<td>";
6055 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6056 hash_base=>$hash, file_name=>$diff->{'file'}),
6057 -class => "list"}, esc_path($diff->{'file'}));
6058 print "</td>\n";
6059 print "<td>$mode_chnge</td>\n";
6060 print "<td class=\"link\">";
6061 if ($action eq 'commitdiff') {
6062 # link to patch
6063 $patchno++;
6064 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6065 "patch") .
6066 $barsep;
6067 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6068 # "commit" view and modified file (not onlu mode changed)
6069 print $cgi->a({-href => href(action=>"blobdiff",
6070 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
6071 hash_base=>$hash, hash_parent_base=>$parent,
6072 file_name=>$diff->{'file'})},
6073 "diff") .
6074 $barsep;
6076 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6077 hash_base=>$hash, file_name=>$diff->{'file'})},
6078 "blob") . $barsep;
6079 if ($have_blame) {
6080 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
6081 file_name=>$diff->{'file'}),
6082 -class => "blamelink"},
6083 "blame") . $barsep;
6085 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
6086 file_name=>$diff->{'file'})},
6087 "history");
6088 print "</td>\n";
6090 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6091 my %status_name = ('R' => 'moved', 'C' => 'copied');
6092 my $nstatus = $status_name{$diff->{'status'}};
6093 my $mode_chng = "";
6094 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6095 # mode also for directories, so we cannot use $to_mode_str
6096 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6098 print "<td>" .
6099 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
6100 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
6101 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
6102 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6103 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
6104 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
6105 -class => "list"}, esc_path($diff->{'from_file'})) .
6106 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6107 "<td class=\"link\">";
6108 if ($action eq 'commitdiff') {
6109 # link to patch
6110 $patchno++;
6111 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6112 "patch") .
6113 $barsep;
6114 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6115 # "commit" view and modified file (not only pure rename or copy)
6116 print $cgi->a({-href => href(action=>"blobdiff",
6117 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
6118 hash_base=>$hash, hash_parent_base=>$parent,
6119 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
6120 "diff") .
6121 $barsep;
6123 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6124 hash_base=>$parent, file_name=>$diff->{'to_file'})},
6125 "blob") . $barsep;
6126 if ($have_blame) {
6127 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
6128 file_name=>$diff->{'to_file'}),
6129 -class => "blamelink"},
6130 "blame") . $barsep;
6132 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
6133 file_name=>$diff->{'to_file'})},
6134 "history");
6135 print "</td>\n";
6137 } # we should not encounter Unmerged (U) or Unknown (X) status
6138 print "</tr>\n";
6140 print "</tbody>" if $has_header;
6141 print "</table>\n";
6144 # Print context lines and then rem/add lines in a side-by-side manner.
6145 sub print_sidebyside_diff_lines {
6146 my ($ctx, $rem, $add) = @_;
6148 # print context block before add/rem block
6149 if (@$ctx) {
6150 print join '',
6151 '<div class="chunk_block ctx">',
6152 '<div class="old">',
6153 @$ctx,
6154 '</div>',
6155 '<div class="new">',
6156 @$ctx,
6157 '</div>',
6158 '</div>';
6161 if (!@$add) {
6162 # pure removal
6163 print join '',
6164 '<div class="chunk_block rem">',
6165 '<div class="old">',
6166 @$rem,
6167 '</div>',
6168 '</div>';
6169 } elsif (!@$rem) {
6170 # pure addition
6171 print join '',
6172 '<div class="chunk_block add">',
6173 '<div class="new">',
6174 @$add,
6175 '</div>',
6176 '</div>';
6177 } else {
6178 print join '',
6179 '<div class="chunk_block chg">',
6180 '<div class="old">',
6181 @$rem,
6182 '</div>',
6183 '<div class="new">',
6184 @$add,
6185 '</div>',
6186 '</div>';
6190 # Print context lines and then rem/add lines in inline manner.
6191 sub print_inline_diff_lines {
6192 my ($ctx, $rem, $add) = @_;
6194 print @$ctx, @$rem, @$add;
6197 # Format removed and added line, mark changed part and HTML-format them.
6198 # Implementation is based on contrib/diff-highlight
6199 sub format_rem_add_lines_pair {
6200 my ($rem, $add, $num_parents) = @_;
6202 # We need to untabify lines before split()'ing them;
6203 # otherwise offsets would be invalid.
6204 chomp $rem;
6205 chomp $add;
6206 $rem = untabify($rem);
6207 $add = untabify($add);
6209 my @rem = split(//, $rem);
6210 my @add = split(//, $add);
6211 my ($esc_rem, $esc_add);
6212 # Ignore leading +/- characters for each parent.
6213 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6214 my ($prefix_has_nonspace, $suffix_has_nonspace);
6216 my $shorter = (@rem < @add) ? @rem : @add;
6217 while ($prefix_len < $shorter) {
6218 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6220 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6221 $prefix_len++;
6224 while ($prefix_len + $suffix_len < $shorter) {
6225 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6227 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6228 $suffix_len++;
6231 # Mark lines that are different from each other, but have some common
6232 # part that isn't whitespace. If lines are completely different, don't
6233 # mark them because that would make output unreadable, especially if
6234 # diff consists of multiple lines.
6235 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6236 $esc_rem = esc_html_hl_regions($rem, 'marked',
6237 [$prefix_len, @rem - $suffix_len], -nbsp=>1);
6238 $esc_add = esc_html_hl_regions($add, 'marked',
6239 [$prefix_len, @add - $suffix_len], -nbsp=>1);
6240 } else {
6241 $esc_rem = esc_html($rem, -nbsp=>1);
6242 $esc_add = esc_html($add, -nbsp=>1);
6245 return format_diff_line(\$esc_rem, 'rem'),
6246 format_diff_line(\$esc_add, 'add');
6249 # HTML-format diff context, removed and added lines.
6250 sub format_ctx_rem_add_lines {
6251 my ($ctx, $rem, $add, $num_parents) = @_;
6252 my (@new_ctx, @new_rem, @new_add);
6253 my $can_highlight = 0;
6254 my $is_combined = ($num_parents > 1);
6256 # Highlight if every removed line has a corresponding added line.
6257 if (@$add > 0 && @$add == @$rem) {
6258 $can_highlight = 1;
6260 # Highlight lines in combined diff only if the chunk contains
6261 # diff between the same version, e.g.
6263 # - a
6264 # - b
6265 # + c
6266 # + d
6268 # Otherwise the highlightling would be confusing.
6269 if ($is_combined) {
6270 for (my $i = 0; $i < @$add; $i++) {
6271 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6272 my $prefix_add = substr($add->[$i], 0, $num_parents);
6274 $prefix_rem =~ s/-/+/g;
6276 if ($prefix_rem ne $prefix_add) {
6277 $can_highlight = 0;
6278 last;
6284 if ($can_highlight) {
6285 for (my $i = 0; $i < @$add; $i++) {
6286 my ($line_rem, $line_add) = format_rem_add_lines_pair(
6287 $rem->[$i], $add->[$i], $num_parents);
6288 push @new_rem, $line_rem;
6289 push @new_add, $line_add;
6291 } else {
6292 @new_rem = map { format_diff_line($_, 'rem') } @$rem;
6293 @new_add = map { format_diff_line($_, 'add') } @$add;
6296 @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
6298 return (\@new_ctx, \@new_rem, \@new_add);
6301 # Print context lines and then rem/add lines.
6302 sub print_diff_lines {
6303 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6304 my $is_combined = $num_parents > 1;
6306 ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
6307 $num_parents);
6309 if ($diff_style eq 'sidebyside' && !$is_combined) {
6310 print_sidebyside_diff_lines($ctx, $rem, $add);
6311 } else {
6312 # default 'inline' style and unknown styles
6313 print_inline_diff_lines($ctx, $rem, $add);
6317 sub print_diff_chunk {
6318 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6319 my (@ctx, @rem, @add);
6321 # The class of the previous line.
6322 my $prev_class = '';
6324 return unless @chunk;
6326 # incomplete last line might be among removed or added lines,
6327 # or both, or among context lines: find which
6328 for (my $i = 1; $i < @chunk; $i++) {
6329 if ($chunk[$i][0] eq 'incomplete') {
6330 $chunk[$i][0] = $chunk[$i-1][0];
6334 # guardian
6335 push @chunk, ["", ""];
6337 foreach my $line_info (@chunk) {
6338 my ($class, $line) = @$line_info;
6340 # print chunk headers
6341 if ($class && $class eq 'chunk_header') {
6342 print format_diff_line($line, $class, $from, $to);
6343 next;
6346 ## print from accumulator when have some add/rem lines or end
6347 # of chunk (flush context lines), or when have add and rem
6348 # lines and new block is reached (otherwise add/rem lines could
6349 # be reordered)
6350 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6351 (@rem && @add && $class ne $prev_class)) {
6352 print_diff_lines(\@ctx, \@rem, \@add,
6353 $diff_style, $num_parents);
6354 @ctx = @rem = @add = ();
6357 ## adding lines to accumulator
6358 # guardian value
6359 last unless $line;
6360 # rem, add or change
6361 if ($class eq 'rem') {
6362 push @rem, $line;
6363 } elsif ($class eq 'add') {
6364 push @add, $line;
6366 # context line
6367 if ($class eq 'ctx') {
6368 push @ctx, $line;
6371 $prev_class = $class;
6375 sub git_patchset_body {
6376 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6377 my ($hash_parent) = $hash_parents[0];
6379 my $is_combined = (@hash_parents > 1);
6380 my $patch_idx = 0;
6381 my $patch_number = 0;
6382 my $patch_line;
6383 my $diffinfo;
6384 my $to_name;
6385 my (%from, %to);
6386 my @chunk; # for side-by-side diff
6388 print "<div class=\"patchset\">\n";
6390 # skip to first patch
6391 while ($patch_line = to_utf8(scalar <$fd>)) {
6392 chomp $patch_line;
6394 last if ($patch_line =~ m/^diff /);
6397 PATCH:
6398 while ($patch_line) {
6400 # parse "git diff" header line
6401 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6402 # $1 is from_name, which we do not use
6403 $to_name = unquote($2);
6404 $to_name =~ s!^b/!!;
6405 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6406 # $1 is 'cc' or 'combined', which we do not use
6407 $to_name = unquote($2);
6408 } else {
6409 $to_name = undef;
6412 # check if current patch belong to current raw line
6413 # and parse raw git-diff line if needed
6414 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
6415 # this is continuation of a split patch
6416 print "<div class=\"patch cont\">\n";
6417 } else {
6418 # advance raw git-diff output if needed
6419 $patch_idx++ if defined $diffinfo;
6421 # read and prepare patch information
6422 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6424 # compact combined diff output can have some patches skipped
6425 # find which patch (using pathname of result) we are at now;
6426 if ($is_combined) {
6427 while ($to_name ne $diffinfo->{'to_file'}) {
6428 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6429 format_diff_cc_simplified($diffinfo, @hash_parents) .
6430 "</div>\n"; # class="patch"
6432 $patch_idx++;
6433 $patch_number++;
6435 last if $patch_idx > $#$difftree;
6436 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6440 # modifies %from, %to hashes
6441 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
6443 # this is first patch for raw difftree line with $patch_idx index
6444 # we index @$difftree array from 0, but number patches from 1
6445 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6448 # git diff header
6449 #assert($patch_line =~ m/^diff /) if DEBUG;
6450 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6451 $patch_number++;
6452 # print "git diff" header
6453 print format_git_diff_header_line($patch_line, $diffinfo,
6454 \%from, \%to);
6456 # print extended diff header
6457 print "<div class=\"diff extended_header\">\n";
6458 EXTENDED_HEADER:
6459 while ($patch_line = to_utf8(scalar<$fd>)) {
6460 chomp $patch_line;
6462 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
6464 print format_extended_diff_header_line($patch_line, $diffinfo,
6465 \%from, \%to);
6467 print "</div>\n"; # class="diff extended_header"
6469 # from-file/to-file diff header
6470 if (! $patch_line) {
6471 print "</div>\n"; # class="patch"
6472 last PATCH;
6474 next PATCH if ($patch_line =~ m/^diff /);
6475 #assert($patch_line =~ m/^---/) if DEBUG;
6477 my $last_patch_line = $patch_line;
6478 $patch_line = to_utf8(scalar <$fd>);
6479 chomp $patch_line;
6480 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6482 print format_diff_from_to_header($last_patch_line, $patch_line,
6483 $diffinfo, \%from, \%to,
6484 @hash_parents);
6486 # the patch itself
6487 LINE:
6488 while ($patch_line = to_utf8(scalar <$fd>)) {
6489 chomp $patch_line;
6491 next PATCH if ($patch_line =~ m/^diff /);
6493 my $class = diff_line_class($patch_line, \%from, \%to);
6495 if ($class eq 'chunk_header') {
6496 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
6497 @chunk = ();
6500 push @chunk, [ $class, $patch_line ];
6503 } continue {
6504 if (@chunk) {
6505 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
6506 @chunk = ();
6508 print "</div>\n"; # class="patch"
6511 # for compact combined (--cc) format, with chunk and patch simplification
6512 # the patchset might be empty, but there might be unprocessed raw lines
6513 for (++$patch_idx if $patch_number > 0;
6514 $patch_idx < @$difftree;
6515 ++$patch_idx) {
6516 # read and prepare patch information
6517 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6519 # generate anchor for "patch" links in difftree / whatchanged part
6520 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6521 format_diff_cc_simplified($diffinfo, @hash_parents) .
6522 "</div>\n"; # class="patch"
6524 $patch_number++;
6527 if ($patch_number == 0) {
6528 if (@hash_parents > 1) {
6529 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6530 } else {
6531 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6535 print "</div>\n"; # class="patchset"
6538 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6540 sub git_project_search_form {
6541 my ($searchtext, $search_use_regexp) = @_;
6543 my $limit = '';
6544 if ($project_filter) {
6545 $limit = " in '$project_filter'";
6548 print "<div class=\"projsearch\">\n";
6549 print $cgi->start_form(-method => 'get', -action => $my_uri) .
6550 $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
6551 print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
6552 if (defined $project_filter);
6553 print $cgi->textfield(-name => 's', -value => $searchtext,
6554 -title => "Search project by name and description$limit",
6555 -size => 60) . "\n" .
6556 "<span title=\"Extended regular expression\">" .
6557 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
6558 -checked => $search_use_regexp) .
6559 "</span>\n" .
6560 $cgi->submit(-name => 'btnS', -value => 'Search') .
6561 $cgi->end_form() . "\n" .
6562 "<span class=\"projectlist_link\">" .
6563 $cgi->a({-href => href(project => undef, searchtext => undef,
6564 action => 'project_list',
6565 project_filter => $project_filter)},
6566 esc_html("List all projects$limit")) . "</span><br />\n";
6567 print "<span class=\"projectlist_link\">" .
6568 $cgi->a({-href => href(project => undef, searchtext => undef,
6569 action => 'project_list',
6570 project_filter => undef)},
6571 esc_html("List all projects")) . "</span>\n" if $project_filter;
6572 print "</div>\n";
6575 # entry for given @keys needs filling if at least one of keys in list
6576 # is not present in %$project_info
6577 sub project_info_needs_filling {
6578 my ($project_info, @keys) = @_;
6580 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6581 foreach my $key (@keys) {
6582 if (!exists $project_info->{$key}) {
6583 return 1;
6586 return;
6589 sub git_cache_file_format {
6590 return GITWEB_CACHE_FORMAT .
6591 (gitweb_check_feature('forks') ? " (forks)" : "");
6594 sub git_retrieve_cache_file {
6595 my $cache_file = shift;
6597 use Storable qw(retrieve);
6599 if ((my $dump = eval { retrieve($cache_file) })) {
6600 return $$dump[1] if
6601 ref($dump) eq 'ARRAY' &&
6602 @$dump == 2 &&
6603 ref($$dump[1]) eq 'ARRAY' &&
6604 @{$$dump[1]} == 2 &&
6605 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6606 ref(${$$dump[1]}[1]) eq 'HASH' &&
6607 $$dump[0] eq git_cache_file_format();
6610 return undef;
6613 sub git_store_cache_file {
6614 my ($cache_file, $cachedata) = @_;
6616 use File::Basename qw(dirname);
6617 use File::stat;
6618 use POSIX qw(:fcntl_h);
6619 use Storable qw(store_fd);
6621 my $result = undef;
6622 my $cache_d = dirname($cache_file);
6623 my $mask = umask();
6624 umask($mask & ~0070) if $cache_grpshared;
6625 if ((-d $cache_d || mkdir($cache_d, $cache_grpshared ? 0770 : 0700)) &&
6626 sysopen(my $fd, "$cache_file.lock", O_WRONLY|O_CREAT|O_EXCL, $cache_grpshared ? 0660 : 0600)) {
6627 store_fd([git_cache_file_format(), $cachedata], $fd);
6628 close $fd;
6629 rename "$cache_file.lock", $cache_file;
6630 $result = stat($cache_file)->mtime;
6632 umask($mask) if $cache_grpshared;
6633 return $result;
6636 sub verify_cached_project {
6637 my ($hashref, $path) = @_;
6638 return undef unless $path;
6639 delete $$hashref{$path}, return undef unless is_valid_project($path);
6640 return $$hashref{$path} if exists $$hashref{$path};
6642 # A valid project was requested but it's not yet in the cache
6643 # Manufacture a minimal project entry (path, name, description)
6644 # Also provide age, but only if it's available via $lastactivity_file
6646 my %proj = ('path' => $path);
6647 my $val = git_get_project_description($path);
6648 defined $val or $val = '';
6649 $proj{'descr_long'} = $val;
6650 $proj{'descr'} = chop_str($val, $projects_list_description_width, 5);
6651 unless ($omit_owner) {
6652 $val = git_get_project_owner($path);
6653 defined $val or $val = '';
6654 $proj{'owner'} = $val;
6656 unless ($omit_age_column) {
6657 ($val) = git_get_last_activity($path, 1);
6658 $proj{'age_epoch'} = $val if defined $val;
6660 $$hashref{$path} = \%proj;
6661 return \%proj;
6664 sub git_filter_cached_projects {
6665 my ($cache, $projlist, $verify) = @_;
6666 my $hashref = $$cache[1];
6667 my $sub = $verify ?
6668 sub {verify_cached_project($hashref, $_[0])} :
6669 sub {$$hashref{$_[0]}};
6670 return map {
6671 my $c = &$sub($_->{'path'});
6672 defined $c ? ($_ = $c) : ()
6673 } @$projlist;
6676 # fills project list info (age, description, owner, category, forks, etc.)
6677 # for each project in the list, removing invalid projects from
6678 # returned list, or fill only specified info.
6680 # Invalid projects are removed from the returned list if and only if you
6681 # ask 'age_epoch' to be filled, because they are the only fields
6682 # that run unconditionally git command that requires repository, and
6683 # therefore do always check if project repository is invalid.
6685 # USAGE:
6686 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6687 # ensures that 'descr_long' and 'ctags' fields are filled
6688 # * @project_list = fill_project_list_info(\@project_list)
6689 # ensures that all fields are filled (and invalid projects removed)
6691 # NOTE: modifies $projlist, but does not remove entries from it
6692 sub fill_project_list_info {
6693 my ($projlist, @wanted_keys) = @_;
6695 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6696 return fill_project_list_info_uncached($projlist, @wanted_keys)
6697 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6699 use File::stat;
6701 my $cache_lifetime = $rebuild ? 0 : $projlist_cache_lifetime;
6702 my $cache_file = "$cache_dir/$projlist_cache_name";
6704 my @projects;
6705 my $stale = 0;
6706 my $now = time();
6707 my $cache_mtime;
6708 if ($cache_lifetime && -f $cache_file) {
6709 $cache_mtime = stat($cache_file)->mtime;
6710 $cache_dump = undef if $cache_mtime &&
6711 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6713 if (defined $cache_mtime && # caching is on and $cache_file exists
6714 $cache_mtime + $cache_lifetime*60 > $now &&
6715 ($cache_dump || ($cache_dump = git_retrieve_cache_file($cache_file)))) {
6716 # Cache hit.
6717 $cache_dump_mtime = $cache_mtime;
6718 $stale = $now - $cache_mtime;
6719 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6720 gitweb_check_feature('forks');
6721 @projects = git_filter_cached_projects($cache_dump, $projlist, $verify);
6723 } else { # Cache miss.
6724 if (defined $cache_mtime) {
6725 # Postpone timeout by two minutes so that we get
6726 # enough time to do our job, or to be more exact
6727 # make cache expire after two minutes from now.
6728 my $time = $now - $cache_lifetime*60 + 120;
6729 utime $time, $time, $cache_file;
6731 my @all_projects = git_get_projects_list();
6732 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6733 fill_project_list_info_uncached(\@all_projects);
6734 map { $all_projects_filled{$_->{'path'}} = $_ }
6735 filter_forks_from_projects_list([values(%all_projects_filled)])
6736 if gitweb_check_feature('forks');
6737 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6738 \%all_projects_filled];
6739 $cache_dump_mtime = git_store_cache_file($cache_file, $cache_dump);
6740 @projects = git_filter_cached_projects($cache_dump, $projlist);
6743 if ($cache_lifetime && $stale > 0) {
6744 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6745 unless $shown_stale_message;
6746 $shown_stale_message = 1;
6749 return @projects;
6752 sub fill_project_list_info_uncached {
6753 my ($projlist, @wanted_keys) = @_;
6754 my @projects;
6755 my $filter_set = sub { return @_; };
6756 if (@wanted_keys) {
6757 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6758 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6761 my $show_ctags = gitweb_check_feature('ctags');
6762 PROJECT:
6763 foreach my $pr (@$projlist) {
6764 if (project_info_needs_filling($pr, $filter_set->('age_epoch'))) {
6765 my (@activity) = git_get_last_activity($pr->{'path'});
6766 unless (@activity) {
6767 next PROJECT;
6769 ($pr->{'age_epoch'}) = @activity;
6771 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
6772 my $descr = git_get_project_description($pr->{'path'}) || "";
6773 $descr = to_utf8($descr);
6774 $pr->{'descr_long'} = $descr;
6775 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
6777 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
6778 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
6780 if ($show_ctags &&
6781 project_info_needs_filling($pr, $filter_set->('ctags'))) {
6782 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
6784 if ($projects_list_group_categories &&
6785 project_info_needs_filling($pr, $filter_set->('category'))) {
6786 my $cat = git_get_project_category($pr->{'path'}) ||
6787 $project_list_default_category;
6788 $pr->{'category'} = to_utf8($cat);
6791 push @projects, $pr;
6794 return @projects;
6797 sub sort_projects_list {
6798 my ($projlist, $order) = @_;
6800 sub order_str {
6801 my $key = shift;
6802 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6805 sub order_reverse_num_then_undef {
6806 my $key = shift;
6807 return sub {
6808 defined $a->{$key} ?
6809 (defined $b->{$key} ? $b->{$key} <=> $a->{$key} : -1) :
6810 (defined $b->{$key} ? 1 : 0)
6814 my %orderings = (
6815 project => order_str('path'),
6816 descr => order_str('descr_long'),
6817 owner => order_str('owner'),
6818 age => order_reverse_num_then_undef('age_epoch'),
6821 my $ordering = $orderings{$order};
6822 return defined $ordering ? sort $ordering @$projlist : @$projlist;
6825 # returns a hash of categories, containing the list of project
6826 # belonging to each category
6827 sub build_projlist_by_category {
6828 my ($projlist, $from, $to) = @_;
6829 my %categories;
6831 $from = 0 unless defined $from;
6832 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6834 for (my $i = $from; $i <= $to; $i++) {
6835 my $pr = $projlist->[$i];
6836 push @{$categories{ $pr->{'category'} }}, $pr;
6839 return wantarray ? %categories : \%categories;
6842 # print 'sort by' <th> element, generating 'sort by $name' replay link
6843 # if that order is not selected
6844 sub print_sort_th {
6845 print format_sort_th(@_);
6848 sub format_sort_th {
6849 my ($name, $order, $header) = @_;
6850 my $sort_th = "";
6851 $header ||= ucfirst($name);
6853 if ($order eq $name) {
6854 $sort_th .= "<th>$header</th>\n";
6855 } else {
6856 $sort_th .= "<th>" .
6857 $cgi->a({-href => href(-replay=>1, order=>$name),
6858 -class => "header"}, $header) .
6859 "</th>\n";
6862 return $sort_th;
6865 sub git_project_list_rows {
6866 my ($projlist, $from, $to, $check_forks) = @_;
6868 $from = 0 unless defined $from;
6869 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6871 my $now = time;
6872 my $alternate = 1;
6873 for (my $i = $from; $i <= $to; $i++) {
6874 my $pr = $projlist->[$i];
6876 if ($alternate) {
6877 print "<tr class=\"dark\">\n";
6878 } else {
6879 print "<tr class=\"light\">\n";
6881 $alternate ^= 1;
6883 if ($check_forks) {
6884 print "<td>";
6885 if ($pr->{'forks'}) {
6886 my $nforks = scalar @{$pr->{'forks'}};
6887 my $s = $nforks == 1 ? '' : 's';
6888 if ($nforks > 0) {
6889 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
6890 -title => "$nforks fork$s"}, "+");
6891 } else {
6892 print $cgi->span({-title => "$nforks fork$s"}, "+");
6895 print "</td>\n";
6897 my $path = $pr->{'path'};
6898 my $dotgit = $path =~ s/\.git$// ? '.git' : '';
6899 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
6900 -class => "list"},
6901 esc_html_match_hl($path, $search_regexp).$dotgit) .
6902 "</td>\n" .
6903 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
6904 -class => "list",
6905 -title => $pr->{'descr_long'}},
6906 $search_regexp
6907 ? esc_html_match_hl_chopped($pr->{'descr_long'},
6908 $pr->{'descr'}, $search_regexp)
6909 : esc_html($pr->{'descr'})) .
6910 "</td>\n";
6911 unless ($omit_owner) {
6912 print "<td><i>" . ($owner_link_hook
6913 ? $cgi->a({-href => $owner_link_hook->($pr->{'owner'}), -class => "list"},
6914 chop_and_escape_str($pr->{'owner'}, 15))
6915 : chop_and_escape_str($pr->{'owner'}, 15)) . "</i></td>\n";
6917 unless ($omit_age_column) {
6918 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6919 $age_string = defined $age_epoch ? age_string($age_epoch, $now) : "No commits";
6920 print "<td class=\"". age_class($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6922 print"<td class=\"link\">" .
6923 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . $barsep .
6924 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "log") . $barsep .
6925 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
6926 ($pr->{'forks'} ? $barsep . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
6927 "</td>\n" .
6928 "</tr>\n";
6932 sub git_project_list_body {
6933 # actually uses global variable $project
6934 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6935 my @projects = @$projlist;
6937 my $check_forks = gitweb_check_feature('forks');
6938 my $show_ctags = gitweb_check_feature('ctags');
6939 my $tagfilter = $show_ctags ? $input_params{'ctag_filter'} : undef;
6940 $check_forks = undef
6941 if ($tagfilter || $search_regexp);
6943 # filtering out forks before filling info allows us to do less work
6944 if ($check_forks) {
6945 @projects = filter_forks_from_projects_list(\@projects);
6946 push @projects, { 'path' => "$project_filter.git" }
6947 if $project_filter && $keep_top && is_valid_project("$project_filter.git");
6949 # search_projects_list pre-fills required info
6950 @projects = search_projects_list(\@projects,
6951 'search_regexp' => $search_regexp,
6952 'tagfilter' => $tagfilter)
6953 if ($tagfilter || $search_regexp);
6954 # fill the rest
6955 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
6956 push @all_fields, 'age_epoch' unless($omit_age_column);
6957 push @all_fields, 'owner' unless($omit_owner);
6958 @projects = fill_project_list_info(\@projects, @all_fields);
6960 $order ||= $default_projects_order;
6961 $from = 0 unless defined $from;
6962 $to = $#projects if (!defined $to || $#projects < $to);
6964 # short circuit
6965 if ($from > $to) {
6966 print "<center>\n".
6967 "<b>No such projects found</b><br />\n".
6968 "Click ".$cgi->a({-href=>href(project=>undef,action=>'project_list')},"here")." to view all projects<br />\n".
6969 "</center>\n<br />\n";
6970 return;
6973 @projects = sort_projects_list(\@projects, $order);
6975 if ($show_ctags) {
6976 my $ctags = git_gather_all_ctags(\@projects);
6977 my $cloud = git_populate_project_tagcloud($ctags, $ctags_action||'project_list');
6978 print git_show_project_tagcloud($cloud, 64);
6981 print "<table class=\"project_list\">\n";
6982 unless ($no_header) {
6983 print "<tr>\n";
6984 if ($check_forks) {
6985 print "<th></th>\n";
6987 print_sort_th('project', $order, 'Project');
6988 print_sort_th('descr', $order, 'Description');
6989 print_sort_th('owner', $order, 'Owner') unless $omit_owner;
6990 print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
6991 print "<th></th>\n" . # for links
6992 "</tr>\n";
6995 if ($projects_list_group_categories) {
6996 # only display categories with projects in the $from-$to window
6997 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
6998 my %categories = build_projlist_by_category(\@projects, $from, $to);
6999 foreach my $cat (sort keys %categories) {
7000 unless ($cat eq "") {
7001 print "<tr>\n";
7002 if ($check_forks) {
7003 print "<td></td>\n";
7005 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
7006 print "</tr>\n";
7009 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
7011 } else {
7012 git_project_list_rows(\@projects, $from, $to, $check_forks);
7015 if (defined $extra) {
7016 print "<tr class=\"extra\">\n";
7017 if ($check_forks) {
7018 print "<td></td>\n";
7020 print "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7021 "</tr>\n";
7023 print "</table>\n";
7026 sub git_log_body {
7027 # uses global variable $project
7028 my ($commitlist, $from, $to, $refs, $extra) = @_;
7030 $from = 0 unless defined $from;
7031 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7033 for (my $i = 0; $i <= $to; $i++) {
7034 my %co = %{$commitlist->[$i]};
7035 next if !%co;
7036 my $commit = $co{'id'};
7037 my $ref = format_ref_marker($refs, $commit);
7038 git_print_header_div('commit',
7039 "<span class=\"age\">$co{'age_string'}</span>" .
7040 esc_html($co{'title'}),
7041 $commit, undef, $ref);
7042 print "<div class=\"title_text\">\n" .
7043 "<div class=\"log_link\">\n" .
7044 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
7045 $barsep .
7046 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
7047 $barsep .
7048 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
7049 "<br/>\n" .
7050 "</div>\n";
7051 git_print_authorship(\%co, -tag => 'span');
7052 print "<br/>\n</div>\n";
7054 print "<div class=\"log_body\">\n";
7055 git_print_log($co{'comment'}, -final_empty_line=> 1);
7056 print "</div>\n";
7058 if ($extra) {
7059 print "<div class=\"page_nav_trailer\">\n";
7060 print "$extra\n";
7061 print "</div>\n";
7065 sub git_shortlog_body {
7066 # uses global variable $project
7067 my ($commitlist, $from, $to, $refs, $extra) = @_;
7069 $from = 0 unless defined $from;
7070 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7072 print "<table class=\"shortlog\">\n";
7073 my $alternate = 1;
7074 for (my $i = $from; $i <= $to; $i++) {
7075 my %co = %{$commitlist->[$i]};
7076 my $commit = $co{'id'};
7077 my $ref = format_ref_marker($refs, $commit);
7078 if ($alternate) {
7079 print "<tr class=\"dark\">\n";
7080 } else {
7081 print "<tr class=\"light\">\n";
7083 $alternate ^= 1;
7084 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7085 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7086 format_author_html('td', \%co, 10) . "<td>";
7087 print format_subject_html($co{'title'}, $co{'title_short'},
7088 href(action=>"commit", hash=>$commit), $ref);
7089 print "</td>\n" .
7090 "<td class=\"link\">" .
7091 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . $barsep .
7092 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . $barsep .
7093 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
7094 my $snapshot_links = format_snapshot_links($commit);
7095 if (defined $snapshot_links) {
7096 print $barsep . $snapshot_links;
7098 print "</td>\n" .
7099 "</tr>\n";
7101 if (defined $extra) {
7102 print "<tr class=\"extra\">\n" .
7103 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7104 "</tr>\n";
7106 print "</table>\n";
7109 sub git_history_body {
7110 # Warning: assumes constant type (blob or tree) during history
7111 my ($commitlist, $from, $to, $refs, $extra,
7112 $file_name, $file_hash, $ftype) = @_;
7114 $from = 0 unless defined $from;
7115 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7117 print "<table class=\"history\">\n";
7118 my $alternate = 1;
7119 for (my $i = $from; $i <= $to; $i++) {
7120 my %co = %{$commitlist->[$i]};
7121 if (!%co) {
7122 next;
7124 my $commit = $co{'id'};
7126 my $ref = format_ref_marker($refs, $commit);
7128 if ($alternate) {
7129 print "<tr class=\"dark\">\n";
7130 } else {
7131 print "<tr class=\"light\">\n";
7133 $alternate ^= 1;
7134 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7135 # shortlog: format_author_html('td', \%co, 10)
7136 format_author_html('td', \%co, 15, 3) . "<td>";
7137 # originally git_history used chop_str($co{'title'}, 50)
7138 print format_subject_html($co{'title'}, $co{'title_short'},
7139 href(action=>"commit", hash=>$commit), $ref);
7140 print "</td>\n" .
7141 "<td class=\"link\">" .
7142 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . $barsep .
7143 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
7145 if ($ftype eq 'blob') {
7146 my $blob_current = $file_hash;
7147 my $blob_parent = git_get_hash_by_path($commit, $file_name);
7148 if (defined $blob_current && defined $blob_parent &&
7149 $blob_current ne $blob_parent) {
7150 print $barsep .
7151 $cgi->a({-href => href(action=>"blobdiff",
7152 hash=>$blob_current, hash_parent=>$blob_parent,
7153 hash_base=>$hash_base, hash_parent_base=>$commit,
7154 file_name=>$file_name)},
7155 "diff to current");
7158 print "</td>\n" .
7159 "</tr>\n";
7161 if (defined $extra) {
7162 print "<tr class=\"extra\">\n" .
7163 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7164 "</tr>\n";
7166 print "</table>\n";
7169 sub git_tags_body {
7170 # uses global variable $project
7171 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7172 $from = 0 unless defined $from;
7173 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7174 $order ||= $default_refs_order;
7176 print "<table class=\"tags\">\n";
7177 if ($full) {
7178 print "<tr class=\"tags_header\">\n";
7179 print_sort_th('age', $order, 'Last Change');
7180 print_sort_th('name', $order, 'Name');
7181 print "<th></th>\n" . # for comment
7182 "<th></th>\n" . # for tag
7183 "<th></th>\n" . # for links
7184 "</tr>\n";
7186 my $alternate = 1;
7187 for (my $i = $from; $i <= $to; $i++) {
7188 my $entry = $taglist->[$i];
7189 my %tag = %$entry;
7190 my $comment = $tag{'subject'};
7191 my $comment_short;
7192 if (defined $comment) {
7193 $comment_short = chop_str($comment, 30, 5);
7195 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7196 if ($alternate) {
7197 print "<tr class=\"dark\">\n";
7198 } else {
7199 print "<tr class=\"light\">\n";
7201 $alternate ^= 1;
7202 if (defined $tag{'age'}) {
7203 print "<td><i>$tag{'age'}</i></td>\n";
7204 } else {
7205 print "<td></td>\n";
7207 print(($curr ? "<td class=\"current_head\">" : "<td>") .
7208 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
7209 -class => "list name"}, esc_html($tag{'name'})) .
7210 "</td>\n" .
7211 "<td>");
7212 if (defined $comment) {
7213 print format_subject_html($comment, $comment_short,
7214 href(action=>"tag", hash=>$tag{'id'}));
7216 print "</td>\n" .
7217 "<td class=\"selflink\">";
7218 if ($tag{'type'} eq "tag") {
7219 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
7220 } else {
7221 print "&#160;";
7223 print "</td>\n" .
7224 "<td class=\"link\">" . $barsep .
7225 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
7226 if ($tag{'reftype'} eq "commit") {
7227 print $barsep . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "log");
7228 print $barsep . $cgi->a({-href => href(action=>"tree", hash=>$tag{'fullname'})}, "tree") if $full;
7229 } elsif ($tag{'reftype'} eq "blob") {
7230 print $barsep . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
7232 print "</td>\n" .
7233 "</tr>";
7235 if (defined $extra) {
7236 print "<tr class=\"extra\">\n" .
7237 "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7238 "</tr>\n";
7240 print "</table>\n";
7243 sub git_heads_body {
7244 # uses global variable $project
7245 my ($headlist, $head_at, $from, $to, $extra) = @_;
7246 $from = 0 unless defined $from;
7247 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7249 print "<table class=\"heads\">\n";
7250 my $alternate = 1;
7251 for (my $i = $from; $i <= $to; $i++) {
7252 my $entry = $headlist->[$i];
7253 my %ref = %$entry;
7254 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7255 if ($alternate) {
7256 print "<tr class=\"dark\">\n";
7257 } else {
7258 print "<tr class=\"light\">\n";
7260 $alternate ^= 1;
7261 print "<td><i>$ref{'age'}</i></td>\n" .
7262 ($curr ? "<td class=\"current_head\">" : "<td>") .
7263 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
7264 -class => "list name"},esc_html($ref{'name'})) .
7265 "</td>\n" .
7266 "<td class=\"link\">" .
7267 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "log") . $barsep .
7268 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
7269 "</td>\n" .
7270 "</tr>";
7272 if (defined $extra) {
7273 print "<tr class=\"extra\">\n" .
7274 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7275 "</tr>\n";
7277 print "</table>\n";
7280 # Display a single remote block
7281 sub git_remote_block {
7282 my ($remote, $rdata, $limit, $head) = @_;
7284 my $heads = $rdata->{'heads'};
7285 my $fetch = $rdata->{'fetch'};
7286 my $push = $rdata->{'push'};
7288 my $urls_table = "<table class=\"projects_list\">\n" ;
7290 if (defined $fetch) {
7291 if ($fetch eq $push) {
7292 $urls_table .= format_repo_url("URL", $fetch);
7293 } else {
7294 $urls_table .= format_repo_url("Fetch&#160;URL", $fetch);
7295 $urls_table .= format_repo_url("Push&#160;URL", $push) if defined $push;
7297 } elsif (defined $push) {
7298 $urls_table .= format_repo_url("Push&#160;URL", $push);
7299 } else {
7300 $urls_table .= format_repo_url("", "No remote URL");
7303 $urls_table .= "</table>\n";
7305 my $dots;
7306 if (defined $limit && $limit < @$heads) {
7307 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
7310 print $urls_table;
7311 git_heads_body($heads, $head, 0, $limit, $dots);
7314 # Display a list of remote names with the respective fetch and push URLs
7315 sub git_remotes_list {
7316 my ($remotedata, $limit) = @_;
7317 print "<table class=\"heads\">\n";
7318 my $alternate = 1;
7319 my @remotes = sort keys %$remotedata;
7321 my $limited = $limit && $limit < @remotes;
7323 $#remotes = $limit - 1 if $limited;
7325 while (my $remote = shift @remotes) {
7326 my $rdata = $remotedata->{$remote};
7327 my $fetch = $rdata->{'fetch'};
7328 my $push = $rdata->{'push'};
7329 if ($alternate) {
7330 print "<tr class=\"dark\">\n";
7331 } else {
7332 print "<tr class=\"light\">\n";
7334 $alternate ^= 1;
7335 print "<td>" .
7336 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
7337 -class=> "list name"},esc_html($remote)) .
7338 "</td>";
7339 print "<td class=\"link\">" .
7340 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
7341 $barsep .
7342 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
7343 "</td>";
7345 print "</tr>\n";
7348 if ($limited) {
7349 print "<tr>\n" .
7350 "<td colspan=\"3\">" .
7351 $cgi->a({-href => href(action=>"remotes")}, "...") .
7352 "</td>\n" . "</tr>\n";
7355 print "</table>";
7358 # Display remote heads grouped by remote, unless there are too many
7359 # remotes, in which case we only display the remote names
7360 sub git_remotes_body {
7361 my ($remotedata, $limit, $head) = @_;
7362 if ($limit and $limit < keys %$remotedata) {
7363 git_remotes_list($remotedata, $limit);
7364 } else {
7365 fill_remote_heads($remotedata);
7366 while (my ($remote, $rdata) = each %$remotedata) {
7367 git_print_section({-class=>"remote", -id=>$remote},
7368 ["remotes", $remote, $remote], sub {
7369 git_remote_block($remote, $rdata, $limit, $head);
7375 sub git_search_message {
7376 my %co = @_;
7378 my $greptype;
7379 if ($searchtype eq 'commit') {
7380 $greptype = "--grep=";
7381 } elsif ($searchtype eq 'author') {
7382 $greptype = "--author=";
7383 } elsif ($searchtype eq 'committer') {
7384 $greptype = "--committer=";
7386 $greptype .= $searchtext;
7387 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
7388 $greptype, '--regexp-ignore-case',
7389 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
7391 my $paging_nav = "<span class=\"paging_nav\">";
7392 if ($page > 0) {
7393 $paging_nav .= tabspan(
7394 $cgi->a({-href => href(-replay=>1, page=>undef)},
7395 "first")) .
7396 $mdotsep . tabspan(
7397 $cgi->a({-href => href(-replay=>1, page=>$page-1),
7398 -accesskey => "p", -title => "Alt-p"}, "prev"));
7399 } else {
7400 $paging_nav .= tabspan("first", 1, 0).${mdotsep}.tabspan("prev", 0, 1);
7402 my $next_link = '';
7403 if ($#commitlist >= 100) {
7404 $next_link = tabspan(
7405 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7406 -accesskey => "n", -title => "Alt-n"}, "next"));
7407 $paging_nav .= "${mdotsep}$next_link";
7408 } else {
7409 $paging_nav .= ${mdotsep}.tabspan("next", 0, 1);
7412 git_header_html();
7414 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav."</span>");
7415 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7416 if ($page == 0 && !@commitlist) {
7417 print "<p>No match.</p>\n";
7418 } else {
7419 git_search_grep_body(\@commitlist, 0, 99, $next_link);
7422 git_footer_html();
7425 sub git_search_changes {
7426 my %co = @_;
7428 local $/ = "\n";
7429 defined(my $fd = git_cmd_pipe '--no-pager', 'log', @diff_opts,
7430 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7431 ($search_use_regexp ? '--pickaxe-regex' : ()))
7432 or die_error(500, "Open git-log failed");
7434 git_header_html();
7436 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7437 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7439 print "<table class=\"pickaxe search\">\n";
7440 my $alternate = 1;
7441 undef %co;
7442 my @files;
7443 while (my $line = to_utf8(scalar <$fd>)) {
7444 chomp $line;
7445 next unless $line;
7447 my %set = parse_difftree_raw_line($line);
7448 if (defined $set{'commit'}) {
7449 # finish previous commit
7450 if (%co) {
7451 print "</td>\n" .
7452 "<td class=\"link\">" .
7453 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
7454 "commit") .
7455 $barsep .
7456 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
7457 hash_base=>$co{'id'})},
7458 "tree") .
7459 "</td>\n" .
7460 "</tr>\n";
7463 if ($alternate) {
7464 print "<tr class=\"dark\">\n";
7465 } else {
7466 print "<tr class=\"light\">\n";
7468 $alternate ^= 1;
7469 %co = parse_commit($set{'commit'});
7470 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
7471 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7472 "<td><i>$author</i></td>\n" .
7473 "<td>" .
7474 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7475 -class => "list subject"},
7476 chop_and_escape_str($co{'title'}, 50) . "<br/>");
7477 } elsif (defined $set{'to_id'}) {
7478 next if ($set{'to_id'} =~ m/^0{40}$/);
7480 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
7481 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
7482 -class => "list"},
7483 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
7484 "<br/>\n";
7487 close $fd;
7489 # finish last commit (warning: repetition!)
7490 if (%co) {
7491 print "</td>\n" .
7492 "<td class=\"link\">" .
7493 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
7494 "commit") .
7495 $barsep .
7496 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
7497 hash_base=>$co{'id'})},
7498 "tree") .
7499 "</td>\n" .
7500 "</tr>\n";
7503 print "</table>\n";
7505 git_footer_html();
7508 sub git_search_files {
7509 my %co = @_;
7511 local $/ = "\n";
7512 defined(my $fd = git_cmd_pipe 'grep', '-n', '-z',
7513 $search_use_regexp ? ('-E', '-i') : '-F',
7514 $searchtext, $co{'tree'})
7515 or die_error(500, "Open git-grep failed");
7517 git_header_html();
7519 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7520 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7522 print "<table class=\"grep_search\">\n";
7523 my $alternate = 1;
7524 my $matches = 0;
7525 my $lastfile = '';
7526 my $file_href;
7527 while (my $line = to_utf8(scalar <$fd>)) {
7528 chomp $line;
7529 my ($file, $lno, $ltext, $binary);
7530 last if ($matches++ > 1000);
7531 if ($line =~ /^Binary file (.+) matches$/) {
7532 $file = $1;
7533 $binary = 1;
7534 } else {
7535 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7536 $file =~ s/^$co{'tree'}://;
7538 if ($file ne $lastfile) {
7539 $lastfile and print "</td></tr>\n";
7540 if ($alternate++) {
7541 print "<tr class=\"dark\">\n";
7542 } else {
7543 print "<tr class=\"light\">\n";
7545 $file_href = href(action=>"blob", hash_base=>$co{'id'},
7546 file_name=>$file);
7547 print "<td class=\"list\">".
7548 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
7549 print "</td><td>\n";
7550 $lastfile = $file;
7552 if ($binary) {
7553 print "<div class=\"binary\">Binary file</div>\n";
7554 } else {
7555 $ltext = untabify($ltext);
7556 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7557 $ltext = esc_html($1, -nbsp=>1);
7558 $ltext .= '<span class="match">';
7559 $ltext .= esc_html($2, -nbsp=>1);
7560 $ltext .= '</span>';
7561 $ltext .= esc_html($3, -nbsp=>1);
7562 } else {
7563 $ltext = esc_html($ltext, -nbsp=>1);
7565 print "<div class=\"pre\">" .
7566 $cgi->a({-href => $file_href.'#l'.$lno,
7567 -class => "linenr"}, sprintf('%4i', $lno)) .
7568 ' ' . $ltext . "</div>\n";
7571 if ($lastfile) {
7572 print "</td></tr>\n";
7573 if ($matches > 1000) {
7574 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7576 } else {
7577 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7579 close $fd;
7581 print "</table>\n";
7583 git_footer_html();
7586 sub git_search_grep_body {
7587 my ($commitlist, $from, $to, $extra) = @_;
7588 $from = 0 unless defined $from;
7589 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7591 print "<table class=\"commit_search\">\n";
7592 my $alternate = 1;
7593 for (my $i = $from; $i <= $to; $i++) {
7594 my %co = %{$commitlist->[$i]};
7595 if (!%co) {
7596 next;
7598 my $commit = $co{'id'};
7599 if ($alternate) {
7600 print "<tr class=\"dark\">\n";
7601 } else {
7602 print "<tr class=\"light\">\n";
7604 $alternate ^= 1;
7605 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7606 format_author_html('td', \%co, 15, 5) .
7607 "<td>" .
7608 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7609 -class => "list subject"},
7610 chop_and_escape_str($co{'title'}, 50) . "<br/>");
7611 my $comment = $co{'comment'};
7612 foreach my $line (@$comment) {
7613 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7614 my ($lead, $match, $trail) = ($1, $2, $3);
7615 $match = chop_str($match, 70, 5, 'center');
7616 my $contextlen = int((80 - length($match))/2);
7617 $contextlen = 30 if ($contextlen > 30);
7618 $lead = chop_str($lead, $contextlen, 10, 'left');
7619 $trail = chop_str($trail, $contextlen, 10, 'right');
7621 $lead = esc_html($lead);
7622 $match = esc_html($match);
7623 $trail = esc_html($trail);
7625 print "$lead<span class=\"match\">$match</span>$trail<br />";
7628 print "</td>\n" .
7629 "<td class=\"link\">" .
7630 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7631 $barsep .
7632 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
7633 $barsep .
7634 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7635 print "</td>\n" .
7636 "</tr>\n";
7638 if (defined $extra) {
7639 print "<tr class=\"extra\">\n" .
7640 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7641 "</tr>\n";
7643 print "</table>\n";
7646 ## ======================================================================
7647 ## ======================================================================
7648 ## actions
7650 sub git_project_list_load {
7651 my $empty_list_ok = shift;
7652 my $order = $input_params{'order'};
7653 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7654 die_error(400, "Unknown order parameter");
7657 my @list = git_get_projects_list($project_filter, $strict_export);
7658 if ($project_filter && (!@list || !gitweb_check_feature('forks'))) {
7659 push @list, { 'path' => "$project_filter.git" }
7660 if is_valid_project("$project_filter.git");
7662 if (!@list) {
7663 die_error(404, "No projects found") unless $empty_list_ok;
7666 return (\@list, $order);
7669 sub git_frontpage {
7670 my ($projlist, $order);
7672 if ($frontpage_no_project_list) {
7673 $project = undef;
7674 $project_filter = undef;
7675 } else {
7676 ($projlist, $order) = git_project_list_load(1);
7678 git_header_html();
7679 if (defined $home_text && -f $home_text) {
7680 print "<div class=\"index_include\">\n";
7681 insert_file($home_text);
7682 print "</div>\n";
7684 git_project_search_form($searchtext, $search_use_regexp);
7685 if ($frontpage_no_project_list) {
7686 my $show_ctags = gitweb_check_feature('ctags');
7687 if ($frontpage_no_project_list == 1 and $show_ctags) {
7688 my @projects = git_get_projects_list($project_filter, $strict_export);
7689 @projects = filter_forks_from_projects_list(\@projects) if gitweb_check_feature('forks');
7690 @projects = fill_project_list_info(\@projects, 'ctags');
7691 my $ctags = git_gather_all_ctags(\@projects);
7692 my $cloud = git_populate_project_tagcloud($ctags, 'project_list');
7693 print git_show_project_tagcloud($cloud, 64);
7695 } else {
7696 git_project_list_body($projlist, $order, undef, undef, undef, undef, undef, 1);
7698 git_footer_html();
7701 sub git_project_list {
7702 my ($projlist, $order) = git_project_list_load();
7703 git_header_html();
7704 if (!$frontpage_no_project_list && defined $home_text && -f $home_text) {
7705 print "<div class=\"index_include\">\n";
7706 insert_file($home_text);
7707 print "</div>\n";
7709 git_project_search_form();
7710 git_project_list_body($projlist, $order, undef, undef, undef, undef, undef, 1);
7711 git_footer_html();
7714 sub git_forks {
7715 my $order = $input_params{'order'};
7716 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7717 die_error(400, "Unknown order parameter");
7720 my $filter = $project;
7721 $filter =~ s/\.git$//;
7722 my @list = git_get_projects_list($filter);
7723 if (!@list) {
7724 die_error(404, "No forks found");
7727 git_header_html();
7728 git_print_page_nav('','');
7729 git_print_header_div('summary', "$project forks");
7730 git_project_list_body(\@list, $order, undef, undef, undef, undef, 'forks');
7731 git_footer_html();
7734 sub git_project_index {
7735 my @projects = git_get_projects_list($project_filter, $strict_export);
7736 if (!@projects) {
7737 die_error(404, "No projects found");
7740 print $cgi->header(
7741 -type => 'text/plain',
7742 -charset => 'utf-8',
7743 -content_disposition => 'inline; filename="index.aux"');
7745 foreach my $pr (@projects) {
7746 if (!exists $pr->{'owner'}) {
7747 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
7750 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7751 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7752 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
7753 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
7754 $path =~ s/ /\+/g;
7755 $owner =~ s/ /\+/g;
7757 print "$path $owner\n";
7761 sub git_summary {
7762 my $descr = git_get_project_description($project) || "none";
7763 my %co = parse_commit("HEAD");
7764 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7765 my $head = $co{'id'};
7766 my $remote_heads = gitweb_check_feature('remote_heads');
7768 my $owner = git_get_project_owner($project);
7769 my $homepage = git_get_project_config('homepage');
7770 my $base_url = git_get_project_config('baseurl');
7772 my $refs = git_get_references();
7773 # These get_*_list functions return one more to allow us to see if
7774 # there are more ...
7775 my @taglist = git_get_tags_list(16);
7776 my @headlist = git_get_heads_list(16);
7777 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
7778 my @forklist;
7779 my $check_forks = gitweb_check_feature('forks');
7781 if ($check_forks) {
7782 # find forks of a project
7783 my $filter = $project;
7784 $filter =~ s/\.git$//;
7785 @forklist = git_get_projects_list($filter);
7786 # filter out forks of forks
7787 @forklist = filter_forks_from_projects_list(\@forklist)
7788 if (@forklist);
7791 git_header_html();
7792 git_print_page_nav('summary','', $head);
7794 if ($check_forks and $project =~ m#/#) {
7795 my $xproject = $project; $xproject =~ s#/[^/]+$#.git#; #
7796 my $r = $cgi->a({-href=> href(project => $xproject, action => 'summary')}, $xproject);
7797 print <<EOT;
7798 <div class="forkinfo">
7799 This project is a fork of the $r project. If you have that one
7800 already cloned locally, you can use
7801 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7802 to save bandwidth during cloning.
7803 </div>
7807 print "<div class=\"title\">&#160;</div>\n";
7808 print "<table class=\"projects_list\">\n" .
7809 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
7810 if ($homepage) {
7811 print "<tr id=\"metadata_homepage\"><td>homepage&#160;URL</td><td>" . $cgi->a({-href => $homepage}, $homepage) . "</td></tr>\n";
7813 if ($base_url) {
7814 print "<tr id=\"metadata_baseurl\"><td>repository&#160;URL</td><td>" . esc_html($base_url) . "</td></tr>\n";
7816 if ($owner and not $omit_owner) {
7817 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7818 ? $cgi->a({-href => $owner_link_hook->($owner)}, email_obfuscate($owner))
7819 : email_obfuscate($owner)) . "</td></tr>\n";
7821 if (defined $cd{'rfc2822'}) {
7822 print "<tr id=\"metadata_lchange\"><td>last&#160;change</td>" .
7823 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
7825 print format_lastrefresh_row(), "\n";
7827 # use per project git URL list in $projectroot/$project/cloneurl
7828 # or make project git URL from git base URL and project name
7829 my $url_tag = $base_url ? "mirror&#160;URL" : "URL";
7830 my @url_list = git_get_project_url_list($project);
7831 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
7832 foreach my $git_url (@url_list) {
7833 next unless $git_url;
7834 print format_repo_url($url_tag, $git_url);
7835 $url_tag = "";
7837 @url_list = map { "$_/$project" } @git_base_push_urls;
7838 if ((git_get_project_config("showpush", '--bool')||'false') eq "true" ||
7839 -f "$projectroot/$project/.nofetch") {
7840 $url_tag = "push&#160;URL";
7841 foreach my $git_push_url (@url_list) {
7842 next unless $git_push_url;
7843 my $hint = $https_hint_html && $git_push_url =~ /^https:/i ?
7844 "&#160;$https_hint_html" : '';
7845 print "<tr class=\"metadata_pushurl\"><td>$url_tag</td><td>$git_push_url$hint</td></tr>\n";
7846 $url_tag = "";
7850 if (defined($git_base_bundles_url) && -d "$projectroot/$project/bundles") {
7851 my $projname = $project;
7852 $projname =~ s|^.*/||;
7853 my $url = "$git_base_bundles_url/$project/bundles";
7854 print format_repo_url(
7855 "bundle&#160;info",
7856 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7859 # Tag cloud
7860 my $show_ctags = gitweb_check_feature('ctags');
7861 if ($show_ctags) {
7862 my $ctags = git_get_project_ctags($project);
7863 if (%$ctags || $show_ctags !~ /^\d+$/) {
7864 # without ability to add tags, don't show if there are none
7865 my $cloud = git_populate_project_tagcloud($ctags, 'project_list');
7866 print "<tr id=\"metadata_ctags\">" .
7867 "<td style=\"vertical-align:middle\">content&#160;tags<br />";
7868 print "</td>\n<td>" unless %$ctags;
7869 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7870 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7871 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7872 unless $show_ctags =~ /^\d+$/;
7873 print "</td>\n<td>" if %$ctags;
7874 print git_show_project_tagcloud($cloud, 48)."</td>" .
7875 "</tr>\n";
7879 print "</table>\n";
7881 # If XSS prevention is on, we don't include README.html.
7882 # TODO: Allow a readme in some safe format.
7883 if (!$prevent_xss) {
7884 my $readme = -s "$projectroot/$project/README.html"
7885 ? collect_html_file("$projectroot/$project/README.html")
7886 : collect_output($git_automatic_readme_html, "$projectroot/$project");
7887 if (defined($readme)) {
7888 $readme =~ s/^\s+//s;
7889 $readme =~ s/\s+$//s;
7890 print "<div class=\"title\">readme</div>\n",
7891 "<div id=\"readme\" class=\"readme\">\n",
7892 $readme,
7893 "\n</div>\n"
7894 if $readme ne '';
7898 # we need to request one more than 16 (0..15) to check if
7899 # those 16 are all
7900 my @commitlist = $head ? parse_commits($head, 17) : ();
7901 if (@commitlist) {
7902 git_print_header_div('shortlog');
7903 git_shortlog_body(\@commitlist, 0, 15, $refs,
7904 $#commitlist <= 15 ? undef :
7905 $cgi->a({-href => href(action=>"shortlog")}, "..."));
7908 if (@taglist) {
7909 git_print_header_div('tags');
7910 git_tags_body(\@taglist, 0, 15,
7911 $#taglist <= 15 ? undef :
7912 $cgi->a({-href => href(action=>"tags")}, "..."));
7915 if (@headlist) {
7916 git_print_header_div('heads');
7917 git_heads_body(\@headlist, $head, 0, 15,
7918 $#headlist <= 15 ? undef :
7919 $cgi->a({-href => href(action=>"heads")}, "..."));
7922 if (%remotedata) {
7923 git_print_header_div('remotes');
7924 git_remotes_body(\%remotedata, 15, $head);
7927 if (@forklist) {
7928 git_print_header_div('forks');
7929 git_project_list_body(\@forklist, 'age', 0, 15,
7930 $#forklist <= 15 ? undef :
7931 $cgi->a({-href => href(action=>"forks")}, "..."),
7932 'no_header', 'forks');
7935 git_footer_html();
7938 sub git_tag {
7939 my %tag = parse_tag($hash);
7941 if (! %tag) {
7942 die_error(404, "Unknown tag object");
7945 my $fullhash;
7946 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
7947 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
7949 my $head = git_get_head_hash($project);
7950 git_header_html();
7951 git_print_page_nav('','', $head,undef,$head);
7952 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
7953 print "<div class=\"title_text\">\n" .
7954 "<table class=\"object_header\">\n" .
7955 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
7956 "<tr>\n" .
7957 "<td>object</td>\n" .
7958 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
7959 $tag{'object'}) . "</td>\n" .
7960 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
7961 $tag{'type'}) . "</td>\n" .
7962 "</tr>\n";
7963 if (defined($tag{'author'})) {
7964 git_print_authorship_rows(\%tag, 'author');
7966 print "</table>\n\n" .
7967 "</div>\n";
7968 print "<div class=\"page_body\">";
7969 my $comment = $tag{'comment'};
7970 foreach my $line (@$comment) {
7971 chomp $line;
7972 print esc_html($line, -nbsp=>1) . "<br/>\n";
7974 print "</div>\n";
7975 git_footer_html();
7978 sub git_blame_common {
7979 my $format = shift || 'porcelain';
7980 if ($format eq 'porcelain' && $input_params{'javascript'}) {
7981 $format = 'incremental';
7982 $action = 'blame_incremental'; # for page title etc
7985 # permissions
7986 gitweb_check_feature('blame')
7987 or die_error(403, "Blame view not allowed");
7989 # error checking
7990 die_error(400, "No file name given") unless $file_name;
7991 $hash_base ||= git_get_head_hash($project);
7992 die_error(404, "Couldn't find base commit") unless $hash_base;
7993 my %co = parse_commit($hash_base)
7994 or die_error(404, "Commit not found");
7995 my $ftype = "blob";
7996 if (!defined $hash) {
7997 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
7998 or die_error(404, "Error looking up file");
7999 } else {
8000 $ftype = git_get_type($hash);
8001 if ($ftype !~ "blob") {
8002 die_error(400, "Object is not a blob");
8006 my $fd;
8007 if ($format eq 'incremental') {
8008 # get file contents (as base)
8009 defined($fd = git_cmd_pipe 'cat-file', 'blob', $hash)
8010 or die_error(500, "Open git-cat-file failed");
8011 } elsif ($format eq 'data') {
8012 # run git-blame --incremental
8013 defined($fd = git_cmd_pipe "blame", "--incremental",
8014 $hash_base, "--", $file_name)
8015 or die_error(500, "Open git-blame --incremental failed");
8016 } else {
8017 # run git-blame --porcelain
8018 defined($fd = git_cmd_pipe "blame", '-p',
8019 $hash_base, '--', $file_name)
8020 or die_error(500, "Open git-blame --porcelain failed");
8023 # incremental blame data returns early
8024 if ($format eq 'data') {
8025 print $cgi->header(
8026 -type=>"text/plain", -charset => "utf-8",
8027 -status=> "200 OK");
8028 local $| = 1; # output autoflush
8029 while (<$fd>) {
8030 print to_utf8($_);
8032 close $fd
8033 or print "ERROR $!\n";
8035 print 'END';
8036 if (defined $t0 && gitweb_check_feature('timed')) {
8037 print ' '.
8038 tv_interval($t0, [ gettimeofday() ]).
8039 ' '.$number_of_git_cmds;
8041 print "\n";
8043 return;
8046 # page header
8047 git_header_html();
8048 my $formats_nav = tabspan(
8049 $cgi->a({-href => href(action=>"blob", -replay=>1)},
8050 "blob"));
8051 $formats_nav .=
8052 $barsep . tabspan(
8053 $cgi->a({-href => href(action=>"history", -replay=>1)},
8054 "history")) .
8055 $barsep . tabspan(
8056 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
8057 "HEAD"));
8058 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8059 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
8060 git_print_page_path($file_name, $ftype, $hash_base);
8062 # page body
8063 if ($format eq 'incremental') {
8064 print "<noscript>\n<div class=\"error\"><center><b>\n".
8065 "This page requires JavaScript to run.\n Use ".
8066 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
8067 'this page').
8068 " instead.\n".
8069 "</b></center></div>\n</noscript>\n";
8071 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
8074 print qq!<div class="page_body">\n!;
8075 print qq!<div id="progress_info">... / ...</div>\n!
8076 if ($format eq 'incremental');
8077 print qq!<table id="blame_table" class="blame" width="100%">\n!.
8078 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8079 qq!<thead>\n!.
8080 qq!<tr><th nowrap="nowrap" style="white-space:nowrap">!.
8081 qq!Commit&#160;<a href="javascript:extra_blame_columns()" id="columns_expander" !.
8082 qq!title="toggles blame author information display">[+]</a></th>!.
8083 qq!<th class="extra_column">Author</th><th class="extra_column">Date</th>!.
8084 qq!<th>Line</th><th width="100%">Data</th></tr>\n!.
8085 qq!</thead>\n!.
8086 qq!<tbody>\n!;
8088 my @rev_color = qw(light dark);
8089 my $num_colors = scalar(@rev_color);
8090 my $current_color = 0;
8092 if ($format eq 'incremental') {
8093 my $color_class = $rev_color[$current_color];
8095 #contents of a file
8096 my $linenr = 0;
8097 LINE:
8098 while (my $line = to_utf8(scalar <$fd>)) {
8099 chomp $line;
8100 $linenr++;
8102 print qq!<tr id="l$linenr" class="$color_class">!.
8103 qq!<td class="sha1"><a href=""> </a></td>!.
8104 qq!<td class="extra_column" nowrap="nowrap"></td>!.
8105 qq!<td class="extra_column" nowrap="nowrap"></td>!.
8106 qq!<td class="linenr">!.
8107 qq!<a class="linenr" href="">$linenr</a></td>!;
8108 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
8109 print qq!</tr>\n!;
8112 } else { # porcelain, i.e. ordinary blame
8113 my %metainfo = (); # saves information about commits
8115 # blame data
8116 LINE:
8117 while (my $line = to_utf8(scalar <$fd>)) {
8118 chomp $line;
8119 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8120 # no <lines in group> for subsequent lines in group of lines
8121 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8122 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8123 if (!exists $metainfo{$full_rev}) {
8124 $metainfo{$full_rev} = { 'nprevious' => 0 };
8126 my $meta = $metainfo{$full_rev};
8127 my $data;
8128 while ($data = to_utf8(scalar <$fd>)) {
8129 chomp $data;
8130 last if ($data =~ s/^\t//); # contents of line
8131 if ($data =~ /^(\S+)(?: (.*))?$/) {
8132 $meta->{$1} = $2 unless exists $meta->{$1};
8134 if ($data =~ /^previous /) {
8135 $meta->{'nprevious'}++;
8138 my $short_rev = substr($full_rev, 0, 8);
8139 my $author = $meta->{'author'};
8140 my %date =
8141 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
8142 my $date = $date{'iso-tz'};
8143 if ($group_size) {
8144 $current_color = ($current_color + 1) % $num_colors;
8146 my $tr_class = $rev_color[$current_color];
8147 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8148 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8149 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8150 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8151 if ($group_size) {
8152 my $rowspan = $group_size > 1 ? " rowspan=\"$group_size\"" : "";
8153 print "<td class=\"sha1\"";
8154 print " title=\"". esc_html($author) . ", $date\"";
8155 print "$rowspan>";
8156 print $cgi->a({-href => href(action=>"commit",
8157 hash=>$full_rev,
8158 file_name=>$file_name)},
8159 esc_html($short_rev));
8160 if ($group_size >= 2) {
8161 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8162 if (@author_initials) {
8163 print "<br />" .
8164 esc_html(join('', @author_initials));
8165 # or join('.', ...)
8168 print "</td>\n";
8169 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html($author) . "</td>";
8170 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8172 # 'previous' <sha1 of parent commit> <filename at commit>
8173 if (exists $meta->{'previous'} &&
8174 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8175 $meta->{'parent'} = $1;
8176 $meta->{'file_parent'} = unquote($2);
8178 my $linenr_commit =
8179 exists($meta->{'parent'}) ?
8180 $meta->{'parent'} : $full_rev;
8181 my $linenr_filename =
8182 exists($meta->{'file_parent'}) ?
8183 $meta->{'file_parent'} : unquote($meta->{'filename'});
8184 my $blamed = href(action => 'blame',
8185 file_name => $linenr_filename,
8186 hash_base => $linenr_commit);
8187 print "<td class=\"linenr\">";
8188 print $cgi->a({ -href => "$blamed#l$orig_lineno",
8189 -class => "linenr" },
8190 esc_html($lineno));
8191 print "</td>";
8192 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
8193 print "</tr>\n";
8194 } # end while
8198 # footer
8199 print "</tbody>\n".
8200 "</table>\n"; # class="blame"
8201 print "</div>\n"; # class="blame_body"
8202 close $fd
8203 or print "Reading blob failed\n";
8205 git_footer_html();
8208 sub git_blame {
8209 git_blame_common();
8212 sub git_blame_incremental {
8213 git_blame_common('incremental');
8216 sub git_blame_data {
8217 git_blame_common('data');
8220 sub git_tags {
8221 my $head = git_get_head_hash($project);
8222 git_header_html();
8223 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
8224 git_print_header_div('summary', $project);
8226 my @tagslist = git_get_tags_list();
8227 if (@tagslist) {
8228 git_tags_body(\@tagslist);
8230 git_footer_html();
8233 sub git_refs {
8234 my $order = $input_params{'order'};
8235 if (defined $order && $order !~ m/age|name/) {
8236 die_error(400, "Unknown order parameter");
8239 my $head = git_get_head_hash($project);
8240 git_header_html();
8241 git_print_page_nav('','', $head,undef,$head,format_ref_views('refs'));
8242 git_print_header_div('summary', $project);
8244 my @refslist = git_get_tags_list(undef, 1, $order);
8245 if (@refslist) {
8246 git_tags_body(\@refslist, undef, undef, undef, $head, 1, $order);
8248 git_footer_html();
8251 sub git_heads {
8252 my $head = git_get_head_hash($project);
8253 git_header_html();
8254 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
8255 git_print_header_div('summary', $project);
8257 my @headslist = git_get_heads_list();
8258 if (@headslist) {
8259 git_heads_body(\@headslist, $head);
8261 git_footer_html();
8264 # used both for single remote view and for list of all the remotes
8265 sub git_remotes {
8266 gitweb_check_feature('remote_heads')
8267 or die_error(403, "Remote heads view is disabled");
8269 my $head = git_get_head_hash($project);
8270 my $remote = $input_params{'hash'};
8272 my $remotedata = git_get_remotes_list($remote);
8273 die_error(500, "Unable to get remote information") unless defined $remotedata;
8275 unless (%$remotedata) {
8276 die_error(404, defined $remote ?
8277 "Remote $remote not found" :
8278 "No remotes found");
8281 git_header_html(undef, undef, -action_extra => $remote);
8282 git_print_page_nav('', '', $head, undef, $head,
8283 format_ref_views($remote ? '' : 'remotes'));
8285 fill_remote_heads($remotedata);
8286 if (defined $remote) {
8287 git_print_header_div('remotes', "$remote remote for $project");
8288 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
8289 } else {
8290 git_print_header_div('summary', "$project remotes");
8291 git_remotes_body($remotedata, undef, $head);
8294 git_footer_html();
8297 sub git_blob_plain {
8298 my $type = shift;
8299 my $expires;
8301 if (!defined $hash) {
8302 if (defined $file_name) {
8303 my $base = $hash_base || git_get_head_hash($project);
8304 $hash = git_get_hash_by_path($base, $file_name, "blob")
8305 or die_error(404, "Cannot find file");
8306 } else {
8307 die_error(400, "No file name defined");
8309 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8310 # blobs defined by non-textual hash id's can be cached
8311 $expires = "+1d";
8314 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
8315 or die_error(500, "Open git-cat-file blob '$hash' failed");
8316 binmode($fd);
8318 # content-type (can include charset)
8319 my $leader;
8320 ($type, $leader) = blob_contenttype($fd, $file_name, $type);
8322 # "save as" filename, even when no $file_name is given
8323 my $save_as = "$hash";
8324 if (defined $file_name) {
8325 $save_as = $file_name;
8326 } elsif ($type =~ m/^text\//) {
8327 $save_as .= '.txt';
8330 # With XSS prevention on, blobs of all types except a few known safe
8331 # ones are served with "Content-Disposition: attachment" to make sure
8332 # they don't run in our security domain. For certain image types,
8333 # blob view writes an <img> tag referring to blob_plain view, and we
8334 # want to be sure not to break that by serving the image as an
8335 # attachment (though Firefox 3 doesn't seem to care).
8336 my $sandbox = $prevent_xss &&
8337 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8339 # serve text/* as text/plain
8340 if ($prevent_xss &&
8341 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8342 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
8343 my $rest = $1;
8344 $rest = defined $rest ? $rest : '';
8345 $type = "text/plain$rest";
8348 print $cgi->header(
8349 -type => $type,
8350 -expires => $expires,
8351 -content_disposition =>
8352 ($sandbox ? 'attachment' : 'inline')
8353 . '; filename="' . $save_as . '"');
8354 binmode STDOUT, ':raw';
8355 $fcgi_raw_mode = 1;
8356 print $leader if defined $leader;
8357 my $buf;
8358 while (read($fd, $buf, 32768)) {
8359 print $buf;
8361 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
8362 $fcgi_raw_mode = 0;
8363 close $fd;
8366 sub git_blob {
8367 my $expires;
8369 my $fullhash;
8370 if (!defined $hash) {
8371 if (defined $file_name) {
8372 my $base = $hash_base || git_get_head_hash($project);
8373 $hash = git_get_hash_by_path($base, $file_name, "blob")
8374 or die_error(404, "Cannot find file");
8375 $fullhash = $hash;
8376 } else {
8377 die_error(400, "No file name defined");
8379 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8380 # blobs defined by non-textual hash id's can be cached
8381 $expires = "+1d";
8382 $fullhash = $hash;
8384 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
8386 my $have_blame = gitweb_check_feature('blame');
8387 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
8388 or die_error(500, "Couldn't cat $file_name, $hash");
8389 binmode($fd);
8390 my $mimetype = blob_mimetype($fd, $file_name);
8391 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8392 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
8393 close $fd;
8394 return git_blob_plain($mimetype);
8396 # we can have blame only for text/* mimetype
8397 $have_blame &&= ($mimetype =~ m!^text/!);
8399 my $highlight = gitweb_check_feature('highlight') && defined $highlight_bin;
8400 my $syntax = guess_file_syntax($fd, $mimetype, $file_name) if $highlight;
8401 my $highlight_mode_active;
8402 ($fd, $highlight_mode_active) = run_highlighter($fd, $syntax) if $syntax;
8404 git_header_html(undef, $expires);
8405 my $formats_nav = '';
8406 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
8407 if (defined $file_name) {
8408 if ($have_blame) {
8409 $formats_nav .= tabspan(
8410 $cgi->a({-href => href(action=>"blame", -replay=>1),
8411 -class => "blamelink"},
8412 "blame")) .
8413 $barsep;
8415 $formats_nav .= tabspan(
8416 $cgi->a({-href => href(action=>"history", -replay=>1)},
8417 "history")) .
8418 $barsep . tabspan(
8419 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
8420 "raw")) .
8421 $barsep . tabspan(
8422 $cgi->a({-href => href(action=>"blob",
8423 hash_base=>"HEAD", file_name=>$file_name)},
8424 "HEAD"));
8425 } else {
8426 $formats_nav .= tabspan(
8427 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
8428 "raw"));
8430 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8431 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
8432 } else {
8433 print "<div class=\"page_nav\">\n" .
8434 "<br/><br/></div>\n" .
8435 "<div class=\"title\">".esc_html($hash)."</div>\n";
8437 git_print_page_path($file_name, "blob", $hash_base);
8438 print "<div class=\"title_text\">\n" .
8439 "<table class=\"object_header\">\n";
8440 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8441 print "</table>".
8442 "</div>\n";
8443 print "<div class=\"page_body\">\n";
8444 if ($mimetype =~ m!^image/!) {
8445 print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;
8446 if ($file_name) {
8447 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
8449 print qq! src="! .
8450 href(action=>"blob_plain", hash=>$hash,
8451 hash_base=>$hash_base, file_name=>$file_name) .
8452 qq!" />\n!;
8453 } else {
8454 my $nr;
8455 while (my $line = to_utf8(scalar <$fd>)) {
8456 chomp $line;
8457 $nr++;
8458 $line = untabify($line);
8459 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
8460 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
8461 $highlight_mode_active ? sanitize($line) : esc_html($line, -nbsp=>1);
8464 close $fd
8465 or print "Reading blob failed.\n";
8466 print "</div>";
8467 git_footer_html();
8470 sub git_tree {
8471 my $fullhash;
8472 if (!defined $hash_base) {
8473 $hash_base = "HEAD";
8475 if (!defined $hash) {
8476 if (defined $file_name) {
8477 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
8478 $fullhash = $hash;
8479 } else {
8480 $hash = $hash_base;
8483 die_error(404, "No such tree") unless defined($hash);
8484 $fullhash = $hash if !$fullhash && $hash =~ m/^[0-9a-fA-F]{40}$/;
8485 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
8487 my $show_sizes = gitweb_check_feature('show-sizes');
8488 my $have_blame = gitweb_check_feature('blame');
8490 my @entries = ();
8492 local $/ = "\0";
8493 defined(my $fd = git_cmd_pipe "ls-tree", '-z',
8494 ($show_sizes ? '-l' : ()), @extra_options, $hash)
8495 or die_error(500, "Open git-ls-tree failed");
8496 @entries = map { chomp; to_utf8($_) } <$fd>;
8497 close $fd
8498 or die_error(404, "Reading tree failed");
8501 git_header_html();
8502 my $basedir = '';
8503 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
8504 my $refs = git_get_references();
8505 my $ref = format_ref_marker($refs, $co{'id'});
8506 my @views_nav = ();
8507 if (defined $file_name) {
8508 push @views_nav,
8509 tabspan($cgi->a({-href => href(action=>"history", -replay=>1)},
8510 "history")),
8511 tabspan($cgi->a({-href => href(action=>"tree",
8512 hash_base=>"HEAD", file_name=>$file_name)},
8513 "HEAD")),
8515 my $snapshot_links = format_snapshot_links($hash);
8516 if (defined $snapshot_links) {
8517 # FIXME: Should be available when we have no hash base as well.
8518 push @views_nav, $snapshot_links;
8520 git_print_page_nav('tree','', $hash_base, undef, undef,
8521 join($barsep, @views_nav));
8522 git_print_header_div('commit', esc_html($co{'title'}), $hash_base, undef, $ref);
8523 } else {
8524 undef $hash_base;
8525 print "<div class=\"page_nav\">\n";
8526 print "<br/><br/></div>\n";
8527 print "<div class=\"title\">".esc_html($hash)."</div>\n";
8529 if (defined $file_name) {
8530 $basedir = $file_name;
8531 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8532 $basedir .= '/';
8534 git_print_page_path($file_name, 'tree', $hash_base);
8536 print "<div class=\"title_text\">\n" .
8537 "<table class=\"object_header\">\n";
8538 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8539 print "</table>".
8540 "</div>\n";
8541 print "<div class=\"page_body\">\n";
8542 print "<table class=\"tree\">\n";
8543 my $alternate = 1;
8544 # '..' (top directory) link if possible
8545 if (defined $hash_base &&
8546 defined $file_name && $file_name =~ m![^/]+$!) {
8547 if ($alternate) {
8548 print "<tr class=\"dark\">\n";
8549 } else {
8550 print "<tr class=\"light\">\n";
8552 $alternate ^= 1;
8554 my $up = $file_name;
8555 $up =~ s!/?[^/]+$!!;
8556 undef $up unless $up;
8557 # based on git_print_tree_entry
8558 print '<td class="mode">' . mode_str('040000') . "</td>\n";
8559 print '<td class="size">&#160;</td>'."\n" if $show_sizes;
8560 print '<td class="list">';
8561 print $cgi->a({-href => href(action=>"tree",
8562 hash_base=>$hash_base,
8563 file_name=>$up)},
8564 "..");
8565 print "</td>\n";
8566 print "<td class=\"link\"></td>\n";
8568 print "</tr>\n";
8570 foreach my $line (@entries) {
8571 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
8573 if ($alternate) {
8574 print "<tr class=\"dark\">\n";
8575 } else {
8576 print "<tr class=\"light\">\n";
8578 $alternate ^= 1;
8580 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
8582 print "</tr>\n";
8584 print "</table>\n" .
8585 "</div>";
8586 git_footer_html();
8589 sub sanitize_for_filename {
8590 my $name = shift;
8592 $name =~ s!/!-!g;
8593 $name =~ s/[^[:alnum:]_.-]//g;
8595 return $name;
8598 sub snapshot_name {
8599 my ($project, $hash) = @_;
8601 # path/to/project.git -> project
8602 # path/to/project/.git -> project
8603 my $name = to_utf8($project);
8604 $name =~ s,([^/])/*\.git$,$1,;
8605 $name = sanitize_for_filename(basename($name));
8607 my $ver = $hash;
8608 if ($hash =~ /^[0-9a-fA-F]+$/) {
8609 # shorten SHA-1 hash
8610 my $full_hash = git_get_full_hash($project, $hash);
8611 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8612 $ver = git_get_short_hash($project, $hash);
8614 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8615 # tags don't need shortened SHA-1 hash
8616 $ver = $1;
8617 } else {
8618 # branches and other need shortened SHA-1 hash
8619 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
8620 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8621 my $ref_dir = (defined $1) ? $1 : '';
8622 $ver = $2;
8624 $ref_dir = sanitize_for_filename($ref_dir);
8625 # for refs neither in heads nor remotes we want to
8626 # add a ref dir to archive name
8627 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8628 $ver = $ref_dir . '-' . $ver;
8631 $ver .= '-' . git_get_short_hash($project, $hash);
8633 # special case of sanitization for filename - we change
8634 # slashes to dots instead of dashes
8635 # in case of hierarchical branch names
8636 $ver =~ s!/!.!g;
8637 $ver =~ s/[^[:alnum:]_.-]//g;
8639 # name = project-version_string
8640 $name = "$name-$ver";
8642 return wantarray ? ($name, $name) : $name;
8645 sub exit_if_unmodified_since {
8646 my ($latest_epoch) = @_;
8647 our $cgi;
8649 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8650 if (defined $if_modified) {
8651 my $since;
8652 if (eval { require HTTP::Date; 1; }) {
8653 $since = HTTP::Date::str2time($if_modified);
8654 } elsif (eval { require Time::ParseDate; 1; }) {
8655 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
8657 if (defined $since && $latest_epoch <= $since) {
8658 my %latest_date = parse_date($latest_epoch);
8659 print $cgi->header(
8660 -last_modified => $latest_date{'rfc2822'},
8661 -status => '304 Not Modified');
8662 CORE::die;
8667 sub git_snapshot {
8668 my $format = $input_params{'snapshot_format'};
8669 if (!@snapshot_fmts) {
8670 die_error(403, "Snapshots not allowed");
8672 # default to first supported snapshot format
8673 $format ||= $snapshot_fmts[0];
8674 if ($format !~ m/^[a-z0-9]+$/) {
8675 die_error(400, "Invalid snapshot format parameter");
8676 } elsif (!exists($known_snapshot_formats{$format})) {
8677 die_error(400, "Unknown snapshot format");
8678 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8679 die_error(403, "Snapshot format not allowed");
8680 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8681 die_error(403, "Unsupported snapshot format");
8684 my $type = git_get_type("$hash^{}");
8685 if (!$type) {
8686 die_error(404, 'Object does not exist');
8687 } elsif ($type eq 'blob') {
8688 die_error(400, 'Object is not a tree-ish');
8691 my ($name, $prefix) = snapshot_name($project, $hash);
8692 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8694 my %co = parse_commit($hash);
8695 exit_if_unmodified_since($co{'committer_epoch'}) if %co;
8697 my @cmd = (
8698 git_cmd(), 'archive',
8699 "--format=$known_snapshot_formats{$format}{'format'}",
8700 "--prefix=$prefix/", $hash);
8701 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8702 @cmd = ($posix_shell_bin, '-c', quote_command(@cmd) .
8703 ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}));
8706 $filename =~ s/(["\\])/\\$1/g;
8707 my %latest_date;
8708 if (%co) {
8709 %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
8712 print $cgi->header(
8713 -type => $known_snapshot_formats{$format}{'type'},
8714 -content_disposition => 'inline; filename="' . $filename . '"',
8715 %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
8716 -status => '200 OK');
8718 defined(my $fd = cmd_pipe @cmd)
8719 or die_error(500, "Execute git-archive failed");
8720 binmode($fd);
8721 binmode STDOUT, ':raw';
8722 $fcgi_raw_mode = 1;
8723 my $buf;
8724 while (read($fd, $buf, 32768)) {
8725 print $buf;
8727 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
8728 $fcgi_raw_mode = 0;
8729 close $fd;
8732 sub git_log_generic {
8733 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8735 my $head = git_get_head_hash($project);
8736 if (!defined $base) {
8737 $base = $head;
8739 if (!defined $page) {
8740 $page = 0;
8742 my $refs = git_get_references();
8744 my $commit_hash = $base;
8745 if (defined $parent) {
8746 $commit_hash = "$parent..$base";
8748 my @commitlist =
8749 parse_commits($commit_hash, 101, (100 * $page),
8750 defined $file_name ? ($file_name, "--full-history") : ());
8752 my $ftype;
8753 if (!defined $file_hash && defined $file_name) {
8754 # some commits could have deleted file in question,
8755 # and not have it in tree, but one of them has to have it
8756 for (my $i = 0; $i < @commitlist; $i++) {
8757 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
8758 last if defined $file_hash;
8761 if (defined $file_hash) {
8762 $ftype = git_get_type($file_hash);
8764 if (defined $file_name && !defined $ftype) {
8765 die_error(500, "Unknown type of object");
8767 my %co;
8768 if (defined $file_name) {
8769 %co = parse_commit($base)
8770 or die_error(404, "Unknown commit object");
8774 my $next_link = '';
8775 if ($#commitlist >= 100) {
8776 $next_link =
8777 $cgi->a({-href => href(-replay=>1, page=>$page+1),
8778 -accesskey => "n", -title => "Alt-n"}, "next");
8780 my $extra = '';
8781 my ($patch_max) = gitweb_get_feature('patches');
8782 if ($patch_max && !defined $file_name) {
8783 if ($patch_max < 0 || @commitlist <= $patch_max) {
8784 $extra = $cgi->a({-href => href(action=>"patches", -replay=>1)},
8785 "patches");
8788 my $paging_nav = format_log_nav($fmt_name, $page, $#commitlist >= 100, $extra);
8791 local $action = 'fulllog';
8792 git_header_html();
8794 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8795 if (defined $file_name) {
8796 git_print_header_div('commit', esc_html($co{'title'}), $base);
8797 } else {
8798 git_print_header_div('summary', $project)
8800 git_print_page_path($file_name, $ftype, $hash_base)
8801 if (defined $file_name);
8803 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
8804 $file_name, $file_hash, $ftype);
8806 git_footer_html();
8809 sub git_log {
8810 git_log_generic('log', \&git_log_body,
8811 $hash, $hash_parent);
8814 sub git_commit {
8815 $hash ||= $hash_base || "HEAD";
8816 my %co = parse_commit($hash)
8817 or die_error(404, "Unknown commit object");
8819 my $parent = $co{'parent'};
8820 my $parents = $co{'parents'}; # listref
8822 # we need to prepare $formats_nav before any parameter munging
8823 my $formats_nav;
8824 if (!defined $parent) {
8825 # --root commitdiff
8826 $formats_nav .= '<span class="parents none">(initial)</span>';
8827 } elsif (@$parents == 1) {
8828 # single parent commit
8829 $formats_nav .=
8830 '<span class="parents single">(parent:&#160;' .
8831 $cgi->a({-href => href(action=>"commit",
8832 hash=>$parent)},
8833 esc_html(substr($parent, 0, 7))) .
8834 ')</span>';
8835 } else {
8836 # merge commit
8837 $formats_nav .=
8838 '<span class="parents multiple">(merge:&#160;' .
8839 join(' ', map {
8840 $cgi->a({-href => href(action=>"commit",
8841 hash=>$_)},
8842 esc_html(substr($_, 0, 7)));
8843 } @$parents ) .
8844 ')</span>';
8846 if (gitweb_check_feature('patches') && @$parents <= 1) {
8847 $formats_nav .= $barsep . tabspan(
8848 $cgi->a({-href => href(action=>"patch", -replay=>1)},
8849 "patch"));
8852 if (!defined $parent) {
8853 $parent = "--root";
8855 my @difftree;
8856 defined(my $fd = git_cmd_pipe "diff-tree", '-r', "--no-commit-id",
8857 @diff_opts,
8858 (@$parents <= 1 ? $parent : '-c'),
8859 $hash, "--")
8860 or die_error(500, "Open git-diff-tree failed");
8861 @difftree = map { chomp; to_utf8($_) } <$fd>;
8862 close $fd or die_error(404, "Reading git-diff-tree failed");
8864 # non-textual hash id's can be cached
8865 my $expires;
8866 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8867 $expires = "+1d";
8869 my $refs = git_get_references();
8870 my $ref = format_ref_marker($refs, $co{'id'});
8872 git_header_html(undef, $expires);
8873 git_print_page_nav('commit', '',
8874 $hash, $co{'tree'}, $hash,
8875 $formats_nav);
8877 if (defined $co{'parent'}) {
8878 git_print_header_div('commitdiff', esc_html($co{'title'}), $hash, undef, $ref);
8879 } else {
8880 git_print_header_div('tree', esc_html($co{'title'}), $co{'tree'}, $hash, $ref);
8882 print "<div class=\"title_text\">\n" .
8883 "<table class=\"object_header\">\n";
8884 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8885 git_print_authorship_rows(\%co);
8886 print "<tr>" .
8887 "<td>tree</td>" .
8888 "<td class=\"sha1\">" .
8889 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
8890 class => "list"}, $co{'tree'}) .
8891 "</td>" .
8892 "<td class=\"link\">" .
8893 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
8894 "tree");
8895 my $snapshot_links = format_snapshot_links($hash);
8896 if (defined $snapshot_links) {
8897 print $barsep . $snapshot_links;
8899 print "</td>" .
8900 "</tr>\n";
8902 foreach my $par (@$parents) {
8903 print "<tr>" .
8904 "<td>parent</td>" .
8905 "<td class=\"sha1\">" .
8906 $cgi->a({-href => href(action=>"commit", hash=>$par),
8907 class => "list"}, $par) .
8908 "</td>" .
8909 "<td class=\"link\">" .
8910 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
8911 $barsep .
8912 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
8913 "</td>" .
8914 "</tr>\n";
8916 print "</table>".
8917 "</div>\n";
8919 print "<div class=\"page_body\">\n";
8920 git_print_log($co{'comment'});
8921 print "</div>\n";
8923 git_difftree_body(\@difftree, $hash, @$parents);
8925 git_footer_html();
8928 sub git_object {
8929 # object is defined by:
8930 # - hash or hash_base alone
8931 # - hash_base and file_name
8932 my $type;
8934 # - hash or hash_base alone
8935 if ($hash || ($hash_base && !defined $file_name)) {
8936 my $object_id = $hash || $hash_base;
8938 defined(my $fd = git_cmd_pipe 'cat-file', '-t', $object_id)
8939 or die_error(404, "Object does not exist");
8940 $type = <$fd>;
8941 chomp $type;
8942 close $fd
8943 or die_error(404, "Object does not exist");
8945 # - hash_base and file_name
8946 } elsif ($hash_base && defined $file_name) {
8947 $file_name =~ s,/+$,,;
8949 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
8950 or die_error(404, "Base object does not exist");
8952 # here errors should not happen
8953 defined(my $fd = git_cmd_pipe "ls-tree", $hash_base, "--", $file_name)
8954 or die_error(500, "Open git-ls-tree failed");
8955 my $line = to_utf8(scalar <$fd>);
8956 close $fd;
8958 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8959 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
8960 die_error(404, "File or directory for given base does not exist");
8962 $type = $2;
8963 $hash = $3;
8964 } else {
8965 die_error(400, "Not enough information to find object");
8968 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
8969 hash=>$hash, hash_base=>$hash_base,
8970 file_name=>$file_name),
8971 -status => '302 Found');
8974 sub git_blobdiff {
8975 my $format = shift || 'html';
8976 my $diff_style = $input_params{'diff_style'} || 'inline';
8978 my $fd;
8979 my @difftree;
8980 my %diffinfo;
8981 my $expires;
8983 # preparing $fd and %diffinfo for git_patchset_body
8984 # new style URI
8985 if (defined $hash_base && defined $hash_parent_base) {
8986 if (defined $file_name) {
8987 # read raw output
8988 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
8989 $hash_parent_base, $hash_base,
8990 "--", (defined $file_parent ? $file_parent : ()), $file_name)
8991 or die_error(500, "Open git-diff-tree failed");
8992 @difftree = map { chomp; to_utf8($_) } <$fd>;
8993 close $fd
8994 or die_error(404, "Reading git-diff-tree failed");
8995 @difftree
8996 or die_error(404, "Blob diff not found");
8998 } elsif (defined $hash &&
8999 $hash =~ /[0-9a-fA-F]{40}/) {
9000 # try to find filename from $hash
9002 # read filtered raw output
9003 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9004 $hash_parent_base, $hash_base, "--")
9005 or die_error(500, "Open git-diff-tree failed");
9006 @difftree =
9007 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9008 # $hash == to_id
9009 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9010 map { chomp; to_utf8($_) } <$fd>;
9011 close $fd
9012 or die_error(404, "Reading git-diff-tree failed");
9013 @difftree
9014 or die_error(404, "Blob diff not found");
9016 } else {
9017 die_error(400, "Missing one of the blob diff parameters");
9020 if (@difftree > 1) {
9021 die_error(400, "Ambiguous blob diff specification");
9024 %diffinfo = parse_difftree_raw_line($difftree[0]);
9025 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9026 $file_name ||= $diffinfo{'to_file'};
9028 $hash_parent ||= $diffinfo{'from_id'};
9029 $hash ||= $diffinfo{'to_id'};
9031 # non-textual hash id's can be cached
9032 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9033 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9034 $expires = '+1d';
9037 # open patch output
9038 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9039 '-p', ($format eq 'html' ? "--full-index" : ()),
9040 $hash_parent_base, $hash_base,
9041 "--", (defined $file_parent ? $file_parent : ()), $file_name)
9042 or die_error(500, "Open git-diff-tree failed");
9045 # old/legacy style URI -- not generated anymore since 1.4.3.
9046 if (!%diffinfo) {
9047 die_error('404 Not Found', "Missing one of the blob diff parameters")
9050 # header
9051 if ($format eq 'html') {
9052 my $formats_nav =
9053 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
9054 "raw");
9055 $formats_nav .= diff_style_nav($diff_style);
9056 git_header_html(undef, $expires);
9057 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
9058 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9059 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
9060 } else {
9061 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9062 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
9064 if (defined $file_name) {
9065 git_print_page_path($file_name, "blob", $hash_base);
9066 } else {
9067 print "<div class=\"page_path\"></div>\n";
9070 } elsif ($format eq 'plain') {
9071 print $cgi->header(
9072 -type => 'text/plain',
9073 -charset => 'utf-8',
9074 -expires => $expires,
9075 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
9077 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9079 } else {
9080 die_error(400, "Unknown blobdiff format");
9083 # patch
9084 if ($format eq 'html') {
9085 print "<div class=\"page_body\">\n";
9087 git_patchset_body($fd, $diff_style,
9088 [ \%diffinfo ], $hash_base, $hash_parent_base);
9089 close $fd;
9091 print "</div>\n"; # class="page_body"
9092 git_footer_html();
9094 } else {
9095 while (my $line = to_utf8(scalar <$fd>)) {
9096 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9097 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9099 print $line;
9101 last if $line =~ m!^\+\+\+!;
9103 while (<$fd>) {
9104 print to_utf8($_);
9106 close $fd;
9110 sub git_blobdiff_plain {
9111 git_blobdiff('plain');
9114 # assumes that it is added as later part of already existing navigation,
9115 # so it returns "| foo | bar" rather than just "foo | bar"
9116 sub diff_style_nav {
9117 my ($diff_style, $is_combined) = @_;
9118 $diff_style ||= 'inline';
9120 return "" if ($is_combined);
9122 my @styles = (inline => 'inline', 'sidebyside' => 'side&#160;by&#160;side');
9123 my %styles = @styles;
9124 @styles =
9125 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9127 return $barsep . '<span class="diffstyles">' . join($barsep,
9128 map {
9129 $_ eq $diff_style ?
9130 '<span class="diffstyle selected">' . $styles{$_} . '</span>' :
9131 '<span class="diffstyle">' .
9132 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_}) .
9133 '</span>'
9134 } @styles) . '</span>';
9137 sub git_commitdiff {
9138 my %params = @_;
9139 my $format = $params{-format} || 'html';
9140 my $diff_style = $input_params{'diff_style'} || 'inline';
9142 my ($patch_max) = gitweb_get_feature('patches');
9143 if ($format eq 'patch') {
9144 die_error(403, "Patch view not allowed") unless $patch_max;
9147 $hash ||= $hash_base || "HEAD";
9148 my %co = parse_commit($hash)
9149 or die_error(404, "Unknown commit object");
9151 # choose format for commitdiff for merge
9152 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
9153 $hash_parent = '--cc';
9155 # we need to prepare $formats_nav before almost any parameter munging
9156 my $formats_nav;
9157 if ($format eq 'html') {
9158 $formats_nav = tabspan(
9159 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
9160 "raw"));
9161 if ($patch_max && @{$co{'parents'}} <= 1) {
9162 $formats_nav .= $barsep . tabspan(
9163 $cgi->a({-href => href(action=>"patch", -replay=>1)},
9164 "patch"));
9166 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
9168 if (defined $hash_parent &&
9169 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9170 # commitdiff with two commits given
9171 my $hash_parent_short = $hash_parent;
9172 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9173 $hash_parent_short = substr($hash_parent, 0, 7);
9175 $formats_nav .= $spcsep . '<span class="parents multiple">' .
9176 '(from';
9177 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
9178 if ($co{'parents'}[$i] eq $hash_parent) {
9179 $formats_nav .= '&#160;parent&#160;' . ($i+1);
9180 last;
9183 $formats_nav .= ':&#160;' .
9184 $cgi->a({-href => href(-replay=>1,
9185 hash=>$hash_parent, hash_base=>undef)},
9186 esc_html($hash_parent_short)) .
9187 ')</span>';
9188 } elsif (!$co{'parent'}) {
9189 # --root commitdiff
9190 $formats_nav .= $spcsep . '<span class="parents none">(initial)</span>';
9191 } elsif (scalar @{$co{'parents'}} == 1) {
9192 # single parent commit
9193 $formats_nav .= $spcsep .
9194 '<span class="parents single">(parent:&#160;' .
9195 $cgi->a({-href => href(-replay=>1,
9196 hash=>$co{'parent'}, hash_base=>undef)},
9197 esc_html(substr($co{'parent'}, 0, 7))) .
9198 ')</span>';
9199 } else {
9200 # merge commit
9201 if ($hash_parent eq '--cc') {
9202 $formats_nav .= $barsep . tabspan(
9203 $cgi->a({-href => href(-replay=>1,
9204 hash=>$hash, hash_parent=>'-c')},
9205 'combined'));
9206 } else { # $hash_parent eq '-c'
9207 $formats_nav .= $barsep . tabspan(
9208 $cgi->a({-href => href(-replay=>1,
9209 hash=>$hash, hash_parent=>'--cc')},
9210 'compact'));
9212 $formats_nav .= $spcsep .
9213 '<span class="parents multiple">(merge:&#160;' .
9214 join(' ', map {
9215 $cgi->a({-href => href(-replay=>1,
9216 hash=>$_, hash_base=>undef)},
9217 esc_html(substr($_, 0, 7)));
9218 } @{$co{'parents'}} ) .
9219 ')</span>';
9223 my $hash_parent_param = $hash_parent;
9224 if (!defined $hash_parent_param) {
9225 # --cc for multiple parents, --root for parentless
9226 $hash_parent_param =
9227 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
9230 # read commitdiff
9231 my $fd;
9232 my @difftree;
9233 if ($format eq 'html') {
9234 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9235 "--no-commit-id", "--patch-with-raw", "--full-index",
9236 $hash_parent_param, $hash, "--")
9237 or die_error(500, "Open git-diff-tree failed");
9239 while (my $line = to_utf8(scalar <$fd>)) {
9240 chomp $line;
9241 # empty line ends raw part of diff-tree output
9242 last unless $line;
9243 push @difftree, scalar parse_difftree_raw_line($line);
9246 } elsif ($format eq 'plain') {
9247 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9248 '-p', $hash_parent_param, $hash, "--")
9249 or die_error(500, "Open git-diff-tree failed");
9250 } elsif ($format eq 'patch') {
9251 # For commit ranges, we limit the output to the number of
9252 # patches specified in the 'patches' feature.
9253 # For single commits, we limit the output to a single patch,
9254 # diverging from the git-format-patch default.
9255 my @commit_spec = ();
9256 if ($hash_parent) {
9257 if ($patch_max > 0) {
9258 push @commit_spec, "-$patch_max";
9260 push @commit_spec, '-n', "$hash_parent..$hash";
9261 } else {
9262 if ($params{-single}) {
9263 push @commit_spec, '-1';
9264 } else {
9265 if ($patch_max > 0) {
9266 push @commit_spec, "-$patch_max";
9268 push @commit_spec, "-n";
9270 push @commit_spec, '--root', $hash;
9272 defined($fd = git_cmd_pipe "format-patch", @diff_opts,
9273 '--encoding=utf8', '--stdout', @commit_spec)
9274 or die_error(500, "Open git-format-patch failed");
9275 } else {
9276 die_error(400, "Unknown commitdiff format");
9279 # non-textual hash id's can be cached
9280 my $expires;
9281 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9282 $expires = "+1d";
9285 # write commit message
9286 if ($format eq 'html') {
9287 my $refs = git_get_references();
9288 my $ref = format_ref_marker($refs, $co{'id'});
9290 git_header_html(undef, $expires);
9291 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9292 git_print_header_div('commit', esc_html($co{'title'}), $hash, undef, $ref);
9293 print "<div class=\"title_text\">\n" .
9294 "<table class=\"object_header\">\n";
9295 git_print_authorship_rows(\%co);
9296 print "</table>".
9297 "</div>\n";
9298 print "<div class=\"page_body\">\n";
9299 if (@{$co{'comment'}} > 1) {
9300 print "<div class=\"log\">\n";
9301 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
9302 print "</div>\n"; # class="log"
9305 } elsif ($format eq 'plain') {
9306 my $refs = git_get_references("tags");
9307 my $tagname = git_get_rev_name_tags($hash);
9308 my $filename = basename($project) . "-$hash.patch";
9310 print $cgi->header(
9311 -type => 'text/plain',
9312 -charset => 'utf-8',
9313 -expires => $expires,
9314 -content_disposition => 'inline; filename="' . "$filename" . '"');
9315 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
9316 print "From: " . to_utf8($co{'author'}) . "\n";
9317 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9318 print "Subject: " . to_utf8($co{'title'}) . "\n";
9320 print "X-Git-Tag: $tagname\n" if $tagname;
9321 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9323 foreach my $line (@{$co{'comment'}}) {
9324 print to_utf8($line) . "\n";
9326 print "---\n\n";
9327 } elsif ($format eq 'patch') {
9328 my $filename = basename($project) . "-$hash.patch";
9330 print $cgi->header(
9331 -type => 'text/plain',
9332 -charset => 'utf-8',
9333 -expires => $expires,
9334 -content_disposition => 'inline; filename="' . "$filename" . '"');
9337 # write patch
9338 if ($format eq 'html') {
9339 my $use_parents = !defined $hash_parent ||
9340 $hash_parent eq '-c' || $hash_parent eq '--cc';
9341 git_difftree_body(\@difftree, $hash,
9342 $use_parents ? @{$co{'parents'}} : $hash_parent);
9343 print "<br/>\n";
9345 git_patchset_body($fd, $diff_style,
9346 \@difftree, $hash,
9347 $use_parents ? @{$co{'parents'}} : $hash_parent);
9348 close $fd;
9349 print "</div>\n"; # class="page_body"
9350 git_footer_html();
9352 } elsif ($format eq 'plain') {
9353 while (<$fd>) {
9354 print to_utf8($_);
9356 close $fd
9357 or print "Reading git-diff-tree failed\n";
9358 } elsif ($format eq 'patch') {
9359 while (<$fd>) {
9360 print to_utf8($_);
9362 close $fd
9363 or print "Reading git-format-patch failed\n";
9367 sub git_commitdiff_plain {
9368 git_commitdiff(-format => 'plain');
9371 # format-patch-style patches
9372 sub git_patch {
9373 git_commitdiff(-format => 'patch', -single => 1);
9376 sub git_patches {
9377 git_commitdiff(-format => 'patch');
9380 sub git_history {
9381 git_log_generic('history', \&git_history_body,
9382 $hash_base, $hash_parent_base,
9383 $file_name, $hash);
9386 sub git_search {
9387 $searchtype ||= 'commit';
9389 # check if appropriate features are enabled
9390 gitweb_check_feature('search')
9391 or die_error(403, "Search is disabled");
9392 if ($searchtype eq 'pickaxe') {
9393 # pickaxe may take all resources of your box and run for several minutes
9394 # with every query - so decide by yourself how public you make this feature
9395 gitweb_check_feature('pickaxe')
9396 or die_error(403, "Pickaxe search is disabled");
9398 if ($searchtype eq 'grep') {
9399 # grep search might be potentially CPU-intensive, too
9400 gitweb_check_feature('grep')
9401 or die_error(403, "Grep search is disabled");
9404 if (!defined $searchtext) {
9405 die_error(400, "Text field is empty");
9407 if (!defined $hash) {
9408 $hash = git_get_head_hash($project);
9410 my %co = parse_commit($hash);
9411 if (!%co) {
9412 die_error(404, "Unknown commit object");
9414 if (!defined $page) {
9415 $page = 0;
9418 if ($searchtype eq 'commit' ||
9419 $searchtype eq 'author' ||
9420 $searchtype eq 'committer') {
9421 git_search_message(%co);
9422 } elsif ($searchtype eq 'pickaxe') {
9423 git_search_changes(%co);
9424 } elsif ($searchtype eq 'grep') {
9425 git_search_files(%co);
9426 } else {
9427 die_error(400, "Unknown search type");
9431 sub git_search_help {
9432 git_header_html();
9433 git_print_page_nav('','', $hash,$hash,$hash);
9434 print <<EOT;
9435 <div class="search_help">
9436 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9437 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9438 the pattern entered is recognized as the POSIX extended
9439 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9440 insensitive).</p>
9441 <dl>
9442 <dt><b>commit</b></dt>
9443 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9445 my $have_grep = gitweb_check_feature('grep');
9446 if ($have_grep) {
9447 print <<EOT;
9448 <dt><b>grep</b></dt>
9449 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9450 a different one) are searched for the given pattern. On large trees, this search can take
9451 a while and put some strain on the server, so please use it with some consideration. Note that
9452 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9453 case-sensitive.</dd>
9456 print <<EOT;
9457 <dt><b>author</b></dt>
9458 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9459 <dt><b>committer</b></dt>
9460 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9462 my $have_pickaxe = gitweb_check_feature('pickaxe');
9463 if ($have_pickaxe) {
9464 print <<EOT;
9465 <dt><b>pickaxe</b></dt>
9466 <dd>All commits that caused the string to appear or disappear from any file (changes that
9467 added, removed or "modified" the string) will be listed. This search can take a while and
9468 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9469 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9472 print "</dl>\n</div>\n";
9473 git_footer_html();
9476 sub git_shortlog {
9477 git_log_generic('shortlog', \&git_shortlog_body,
9478 $hash, $hash_parent);
9481 ## ......................................................................
9482 ## feeds (RSS, Atom; OPML)
9484 sub git_feed {
9485 my $format = shift || 'atom';
9486 my $have_blame = gitweb_check_feature('blame');
9488 # Atom: http://www.atomenabled.org/developers/syndication/
9489 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9490 if ($format ne 'rss' && $format ne 'atom') {
9491 die_error(400, "Unknown web feed format");
9494 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9495 my $head = $hash || 'HEAD';
9496 my @commitlist = parse_commits($head, 150, 0, $file_name);
9498 my %latest_commit;
9499 my %latest_date;
9500 my $content_type = "application/$format+xml";
9501 if (defined $cgi->http('HTTP_ACCEPT') &&
9502 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9503 # browser (feed reader) prefers text/xml
9504 $content_type = 'text/xml';
9506 if (defined($commitlist[0])) {
9507 %latest_commit = %{$commitlist[0]};
9508 my $latest_epoch = $latest_commit{'committer_epoch'};
9509 exit_if_unmodified_since($latest_epoch);
9510 %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
9512 print $cgi->header(
9513 -type => $content_type,
9514 -charset => 'utf-8',
9515 %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
9516 -status => '200 OK');
9518 # Optimization: skip generating the body if client asks only
9519 # for Last-Modified date.
9520 return if ($cgi->request_method() eq 'HEAD');
9522 # header variables
9523 my $title = "$site_name - $project/$action";
9524 my $feed_type = 'log';
9525 if (defined $hash) {
9526 $title .= " - '$hash'";
9527 $feed_type = 'branch log';
9528 if (defined $file_name) {
9529 $title .= " :: $file_name";
9530 $feed_type = 'history';
9532 } elsif (defined $file_name) {
9533 $title .= " - $file_name";
9534 $feed_type = 'history';
9536 $title .= " $feed_type";
9537 $title = esc_html($title);
9538 my $descr = git_get_project_description($project);
9539 if (defined $descr) {
9540 $descr = esc_html($descr);
9541 } else {
9542 $descr = "$project " .
9543 ($format eq 'rss' ? 'RSS' : 'Atom') .
9544 " feed";
9546 my $owner = git_get_project_owner($project);
9547 $owner = esc_html($owner);
9549 #header
9550 my $alt_url;
9551 if (defined $file_name) {
9552 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
9553 } elsif (defined $hash) {
9554 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
9555 } else {
9556 $alt_url = href(-full=>1, action=>"summary");
9558 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
9559 if ($format eq 'rss') {
9560 print <<XML;
9561 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9562 <channel>
9564 print "<title>$title</title>\n" .
9565 "<link>$alt_url</link>\n" .
9566 "<description>$descr</description>\n" .
9567 "<language>en</language>\n" .
9568 # project owner is responsible for 'editorial' content
9569 "<managingEditor>$owner</managingEditor>\n";
9570 if (defined $logo || defined $favicon) {
9571 # prefer the logo to the favicon, since RSS
9572 # doesn't allow both
9573 my $img = esc_url($logo || $favicon);
9574 print "<image>\n" .
9575 "<url>$img</url>\n" .
9576 "<title>$title</title>\n" .
9577 "<link>$alt_url</link>\n" .
9578 "</image>\n";
9580 if (%latest_date) {
9581 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9582 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9584 print "<generator>gitweb v.$version/$git_version</generator>\n";
9585 } elsif ($format eq 'atom') {
9586 print <<XML;
9587 <feed xmlns="http://www.w3.org/2005/Atom">
9589 print "<title>$title</title>\n" .
9590 "<subtitle>$descr</subtitle>\n" .
9591 '<link rel="alternate" type="text/html" href="' .
9592 $alt_url . '" />' . "\n" .
9593 '<link rel="self" type="' . $content_type . '" href="' .
9594 $cgi->self_url() . '" />' . "\n" .
9595 "<id>" . href(-full=>1) . "</id>\n" .
9596 # use project owner for feed author
9597 '<author><name>'. email_obfuscate($owner) . '</name></author>\n';
9598 if (defined $favicon) {
9599 print "<icon>" . esc_url($favicon) . "</icon>\n";
9601 if (defined $logo) {
9602 # not twice as wide as tall: 72 x 27 pixels
9603 print "<logo>" . esc_url($logo) . "</logo>\n";
9605 if (! %latest_date) {
9606 # dummy date to keep the feed valid until commits trickle in:
9607 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9608 } else {
9609 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9611 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9614 # contents
9615 for (my $i = 0; $i <= $#commitlist; $i++) {
9616 my %co = %{$commitlist[$i]};
9617 my $commit = $co{'id'};
9618 # we read 150, we always show 30 and the ones more recent than 48 hours
9619 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9620 last;
9622 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
9624 # get list of changed files
9625 defined(my $fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9626 $co{'parent'} || "--root",
9627 $co{'id'}, "--", (defined $file_name ? $file_name : ()))
9628 or next;
9629 my @difftree = map { chomp; to_utf8($_) } <$fd>;
9630 close $fd
9631 or next;
9633 # print element (entry, item)
9634 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
9635 if ($format eq 'rss') {
9636 print "<item>\n" .
9637 "<title>" . esc_html($co{'title'}) . "</title>\n" .
9638 "<author>" . esc_html($co{'author'}) . "</author>\n" .
9639 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9640 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9641 "<link>$co_url</link>\n" .
9642 "<description>" . esc_html($co{'title'}) . "</description>\n" .
9643 "<content:encoded>" .
9644 "<![CDATA[\n";
9645 } elsif ($format eq 'atom') {
9646 print "<entry>\n" .
9647 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
9648 "<updated>$cd{'iso-8601'}</updated>\n" .
9649 "<author>\n" .
9650 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
9651 if ($co{'author_email'}) {
9652 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
9654 print "</author>\n" .
9655 # use committer for contributor
9656 "<contributor>\n" .
9657 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
9658 if ($co{'committer_email'}) {
9659 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
9661 print "</contributor>\n" .
9662 "<published>$cd{'iso-8601'}</published>\n" .
9663 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9664 "<id>$co_url</id>\n" .
9665 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
9666 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9668 my $comment = $co{'comment'};
9669 print "<pre>\n";
9670 foreach my $line (@$comment) {
9671 $line = esc_html($line);
9672 print "$line\n";
9674 print "</pre><ul>\n";
9675 foreach my $difftree_line (@difftree) {
9676 my %difftree = parse_difftree_raw_line($difftree_line);
9677 next if !$difftree{'from_id'};
9679 my $file = $difftree{'file'} || $difftree{'to_file'};
9681 print "<li>" .
9682 "[" .
9683 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
9684 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
9685 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
9686 file_name=>$file, file_parent=>$difftree{'from_file'}),
9687 -title => "diff"}, 'D');
9688 if ($have_blame) {
9689 print $cgi->a({-href => href(-full=>1, action=>"blame",
9690 file_name=>$file, hash_base=>$commit),
9691 -class => "blamelink",
9692 -title => "blame"}, 'B');
9694 # if this is not a feed of a file history
9695 if (!defined $file_name || $file_name ne $file) {
9696 print $cgi->a({-href => href(-full=>1, action=>"history",
9697 file_name=>$file, hash=>$commit),
9698 -title => "history"}, 'H');
9700 $file = esc_path($file);
9701 print "] ".
9702 "$file</li>\n";
9704 if ($format eq 'rss') {
9705 print "</ul>]]>\n" .
9706 "</content:encoded>\n" .
9707 "</item>\n";
9708 } elsif ($format eq 'atom') {
9709 print "</ul>\n</div>\n" .
9710 "</content>\n" .
9711 "</entry>\n";
9715 # end of feed
9716 if ($format eq 'rss') {
9717 print "</channel>\n</rss>\n";
9718 } elsif ($format eq 'atom') {
9719 print "</feed>\n";
9723 sub git_rss {
9724 git_feed('rss');
9727 sub git_atom {
9728 git_feed('atom');
9731 sub git_opml {
9732 my @list = git_get_projects_list($project_filter, $strict_export);
9733 if (!@list) {
9734 die_error(404, "No projects found");
9737 print $cgi->header(
9738 -type => 'text/xml',
9739 -charset => 'utf-8',
9740 -content_disposition => 'inline; filename="opml.xml"');
9742 my $title = esc_html($site_name);
9743 my $filter = " within subdirectory ";
9744 if (defined $project_filter) {
9745 $filter .= esc_html($project_filter);
9746 } else {
9747 $filter = "";
9749 print <<XML;
9750 <?xml version="1.0" encoding="utf-8"?>
9751 <opml version="1.0">
9752 <head>
9753 <title>$title OPML Export$filter</title>
9754 </head>
9755 <body>
9756 <outline text="git RSS feeds">
9759 foreach my $pr (@list) {
9760 my %proj = %$pr;
9761 my $head = git_get_head_hash($proj{'path'});
9762 if (!defined $head) {
9763 next;
9765 $git_dir = "$projectroot/$proj{'path'}";
9766 my %co = parse_commit($head);
9767 if (!%co) {
9768 next;
9771 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
9772 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
9773 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
9774 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
9776 print <<XML;
9777 </outline>
9778 </body>
9779 </opml>